ImageMagick color mapping by different metric?

74 views Asked by At

I'd like to use ImageMagick to convert pictures to 4-bit RGBI format. It turns out I can do that by making a palette as a separate step. However, the results are very unsatisfying.

For example, here is an input image to the above 15-color palette:

Input

And here's the output with RGBI4 palette:

Output with RGBI4 palette

In comparison, using just 8 colors with -posterize 2, i.e. not allowing for mid intensity, I get this actually better picture:

Output with -posterize 2

Note that all of the colors used in the second conversion are available for the first conversion as well; they are just not picked by ImageMagick.

To me, this suggests that the problem is the color space metric used by ImageMagick to choose colors. How do I control that to get something that is at least as good as -posterize 2?

1

There are 1 answers

0
Mark Setchell On

AFAIK, ImageMagick always does the remapping in RGB colourspace which doesn't produce very good results for you. I therefore tried converting to a different colourspace, but hiding that from ImageMagick so it believes the image and palette are RGB and then, when the remapping is done, I admitted the data was HSV and converted back to RGB. It seems to work ok for your sample image:

#!/bin/bash

# Make RGBI palette and save in HSV colourspace as three separate PGM images (no colourspace specified so IM treats as RGB)
magick $(for l in 128 255                        
  do
    for r in 0 $l
    do
      for g in 0 $l
      do
        for b in 0 $l
        do
          echo "xc:rgb($r,$g,$b)"
        done
      done
    done
  done) +append -unique-colors -colorspace HSV -separate pal-%d.pgm

# Split input image into HSV too and hide colourspace using PGM
magick input.png -colorspace HSV -separate chan-%d.pgm

# Recombine image and palette (pretending RGB), remap, admit HSV colourspace
magick chan-[012].pgm -combine \
   \( pal-[012].pgm -combine -write MPR:palette +delete \) \
   +dither -remap mpr:palette -set colorspace HSV result.png

enter image description here

We may have to work on the yellows... in fact, that brings another idea to mind... we could take the few individual colours in your input image separately and force each one to a new output colour, one-at-a-time...

TL;DR - the following paragraph is the solution

I can pick individual colours (within any tolerance, or "fuzz") in the input image and change them to anything I want. So here, I'll make the yellows become red and the greens become magenta:

magick input.png -fuzz 10% \
   -fill red     -opaque "rgb(201,215,112)" \
   -fill magenta -opaque "rgb(95,159,67)" result.png

enter image description here

I'll leave you to do the exact mapping of your input colours, you just need to add an extra line for each input colour and its corresponding output colour... and maybe diddle with the fuzz factor.


Just for reference, here is the ugly, proof-of-concept code I worked on in Python and OpenCV - it works, by the way:

#!/usr/bin/env python3

import numpy as np
import cv2 as cv
from functools import lru_cache 

def show(name, na):
   print(f'{name}: {na.shape}, {na.dtype}, {na.min()}, {na.max()}')

# Load image and convert to Luv
im    = cv.imread('input.png')
imLab = cv.cvtColor(np.float32(im/255.), cv.COLOR_BGR2Luv)
rows, cols, ch = im.shape
show('imLab', imLab)

# Load palette and convert to Luv
pal  = cv.imread('palette.png')
pLab = cv.cvtColor(np.float32(pal/255.), cv.COLOR_BGR2Luv)
pEntries = 15
show('pLab', pLab)

def labnearest(i0,i1,i2):
   dNearest = 1_000_000
   for pEntry in range(pEntries):
      p0, p1, p2 = pLab[0,pEntry,0], pLab[0,pEntry,1], pLab[0,pEntry,2]
      # Work out distance between this pixel and this palette entry
      d = (p0 - i0)*(p0 - i0) + (p1 - i1)*(p1 -i1) + (p2 - i2)*(p2 - i2)
      if d < dNearest:
         dNearest = d
         result = (p0, p1, p2)
   return result

for x in range(rows):
   for y in range(cols):
       px = imLab[x,y]
       imLab[x,y] = labnearest(px[0], px[1], px[2])

show('imLab', imLab)
res = (cv.cvtColor(imLab, cv.COLOR_Luv2BGR)*255).astype(np.uint8)
show('res', res)
cv.imwrite('result.png', res)