AI-Created Software

Last Updated: October 13, 2026

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.

Image to Image Color Matcher

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.

Contents

Requirements

  • Python 3.8 or newer, with Tkinter (included in the python.org installers; on Debian and Ubuntu: sudo apt install python3-tk).
  • Pillow and NumPy: pip install pillow numpy
  • Optional: rawpy to open camera RAW files: pip install rawpy

Command line

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.

Using the app

  1. Choose the photos in the path row at the top: for ■ Reference (the original photo) and □ Processed, File… chooses one photo and Folder… a folder of them; you can also type or paste a path in the field. Then click Rescan, at the right end of the row after the Recursive switch (or press Enter in a field). Two files make one pair, whatever their names; two folders pair the photos with the same name, listed in the Pairs panel on the left. If the photos differ in resolution, the reference is scaled to the processed photo’s size; the info line above the photos says so.
  2. Click a spot on either photo that should have the same color in both, preferably an even area such as a gray wall, sky or skin. A pin appears at that place on both photos, numbered, and photo 2 is corrected at once. One pin corrects the whole photo.
  3. Add more pins on other colors that still look wrong: shadows, highlights, greens, skin. Each pin corrects the colors close to its own, and its correction fades for colors further away, so a new pin doesn’t undo the others. Or let the app place them: type a number in Auto-place beside the Pins heading and click Place (or press Enter). The photo is cut into that many equal parts, and each gets a pin where the two photos differ most. See Auto-place.
  4. Try the other modes with the switch under Matching, or the keys 1 to 5. Under each button is the whole photo’s average ΔE from the reference in that mode, with best beside the lowest, so you can see which suits the photo without trying each; rest the pointer on a button for what the mode does and what it is best for.
    The line under the switch says how the mode uses pins: Needs pins, Pins optional or Doesn’t use pins. The pins stay put. See Matching modes.
  5. Compare. Switch to Stacked and press backslash to flip between the reference and photo 2, or choose Split and drag the bar. Photo 2: Processed | Matched (or the M key) shows photo 2 with or without the correction, and ΔE map (or D) shows where it still differs from the reference. See The difference map.
  6. Leave out any area you changed on purpose, such as a local edit, by painting over it with the brush (B), so the modes that learn from the whole photo don’t try to undo it. See Leaving areas out.
  7. Save matched… (Ctrl+S), on the Matching heading’s row in the right column, saves the corrected photo at full size, by default beside the processed photo as <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.

Pairs

  • The Pairs panel on the left lists every pair, numbered, by the name of its photos (with Recursive on, by their path inside the folder), with the number of pairs above. The open pair’s row is raised and bold, with small thumbnails of its reference and processed photo under it.
  • Click a row to open that pair, or use ◀ Prev and Next ▶ under the Pairs heading, or the left and right arrow keys. Prev is grayed out at the first pair and Next at the last.
    The counter beside the heading says which pair is open and how many there are.
  • ✓ marks a pair whose matched photo was saved this session, and the line under the list counts them.
  • ⚠ marks a pair whose photos differ in shape by more than 1%, found in the background from the files’ headers, without opening them; the line under the list counts them (“scanning…” while the check runs).
  • Recursive On | Off pairs the photos in sub-folders too, matched by their path inside the folder (sub/a.jpg with sub/a.jpg). Changing it pairs the folders again.
  • Rescan pairs the two paths again, for photos added since or new paths typed in, and keeps the open pair if it is still there.
  • A pair that can’t be opened clears the photos, and the canvas says which photo failed and why; the next pair opens as usual.

