Detecting thresholds in HSV color space (from RGB) using Python / PIL

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


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

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

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.