snibgo's ImageMagick pages

'Jump' method

How many colours do we need?

Complex images such as photographs can be reduced to a smaller number of colours. But how many colours should that be? This method provides an answer to that question.

References

Also interesting:

The problem

How many different colours does this image have?

%IMG7%magick ^
  -size 100x200 ^
  xc:rgb(10%%,15%%,20%%) ^
  xc:rgb(60%%,50%%,40%%) ^
  xc:rgb(80%%,85%%,90%%) ^
  +append +repage ^
  jm_src1.png
jm_src1.pngjpg

Answer: it has three colours. IM can count them, and list them:

%IMG7%magick ^
  jm_src1.png ^
  -unique-colors ^
  -format "numCols=%%w\n" ^
  +write info: ^
  txt: 
numCols=3
# ImageMagick pixel enumeration: 3,1,0,65535,srgb
0,0: (6554,9830,13107)  #199A26663333  srgb(10.0008%,14.9996%,20%)
1,0: (39321,32768,26214)  #999980006666  srgb(60%,50.0008%,40%)
2,0: (52428,55705,58982)  #CCCCD999E666  srgb(80%,85.0004%,90.0008%)

We make the problem more difficult. How many colours does this image have?

%IMG7%magick ^
  -seed 1234 ^
  jm_src1.png ^
  -blur 0x10 ^
  -attenuate 30% ^
  +noise uniform ^
  jm_src2.png
jm_src2.pngjpg

We can see there are, essentially, just three colours. If we imagine the pixels occuping positions within a hollow 3D colour cube, there would be three clouds of pixels, three clusters. As before, IM can count the unique colours:

%IMG7%magick ^
  jm_src2.png ^
  -unique-colors ^
  -format "numCols=%%w\n" ^
  info: 
numCols=59859

There are nearly as many colours as pixels. Perhaps the histogram can help us find the "real" colours.

%IMG7%magick ^
  jm_src2.png ^
  -define histogram:unique-colors=true -format "%%c" ^
  histogram:info:jm_hist1.lis

%windir%\system32\sort /R <jm_hist1.lis >jm_hist2.lis

cHead /ijm_hist2.lis /h15

The first 15 entries in the histogram:

 
            61: (65535,65535,65535) #FFFFFFFFFFFF white
            50: (0,0,0) #000000000000 black
             3: (0,0,18827) #00000000498B srgb(0%,0%,28.7282%)
             3: (0,0,11506) #000000002CF2 srgb(0%,0%,17.557%)
             2: (65535,65535,61319) #FFFFFFFFEF87 srgb(100%,100%,93.5668%)
             2: (65535,65535,57677) #FFFFFFFFE14D srgb(100%,100%,88.0095%)
             2: (65535,58827,65535) #FFFFE5CBFFFF srgb(100%,89.7642%,100%)
             2: (65169,65535,65535) #FE91FFFFFFFF srgb(99.4415%,100%,100%)
             2: (62434,65535,65535) #F3E2FFFFFFFF srgb(95.2682%,100%,100%)
             2: (61541,65535,65535) #F065FFFFFFFF srgb(93.9055%,100%,100%)
             2: (61285,65535,65535) #EF65FFFFFFFF srgb(93.5149%,100%,100%)
             2: (56139,65535,65535) #DB4BFFFFFFFF srgb(85.6626%,100%,100%)
             2: (49257,65535,65535) #C069FFFFFFFF srgb(75.1614%,100%,100%)
             2: (44933,65535,65535) #AF85FFFFFFFF srgb(68.5634%,100%,100%)
             2: (44931,65535,65535) #AF83FFFFFFFF srgb(68.5603%,100%,100%)

The histogram doesn't help us much.

The solution

Let's reduce the number of colours using "-colors N", where N=1,2,3,4, and compare each result to the input. We show the RMSE distortion; 0.0 means there is no distortion, with larger numbers up to 1.0 meaning the distortion is worse.

