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 Color Checker

A single-file desktop app for checking the colors in a photo against physically based and measured references. Click the sky, foliage, skin, snow, clouds, pale rock or whitewater, and the app shows how far each one sits from what it should look like, then tells you which adjustments in darktable’s color calibration and color equalizer modules would bring it closer. It can also apply those corrections to the whole photo, show the result side by side with the original, and save it.

It never changes your original photo: a corrected version is only written when you save it, under a new name.

Contents

Requirements

  • Python 3.8 or newer
  • Pillow and NumPy
  • Tkinter, Python’s built-in interface toolkit
pip install pillow numpy

Tkinter comes with the python.org installers for Windows and macOS.
Elsewhere:

System Command
Debian, Ubuntu sudo apt install python3-tk
Fedora sudo dnf install python3-tkinter
macOS with Homebrew Python brew install python-tk

Optional extras:

pip install rawpy          # camera RAW files (CR2, CR3, NEF, ARW, DNG, RAF, ORF, RW2 and more)

Keep image-color-checker.ico in the same folder as the script: it is the app’s icon. Without it, the app runs with Tk’s standard icon.

Running the app

python image-color-checker.py                                # open the app
python image-color-checker.py --photo-file-path photo.jpg    # open the app with a photo loaded

See Command line for the options.

The app’s icon

The icon shows the measured blue sky, foliage and light skin patches and a neutral gray, the kind of colors the app checks, with a sample ring over them from 32 pixels up. It appears in the title bar, on the taskbar and in Alt+Tab, and on the app’s dialogs. It is the file image-color-checker.ico beside the script, which holds it in every size from 16 to 256 pixels; on Windows, the title bar and the taskbar each take the size they need from it.

Desktop shortcut on Windows. To start the app from a shortcut with its icon:

  1. Right-click the desktop and choose New > Shortcut.
  2. For the location, enter pythonw.exe followed by the script’s full path in quotes, for example pythonw.exe "D:\tools\image-color-checker.py".
    pythonw starts the app without a console window.
  3. Name the shortcut Image Color Checker and choose Finish.
  4. Right-click the shortcut, choose Properties > Change Icon…, and browse to image-color-checker.ico beside the script, for example D:\tools\image-color-checker.ico.

The shortcut can then be copied to the Start menu or pinned to the taskbar; see Limitations for how a pinned shortcut behaves.

Keyboard and mouse shortcuts

Keys Action
Ctrl+O (Cmd+O on macOS) Open a photo
Ctrl+V (Cmd+V on macOS) Paste an image from the clipboard
Ctrl+S (Cmd+S on macOS) Save the corrected photo
\ (backslash) Switch between the original and the corrected photo
Delete or Backspace Remove the selected sample (in the sample list)
Click a pin on the photo Select that sample (the pointer becomes a hand over a pin)
Right-click a pin on the photo (Ctrl+click on macOS also works) Remove that sample
Arrow keys Move the sampled position on the sky panorama

The Ctrl, Cmd and backslash shortcuts don’t act while you’re typing in a field, where Ctrl+V pastes text as usual, or while a dialog such as Export LUT is open. They work with Caps Lock on too.

Command line

python image-color-checker.py [options]
Option What it does
--photo-file-path PHOTO A photo to open when the app starts.
--layout auto, standard or wide The panel layout (see Layout and screen size). The default, auto, picks wide for wide windows.
--working-folder-path FOLDER The folder for the settings file and temporary files, created if needed. The default is shown by --help; see Settings and working folder.
--selftest TRUE Check the app’s calculations and exit (see below).
--help (or -h) Show the usage, and the default working folder on this computer, and exit.

The command line is read with Python’s argparse, and every option is named, so script launchers that read argparse options (such as Scriptary) find them all, and offer the values of --layout and --selftest as buttons; nothing is positional. Options can come in any order, written --name VALUE or --name=VALUE, in full (shortened names aren’t accepted), each at most once. Every option takes a value, so yes/no options are written TRUE or FALSE (also 1/0, YES/NO or ON/OFF). A mistake, such as an unknown option, a missing value, an option given twice or a folder that can’t be created, is reported with “Use –help for usage.” and exit status 2.
Started with pythonw.exe, which has no console, the app shows the mistake in a message box instead.

For example:

python image-color-checker.py --selftest TRUE
python image-color-checker.py --photo-file-path photo.jpg --layout wide
python image-color-checker.py --working-folder-path D:\tools\icc-data

The self-test verifies the color-difference math against published test data, checks the color conversions, compares the sky model with a measured reference, checks the correction, the LUT export and the darktable models against darktable 5.6.1’s output, converts an Adobe RGB test image to sRGB and saves it (it matched the published matrices within 0.4 levels), checks the icon file beside the script, and reports whether RAW and Tkinter support are available. It prints one line per check, ending in ok or FAIL, and exits with 0 when every check passes.

Checking a photo

  1. Open a photo with Open photo, Paste, or by passing a file on the command line with --photo-file-path.
  2. Click the sky. Choose the conditions you shot in, then mark on the sky panorama where you clicked relative to the sun.
  3. Click some foliage or skin, and anything that should be neutral: sunlit snow, the bright tops of clouds, pale rock, whitewater, concrete, a white shirt.
  4. Read the summary under All samples. If every sample leans the same way, the photo has an overall color cast: fix white balance first, then check again.
  5. Correct what’s left using the hue, saturation and luminance directions shown for each sample.

