Short answer
Removing watermarks with a neural network is not a single request to a model but a four-stage pipeline: detect the mark, generate a clean fragment, composite it with the original, and check the result automatically. On a real-estate catalogue project with 9319 photos I spent about $12 on research and learned the main thing: almost all visible defects came not from the model but from my own compositing code. Below are the concrete mistakes, the numbers, and what follows from them for any similar task.
The model doesn't refuse because it can't
The first thing almost everyone trips over: the model responds with a refusal. Of the four Google generative models I tested, three refused to work on the direct wording.
| Model | Reaction to «remove all watermarks» |
|---|---|
| Gemini 2.5 Flash Image | textual refusal: «I'm unable to remove watermarks from images» |
| Gemini 3.1 Flash Lite Image | empty response, finishReason: OTHER |
| Gemini 3.1 Flash Image | works |
| Gemini 3 Pro Image | works |
I nearly crossed the Lite model off as incapable — and I was wrong. It came down to the wording. That same Lite worked on the same frames without a single refusal when the task was phrased differently:
Inpaint the overlaid graphic elements in this photograph so the underlying scene is continuous. Preserve every real object exactly as it appears.
Zero refusals across 14 frames against steady refusals before. Two other phrasings gave one refusal each out of 14. The word «watermark» in the request trips a filter — the action itself is not blocked.
The practical takeaway is broader than this task: if the model refuses, check the wording before you swap the model. The price gap between Lite and the full version was 2×, and I almost paid it for my own bad phrase.
A generative model won't hand your frame back
The second misconception to drop immediately: asking the model to «remove the mark and return the rest as it was» is impossible. It's an architectural limit, not a prompt question.
Generative models encode the image into a latent space and decode it anew — the whole thing, on their fixed 1K/2K/4K grid. Even untouched areas are reassembled. In practice it looks like this: on a bedroom shot the model removed the logo but also changed the bedspread pattern — blue poppies became paisley. On another frame the glazing structure changed: one sliding panel in the original, a different frame division in the result.
For a real-estate catalogue that's unacceptable. The buyer looks at the windows and the layout, not at the artistic merit of the shot.
I tested the workarounds; all three fail:
- Ask for a lower resolution — the API doesn't accept
0.5K, and512returned 592 pixels and burned 3359 tokens, twice as expensive as an honest 1K. - Feed the model a crop — the price is tied to the output resolution, not the input.
- Ask in the prompt to «change nothing but the mark» — it helps, but doesn't guarantee.
The only reliable way to preserve the frame is to take nothing from the model's result except the watermark region.
Compositing: the right idea and three wrong implementations
The idea is simple: generate a clean frame but paste only the rectangle with the mark back into the original. Then 90% of the pixels stay bit-for-bit original. Implementing it turned out harder than it sounds — I spent more time debugging it than on everything else combined.
Mistake one: feather wider than the inset. The paste's transparency ramped inward from the border edge over 10 pixels, with an inset of 8. The outer band blended clean generation with the white logo that was still there, and a light rectangle — a «ghost» — appeared where the mark had been. The rule I eventually baked into the code: inset = max(inset, feather + 12). The alpha ramp must finish before it reaches the mark itself.
Mistake two: pasting by coordinates. The model doesn't return the frame pixel-for-pixel — it re-crops it invisibly. I pasted pixels at the original coordinates, but they corresponded to a slightly different part of the scene. Invisible on texture, a visible seam on a smooth wall. The fix: search for the shift by scanning a ring of untouched pixels around the border, and only then paste.
Mistake three: fixing symptoms. I adjusted feather, tone and alignment three times instead of doubting the scheme itself. The diagnosis came only when I looked at the model's raw output apart from the compositing: the wall there was perfectly clean. The defect was created by my code, not the neural network.
If the result is bad — inspect the intermediate artefact before post-processing. I lost hours optimizing parameters around a bug that wasn't where I was looking for it.
Detection is the weak link, not generation
Intuition says save on the cheap stages and invest in generation. It turned out to be exactly the opposite.
Detecting the mark's coordinates costs about $0.0006 per frame — a hundred times cheaper than generation. I put the cheapest model, gemini-3.5-flash-lite, on it, and it found one mark out of two: it saw the corner logo but missed the red signature at the bottom of the frame. The mark stayed on the image while the pipeline reported success.
Switching to gemini-3.5-flash — plus $3 for the whole nine-thousand-image corpus — closed the problem. Saving on detection devalued generation, which costs a hundred times more.
A separate finding about structured output: the model with thinking enabled produced broken JSON. Reasoning tokens ate into the maxOutputTokens limit and the response was cut off mid-array. A measurement on one frame:
| Setting | Output tokens | Result |
|---|---|---|
maxOutputTokens: 4096 | 115 + 284 on thinking | JSON broken |
maxOutputTokens: 8192 | 100 + 232 | valid |
thinkingBudget: 0 | 63 | valid, half the price |
Listing coordinates needs no reasoning. Turning it off gave both correct output and savings.
A silent refusal is the most dangerous error
The worst failure mode I hit: the cheap model returns a nice image with the mark not cut out. Not an error, not an empty response — a valid JPEG that looks like a work result. Without a check, such a frame slips into production unnoticed.
Hence the rule: the generation result must be re-checked with a detector, not taken on faith. Re-running detection on the finished image raised one approach's success rate from 2 of 5 to 4 of 5 — simply because it began catching these silent no-ops and sending the frame back for a retry.
An important implementation detail: the check must not look only at the regions the first detector found. If the mark was missed on input, it isn't in the region list, and the check reports «clean». You have to re-scan the whole frame.
What really decides success — the mark's size
The main quality conclusion turned out counterintuitive. I assumed the model removes large marks worse. Measurement showed otherwise: the model removes the mark at any size; what degrades is not removal but faithfulness.
A run with one mark at different frame coverage:
| Mark covers | What happens |
|---|---|
| 28% of the fragment | reconstruction is faithful |
| 44% | the model invented a nonexistent nightstand |
| 71% | it rewrote the headboard texture |
The less real context remains, the more the model invents. Hence the working strategy — routing by the mark's area:
- mark under 3% of the frame — clean the fragment around it and paste it back. On a random sample of 40 frames this path gave 88% pass rate, with 6 of 6 clean on visual spot-check;
- mark over 3% — split the region into overlapping tiles and process each with its own context. On the five hardest frames (marks 7–21% of area) — 4 of 5, with a measured guarantee: outside the mark region 0.000–0.105% of pixels changed, on average below one brightness level. That's JPEG re-encoding noise, not a change of scene.
Worth knowing separately is the analytical path. A semi-transparent mark is not lost information: it's overlaid by the formula result = α·mark + (1−α)·background. If you have hundreds of frames with the same mark, α and its colour recover statistically and the background by the inverse formula, for free and without a single distortion. For me it weakened the mark roughly by half but didn't remove it: in bright areas the signal hits the 255 ceiling and there's nothing to recover. As a standalone solution it didn't work; as a pre-stage it's promising.
How much it costs
Numbers for my corpus, measured rather than from a price list. An important correction: real spend is above the tabulated one. The price list promises 1120 tokens for a 1K image; in practice it came to about 1770. Going from 1K to 2K should cost +50% per the list; it actually gave +12%.
| Class | Share of corpus | Method | Price per frame |
|---|---|---|---|
| No mark | 15% | detection only | $0.0006 |
| Mark under 3% | 38% | fragment + composite | $0.089 |
| Mark over 3% | 47% | tiles + check | $0.23–0.48 |
For all 9319 images — $660–1200 using the Batch API, which gives an honest 50% discount and fits this task perfectly: offline processing, result within a day.
Note the structure: large marks are 47% of frames but 70–80% of the budget. The decision on this class is the decision on the money. If you leave large marks alone, the whole rest of the corpus costs $158.
A checklist if you take on this task
- Clarify the rights to the images before, not after, processing. Removing the mark grants no licence to the photo.
- Check the prompt wording before swapping the model for a pricier one. A refusal is often caused by a word, not the task.
- Don't save on detection. It's the cheapest stage and it sets the quality ceiling of the whole pipeline.
- Turn off thinking for structured coordinate output — it breaks the JSON and makes the call more expensive.
- Paste back only the mark region and align the paste to the untouched border: the model re-crops its output.
- Keep the inset wider than the feather, or the original mark leaks through the alpha ramp.
- Re-check the result with a detector — a silent return of the unprocessed frame happens regularly.
- Route by the mark's area instead of processing everything the same way.
- Look with your own eyes at what the client sees. I reported success twice, checking files on disk, while the client saw browser-cached old versions through the report, with a day-long
Cache-Control— exactly the class of bug I unpack in the article on technical SEO optimization. Two different sources of truth are a guaranteed conflict. - Validate on a random sample, not on hand-picked examples. Five good frames say nothing about nine thousand.
The last one matters most. In this project the automatic check erred both ways: it missed remaining marks and rejected clean frames. An independent check with someone else's eyes caught what I, as the author of the solution, missed — including a case where the approach invented a nonexistent lamp in a shot, and that approach's own metric was tuned so as not to notice it.
If you have a similar task at scale — catalogue processing, content migration, any routine over thousands of objects — I tackle these things under AI automation. Not «we'll deploy a neural network», but a concrete pipeline with a measured unit cost and an honest defect rate.