%IMG7%magick ^
  jm_src2.png ^
  ( +clone ^
    +dither -colors 1 ^
    +write jm_col1.png ^
  ) ^
  -metric RMSE ^
  -format %%[distortion] ^
  -compare ^
  info: 
0.304303
jm_col1.pngjpg
%IMG7%magick ^
  jm_src2.png ^
  ( +clone ^
    +dither -colors 2 ^
    +write jm_col2.png ^
  ) ^
  -metric RMSE ^
  -format %%[distortion] ^
  -compare ^
  info: 
0.185066
jm_col2.pngjpg
%IMG7%magick ^
  jm_src2.png ^
  ( +clone ^
    +dither -colors 3 ^
    +write jm_col3.png ^
  ) ^
  -metric RMSE ^
  -format %%[distortion] ^
  -compare ^
  info: 
0.132678
jm_col3.pngjpg
%IMG7%magick ^
  jm_src2.png ^
  ( +clone ^
    +dither -colors 4 ^
    +write jm_col4.png ^
  ) ^
  -metric RMSE ^
  -format %%[distortion] ^
  -compare ^
  info: 
0.127757
jm_col4.pngjpg

None of the RMSE metrics are particularly close. With one colour, the distortion is bad. It improves greatly with two colors, and again with three colours. But the improvement with four colours is very slight. We could continue with more colours, getting a slight improvement each time, but it would be only slight. The last big improvement comes from "-colors 3", and this is the solution.

(Caution: even when the image has more than N colours, -colors N may find fewer than N colours.)

The "Jump" script

The script jumpMethod.bat reads an input image and creates an output, by running an IM transformation a number of times, seeking the biggest improvement. It can also create explanatory graphs, if we want.

Parameter Description
%1 Input image
%2 Output image
%3 Initial trial value for k.
%4 Final trial value for k.
%5 Any IM process that will transform an image. It should contain the four characters _XX_ which will be substituted by a value of k.
%6 A skeleton filename for output graphs. Leave blank or "." if graphs are not wanted. Should contain _XX_, which will be substituted by 1, 2 or 3.

(The graphs are made by gnuplot. If gnuplot isn't installed, the script won't create graphs.)

We continue the tests to "-colors 19", and plot the distortions.

call %PICTBAT%jumpMethod jm_src2.png jm_src2_x.png 1 19 "+dither -colors _XX_" jm_col_gr_XX_.png
jm_col_gr1.png

The graph shows an "elbow" at "-colors 3". Distortion improves from 1 to 2, and improves more (proportionally) from 2 to 3. After that, the improvement is slight.

Caution: the graph is truncated, and doesn't include the origin, at distortion=0.


jm_col_gr2.png

We quantify this in two stages. First, we transform the graph by raising to a negative power: D = dist-1.5.

For a distribution with no clustering, the distortion graph would be a hyperbolic. Raising this to a negative power would produce roughly a straight line, in the absence of clustering: D = a*N + b where a and b are constants.

The positive part of the power is called "Y", so D = dist-Y where Y=1.5. Okay, so why 1.5? Because colours have three dimensions (eg red, green and blue) and half of 3 is 1.5.

One way to view this graph is to observe that the line is is two portions, each more or less straight. The first is from N=0 to N=3; the second is from N=3 to N=19. But later examples on this page are not so simple: are there more than two portions? Or just one single portion with no definite elbow? If there is an elbow, exactly where is it?

Further analysis provides definite answers.


jm_col_gr3.png

We differentiate the graph of D: JN = DN - DN-1, but we define D0 to be zero. We look for the maximum value, the peak value, of J. The value of N at the peak gives us the number of clusters.

The position of the peak shows the value of N that gives us the best improvement in quality. This isn't the least distortion -- the distortion keeps improving with increasing N. The peak is at the best improvement in distortion.

In the absence of a cluster, J1 will be a high value, and following values will be roughly constant and lower than J1.

This chart shows a clear peak in the value of J at N=3.


The script has created an image using just the colours of the three clusters, with each pixel the appropriate colour.

jm_src2_x.png

