snibgo's ImageMagick pages

Polygonal pixelation

Simplify images by pixelating into polygons or other shapes.

Pixelation sometimes means transforming an image so the result contains squares larger than one pixel, where each square is a constant colour that is the mean of the corresponding input pixels. For example, news media often pixelate faces to obscure identities. This is easily done in IM, with two -scale operations.

A more difficult task is to pixelate into other shapes: triangles, hexagons, or arbitrary polygons, or any other shapes.

To calculate the mean, we do the arithmetic in whatever colorspace the image is encoded in. On this page, most examples are encoded in sRGB. If desired, images could be encoded as linear RGB before doing the arithmetic.

This page was inspired by a post on the IM forum: Hexagon Filter?.

Sample input

The usual toes.png is a sample input.

set SRC=toes.png
toes.jpg

We will also use a simple graphic:

set SRC2=ppix_src2.png

%IMG7%magick ^
  %SRC% ^
  +antialias ^
  -fill #88f -colorize 100 ^
  -fill #f00 -draw "circle 50,60 50,10" ^
  -fill None -strokewidth 20 ^
  -stroke #0f0 -draw "line 30,30 200,200" ^
  -stroke #f8f -draw "line 30,180 220,20" ^
  -stroke None ^
  -fill #ff0 -draw "rectangle 150,50 230,180" ^
  %SRC2%
ppix_src2.png

Rectangles

When the desired polygons are rectangles that are equal sizes and aligned horizontally and vertically, we can use -scale. The first -scale shrinks the image to one pixel per output rectangle, where that pixel is the mean colour of the corresponding input pixels. The second -scale enlarges to the same size as the input, "spreading" each pixel to the required size of the rectangle.

If the input dimensions are not exactly divisable by the rectangle dimensions, the rectangles will not be exactly the same size.

For example, suppose we want rectangles of width 18 pixels and height 10:

set RW=18
set RH=10

%IMG7%magick ^
  %SRC% ^
  -set option:MYSIZE %%wx%%h ^
  -scale "%%[fx:w/%RW%]x%%[fx:h/%RH%]^!" ^
  -scale "%%[MYSIZE]^!" ^
  ppix_rect1.png
ppix_rect1.png
%IMG7%magick ^
  %SRC2% ^
  -set option:MYSIZE %%wx%%h ^
  -scale "%%[fx:w/%RW%]x%%[fx:h/%RH%]^!" ^
  -scale "%%[MYSIZE]^!" ^
  ppix_rect1_s2.png
ppix_rect1_s2.png

Arbitrary polygons

The program polyPixB.exe takes two input images and makes one output. The first is the image that we want to be pixelated. The second is a map of the desired pixelation. The map must be the same size as the main image, and opaque, and grayscale.

The code is written as both a standalone program polyPixB.c and as a magick process module polypix.c. Both of these use polypix.inc, which does the real work.

As a standalone program, we need to specify the two input files and the output file. Program options can be given in any order.

%IM7DEV%polyPixB help 
Pixelates an image.
Usage: polyPixB [OPTION]...
Options are:
  i, infile string    main input file
  map, map string     input map file
  o, outfile string   output file
  m, method string    'mean' or 'centroid'
  f, file string      Write verbose text to 'stderr' or 'stdout'
  v, verbose          write text information

As a process module, the code needs a list of exactly two images. It replaces that list with a single image that is pixelated.

%IM7DEV%magick xc: -process 'polypix help' NULL: 
Pixelates an image.
Usage: -process 'polypix [OPTION]...'
Options are:
  m, method string    'mean' or 'centroid'
  f, file string      Write verbose text to 'stderr' or 'stdout'
  v, verbose          write text information

Polypix finds the unique colours in the pixelation map. The pixels in the map that are the same colour as each other define a set of pixels (that need not be contiguous). For each set, the corresponding pixels from the main input image are used to calculate a colour for that set. By default, the method used to calculate the colour is the arithmetic mean. An alternative method uses the centroid of the set of pixels.

For example, the pixelation map may represent bricks: different rectangles that are not aligned. Suppose the required bricks are all height 10 pixels, with widths alternating between 12 and 24 pixels.

To make the pixelation map, we start by making a small "prototype" image that shows four bricks, in four different colours. Then we tile that up to the same size as the input, then we use -connected-components to make each brick a different shade of gray.

In the prototype image, I ensure the bricks are different colours. I don't use black or white for any of the colours, so I can create borders (see Bordering each polygon below).

set BH=10
set BW1=12
set BW2=24

set /A PW=%BW1%+%BW2%
set /A PH=%BH%*2

set /A X0=%BW1%+(%BW2%-%BW1%)/2
set /A X1=2*%BW1%+(%BW2%-%BW1%)/2

%IMG7%magick ^
  -size %PW%x%PH% xc:#44f ^
  -fill #f80 -draw "rectangle 0,0 %%[fx:%BW1%-1],%%[fx:%BH%-1]" ^
  -fill #8f8 -draw "rectangle %BW1%,0 %%[fx:%PW%-1],%%[fx:%BH%-1]" ^
  -fill #f0f -draw "rectangle %X0%,%BH% %X1%,%%[fx:%PH%-1]" ^
  -write mpr:PROTO ^
  -write ppix_proto1.png ^
  +delete ^
  %SRC% ^
  -size %%wx%%h ^
  -delete 0 ^
  tile:mpr:PROTO ^
  -write ppix_proto_t1.png ^
  -connected-components 4 ^
  -alpha off ^
  -auto-level ^
  ppix_proto_c1.png
ppix_proto1.png ppix_proto_t1.png ppix_proto_c1.png
%IM7DEV%polyPixB ^
  infile %SRC% map ppix_proto_c1.png outfile ppix_rect2.png
ppix_rect2.png

For this example, use the standalone program.

%IM7DEV%polyPixB ^
  infile %SRC2% map ppix_proto_c1.png outfile ppix_rect2_s2.png
ppix_rect2_s2.png

As the previous example, using the process module.

%IM7DEV%magick ^
  %SRC2% ppix_proto_c1.png ^
  -process polypix ^
  ppix_rect2_s2p.png
ppix_rect2_s2p.png

Taking the mean of the set of colours will give different results depending on whether we do the work in non-linear sRGB or linear RGB.

%IM7DEV%magick ^
  %SRC% ^
  -colorspace RGB ^
  ppix_proto_c1.png ^
  -process polypix ^
  -colorspace sRGB ^
  ppix_rect2_l.png
ppix_rect2_l.png

As the previous example, using the process module.

%IM7DEV%magick ^
  %SRC2% ^
  -colorspace RGB ^
  ppix_proto_c1.png ^
  -process polypix ^
  -colorspace sRGB ^
  ppix_rect2_s2p_l.png
ppix_rect2_s2p_l.png

We can do blink-comparisons like this:

%IMG7%magick ^
  -loop 0 -delay 50 ^
  -gravity NorthWest ^
  ( ppix_rect2.png ^
    -annotate +5+5 "non-linear sRGB" ) ^
  ( ppix_rect2_l.png ^
    -annotate +5+5 "linear RGB" ) ^
  ppix_blnk.gif

There is little change, except bottom-right.

ppix_blnk.gif
%IMG7%magick ^
  -loop 0 -delay 50 ^
  -gravity NorthWest ^
  ( ppix_rect2_s2p.png ^
    -annotate +5+5 "non-linear sRGB" ) ^
  ( ppix_rect2_s2p_l.png ^
    -annotate +5+5 "linear RGB" ) ^
  ppix_blnk_s2.gif

There is obvious change.

ppix_blnk_s2.gif

Instead of using the mean colour of each rectangle, we can use the colour at the centroid of each rectangle.

%IM7DEV%polyPixB ^
  infile %SRC% ^
  map ppix_proto_c1.png ^
  method centroid ^
  outfile ppix_rect2_c.png
ppix_rect2_c.png
%IM7DEV%polyPixB ^
  infile %SRC2% ^
  map ppix_proto_c1.png ^
  method centroid ^
  outfile ppix_rect2_c_s2.png
ppix_rect2_c_s2.png
%IM7DEV%magick ^
  %SRC2% ppix_proto_c1.png ^
  -process 'polypix method Centroid' ^
  ppix_rect2_s2pc.png
ppix_rect2_s2pc.png

Where the centroid falls between pixels, the code interpolates between nearby pixels.

When used as a process module, we can use a different interpolation method. "Nearest" will use the colour of the nearest pixel to the centroid, so the output will contain no new colours.

%IM7DEV%magick ^
  %SRC2% ppix_proto_c1.png ^
  -interpolate Nearest ^
  -process 'polypix method Centroid' ^
  ppix_rect2_s2pn.png