The right column

  • Matching:
    • Blend | Spline | Matrix | Auto | Local chooses the matching mode. Resting the pointer on a button shows what that mode does and what it is best for. Under each button is the whole photo’s average ΔE from the reference after matching in that mode, as on the Whole photo line, with best beside the lowest (or each of the lowest, within 0.05); “…” while it is worked out, one mode at a time, once you stop changing things. Below them, one line says how the mode uses pins, which for Matrix depends on Fit to. Only the settings the mode uses are shown.
    • Fit to: Pins | Whole photo (Matrix; default Pins) fits Matrix to the pins, or to every pixel of both photos. Fitted to the whole photo, Matrix needs no pins and doesn’t use those placed: they stay for the other modes, drawn with a dashed ring, and their rows still show the matched color under them.
    • Reach (Blend, Auto and Local; 5 to 100 ΔE, default 30) sets how far in color each pin’s correction extends. In Blend, it is how far before nearer pins take over: a short reach keeps each pin’s correction to colors very close to its own, and a long reach blends the corrections more evenly. In Auto and Local, it is how far a pin’s fine-tuning reaches.
    • Smoothing (Spline; 0 to 100, default 0) lets pins give a little instead of being matched exactly, for a smoother warp when pins disagree.
    • Pin size (1 to 51 pixels, default 11) is the square of pixels each pin samples, in pixels of the processed photo. Its outline follows the cursor over the photos.
    • Match lightness, when on (the default), matches lightness as well as hue and saturation, in every mode. Turn it off to correct only the color and keep photo 2’s own brightness and contrast.
    • Whole photo shows the average ΔE from the reference over 20,000 pixels spread across the photos, before and after matching in the current mode. It is updated once a change has settled, and is the quickest way to compare modes.
  • Pins: each pin is labeled on the photos with its number and its ΔE from the reference before and after matching (“3 12.1 → 0.4”). With no pins, the list says whether the mode needs them. One row per pin, with three swatches (the reference, processed and matched colors under the pin) and its ΔE from the reference before and after matching. ΔE is the CIELAB color difference: about 1 is just noticeable side by side, and below 2 is hard to see. Above the list is the average ΔE at the pins, or, when the mode doesn’t use them, a note saying so.
    Click a row to select its pin, or its × to remove it. Clear pins removes them all.
  • Auto-place (1 to 100, default 9) and Place, beside the Pins heading, replace the pins with that many, placed by the app where the photos differ most. Enter in the box does the same as Place. See Auto-place.
  • Readout: the colors under the cursor, in the square a pin would sample, in the reference, the processed and the matched photo: hex, RGB, CIELAB, and the ΔE from the reference.

Keyboard and mouse shortcuts

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.

The difference map

Δ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.

Leaving areas out

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:

  1. Choose Brush: Leave out at the top left of the photo panel, above Side by side, or press B. The left-out areas show on both photos in yellow while the brush is on, and the pointer becomes the brush’s circle.
  2. Drag over the area on either photo. Size sets the brush’s diameter on screen (4 to 300 pixels at 100% display scaling, remembered), and [ and ] make it smaller or larger; a single click paints one dab. Paint in as many strokes as you like.
  3. Bring back erases: drag over a left-out area to take it out of the left-out areas again. Clear areas brings back every area. Undo takes back a stroke.
  4. Choose Off, or press B or Escape, to place pins again.

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.

Matching modes

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:

  • Needs pins: Blend, Spline, and Matrix fitted to the pins. With no pins they leave the photo as it is; Auto-place can place them for you.
  • Pins optional: Auto and Local learn from every pixel and match the whole photo without pins. Pins fine-tune the colors near their own.
  • Doesn’t use pins: Matrix fitted to the whole photo learns from every pixel and ignores the 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.

How the matching works

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.

Blend

  • Every pixel of photo 2 takes a blend of the pins’ corrections, weighted by how close the pixel’s color is to the processed color under each pin. The weight falls off with the color distance d as exp(−d² / 2 reach²) / (1 + d²). The second part makes each pin’s own color get that pin’s correction almost exactly; the first, set by Reach, decides how quickly a pin gives way to nearer ones. The weights always add up to one, so a single pin corrects every color, and colors far from every pin take a blend of all the corrections.
  • Black and white stay put. Each pin’s correction also tapers with lightness, like a curve through the pin’s point: the lightness change fades to nothing at black and at white, and the color change fades to nothing at black and over the top 5 units of lightness.
    So blacks aren’t lifted or tinted, clipped whites stay white, and a single pin keeps a gray ramp in order.

Spline

  • A thin-plate spline in CIELAB: the smoothest warp that takes each pin’s processed color to its reference color, made of a straight-line (affine) part and a bend for each pin. Black and white are two extra fixed points.
  • Smoothing adds give to each pin. At 0 every pin is matched exactly; higher values let pins that disagree (nearly the same processed color, different reference colors) compromise instead of bending the warp sharply between them.
  • No color is shifted more than 1.5 times the largest pin correction, so the straight-line extension can’t run away far from the pins.