We can count the colours, and list them:

%IMG7%magick ^
  jm_src2_x.png ^
  -unique-colors ^
  -format "numCols=%%w\n" ^
  +write info: ^
  txt: 
numCols=3
# ImageMagick pixel enumeration: 3,1,0,65535,srgb
0,0: (10854,12611,14487)  #2A6631433897  srgb(16.5621%,19.2432%,22.1057%)
1,0: (39532,32693,25980)  #9A6C7FB5657C  srgb(60.322%,49.8863%,39.6429%)
2,0: (51178,53174,54723)  #C7EACFB6D5C3  srgb(78.0926%,81.1383%,83.5019%)

Clusterless example

Some images don't have any number of clusters.

%IMG7%magick ^
  -seed 1234 ^
  -size 300x200 xc: ^
  +noise random ^
  jm_rand.png
jm_rand.pngjpg
call %PICTBAT%jumpMethod jm_rand.png jm_rand_x.png 1 19 "+dither -colors _XX_" jm_rand_gr_XX_.png

if ERRORLEVEL 1 goto error

echo jmNatMax=%jmNatMax% 
jmNatMax=1 
jm_rand_gr1.png jm_rand_gr2.png jm_rand_gr3.png

The final graph shows a peak in J at N=1, which indicates there is no division into clusters, which is a good conclusion to make for an image of random pixels. If we don't want that solution, we can search again, starting at 2:

call %PICTBAT%jumpMethod jm_rand.png jm_rand2_x.png 2 19 "+dither -colors _XX_" jm_rand2_gr_XX_.png

if ERRORLEVEL 1 goto error

echo jmNatMax=%jmNatMax% 
jmNatMax=7 
jm_rand2_gr1.png jm_rand2_gr2.png jm_rand2_gr3.png

The script has created the result with seven colours:

jm_rand2_x.png

Photograph example

set SRC=toes.png
toes.pngjpg
call %PICTBAT%jumpMethod %SRC% jm_toes_x.png 1 19 "+dither -colors _XX_" jm_toes_gr_XX_.png

if ERRORLEVEL 1 goto error

echo jmNatMax=%jmNatMax% 
jmNatMax=6 
jm_toes_gr1.png jm_toes_gr2.png jm_toes_gr3.png

The final graph shows a peak in J at N=6, a lesser peak at N=14, with N=2 and N=1 very close behind, and so on.

The script has created the version with N=6:

jm_toes_x.png

We could repeat the search, starting at N=7. Instead, we simply create the N=14 version:

%IMG7%magick ^
  toes.png ^
  +dither -colors 14 ^
  jm_toes_14.png
jm_toes_14.pngjpg

We can see that doubling the number of colours has improved the reslut, but not by much. So N=7 is a reasonable answer to "How many colours do we need?"

Dither

We repeat the test, but with dithering.

call %PICTBAT%jumpMethod %SRC% jm_toesd_x.png 1 19 "-colors _XX_" jm_toesd_gr_XX_.png

if ERRORLEVEL 1 goto error

echo jmNatMax=%jmNatMax% 
jmNatMax=6 
jm_toesd_gr1.png jm_toesd_gr2.png jm_toesd_gr3.png

The first graph, of "distortion", shows more distortion at every N, compared to the graph above without dithering. Dithering always increases RMSE distortion. Therefore, the graph of "D" shows decreased values at every N compared to no dithering. Also note that increasing N sometimes increases distortion, which decreases D, and hence makes J negative.

This time, the largest peak is at N=16. The result is:

jm_toesd_x.png

And we can create the N=6 version:

%IMG7%magick ^
  toes.png ^
  -colors 6 ^
  jm_toesd_6.png
jm_toesd_6.pngjpg

Dithering works because the human eye doesn't evaluate individual pixels, but groups of pixels together.

Why does dithering always increase RMSE distortion? IM finds the best set of N colours, then changes each output pixel to the closest of this set. When dithering is used, the error at each pixel is carried to adjacent pixels, and they may be adjusted if this will reduce the cumulative error. Thus, we get the best possible RMSR score without dithering, and dithering can only make it worse.