ppix_rect2_s2pn.png

For ordinary photos, the centroid method is likely to make a more varied result than the mean method. For an intermediate result, we could blend the results, or blur the input to PolyPix..

Suppose we want triangles of width 10 pixels and height 20 pixels:

set TW=11
set TH=20

set /A PW=%TW%+1
set /A PH=%TH%*2

set /A PWm1=%PW%-1
set /A PHm1=%PH%-1
set /A TWm1=%TW%-1
set /A THm1=%TH%-1

%IMG7%magick ^
  -size %PW%x%PH% xc:#44f ^
  +antialias ^
  -fill #808 -draw "rectangle 0,0 %PWm1%,%THm1%" ^
  -fill #f80 -draw "polygon 0,%THm1% %%[fx:%TWm1%/2],0 %TWm1%,%THm1%" ^
  -fill #8f8 -draw "polygon 0,%PHm1% %%[fx:%TWm1%/2],%TH% %TWm1%,%PHm1%" ^
  -write mpr:PROTO ^
  -write ppix_proto2.png ^
  +delete ^
  %SRC% ^
  -size %%wx%%h ^
  -delete 0 ^
  tile:mpr:PROTO ^
  -write ppix_proto_t2.png ^
  -connected-components 4 ^
  -alpha off ^
  -auto-level ^
  ppix_proto_c2.png
ppix_proto2.png ppix_proto_t2.png ppix_proto_c2.png
%IM7DEV%polyPixB ^
  infile %SRC% map ppix_proto_c2.png outfile ppix_tri1.png
ppix_tri1.png
%IM7DEV%polyPixB ^
  infile %SRC2% map ppix_proto_c2.png outfile ppix_tri1_s2.png
ppix_tri1_s2.png

For hexagons, we use the method shown at Polygonal tiling: hexagons to make a prototype, which we tile to a larger size, then use connected-components to make each hexagon a different shade of gray. We use this as the pixelation map for -process polypix.

rem Half the hexagon width; length of each side.
set HexRad=15

set HexH=(%HexRad%*sqrt(3))

rem Drawing starts at left-hand vertex, procedes clockwise.
set HexPoly=^
  0,%%[fx:%HexH%/2] ^
  %%[fx:%HexRad%/2],0 ^
  %%[fx:%HexRad%*3/2],0 ^
  %%[fx:%HexRad%*2],%%[fx:%HexH%/2] ^
  %%[fx:%HexRad%*3/2],%%[fx:%HexH%] ^
  %%[fx:%HexRad%/2],%%[fx:%HexH%]

set HexPolyWd=^
  0,%%[fx:%HexH%/2] ^
  %%[fx:%HexRad%/2-2],0 ^
  %%[fx:%HexRad%*3/2+2],0 ^
  %%[fx:%HexRad%*2+2],%%[fx:%HexH%/2] ^
  %%[fx:%HexRad%*3/2+2],%%[fx:%HexH%] ^
  %%[fx:%HexRad%/2-2],%%[fx:%HexH%]

set col1=#b11
set col2=#118
set col3=#0a0
set col4=#818
set col5=#aa0

rem  -fill %col2% -draw "polygon %HexPoly%"

rem  -fill %col1% -draw "color %HexRad%,%%[fx:%HexH%/2] floodfill"

%IM7DEV%magick ^
  toes.png ^
  -set option:MYSIZE %%wx%%h ^
  -write mpr:INP ^
  +delete ^
  -size %%[fx:3*%HexRad%]x%%[fx:%HexH%*3] ^
  xc:%col2% ^
  +antialias ^
  -fill %col3% -draw "translate 0,%%[fx:%HexH%] polygon %HexPolyWd%" ^
  -fill %col1% -draw "translate 0,%%[fx:%HexH%*2] polygon %HexPolyWd%" ^
  -roll -%%[fx:%HexRad%*3/2]-%%[fx:%HexH%/2] ^
  -fill %col1% -draw "polygon %HexPoly%" ^
  -fill %col2% -draw "translate 0,%%[fx:%HexH%] polygon %HexPoly%" ^
  -fill %col3% -draw "translate 0,%%[fx:%HexH%*2] polygon %HexPoly%" ^
  +write ppix_hex3.png ^
  -write mpr:HEX3 +delete ^
  -size %%[MYSIZE] ^
  tile:mpr:HEX3 ^
  -write ppix_hex3_t.png ^
  -connected-components 4 ^
  -colorspace Gray ^
  -alpha off ^
  -auto-level ^
  -write ppix_hex3_tc.png ^
  mpr:INP ^
  +swap ^
  -process polypix ^
  ppix_hex3_result.png

Prototype: ppix_hex3.png

ppix_hex3.png

tiled result: ppix_hex3_t.png

ppix_hex3_t.png

Pixelation map: ppix_hex3_tc.png

ppix_hex3_tc.png

Result: ppix_hex3_result.png

ppix_hex3_result.png

The shapes in the pixelation map do not need to be polygons:

Do the work in a single command.
Save and show intermediate images.

%IM7DEV%magick ^
  -size 60x60 xc:%col2% ^
  +antialias ^
  -fill %col1% -draw "circle 14.5,14.5,14.5,28.6" ^
  -fill %col3% -draw "translate +30+0 circle 14.5,14.5,14.5,28.6" ^
  -fill %col4% -draw "translate +0+30 circle 14.5,14.5,14.5,28.6" ^
  -fill %col5% -draw "translate +30+30 circle 14.5,14.5,14.5,28.6" ^
  -write mpr:PROTO ^
  -write ppix_nc_proto.png ^
  +delete ^
  %SRC% ^
  -write mpr:INP ^
  -size %%wx%%h ^
  -delete 0 ^
  tile:mpr:PROTO ^
  -write ppix_nc_proto_t.png ^
  -connected-components 4 ^
  -colorspace Gray ^
  -alpha off ^
  -auto-level ^
  -write ppix_nc_proto_c.png ^
  mpr:INP ^
  +swap ^
  -process polypix ^
  ppix_nc_out.png
ppix_nc_proto.png ppix_nc_proto_t.png ppix_nc_proto_c.png ppix_nc_out.png

With the same map, pixelate the other source:

%IM7DEV%polyPixB ^
  infile %SRC2% map ppix_nc_proto_c.png ^
  outfile ppix_nc_out_s2.png
ppix_nc_out_s2.png

Do the work in a single command.
Save and show intermediate images.

%IM7DEV%magick ^
  -size 225x150 xc:#88f ^
  +antialias ^
  -fill #f00 -draw "translate  95, 65 circle 0,0 0,23" ^
  -fill #f00 -draw "translate  95,115 circle 0,0 0,23" ^
  -fill #f00 -draw "translate 170, 65 circle 0,0 0,23" ^
  -fill #f00 -draw "translate 170,115 circle 0,0 0,23" ^
  -fill #0f0 -draw "translate 104, 80 circle 0,0 0,22" ^
  -fill #00f -draw "translate 135, 60 circle 0,0 0,18" ^
  -fill #0f0 -draw "translate 104, 30 circle 0,0 0,22" ^
  -fill #00f -draw "translate 135,110 circle 0,0 0,18" ^
  -fill #00f -draw "translate  60, 60 circle 0,0 0,18" ^
  -crop 75x50+75+50 +repage ^
  -write mpr:PROTO ^
  -write ppix_nc2_proto.png ^
  +delete ^
  %SRC% ^
  -write mpr:INP ^
  -size %%wx%%h ^
  -delete 0 ^
  tile:mpr:PROTO ^
  -write ppix_nc2_proto_t.png ^
  -connected-components 4 ^
  -colorspace Gray ^
  -alpha off ^
  -auto-level ^
  -write ppix_nc2_proto_c.png ^
  mpr:INP ^
  +swap ^
  -process polypix ^
  ppix_nc2_out.png
ppix_nc2_proto.png ppix_nc2_proto_t.png ppix_nc2_proto_c.png ppix_nc2_out.png

With the same map, pixelate the other source:

%IM7DEV%polyPixB ^
  infile %SRC2% map ppix_nc2_proto_c.png ^
  outfile ppix_nc2_out_s2.png
ppix_nc2_out_s2.png

Shapes by saliency

We can make polygons with sizes that depend on the amount of detail in the image. We use detail as a proxy for saliency. The method has three stages:

  1. Make a slope magnitude image; dilate the light portions and increase contrast.
  2. For the pixelation map, make an image of polygons: create non-black dots (of different colours) with probability according to the lightness, with maximum probability 0.05 (ie 1 in 20 pixels); do a -connected-components to make the non-black pixels different colours. Make a list of the non-black pixels, and pipe that to a voronoi generator.
  3. Use that as the pixelation map for polyPixB.exe.