Click any row in the sample list, or a sample’s pin on the photo, to see and adjust that sample. The app guesses each sample’s material from its color, brightness and position in the frame; correct it with the Sky, Foliage, Skin and Should be neutral buttons if it guesses wrong. To remove a sample, right-click its pin on the photo, or select it in the list and press Remove.

Sample size sets how many pixels each click averages, from a single point up to 51 × 51. Larger sizes smooth out grain, leaf texture and noise. Choose evenly lit areas away from highlights and deep shadow.

References

Each sample is compared with a reference for its material.

Sky

Physical sky model. For daylight skies the reference comes from the Preetham analytic daylight model, which predicts sky color from:

  • Sun height: 10° to 90° above the horizon.
  • Haze: from very clear air to very hazy. This is the model’s turbidity setting, from 2 to 7.
  • Where you clicked: marked on a panorama of the modeled sky, with the sun in the center. Sky color changes a lot with direction, so this matters.

Presets cover clear midday, clear mid-morning, hazy mid-morning, hazy midday and late afternoon. The sliders fine-tune them. Sun height and haze apply to the whole photo; the sampled position is set for each sample.

As a check, the model’s clear-midday zenith is within ΔE 3.7 of the measured ColorChecker blue-sky patch.

Looks. The model is unreliable with the sun below about 10°, and it cannot describe cloud. For golden hour, civil twilight, blue hour, overcast and storm skies, the app offers illustrative gradients instead. You can pick which band of the gradient you sampled, or let the app choose the closest. These are plausible values, not measurements.

Foliage

The default reference is the foliage patch of the X-Rite ColorChecker Classic (CIELAB D50, post-2014 formulation), a chart color made to match typical green leaves in neutral daylight. It is the right target for accurate color.

Sixteen illustrative looks cover specific plants, seasons and light, such as sunlit meadow grass, evergreen needles, early autumn and golden-hour foliage. They are plausible values, not measurements. Real foliage varies a lot by species, so treat differences under about ΔE 5 as normal.

Skin

Two measured references come from the X-Rite ColorChecker Classic: the light skin and dark skin patches (CIELAB D50, post-2014 formulation), chart colors made to match typical light and dark skin in neutral daylight.

Typical skin tones cover nine ancestry groups: Northern European, Southern European, Middle Eastern, East Asian, Southeast Asian, South Asian, Latin American, African American and Sub-Saharan African. Each comes at four tan levels: no tan, light tan, medium tan and heavy tan. Each step darkens the skin and makes it more yellow. Deeper skin changes less, so its steps are smaller. Every tone shows its individual typology angle (ITA°) and ITA skin category, from very light to dark.

These tones are illustrative typical values within the ranges reported by skin colorimetry studies, not measurements. Skin varies more within any group than between groups, so pick the tone that looks closest to the person, not the one that matches their background. The picker lists the three closest tones, lightness included. The first skin sample starts on the closest reference; after you choose one, later skin samples use your choice.

Makeup, flushing and the light all shift skin color, so treat differences under about ΔE 5 as normal and trust the direction of the hue more than the exact numbers. Skin sits close to the skin-tone line on the vectorscope.

Natural neutrals

Neutral surfaces are the most direct check of white balance: any color they carry is a cast. Choose what you sampled.

Reference What it is Keep in mind
Neutral surface A gray card, concrete, or anything else that should have no color
Sunlit snow Fresh snow in direct sun reflects roughly 85 to 90% of visible light, almost equally at every wavelength, so it should be a bright neutral white, about L* 94 in a normal exposure Only snow in direct sun is neutral. Shaded snow is lit by blue skylight and should look blue; old or dirty snow is darker and slightly warm
Sunlit cloud The bright tops of daytime cumulus scatter all wavelengths about equally Near sunrise and sunset clouds take on the color of the light.
Shaded cloud bases can lean slightly blue
Pale rock Limestone, granite and pale weathered stone Rarely perfectly neutral; a slight warm lean is normal. When rock disagrees with snow or clouds, trust the snow or clouds
Whitewater Foam, breaking surf and rapids: the bubbles scatter all wavelengths about equally Like clouds, it takes on the color of the light at low sun and leans blue in shade. Foam on peaty or muddy water can be tinted yellow or brown

Bright, weakly colored areas are usually recognized as snow (low in the frame) or cloud (high in the frame) automatically, even under a warm cast. A cloud with a strong blue cast may be taken for sky; switch it to Should be neutral.

With Compare color only turned off, sunlit snow is also checked for brightness against L* 94. This catches the common metering error that renders snow gray.

Reading the results

ΔE 2000

Each comparison is scored with CIEDE2000, the standard measure of how different two colors look.

ΔE 2000 Meaning
under 1 Indistinguishable
1 to 2 Visible only side by side
2 to 5 Noticeable, within normal natural variation
5 to 10 Obvious mismatch
over 10 A different color

Correction directions

