[HN Gopher] Okmain: How to pick an OK main colour of an image
___________________________________________________________________
Okmain: How to pick an OK main colour of an image
Author : dgroshev
Score : 225 points
Date : 2026-03-09 14:16 UTC (4 days ago)
(HTM) web link (dgroshev.com)
(TXT) w3m dump (dgroshev.com)
| latexr wrote:
| I'd be interested in trying this out as a command-line tool. It
| would be useful on its own and the fastest way to evaluate
| results.
| woodrowbarlow wrote:
| looks like it's a rust lib with a python wrapper. making a CLI
| tool should be just a few lines of code.
| latexr wrote:
| Yeah, but then I'd have to be working with Python (which I
| don't enjoy) and be pulling in dependencies (which I avoid)
| to have a custom system with moving parts (Python
| interpreter, library, script) (which I don't want).
|
| A rust CLI would make a lot of sense here. Single binary.
| blipvert wrote:
| This sounds like a job for <ta-ta-ta-taaaa> contrib-
| directory-man!
| latexr wrote:
| So your solution to "I'd be interested in having a small
| ready-made tool and try this out" is "spend a bunch of
| time to get acquainted with the code base of something
| you may not even like, create a separate tool, and submit
| it without even knowing if it'll be accepted"?
|
| That's like having someone looking at a display of ice
| cream in a supermarket saying "I'd be interested in
| trying a few samples before committing" and then getting
| a reply like "here are the recipes for all the ice
| creams, you can try to make them at home and taste them
| for yourself".
|
| I know I could theoretically spend my weekend working on
| a CLI tool for this or making ice cream. Every developer
| knows that, there's no reason to point that out except
| snark. But you know who might do it even faster and
| better and perhaps even enjoy it? The author.
|
| Look, the maintainer owes me nothing. I owe them nothing.
| This project has been shared to HN by the author and I'm
| making a simple, sensible, and sensical suggestion for
| something which I would like to see and believe would be
| an improvement overall, and I explained why. The author
| is free to agree or disagree, reply or ignore. Every one
| of those options is fine.
| cvwright wrote:
| You're not wrong, but you probably could have built the
| thing with Claude in the time it took you to write this
| comment.
| blipvert wrote:
| You could likely have written that tool in the time that
| it took to write that comment.
|
| If it would be useful to you, and to others, then why
| not?
|
| And if it is not accepted then publish it yourself. Stop
| your whining.
| sorenjan wrote:
| > uvx --with pillow --with okmain python -c "from PIL
| import Image; import okmain;
| print(okmain.colors(Image.open('bluemarble.jpg')))"
| [RGB(r=79, g=87, b=120), RGB(r=27, g=33, b=66), RGB(r=152,
| g=155, b=175), RGB(r=0, g=0, b=0)]
|
| It would make sense to add an entrypoint in the
| pyproject.toml so you can use uvx okmain directly.
| jcynix wrote:
| ImageMagick is a wonderful command line tool, IMO. You could
| use it to extract various information, e.g. the 5 most used
| colors of an image, as in convert $IMG
| -colors 5 -depth 8 -format "%c" histogram:info: | sort -nr
|
| If needed you can easily remove colored borders first (trim
| subcommand with fuzz option) or sample only xy% from the
| image's center, or where the main subject might be.
| dgroshev wrote:
| Good idea, I'll add a CLI tool over the weekend.
| iamcalledrob wrote:
| As a designer, I've built variants of this several times
| throughout my career.
|
| The author's approach is really good, and he hits on pretty much
| all the problems that arise from more naive approaches. In
| particular, using a perceptual colorspace, and how the most
| representative colour may not be the one that appears the most.
|
| However, image processing makes my neck tingle because there are
| a lot of footguns. PNG bombs, anyone? I feel like any library
| needs to either be defensively programmed or explicit in its
| documentation.
|
| The README says "Finding main colors of a reasonably sized image
| takes about 100ms" -- that's way too slow. I bet the operation
| takes a few hundred MB of RAM too.
|
| For anyone that uses this, scale down your images substantially
| first, or only sample every N pixels. Avoid loading the whole
| thing into memory if possible, unless this handled serially by a
| job queue of some sort.
|
| You can operate this kind of algorithm much faster and with less
| RAM usage on a small thumbnail than you would on a large input
| image. This makes performance concerns less of an issue. And
| prevents a whole class of OOM DoS vulnerabilities!
|
| As a defensive step, I'd add something like this
| https://github.com/iamcalledrob/saferimg/blob/master/asset/p...
| to your test suite and see what happens.
| latexr wrote:
| > I've built variants of this several times throughout my
| career.
|
| Got any to share? A self-contained command-line tool to get a
| good palette from an image is something I'd have a use for.
| PaulHoule wrote:
| Back in the late 1980s people thought about color
| quantization a lot because a lot of computers of the time had
| 16 or 256 colors you could choose out of a larger palette and
| if you chose well you could do pretty well with photographic
| images.
| dylan604 wrote:
| Fred's dominantcolor script for imagemagick might work for
| you:
|
| https://www.fmwconcepts.com/imagemagick/dominantcolor/index..
| ..
| jaen wrote:
| I really wish people would read the article, the library does
| exactly this:
|
| > Okmain downsamples the image by a power of two until the
| total number of pixels is below 250,000.
| vasco wrote:
| That's 500x500, I'm sure you can get good results at 32x32 or
| 64x64 but then part of your color choice is also getting done
| by the downsampling algorithm. I wonder if you could get away
| with just using a downsampling algorithm into a 1x1 and just
| use that as the main color.
| PaulHoule wrote:
| That last one is talked about in the article -- it sucks!
|
| I think if you were going to "downsample" for the purpose
| of creating a color set you could just scan through the
| picture and randomly select 10% (or whatever) of the pixels
| and apply k-means to that and not do any averaging which
| costs resources and makes your colors muddy.
| dgroshev wrote:
| Random sampling makes a lot of intuitive sense, but
| unfortunately doesn't work well. I just answered over at
| lobsters: https://lobste.rs/s/t43mh5/okmain_you_have_imag
| e_you_want_co...
|
| I should probably add this nuance to the post itself.
|
| Edit: added a footnote
| SigmundurM wrote:
| How about sampling more from the more "prominent" areas
| of the image[0] and less from less "prominent" areas?
|
| [0]: https://dgroshev.com/blog/okmain/img/distance_mask.p
| ng?hash=...
| iamcalledrob wrote:
| Somehow I missed that, oops. I see that the library samples a
| maximum of 250K pixels from the input buffer (I jumped over
| to the project readme)
|
| That being said, this is sampling the fixed-size input buffer
| for the purposes of determining the right colour. You still
| have to load the bitmap into memory, with all the associated
| footguns that arise there. The library just isn't making it
| worse :) I suppose you could memmap it.
|
| Makes me wonder if the sub-sampling is actually a bit of a
| red herring, as ideally you'd want to be operating on a small
| input buffer anyway. Or some sort of interface on top of the
| raw pixel data, so you can load what's needed on-demand.
| dgroshev wrote:
| Author here: the library just accepts RGB8 bitmaps, probably
| coming either from Rust's image crate [1] or Python's Pillow
| [2], which are both mature and widely used. Dealing with codecs
| is way out of scope.
|
| As for loading into memory at once: I suppose I could integrate
| with something like libvips and stream strips out of the
| decoded image without holding the entire bitmap, but that'd
| require substantially more glue and complexity. The current
| approach works fine for extracting dominant colours once to
| save in a database.
|
| You're right that pre-resizing the images makes everything
| faster, but keep in mind that k-means still requires a pretty
| nontrivial amount of computation.
|
| [1]: https://crates.io/crates/image
|
| [2]: https://pypi.org/project/pillow/
| hedgehog wrote:
| If you ever did want to wrap this in code processing
| untrusted images there's a library called "glycin" designed
| for that purpose (it's used by Loupe, the default Gnome image
| viewer).
|
| https://gnome.pages.gitlab.gnome.org/glycin/
| chrisweekly wrote:
| your gh link returned 404
|
| EDIT: then (when url refreshed) triggered a redir loop
| culminating in a different error ("problem occurred
| repeatedly")...
|
| ah, ofc, your intent was to demonstrate a problematic asset.
| TheJoeMan wrote:
| Realizing I intentionally opened a png bomb made me chuckle,
| like what did I think was going to happen?
| llimllib wrote:
| OKPalette by David Aerne is my favorite tool for this, it chooses
| points sensibly but then also lets you drag around or change the
| number of colors you want: https://okpalette.color.pizza/
| lemonad wrote:
| This is nice! I looked into this quite a lot some years back when
| I was trying to summarize IKEA catalogs using color and
| eventually wrote an R package if you want to look into an
| alternative to e.g. k-means: https://github.com/lemonad/colorhull
| (download https://github.com/lemonad/ikea-colors-through-
| time/blob/mas... for more details on how it works)
| bee_rider wrote:
| I'm surprised the baseline to compare against is shrinking the
| image to one pixel, that seems extremely hacky and very dependent
| on what your image editor happens to do (and also seems quite
| wasteful... the rescaling operation must be doing a lot of extra
| pointless work keeping track of the position of pixels that are
| all ultimately going to be collapsed to one point).
|
| So, making a library that provides an alternative is a great
| service to the world, haha.
|
| An additional feature that might be nice: the most prominent
| colors seem like they might be a bad pick in some cases, if you
| want the important part of the image to stand out. Maybe a color
| that is the close (in the color space) to the edges of your
| image, but far away (in the color space) from the center of your
| image could be interesting?
| mungoman2 wrote:
| Tbh shrinking the image is probably the cheapest operation you
| can do that still lets every pixel influence the result. It's
| just the average of all pixels, after suitable color
| conversion.
| bombcar wrote:
| It might work decently well, but I wonder if it makes it
| "visually" match - sometimes the perfect average is not what
| our eyes see as the color.
| LoganDark wrote:
| The author of the article seems to assume there is no color
| conversion (e.g., the resizing of the image is done with
| sRGB-encoded values rather than converting them to linear
| first). Which is a stupid way to do it but I'd believe most
| handwritten routines are just that.
| airstrike wrote:
| See also https://github.com/material-foundation/material-color-
| utilit...
| eloisius wrote:
| Really interesting read. Thanks for sharing. Is the performance
| bottleneck around the resizing to 250k pixels? Would it still
| work if you sampled 15,625 4x4 patches evenly around the image to
| gather those pixels instead of resizing?
| bawolff wrote:
| In the past when i tried just using image magick's built in
| -kmeans for this, i found chosing the second most prominent
| colour often looked really good. The primary was too much of the
| same thing.
| vova_hn2 wrote:
| > simple 1x1 resize
|
| How is it "simple"? There are like a ton of different downscaling
| algorithms and each of them might produce a different result.
|
| Cool article otherwise.
| gzread wrote:
| At 1x1 I don't expect any difference. It would be the average
| of all pixels in the image if you don't unevenly weight them
| (which you might decide when choosing a main color, but no
| downscaling algorithm would do) and the only difference is
| whether you remembered to gamma-correct.
| vova_hn2 wrote:
| Nearest-neighbor interpolation may pick just one pixel
| closest to the center.
| kristjan wrote:
| I've been doing something similar! I've got a Home Assistant
| dashboard on my desk and wanted the media controls to match the
| current album art. I need three colors: background, foreground,
| and something vibrant to set my desk lamp to [1].
|
| The SpotifyPlus HA integration [2] was near at hand and does a
| reasonably good job clustering with a version of ColorThief [3]
| under the hood. It has the same two problems you started with
| though: muddying when there's lots of gradation, even within a
| cluster; and no semantic understanding when the cover has
| something resembling a frame. A bit swapped from okmain's goal,
| but I can invert with the best of them and will give it a shot
| next time I fiddle. Thanks for posting!
|
| [1]
| https://gist.github.com/kristjan/b305b83b0eb4455ee8455be108a...
| [2]
| https://github.com/thlucas1/homeassistantcomponent_spotifypl...
| [3]
| https://github.com/thlucas1/SpotifyWebApiPython/blob/master/...
| slazaro wrote:
| It reminds me a bit of this post from the Facebook engineering
| blog (2015) [1] where they discuss embedding a very tiny preview
| of images into the html itself so they show immediately while
| loading the page, especially with very slow connections.
|
| [1] https://engineering.fb.com/2015/08/06/android/the-
| technology...
| GauntletWizard wrote:
| I really like this approach. I worked on this problem (create a
| nice background for an image) for a couple weeks many years ago
| while organizing my desktop wallpaper collection, and never came
| up with a good answer. Unfortunately, I think that it's been
| "solved" in the tiktok era; an enlarged and blurred version of
| the image is used to fill the background space.
|
| The blurred mirror is inoffensive to almost everyone, and yet it
| always strikes me as gauche. Easy to ignore and yet I feel that
| it adds a lot of useless visual noise.
| Animats wrote:
| I've wanted something like this for level of detail processing.
|
| This is a render from Second Life, in which all the texture
| images were shrunk down to one pixel, the lowest possible level
| of detail, producing a monocolor image. For distant objects, or
| for objects where the texture is still coming in from the net,
| there needs to be some default color. The existing system used
| grey for everything. I tried using an average of all the pixels,
| and, as the original poster points out, the result looks
| murky.[1] This new approach has real promise for big-world
| rendering.
|
| [1]
| https://media.invisioncic.com/Mseclife/monthly_2023_05/monoc...
| Groxx wrote:
| I thought somewhere online there was a relatively-old (Material
| v1?) breakdown of how Android selects "main" colors (because it
| does a fairly good job imo), for the Palette1 class... but I'm
| having no luck finding it at all. I can (and will) just read its
| source code, but it doesn't carry along any qualitative
| justification or comparisons or etc and it'll take a fair bit of
| time to re-research that knowledge.
|
| Does anyone know how this strategy differs? I've been wanting to
| build a product-color-selection-thing for stuff like fabric,
| where finding something that has similar tones is important, but
| I'm struggling to find much with concrete details about the
| strategy like this article has.
|
| 1:
| https://developer.android.com/reference/androidx/palette/gra...
| with most of the color selection logic here:
| https://cs.android.com/androidx/platform/frameworks/support/...
___________________________________________________________________
(page generated 2026-03-13 23:00 UTC)