Whether you lose quality depends on one thing: does the operation touch the picture, and does the tool copy the other streams or re-encode them. A mark that lives in the container — a C2PA box, a title tag — comes off with zero bytes changed: the picture decodes to identical frames and the file is the same size. A mark painted across the frames has to be re-encoded, and that is where quality is spent — measured below, almost all of it in the fill rather than in the export setting. But the damage people actually report is neither of those: it is a tool that re-encodes the picture and silently drops the audio and subtitle tracks on the way out.
So "without losing quality" is not one question, it is five. The picture, the audio track, the subtitle track, the duration and frame rate, and the alignment between picture and sound are five separate things, and each one can be checked on your own file with a single command. Below is what each method does to each of them, measured, and how to run the checks yourself.
"Losing quality" is usually said as if it were one thing, but a video file carries five things that can each be damaged separately, and a watermark remover touches them differently:
Keeping all five is the real meaning of "without losing quality". The methods below are judged on all five, not just on whether the mark disappeared — that part is covered on the three layers a mark can live in and on the page about avoiding a blur box.
Every number below comes from one script on this machine that builds its own fixtures with ffmpeg and prints the table. The test file is 640x360, 30 fps, 4 seconds, 120 frames, H.264 at CRF 20, AAC audio, a mov_text subtitle track and four container tags. A clean copy and a marked copy are made, so the mark's own cost can be separated from the method's cost.
| Method | Picture | Audio stream | Subtitle | Duration / fps | Sync |
|---|---|---|---|---|---|
| Metadata strip | identical frames | identical bytes | kept | 4.000000 s / 30 fps | -2.7 ms |
| Crop the marked strip | re-encoded, 44.15 dB vs clean | identical bytes | kept | 4.000000 s / 30 fps | -2.7 ms |
| Inpaint the rectangle | re-encoded, 38.62 dB vs clean | identical bytes | kept | 4.000000 s / 30 fps | -2.7 ms |
| Re-encode, video only | re-encoded | gone | gone | 4.000000 s / 30 fps | no audio left |
Read the audio column first, because that is where the surprises are. In three of the four rows the audio stream is byte-identical: the same compressed bytes went in and came out, which is a stronger statement than "sounds fine". The fourth row has no audio at all. And the sync column never moves from -2.7 ms — including in the row that re-encoded the picture, because the picture and the sound were never re-timed relative to each other.
When the mark is a record in the container rather than pixels — a C2PA provenance box, a title or artist tag — removing it does not have to touch the media at all. The cleaner on this site rewrites each such box in place as a same-size padding box, so no byte in the file changes position and the media payload is never re-encoded. On the test file:
file size 508778 -> 508778 bytes (unchanged) mdat payload identical md5 video stream identical md5 audio stream identical md5 video frames identical framemd5 (every frame decodes to the same bytes) streams video, audio, subtitle (all still present) duration 4.000000 s 30/1 fps 120 frames tags removed title, artist, encoder, comment
That is the strongest form of "no quality loss" available: not "looks the same", but "decodes to the same bytes". The trade is stated on the free video watermark remover page and it is worth repeating here — because the file does not get smaller, the file size does not drop. That is the price of never moving a byte, and it is why a faststart file keeps playing after the edit.
Once the mark is in the pixels, no method can restore the original frames — they were overwritten when the mark was drawn on. What you can choose is which cost to pay, and the measurement shows the audio is not part of it. Cropping the marked strip off and inpainting the rectangle both re-encode the picture, and both leave the audio stream byte-identical, because ffmpeg copies it with -c:a copy while it works on the video:
crop audio stream md5 7a4f62b07fea6d77a00f485326b448fc (same as source) inpaint audio stream md5 7a4f62b07fea6d77a00f485326b448fc (same as source) one extra encode, no mark 48.80 dB (the encode bill, nothing else) marked vs clean 23.79 dB (what the mark itself costs) crop vs the same crop of the clean file 44.15 dB inpaint vs clean 38.62 dB
Read those together. A single extra encode of a clip that had no mark at all already costs 48.80 dB, so cropping at 44.15 dB is spending roughly the price of one encode and inventing nothing — every pixel you keep is a real pixel. Inpainting at 38.62 dB is spending the same encode plus the reconstruction, and it is still far better than leaving the mark (23.79 dB). Neither number is a quality loss in the audio, and neither changes the timeline. If your concern is the fill rather than the file, the logo page scores the same methods in more detail.
38.62 dB is the figure above for inpainting, and the first instinct is to win it back with a better export setting. The two halves of an inpainted frame answer that question differently: outside the mark's rectangle the only error is compression, and inside it the error is whatever the fill invented. PSNR is mean squared error, so the halves can be separated exactly rather than estimated — the box is measured on its own and the rest of the frame is derived from the whole-frame figure:
mse_outside = (mse_whole x pixels_total - mse_box x pixels_box) / pixels_outside
The same repaired file was then exported six times, CRF 12 through CRF 32, by dev/inpaint_ladder.py, which rebuilds the fixture above and prints the table. Both regions were scored against the clean file at every setting, and higher means closer to it. The whole frame is given twice — once as the YUV average the table above uses, so the CRF 20 row can be checked against its 38.62 dB, and once as luma alone, which is the metric the two region columns are built from:
CRF file size whole (YUV) whole (luma) inside box outside box 12 784,337 38.88 41.48 26.25 46.54 16 630,009 38.78 41.30 26.24 46.00 20 493,230 38.62 41.04 26.27 45.21 23 387,411 38.06 40.05 26.28 42.94 28 205,075 36.50 37.61 26.26 38.97 32 136,928 35.09 35.73 26.30 36.51
Read the last two columns down. CRF 12 to CRF 32 is a 5.7 times change in file size, and it moves the picture outside the box by 10.03 dB while moving the inside of the box by 0.05 dB. The fill has already decided that rectangle. No setting reopens it.
That column could be measuring encoder noise rather than the mark, so the same box was scored on the marked file with no repair at all. If it were noise, it would climb at a lower CRF. It does not: 6.06 dB at CRF 20 and 6.06 dB at CRF 12, while the whole frame barely moves either, at 22.84 and 22.85 dB. The box column sees the mark and nothing else. In that rectangle the repair is worth 20.19 dB and the encoder is worth 0.00 dB.
The box is 120 x 40 px, 2.08% of a 640 x 360 frame, and it costs the whole-frame luma score 4.17 dB: 45.21 dB outside it against 41.04 dB over the whole picture at CRF 20. A mark that size is still the loudest thing in the file, and it is why the whole-frame figure is the wrong one to optimise — 98% of the pixels can be near-perfect and the score still belongs to the corner.
What follows for the export setting is unglamorous. Land at CRF 20, or at whatever the source was encoded with and no better. CRF 28 saves 58% of the bytes and gives up 6.24 dB outside the box; CRF 12 spends 59% more bytes than CRF 20 to buy 1.33 dB outside it and 0.02 dB less inside it. Bytes spent chasing the box are spent where the outcome was already fixed.
The 26 dB inside the box is visible, and this page does not pretend otherwise. It is also this fixture's own number: a solid 120 x 40 mark over moving synthetic detail. A flat background scores better, texture scores worse, and a wider mark costs more of the frame — the crop arithmetic for a full-width bar is measured on the Snapchat page, and the four pixel routes are scored against each other on the logo page.
Every number above describes the file on disk. By the time a viewer sees it, a second encoder has run: the delivery ladder. The complaint that follows — "it looked fine before I uploaded it" — is usually read as proof that the removal was bad. It can be measured instead of argued, because mean squared error adds when two errors are independent, so the removal's error outside the mark and the ladder's error outside it can be computed separately and then added back to predict the file that comes out of the platform.
The method, stated before the numbers: nothing was uploaded and no platform's encoder was used. The cleaned file was put through libx264 at a bitrate matched in bits per pixel to a published ladder — 0.129 bpp, which is 1920 x 1080 at 30 fps and 8 Mbps, and 0.064 bpp, the same frame at 4 Mbps — and the untouched clean file went through the identical encode as the control. dev/generation_two.py rebuilds the fixture, runs both and prints every table below. Matching bits per pixel is the part that makes a 640 x 360 test stand in for a 1080p upload: the encoder is handed the same budget per pixel either way.
kbps bytes whole luma outside box
gen 1, on disk - 493,230 41.04 45.21
ladder A, 0.129 bpp 891 436,834 40.09 43.01
ladder B, 0.064 bpp 442 218,331 37.52 38.82
The second encode moves the picture outside the mark by 2.20 dB at the generous ladder and 6.39 dB at the tight one. Both of those are larger errors than the removal's own export left, which is the first surprising thing: the step nobody thinks of as lossy is the more lossy of the two.
Because the two errors are separable, the split can be computed rather than estimated:
removal MSE ladder MSE removal share ladder share
ladder A 1.9592 1.6986 53.6% 46.4%
ladder B 1.9592 7.6750 20.3% 79.7%
Two honest qualifications. The shares are not an exact decomposition: adding the two errors predicts 42.50 dB outside the box at the generous ladder and 38.29 dB at the tight one, while the files measured 43.01 and 38.82 — half a decibel better than the sum in both cases, so the second encode absorbs part of the first one's error instead of stacking on it. Treat each share as an upper bound. What the arithmetic does establish is the ordering, and the ordering is the answer to the question: at a tight ladder the second encode is four times the removal's error; at a generous one the two are close to even.
Inside the mark's box the second encode does nothing at all. The repaired box scored 26.27 dB, and 26.29 and 26.34 dB after the two ladders — +0.02 and +0.07 dB. The same encode applied to the clean file scores 54.03 and 49.26 dB in that rectangle, roughly 23 dB better than the repair, so what is left in the box is the fill's error and no ladder reaches it. Uploading does not hide a bad repair and does not repair one either.
That leaves the one knob the reader controls, the export setting. The ladder above says CRF 12 is worth 7.57 dB outside the box over CRF 28. Put all three settings through the tight delivery encode and most of it disappears:
CRF on disk after upload bytes after upload 12 46.54 38.89 219,095 20 45.21 38.80 217,870 28 38.97 37.78 209,066
15% of the advantage survives. CRF 12 and CRF 20 land 0.09 dB apart after the upload — a tie, for 59% more bytes on disk — and only CRF 28 is still measurably worse, by 1.02 dB. The mechanism is not mysterious: a bitrate-targeted encode has a quality ceiling set by the bitrate it is handed, and a better source runs into that ceiling and stops improving. So the export setting is worth choosing once, at CRF 20 or at whatever the source was encoded with, and not worth optimising past that for a file that is going to a platform. Keep the low-CRF master for the copy that stays on a disk and in an editor, where nothing re-encodes it a second time — that is the case the section above measures.
Two limits belong in the same breath. The ladder here is modelled, not observed: a real platform's encoder, its scene analysis and its per-title settings will differ, and one that spends more bits will take less. And the fixture is 640 x 360 synthetic detail under a solid mark; a soft mark over flat sky gives an encoder less to spend and less to lose. What transfers is the direction and the order of magnitude, not the third decimal.
Everything above is a compression loss, and compression loss is bought back with bytes. There is a third axis that no bitrate setting touches: how many luminance levels the frame is carried in, and how many the fill is handed. A 10-bit source has 1024 of them. If the fill is handed 256, three quarters of the gradient is gone before the mark has even been looked at, and no CRF, no bitrate and no "maximum quality" preset puts a single level of it back.
The mechanism is one line in the filter graph, and it is worth reading rather than trusting. ffmpeg 9.0.1's delogo declares 8-bit pixel formats only, so on a 10-bit input the graph silently inserts a conversion before the fill and another one after it:
ffmpeg -v verbose -i ten_bit.mkv -vf "delogo=x=1000:y=40:w=140:h=80" -frames:v 1 -f null - [Parsed_delogo_0] auto-inserting filter 'auto_scale_0' between the filter 'graph -1 input from stream 0:0' and the filter 'Parsed_delogo_0' [auto_scale_0] w:1280 h:720 fmt:yuv420p10le -> w:1280 h:720 fmt:yuv420p
The measurement follows from that line. One 1280 x 720 fixture, a luminance ramp from 0 to 1023 across the width, a solid 140 x 80 logo plate in the top right, twelve frames — and every file stored as FFV1 lossless, so no encoder is in the chain and a difference between two outputs cannot be a compression difference. dev/bit_depth.py rebuilds the fixture and prints the table.
levels max step step % clean 10-bit, no mark 1024 1 0.098% clean + logo, 10-bit 1024 1 0.098% delogo, 10-bit in, 10-bit out 256 4 0.391% boxblur, 10-bit in, 10-bit out 1009 1 0.098% eq, 10-bit in, 10-bit out 256 4 0.391% 8-bit in, delogo, 8-bit out 256 1 0.392% 10-bit in, delogo, 8-bit out 256 1 0.392% 8-bit, no delogo (mark kept) 256 1 0.392% 8-bit, no mark, nothing removed 256 1 0.392%
Read the second and third rows together. The frame arrives with 1024 levels and leaves the fill with 256, and the largest step between two neighbouring pixels on a smooth ramp goes from 1 to 4 of 1023 — from 0.098% of full scale to 0.391%. That is banding stated in the only unit an eye cares about: how far apart two adjacent pixels are allowed to be. A step four times wider means four times fewer steps across the same gradient, and on a sky or a wall that is exactly what a band is.
The boxblur row is the control that decides whose fault this is. Same input, same 10-bit output, but a filter that does declare 10-bit formats: 1009 of 1024 levels survive and the step stays at 1. The container, the codec and the pipeline can all carry ten bits, so the loss belongs to the filter's declared format list, not to the file. The eq row says the same thing from the other side — it drops to 256 as well, so this is not a quirk of delogo but a property of a whole class of common filters, and it stays invisible until something counts.
The third result is the one that changed this page's mind. If a 10-bit filter chain helped at all, then "10-bit in, 10-bit out" should beat "10-bit in, 8-bit out". Measured against the clean 10-bit frame, in one domain so depth is not counted as error, they are not close — they are identical:
box PSNR whole frame 10-bit in, 10-bit out 58.24 dB 56.06 dB 8-bit in, delogo, 8-bit out 58.24 dB 56.06 dB 10-bit in, delogo, 8-bit out 58.24 dB 56.06 dB
Same pixels to the second decimal, because they are the same pixels: all three went through an 8-bit fill. Keeping the result in a 10-bit container afterwards is a labelling decision, not a quality one. It costs bytes, buys nothing, and the levels column had already said so — 256 in all three rows.
The honest boundary is grain. Repeat the comparison with plus or minus 8 of noise added to the ramp, 0.8% of full scale, and the level count collapses on both sides: 738 for the 10-bit frame against 254 for the 8-bit one, while the largest step is set by the noise itself, 1.662% against 1.955%. Dither does what dither does — it hides banding by destroying the smoothness that made it visible. This is a loss you will see on a gradient and not see on foliage, and a page claiming otherwise would be selling a metric rather than a picture.
What follows is a trade, not a fix. Keeping ten bits means the fill has to be built from filters that carry them, and the obvious 10-bit-capable fill in ffmpeg's default set is a blur — the one fill this site has already measured as keeping the mark's average brightness instead of removing it (measured on the blur page). So the choice is an 8-bit interpolated fill that bands on a smooth gradient, or a 10-bit blur that does not band and does not remove the mark either. There is no free 10-bit fill in the default filter set, and saying so is more use than implying that a 10-bit project setting solved it.
One limit on the word HDR: bit depth is what is measured here, and HDR adds a transfer function — PQ or HLG — on top of it. This page measured depth, not tone mapping, and a file that was tone-mapped down to SDR on the way in will have lost more than the four-to-one above before any filter ran.
The damage people report is not a soft picture. It is a video that comes back with no sound, or no captions — because the pipeline re-encoded the picture and never mapped the other streams. That is not hypothetical: it is what happens when a re-encode maps only the video, which is the default shape of a great many one-click tools. Measured on the same test file:
map every stream streams: video, audio, subtitle
map only the video streams: video
(the audio and the subtitle are not degraded - they are absent)
There is no warning for this. The file plays, the picture looks right, and the audio is simply gone. It is also the reason "does removing a watermark remove the audio" is worth answering with a command rather than an assurance: the stream list is the first thing to read, and it takes one line.
To measure sync rather than assume it, the test file is built with a sound and a picture event at the same instant: a 1 kHz tone gated to a burst at exactly 1.000 s, and a white flash at exactly 1.000 s. The tone's first loud sample and the flash's frame are then located in each output. The source file itself measures the tone at 1.0027 s against the flash at 1.0000 s — a residual 2.7 ms that the AAC encode left in the file before anything was done to it. That same -2.7 ms offset appears after the metadata strip, after cropping and after inpainting:
source file tone 1.0027 s flash 1.0000 s offset -2.7 ms after metadata strip tone 1.0027 s flash 1.0000 s offset -2.7 ms after cropping tone 1.0027 s flash 1.0000 s offset -2.7 ms after inpainting tone 1.0027 s flash 1.0000 s offset -2.7 ms
The offset does not move because nothing re-timed it. Copying the audio stream cannot change the alignment — the bytes are the same bytes — and re-encoding the picture does not touch the audio at all. This is why the audio column in the table above is the one to watch: as long as the audio is copied, sync is preserved by construction, and the moment a tool re-encodes the audio it has introduced a variable that was not there before.
None of this needs the tool that made the file, or this site. On the file you have:
# 1. Is everything still there, and is the timeline right? ffprobe -v error -show_format -show_streams cleaned.mp4 # look for: one video stream, one audio stream, any subtitle stream, # and duration / r_frame_rate / nb_frames matching the original # 2. Is the audio stream the same bytes as before? ffmpeg -i original.mp4 -map 0:a -c copy -f md5 - ffmpeg -i cleaned.mp4 -map 0:a -c copy -f md5 - # identical output means the audio was copied, not re-encoded # 3. Did the picture change at all? ffmpeg -i original.mp4 -map 0:v -f framemd5 - > before.txt ffmpeg -i cleaned.mp4 -map 0:v -f framemd5 - > after.txt diff before.txt after.txt # no output means every frame decodes to identical bytes
If the mark was in the container, check three gives no output and the file is provably untouched. If the mark was in the pixels, check three will differ on every frame — that is expected, because the picture was re-encoded — and checks one and two are the ones that catch a broken file. The metadata-strip method this site's browser tool performs is the case where all three pass at once, and it is described on the page about removing metadata from a video.
It depends on which layer the mark is in. If the mark is a record in the container — a C2PA provenance box, a title or an artist tag — then removing it does not touch a single pixel: on this site's test file the picture decoded to identical bytes before and after, and the file did not change size. If the mark is painted across the frames, then removing it means re-encoding the picture, and that is where quality is spent, on every pixel in the frame rather than only the ones under the mark. Measured here, one extra encode of a clip that had no mark at all cost 48.80 dB; inpainting the marked rectangle reached 38.62 dB against the clean original. So the honest answer is: metadata removal costs nothing, pixel removal costs one generation.
No, and the split is measurable. The fill's error sits inside the mark's rectangle; the encoder's error sits outside it. Across CRF 12 to CRF 32 the inside of the box moves from 26.25 dB to 26.30 dB, 0.05 dB in total, while the picture outside it moves 10.03 dB. The files behind those two ends are 784,337 and 136,928 bytes. A better export setting buys back compression, and compression is not what was lost in that rectangle.
It should not, and there is a way to prove it did not. The audio is a separate stream in the file, so a tool can copy it byte for byte while it rewrites the picture. On this site's test file, the audio stream's MD5 was identical after the metadata strip, after cropping and after inpainting — the same compressed bytes went in and came out. What does lose the audio is a pipeline that re-encodes the picture and forgets to map the other streams: the same command with only -map 0:v produced a file whose stream list was video and nothing else. That is the failure to look for, and it is visible in one line of ffprobe output.
If they are a real subtitle track inside the file — mov_text in an MP4, for instance — they survive anything that copies the streams, including the metadata strip, cropping and inpainting, as long as the tool maps them. On the test file the subtitle stream was still present after all three. The risk is the same one that loses the audio: a re-encode that maps only the video drops the subtitle track silently, and you find out when the file plays with no captions. Burned-in subtitles are a different case entirely — those are pixels, and they come off only the way the mark does.
Not on any method measured here. The test file was 4.000000 seconds, 30/1 frames per second and 120 frames, and those three numbers were identical after the metadata strip, after cropping the marked strip off and after inpainting the rectangle. Removing a watermark is not a trim and not a speed change, so there is no reason for the timeline to move — but it is one of the first things to check, because a tool that re-times the file to its own container will show it here. ffprobe prints duration, r_frame_rate and nb_frames in one call.
Only if the tool re-encodes the audio instead of copying it. When the audio stream is copied, its bytes are unchanged, so the alignment cannot move — that is a proof, not a promise. On the test file the beep was authored at exactly 1.000 s; it measured at 1.0027 s in the source, a residual 2.7 ms from the AAC encode that was already there before anything was done to the file, and it measured at exactly the same 1.0027 s after the metadata strip, after cropping and after inpainting. The offset never moved. Re-encoding the audio produces a different stream, which is where a tool can introduce a delay that was not in the original.
Three commands, and none of them needs the original tool. First, ffprobe -v error -show_format -show_streams cleaned.mp4 to confirm the stream list still contains the video, the audio and any subtitle track, and that duration and frame rate are what you expect. Second, ffmpeg -i original.mp4 -map 0:a -c copy -f md5 - against the same command on the cleaned file, to confirm the audio stream is byte-identical. Third, ffmpeg -i original.mp4 -f framemd5 - against the cleaned file, to see whether the picture changed at all — no output from a diff means every frame decodes to the same bytes. If the tool re-encoded the picture, that last comparison will differ everywhere, which is expected; the first two are the ones that catch a broken file.
Because a second encode happens after yours. The cleaned file is re-encoded by the delivery ladder at a bitrate the platform chooses, and that step can be measured on its own: putting a repaired file through an encode matched to a 4 Mbps 1080p ladder moved the picture outside the mark by 6.39 dB, while the removal's own export still scored 45.21 dB against the clean original. Outside the mark, 79.7% of the squared error at that bitrate belongs to the second encode and 20.3% to the removal. A platform that spends more bits takes less — at a ladder matched to 8 Mbps the split was 46.4% against 53.6% — so the exact figure is not fixed, but the direction is, and it is why a file can look clean on disk and soft on the web.