Each sample lists the adjustments that would move it toward its reference:

  • Hue, Saturation, Luminance: how far to rotate the hue, add or remove chroma, or brighten or darken the sample, for the hue, saturation and brightness tabs of darktable’s color equalizer, naming the node or nodes that act on the sample’s color (for example “red and orange” for skin).
  • White balance: warmer or cooler (blue to yellow) and toward green or magenta, for the CAT tab of darktable’s color calibration, shown for neutral samples or when a shift is large.
  • Brightness: exposure or tone equalizer direction, shown when brightness is being compared.

Amounts are in Lab units, not slider units. For slider values, use darktable… (see Settings for darktable).

Compare color only

On by default. How bright the sky or a leaf records depends on your exposure and on shade, so each reference is shown at your sample’s brightness and only color is compared. Turn it off to include brightness. That is most useful for sunlit snow, which has a predictable brightness.

The summary

The summary under All samples looks at every sample together:

  • All samples lean the same way: the photo has an overall cast. Fix white balance first.
  • A single neutral sample is off: its cast is shown directly.
  • Samples are off in different directions: white balance alone won’t fix them; correct each color range in the color equalizer, or with a drawn mask.
  • Only one kind of sample: add another, such as a neutral, to tell an overall cast from a color-specific shift.

Clipped highlights

If any channel of a sample is at its maximum, the true color there is lost. The sample is flagged as clipped and left out of the summary.
Recover highlights, or sample a slightly darker area.

Scope and readout

The scope shows every pixel of the photo as a colored cloud, either as a BT.709 vectorscope or as a CIELAB a*b* plot, at 1×, 2× or 4× zoom.
Your samples appear as numbered rings, with a dashed line to each one’s reference; a sample beyond the outer ring is drawn at its edge as a triangle pointing outward, and the caption under the scope says so.
Small squares mark the measured skin, foliage and blue-sky patches. The dashed diagonal on the vectorscope is the skin-tone line. The scope is 300 pixels across (at 100% display scaling) when there’s room. In a short window it shrinks, down to 200 pixels, so the sample list below keeps at least three rows in view; in a tall one it grows to fill the width of its column once the list has room for six rows. Widen the right column to enlarge it.

The readout shows the color under the cursor, or of the selected sample, as hex, RGB, RGB %, HSV, L*a*b* D50, LCh and vectorscope position. RGB %, HSV, Lab and LCh are among the formats darktable’s color picker shows; Lab is D50 in both.

Correcting the photo

The app can apply its corrections to the whole photo, so that each sample matches its reference.

The toolbar above the photo switches between four views:

  • Original: your photo as it is.
  • Corrected: the whole photo with the correction applied.
  • Split: the original on the left and the corrected photo on the right of a vertical divider. Drag the divider across the photo to compare any part of it; the pointer becomes a double arrow over it.
  • Changes: where the correction changes the photo, and by how much. The photo is shown in dim gray and tinted amber by how far each pixel moves: not at all below ΔE 1, full amber at 10 or more. A legend gives the largest change and how much of the photo changes by more than 2. Use it to spot a correction spilling into areas you didn’t sample.

To flip between before and after without moving the pointer, click Corrected repeatedly: each click switches between the corrected photo and the original. Split and Changes work the same way. You can also press \ (backslash).

How it corrects. Choose one of two corrections next to the views:

  • White balance only adjusts the red, green and blue channels of the whole photo, like the white balance in color calibration. It is fitted to all your samples at once, weighted by how much each reference is trusted (below), with neutral samples counting double. With one sample, that sample matches its reference exactly. With several, it is the best compromise a single white balance can reach.
  • White balance + colors then corrects what white balance could not, such as sky and foliage that are off in different directions. Each sample’s remaining difference is applied to its own range of colors and fades out with distance from it, like a color equalizer adjustment. Similar colors move with the sample, and unrelated colors and grays are left alone. It comes in three strengths:
    • Balanced (the default) moves each sample most of the way to its reference, but treats a small leftover difference as normal variation and leaves it. Samples with more trusted references are matched more closely. When two samples of similar color want opposite corrections, it settles on a modest compromise instead of distorting the colors between them.
    • Gentle does the same with more room for variation.
      Use it when the references are only a rough guide for your scene.
    • Exact puts every sample on its reference. Use it when you trust the references, and check the colors between samples, since conflicting samples can then push nearby colors further than either sample moved.

How much each reference is trusted: measured ColorChecker patches and neutral surfaces fully, the sky model a little less, and illustrative looks and typical skin tones half as much.
Neutral samples also count double, so a gray card or sunlit snow is matched most closely of all.

The color step judges how similar two colors are mostly by hue and saturation, and by lightness at half weight. Correcting a bright sky therefore changes a dark blue shadow much less. Each sample’s reach adapts to your other samples: a sample on its own reaches further, and samples close together in color reach less, so they don’t fight.

Amount sets how much of the correction to apply, from 0% (the original photo) to 100% (each sample on its reference).
References are typical values, so a partial correction often looks more natural; the app remembers the amount and the kind of correction between sessions. Compare, the preview and the saved photo all use the amount.

While you drag Amount, or the sky’s sun height and haze (which change the correction too), the photo follows at once with a quick, lower-resolution preview, and shows the full-resolution result once the slider has rested for 0.4 seconds.