Step 1.

call %PICTBAT%slopeMag %SRC% ppix_slopmag.png

For slopemag.bat, see Details, details.

ppix_slopmag.png

Step 2.

%IMG7%magick ^
  ppix_slopmag.png ^
  -morphology dilate disk:3 ^
  -sigmoidal-contrast 5,70%% -auto-level ^
  +write ppix_sm.png ^
  -fx "rnd=rand(); u*0.05 > rnd ? u : 0" ^
  -connected-components 4 -auto-level ^
  +write ppix_sm2.png ^
  -transparent Black ^
  sparse-color:- | %IMG7%magick ^
  %SRC% ^
  -sparse-color voronoi @- ^
  ppix_voron.png
ppix_sm.png ppix_sm2.png ppix_voron.png

Step 3.

%IM7DEV%polyPixB ^
  infile %SRC% map ppix_voron.png ^
  outfile ppix_det1.png
ppix_det1.png

Step 3, with the other source image.

%IM7DEV%polyPixB ^
  infile %SRC2% map ppix_voron.png ^
  outfile ppix_det1_s2.png
ppix_det1_s2.png

Detail as saliency

We can draw circles with radii inversely proportional to the amount of detail; more detailed areas have smaller circles. We order the drawing so larger circles are drawn first. After drawing the circles, different components will share the same colour, so -connected-components makes them different colours. The task is a bit messy, so we implement it as a script sal2circ.bat.