Dither plus blur

Can we devise a scoring system that more closely matches the human visual system? This is rather subjective, but it seems to me that a small blur after the dithering does the trick.

call %PICTBAT%jumpMethod %SRC% jm_toesdb_x.png 1 19 "-colors _XX_ -blur 0x1" jm_toesdb_gr_XX_.png

if ERRORLEVEL 1 goto error

echo jmNatMax=%jmNatMax% 
jmNatMax=6 
jm_toesdb_gr1.png jm_toesdb_gr2.png jm_toesdb_gr3.png

The distortion at every N is lower than for the "no dither, no blur" case. It is also better than for the "dither only" case.

jm_toesdb_x.png

So, I suggest that to use the "jump" method to find the best N for dithering, we should use "-blur 0x1" in the jump process. This gives us a good value for N. When using this to make the final image, we would omit the blur. (Or we could include it, if we want, of course.)

No dither, just blur

This raises the obvious question: what happens if we blur, without dithering?

call %PICTBAT%jumpMethod %SRC% jm_toesb_x.png 1 19 "+dither -colors _XX_ -blur 0x1" jm_toesb_gr_XX_.png

if ERRORLEVEL 1 goto error

echo jmNatMax=%jmNatMax% 
jmNatMax=6 
jm_toesb_gr1.png jm_toesb_gr2.png jm_toesb_gr3.png

Again, the distortions are better than with the non-dither non-blur process, though worse than the dither-plus-blur process.

(Blurring smooths the boundaries between colours, which is usually where distortion is worst, and the act of smoothing will create intermediate colours that are likely to be more accurate, thus reducing overall distortion.)

Here is the best result, at N=6:

jm_toesb_x.png

Here is the next best result, at N=2:

%IMG7%magick ^
  toes.png ^
  +dither -colors 2 ^
  -blur 0x1 ^
  jm_toesb_2.png
jm_toesb_2.pngjpg

Just the hue

A 3D object, illuminated by white light, varies in lightness and chroma (or saturation) because of shadowing, but the hue is reasonably constant.

The script justHue.bat reduces the image to just the hue:

call %PICTBAT%justHue ^
  toes.png ^
  jm_justhue.png
jm_justhue.pngjpg

Use the jump method, without dithering or blurring. We have only one dimension.

set jmPdims=1

call %PICTBAT%jumpMethod jm_justhue.png jm_toesjh_x.png 1 19 "+dither -colors _XX_" jm_toesjh_gr_XX_.png

if ERRORLEVEL 1 goto error

echo jmNatMax=%jmNatMax% 
jmNatMax=1 

The best N=1, which isn't particularly useful.

jm_toesjh_gr1.png jm_toesjh_gr2.png jm_toesjh_gr3.png

Here is the best result, at N=1:

jm_toesjh_x.png

Try again, starting at 2:

call %PICTBAT%jumpMethod jm_justhue.png jm_toesjh2_x.png 2 19 "+dither -colors _XX_" jm_toesjh2_gr_XX_.png

if ERRORLEVEL 1 goto error

echo jmNatMax=%jmNatMax% 
jmNatMax=6 
jm_toesjh2_gr1.png jm_toesjh2_gr2.png jm_toesjh2_gr3.png

Here is the best result, at N=6:

jm_toesjh2_x.png

N=5 isn't far behind, so let's try that:

%IMG7%magick ^
  jm_justhue.png ^
  +dither -colors 5 ^
  jm_justhue_5.png
jm_justhue_5.pngjpg

The N=5 solution has no gray pixels, so N=6 is indeed the best solution.

Conclusions

This is a simple but powerful method for answering the question, "How many colours are really in this image?"

The jump method is one component of K-clustering. A variation is used in Multi-phase searching.

Scripts

For convenience, .bat scripts are also available in a single zip file. See Zipped BAT files.

jumpMethod.bat