The correction uses the same samples as the summary, leaving out any too far from their reference to compare and any that are clipped; the toolbar says how many it uses. With Compare color only turned on, lightness is left alone and only color changes. Turn it off to correct brightness too, for example to lift gray snow toward L* 94.

Conflicting samples. Sometimes two samples have similar colors but need very different corrections, for example snow in shade marked as neutral next to a blue sky. Any correction by color moves similar colors alike, so it cannot give both. When the corrections differ by more than three quarters of the difference between the colors, and by at least 2, a bold warning under the correction’s summary, just above the photo, says so, both samples get a ⚠ in the sample list, and Compare explains the conflict for each. Usually one sample has the wrong material or reference: snow in shade is lit by blue skylight and is not neutral. Fix or remove it.

When both samples are right, for example sky and a blue lake below it that need different corrections, the app can separate conflicting samples by position. A checkbox for this appears beside the correction’s summary when there is a conflict; it is on by default and remembered. Each conflicting sample then owns the part of the photo nearer to it than to the other conflicting samples, and its correction applies everywhere except in the parts owned by the samples it conflicts with. The parts blend into each other across a narrow band.
Samples without conflicts, and typed values, which have no position, still apply everywhere by color alone, and samples that agree, such as two sky samples, cover the same parts, so large areas are corrected evenly. The Changes view outlines the parts in white.

The parts are drawn by nearness alone, so a border can cut through an object: for example, a corner of the sky that is nearer to a snow sample than to the sky sample. Add another sample on that object beyond the border, and the parts follow it. With the checkbox off, the correction compromises between conflicting samples instead.

For the selected sample, Compare shows its color in the corrected photo and its ΔE from the reference: normally 0 to 0.5 with Exact, and up to about 2 with Balanced.

Samples and the readout always read the original photo, even while the corrected one is shown.

Saving. Save corrected… opens in the photo’s own folder with the name <photo>-corrected and the same file type (TIFF for RAW files), and can save JPEG (quality 95), PNG, TIFF or WebP. The correction is applied to the full-resolution photo, not the preview, so large photos take a few seconds; the status bar shows progress. The saved photo is 8-bit sRGB with an embedded sRGB profile, and keeps the original’s EXIF data where the format allows. Saving over the original photo asks for confirmation first.

Exporting a LUT. Export LUT… saves the correction as a .cube 3D lookup table, so you can apply it in darktable to the original photo at its full bit depth and gamut, instead of using the 8-bit corrected file. It opens in the photo’s folder with the name <photo>-correction.cube, and records in its header what it was made from. The LUT carries the current correction at the current Amount, as a table over sRGB values. Before saving, the app asks for its size, and shows how closely each size reproduces the correction on your photo:

  • 33 points (the default) is about 1 MB. It matches the correction to within about ΔE 0.1 on typical colors, and to a mean of about ΔE 0.4 even for strong corrections near gray.
  • 65 points is several times as precise, about ΔE 0.1 even for strong corrections, but the file is about 7.4 MB. Its default name ends in -65.cube.

The app remembers your choice.

To apply it in darktable:

  1. Save the LUT inside darktable’s LUT 3D root folder (Preferences › processing › LUT 3D root folder; changing it needs a restart). darktable will not open a LUT from outside that folder.
  2. In the darkroom, add the LUT 3D module, click its file button and choose the LUT.
  3. Leave application color space at sRGB.

darktable stores only the LUT’s path in the edit, so keep the file where it is for as long as the edit uses it.

A LUT knows only colors, not where they are in the photo, so it cannot separate conflicting samples by position. When separation is on, the export dialog says so, and the LUT uses the compromise between them instead, as with separation turned off; its header notes this too.

Treat the corrected photo as a starting point for your own edit rather than the final word.

Settings for darktable

Most corrections are made once, for one photo, so rather than a file to import and later remove, darktable… (next to Export LUT) translates the correction into slider settings for two modules in darktable 5.6: color calibration for the white balance, and color equalizer for individual colors. Both work on the scene-referred data, before tone mapping, which is where darktable recommends making these changes.

The window stays open and updates as you add samples, change references, or change the correction or its amount:

  1. Color calibration, CAT tab.
    • With a neutral sample (snow, cloud, pale rock, whitewater or gray), click the color picker to the right of the illuminant color swatch and draw a box over that sample’s area. The window says which sample and where it is. darktable sets the illuminant to custom and makes the area exactly neutral. If the area color mapping section’s header says “active”, reset its target first.
    • Without one, set the illuminant to custom (it keeps the same light) and type its current hue and chroma into the window. It gives the new values.
  2. Exposure, when Compare color only is off and the correction changes brightness.
  3. Color equalizer. A table of changes for the hue, saturation and brightness tabs, for each node that needs one. Add them to the sliders’ current values; all start at 0, and right-clicking a slider lets you type a value. Copy as text puts the settings on the clipboard.

Then export a JPEG from darktable and open it in the app. When the new photo has the same shape as the last one, the app offers to keep the samples at the same places, with their references, and measures them again. The window then shows further changes to add to the ones you made, and prefills the illuminant’s hue and chroma with the values it last suggested. One or two rounds are usually enough.

