Product logo swap
Replace a brand logo printed on an existing product photo and finish the result as clean studio product photography. Runs fully locally — no API keys, no paid services.
npx -y skills add Martin-Code202/skills --skill product-logo-swapAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 23 days oldThe repository was created 23 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Replace a brand logo printed on an existing product photo and finish the result as clean studio product photography (seamless white, grounding shadow, product-only grade). Use when asked to rebrand, re-logo, or white-label a product shot, swap a wordmark on packaging or a device, or clean up a supplier/marketplace photo for a storefront. Runs fully locally — no API keys, no paid services.
SKILL.md
12.0 KB, as published. Nobody here has run it
Product logo swap
Rebrand an existing product photograph: erase the printed logo, composite a new one so it sits in the surface rather than on top of it, and finish the frame as studio product photography.
Fully local. No image-generation API, no key, no upload of the user's product shot to a third party. If the user asks for a hosted/AI editing service, say no and use this instead.
Before the first run
The scripts need three packages. Check once and install if missing — a traceback
on import cv2 is a worse first impression than a two-second install:
python3 -c "import numpy, cv2, PIL" 2>/dev/null || \
pip install -r skills/product-logo-swap/requirements.txt
The two things that will bite you
1. A logo with thin or outlined strokes cannot survive at small pixel widths. Supplier photos are routinely 500–800 px, and a wordmark on them lands ~40 px wide. An outlined mark at 40 px has sub-pixel strokes and dissolves into a smudge. It is not a sharpening problem and no amount of tuning fixes it.
The fix is structural and it is what this skill is built around: do the swap on an upscaled master, then supersample the delivery sizes down from it. The mark gets rendered at 150+ px where its strokes are real, and the downsampled 560 px version inherits clean antialiasing instead of mush.
logo_swap.py computes the mark's thinnest stroke and warns you if your
--width × --scale lands below the legibility floor. Listen to it.
2. A printed mark obeys the surface it is printed on. Product photography is full of faces that are not square to the camera — a gloss lid, a bevelled front, a box shot at three-quarters. Paste a level wordmark onto a face whose baseline runs 13° downhill and it reads as a sticker instantly, no matter how good the shading is. Nobody looks at it and says "the homography is wrong"; they say it looks fake, and they are right.
So the swap fits the outgoing mark's own geometry and warps the new mark onto
it (--perspective auto, the default). The old mark is the ground truth for
how the surface sits: its ink gives a baseline angle. swap prints what it
fitted:
fit at 367,174: baseline +13.4 deg, rotation only
Read that line. If it says +0.3 deg on a face you can see is rotated, the
fit found something other than the mark, and you want --rotate or --quad.
The auto fit gets the rotation, not the tilt — and the tilt is the bigger cue
This is the trap, and it cost two rounds of review to see. The auto fit recovers the baseline angle reliably. It does not recover the tilt away from the camera, because a 50 px mark's top and bottom envelopes cannot support a trustworthy foreshortening estimate — marks on faces demonstrably square to the camera fit tapers as low as 0.79 out of pure noise, so believing that number would put a wedge into every flat panel.
The consequence: on a lid that recedes hard, auto gives you a correctly-angled
mark at its full height. It looks tilted. It still reads as a decal floating
above the surface, because nothing about its proportions says the surface is
going away from you. A reviewer will say "it's still too upright" and be unable
to tell you why — the missing cue is vertical squash, not rotation.
When the face is visibly tilted, place in 3D instead:
--rotate 70,10,14 # rotateX, rotateY, rotateZ in degrees
Axes and order match CSS rotateX() rotateY() rotateZ(), so angles read off a 3D
transform tool transfer directly. rx is the one that matters: cos(70°)
squashes the mark to a third of its height, and that is what puts it on the
lid. Sanity-check against the outgoing mark by measuring both — the new mark's
ink bounding box and principal-axis angle should land on the old one's:
OUTGOING mark : bbox 56x17 PCA +12.3°
INCOMING mark : bbox 58x17 PCA +13.6°
Note the two rotations interact: an in-plane rz of 30° tilted back by rx=70°
projects to an apparent slope of only ~12°. Don't set rz to the slope you see
in the photo — that double-counts, and the mark comes out visibly over-rotated.
Workflow
1. Find the existing marks. Give it the rows to search; it reports centres in the exact flag format the next step wants.
python3 scripts/logo_swap.py detect --src product.jpg --band 128,150
# --at 105,138 (box 37x5 at 87,136)
# --at 268,138 (box 37x5 at 250,136)
# --at 427,138 (box 37x5 at 408,136)
Read the reported boxes back against the image before trusting them — cabinet seams, screws and panel edges are also dark-on-light and can be picked up. Crop and zoom the band to confirm.
2. Swap. --width is the new mark's width in source pixels — match the
old wordmark's footprint (±10%) unless there's a reason not to; that footprint is
what the industrial designer laid out for.
python3 scripts/logo_swap.py swap \
--src product.jpg --logo brand.png \
--at 105,138 --at 268,138 --at 427,138 \
--width 41 --scale 4 --out-dir .
Writes a _2240.png master plus downsampled .jpg deliverables.
3. Look at the output. Zoom the retouched band at 100% and check the erase didn't smear a panel edge or eat the top of a display bezel. Always.
Flags worth knowing
| Flag | Why |
|---|---|
--scale N | Master multiplier. 4 is the default and is right for ~500–800 px sources. |
--perspective off | Force a flat paste. The default auto fits the old mark's angle. |
--rotate RX,RY,RZ | Place in 3D. Use whenever the printed face is tilted away from the camera — auto will not find that on its own. |
--persp N | Camera distance for --rotate, in units of mark width (default 2.5). Lower = stronger perspective divide. |
--quad X1,Y1,...,X4,Y4 | Place by explicit corners (TL,TR,BR,BL) when you'd rather nail them by hand. |
--ink match | Keep the brand hue, but inherit the outgoing mark's rendered brightness. This is usually what you want on a dark surface. |
--ink white | Use when the brand colour is too low-contrast on a dark or saturated surface, and the brand hue doesn't matter. |
--ink R,G,B | Force an exact brand colour; otherwise it's sampled from the logo file. |
--erase-pad W,H | Half-extents of the erase patch. Shrink it if the mark sits near a bezel or edge. Note this box also defines what the perspective fit looks at. |
--no-grade / --no-shadow | The photo is already graded, or already has a real shadow you'd double up on. |
--sizes | Delivery ladder, e.g. 2240,1120,560. |
Ink on a dark surface
Brand navy is a fact about the brand. How bright a mark reads on a black gloss
lid is a fact about the photograph. On a dark surface the brand value alone is
unreadable, and pure white is a different brand — so --ink match keeps the
brand's hue and lifts it to the luminance the outgoing mark was rendered at.
That's the value the manufacturer's own photographer chose for that surface.
How each stage works, and why
Erase — three passes, because one isn't enough. cv2.inpaint gets the bulk;
then a row-wise linear solve across the patch, because a lit panel's rows are
very close to linear gradients and inpainting alone leaves a visible smear; then
grain is re-added, sampled from the surrounding surface. A perfectly smooth patch
on a noisy JPEG is the single clearest tell of a bad retouch.
Both the solve and the re-grain use robust statistics, deliberately. The
patch is a rectangle laid over a photograph, so its edges routinely catch things
that aren't the panel — a frame rule on a marketing composite, a cable, a
specular highlight. Anchor the row solve on the single nearest kept pixel and it
will happily wash a frame's blue straight across the patch. Estimate grain with a
plain std() and one highlight in the sample strip turns into a box of coloured
speckle on a black lid. Medians and MADs throughout, and the grain is capped.
Fit — the outgoing mark's ink is lifted by distance from the local surface colour, which is polarity- and hue-agnostic: it finds navy on a grey cabinet, pale blue on a black lid, and white on a dark bezel with the same code. (A luminance test does not: a navy wordmark on a white cabinet can be luminance 160 on a 214 surface, and a brightness detector walks straight past it.) Its principal axis gives the baseline; slicing along that axis and fitting the top and bottom envelopes gives the foreshortening.
Two guards matter here. Ink touching the border of the box is discarded — that's how a specular highlight in a corner gets in, and one such blob swings the fitted baseline by nearly 20°. And the taper is only believed when it's well clear of the noise floor: marks on faces demonstrably square to the camera still fit tapers as low as 0.79 out of pure noise, so a gently-tapered fit degrades to a pure rotation, and a fit under ~2° degrades to a flat paste. Angle is stable at 50 px. Taper is not. Don't pretend otherwise.
Upscale — light bilateral filter first to kill JPEG mosquito noise (blended 65/35 so real texture survives), then Lanczos, then a two-radius unsharp. Be honest with the user that upscaled detail is reconstructed, not captured: fine texture like a perforated grille will be soft under close inspection.
Composite — the mark is warped onto its fitted quad, then the ink is multiplied by the surface's own blurred luminance, so it darkens and lightens exactly where the panel does. Ambient bounce off the gloss is mixed in — but scaled to the surface: 7% is right on a lit grey panel, and on black it just drags white ink toward grey until the mark reads embossed. Then the region is blurred by half a source-pixel to match the lens. Skip these and the logo reads as a sticker: too even, too sharp, too clean.
Note load_mark keys the logo hard (anything above luminance 215 is background).
Brand logos arrive as JPEG-compressed PNGs more often than not, and the mosquito
ringing hugging a bold stroke sits at 225–250. Key gently and that ringing
survives at alpha ~0.08 — invisible at 40 px, a pale rectangle behind the mark at
1400 px. The same reason the resampled alpha gets its ringing floored: Lanczos
overshoot beside a heavy stroke is 5% ink over paper white, and you will see it.
Finish — a soft product matte separates subject from backdrop; the backdrop is reseated at true paper white; a two-layer shadow (broad ambient pool plus a tight contact line) grounds the product; and contrast/saturation are applied to the product only — grading the backdrop is what turns white into grey haze.
Judgment calls to raise with the user
- Tilted faces.
autowill not detect a tilt away from the camera; it only finds the in-plane angle. If the face is visibly receding, say so and reach for--rotate— don't ship a full-height mark on a receding lid and hope. - Curved surfaces.
--rotatehandles a tilted plane. If the logo wraps a genuinely curved surface — a bottle, a drum — it needs a displacement map, not a homography. Say so rather than shipping a mark that ignores the geometry. - Contrast. Brand-accurate ink can read weak from across a storefront. Offer
--ink matchand--ink whiterather than deciding silently. - Honesty about resolution. If the source is small, tell them the big master is an upscale. Don't present reconstructed detail as captured detail.
- Not every frame is a product shot. A marketing composite with a frame and a
banner, or a real-world installation photo, must not get the studio finish —
it will eat the frame and drop a shadow under a beach. Check the backdrop
before enabling
finish, and pass--no-shadow --no-gradewhen it isn't one.