rem Applies a series of distortions,
rem   using "jump" method to find the best improvement.
rem %1 input image
rem %2 output image
rem %3 first number
rem %4 last number
rem %5 IM command, with _XX_, which will be substituted.
rem      For example: "-colors _XX_"
rem %6 optional template for gnuplot graph images, with _XX_
@rem
@rem Also uses:
@rem   jmPdims [3] Smaller values give more weight to jumps at high distortions.
@rem
@rem See https://en.wikipedia.org/wiki/Determining_the_number_of_clusters_in_a_data_set
@rem
@rem Updated:
@rem   6-August-2022 for IM v7
@rem

@if "%~5"=="" findstr /B "rem @rem" %~f0 & exit /B 1

@setlocal

@call echoOffSave

call %PICTBAT%setInOut %1 jm


set INFILE=%1
set OUTFILE=%2

set nStart=%3
set nEnd=%4

set CMD=%~5

set TMPLT=%6
if "%TMPLT%"=="." set TMPLT=

set TMPLT_SVG=%~dpn6.svg

set TMPDIR=\temp\

set GPDATA1=%TMPDIR%jm_gp1.csv
set GPDATA2=%TMPDIR%jm_gp2.csv
set GPDATA3=%TMPDIR%jm_gp3.csv
set GPDATA4=%TMPDIR%jm_gp4.csv
set GPDATA5=%TMPDIR%jm_gp5.csv

set GPSVG1=%TMPLT_SVG:_XX_=1%
set GPSVG2=%TMPLT_SVG:_XX_=2%
set GPSVG3=%TMPLT_SVG:_XX_=3%
set GPSVG4=%TMPLT_SVG:_XX_=4%
set GPSVG5=%TMPLT_SVG:_XX_=5%

echo i,v >%GPDATA1%
echo i,v >%GPDATA2%
echo i,v >%GPDATA3%
echo i,v >%GPDATA4%
echo i,v >%GPDATA5%

if "%jmPdims%"=="" set jmPdims=3
set Y=%jmPdims%/2

set DPREV=0
set maxJ=0
set NatMax=0

if %nStart% LEQ %nEnd% (
  set nStep=1
) else (
  set nStep=-1
)

if %nStart%==1 (
  set ignoreThis=0
) else (
  set ignoreThis=1
  set /A nStart-=%nStep%
)

echo nStart=%nStart% ignoreThis=%ignoreThis%

for /L %%N in (%nStart%,%nStep%,%nEnd%) do (

  set sCMD=!CMD:_XX_=%%N!
  echo sCMD=!sCMD!

  for /F "usebackq" %%L in (`%IM7DEV%magick ^
    %INFILE% ^
    ^( +clone ^
       !sCMD! ^
    ^) ^
    -metric RMSE ^
    -precision 19 ^
    -format %%[distortion] ^
    -compare ^
    info:`) do set DIST=%%L

rem FIXME: Beware DIST=0

  for /F "usebackq" %%L in (`%IMG7%magick identify ^
    -precision 19 ^
    -format "D=%%[fx:pow(!DIST!,-(%Y%))]\n" ^
    xc:`) do set %%L

  for /F "usebackq" %%L in (`%IMG7%magick identify ^
    -precision 19 ^
    -format "J=%%[fx:!D!-!DPREV!]\nBPB=%%[fx:!D!/%%N]\n" ^
    xc:`) do set %%L

  for /F "usebackq" %%L in (`%IMG7%magick identify ^
    -format "isMax=%%[fx:!J!>!maxJ!?1:0]" ^
    xc:`) do set %%L

  if !ignoreThis!==0 (
    if !isMax!==1 (
      set maxJ=!J!
      set NatMax=%%N
      echo %%N,!j! >>%GPDATA4%
    )
    if not "%TMPLT%"=="" (
      echo %%N,!DIST! >>%GPDATA1%
      echo %%N,!D! >>%GPDATA2%
      echo %%N,!j! >>%GPDATA3%
      echo %%N,!BPB! >>%GPDATA5%
    )
  )
  echo %%N, DIST=!DIST!  D=!D! J=!J!  BPB=!BPB!  isMax=!isMax!

  set DPREV=!D!

  set ignoreThis=0
)