Tone mapping. Choose the tone mapper you use in darktable at the top of the window: AgX, sigmoid or other. It starts on AgX, and remembers your choice. The settings are worked out through models of darktable’s own modules: dt UCS 22 and the color equalizer’s node curves, color calibration’s CAT16 adaptation, and the tone mapper. With AgX or sigmoid, the app works out what each sample was before tone mapping and solves the settings there. AgX is modeled at the AgX workflow’s scene-referred default, with Blender-like primaries, and sigmoid at its default settings. Changes to the tone mapper’s settings (other than exposure) make the first round less exact; the second round takes care of it. With other (filmic, for example), the app treats the modules as if they acted on the exported photo, which gets most of the way in the first round and needs another to finish.

What to expect. Tested with darktable-cli 5.6.1 on a photo with a strong warm cast (samples 4.4 to 6.0 ΔE from their references), using AgX at its scene-referred default and the Balanced correction: after the first round, foliage, skin and sky were within ΔE 0.2 to 0.9 of the correction, and after the second within 0.6. Picking a snow sample made it exactly neutral. Sigmoid gave much the same, and a cool cast too. Without the AgX model, the first round had left skin 3.2 from the correction. The window gives the expected result for the first round.

Limits.

  • The color equalizer changes colors by hue only. Two samples of similar hue that need different changes, such as light and dark skin, can only be met halfway. The window names any sample that can’t get within ΔE 1.5 of the correction; a drawn mask can separate them.
  • Like a LUT, the modules can’t separate conflicting samples by position, so the settings use the compromise between them.
  • The model assumes the color equalizer’s other settings are at their defaults, and that no module between it and tone mapping (such as color balance rgb) changes the samples’ colors much. Another round takes care of small differences.

Files and color management

Format Support
JPEG, PNG, TIFF, WebP, BMP Built in
Camera RAW With rawpy
Clipboard images Built in; on Linux needs xclip or wl-clipboard

Embedded color profiles are honored. Photos in Adobe RGB, Display P3, ProPhoto RGB or CMYK are converted to sRGB with LittleCMS before any reading is taken, so the numbers match what darktable shows. In testing, a Display P3 image read within ΔE 0.4 of its true colors; ignoring its profile would have produced errors up to ΔE 3.4. Files without a profile are treated as sRGB. The line above the photo shows the photo’s name and size and which of these applied; it stays there while the status bar at the bottom reports other things.

RAW files are decoded with the camera’s white balance and a default rendering, not your edit. Use them as a starting point; to check an edit, export it as a JPEG or TIFF.

Photos larger than 6000 pixels on the long side are downscaled for sampling.

Exporting results

Export CSV… saves one row per sample with:

  • position, sample size and material
  • the sample’s hex, RGB and Lab values
  • the reference’s kind, name, detail, hex and Lab values
  • ΔE 2000, ΔL*, Δa*, Δb* and hue difference
  • the correction advice as text
  • whether brightness was ignored

The file is UTF-8 with a byte-order mark and Windows line ends, so Excel opens it with symbols such as ° and ΔE intact. It is written through a temporary file, so an export that fails never leaves half a file in place of an earlier one.

Layout and screen size

The app arranges its panels in one of two layouts, chosen from the window’s width and shape:

  • Standard: the photo on top, with Compare and the reference picker side by side below it, and the scope, readout and sample list in a column on the right. Used for most window sizes.
  • Wide: three columns, with the photo at full height in the center: Compare above the reference picker on the left, and the scope, readout and sample list on the right. The two side columns start at the same width, so the photo sits in the middle of the window. Used when the window is at least 1800 pixels wide (at 100% display scaling) and at least 1.6 times as wide as it is tall, for example maximized on a 4K monitor at 100% to 200% scaling, or on a 1920 × 1080 or 2560 × 1440 monitor at 100%. On a 4K monitor at 150% scaling, it shows a landscape photo about twice as large as the standard layout.

The layout switches as you resize or maximize the window. It switches back to standard only below 1700 pixels wide or 1.5 times as wide as tall, so a window near the threshold doesn’t flip back and forth. To always use one layout, start the app with --layout standard or --layout wide.

Dividers, each shown as a faint line, can be dragged to resize the panels; the pointer changes to a double arrow over each one. Each panel keeps a minimum size, so none can be dragged shut. When the window is resized, the photo takes up the change and the other columns keep their width. Each layout remembers its own divider positions.

The Export LUT dialog and the darktable settings window open where you last closed them, also the next time the app starts. A place on a monitor that is no longer connected is ignored.

In a narrow reference picker, the reference buttons move into fewer columns so their names are never cut off. When the picker is taller than its panel, as the foliage and skin pickers can be in a small window, a scrollbar appears beside it, and the mouse wheel scrolls it while the pointer is over it. The directions in Compare get a scrollbar in the same way when they are longer than their panel. When the photo’s panel is narrow, the correction’s settings move to a second row of its toolbar, and the Save, Export LUT and darktable buttons to a row of their own if they don’t fit beside the views.

Smallest window. The window can be made as small as 1200 × 890 pixels (at 100% display scaling). Every button, label and reference fits at that size, at 100% and at 150%, and the sample list keeps at least three rows in view.

High-resolution screens. Everything is drawn to match the display scaling set in Windows or Linux, not only the text: dividers, pins, swatches, list rows, the scope, spacing and the minimum panel sizes. At 150% they are all half as large again as at 100%. On macOS the system handles this itself.

