10

I want to take an RGB image and convert it to a black and white RGB image, where a pixel is black if its HSV value is between a certain range and white otherwise.

Currently I create a new image, then create a list of new pixel values by iterating through its data, then .putdata() that list to form the new image.

It feels like there should be a much faster way of doing this, e.g. with .point(), but it seems .point() doesn't get given pixels but values from 0 to 255 instead. Is there a .point() transform but on pixels?

Claudiu
  • 224,032
  • 165
  • 485
  • 680
  • Is it necessary to convert the image to HSV? You might consider doing the transformation on your requirements range to find an adequate RGB requirements window (the transformation is not linear, so, wondering if an approximation is ok) – Paul Feb 03 '11 at 19:12
  • 2
    Do you use NumPy? I usually eschew most PIL functions for numpy array operations when things are not "standard" image tweaks. – Paul Feb 03 '11 at 19:15
  • i could use numpy, though im not familiar with it. and yeah the transform must be hsv, but if it didnt, how would i do it with rgb anyway? – Claudiu Feb 03 '11 at 19:18
  • For example, if your HSV window is narrow and you want to catch all colors within a sphere of, say, 10 HSV values in any direction from a specific color, then it would be good enough to approximate the same window in RGB, but it might be more of an oval-shaped window surrounding the same color in RGB-space. – Paul Feb 03 '11 at 19:27
  • this http://stackoverflow.com/questions/4554627/rgb-to-hsv-conversion-using-pil question addresses some of the difficulties of the RGB-HSV transformation. I don't know why a fast RGB-HSV conversion hasn't been included in PIL yet. There's clearly a need. – Paul Feb 03 '11 at 19:35
  • This had good C code for the RGB-HSV conversion http://www.cs.rit.edu/~ncs/color/t_convert.html all that's left is to Numpyify it... – Benjamin Feb 03 '11 at 20:08
  • Good find, Benjamin. It looks like the same basic algorithm that i used. One could even use it pretty much as-is compiled as an extension. – Paul Feb 03 '11 at 21:04
  • @Claudiu: If appropriate to your question, could I suggest a change in title to something like "Detecting thresholds in HSV color space (from RGB) using Python / PIL"? – Benjamin Feb 04 '11 at 15:45
  • @Benjamin: sure that sounds lovely – Claudiu Feb 04 '11 at 16:25
  • 2
    I use `scikits.image.color.rgb2hsv()` to get HSV values, which is nice if you can afford to install numpy/scipy/et al. But @paul I would wager that HSV functions aren't in PIL because there aren't any image binary formats that store their data as HSV values. Also, not much of anything has been added in PIL of late... – fish2000 Aug 13 '11 at 18:29

4 Answers4

21

Ok, this does work (fixed some overflow errors):

import numpy, Image
i = Image.open(fp).convert('RGB')
a = numpy.asarray(i, int)

R, G, B = a.T

m = numpy.min(a,2).T
M = numpy.max(a,2).T

C = M-m #chroma
Cmsk = C!=0

# Hue
H = numpy.zeros(R.shape, int)
mask = (M==R)&Cmsk
H[mask] = numpy.mod(60*(G-B)/C, 360)[mask]
mask = (M==G)&Cmsk
H[mask] = (60*(B-R)/C + 120)[mask]
mask = (M==B)&Cmsk
H[mask] = (60*(R-G)/C + 240)[mask]
H *= 255
H /= 360 # if you prefer, leave as 0-360, but don't convert to uint8

# Value
V = M

# Saturation
S = numpy.zeros(R.shape, int)
S[Cmsk] = ((255*C)/V)[Cmsk]

# H, S, and V are now defined as integers 0-255

It is based on the Wikipedia's definition of HSV. I'll look it over as I get more time. There are definitely speedups and maybe bugs. Please let me know if you find any. cheers.


Results:

starting with this colorwheel: enter image description here

I get these results:

Hue:

enter image description here

Value:

enter image description here

Saturation:

enter image description here

Paul
  • 42,322
  • 15
  • 106
  • 123
  • I've never seen this way of using masks. I must admit it's easier to follow than my method. – Benjamin Feb 03 '11 at 22:00
  • I'll delete them. Would you mind posting your own results using the same color wheel? a .png can be found here: http://www.palette.com/hsvwheel.png – Paul Feb 04 '11 at 03:09
  • 1
    chroma (`c`) is always defined as `M-m` regardless of the model. The definition of Value `v` should be changed to M in your code. Value is always `M` and is specific to the hexcone model. – Paul Feb 05 '11 at 18:57
  • Using your code results in a division by zero because `C` seems to be zero (using hsvwheel.png)?! – Stefan Profanter Jun 29 '13 at 16:28
  • I fixed the code above and added RGB_TO_HSL. See my post in this thread. – Stefan Profanter Jun 29 '13 at 17:25