Matrix

  • The matrix works in linear light. With 1 or 2 pins it is a gain for each channel (a white balance). A channel the pins hold very little of (pins with almost no green, say) takes the overall gain instead. With 3 or more pins it is a full 3 × 3 matrix, fitted by least squares and pulled a little towards the white balance, so pins of similar colors can’t make it swing wildly.
  • A lightness curve, kept in order from black to white, then takes the lightness after the matrix to the reference’s at the pins, for tone differences a matrix can’t make. Each color’s saturation is kept as its lightness changes.
  • In the self-test, 6 pins undo a channel mix on 3000 other colors, from 9.2 to 2.2 ΔE. With only 3 pins on similar colors the fit can be poor, so use 4 or more pins on varied colors.
  • Fitted to the whole photo, the matrix is fitted by least squares to every pixel pair, from the same shrunk copies of the photos as Auto, then again without the pixels it fitted worst (a local edit, a moved edge). Pixels clipped in either photo are left out of the matrix, since their true color is lost, but not out of the lightness curve, which goes through the median reference lightness at 50 levels of lightness after the matrix. In the self-test, fitted to a photo of random colors under a channel mix, it took 3000 other colors from 9.2 to 0.63 ΔE (Auto, which fits every color separately, reached 1.8 on them), and with a strong edit over a tenth of the photo it left the rest matched to 0.33 ΔE. On a 6000 × 4000 photo the fit and its table took about 0.2 seconds, after the photos are shrunk.

Auto

  • Both photos are shrunk to about 200,000 pixels by averaging, which also evens out noise and sharpening. Each pixel pair is a sample: the processed color and the reference color at the same place.
  • The samples are gathered on a 33 × 33 × 33 grid of processed colors, averaged in each cell and smoothed across neighboring cells. Where a color is rare, the fit falls back to a straight-line (affine) fit of all the samples.
  • The fit is then done again without the samples it fitted worst.
    Those are pixels the processing changed for some reason other than their color: a local edit, noise, a moved edge. In the self-test, a strong edit over a tenth of the photo left the rest matched to 0.96 ΔE.
  • Pins fine-tune the result: each pin’s remaining difference is spread to colors near its own, fading over Reach, and solved so each pin’s own color lands on the reference. With no pins, Auto matches the whole photo by itself.

Local

  • 20 anchor points are spread evenly over the frame (5 across and 4 down for a 3:2 photo), corners included. Each anchor adds its own straight-line (affine) color correction on top of the Auto fit, fitted to the pixels around it, weighted by how near they are. Every pixel counts here, including those Auto left out.
  • Each pixel blends the corrections of the four anchors around it by its place in the frame, so the correction changes smoothly across the photo.
  • Pins fine-tune as in Auto. Their differences are measured at each pin’s place, so each pin still lands on the reference.
  • In the self-test, on random colors darkened towards the edges like a vignette, Local left 1.2 ΔE where Auto left 2.5.

Auto-place

Auto-place puts the pins for you:

  1. The photo is cut into as many rectangles as pins asked for, all of exactly the same area, in rows chosen so the rectangles come out close to square: 7 pins on a 3:2 photo are a row of 4 over a row of 3, not seven strips.
  2. In each rectangle, every square of the current Pin size is compared between the reference and the processed photo, as a pin would sample it, and the pin goes on the square where they differ most (the largest ΔE). With Match lightness off, only the hue and saturation part of the difference counts, since lightness won’t be matched anyway.
  3. Some squares make poor samples even where the difference is largest, so they are passed over: squares with anything clipped (a channel at 0 or 255) in either photo, whose true color is lost; squares near an edge or in fine texture, where a slight misalignment, sharpening or JPEG ringing makes the photos differ more than their colors do; and the outer 1% of the frame. If a rectangle has nothing else, its largest difference is used anyway. Squares touching a left-out area are never used, and a rectangle that is all left out gets no pin, so fewer pins than asked for may be placed; the status line says how many parts were left out.
  4. Differences within 1 ΔE of the largest count as equal, and the pin goes in the middle of their area, so a pin lands in the middle of an even patch of color rather than on its edge.
  5. A rectangle where the photos hardly differ still gets a pin: there it holds that color in place.

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.