Settings and working folder

The app remembers its window size, position and maximized state, where you left each divider in each layout, the sample size, the scope’s mode and zoom, the correction’s mode, Amount, whether conflicting samples are separated by position, the LUT size, and the tone mapper chosen in the darktable window. They are saved when you close the app and restored the next time you open it. A window saved on a monitor that is no longer connected opens at its default position instead.

Settings and temporary files are kept in a working folder, created if needed:

System Default working folder
Windows %APPDATA%\image-color-checker
macOS ~/Library/Application Support/image-color-checker
Linux and others $XDG_CONFIG_HOME/image-color-checker, or ~/.config/image-color-checker

Use --working-folder-path FOLDER to choose a different folder, for example to keep the app and its settings together on a USB drive, or to keep separate setups:

python image-color-checker.py --working-folder-path D:\tools\icc-data

Settings are stored in image-color-checker.ini, a plain text file, and temporary files go in the folder’s tmp folder. A value in the settings file that can’t be used, for example after editing it by hand, is ignored and its default used instead; a settings file that can’t be read at all is ignored. Delete image-color-checker.ini to go back to the defaults. If the settings can’t be saved when the app closes, for example because the folder was removed or is read-only, a message says so and the app closes anyway.

Settings from earlier versions. Earlier versions kept their settings in settings.json in the same folder, and took the folder as --workdir. The first time this version runs in a folder with no image-color-checker.ini, it carries the settings over from settings.json; after that, settings.json is no longer read and can be deleted.
The default folder hasn’t changed. A shortcut or script that passes --workdir needs --working-folder-path instead; the app says so rather than starting.

Testing

The app checks itself in three ways. None of them touches your photos or your settings.

  • Self-test.
    python image-color-checker.py --selftest TRUE checks the app’s calculations and exits (see Command line for what it covers). It takes under a second, prints one line per check ending in ok or FAIL, and exits with 0 when every check passes. Run it first if the app seems to give wrong numbers.
  • Tests. python -m pytest, run from the app’s folder, runs 37 tests (it needs pytest: pip install pytest). Twenty-eight open the real app on a generated test photo and check sampling, the views, saving, the quick preview while Amount, sun height or haze move, conflicts, LUT export, the darktable window, that the LUT dialog and the darktable window open where they were closed, the icon, settings (saved, hand-edited, carried over from settings.json, and a failed save shown in a message box), starting from the command line, a photo that fails to open, keyboard shortcuts, the interface’s neutral grays, that buttons, fields, drop-downs and switches are all one height, that closing leaves nothing running, the main buttons clicked as a user would (with the CSV export’s byte-order mark and line ends), the switch between layouts and the dividers’ minimum sizes, that no button, label or reference is cut off or out of reach in the smallest windows and the sample list keeps three rows in view, that the picker’s images don’t pile up, that text drawn on the photo, the sky panorama and the scope stays inside them, and that the scope marks off-scale samples by shape. Eight need no window: the self-test, the named options, command-line mistakes and --help, choices written out, the icon file, and reading old settings. One more opens a camera RAW file, when RAW_TEST_FILE names one (it needs rawpy); otherwise it is skipped. They take about 31 seconds at 100% display scaling and 36 at 150%, without a RAW file. At 100% they all pass. At 150% under Xvfb with a Linux container’s fonts, test_picker_scrolls fails because the foliage picker fits its pane there and needs no scrollbar; the others pass. The window tests need a display; on Linux without one, the tests start Xvfb themselves, and without either they are skipped.
    python -m pytest -m "not gui" runs only the eight that open no window, python -m pytest -k amount only the tests whose names contain “amount”, and APP_SCRIPT=path/to/copy.py tests another copy of the script.
  • Icon.
    python tools/make_icon.py --check TRUE checks that image-color-checker.ico matches the drawing in tools/make_icon.py. The tests run this check too.

tests/darktable/ checks the darktable models against darktable-cli 5.6 itself and runs the darktable workflow end to end. It needs a darktable 5.6 AppImage; see tests/darktable/dtx.py for getting one.

Troubleshooting

Problem What to do
Nothing happens when the app is started from a shortcut with pythonw.exe Run the same command with python.exe in a command prompt, which shows any error. A mistake in the shortcut’s options is shown in a message box.
“This app needs Pillow and NumPy” pip install pillow numpy, using the same Python that runs the app.
“Tkinter is not available” Install Tkinter for that Python (see Requirements).
“Error: unrecognized arguments: –workdir” The option is now --working-folder-path (see Settings and working folder).
“Error: argument –selftest: expected one argument” Every option takes a value: --selftest TRUE.
“Opening camera RAW files needs rawpy” pip install rawpy, or export the photo from darktable as an sRGB JPEG, PNG or TIFF.
A HEIC or HEIF photo won’t open The app doesn’t read HEIC. Export the photo as a JPEG, PNG or TIFF.
Paste says it could not read the clipboard, on Linux Install xclip (X11) or wl-clipboard (Wayland).
The window has Tk’s standard icon instead of the app’s Keep image-color-checker.ico in the same folder as the script.
“Could not save the settings” when closing The working folder was removed, or can’t be written to. Start the app with --working-folder-path set to a folder you can write to.
The window or its dividers are laid out oddly Close the app and delete image-color-checker.ini in the working folder to go back to the defaults. --help shows where the folder is.
A photo won’t open, and the previous one stays on screen That’s deliberate, so its samples aren’t lost. The message says why the new photo couldn’t be read.
The numbers differ from darktable’s color picker Check the line above the photo: it says whether the photo’s color profile was converted. Export from darktable in sRGB, and compare in darktable’s Lab readout, which uses the same D50 white.
The darktable settings don’t land where the window said Check that the tone mapper chosen in the window matches darktable’s, and that no module between the color equalizer and tone mapping changes colors. Then do a second round (see Settings for darktable).
The self-test reports FAIL Note the failing lines and the Python, Pillow and NumPy versions (pip list); the app’s numbers may be wrong on that setup.

