Color Palette Extraction - #1509
Conversation
|
@olivervogel no problem, want me to post a PR to this branch with the fixes? I have some extra claude usage I can dedicate to this :) |
|
@deluxetom That would be really great. I pushed my last local commits. |
… array access - rebuild bin keys on toColorspace() so lookups work after conversion and colors that become identical are merged into one bin - convert each color individually in sortByChannel() so palettes with mixed colorspaces no longer throw - throw NotSupportedException on array write access instead of silently mutating a temporary array
…tization - bins keep the first actual color as representative instead of the calculated bin center, so every reported color exists in the image - alpha is left out of the bin key, colors that only differ slightly in transparency no longer occupy multiple palette slots - popular() sorts by presence before applying the limit, so the result contains the globally most frequent colors - the LEVEL_MAX decision is made from the distinct sampled colors instead of a driver-specific color count, GD and Imagick now return identical palettes and the full resolution Imagick histogram pre-scan is gone - VibrantMuted uses a coarsely quantized palette so the population weight has an actual influence on swatch scoring
- the RNG is re-seeded on every analyze() call, so a reused analyzer
instance returns identical results for the same image
- colors are flattened once to plain Oklab float triples and centroid
updates accumulate per-cluster sums in a single pass over the
assignments, O(n) instead of O(k*n) per iteration
Measured on a 2000x1500 image (GD):
dominant(8): 0.64s -> 0.04s
dominant(16): 3.05s -> 0.10s
dominant(64): 16.43s -> 0.43s
The previous fitsWithin() guard compared only width and height, so an offset region passed validation and failed later with a misleading pixel coordinate error. The test also moves to tests/Unit/Analyzers and receives a CoversClass attribute.
Checking the type after new $swatches() let undeclared errors escape for abstract classes or constructors with required parameters. The unsound @return T generic is removed because the returned object comes from the color filter, not from the given argument.
unset() on a typed property leaves it uninitialized and every later read fatals. The offset is set to null instead. Also removes the duplicated reflection filter in getIterator().
All public entry points are typed ImageInterface, without the declaration the color extractor was unreachable for static analysis.
|
@deluxetom I've moved a few things around. Mainly put the quantization logic directly to the palette and added a method to quantize but keep original channel values. Thanks again for the support. |
|
@olivervogel last review, almost there :) |
Unfinished Business1. Optimization of sample pixel collectionsrc/Analyzers/AbstractPaletteAnalyzer.php:25 - every sampled pixel calls $image->colorAt(), which goes through Image::analyze() and a full AbstractDriver::specialize(). Up to ~111k specializations per analysis on a 10000x10000 image. Reuse one specialized analyzer, or do a driver-level bulk pixel read. 2. Standardize SwatchesInterface and PaletteInterfaceWe could consider extending SwatchesInterface from PaletteInterface to standardize the handling, since they both do almost the same thing. The main difference is that Palette objects use integers as iteration keys, while Swatches objects use strings. The implementation of sorting functions in Swatches is also still undecided. 3. Palettes and ColorFiltersI think it makes more sense overall if A new PaletteInterface::filter() method is a good option; it accepts It's unclear where to put the swatch object instantiation. 4. Pick most used color in reduce()Pick most used color instead of just the first one. |
The ability to generate color palettes from an image instance.