Speed

  • Lookup tables. Except in Local, the correction depends only on a pixel’s color, so it is worked out once on a grid of 65 × 65 × 65 colors and applied to the photo through that table, which takes the same time however many pins there are. Local uses a 33-point table per anchor, and blends four of them for each pixel. The table agrees with the exact math to 0.03 ΔE on average, and within 0.83 ΔE for 99.9% of colors (measured by the self-test on 20,000 random colors under 5 pins). While a pin is being dragged, its marker follows the cursor without the matching being worked out, so dragging stays smooth (about 3 ms a move, against 130 ms before, and 1.2 seconds in Local, for a photo shown at 2400 × 1600 px). Once the pin rests for 0.15 seconds, a quick preview is drawn, from a coarser 33-point table applied at half the display size; letting go of the pin draws the full version at once, and so does a pause of 0.4 seconds after the preview. Changing a slider shows the quick preview right away. The pin list and the readout use the exact math.

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.

Photos, color profiles and saving

  • Photos with an embedded ICC profile (Adobe RGB, Display P3, ProPhoto and others) are converted to sRGB when opened, and turned upright from their EXIF orientation. 16-bit files are reduced to 8 bits. The info line says how each photo’s colors were read.
  • Camera RAW files are decoded with rawpy, using the camera’s white balance and a default rendering, not your darktable edit. Decoding the Canon 5D Mark II test file took 1.6 seconds.
  • Photos are shown and sampled at up to 6000 pixels on the long side.
  • If the photos’ shapes differ by more than 1%, a warning (⚠) appears under the info line: the reference is stretched to fit, so pins may not land on the same spot in both.
  • Saving works from the full-size processed file, embeds an sRGB profile and keeps the processed photo’s camera data (EXIF), with its orientation reset because the photo is saved upright.
    JPEG is saved at quality 95 without chroma subsampling, TIFF with LZW compression; PNG and WebP are also offered. The app asks before overwriting either of the photos you opened.
  • A pair that can’t be opened clears the photos (the Pairs list has already moved to it, so the old photos would sit under its name), and the canvas says which photo failed and why. The pins stay for the next pair.

Layout and display scaling

  • The path row is at the top, with Recursive and Rescan at its right end, and under it three panels, each starting right under the path row.
    The Pairs panel is on the left, with Prev and Next under its heading.
    The photos are in the middle, with the brush and Undo and Redo above their toolbar (the brush in line with Side by side, Undo and Redo with ΔE map, and Undo and Redo on a row of their own when the panel is narrow), so they move with the panel as it is resized. The matching settings, the pins and the readout in a column on the right, with Save matched beside the Matching heading. Drag the dividers to resize the Pairs panel and the right column; their widths are remembered.
  • Side by side shows the reference on the left (■, a filled square) and photo 2 on the right (□, a hollow square).
    Stacked shows one photo at a time, or both split by a bar. Each photo is named in its top-left corner, and in the split view beside the bar.
  • When the photo panel is too narrow for its toolbar on one row, the Stacked shows switch moves to a second row.
  • The pin list starts three rows tall at the smallest window size and grows with the window.
  • Everything is drawn to match the display scaling set in Windows or Linux: text, spacing, sliders, pins, lines and swatches are half as large again at 150% as at 100%. The photos themselves aren’t scaled: they are fitted to the space available. The window can’t be made smaller than 1340 × 800 (at 100%), the size at which every control still fits.
  • Icon. The window and the Windows taskbar show the app’s icon, 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 interface uses only neutral grays, so nothing next to the photos tints how you see them. The only colors come from the photos and what is drawn from them: the swatches in the pin list and the readout, the difference map and its scale, and the yellow of the left-out areas while the brush is on.

Settings and working folder

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.

Testing

  • 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).