echo maxJ=%maxJ%  NatMax=%NatMax%

set sCMD=!CMD:_XX_=%NatMax%!
echo sCMD=!sCMD!

%IM7DEV%magick ^
  %INFILE% ^
  !sCMD! ^
  %OUTFILE%

if "%TMPLT%"=="" goto end

gnuplot-base ^
  -c %PICTBAT%plotScr.gp ^
  %GPDATA1% %GPSVG1% 450 400 "2" ^
  "N" "distortion" ^
  "set key off"

gnuplot-base ^
  -c %PICTBAT%plotScr.gp ^
  %GPDATA2% %GPSVG2% 450 400 "2" ^
  "N" "D" ^
  "set key off"

gnuplot-base ^
  -c %PICTBAT%plotScr.gp ^
  %GPDATA3% %GPSVG3% 450 400 "2" ^
  "N" "J" ^
  "set key off"

gnuplot-base ^
  -c %PICTBAT%plotScr.gp ^
  %GPDATA4% %GPSVG4% 450 400 "2" ^
  "N" "best J so far" ^
  "set key off"

gnuplot-base ^
  -c %PICTBAT%plotScr.gp ^
  %GPDATA5% %GPSVG5% 450 400 "2" ^
  "N" "BPB" ^
  "set key off"

set GR1=%TMPLT:_XX_=1%
set GR2=%TMPLT:_XX_=2%
set GR3=%TMPLT:_XX_=3%
set GR4=%TMPLT:_XX_=4%
set GR5=%TMPLT:_XX_=5%
set GRALL=%TMPLT:_XX_=all%

call %PICTBAT%gpTrimY %GPSVG1% %GR1%
call %PICTBAT%gpTrimY %GPSVG2% %GR2%
call %PICTBAT%gpTrimY %GPSVG3% %GR3%
call %PICTBAT%gpTrimY %GPSVG4% %GR4%
call %PICTBAT%gpTrimY %GPSVG5% %GR5%

%IMG7%magick  %GR1% %GR2% %GR3% +append +repage %GRALL%

:end

call echoRestore

endlocal &set jmOUTFILE=%OUTFILE%& set jmNatMax=%NatMax%

justHue.bat

rem From image %1, makes version with just hue.
@rem
@rem Updated:
@rem   25-September-2022 for IM v7.
@rem

@if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1

@setlocal

@call echoOffSave

@call %PICTBAT%setInOut %1 jh

if not "%2"=="" if not "%2"=="." set OUTFILE=%2

%IMG7%magick ^
  %INFILE% ^
  -colorspace HCL ^
  -channel G -level 3%%,5%% ^
  -channel B -evaluate set 50%% ^
  +channel ^
  -colorspace sRGB ^
  %OUTFILE%


call echoRestore

All images on this page were created by the commands shown, using:

%IMG7%magick -version
Version: ImageMagick 7.1.0-49 Q16-HDRI x64 7a3f3f1:20220924 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI OpenCL 
Delegates (built-in): bzlib cairo freetype gslib heic jng jp2 jpeg jxl lcms lqr lzma openexr pangocairo png ps raqm raw rsvg tiff webp xml zip zlib
Compiler: Visual Studio 2022 (193331630)

To improve internet download speeds, some images may have been automatically converted (by ImageMagick, of course) from PNG or TIFF or MIFF to JPG.

Source file for this web page is jumpmeth.h1. To re-create this web page, execute "procH1 jumpmeth".


This page, including the images, is my copyright. Anyone is permitted to use or adapt any of the code, scripts or images for any purpose, including commercial use.

Anyone is permitted to re-publish this page, but only for non-commercial use.

Anyone is permitted to link to this page, including for commercial use.


Page version v1.0 13-August-2017.

Page created 26-Sep-2022 15:54:54.

Copyright © 2022 Alan Gibson.