Home (All Topics) → Technology → Software → AI-Created Software
The following is AI-generated (aka vibe-coded) software. The software is specific to my needs and my system and may not work for you or your system. Support is limited, but if you report a bug I'll certainly look into it. All code is provided in a zip file for you to review should you choose.
A desktop app that matches the colors of a processed photo back to the original it came from. Choose the original (the reference) and the processed version of the same photo, or two folders of them paired by name, then click spots that should have the same color in both. Each click places a pin at the same place on both photos, and the processed photo is corrected so the pixels under each pin take on the reference’s color. Save the result as a new file.
There are five matching modes, from a blend of the pins’ corrections to a correction learned from every pixel of both photos, and the mode can be changed at any time without losing the pins, so you can pick whichever suits the photo best. Auto, Local, and Matrix fitted to the whole photo need no pins at all.
The photos are shown side by side or stacked. Stacked, one key flips between them, and a vertical bar can be dragged to show more of one or the other. With two folders, the Pairs panel lists every pair, and a click or the arrow keys open the next one.
A difference map shows where photo 2 still differs from the reference, each mode’s score for the whole photo is shown under its button, a brush leaves out areas you changed on purpose, every change to the pins can be undone, and the pins are kept from one session to the next.
sudo apt install python3-tk).pip install pillow numpypip install rawpy
python image-to-image-color-matcher.py # open the app
python image-to-image-color-matcher.py --reference-file-or-folder-path original.jpg --processed-file-or-folder-path edited.jpg
python image-to-image-color-matcher.py --reference-file-or-folder-path originals --processed-file-or-folder-path edited --recursive TRUE
python image-to-image-color-matcher.py --selftest TRUE # check the image functions and exit
python image-to-image-color-matcher.py --working-folder-path ~/app-data # keep settings and temporary files there
python image-to-image-color-matcher.py --help # list the options and their defaults
Every option is named and takes a value, written --name VALUE or --name=VALUE, in any order.
Yes/no options take TRUE or FALSE (also 1/0, YES/NO, ON/OFF, in any case); script launchers that read the options (such as Scriptary) offer them as buttons. A mistake on the command line (an unknown or abbreviated option, an option given twice or without its value, or a file name without an option) stops the app with a message and exit status 2; started without a console, as with pythonw.exe, the message is shown in a message box instead.
| Option | What it does |
|---|---|
--reference-file-or-folder-path PATH |
The original photo, whose colors are the target, or a folder of them. Blank or not given: the one used last. |
--processed-file-or-folder-path PATH |
The processed photo, whose colors are matched, or a folder of them. Two files are paired whatever their names; two folders pair the photos with the same name. The first pair opens when the app starts. Blank or not given: the one used last. |
--recursive TRUE |
Pair the photos in sub-folders too, by their path inside the folder (FALSE pairs only the folders’ own photos). Blank or not given: the choice saved last time. |
--selftest TRUE |
Checks the image functions, color management and settings without opening a window, lists which optional features are installed, and exits with 0 if every check passed or 1 if not. |
--working-folder-path FOLDER |
The folder for the settings file and temporary files, created if needed; if it can’t be created, the app stops with exit status 2. Blank or not given: the default in Settings and working folder. |
<name>-matched.<ext>. The pair gets a ✓ in the Pairs list.Undo (Ctrl+Z) and Redo (Ctrl+Y or Ctrl+Shift+Z), at the top right of the photo panel, above ΔE map, take back and make again any change to the pins or the left-out areas: a pin placed, moved or removed, Auto-place, Clear pins, a brush stroke or Clear areas, up to 100 changes back. The history starts afresh each session.
Pins stay in place when you open another pair, so after exporting the processed photo again you can open the new version and check it against the same spots, and a series of similar photos can be matched at the same spots one after another. They are also saved when you close the app, with the left-out areas, and are back in place the next time it starts.
sub/a.jpg with sub/a.jpg). Changing it pairs the folders again.
| Keys or mouse | Action |
|---|---|
| Ctrl+S (Cmd+S on macOS) | Save the matched photo |
| Left and right arrows | Open the previous or next pair |
| Enter in a path field | Rescan |
| Enter in the Auto-place box | Place that many pins |
| Ctrl+Z (Cmd+Z on macOS) | Undo the last change to the pins or the left-out areas |
| Ctrl+Y or Ctrl+Shift+Z | Redo |
| D | Show photo 2 as the difference map, or as itself |
| B | Turn the brush on (Leave out) or off |
| Escape | Turn the brush off |
[ and ] |
Make the brush smaller or larger |
\ (backslash) |
Stacked: flip between the reference and photo 2 (from Split, show the reference). Side by side: flip photo 2 between matched and processed |
| M | Show photo 2 matched or as processed |
| Delete or Backspace | Remove the selected pin |
| 1, 2, 3, 4, 5 | Matching mode: Blend, Spline, Matrix, Auto, Local |
| Click a photo | Place a pin there, on both photos. Keep the button down to drag it into place |
| Drag a pin | Move it. Photo 2 updates when you pause or let go |
| Right-click a pin (Control-click on macOS) | Remove it |
| Click the showing photo’s button again (Stacked shows) | Flip to the other photo, so one button flips between them without moving the pointer |
| Drag the bar in the Split view | Show more of one photo or the other |
| Drag on a photo with the brush on | Leave out the area painted over (Leave out), or bring it back (Bring back). Pins can’t be grabbed while the brush is on |
Rest the pointer on a mode button, ΔE map or the brush for a tip. No shortcut acts while you type in a field (the arrows move the cursor there) or while a dialog is open, and the arrows move a slider you have just clicked rather than changing pairs; click a photo to give the keys back.
ΔE map, beside Photo 2: Processed | Matched above the photos (or the D key), shows photo 2 as its difference from the reference, pixel by pixel: as processed, it shows where the processing changed the colors; matched, where the matching still falls short, which is where the next pin is most useful, or a sign that another mode would suit the photo better. It works side by side, stacked and in the split view, where the bar moves between the reference and the map.
Each pixel’s ΔE is shown as a color, from dark through purple, red and orange to pale yellow, each lighter than the one before, so the order reads without telling the colors apart: under 1 ΔE (a match) the map shows the photo’s own lightness, dimmed, so you can see where you are; from 2 ΔE the color shows in full. The scale at the bottom of the map gives the steps: 1, 2, 5, 10 and 20 or more. Left-out areas are hatched in gray. The map is worked out from the photos at the size they are shown, so a small detail counts as much as it shows. Whether it is on is remembered.
Some differences between the photos are changes you made on purpose: a local edit, a dodged face, a darkened sky. Auto, Local and Matrix fitted to the whole photo learn from every pixel, so they would try to undo such an edit, or let it skew the colors everywhere else. Paint over it to leave it out:
[ and ] make it smaller or larger; a single click paints one dab. Paint in as many strokes as you like.Once a stroke is finished, Auto, Local and Matrix fitted to the whole photo learn again without the left-out areas, the Whole photo line and the row of modes measure the rest of the photo, and Auto-place puts no pin on them. The info line above the photos says how much of the photo is left out. The correction is still applied to the left-out areas when the photo is shown and saved, as to the rest of it, and a pin placed in one by hand still counts. The areas are kept as shares of the frame, like the pins, so they fit another pair’s photos at any resolution, and they are saved with the pins.
Every mode works from the same pins, so switching modes never loses them. Pins are exact in Blend, Spline, Auto and Local (each pin’s color lands within about 0.1 ΔE of the reference); Matrix fits them by least squares once there are more than three.
How each mode uses pins:
The status line under the photos says the same for the current mode.
| Mode | Pins | What it does | Best for |
|---|---|---|---|
| Blend (1) | Needed | Each pixel takes a mix of the pins’ corrections, weighted by how close its color is to each pin’s color. One pin corrects the whole photo. | A few pins; the most predictable. |
| Spline (2) | Needed | A smooth warp of color space through every pin (a thin-plate spline). Beyond the pins it continues in straight lines instead of averaging, with no rings around each pin’s color. Black and white are held in place. | Several pins (5 or more) on varied colors. |
| Matrix (3), fitted to the pins | Needed | A 3 × 3 color matrix in linear light, the way white balance and channel mixing work, then a lightness curve through the pins. 1 or 2 pins give a white balance; 3 or more a full matrix, fitted by least squares. | Differences in white balance or color calibration. With 1 or 2 pins, put them on grays or whites; for a full matrix, use 4 or more pins on varied colors. |
| Matrix (3), fitted to the whole photo | Not used | The same matrix and lightness curve, fitted to every pixel of both photos. | Processing that was one change over the whole photo (white balance, color calibration, exposure, a tone curve), when you want no pins. Being simpler than Auto’s fit, it also carries over better to colors that hardly appear in the photo. |
| Auto (4) | Optional | Learns the correction from every pixel of both photos, so it works with no pins at all. Pins then fine-tune the colors near their own. | Processing that treated every part of the photo the same way. |
| Local (5) | Optional | Like Auto, then corrected again area by area across the frame. | Processing that changed some areas more than others: a vignette, a graduated filter, local edits. |
Colors are compared and corrected in CIELAB, where equal distances look like roughly equal color differences. Each pin samples the average color of its square in both photos (averaged in linear light). Its correction is the reference color minus the processed color.
Auto-place puts the pins for you:
The pins replace those already placed, and the matching is worked out once, after the last pin. They are ordinary pins from then on: drag one to move it, or remove it, and they stay when another pair opens. To be quick, the search runs on a copy of the photos reduced to about 1500 pixels on the long side, then each find is refined at full size within a few pixels. On generated photos of colored blocks it took 1.7 seconds at 6000 × 4000 (4 pins of 11 px) and 2.5 seconds for 100 pins of 51 px, and 1.3 and 1.8 seconds at 3000 × 2000; comparing every square at full size took 24 seconds and 3.5 GB of memory at 6000 × 4000.
The row of modes under the switch works out each mode from 20,000 pixels, not the whole photo. On generated photos of colored blocks at 6000 × 4000 with 6 pins, all five took 0.41 seconds the first time on the pair (0.33 of them for Local, which learns from the photos then) and 0.12 seconds after a pin moved; at 3000 × 2000, 0.37 and 0.15. Leaving areas out makes the photos’ pixel pairs again when a stroke is finished: 0.58 seconds at 6000 × 4000 (0.26 without areas), of which drawing the areas took 0.04. The difference map is worked out at up to 2 megapixels and scaled up to the photo shown: 0.3 seconds for a photo shown at 2400 × 1600 px, kept until the photo, the matching or the areas change.
Measured on a Canon 5D Mark II RAW file (5634 × 3752) as the reference and a 3000 × 2000 JPEG as the processed photo: working out Blend’s table took 0.05 seconds for 1 pin, 0.14 for 5, 0.22 for 10 and 0.39 for 20, and applying it to the photos on screen about 0.01 seconds.
Saving a 5634 × 3752 processed photo as TIFF with 20 pins took 1.8 seconds, 0.7 of them for the matching.
With 6 pins, and a processed version of that photo made warmer, with more contrast and a vignette, switching to each mode and redrawing took 0.45 seconds (Blend), 0.60 (Spline), 0.53 (Matrix), 0.75 (Auto, including learning from the pixels) and 0.97 (Local). Saving at 5634 × 3752 took about 2.6 to 2.8 seconds in each mode but Local, which took 5.2 seconds. The whole photo’s average ΔE went from 2.9 to 1.2 (Blend), 0.9 (Spline), 0.7 (Matrix), 0.8 (Auto) and 0.7 (Local). Applying the matching to 2.2 megapixels on screen took 0.07 seconds in Auto and 0.31 seconds in Local.
image-to-image-color-matcher.ico beside the script: the split view of a small landscape, true colors on the left and warm-shifted on the right, with the bar and a pin. On Windows the app has its own taskbar ID, so it groups and pins under this icon rather than Python’s. Without the file the app runs with Tk’s icon. The icon is drawn by python image-to-image-color-matcher-generate-icons.py, which writes the .ico (16 to 256 pixels) and a PNG of each size in icon/; the sizes up to 32 pixels are drawn simpler, with a thicker bar and no pin, so they stay clear.
The app remembers its window size, position and maximized state, the widths of the Pairs panel and the right column, the reference and processed files or folders, Recursive, Reach, Pin size, the Auto-place number, Match lightness, the matching mode, Smoothing, what Matrix is fitted to, the arrangement (side by side or stacked), what the stacked view shows, whether photo 2 is shown matched, whether the difference map is on, and the brush’s size. They are saved when you close the app, and the first pair of the saved files or folders opens when it starts again.
A window saved on a monitor that is no longer connected opens at its default position instead.
The pins and the left-out areas are saved when you close the app too, in image-to-image-color-matcher-pins.json in the working folder, and put back when it starts. The Undo history and the ✓ marks of saved pairs are not kept.
Settings and temporary files are kept in a working folder, created if needed:
| System | Default working folder |
|---|---|
| Windows | %APPDATA%\image-to-image-color-matcher |
| macOS | ~/Library/Application Support/image-to-image-color-matcher |
| Linux and others | $XDG_CONFIG_HOME/image-to-image-color-matcher, or ~/.config/image-to-image-color-matcher |
Use --working-folder-path FOLDER to choose a different folder; if it can’t be created, the app stops with an error (exit status 2). If the default folder can’t be created, the app still opens but doesn’t save its settings.
Settings are stored as INI in image-to-image-color-matcher.ini, and temporary files go in the folder’s tmp folder. The file has a [window] section (size, position and maximized state), [panels] (pairs_width and side_width, the Pairs panel’s and the right column’s widths), [match] (reach, pin_size, lightness, mode, smoothing), [view] (arrangement, stacked, photo2, difference), [brush] (size), [paths] (reference, processed) and [pairs] (recursive). It can be edited by hand: a value that is missing, unreadable or out of range is replaced by its default, and a file that isn’t valid INI is ignored. Delete the file to go back to the defaults. If the settings can’t be saved when the app closes (a full disk, or a folder that is read-only), a message says so.
The pins file is JSON: {"format": 1, "pins": [[u, v], ...], "areas": [...]}, each pin’s place as shares of the photo’s width and height (0 to 1), and each brush stroke with its radius as a share of the width and its points. A pins file that isn’t valid JSON is kept as image-to-image-color-matcher-pins.damaged-<date and time>.json and the app starts without pins; one that holds something else is left as it is, and the pins aren’t saved over it that session. Delete the file to start with no pins. If the pins can’t be saved when the app closes, a message says so.
python image-to-image-color-matcher.py --selftest TRUE checks, without opening a window: the sRGB and CIELAB conversions (every gray level and 5000 random colors come back unchanged; red and white give their textbook values); patch averaging; that no pins change nothing; that one pin matches its color exactly, corrects the other colors too, and keeps black and white; that two pins each match their own color, with a color near one pin following that pin; that a longer reach blends more; that Match lightness off keeps lightness; that the lookup table agrees with the exact math; that a darkening pin keeps a gray ramp in order; that Spline matches 4 pins exactly, keeps black and white, and lets clashing pins give with Smoothing; that Matrix undoes a channel mix from 6 pins, and is a white balance with one pin that keeps black and white; that Matrix fitted to the whole photo undoes a channel mix without pins, keeps black and white, and ignores pins; that Auto learns the correction from the pixels without pins; that Auto and Matrix fitted to the whole photo leave out a local edit; that Local corrects a vignette better than Auto; that Auto and Local match each pin’s color; that Auto-place puts a pin in each part of the photo where it differs most, passing over a clipped spot; that a brush stroke leaves out what it covers and a Bring back stroke erases it; that Auto-place passes over a left-out area and places nothing where everything is left out; that the whole photo’s difference leaves out a left-out area; that the difference map is dim where the photos match, bright where they differ and gray where left out; scaling the reference to the processed photo’s size; an Adobe RGB photo converted back to its sRGB colors; a photo stored sideways turned upright; 16-bit grayscale (scaled and rounded to 8 bits); saving with camera data and an sRGB profile; settings (saved as INI and read back, fallbacks for bad values, a damaged file ignored); and pairing (two folders by name, and with recursive by relative path; two files whatever their names; a folder and a file not at all). It takes under a second (0.72 to 0.88 seconds in three runs, with Python’s start-up).python -m pytest drives the real app on a generated pair of photos: opening them with the reference scaled and the info line and warning; two folders paired by name, the Pairs list with its thumbnails and ⚠ marks, a click on a row, Prev and Next (under the Pairs heading, grayed out at the first and last pair, without wrapping around), the brush and Undo and Redo in line with the photo toolbar under them and the three panels starting at the same height, Recursive, and Rescan and Enter keeping the pair on show; a missing path, a folder and a file, and folders with no names in common; a pair that fails to open clearing the photos and saying why; clicking either photo placing a pin on both, with the matched colors checked on screen; one pin correcting the whole photo and more pins keeping to their own colors; dragging, right-clicking, Delete and the list’s × and rows; the stacked view with backslash, the showing button clicked again, the split bar and the M key; Match lightness; the sliders, and a dragged pin following the cursor with the quick preview when it rests and the full version when let go; the readout; saving at full size with camera data, and asking before overwriting a photo; at the minimum window size, side by side and stacked, with pins placed and the long notes of RAW files, and again in Matrix fitted to the whole photo: no button, switch, check box or label cut off, the toolbar’s groups and the brush’s row not crowding each other, Redo in line with ΔE map, the notes wrapping, the path fields at least 150 pixels wide with Recursive and then Rescan, set apart, on their row, Save matched on the Matching heading’s row, the pin list showing at least three rows and the readout on screen; settings saved as INI and restored (the folders, Recursive and the Pairs panel’s width too, with the first pair opened again), a hand-edited file, and a message when they can’t be saved; starting from the command line with two files and with two folders (each put in its path field, and the first pair opened), --recursive and --working-folder-path, and command-line mistakes (an unknown option, a file name without an option, an option given twice, a bare --selftest) stopping with exit status 2; the window icon, and the app’s own icon with every size drawn for its size; every style color being a true neutral gray in every state, with the scrollbar’s thumb lighter than its trough; Ctrl+S and the arrow keys not acting while typing or while a dialog has the grab, and the arrows not changing pairs on a slider; closing without leaving scheduled jobs behind; a second window beside the first showing its own photos; every matching mode chosen by its button and by the keys 1 to 5 without losing the pins, each matching the pinned spots on screen, with the mode’s own settings shown; Auto matching the whole photo without pins; Matrix fitted to the whole photo matching it without pins and ignoring those placed, and each mode’s note, the status line and the empty pin list saying whether the mode needs pins; Local correcting a vignette better than Auto, on screen and when saved; and Auto-place replacing the pins with one in each of the equal parts of the frame, on the regions that differ most but not on a clipped one, matching once after the last pin, keeping its number, refusing a number out of range, and placing on Enter without the digits switching the mode; the difference map (its button and D) bright where the processed photo differs and dark once matched, with its caption and scale; the row of modes showing each mode’s whole-photo ΔE, the lowest marked best, and following the pins; each pin’s label on the photos matching its row in the list; Undo and Redo (buttons, Ctrl+Z, Ctrl+Y, Ctrl+Shift+Z, with Caps Lock on too) through a pin placed, dragged and removed, Auto-place, Clear pins and a brush stroke, not while typing, with a new change clearing Redo; the ✓ on a saved pair and the count under the list; the brush (B, [ and ], Escape) painting a left-out area instead of placing pins, shown in yellow, left out of the Whole photo line, of Matrix fitted to the whole photo and of Auto-place, then erased by Bring back and cleared by Clear areas; the pins and areas saved on closing and put back at the next start, a damaged pins file kept under another name, and a file of another shape left alone; and the tips on the mode buttons.tests/test_place.py checks Auto-place’s search without a window: equal rectangles that tile the frame close to square, the largest difference found (also through the reduced copy), each pin inside its rectangle and off the frame’s edge, clipped spots and sharp edges passed over, lightness ignored with Match lightness off, a pin in the middle of an even area, and more pins than fit. The 46 tests (53 with those run once per view or per number of pins) take under two minutes (113.5 seconds at 100% and 114.5 at 150% display scaling) and pass at both. python -m pytest -k NAME runs the tests whose names match, and python -m pytest -m "not gui" leaves out those that open windows (all but Auto-place’s search). Without a working $DISPLAY, the tests start Xvfb (and openbox, if installed) themselves; to test at 150%, start Xvfb :98 -screen 0 3000x1800x24 -dpi 144 and run them with DISPLAY=:98. The tests find the script by looking for the .py file in the root folder that sets APP_ID; to test another copy, set APP_SCRIPT=/path/to/copy.py. They need pytest (pip install pytest).
pip install pillow numpy for the Python you start the app with.sudo apt install python3-tk; the python.org installers include it.pip install rawpy), or export the photo from darktable as an sRGB JPEG, PNG or TIFF.
a.jpg and a-edited.jpg) aren’t paired.

Leave a Reply