240 lines
14 KiB
Markdown
240 lines
14 KiB
Markdown
# whoshue
|
||
|
||
A pixel-art color analyzer for KDE/Wayland. Capture a game/application window,
|
||
select part of its preview, and inspect the captured colors. The analyzer is a
|
||
normal desktop window; it does not flash calibration colors or hide itself.
|
||
|
||
## Run
|
||
|
||
On CachyOS/Arch with KDE, the capture backend uses `xdg-desktop-portal`,
|
||
`xdg-desktop-portal-kde`, and PipeWire. Building needs Rust, a C toolchain,
|
||
`pkgconf`, Clang/libclang, and PipeWire development libraries (`libpipewire`).
|
||
|
||
```sh
|
||
cargo run --release
|
||
# Or start with a local image, without requesting screen sharing:
|
||
cargo run --release -- /path/to/screenshot.png
|
||
```
|
||
|
||
1. Click **Choose source…**. KDE handles selecting a window, screen, or region.
|
||
Click it again to choose a different source.
|
||
2. Drag a rectangle in the source preview. Capture display pauses during the
|
||
drag, then returns to its previous live/frozen state. The crop is stored in
|
||
source pixels and resets if the source dimensions change.
|
||
3. **Freeze** holds the displayed frame and analysis; **Resume** takes the newest
|
||
available frame. **Stop sharing** closes capture and keeps the last image.
|
||
4. Inspect the nearest-neighbor zoomed crop. Zoom ranges from 0.1× to 16×,
|
||
starting at 1×. Hover for source coordinates,
|
||
RGB/hex and HSV; click to copy hex. Click palette swatches to copy them too.
|
||
5. **Save crop PNG** writes a uniquely named PNG in the system temporary directory
|
||
and displays its path. Drop an image or click **Open image…** to browse for a file in KDE's file picker.
|
||
|
||
## Color grouping and mapping
|
||
|
||
**Group similar colors** is enabled by default in the **Palette**. The default
|
||
**Edited** view shows the resulting colors. Switch to **Source / map** to select
|
||
original colors for replacement. Nearby shades
|
||
from scaling or compression share a row showing their combined percentage and
|
||
shade count. Adjust **Tolerance** (default 8, range 0–32) to control the maximum
|
||
difference in each RGB channel from the group's most frequent color. Groups do
|
||
not chain together through intermediate shades. Turn grouping off or set tolerance
|
||
to zero for the exact palette. Hover a row to highlight its pixels in magenta in
|
||
the crop preview; this temporary highlight is never exported.
|
||
|
||
Freeze a capture or import an image, choose **Source / map**, then click **Map**
|
||
beside a palette row.
|
||
In **Color mapping**, click its replacement swatch to choose another color or enter
|
||
RGB values. Every original shade in that group maps to the chosen replacement.
|
||
Add multiple mappings to try a new palette. Each mapping keeps its original group
|
||
members even if you later change tolerance or crop. If a new mapping overlaps an
|
||
older one, the new mapping owns those shades; the older mapping keeps any others.
|
||
Replacements are simultaneous, so swapping two colors works without cascading.
|
||
Alpha is preserved and fully transparent pixels are left untouched.
|
||
|
||
Toggle **Preview mappings** to compare with the original palette. Remove an
|
||
individual mapping with **×** or use **Reset mappings**. Mappings survive crop
|
||
changes, pause during live capture, and return when frozen again. Opening a new
|
||
image or choosing a new capture source clears them.
|
||
|
||
Mappings apply before the preview adjustments or vision filter. Source preview,
|
||
copied pixel values, and **Save crop PNG** still use the original. Color analysis
|
||
and the vision check use the edited selection.
|
||
**Save mapped crop PNG** exports the crop with replacements, preserving alpha
|
||
and excluding preview adjustments and filters.
|
||
|
||
## Automatic harmony
|
||
|
||
Freeze a capture or import an image, open **Harmonize**, and enable **Preview
|
||
harmony**. Choose **Monochromatic**, **Analogous**, **Complementary**,
|
||
**Split-Complementary**, or **Triadic**. The app fits that rule to the crop automatically, then moves its hue families toward the
|
||
chosen harmony while preserving shading. **Strength** blends from the input
|
||
at 0% to the full transformation at 100%. **Rotation**, or dragging the color
|
||
wheel, changes the palette's direction. White dots on the wheel mark input hue
|
||
families; colored spokes mark target hues. The wheel shows targets at full
|
||
strength. **Auto fit** resets rotation and fits the current crop again.
|
||
|
||
**Protect neutrals** is enabled by default: gray and near-gray colors stay
|
||
unchanged, with a gradual transition into chromatic colors. **Reset harmony**
|
||
turns harmony off and restores its defaults. Disabling **Preview harmony** lets
|
||
you compare with the input (including any enabled manual mappings). Settings
|
||
pause during live capture, resume when frozen, and reset for a new image or
|
||
capture source. Changing the crop or manual mappings refits the harmony while
|
||
retaining your rotation and strength.
|
||
|
||
Harmony works in OKLCH: it holds perceptual lightness and chroma while shifting
|
||
hue, reducing chroma when needed to fit sRGB. It retains small hue variations
|
||
within a family and compresses broad families into bands around target hues.
|
||
Monochromatic instead converges to a single hue at full strength, retaining
|
||
lightness and chroma variations (with neutral protection still applied).
|
||
The offsets are 0° for monochromatic, −30°/0°/+30° for analogous, 0°/180° for
|
||
complementary, 0°/150°/210° for split-complementary, and 0°/120°/240° for triadic,
|
||
on the **OKLCH** wheel. These angles do not produce
|
||
identical color pairs to HSV or traditional artist color wheels.
|
||
|
||
The fit uses a circular hue histogram, finds hue families, and searches for the
|
||
orientation requiring the least weighted squared hue movement. Counts are
|
||
square-root weighted after hue binning so a large background has less influence
|
||
and compression-generated near-duplicates don't each get a separate vote. This
|
||
is an experimental palette heuristic: it does not recognize objects, protect
|
||
skin or gameplay indicators automatically, or guarantee that every target hue
|
||
is used. Families may merge, and family boundaries can still affect subtle hue
|
||
ramps. Start with moderate strength and compare the result in context.
|
||
|
||
The processing order is **source → manual mappings → harmony → preview filter
|
||
or adjustments**. The source preview and copied pixel values remain original; color analysis and
|
||
the vision check use the edited selection. **Save harmonized crop PNG** includes enabled mappings and harmony,
|
||
preserving transparency, without hover highlights, preview filters, or
|
||
adjustments. **Save mapped crop PNG** still saves only manual replacements;
|
||
**Save crop PNG** still saves the original.
|
||
|
||
The conversion uses [Björn Ottosson's Oklab transforms](https://bottosson.github.io/posts/oklab/).
|
||
|
||
## Color analysis and vision checks
|
||
|
||
The **Color analysis** tab uses the edited selection after active mappings,
|
||
harmony, and the selected filter or adjustments. Histograms, means, neutral
|
||
counts, distinct-color weighting, and the **Edited** palette all update together.
|
||
Merged colors count once for distinct-color weighting. Diagnostic simulation
|
||
previews and hover highlights are excluded, just as in the vision check.
|
||
**Source / map** keeps the original palette available for choosing replacements.
|
||
|
||
The tab has four distributions:
|
||
|
||
- **Hue**: HSV hue in 36 ten-degree bins; saturation below 5% counts as neutral.
|
||
- **Saturation**: HSV saturation, from neutral to fully saturated.
|
||
- **Brightness (V)**: HSV value, the maximum RGB channel. This is not perceived
|
||
lightness: fully saturated red and green both have 100% value.
|
||
- **Luminance**: relative luminance in linear sRGB, weighted for contrast.
|
||
|
||
Saturation, value, and luminance use 20 five-percentage-point bins, including
|
||
100% in the final bin. Each has a mean. Switch between pixel frequency and equal
|
||
weight per distinct RGB color; hover bars for counts and percentages. The exact
|
||
palette is ordered by count, then RGB for deterministic ties. Fully transparent
|
||
pixels are excluded; partially transparent pixels retain their stored RGB values
|
||
and count once. Pixel inspection includes HSV and relative luminance.
|
||
|
||
Use **Filter** below the selection to apply protanopia, deuteranopia,
|
||
tritanopia, or grayscale directly to the crop preview, or select **Original**
|
||
to use the live color adjustment sliders. The first three use the
|
||
Machado–Oliveira–Fernandes full-severity model, applied in linear RGB and then
|
||
encoded back to sRGB. Grayscale uses relative luminance and tests removal of color
|
||
cues; it does not model all aspects of achromatopsia. The selected filter updates color analysis and the vision check. Copied pixel
|
||
values and **Save crop PNG** still use the original.
|
||
|
||
**Invert** replaces each RGB channel with its complement (255 minus the channel).
|
||
With **Original** selected, adjust **Hue shift** (−180° to +180°), **Saturation**
|
||
(−100% to +100% relative change), and **Brightness (V)** (−100 to +100 percentage
|
||
points of HSV value). Changes update the crop preview immediately, including live
|
||
captures. Saturation keeps neutral colors neutral; saturation and value clamp to
|
||
their valid ranges. Brightness adjusts HSV value, not relative luminance.
|
||
**Reset adjustments** restores the unmodified preview. Adjustments are remembered
|
||
while another filter is selected and applied again when returning to Original.
|
||
Filters preserve alpha and always start from source pixels, so edits do not
|
||
accumulate rounding errors.
|
||
|
||
**Vision check** automatically checks all four scenarios against the **edited
|
||
selection**, after enabled group/manual mappings, harmony, and the selected
|
||
filter or adjustments. It updates when these edits or the crop change. Palette
|
||
grouping alone only organizes colors; mapping a group changes the checked pixels.
|
||
Colors that merge through editing count as one color, and opaque-pixel coverage
|
||
is recalculated from the result. Color distributions and the edited palette use
|
||
those same pixels; **Source / map** remains available for mapping input colors.
|
||
|
||
The report's **Preview** buttons add a separate simulation of those edited
|
||
pixels. They do not change the selected filter or feed simulated pixels back into
|
||
the report. Use **Return to edited preview** to remove this simulation. Temporary
|
||
palette hover highlights are also excluded from the check.
|
||
|
||
The check flags pairs whose OKLab distance falls from at least 0.08 to at most 0.04, losing at least 50% of the
|
||
separation in the edited image. These are project-specific screening heuristics, not
|
||
validated visibility thresholds or WCAG criteria. Each scenario shows edited
|
||
and simulated swatches, numeric hex values, and up to six closest candidate pairs.
|
||
The preview button opens the corresponding simulation for the edited crop.
|
||
|
||
The check is bounded to the 64 most frequent **opaque** colors and reports both
|
||
color and pixel coverage. Partially/fully transparent pixels have no known
|
||
backdrop and are excluded from this check. No findings does **not** establish
|
||
accessibility. The check cannot infer adjacency, text/background relationships,
|
||
or the gameplay meaning of a color. Review important cues in context and combine
|
||
color with shapes, labels, or differences in lightness. Select a smaller region
|
||
when an important detail is outside the reported palette coverage.
|
||
|
||
References:
|
||
|
||
- [Chrome's contrast audits and vision simulations](https://developer.chrome.com/docs/chromium/cvd)
|
||
- [Machado, Oliveira & Fernandes simulation model](https://www.inf.ufrgs.br/~oliveira/pubs_files/CVD_Simulation/CVD_Simulation.html)
|
||
- [Published matrix coefficients in QGIS](https://api.qgis.org/api/qgsprevieweffect_8cpp_source.html)
|
||
- [OKLab color space](https://bottosson.github.io/posts/oklab/)
|
||
- [W3C: use of color](https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html)
|
||
|
||
Choosing a source always opens KDE's picker. The last portal token is saved at
|
||
`$XDG_CONFIG_HOME/whoshue/config.toml` (normally `~/.config/whoshue/config.toml`)
|
||
for the command-line capture diagnostic. No capture starts until **Choose source…**
|
||
is clicked. Cancelling the file picker leaves the current image/capture unchanged.
|
||
|
||
Portal requests share one application-lifetime async runtime. PipeWire runs on a
|
||
separate blocking worker so restarting capture or opening another portal dialog
|
||
cannot strand the cached D-Bus connection on a stopped executor.
|
||
|
||
## Limits and next work
|
||
|
||
- Window capture is preferred: screen/region capture includes overlapping windows,
|
||
including whoshue if placed inside the captured area.
|
||
- Selection follows coordinates within the source, not a moving sprite.
|
||
- Analysis uses source pixels before preview scaling, but these are rendered
|
||
colors, not necessarily original game-asset colors. HDR and color-profile
|
||
conversion have not been validated; the current path assumes ordinary SDR RGB.
|
||
- Live analysis refreshes at most five times per second. Large selections with
|
||
many unique colors are more expensive; release builds are recommended.
|
||
- Game-specific behavior on focus loss/minimization needs desktop testing.
|
||
- Perceptual OKLCH views, color/hue highlighting, and comparing saved selections
|
||
are future features; the distributions currently use HSV and relative luminance.
|
||
|
||
## Validation
|
||
|
||
```sh
|
||
cargo test --offline --locked
|
||
cargo clippy --offline --locked --all-targets -- -D warnings
|
||
cargo fmt --check
|
||
```
|
||
|
||
Tests cover hue boundaries, neutral/transparent pixel handling, weighting,
|
||
cropping, selection coordinates, pixel-format conversion, frozen-frame behavior,
|
||
resize handling, all histogram/vision UI views, file-picker cancellation/reopening, and
|
||
repeated portal requests during/after capture. The portal regression test requires
|
||
`dbus-run-session` and permission to create a local socket; it uses its own private
|
||
bus and does not open desktop dialogs. Color tests cover histogram endpoints and
|
||
weighting, sRGB gamma, simulation reference colors, alpha handling, warning
|
||
positives/negatives, palette coverage, and preserving source pixels.
|
||
Harmony tests cover color-space reference values, gamut reduction, circular hue
|
||
fitting, neutral protection, shading, alpha, preset rendering, crop and capture
|
||
lifecycle, and exported PNG pixels.
|
||
Actual portal capture requires a running
|
||
Wayland desktop and permission through KDE's picker.
|
||
|
||
For a capture-only diagnostic (saves the first full source frame):
|
||
|
||
```sh
|
||
cargo run -- --selftest /tmp/whoshue-selftest.png
|
||
```
|