Limitations

  • References are typical values, not ground truth for your scene. Real skies, plants and rocks vary. Use the app to find clear errors, not to force every color onto a reference.
  • Low sun and cloud. The sky model is not valid with the sun below about 10° and is least reliable within about 10° of the horizon in haze. Use the looks there.
  • Illustrative references are not measurements. The foliage looks, sky looks and typical skin tones are plausible values only.
  • Skin varies from person to person. No reference fits everyone in a group. Choose the tone that looks closest to the person.
  • Sunlit only. The snow and cloud references assume direct daylight. Shade and sunset light legitimately change their color.
  • No drag-and-drop. Tkinter doesn’t support it without an extra library; use Open photo or Paste.
  • Pinned taskbar shortcut on Windows. The app gives itself its own taskbar identity, so its button shows its icon rather than Python’s. As a result, a shortcut pinned to the taskbar starts the app, but the running app appears as a separate button beside the pin.
    Pinning the running app’s own button pins Python instead, so pin a shortcut.
  • Dark interface only. Every interface color is a true neutral gray so it doesn’t bias your color judgement.
  • Samples are session only. The window layout is saved, but samples are not; use Export CSV to keep them. Opening a photo of the same shape can keep them, for checking a new export of the same photo.
  • Corrected photos are 8-bit sRGB. A 16-bit TIFF, a wide-gamut photo or a RAW file is converted to 8-bit sRGB before it is corrected, so edit from your original when you need its full depth or gamut.

Customizing the references

The reference data sits near the top of the file in plain Python lists.

Sky model presets use sun height in degrees and turbidity (2 to 7):

SkyPreset("hazy-morning", "Hazy mid-morning", 35, 5.5),

Sky looks take four (label, hex) stops from top to bottom, plus their positions in percent:

("golden", "Golden hour", "Sun about 5° high",
 [("Zenith", "#28418C"), ("Upper sky", "#7864AA"), ("Horizon", "#FF9146"), ("Solar glow", "#FFD28C")], (0, 45, 85, 100)),

Foliage looks take (name, condition, hex):

("Fresh spring leaf", "Early spring, moist air", "#6EAA46"),

Natural neutrals take an id, a name, a description, an expected L* in a normal exposure (or None if it varies too much to predict), and a caveat shown with every comparison:

NaturalNeutral("snow", "Sunlit snow", "Fresh snow in direct sun ...", 94.0,
               "Only snow in direct sun is neutral. ..."),

Skin tones take an id, a name and the L*, a*, b* of untanned skin (CIELAB D50). The four tan levels are worked out from it using TAN_STEP, the change in L*, a* and b* for each level:

("east-asian", "East Asian", 63.0, 11.0, 18.5),

Anything added appears in the reference picker automatically. Give every entry a unique id.