5

EDIT 2: This now returns the same results as Paul's code, as it should...

import numpy, scipy

image = scipy.misc.imread("test.png") / 255.0

r, g, b = image[:,:,0], image[:,:,1], image[:,:,2]
m, M = numpy.min(image[:,:,:3], 2), numpy.max(image[:,:,:3], 2)
d = M - m

# Chroma and Value
c = d
v = M

# Hue
h = numpy.select([c ==0, r == M, g == M, b == M], [0, ((g - b) / c) % 6, (2 + ((b - r) / c)), (4 + ((r - g) / c))], default=0) * 60

# Saturation
s = numpy.select([c == 0, c != 0], [0, c/v])

scipy.misc.imsave("h.png", h)
scipy.misc.imsave("s.png", s)
scipy.misc.imsave("v.png", v)

which gives hue from 0 to 360, saturation from 0 to 1 and value from 0 to 1. I looked at the results in image format, and they seem good.

I wasn't sure by reading your question whether it was only the "value" as in V from HSV that you were interested in. If it is, then you can bypass most of this code.

You can then select pixels based on those values and set them to 1 (or white/black) using something like:

newimage = (v > 0.3) * 1
Benjamin
  • 11,560
  • 13
  • 70
  • 119
  • I take it back. It's just a convention that grey values are defined with a hue of 0. NAN's work just as well as long as you are aware they are there. – Paul Feb 03 '11 at 21:55
  • Wow! nearly down to a one-liner! You are missing a paren and `d` is not defined. The streaks are gone from the Hue test, but there are other issues. I wonder if scipy's misc.image tool is doing something weird? – Paul Feb 04 '11 at 03:02
  • I must have copied and pasted mid-edit. Nice work by the way. I've always wished for a `select`-type function in numpy. Now I know there is one! – Paul Feb 04 '11 at 03:23
  • Here's what I get for hue using your algorithm: http://i.imgur.com/JKUsa.jpg Do you get the same thing? What version of scipy are you using? – Paul Feb 04 '11 at 21:36
  • 1
    It's a monochrome image. The pixeles in the center of the circle have hue other than zero but your algorithm is saying they are zero. I took the extra step of converting your array to a 'uint8' array and outputting thru PIL (`Image.fromarray((255*h/360).astype('uint8'),'L').save('hsvwheel_Hb2.png')`) and I get the same result. This means the error is not related to scipy's `misc.imsave()` function. There is something still wrong with your algorithm. – Paul Feb 04 '11 at 23:35
  • A-Ha! your definition of `M` and `m` *include* the alpha channel. Your problem is that scipy *does* support alpha! change `m, M = numpy.min(image, 2), numpy.max(image, 2)` to `m, M = numpy.min(image[:,:,:3], 2), numpy.max(image[:,:,:3], 2)` and all is well. – Paul Feb 05 '11 at 19:00
  • Also, your line: `h = numpy.select([h < 0, h >= 0], [h + 360, h])` is not needed as you have `%6` in the previous line. – Paul Feb 05 '11 at 19:03
  • @Paul: Good catch, I didn't notice that I was also using the alpha channel. Down to one line for hue, and finally getting the same results as you. Thanks for your help! – Benjamin Feb 05 '11 at 22:32
2

This solution is based on Paul's code. I fixed DivByZero Bug and implemented RGB to HSL. There is also HSL to RGB:

import numpy