It may seem counter-intuitive that the most salient areas are rendered with the smallest circles instead of the largest. The reason is that circles will take the mean of the corresponding pixels, so larger circles will effectively blur the image (where is doesn't matter) more than small circles (where it does matter).

Use detail as a saliency image.

call %PICTBAT%slopeMag %SRC% ppix_detsal1.png
ppix_detsal1.png

Modify the saliency image.

%IMG7%magick ^
  ppix_detsal1.png ^
  -morphology dilate disk:3 ^
  -sigmoidal-contrast "5,70%%" -auto-level ^
  ppix_detsal1_mod.png
ppix_detsal1_mod.png

We use the modified detail image as the saliency:

set s2c_SEED=-seed 1234

call %PICTBAT%sal2circ ^
  %SRC% ppix_detsal1_mod.png . ppix_circ1.png
ppix_circ1.png

Increase the maximum radius,
and use a power factor to decrease the mean radius.

call %PICTBAT%sal2circ ^
  %SRC% ppix_detsal1_mod.png . ppix_circ2.png . . 40 . 3
ppix_circ2.png

As previous, but more so.

call %PICTBAT%sal2circ ^
  %SRC% ppix_detsal1_mod.png . ppix_circ3.png . . 60 . 15
ppix_circ3.png

As previous, but even more so.

call %PICTBAT%sal2circ ^
  %SRC% ppix_detsal1_mod.png . ppix_circ4.png . . 100 . 30
ppix_circ4.png

To improve the approximation, we can iterate the process. For the next iteration(s), instead of using detail as the source for the circles, use the difference between the result and the source.

The script can generate different shapes:

set s2c_SHAPE=square

call %PICTBAT%sal2circ ^
  %SRC% ppix_detsal1_mod.png . ppix_shp1.png
ppix_shp1.png
set s2c_SHAPE=roundedSquare

call %PICTBAT%sal2circ ^
  %SRC% ppix_detsal1_mod.png . ppix_shp2.png

set s2c_SHAPE=
ppix_shp2.png

Difference as saliency

We can use a difference between images as the saliency.

Make an image that is lightest where the image most deviates from its mean:

%IMG7%magick ^
  %SRC% ^
  -set option:MYSIZE %%wx%%h ^
  ( +clone -scale "1x1^!" -scale "%%[MYSIZE]^!" ) ^
  -compose Difference -composite ^
  -grayscale RMS -auto-level ^
  ppix_diff.png
ppix_diff.png

Use the difference as saliency.

call %PICTBAT%sal2circ ^
  %SRC% ppix_diff.png . ppix_diff_out.png
ppix_diff_out.png

The output has smallest circles where the input is most different from the mean, which are roughly the highlights and shadows, so the result is somewhat literal.

In the next example, we write the circle list to a file, ppix_circles.lis. If the file already exists, the script will append to the file. So we delete it first. We also make a difference image that is lightest where ppix_circ_s1.png most deviates from the source and write it to ppix_circ_s1.png.

del ppix_circles.lis 2>nul

call %PICTBAT%sal2circ ^
  %SRC% ppix_detsal1_mod.png ppix_circles.lis ^
  ppix_circ_s1.png ppix_circ_s1_d.png . 40
ppix_circ_s1.png ppix_circ_s1_d.png

Here are the first few lines of ppix_circles.lis:

head ppix_circles.lis 
fill gray(09.688868692540245%) circle 160,229,160,238
fill gray(09.639429610608834%) circle 85,45,85,54
fill gray(09.635156637436102%) circle 101,31,101,40
fill gray(09.020523533464942%) circle 88,37,88,46
fill gray(09.018081940947585%) circle 81,56,81,65
fill gray(08.80933881942855%) circle 101,32,101,40
fill gray(07.530632784485389%) circle 122,33,122,40
fill gray(07.530632784485389%) circle 121,36,121,43
fill gray(07.530632784485389%) circle 121,34,121,41
fill gray(07.530632784485389%) circle 120,37,120,44

Use the difference as saliency,
adding to the circles previously generated,
ignoring new circles that are larger than 20.

Also show the new difference.

call %PICTBAT%sal2circ ^
  %SRC% ppix_circ_s1_d.png ppix_circles.lis ^
  ppix_diff2_out.png ppix_diff2_out_d.png . 40 20
ppix_diff2_out.png ppix_diff2_out_d.png

Repeat the previous.

Use the difference as saliency,
adding to the circles previously generated,
ignoring new circles that are larger than 20.

Also show the new difference.

call %PICTBAT%sal2circ ^
  %SRC% ppix_diff3.png ppix_circles.lis ^
  ppix_diff3_out.png ppix_diff3_out_d.png . 40 20
ppix_diff3_out.png ppix_diff3_out_d.png

Convergence is slow, so the script sal2circRep.bat can repeat as often as we want:

call %PICTBAT%sal2circRep ^
  toes.png ^
  ppix_diff.png ppix_rep1_out.png ^
  ". 40 . " ^
  ". 40 20" ^
  50
ppix_rep1_out.png

The result is quite a good representation of the toes image, despite having very few small circles.

Each repetition adds to the circle list, so drawing the circles takes longer at each repetition. It would be useful to remove from the list circles that are entirely overwritten by later (smaller) circles, but this would be difficult.

We can use different shapes for different iterations, not shown here.

Bordering each polygon

We can make an image for borders using a simple edge detector which makes output pixels black where the input pixel is different to the pixel to the right, or the pixel below, or the pixel to the right and below:

%IMG7%magick ^
  ppix_proto_c2.png ^
  -fx "u==p[1,0]? (u==p[0,1]? (u==p[1,1]? 1 :0) :0) :0" ^
  ppix_bord1.png
ppix_bord1.png

The right-most column of the result is not what we want. (The bottom row happens to be okay, but in general could be bad.) If this matters, we should create a larger tiled image and border image, and trim them.

We can use this to paint borders over the pixelated image:

%IMG7%magick ^
  ppix_tri1.png ^
  ( ppix_bord1.png -transparent White ) ^
  -compose Over -composite ^
  ppix_tri1_b.png
ppix_tri1_b.png

But the output polygon colours then do not represent the exact mean of the corresponding input pixels. For accuracy, we should first modify the polygon image.

%IMG7%magick ^
  ppix_proto_t2.png ^
  ( ppix_bord1.png -transparent White ) ^
  -compose Over -composite ^
  -write ppix_proto_t2b.png ^
  -connected-components 4 ^
  -alpha off ^
  -auto-level ^
  ppix_proto_c2b.png
ppix_proto_t2b.png ppix_proto_c2b.png
%IM7DEV%polyPixB ^
  infile %SRC% map ppix_proto_c2b.png ^
  outfile ppix_tri1b.png
ppix_tri1b.png
%IM7DEV%polyPixB ^
  infile %SRC2% map ppix_proto_c2b.png ^
  outfile ppix_tri1b_s2.png
ppix_tri1b_s2.png

PolyPixB has no special processing for polygon borders. The border pixels are all the same colour, so the code treats them as a single polygon, and sets each output pixel to the mean of the input pixels that correspond to the polygon.

Of course, if we don't want the border to be this mean colour, we can use ppix_bord1.png to paint the black borders over the pixelated image:

%IMG7%magick ^
  ppix_tri1b.png ^
  ( ppix_bord1.png ^
    -transparent White ^
  ) ^
  -compose Over -composite ^
  ppix_tri1b_b.png
ppix_tri1b_b.png

Use a different colour for the border.

%IMG7%magick ^
  ppix_tri1b.png ^
  ( ppix_bord1.png ^
    -fill #888 -opaque Black ^
    -transparent White ^
  ) ^
  -compose Over -composite ^
  ppix_tri1b_b2.png
ppix_tri1b_b2.png

Performance

For polyPixB.exe, the time taken depends mostly on the number of unique colours in the pixelation map. The worst case is when the image pixels are all different from each other, so all the "polygons" are just a single pixel.

An earlier (unpublished) version of this code allowed the pixelation map to use any colours, provided they were different. For performance reasons, I now restrict the colours to be shades of gray, so the map must contain only one channel. This enables the use of much faster algorithms that take seconds rather than hours.

For a test, we make an image of ten million pixels, each a different shade of gray.

%IMG7%magick ^
  -size 10000x1000 xc: ^
  -fx "(i+j*w)/%%[fx:w*h]" ^
  -colorspace Gray ^
  -define quantum:format=floating-point -depth 32 ^
  ppix_large.miff

We use ppix_large.miff as the main image, and as the pixelation map.

%IM7DEV%polyPixB infile ppix_large.miff map ppix_large.miff outfile ppix_large_out.miff
0 00:00:02

This takes about 3 seconds.

With identical grayscale inputs, the output should be unchanged. Test this:

%IMG7%magick compare -metric RMSE ppix_large.miff ppix_large_out.miff NULL: 
0 (0)

As expected, the output is identical to the input.

Polygonal gradients

Each polygon can be a gradient. For this method, we start with an image that is to be pixelated, and an image that has white lines representing the polygon borders. These make an image that has the main image colours at the polygon borders, and transparent elsewhere. Then we fill the holes (aka "inpaint") to create a gradient in each polygon. The relax-fill method seems the most obvious, with "-compose seamless_blend -composite".

%IMG7%magick ^
  ppix_proto_c2.png ^
  -fx "u==p[1,0]? (u==p[0,1]? (u==p[1,1]? 0 :1) :1) :1" ^
  +write ppix_proto_c2_b.png ^
  -write mpr:BORD ^
  %SRC% ^
  +swap ^
  -alpha off ^
  -compose CopyOpacity -composite ^
  +write ppix_proto_c2_b2.png ^
  ( -clone 0 -alpha off -fill Black -colorize 100 ) ^
  ( mpr:BORD -negate ) ^
  -alpha off ^
  -define compose:args=10000x1e-7+100 ^
    -compose seamless_blend -composite ^
  ppix_proto_c2_b3.png
ppix_proto_c2_b.png ppix_proto_c2_b2.png ppix_proto_c2_b3.png
%IMG7%magick ^
  ppix_voron.png ^
  -fx "u==p[1,0]? (u==p[0,1]? (u==p[1,1]? 0 :1) :1) :1" ^
  +write ppix_voron_b.png ^
  -write mpr:BORD ^
  %SRC% ^
  +swap ^
  -alpha off ^
  -compose CopyOpacity -composite ^
  +write ppix_voron_b2.png ^
  ( -clone 0 -alpha off -fill Black -colorize 100 ) ^
  ( mpr:BORD -negate ) ^
  -alpha off ^
  -define compose:args=10000x1e-7+100 ^
    -compose seamless_blend -composite ^
  ppix_voron_b3.png
ppix_voron_b.png ppix_voron_b2.png ppix_voron_b3.png

The results have almost no traces of the polygons.

Future

Suppose we want to pixelate just part of an image, eg to pixelate a face. We would use a mask, eg black where we want no pixelation, and "-compose Over -composite" with that mask. But there is a good chance that the black/white border would divide polygons. Can we automatically adjust the border inwards or outwards so no polygons are divided?

Can we generate polygons (or other shapes) that follow texture?

There may be mileage in allowing a user-specified uc1, with fuzzy matching of the pixelation map image, and find nearest, not necessarily exactly equal. But that can be done prior to this processing, with -remap?

Scripts

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

detail2circles.bat

rem From input image %1,
rem makes output %2 that is overlapping circles,
rem each shape the mean of the corresponding pixels of the input.
rem with smallest radii where iput has most detail.
rem %3 is the maximum radius of generated circles.
rem %4 is a power adjustment: GTR 1 for more small circles, LSS 1 for fewer small circles.

rem Generates and runs a script that draws circles, with largest first.

rem IM must be HDRI.


set INFILE=%1
set OUTFILE=%2

set MAXRAD=%3
if "%MAXRAD%"=="." set MAXRAD=
if "%MAXRAD%"=="" set MAXRAD=20

set POW=%4
if "%POW%"=="." set POW=
if "%POW%"=="" set POW=1

set sPOW=
if not "%%"=="1" set sPOW=-evaluate pow %POW%

call %PICTBAT%slopeMag %INFILE% d2c_det.png

del d2c_circ.scr 2>nul

echo off
for /F "usebackq tokens=1-4 delims=,() " %%A in (`%IMG7%magick ^
  d2c_det.png ^
  -morphology dilate disk:3 ^
  -sigmoidal-contrast "5,70%%" -auto-level ^
  +write ppix_sm.png ^
  -fx "rnd=rand(); u*0.05 > rnd ? u : 0" ^
  -transparent Black ^
  -channel R ^
    -negate ^
    %sPOW% ^
    -evaluate Multiply %%[fx:%MAXRAD%/100] ^
  +channel ^
  -define "quantum:format=floating-point" -depth 32 ^
  -write d2c_pnts.miff ^
  -precision 16 ^
  -define "txt:compliance=undefined" ^
  sparse-color:  ^| tr " " "\n" `) do (
  if not "%%C"=="graya" (
    echo %0: Third is %%C, not graya
    exit /B 1
  )
  set /A rad=%%D 2>nul
  if not !rad!==0 (
    set /A edgeY=%%B+!rad!
    set zVal=%%D
    if !rad! LSS 10 set zVal=0%%D
    echo fill gray^(!zVal!^) circle %%A,%%B,%%A,!edgeY! >>d2c_circ.scr
  )
)
echo on

rem type d2c_pnts.lis

type d2c_circ.scr

:: Assume the bash sort.
sort --reverse d2c_circ.scr >d2c_circ2.scr

type d2c_circ2.scr

%IM7DEV%magick ^
  %INFILE% ^
  ( +clone ^
    -fill White -colorize 100 ^
    +antialias ^
    -draw "@d2c_circ2.scr" ^
    -alpha off ^
    -colorspace Gray ^
  ) ^
  -process polypix ^
  %OUTFILE%

sal2circ.bat

rem %1 input image
rem %2 input grayscale saliency image (lighter is more salient)
rem %3 text file of circles. If specified, shapes wil be added.
rem %4 output that is %2 pixelated by overlapping circles,
rem   with smallest radii where saliency is white;
rem   each output shape the mean of the corresponding pixels of the input.
rem %5 optional output file that is grayscale difference between %1 and %4.
rem %6 is the maximum probability (where saliency is white) of a pixel being a circle centre.
rem %7 is the maximum radius (where saliency is black) of generated circles.
rem %8 ignore generated circles larger than this radius.
rem %9 is a power adjustment: greater than 1 for more small circles, less than 1 for fewer small circles.
@rem
@rem Also uses:
@rem    s2c_SEED if set, uses this seed. Eg "set s2c_SEED=-seed 1234"
@rem    s2c_SHAPE "circle" or "square" or "roundedSquare"
@rem
@rem Some pixels may not be drawn by any shapes.
@rem Some shapes may be fully occluded by other shapes.

@rem Generates and runs a "-draw" script that draws circles, with largest first.

@rem IM must be HDRI.

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

@setlocal enabledelayedexpansion

@call echoOffSave

set INFILE=%1
set SALIENCY=%2
set CIRCFILE=%3

if "%CIRCFILE%"=="." set CIRCFILE=
if "%CIRCFILE%"=="" (
  set CIRCFILE=%TEMP%\s2c_circ.scr
  del !CIRCFILE! 2>nul
)

set OUTFILE=%4

set DIFFFILE=%5
if "%DIFFFILE%"=="." set DIFFFILE=

if "%DIFFFILE%"=="" (
  set sDIFF=
) else (
  set sDIFF=^( +clone mpr:INP -compose Difference -composite -grayscale RMS -auto-level -write %DIFFFILE% +delete ^)
)

set PROB=%6
if "%PROB%"=="." set PROB=
if "%PROB%"=="" set PROB=0.05

set MAXRAD=%7
if "%MAXRAD%"=="." set MAXRAD=
if "%MAXRAD%"=="" set MAXRAD=20

set IGNRAD=%8
if "%IGNRAD%"=="." set IGNRAD=
if "%IGNRAD%"=="" set /A IGNRAD=%MAXRAD%+1

set POW=%9
if "%POW%"=="." set POW=
if "%POW%"=="" set POW=1

set sPOW=
if not "%POW%"=="1" set sPOW=-evaluate pow %POW%

if /I "%s2c_SHAPE%"=="square" (
  set cShape=s
) else if /I "%s2c_SHAPE%"=="roundedSquare" (
  set cShape=r
) else if /I "%s2c_SHAPE%"=="circle" (
  set cShape=c
) else if "%s2c_SHAPE%"=="" (
  set cShape=c
) else (
  echo %0: Unknown s2c_SHAPE [%s2c_SHAPE%]
  exit /B 1
)

set TMP_CIRC=%TEMP%\s2c_tmp.scr

:: sparse-color: gives values like 01.032495411955479e-07%,
:: so we change values less than 1% to 1%.

(
for /F "usebackq tokens=1-4 delims=,() " %%A in (`%IMG7%magick ^
  %SALIENCY% ^
  %s2c_SEED% ^
  -fx "rnd=rand(); u*%PROB% > rnd ? u : 0" ^
  -transparent Black ^
  -channel R ^
    -negate ^
    %sPOW% ^
    -evaluate Multiply %%[fx:%MAXRAD%/100] ^
  +channel ^
  -evaluate max 1%% ^
  -define "quantum:format=floating-point" -depth 32 ^
  -precision 16 ^
  -define "txt:compliance=undefined" ^
  sparse-color:  ^| tr " " "\n" `) do (
  if not "%%C"=="graya" (
    echo %0: sparse-color is %%C, not graya
    exit /B 1
  )
  set /A rad=%%D 2>nul
  if !rad! GTR %IGNRAD% set rad=0
  if not !rad!==0 (
    set zVal=%%D
    if !rad! LSS 10 set zVal=0%%D
    if !rad! LSS 100 set zVal=0%%D
    set /A x0=%%A-!rad!
    set /A x1=%%A+!rad!
    set /A y0=%%B-!rad!
    set /A y1=%%B+!rad!
    set /A rc=!rad!*2/3
    if %cShape%==c (
      echo fill gray^(!zVal!^) circle %%A,%%B,%%A,!y1!
    ) else if %cShape%==s (
      echo fill gray^(!zVal!^) rectangle !x0!,!y0!,!x1!,!y1!
    ) else  (
      echo fill gray^(!zVal!^) roundRectangle !x0!,!y0!,!x1!,!y1!,!rc!,!rc!
    )
  )
)
) >>%CIRCFILE%

rem type %CIRCFILE%

:: Assume the sort is bash, not Windows.
sort --reverse %CIRCFILE% >%TMP_CIRC%

copy /Y %TMP_CIRC% %CIRCFILE% 2>nul

%IM7DEV%magick ^
  %INFILE% ^
  -write mpr:INP ^
  ( +clone ^
    -fill White -colorize 100 ^
    +antialias ^
    -draw "@%CIRCFILE%" ^
    -alpha off ^
    -colorspace Gray ^
    -connected-components 4 ^
    -alpha off ^
    -auto-level ^
  ) ^
  -process polypix ^
  %sDIFF% ^
  %OUTFILE%

call echoRestore

endlocal & set s2c_CIRCLES=%CIRCFILE%

sal2circRep.bat

rem Repeats (iterates) calls to sal2circ.bat.

rem %1 input image
rem %2 input grayscale saliency image (lighter is more salient)
rem [[ %3 text file of circles ]]
rem %3 output that is overlapping circles,
rem   with smallest radii where saliency is white;
rem   each output shape the mean of the corresponding pixels of the input.
rem %4 set of parameters for first call to cal2circ.bat
rem %5 set of parameters for following calls to cal2circ.bat
rem   %4 and %5 are each 4 numbers, space separated, quoted.
rem %6 number of iterations


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

setlocal enabledelayedexpansion

@call echoOffSave

set INFILE=%1
set SALIENCY=%2
set OUTFILE=%3

set p2c_ARGS1=%~4
set p2c_ARGSn=%~5

if "%p2c_ARGS1%"=="." set p2c_ARGS1=
if "%p2c_ARGS1%"=="" set p2c_ARGS1=. . . .

if "%p2c_ARGSn%"=="." set p2c_ARGSn=
if "%p2c_ARGSn%"=="" set p2c_ARGSn=%p2c_ARGS1%

set nIter=%6
if "%nIter%"=="." set nIter=
if "%nIter%"=="" set nIter=10

set CIRCFILE=s2cr_circs.scr
del %CIRCFILE% 2>nul

set DIFFFILE=%TEMP%\s2cr_diff.miff

set TMP_OUT=s2cr_tmp_out.miff

set p2c_ARGS=%p2c_ARGS1%


for /L %%N in (1,1,%nIter%) do (
  call %PICTBAT%sal2circ %INFILE% !SALIENCY! %CIRCFILE% %TMP_OUT% %DIFFFILE% !p2c_ARGS!

rem  %IMG7%magick ^
rem    %INFILE% %TMP_OUT% ^
rem    -compose Difference -composite ^
rem    -grayscale RMS -auto-level ^
rem    %DIFFFILE%

  set SALIENCY=%DIFFFILE%

  set p2c_ARGS=%p2c_ARGSn%
)

%IMG7%magick %TMP_OUT% %OUTFILE%

call echoRestore

endlocal

There are three C source files:

  1. polyPixB.c the standalone program
  2. polypix.c the process module
  3. polypix.inc the include file for the standalone program and process module.

polyPixB.c

This is the source file for the standalone program.

/*
   For polygon pixelation.

   From an input image and a grayscale "pixelation map" with polygons (or other shapes),
   each polygon a different shade of gray,
   makes an image that has each pixel the average if the input image pixels that are in the same polygon.
   All images are same-size.
   Polygons do not need to be contiguous. Polygon image should not be anti-aliased. It should be opaque.

   This code is written for IM v7, not IM v6.

   If the two inputs are identical, the output should be the same.

   Reference:
     http://im.snibgo.com/polypix.htm

   Build:
     bash snibgo\buildcore.sh snibgo\PolyPixB
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <MagickCore/MagickCore.h>

#define STANDALONE 1

#include "filters/polypix.inc"


int main (const int argc, const char *argv[])
{
  polyPixT pp;

  ExceptionInfo *exception = NULL;

  Image
    *image=NULL,
    *img_map=NULL,
    *img_out=NULL;

  ImageInfo *image_info = NULL;

  if (!menu (argc, argv, &pp)) {
    return 1;
  }

  if (!pp.infile) {
    fprintf (stderr, "Needs an input file.\n");
    return 1;
  }

  if (!pp.mapfile) {
    fprintf (stderr, "Needs an pixelation map file.\n");
    return 1;
  }

  MagickCoreGenesis (*argv, MagickTrue);

  exception = AcquireExceptionInfo ();

#if defined(MAGICKCORE_OPENMP_SUPPORT)
  printf ("Has MAGICKCORE_OPENMP_SUPPORT\n");
#else
  printf ("Does not have MAGICKCORE_OPENMP_SUPPORT\n");
#endif

  image_info = CloneImageInfo((ImageInfo *) NULL);

  (void) strcpy (image_info->filename, pp.infile);
  image = ReadImage (image_info, exception);
  if (!image) {
    fprintf (stderr, "Can't ReadImage [%s]\n", image_info->filename);
    goto error_cleanup;
  }

  (void) strcpy (image_info->filename, pp.mapfile);
  img_map = ReadImage (image_info, exception);
  if (!img_map) {
    fprintf (stderr, "Can't ReadImage [%s]\n", image_info->filename);
    goto error_cleanup;
  }

  AppendImageToList (&image, img_map);

  img_out = polypix (&image, &pp, exception);

  if (pp.outfile && img_out) {
    if (pp.do_verbose) fprintf (stderr, "Write output %s\n", pp.outfile); 
    strcpy (img_out->filename, pp.outfile);
    CopyMagickString (img_out->magick, "", MagickPathExtent);
    if (!SetImageProperty(img_out, "quantum:format", "floating-point", exception)) {
      fprintf (stderr, "SetImageProperty failed.\n");
      goto error_cleanup;
    }
    SetImageDepth (img_out, 32, exception);
    if (!WriteImage (image_info, img_out, exception)) {
      fprintf (stderr, "WriteImage [%s] failed.\n", img_out->filename);
      goto error_cleanup;
    }
  }

error_cleanup:

  if (exception->severity)
    MagickError (exception->severity, exception->reason, exception->description);

  if (image_info) image_info = DestroyImageInfo (image_info);
  if (exception) exception = DestroyExceptionInfo (exception);

  if (img_out) img_out = DestroyImage (img_out);

  MagickCoreTerminus ();

  return 0;
}

polypix.c

This is the source file for the process module.

/*
    Reference:
      http://im.snibgo.com/polypix.htm
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <MagickCore/MagickCore.h>

#define STANDALONE 0

#include "polypix.inc"


ModuleExport size_t polypixImage(Image **images,const int argc,
  const char **argv,ExceptionInfo *exception)
{
  polyPixT
    pp;

  assert(images != (Image **) NULL);
  assert(*images != (Image *) NULL);
  assert((*images)->signature == MagickCoreSignature);

  if (!menu (argc, argv, &pp))
    return (~MagickImageFilterSignature);

  if (!polypix (images, &pp, exception))
    return (~MagickImageFilterSignature);

  return (MagickImageFilterSignature);
}

polypix.inc

This source file is included in the above standalone program and process module.

/*
    Reference:
      http://im.snibgo.com/polypix.htm
*/

typedef enum {
  mMean,
  mCentroid
} methodT;

typedef struct {
  FILE *
    fh_data;

  char
    *infile,
    *mapfile,
    *outfile;

  methodT
    method;

  MagickBooleanType
    do_verbose;

  size_t
    numUniqGrays;

  MagickBooleanType
    HasAlpha;

} polyPixT;

static void usage (void)
{
  printf ("Pixelates an image.\n");
#if STANDALONE==1
  printf ("Usage: polyPixB [OPTION]...\n");
#else
  printf ("Usage: -process 'polypix [OPTION]...'\n");
#endif
  printf ("Options are:\n");
#if STANDALONE==1
  printf ("  i, infile string    main input file\n");
  printf ("  map, map string     input map file\n");
  printf ("  o, outfile string   output file\n");
#endif
  printf ("  m, method string    'mean' or 'centroid'\n");
  printf ("  f, file string      Write verbose text to 'stderr' or 'stdout'\n");
  printf ("  v, verbose          write text information\n");
  printf ("\n");
}


static MagickBooleanType IsArg (char * pa, char * ShtOpt, char * LongOpt)
{
  if ((LocaleCompare(pa, ShtOpt)==0) || (LocaleCompare(pa, LongOpt)==0))
    return MagickTrue;

  return MagickFalse;
}


static MagickBooleanType menu (
  const int argc,
  const char **argv,
  polyPixT * ppp
)
/* Returns MagickTrue if okay. */
{
  int
    i=0;

  MagickBooleanType
    status;

  status = MagickTrue;

  ppp->fh_data = stderr;
  ppp->infile = NULL;
  ppp->mapfile = NULL;
  ppp->outfile = NULL;
  ppp->method = mMean;
  ppp->do_verbose = MagickFalse;

#if STANDALONE==1
  i = 1;
#endif

  for (; i < argc; i++) {
    char * pa = (char *)argv[i];
#if STANDALONE==1
    if (IsArg (pa, "i", "infile")==MagickTrue) {
      i++;
      ppp->infile = (char *)argv[i];
    } else if (IsArg (pa, "map", "map")==MagickTrue) {
      i++;
      ppp->mapfile = (char *)argv[i];
    } else if (IsArg (pa, "o", "outfile")==MagickTrue) {
      i++;
      ppp->outfile = (char *)argv[i];
    } else
#endif
    if (IsArg (pa, "m", "method")==MagickTrue) {
      i++;
      if (LocaleCompare (argv[i], "mean")==0) ppp->method = mMean;
      else if (LocaleCompare (argv[i], "centroid")==0) ppp->method = mCentroid;
      else status = MagickFalse;
    } else if (IsArg (pa, "f", "file")==MagickTrue) {
      i++;
      if (LocaleCompare (argv[i], "stdout")==0) ppp->fh_data = stdout;
      else if (LocaleCompare (argv[i], "stderr")==0) ppp->fh_data = stderr;
      else status = MagickFalse;
    } else if (IsArg (pa, "v", "verbose")==MagickTrue) {
      ppp->do_verbose = MagickTrue;
    } else {
      fprintf (stderr, "polypix: ERROR: unknown option [%s]\n", pa);
      status = MagickFalse;
    }
  }

  if (ppp->do_verbose) {
    fprintf (stderr, "polypix options:");
#if STANDALONE==1
    fprintf (stderr, "  infile %s ", ppp->infile);
    fprintf (stderr, "  mapfile %s ", ppp->mapfile);
    fprintf (stderr, "  outfile %s ", ppp->outfile);
#endif
    fprintf (stderr, "  method ");
    if (ppp->method==mMean) fprintf (stderr, "mean");
    else if (ppp->method==mCentroid) fprintf (stderr, "centroid");
    else fprintf (stderr, "??");

    if (ppp->fh_data == stdout) fprintf (stderr, "  file stdout");
    if (ppp->do_verbose) fprintf (stderr, "  verbose");
    fprintf (stderr, "\n");
  }

  if (status == MagickFalse)
    usage ();

  return (status);
}


static inline ssize_t WhichPixel (const Quantum * pMap, const Quantum * puc1, const size_t nVals)
/* Assuming both images are grayscale,
   and *puc1 is (nVals)x1 pixels, sorted by increasing value,
   finds which pixel in *puc1 is equal to pMap
   by binary search ("binary chop").

   On success, returns index into *puc1, between 0 and nVals-1.
   On error (not found), returns -1.
*/
{
  size_t Lo=0, Hi=nVals-1, Mid;

  Quantum q;

  while (Lo <= Hi) {
    Mid = (Lo + Hi) / 2;

    /* Assume exactly one channel. */
    q = *(puc1+Mid);

    if (q < *pMap) {
      Lo = Mid+1;
    } else if (q > *pMap) {
      Hi = Mid-1;
    } else {
      return (ssize_t)Mid;
    }
  }

  return -1;
}

static int compare_pixels (const void *a, const void *b)
{
  const Quantum qa = *(const Quantum *) a;
  const Quantum qb = *(const Quantum *) b;
  return (qa > qb) - (qa < qb);
}


static Quantum* FindUniqueGreys (
  Image *img_map,
  CacheView *map_view,
  polyPixT * ppp,
  ExceptionInfo *exception)
/* From the image, which should have only one channel (gray),
   returns an array of the unique gray values.
   Also sets ppp->numUniqGrays to the number of unique values.
*/
{
  int status = 0;
  Quantum *uniq_grays = NULL;
  Quantum *qp, *grays = NULL;
  Quantum prevVal;
  size_t y, x;
  size_t countVals=0;
  size_t numElements = img_map->columns * img_map->rows;
  size_t i;

  ppp->numUniqGrays = 0;

  grays = (Quantum *)AcquireMagickMemory (numElements * sizeof (Quantum));
  if (!grays) {
    fprintf (stderr, "oom grays\n");
    status = -1;
    goto error_cleanup;
  }
  memset (grays, 0, numElements * sizeof (Quantum));

  if (ppp->do_verbose) fprintf (ppp->fh_data, "Populate the grays array.\n");
  qp = grays;
  {
    for (y=0; y < (size_t) img_map->rows; y++) {

      const Quantum *p = GetCacheViewVirtualPixels (
        map_view, 0, (ssize_t)y, img_map->columns, 1, exception);
      if (!p) {
        status = -1;
        goto error_cleanup;
      }

      for (x=0; x < (size_t) img_map->columns; x++) {
        *qp++ = *p;
        p += GetPixelChannels (img_map);
      }
    }
  }

  if (ppp->do_verbose) fprintf (ppp->fh_data, "Sort the array.\n");
  qsort (
      (void *)grays,
      img_map->columns * img_map->rows,
      sizeof (Quantum),
      compare_pixels);
  if (ppp->do_verbose) fprintf (ppp->fh_data, "Count the unique elements.\n");

  prevVal = grays[0];
  countVals = 1;
  for (x=1; x < numElements; x++) {
    if (prevVal != grays[x]) {
      prevVal = grays[x];
      countVals++;
    }
  }
  if (ppp->do_verbose) fprintf (ppp->fh_data, "countVals=%li\n", countVals);
  ppp->numUniqGrays = countVals;

  if (ppp->do_verbose) fprintf (ppp->fh_data, "Make another array, one element per unique value.\n");
  uniq_grays = (Quantum *)AcquireMagickMemory (countVals * sizeof (Quantum));
  if (!uniq_grays) {
    fprintf (stderr, "oom uniq_grays\n");
    status = -1;
    goto error_cleanup;
  }
  memset (uniq_grays, 0, countVals * sizeof (Quantum));

  if (ppp->do_verbose) fprintf (ppp->fh_data, "Populate the unique values array.\n");
  prevVal = grays[0];
  uniq_grays[0] = grays[0];
  i=1;
  for (x=1; x < numElements; x++) {
    if (prevVal != grays[x]) {
      prevVal = grays[x];
      assert (i < countVals);
      uniq_grays[i] = prevVal;
      i++;
    }
  }

error_cleanup:

  if (ppp->do_verbose) fprintf (ppp->fh_data, "Cleanup.\n");

  if (status < 0) {
    if (uniq_grays) uniq_grays = (Quantum *)RelinquishMagickMemory (uniq_grays);
  }

  if (grays) grays = (Quantum *)RelinquishMagickMemory (grays);

  return uniq_grays;
}


static size_t * MakeUcIndex (
  Image * img_map,
  CacheView *map_view,
  Quantum *uniq_grays,
  polyPixT * ppp,
  ExceptionInfo *exception)
/*
   Makes and populates array, one entry per map pixel, an index into uniq_grays and uc2.
   On error, returns NULL.
*/
{
  int status = 0;
  size_t * uc_index = (size_t *)AcquireMagickMemory (img_map->columns * img_map->rows * sizeof(*uc_index));

  if (!uc_index) {
    return NULL;
  }

  /* Walk through pixelation map pixels.
     For each one, find closest (identical) entry in uniq_grays, and set uc_index to the index.
  */

  {
    size_t y, x;
    if (ppp->do_verbose) fprintf (ppp->fh_data, "Populate uc_index\n");
    for (y=0; y < (size_t) img_map->rows; y++) {

      size_t prevJ = 0;
      size_t yoffs = y*img_map->columns;

      const Quantum *p = GetCacheViewVirtualPixels (
        map_view, 0, (ssize_t)y, img_map->columns, 1, exception);
      if (!p) {
        status = -1;
        break;
      }

      for (x=0; x < (size_t) img_map->columns; x++) {

        /* Which pixel in uniq_grays is equal to img_map pixel?
        */

        /* Most likely is the previously found match. */
        if (*(p+x)==uniq_grays[prevJ]) {
          uc_index[yoffs + x] = prevJ;
        } else {

          ssize_t ndx = WhichPixel (p+x, uniq_grays, ppp->numUniqGrays);
          if (ndx < 0) {
            fprintf (stderr, "WhichPixel failed x=%li ndx=%li\n", x, ndx);
            status = -1;
            goto error_cleanup;
          }
          uc_index[yoffs + x] = (size_t)ndx;
          prevJ = (size_t)ndx;
        }

      }
    }
  }

error_cleanup:

  if (status != 0) {
    if (uc_index) uc_index = (size_t *)RelinquishMagickMemory (uc_index);
  }
  return uc_index;
}

static int CalcMeanColours (
  Image * image,
  CacheView *image_view,
  Image * img_uc2,
  CacheView *uc2_view,
  size_t *uc_index,
  polyPixT * ppp,
  ExceptionInfo *exception)
/* Populates image uc2 with mean colours from image.
   Returns 0 iff okay.
*/
{
  int status = 0;

  Quantum *q_uc2;

  double
    *uniq_cnt = NULL;  /* One element per unique map colour;
                          count of pixels (or sum of alphas) with that colour. */

  size_t siz_uniq_cnt = image->columns * image->rows * sizeof(*uniq_cnt);
  uniq_cnt = (double *)AcquireMagickMemory (siz_uniq_cnt);
  if (!uniq_cnt) {
    fprintf (stderr, "oom uniq_cnt\n");
    status = -1;
    goto error_cleanup;
  }
  memset (uniq_cnt, 0, siz_uniq_cnt);

  if (ppp->do_verbose) fprintf (ppp->fh_data, "Populate uc2 with mean colours from image.\n");

  if (ppp->do_verbose) fprintf (ppp->fh_data, "img_uc2->columns=%li\n", img_uc2->columns);

  q_uc2 = GetCacheViewAuthenticPixels (
        uc2_view,0,0,img_uc2->columns,1,exception);
  if (!q_uc2) {
    status = -1;
    goto error_cleanup;
  }

  {
    size_t y, x;

    for (y=0; y < (size_t) image->rows; y++) {
      Quantum alpha = 1.0;
      size_t yoffs = y * image->columns;

      const Quantum *p = GetCacheViewVirtualPixels (
        image_view, 0, (ssize_t)y, image->columns, 1, exception);
      if (!p) {
        status = -1;
        break;
      }

      for (x=0; x < (size_t) image->columns; x++) {
        size_t ndx = uc_index [yoffs + x];
        Quantum * qptr = q_uc2 + ndx * GetPixelChannels (img_uc2);

        if (ppp->HasAlpha) alpha = GetPixelAlpha (image, p);

        SetPixelRed (
          img_uc2,
          alpha * GetPixelRed (image, p) + GetPixelRed (img_uc2, qptr),
          qptr);

        SetPixelGreen (
          img_uc2,
          alpha * GetPixelGreen (image, p) + GetPixelGreen (img_uc2, qptr),
          qptr);

        SetPixelBlue (
          img_uc2,
          alpha * GetPixelBlue (image, p) + GetPixelBlue (img_uc2, qptr),
          qptr);

        if (ppp->HasAlpha) {
          SetPixelAlpha (
            img_uc2,
            alpha * GetPixelAlpha (image, p) + GetPixelAlpha (img_uc2, qptr),
            qptr);
        }

        uniq_cnt[ndx] += alpha;
        p += GetPixelChannels (image);
      }
    }
  }
  if (status < 0) goto error_cleanup;

  if (!SyncCacheViewAuthenticPixels (uc2_view,exception)) {
    fprintf (stderr, "Can't sync uc2");
    status = -1;
    goto error_cleanup;
  }

  {
    size_t x;
    Quantum * qptr = q_uc2;
    if (ppp->do_verbose) fprintf (ppp->fh_data, "Divide the colours in uc2 by uniq_cnt.\n");
    for (x=0; x < (size_t) img_uc2->columns; x++) {
      double div = uniq_cnt[x];
      if (div != 0) {
        SetPixelRed (img_uc2, GetPixelRed (img_uc2, qptr) / div, qptr);
        SetPixelGreen (img_uc2, GetPixelGreen (img_uc2, qptr) / div, qptr);
        SetPixelBlue (img_uc2, GetPixelBlue (img_uc2, qptr) / div, qptr);
        if (ppp->HasAlpha) {
          SetPixelAlpha (img_uc2, GetPixelAlpha (img_uc2, qptr) / div, qptr);
        }
      }
      qptr += GetPixelChannels (img_uc2);
    }
    if (!SyncCacheViewAuthenticPixels (uc2_view,exception)) {
      fprintf (stderr, "Can't sync uc2");
      status = -1;
      goto error_cleanup;
    }
  }

error_cleanup:

  if (uniq_cnt) uniq_cnt = (double *)RelinquishMagickMemory (uniq_cnt);

  return status;
}


static int CalcCentroidColours (
  Image * image,
  CacheView *image_view,
  Image * img_uc2,
  CacheView *uc2_view,
  size_t *uc_index,
  polyPixT * ppp,
  ExceptionInfo *exception)
/* Populates image uc2 with centroid colours from image.
   Returns 0 iff okay.
*/
{
  typedef struct {
    size_t sumX;
    size_t sumY;
    size_t count;
  } centroidT;

  centroidT * centroids = NULL;
  int status = 0;
  size_t y, x;
  size_t siz_uniq_cnt;

  if (ppp->do_verbose) fprintf (ppp->fh_data, "Populate uc2 with centroid colours from image.\n");

  siz_uniq_cnt = image->columns * image->rows * sizeof(centroidT);
  centroids = (centroidT *)AcquireMagickMemory (siz_uniq_cnt * sizeof (centroidT));
  if (!centroids) {
    fprintf (stderr, "oom centroids\n");
    status = -1;
    goto error_cleanup;
  }
  memset (centroids, 0, siz_uniq_cnt * sizeof (centroidT));

  {
    for (y=0; y < (size_t) image->rows; y++) {
      size_t yoffs = y * image->columns;
      for (x=0; x < (size_t) image->columns; x++) {
        size_t ndx = uc_index [yoffs + x];
        centroidT *pcent = &(centroids[ndx]);
        pcent->sumX += x;
        pcent->sumY += y;
        pcent->count++;
      }
    }
  }

  {
    size_t i;
    Quantum *q_uc2 = GetCacheViewAuthenticPixels (
        uc2_view,0,0,img_uc2->columns,1,exception);
    if (!q_uc2) {
      status = -1;
      goto error_cleanup;
    }

    for (i=0; i < ppp->numUniqGrays; i++) {
      PixelInfo src_pixel;
      centroidT *pcent = &(centroids[i]);
      double div = (double)pcent->count;
      double fx = (double)pcent->sumX / div;
      double fy = (double)pcent->sumY / div;

      InterpolatePixelInfo (
        image, image_view,
        image->interpolate,
        fx, fy, &src_pixel, exception);

      SetPixelRed (img_uc2, src_pixel.red, q_uc2);
      SetPixelGreen (img_uc2, src_pixel.green, q_uc2);
      SetPixelBlue (img_uc2, src_pixel.blue, q_uc2);

      q_uc2 += GetPixelChannels (img_uc2);
    }
  }

error_cleanup:

  if (centroids) centroids = (centroidT *)RelinquishMagickMemory (centroids);

  return status;
}


static Image * polypix (Image ** images, polyPixT * ppp, ExceptionInfo *exception)
{
  int status=0;

  Image
    *image=NULL,
    *img_map=NULL,
    *img_uc2=NULL,
    *img_out=NULL;

  ImageInfo
    *image_info = NULL;

  CacheView
    *image_view=NULL,
    *map_view=NULL,
    *uc2_view=NULL,
    *out_view=NULL;

  size_t
    *uc_index = NULL;  /* One element per input pixel. Index into uniq_grays array, uc2 and uniq_cnt. */

  Quantum
    *uniq_grays = NULL;  /* One element per unique map colour; the gray value. */

  size_t listlen = GetImageListLength (*images);
  if (listlen != 2) {
    fprintf (stderr, "Need two images; found %li.\n", listlen);
    status = -1;
    goto error_cleanup;
  }

  image_info = CloneImageInfo((ImageInfo *) NULL);

  image = *images;
  img_map = GetNextImageInList (image);

  ppp->HasAlpha = (MagickBooleanType)(image->alpha_trait != UndefinedPixelTrait);

  { /* Check dimensions. */
    if (image->rows != img_map->rows || image->columns != img_map->columns) {
      fprintf (stderr, "Error: Input image dimensions don't match\n");
      goto error_cleanup;
    }
  }

  if (!SetImageStorageClass (image, DirectClass, exception)) {
    fprintf (stderr, "SetImageStorageClass failed\n");
    goto error_cleanup;
  }

  if (!SetImageStorageClass (img_map, DirectClass, exception)) {
    fprintf (stderr, "SetImageStorageClass failed\n");
    goto error_cleanup;
  }

  if (GetPixelChannels (img_map) != 1) {
    fprintf (stderr, "Pixelation map image has %li channels instead of exactly one.\n", GetPixelChannels (img_map));
    goto error_cleanup;
  }

  map_view = AcquireVirtualCacheView (img_map, exception);
  uniq_grays = FindUniqueGreys (img_map, map_view, ppp, exception);
  if (!uniq_grays) {
    fprintf (stderr, "FindUniqueGrays failed\n");
    status = -1;
    goto error_cleanup;
  }

  img_uc2 = CloneImage (image, ppp->numUniqGrays, 1, MagickTrue, exception);
  if (!img_uc2) {
    fprintf (stderr, "clone into uc2 failed\n");
    status = -1;
    goto error_cleanup;
  }
  TransformImageColorspace (img_uc2, sRGBColorspace, exception);

  { /* Make uc2 pixels black. */
    PixelInfo mppBlack;
    GetPixelInfo (img_uc2, &mppBlack);
    SetImageColor(img_uc2, &mppBlack, exception);
  }

  uc2_view = AcquireAuthenticCacheView (img_uc2, exception);

  image_view = AcquireVirtualCacheView (image, exception);

  uc_index = MakeUcIndex (img_map, map_view, uniq_grays, ppp, exception);
  if (!uc_index) {
    fprintf (stderr, "MakeUcIndex failed\n");
    goto error_cleanup;
  }

  if (uniq_grays) uniq_grays = (Quantum *)RelinquishMagickMemory (uniq_grays);

  if (ppp->method == mMean) {
    if (CalcMeanColours (image, image_view, img_uc2, uc2_view, uc_index, ppp, exception) != 0) {
      fprintf (stderr, "CalcMeanColours failed\n");
      status = -1;
      goto error_cleanup;
    }
  } else {
    if (CalcCentroidColours (image, image_view, img_uc2, uc2_view, uc_index, ppp, exception) != 0) {
      fprintf (stderr, "CalcCentroidColours failed\n");
      status = -1;
      goto error_cleanup;
    }
  }

  img_out = CloneImage (image, 0, 0, MagickTrue, exception);
  if (!img_out) {
    fprintf (stderr, "clone image to out failed\n");
    status = -1;
    goto error_cleanup;
  }
  out_view = AcquireAuthenticCacheView (img_out, exception);

  {
    size_t y, x;

    Quantum *q_uc2 = GetCacheViewAuthenticPixels (
            uc2_view,0,0,img_uc2->columns,1,exception);
    if (!q_uc2) {
      status = -1;
      goto error_cleanup;
    }

    if (ppp->do_verbose) fprintf (ppp->fh_data, "Set output colours.\n");

#if defined(MAGICKCORE_OPENMP_SUPPORT)
  #pragma omp parallel for schedule(static,4) shared(status) \
    MAGICK_THREADS(img_out,img_out,img_out->rows,1)
#endif
    for (y=0; y < (size_t) img_out->rows; y++) {
      size_t yoffs = y * img_out->columns;
      Quantum *q = GetCacheViewAuthenticPixels (
        out_view, 0, (ssize_t)y, img_out->columns, 1, exception);
      if (!q) {
        status = -1;
        break;
      }
      for (x=0; x < (size_t) img_out->columns; x++) {
        size_t ndx = uc_index [yoffs + x];
        Quantum * pptr = q_uc2 + ndx * GetPixelChannels (img_uc2);
        SetPixelRed (img_out, GetPixelRed (img_uc2, pptr), q);
        SetPixelGreen (img_out, GetPixelGreen (img_uc2, pptr), q);
        SetPixelBlue (img_out, GetPixelBlue (img_uc2, pptr), q);
        q += GetPixelChannels (img_out);
      }
      if (!SyncCacheViewAuthenticPixels (out_view,exception)) {
        fprintf (stderr, "Can't sync out");
        status = -1;
        goto error_cleanup;
      }
    }
  }

error_cleanup:

  if (ppp->do_verbose) fprintf (ppp->fh_data, "Cleanup\n");
  if (uniq_grays) uniq_grays = (Quantum *)RelinquishMagickMemory (uniq_grays);
  if (uc_index) uc_index = (size_t *)RelinquishMagickMemory (uc_index);
  if (out_view) out_view = DestroyCacheView (out_view);
  if (uc2_view) uc2_view = DestroyCacheView (uc2_view);
  if (map_view) map_view = DestroyCacheView (map_view);
  if (image_view) image_view = DestroyCacheView (image_view);

  if (status < 0 && img_out) img_out = DestroyImage (img_out);

  if (img_uc2) img_uc2 = DestroyImage (img_uc2);
  if (image_info) image_info = DestroyImageInfo (image_info);

  if (img_out) {

    DeleteImageFromList (&img_map);

    ReplaceImageInList (&image, img_out);
    /* Replace messes up the images pointer. Make it good: */
    *images = GetFirstImageInList (image);
    img_out = *images;
  }

  return img_out;
}

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

%IMG7%magick -version
Version: ImageMagick 7.1.1-20 Q16-HDRI x86 98bb1d4:20231008 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI OpenCL OpenMP(2.0) 
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 (193532217)

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 polypix.h1. To re-create this web page, run "procH1 polypix".


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 10-July-2024.

Page created 20-Aug-2024 13:15:23.

Copyright © 2024 Alan Gibson.