3

I'm pretty new to image processing and python so bear with me

I'm trying to take a big image (5632x2048) which is basically a map of the world with provinces (ripped from Hearts of Iron 4), and each province is colored a different RGB value, and color it with a set of colors, each corresponding to a certain country. I'm currently using this code

import numpy as np
import cv2
import sqlite3

dbPath = 'PATH TO DB'
dirPath = 'PATH TO IMAGE'

con = sqlite3.connect(dbPath)
cur = con.cursor()

im = cv2.imread(dirPath)

cur.execute('SELECT * FROM Provinces ORDER BY id')
provinceTable = cur.fetchall()

for line in provinceTable:
    input_rgb = [line[1], line[2], line[3]]
    if line[7] == None:
        output_rgb = [255,255,255]
    else:
        output_rgb = line[7].replace('[', '').replace(']','').split(',')
    im[np.all(im == (int(input_rgb[0]), int(input_rgb[1]), int(input_rgb[2])), axis=-1)] = (int(output_rgb[0]), int(output_rgb[1]), int(output_rgb[2]))

cv2.imwrite('result.png',im)

The problem I'm running into is that it's painfully slow (50 minutes in and it hasn't finished), due to the fact I'm definitely using numpy wrong by looping through it instead of vectorizing (a concept I'm still new to and have no idea how to do). Google hasn't been very helpful either.

What's the best way to do this?

Edit: forgot to mention that the amount of values I'm replacing is pretty big (~15000)

  • 1
    Would you mind sharing the image for testing? – The Singularity Mar 13 '21 at 17:27
  • @Luke Had to upload it to mega because it's 33mb https://mega.nz/file/ugJVEQTR#aet7PQTls20lh28NBFIIbIcwaC3y_tZy0OoixQhBJ1E – inconspicuoususername Mar 13 '21 at 18:43
  • 1
    Your image is actually 33MB (megabytes) rather than 33mb (millibits) - just a factor of 8 billion. – Mark Setchell Mar 13 '21 at 18:47
  • 1
    You have N precise colours (not ranges) that you want to replace by N other colours? What's the range of N, please? – Mark Setchell Mar 13 '21 at 20:47
  • @MarkSetchell Sorry for the late response, I want to replace ~13000 colors in the image with ~100 other colors, each line in the database has its input and output colors defined – inconspicuoususername Mar 14 '21 at 08:23
  • You'll probably want to create an unsigned 16-bit Lookup Table (LUT) mapping old colours to new and use `np.take(LUT, yourImage)`. It will probably work best if you use `np.dot()` to convert your three 8-bit RGB pixels to a single 24-bit unsigned first. – Mark Setchell Mar 14 '21 at 09:19
  • The `np.dot()` thing is explained at the end of this answer... https://stackoverflow.com/a/59671950/2836621 – Mark Setchell Mar 14 '21 at 09:25

3 Answers3

1

Here is one way to do that using Numpy and Python/OpenCV. Here I change red to green.

Input:

enter image description here

import cv2
import numpy as np

# load image
img = cv2.imread('test_red.png')

# change color
result = img.copy()
result[np.where((result==[0,0,255]).all(axis=2))] = [0,255,0]

# save output
cv2.imwrite('test_green.png', result)

# Display various images to see the steps
cv2.imshow('result',result)
cv2.waitKey(0)
cv2.destroyAllWindows()

Result:

enter image description here

fmw42
  • 46,825
  • 10
  • 62
  • 80
1

As I mentioned in the comments, I think you'll want to use np.take(yourImage, LUT) where LUT is a Lookup Table.

So, if you make a dummy image the same shape as yours:

import numpy as np

# Make a dummy image of 5632x2048 RGB values
im = np.random.randint(0,256,(5632,2048,3), np.uint8)

that will be 34MB. Now reshape it to a tall vector of RGB values:

# Make image into a tall vector, as tall as necessary and 3 RGB values wide
v = im.reshape((-1,3))

which will be of shape (11534336, 3) and then flatten that to 24-bit values rather than three 8-bit values with np.dot()

# Make into tall vector of shape 11534336x1 rather than 11534336x3
v24 = np.dot(v.astype(np.uint32),[1,256,65536])

You will now have a 1-D vector of 24-bit pixel values with shape (11534336,)

Now create your RGB lookup table (I am making all 2^24 RGB entries here, you may need less):

RGBLUT = np.zeros((2**24,3),np.uint8)

And set up the LUT. So, supposing you want to map all colours in the original image to mid-grey (128) in the output image:

RGBLUT[:] = 128

Now do the np.dot() thing just the same as we did with the image so we get a LUT with shape (224,1) rather than shape (224,3):

LUT24 = np.dot(RGBLUT.astype(np.uint32), [1,256,65536])

Then do the actual lookup in the table:

result = np.take(LUT24, v24)

On my Mac, that take 334ms for your 5632x2048 image.


Then reshape and convert back to three 8-bit values by shifting and ANDing to undo effect of np.dot().

I am not currently in a position to test the re-assembly, but it will look pretty much like this:

BlueChannel   = result & 0xff         # Blue channel is bottom 8 bits
GreenChannel  = (result>>8)  &0 xff   # Green channel is middle 8 bits
RedChannel    = (result>>16) &0 xff   # Red channel is top 8 bits

Now combine those three single channels into a 3-channel image:

RGB = np.dstack(RedChannel, GreenChannel, BlueChannel))

And reshape back from tall vector to dimensions of original image:

RGB = RGB.reshape(im.shape)

As regards setting up the LUT, to something more interesting than mid-grey, if you want to map say orange, i.e. rgb(255,128,0) to magenta, i.e. rgb(255,0,255) you would do something along the lines of:

LUT[np.dot([255,128,0],[1,256,65536])] = [255,0,255]  # map orange to magenta
LUT[np.dot([255,255,255],[1,256,65536])] = [0,0,0]  # map white to black
LUT[np.dot([0,0,0],[1,256,65536])] = [255,255,255]  # map black to white

Keywords: Python, image processing, LUT, RGB LUT 24-bit LUT, lookup table.

Mark Setchell
  • 191,897
  • 31
  • 273
  • 432
  • Thank you very much for the answer. 2 questions: Do I run a for loop for every single RGB value I want to add to the LUT? and how exactly do I convert the image back? – inconspicuoususername Mar 14 '21 at 16:17
  • If you want to convert 13,000 RGB values into 13,000 other values, you will need to loop through the 13,000 values putting each one in the LUT like at the bottom of my answer. The `np.take()` will then do the 11534336 lookups in a single go, in 300ms. – Mark Setchell Mar 14 '21 at 16:30
  • Thank you for the answer. I've changed my code to this: https://pastebin.com/61XSWgUj The problem I'm running into is that the image comes out completely wrong (screenshotted result in paste). I've tried the gray only approach, and it works perfectly, so I'm assuming I did the LUT loop part wrong. – inconspicuoususername Mar 14 '21 at 18:53
  • Check `RGB.dtype` - it should be `np.uint8`. If not, convert with `RGB = RGB.astype(np.uint8)`. Also, you appear to be using **OpenCV**, which (weirdly) uses BGR ordering rather than RGB, so you'll need to re-order everywhere RGB is assumed. – Mark Setchell Mar 14 '21 at 19:10
  • Or you can convert the image to RGB immediately after loading with `RGB = cv2.cvtColor(XXX, cv2.COLOR_BGR2RGB)` and then convert back to BGR order just before `cv2.imwrite()`. – Mark Setchell Mar 14 '21 at 19:13
  • Cool - was it a pretty decent speed? – Mark Setchell Mar 14 '21 at 19:23
  • It took ~1.45 seconds on my machine, much faster than my previous 50 minutes – inconspicuoususername Mar 14 '21 at 19:26
0

You can create a mask of the image first and use that to replace the colors. There's likely a pure numpy way of doing this that is faster, but I don't know it.

This code takes ~0.5 seconds to run. You should expect it to take about half a second for each color replacement.

import cv2
import numpy as np
import time

# make image
res = (5632, 2048, 3);
img = np.zeros(res, np.uint8);

# change black to white
black = (0,0,0);
white = (255,255,255);

# make a mask
start_time = time.time();
mask = cv2.inRange(img, black, black);
print("Mask Time: " + str(time.time() - start_time));

# replace color
start_time = time.time();
img[mask == 255] = white;
print("Replace Time: " + str(time.time() - start_time));

In terms of your code it'll look like this

for line in provinceTable:
    input_rgb = [line[1], line[2], line[3]]
    input_rgb = (int(input_rgb[0]), int(input_rgb[1]), int(input_rgb[2]))
    if line[7] == None:
        output_rgb = (255,255,255)
    else:
        output_rgb = line[7].replace('[', '').replace(']','').split(',')
        output_rgb = (int(output_rgb[0]), int(output_rgb[1]), int(output_rgb[2]))
    mask = cv2.inRange(im, input_rgb, input_rgb)
    im[mask == 255] = output_rgb
Ian Chu
  • 2,924
  • 9
  • 14
  • Better solution than mine, but still very slow (~10 min to finish). It also seemed like something was wrong with doing inRange with only 1 value as the colors would be wrong half the time. Thank you a lot anyways – inconspicuoususername Mar 13 '21 at 20:24