Troubleshooting

  • “This app needs Pillow and NumPy”: run pip install pillow numpy for the Python you start the app with.
  • “Tkinter is not available”: on Debian and Ubuntu, sudo apt install python3-tk; the python.org installers include it.
  • A RAW file won’t open: install rawpy (pip install rawpy), or export the photo from darktable as an sRGB JPEG, PNG or TIFF.
  • “No photo has the same name on both sides”: the folders’ photos are paired by name, extension included; rename them to match, or turn Recursive on if they are in sub-folders.
  • A pair is missing from the list: a photo whose name is in one folder only isn’t listed.
  • A pinned spot doesn’t match (ΔE after stays high): two pins on nearly the same processed color but with different reference colors pull against each other; remove one of them. With Match lightness off, the lightness difference remains by design.
  • Colors without a pin look wrong: add a pin on them, or try Spline, Auto or Local. In Blend, colors far from every pin take a blend of all the pins’ corrections.
  • Matrix makes colors worse: with 1 or 2 pins it is a white balance, so put them on grays or whites; with 3 pins on similar colors the matrix is poorly fixed. Add pins on varied colors, or use another mode.
  • Which mode should I use? Compare the Whole photo line as you switch modes with the keys 1 to 5, and look at the photos in the Split view. Auto is a good first try, and needs no pins; use Local if the edges or one area stay off, and Matrix fitted to the whole photo for a plain white balance or color calibration change.
  • Clicking a photo paints yellow instead of placing a pin: the brush is on. Press B or Escape, or choose Brush: Off.
  • Pins from an earlier session are on the photo: pins are kept between sessions. Clear pins removes them (and Undo brings them back).
  • “…” stays under a mode: each mode is worked out once you stop changing things (0.3 seconds), one at a time, so a click never waits for more than one; the first time on a pair, Auto and Local learn from the photos.
  • Placing or moving pins changes nothing: Matrix is fitted to the whole photo, which doesn’t use pins (they have dashed rings). Choose Fit to: Pins, or another mode.
  • Local is slower: it applies four tables to every pixel, so drawing takes about four times as long and saving about twice as long as in the other modes.
  • An auto-placed pin is on a spot that shouldn’t match, such as an area edited on purpose: the largest difference isn’t always a color that should go back. Leave that area out and place again, or remove that pin or drag it to a better spot. A larger Pin size keeps pins off small details.
  • A warning says the photos have different shapes: the photos were cropped or rotated differently; pins may not land on the same spot in both. Export both at the same crop.

Limitations

  • The photos must be the same picture with the same framing: the app matches pixels at the same place, and doesn’t align photos that were cropped, rotated or shifted.
  • Auto-place puts each pin where the photos differ most, which may be an area you changed on purpose; it can’t tell a deliberate local edit from a color shift unless you leave it out.
  • Left-out areas only change what the matching learns from: the correction is still applied to them. They are painted by hand; the app doesn’t find edited areas by itself.
  • Only colors can be matched. Detail, sharpness and noise changes stay, and a pin can’t bring back highlights or shadows that the processing clipped.
  • The matching works on 8-bit sRGB, and the matched photo is saved in 8 bits.
  • There is no zoom: the photos are always fitted to the window, so a pin’s spot is chosen at that scale. Use a larger Pin size on photos with fine texture.
  • One set of pins and left-out areas is kept, for every pair alike; there is no set per pair. The Undo history isn’t kept between sessions.
  • Matrix fitted to the whole photo can only make one change for the whole photo, of the kind a white balance, a channel mixer and a tone curve make; anything else (a change of saturation, a hue shift in one color range, a vignette) needs another mode.
  • Auto and Local learn a color’s correction from where it appears in the photos; colors that don’t appear fall back to a straight-line fit, which may not suit them.
  • Local follows changes that spread across the frame smoothly (anchors a quarter to a third of the frame apart). It can’t follow a sharp-edged local edit, such as a brush mask, exactly.
  • Photos larger than 6000 pixels on the long side are reduced for display and sampling; saving works at full size.
  • Camera RAW files are decoded with the camera’s white balance and a default rendering, not with your darktable edit.
  • Files can’t be dragged onto the window, or pasted; use the File… and Folder… buttons, type the path, or use the command line.
  • Folders are paired by name only: photos with different names (a.jpg and a-edited.jpg) aren’t paired.
  • 32-bit float grayscale files are read as 0–255, or as 0–65535 if any value is above 255. A float file stored as 0–1 shows black.
(1 votes, average: 5.00 out of 5)

Leave a Reply

Your email address will not be published. Required fields are marked *

Notify me of followup comments via e-mail.