3

I have an rgb image with 12 distinct colours but I do not know the colours (pixel values) beforehand. I want to convert all the pixel values between 0 and 11,each symbolising a unique colour of the original rgb image.

e.g. all [230,100,140] converted to [0,0,0] , all [130,90,100] converted to [0,0,1] and so on ...all [210,80,50] converted to [0,0,11].

Tadhg McDonald-Jensen
  • 20,699
  • 5
  • 35
  • 59
Fateh Singh
  • 31
  • 1
  • 4

2 Answers2

2

Quick and dirty application. Much can be improved, especially going through the whole image pixel by pixel is not very numpy nor very opencv, but I was too lazy to remember exactly how to threshold and replace RGB pixels..

import cv2
import numpy as np

#finding unique rows
#comes from this answer : http://stackoverflow.com/questions/8560440/removing-duplicate-columns-and-rows-from-a-numpy-2d-array
def unique_rows(a):
    a = np.ascontiguousarray(a)
    unique_a = np.unique(a.view([('', a.dtype)]*a.shape[1]))
    return unique_a.view(a.dtype).reshape((unique_a.shape[0], a.shape[1]))

img=cv2.imread(your_image)

#listing all pixels
pixels=[]
for p in img:
    for k in p:
        pixels.append(k)

#finding all different colors
colors=unique_rows(pixels)

#comparing each color to every pixel
res=np.zeros(img.shape)
cpt=0
for color in colors:
    for i in range(img.shape[0]):
        for j in range(img.shape[1]):
            if (img[i,j,:]==color).all(): #if pixel is this color
                res[i,j,:]=[0,0,cpt] #set the pixel to [0,0,counter]
    cpt+=1
Soltius
  • 2,162
  • 1
  • 16
  • 28
1

You can use np.unique with a bit of trickery:

import numpy as np

def safe_method(image, k):
    # a bit of black magic to make np.unique handle triplets
    out = np.zeros(image.shape[:-1], dtype=np.int32)
    out8 = out.view(np.int8)
    # should really check endianness here
    out8.reshape(image.shape[:-1] + (4,))[..., 1:] = image
    uniq, map_ = np.unique(out, return_inverse=True)
    assert uniq.size == k
    map_.shape = image.shape[:-1]
    # map_ contains the desired result. However, order of colours is most
    # probably different from original
    colours = uniq.view(np.uint8).reshape(-1, 4)[:, 1:]
    return colours, map_

However, if the number of pixels is much larger than the number of colours, the following heuristic algorithm may deliver huge speedups. It tries to find a cheap hash function (such as only looking at the red channel) and if it succeds it uses that to create a lookup table. If not it falls back to the above safe method.

CHEAP_HASHES = [lambda x: x[..., 0], lambda x: x[..., 1], lambda x: x[..., 2]]

def fast_method(image, k):
    # find all colours
    chunk = int(4 * k * np.log(k)) + 1
    colours = set()
    for chunk_start in range(0, image.size // 3, chunk):
        colours |= set(
            map(tuple, image.reshape(-1,3)[chunk_start:chunk_start+chunk]))
        if len(colours) == k:
            break
    colours = np.array(sorted(colours))
    # find hash method
    for method in CHEAP_HASHES:
        if len(set(method(colours))) == k:
            break
    else:
        safe_method(image, k)
    # create lookup table
    hashed = method(colours)
    # should really provide for unexpected colours here
    lookup = np.empty((hashed.max() + 1,), int)
    lookup[hashed] = np.arange(k)
    return colours, lookup[method(image)]

Testing and timings:

from timeit import timeit

def create_image(k, M, N):
    colours = np.random.randint(0, 256, (k, 3)).astype(np.uint8)
    map_ = np.random.randint(0, k, (M, N))
    image = colours[map_, :]
    return colours, map_, image

k, M, N = 12, 1000, 1000

colours, map_, image = create_image(k, M, N)

for f in fast_method, safe_method:
    print('{:16s} {:10.6f} ms'.format(f.__name__, timeit(
        lambda: f(image, k), number=10)*100))
    rec_colours, rec_map_ = f(image, k)
    print('solution correct:', np.all(rec_colours[rec_map_, :] == image))

Sample output (12 colours, 1000x1000 pixels):

fast_method        3.425885 ms
solution correct: True
safe_method       73.622813 ms
solution correct: True
Paul Panzer
  • 51,835
  • 3
  • 54
  • 99
  • The safe_method() worked. The fast_method() throws an error `TypeError: 'numpy.ndarray' object is not callable` in the first `for` loop – Fateh Singh Apr 16 '17 at 18:23
  • @FatehSingh Hm, you didn't by any chance shadow a builtin? Because as far as I can see the only function calls in that loop are to `range, set, map, tuple` and `len` (I think we can rule out `image.reshape`). Could you please check whether one of those is an array. If so you should rename the array to something else. You can get back the builtin using `import builtins` and then for example `map = builtins.map`. – Paul Panzer Apr 16 '17 at 19:29
  • Yeah, it worked , it was name collision that was causing the problem.Thanks – Fateh Singh Apr 16 '17 at 19:50