Technical notes

  • Color space. Readings are sRGB. Lab values are CIELAB D50, computed through XYZ with Bradford adaptation from D65, matching darktable’s Lab readout and the ColorChecker reference data.
  • Color difference. CIEDE2000 with kL = kC = kH = 1, verified against the Sharma, Wu and Dalal (2005) test data.
  • Sky model. Preetham, Shirley and Smits (1999), A Practical Analytic Model for Daylight. The model’s chromaticity is converted to sRGB; with brightness ignored, it is rendered at the sample’s luminance, otherwise at an exposure that places the zenith near L* 51.
  • Brightness matching. References keep their chromaticity and take on the sample’s luminance, which is what the same surface looks like with more or less light on it.
  • Vectorscope. BT.709 luma coefficients on gamma-encoded values; the skin-tone line is drawn at 123°.
  • Icon. tools/make_icon.py draws it with Pillow from the app’s reference colors, at four times each size and reduced, in 9 sizes from 16 to 256 pixels, and saves them all in image-color-checker.ico (about 37 KB); --check TRUE checks the saved file against the drawing. On Windows the app sets the file as the icon of every window with Tk’s iconbitmap, so the title bar and the taskbar each take the size they need; elsewhere it hands every size to Tk’s iconphoto. The 16 to 24 pixel sizes leave out the sample ring and use larger patches, so the colors stay apart; from 32 pixels up, the ring’s white line is at least 1.5 pixels wide, so it stays white. On Windows, the app sets its own AppUserModelID (infolific.image-color-checker) before opening its window; without one, Windows groups it with Python on the taskbar and shows Python’s icon.
  • Shared code. Opening and saving photos, the working folder and settings, the command line, startup on Windows, popup windows that remember their place and the dark theme are exact copies of shared code from the owner’s app templates (infolific/app-templates, recorded in templates.ini), marked in the script with copied from lines.
  • Display scaling. Sizes are designed in pixels at 96 dpi and multiplied by the screen’s scaling, read from Tk (winfo fpixels 1i ÷ 96, rounded to a quarter and never below 1). Divider positions are saved as fractions or in 96-dpi pixels, so they carry over when the scaling changes.
  • Sampling. Samples average the gamma-encoded RGB values of the patch.
  • darktable settings. The models follow darktable 5.6’s source. The color equalizer converts to dt UCS 22 hue, saturation and brightness (with its default white level of 2), and interpolates its eight nodes, at 20° + k × 45° of dt UCS hue, with its periodic radial basis functions. It adds the hue curve to the hue, multiplies saturation by 1 + 2 (curve − 1), and brightness by 1 + 8 × saturation × (curve − 1). Saturation and brightness changes fade out on grays by a logistic function of the RGB saturation in the Rec. 2020 working space, around its default threshold of 6%. Color calibration scales CAT16 LMS by D50 over the illuminant. Sigmoid uses its generalized log-logistic curve at contrast 1.5 per channel, with hue preservation. AgX gamut-compresses, insets and rotates the Rec. 2020 primaries (as darktable’s profile holds them, adapted to D50), log-encodes from −10 to +6.5 EV around 0.18, applies its toe and shoulder curve (contrast 3, powers 1.5 and 3.3, gamma 2.2), restores 60% of the hue from before the curve, and outsets the primaries without rotating them back. Both are inverted by Newton’s method. Against darktable-cli 5.6.1, the color equalizer and color calibration models agree to 0.0002 in linear RGB, AgX to 0.0006 and sigmoid to 0.003. The settings are fitted by Levenberg-Marquardt on the samples’ L*a*b* miss after tone mapping, weighted by confidence. A change of 12° or 25% (12% for brightness) at a node, or between neighboring nodes, weighs like a miss of ΔE 1, so nodes no sample needs stay close to 0. Changes under 0.5 are left out. Without a neutral sample, the white balance is the app’s own: color calibration is set to neutralize, before tone mapping, the middle-gray color the app’s white balance makes neutral.
  • Correction. White balance is a gain per linear sRGB channel, a weighted least-squares fit mapping the samples onto their references; with Compare color only it is scaled so a neutral keeps its luminance. The color step fits each white-balanced sample’s remaining L*a*b* difference with Gaussian radial basis functions, measuring distance in a*b* plus L* at half weight. Each sample’s radius is half the distance to its nearest other sample, between 10 and 20. An anchor on the gray axis (radius 10) holds neutrals in place. The fit is kernel ridge regression: each sample may give way by give ÷ confidence (Exact 0.0001, Balanced 0.15, Gentle 0.5), and the first tolerance ÷ confidence ΔE of its difference (Exact 0, Balanced 1, Gentle 2) is not corrected.
    Confidence is 1 for measured and neutral references, 0.8 for the sky model and 0.5 for illustrative ones, doubled for neutral samples. A partial amount raises the gains to that power and scales the color offsets by it. Two samples conflict when the difference between the corrections they need after white balance exceeds 0.75 times the distance between their colors (in the same a*b* plus half-weight L* measure) and is at least 2. Separation by position uses soft nearest-sample territories: at each pixel the ownership of each conflicting sample is exp(−d/0.03) normalized over those samples, with d the distance as a fraction of the photo’s diagonal, and each sample’s basis function is weighted by the total ownership of the samples it does not conflict with, itself included; the fit uses the same weights at the samples’ positions. The color step fades out over the darkest 12 and brightest 4 L* units, so pure black and white never take on a tint; the fit accounts for this. The Changes view measures each pixel’s change as ΔE (CIE 1976) at display size. An exported LUT is the correction evaluated at every point of a 33³ or 65³ grid of sRGB-encoded values (the .cube format, red changing fastest). The accuracy shown when exporting compares the correction with a trilinear lookup in the LUT, as darktable’s LUT 3D module can, on a sample of up to 40,000 of the photo’s pixels, before rounding to 8 bits. The self-test checks both steps, the soft fit’s behavior with conflicting, near-miss, differently trusted and dark samples, the conflict warning, separation by position, LUT export, the amount, the darktable models against darktable 5.6.1’s output, and the settings for a photo with a cast.
  • Tests. See Testing.

Sources

  • A. J. Preetham, P. Shirley and B. Smits, “A Practical Analytic Model for Daylight,” SIGGRAPH 1999.
  • G. Sharma, W. Wu and E. N. Dalal, “The CIEDE2000 Color-Difference Formula: Implementation Notes, Supplementary Test Data, and Mathematical Observations,” Color Research & Application 30(1), 2005.
  • X-Rite ColorChecker Classic reference values, post-November-2014 formulation, CIELAB D50.
  • A. Chardon, I. Cretois and C. Hourseau, “Skin colour typology and suntanning pathways,” International Journal of Cosmetic Science 13(4), 1991 (individual typology angle and its skin categories).
(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.