def rgb_to_hsl_hsv(a, isHSV=True):
    """
    Converts RGB image data to HSV or HSL.
    :param a: 3D array. Retval of numpy.asarray(Image.open(...), int)
    :param isHSV: True = HSV, False = HSL
    :return: H,S,L or H,S,V array
    """
    R, G, B = a.T

    m = numpy.min(a, 2).T
    M = numpy.max(a, 2).T

    C = M - m #chroma
    Cmsk = C != 0

    # Hue
    H = numpy.zeros(R.shape, int)
    mask = (M == R) & Cmsk
    H[mask] = numpy.mod(60 * (G[mask] - B[mask]) / C[mask], 360)
    mask = (M == G) & Cmsk
    H[mask] = (60 * (B[mask] - R[mask]) / C[mask] + 120)
    mask = (M == B) & Cmsk
    H[mask] = (60 * (R[mask] - G[mask]) / C[mask] + 240)
    H *= 255
    H /= 360 # if you prefer, leave as 0-360, but don't convert to uint8


    # Saturation
    S = numpy.zeros(R.shape, int)

    if isHSV:
        # This code is for HSV:
        # Value
        V = M

        # Saturation
        S[Cmsk] = ((255 * C[Cmsk]) / V[Cmsk])
        # H, S, and V are now defined as integers 0-255
        return H.swapaxes(0, 1), S.swapaxes(0, 1), V.swapaxes(0, 1)
    else:
        # This code is for HSL:
        # Value
        L = 0.5 * (M + m)

        # Saturation
        S[Cmsk] = ((C[Cmsk]) / (1 - numpy.absolute(2 * L[Cmsk]/255.0 - 1)))
        # H, S, and L are now defined as integers 0-255
        return H.swapaxes(0, 1), S.swapaxes(0, 1), L.swapaxes(0, 1)


def rgb_to_hsv(a):
    return rgb_to_hsl_hsv(a, True)


def rgb_to_hsl(a):
    return rgb_to_hsl_hsv(a, False)


def hsl_to_rgb(H, S, L):
    """
    Converts HSL color array to RGB array

    H = [0..360]
    S = [0..1]
    l = [0..1]

    http://en.wikipedia.org/wiki/HSL_and_HSV#From_HSL

    Returns R,G,B in [0..255]
    """

    C = (1 - numpy.absolute(2 * L - 1)) * S

    Hp = H / 60.0
    X = C * (1 - numpy.absolute(numpy.mod(Hp, 2) - 1))

    # initilize with zero
    R = numpy.zeros(H.shape, float)
    G = numpy.zeros(H.shape, float)
    B = numpy.zeros(H.shape, float)

    # handle each case:

    mask = (Hp >= 0) == ( Hp < 1)
    R[mask] = C[mask]
    G[mask] = X[mask]

    mask = (Hp >= 1) == ( Hp < 2)
    R[mask] = X[mask]
    G[mask] = C[mask]

    mask = (Hp >= 2) == ( Hp < 3)
    G[mask] = C[mask]
    B[mask] = X[mask]

    mask = (Hp >= 3) == ( Hp < 4)
    G[mask] = X[mask]
    B[mask] = C[mask]

    mask = (Hp >= 4) == ( Hp < 5)
    R[mask] = X[mask]
    B[mask] = C[mask]

    mask = (Hp >= 5) == ( Hp < 6)
    R[mask] = C[mask]
    B[mask] = X[mask]

    m = L - 0.5*C
    R += m
    G += m
    B += m

    R *=255.0
    G *=255.0
    B *=255.0

    return R.astype(int),G.astype(int),B.astype(int)

def combineRGB(r,g,b):
    """
    Combines separated R G B arrays into one array = image.
    scipy.misc.imsave("rgb.png", combineRGB(R,G,B))
    """
    rgb = numpy.zeros((r.shape[0],r.shape[1],3), 'uint8')
    rgb[..., 0] = r
    rgb[..., 1] = g
    rgb[..., 2] = b
    return rgb
Stefan Profanter
  • 6,458
  • 6
  • 41
  • 73
1

I think the fastest result would be through numpy. The function would look something like (updated, added more detail to example):

limg = im.convert("L", ( 0.5, 0.5, 0.5, 0.5 ) )
na = numpy.array ( limg.getdata() )
na = numpy.piecewise(na, [ na > 128 ], [255, 0])
limg.pytdata(na)
limg.save("new.png")

Ideally, you could use the piecewise function without first converting to black and white, that would be more like the original example. The syntax would be something along the lines of:

na = numpy.piecewise(na, [ na[0] > 128 ], [255, 0])

But, you would have to be careful as an RGB image is either a 3 or 4 tuple on the return value.

Clarus
  • 2,259
  • 16
  • 27
  • 1
    using numpy's `asarray()` function is the usual conversion technique as it saves a step. I'm unclear where you are going with `piecewise()`. Could you elaborate? – Paul Feb 03 '11 at 21:09
  • ya please elaborate whats going on here. – Claudiu Feb 03 '11 at 23:07