diff --git a/CHANGES.rst b/CHANGES.rst index c93572cbd..a0253def0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,9 @@ Changelog (Pillow) 2.6.0 (unreleased) ------------------ +- HSV Support #816 + [wiredfool] + - Removed unusable ImagePalette.new() [hugovk] diff --git a/PIL/Image.py b/PIL/Image.py index 3a42e308b..489b59b92 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -220,6 +220,7 @@ _MODEINFO = { "CMYK": ("RGB", "L", ("C", "M", "Y", "K")), "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")), "LAB": ("RGB", "L", ("L", "A", "B")), + "HSV": ("RGB", "L", ("H", "S", "V")), # Experimental modes include I;16, I;16L, I;16B, RGBa, BGR;15, and # BGR;24. Use these modes only if you know exactly what you're diff --git a/PIL/PyAccess.py b/PIL/PyAccess.py index f76beb820..7ccc313eb 100644 --- a/PIL/PyAccess.py +++ b/PIL/PyAccess.py @@ -261,6 +261,7 @@ mode_map = {'1': _PyAccess8, 'PA': _PyAccess32_2, 'RGB': _PyAccess32_3, 'LAB': _PyAccess32_3, + 'HSV': _PyAccess32_3, 'YCbCr': _PyAccess32_3, 'RGBA': _PyAccess32_4, 'RGBa': _PyAccess32_4, diff --git a/Tests/helper.py b/Tests/helper.py index 64f29bd5d..082fb93f9 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -100,7 +100,7 @@ class PillowTestCase(unittest.TestCase): ave_diff = float(diff)/(a.size[0]*a.size[1]) self.assertGreaterEqual( epsilon, ave_diff, - msg or "average pixel value difference %.4f > epsilon %.4f" % ( + (msg or '') + " average pixel value difference %.4f > epsilon %.4f" % ( ave_diff, epsilon)) def assert_warning(self, warn_class, func): diff --git a/Tests/make_hash.py b/Tests/make_hash.py index 32196e9f8..88bb2994b 100644 --- a/Tests/make_hash.py +++ b/Tests/make_hash.py @@ -9,6 +9,7 @@ modes = [ "RGB", "RGBA", "RGBa", "RGBX", "CMYK", "YCbCr", + "LAB", "HSV", ] diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py new file mode 100644 index 000000000..03603aa9b --- /dev/null +++ b/Tests/test_format_hsv.py @@ -0,0 +1,169 @@ +from helper import unittest, PillowTestCase, lena + +from PIL import Image + +import colorsys, itertools + +class TestFormatHSV(PillowTestCase): + + def int_to_float(self, i): + return float(i)/255.0 + def str_to_float(self, i): + return float(ord(i))/255.0 + def to_int(self, f): + return int(f*255.0) + def tuple_to_ints(self, tp): + x,y,z = tp + return (int(x*255.0), int(y*255.0), int(z*255.0)) + + def test_sanity(self): + im = Image.new('HSV', (100,100)) + + def wedge(self): + w =Image._wedge() + w90 = w.rotate(90) + + (px, h) = w.size + + r = Image.new('L', (px*3,h)) + g = r.copy() + b = r.copy() + + r.paste(w, (0,0)) + r.paste(w90, (px,0)) + + g.paste(w90, (0,0)) + g.paste(w, (2*px,0)) + + b.paste(w, (px,0)) + b.paste(w90, (2*px,0)) + + img = Image.merge('RGB',(r,g,b)) + + #print (("%d, %d -> "% (int(1.75*px),int(.25*px))) + \ + # "(%s, %s, %s)"%img.getpixel((1.75*px, .25*px))) + #print (("%d, %d -> "% (int(.75*px),int(.25*px))) + \ + # "(%s, %s, %s)"%img.getpixel((.75*px, .25*px))) + return img + + def to_xxx_colorsys(self, im, func, mode): + # convert the hard way using the library colorsys routines. + + (r,g,b) = im.split() + + if bytes is str: + conv_func = self.str_to_float + else: + conv_func = self.int_to_float + + if hasattr(itertools, 'izip'): + iter_helper = itertools.izip + else: + iter_helper = itertools.zip_longest + + + converted = [self.tuple_to_ints(func(conv_func(_r), conv_func(_g), conv_func(_b))) + for (_r, _g, _b) in iter_helper(r.tobytes(), g.tobytes(), b.tobytes())] + + if str is bytes: + new_bytes = b''.join(chr(h)+chr(s)+chr(v) for (h,s,v) in converted) + else: + new_bytes = b''.join(bytes(chr(h)+chr(s)+chr(v), 'latin-1') for (h,s,v) in converted) + + hsv = Image.frombytes(mode,r.size, new_bytes) + + return hsv + + def to_hsv_colorsys(self, im): + return self.to_xxx_colorsys(im, colorsys.rgb_to_hsv, 'HSV') + + def to_rgb_colorsys(self, im): + return self.to_xxx_colorsys(im, colorsys.hsv_to_rgb, 'RGB') + + def test_wedge(self): + src = self.wedge().resize((3*32,32),Image.BILINEAR) + im = src.convert('HSV') + comparable = self.to_hsv_colorsys(src) + + #print (im.getpixel((448, 64))) + #print (comparable.getpixel((448, 64))) + + #print(im.split()[0].histogram()) + #print(comparable.split()[0].histogram()) + + #im.split()[0].show() + #comparable.split()[0].show() + + self.assert_image_similar(im.split()[0], comparable.split()[0], + 1, "Hue conversion is wrong") + self.assert_image_similar(im.split()[1], comparable.split()[1], + 1, "Saturation conversion is wrong") + self.assert_image_similar(im.split()[2], comparable.split()[2], + 1, "Value conversion is wrong") + + #print (im.getpixel((192, 64))) + + comparable = src + im = im.convert('RGB') + + #im.split()[0].show() + #comparable.split()[0].show() + #print (im.getpixel((192, 64))) + #print (comparable.getpixel((192, 64))) + + self.assert_image_similar(im.split()[0], comparable.split()[0], + 3, "R conversion is wrong") + self.assert_image_similar(im.split()[1], comparable.split()[1], + 3, "G conversion is wrong") + self.assert_image_similar(im.split()[2], comparable.split()[2], + 3, "B conversion is wrong") + + + def test_convert(self): + im = lena('RGB').convert('HSV') + comparable = self.to_hsv_colorsys(lena('RGB')) + +# print ([ord(x) for x in im.split()[0].tobytes()[:80]]) +# print ([ord(x) for x in comparable.split()[0].tobytes()[:80]]) + +# print(im.split()[0].histogram()) +# print(comparable.split()[0].histogram()) + + self.assert_image_similar(im.split()[0], comparable.split()[0], + 1, "Hue conversion is wrong") + self.assert_image_similar(im.split()[1], comparable.split()[1], + 1, "Saturation conversion is wrong") + self.assert_image_similar(im.split()[2], comparable.split()[2], + 1, "Value conversion is wrong") + + + def test_hsv_to_rgb(self): + comparable = self.to_hsv_colorsys(lena('RGB')) + converted = comparable.convert('RGB') + comparable = self.to_rgb_colorsys(comparable) + + # print(converted.split()[1].histogram()) + # print(target.split()[1].histogram()) + + # print ([ord(x) for x in target.split()[1].tobytes()[:80]]) + # print ([ord(x) for x in converted.split()[1].tobytes()[:80]]) + + + self.assert_image_similar(converted.split()[0], comparable.split()[0], + 3, "R conversion is wrong") + self.assert_image_similar(converted.split()[1], comparable.split()[1], + 3, "G conversion is wrong") + self.assert_image_similar(converted.split()[2], comparable.split()[2], + 3, "B conversion is wrong") + + + + + + + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/libImaging/Access.c b/libImaging/Access.c index 62c97f3a3..97474a0b8 100644 --- a/libImaging/Access.c +++ b/libImaging/Access.c @@ -13,8 +13,8 @@ #include "Imaging.h" /* use Tests/make_hash.py to calculate these values */ -#define ACCESS_TABLE_SIZE 21 -#define ACCESS_TABLE_HASH 30197 +#define ACCESS_TABLE_SIZE 27 +#define ACCESS_TABLE_HASH 3078 static struct ImagingAccessInstance access_table[ACCESS_TABLE_SIZE]; @@ -238,6 +238,7 @@ ImagingAccessInit() ADD("CMYK", line_32, get_pixel_32, put_pixel_32); ADD("YCbCr", line_32, get_pixel_32, put_pixel_32); ADD("LAB", line_32, get_pixel_32, put_pixel_32); + ADD("HSV", line_32, get_pixel_32, put_pixel_32); } ImagingAccess diff --git a/libImaging/Convert.c b/libImaging/Convert.c index 631263b31..4eb106c27 100644 --- a/libImaging/Convert.c +++ b/libImaging/Convert.c @@ -35,6 +35,9 @@ #include "Imaging.h" +#define MAX(a, b) (a)>(b) ? (a) : (b) +#define MIN(a, b) (a)<(b) ? (a) : (b) + #define CLIP(v) ((v) <= 0 ? 0 : (v) >= 255 ? 255 : (v)) #define CLIP16(v) ((v) <= -32768 ? -32768 : (v) >= 32767 ? 32767 : (v)) @@ -236,6 +239,126 @@ rgb2bgr24(UINT8* out, const UINT8* in, int xsize) } } +static void +rgb2hsv(UINT8* out, const UINT8* in, int xsize) +{ // following colorsys.py + float h,s,rc,gc,bc,cr; + UINT8 maxc,minc; + UINT8 r, g, b; + UINT8 uh,us,uv; + int x; + + for (x = 0; x < xsize; x++, in += 4) { + r = in[0]; + g = in[1]; + b = in[2]; + + maxc = MAX(r,MAX(g,b)); + minc = MIN(r,MIN(g,b)); + uv = maxc; + if (minc == maxc){ + *out++ = 0; + *out++ = 0; + *out++ = uv; + } else { + cr = (float)(maxc-minc); + s = cr/(float)maxc; + rc = ((float)(maxc-r))/cr; + gc = ((float)(maxc-g))/cr; + bc = ((float)(maxc-b))/cr; + if (r == maxc) { + h = bc-gc; + } else if (g == maxc) { + h = 2.0 + rc-bc; + } else { + h = 4.0 + gc-rc; + } + // incorrect hue happens if h/6 is negative. + h = fmod((h/6.0 + 1.0), 1.0); + + uh = (UINT8)CLIP((int)(h*255.0)); + us = (UINT8)CLIP((int)(s*255.0)); + + *out++ = uh; + *out++ = us; + *out++ = uv; + + } + *out++ = in[3]; + } +} + +static void +hsv2rgb(UINT8* out, const UINT8* in, int xsize) +{ // following colorsys.py + + int p,q,t; + uint up,uq,ut; + int i, x; + float f, fs; + uint h,s,v; + + for (x = 0; x < xsize; x++, in += 4) { + h = in[0]; + s = in[1]; + v = in[2]; + + if (s==0){ + *out++ = v; + *out++ = v; + *out++ = v; + } else { + i = floor((float)h * 6.0 / 255.0); // 0 - 6 + f = (float)h * 6.0 / 255.0 - (float)i; // 0-1 : remainder. + fs = ((float)s)/255.0; + + p = round((float)v * (1.0-fs)); + q = round((float)v * (1.0-fs*f)); + t = round((float)v * (1.0-fs*(1.0-f))); + up = (UINT8)CLIP(p); + uq = (UINT8)CLIP(q); + ut = (UINT8)CLIP(t); + + switch (i%6) { + case 0: + *out++ = v; + *out++ = ut; + *out++ = up; + break; + case 1: + *out++ = uq; + *out++ = v; + *out++ = up; + break; + case 2: + *out++ = up; + *out++ = v; + *out++ = ut; + break; + case 3: + *out++ = up; + *out++ = uq; + *out++ = v; + break; + case 4: + *out++ = ut; + *out++ = up; + *out++ = v; + break; + case 5: + *out++ = v; + *out++ = up; + *out++ = uq; + break; + + } + } + *out++ = in[3]; + } +} + + + /* ---------------- */ /* RGBA conversions */ /* ---------------- */ @@ -658,6 +781,7 @@ static struct { { "RGB", "RGBX", rgb2rgba }, { "RGB", "CMYK", rgb2cmyk }, { "RGB", "YCbCr", ImagingConvertRGB2YCbCr }, + { "RGB", "HSV", rgb2hsv }, { "RGBA", "1", rgb2bit }, { "RGBA", "L", rgb2l }, @@ -687,6 +811,8 @@ static struct { { "YCbCr", "L", ycbcr2l }, { "YCbCr", "RGB", ImagingConvertYCbCr2RGB }, + { "HSV", "RGB", hsv2rgb }, + { "I", "I;16", I_I16L }, { "I;16", "I", I16L_I }, { "L", "I;16", L_I16L }, diff --git a/libImaging/Pack.c b/libImaging/Pack.c index 1cc1f3a94..fecafbde4 100644 --- a/libImaging/Pack.c +++ b/libImaging/Pack.c @@ -566,6 +566,12 @@ static struct { {"LAB", "A", 8, band1}, {"LAB", "B", 8, band2}, + /* HSV */ + {"HSV", "HSV", 24, ImagingPackRGB}, + {"HSV", "H", 8, band0}, + {"HSV", "S", 8, band1}, + {"HSV", "V", 8, band2}, + /* integer */ {"I", "I", 32, copy4}, {"I", "I;16B", 16, packI16B}, diff --git a/libImaging/Storage.c b/libImaging/Storage.c index c6d2e5c5e..d65de1c0a 100644 --- a/libImaging/Storage.c +++ b/libImaging/Storage.c @@ -186,6 +186,13 @@ ImagingNewPrologueSubtype(const char *mode, unsigned xsize, unsigned ysize, im->pixelsize = 4; im->linesize = xsize * 4; + } else if (strcmp(mode, "HSV") == 0) { + /* 24-bit color, luminance, + 2 color channels */ + /* L is uint8, a,b are int8 */ + im->bands = 3; + im->pixelsize = 4; + im->linesize = xsize * 4; + } else { free(im); return (Imaging) ImagingError_ValueError("unrecognized mode"); diff --git a/libImaging/Unpack.c b/libImaging/Unpack.c index 552c759b9..7c453dbfd 100644 --- a/libImaging/Unpack.c +++ b/libImaging/Unpack.c @@ -1159,6 +1159,12 @@ static struct { {"LAB", "A", 8, band1}, {"LAB", "B", 8, band2}, + /* HSV Color */ + {"HSV", "HSV", 24, ImagingUnpackRGB}, + {"HSV", "H", 8, band0}, + {"HSV", "S", 8, band1}, + {"HSV", "V", 8, band2}, + /* integer variations */ {"I", "I", 32, copy4}, {"I", "I;8", 8, unpackI8}, diff --git a/profile-installed.py b/profile-installed.py new file mode 100755 index 000000000..485792f51 --- /dev/null +++ b/profile-installed.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +import nose +import os +import sys +import glob + +import profile + +# monkey with the path, removing the local directory but adding the Tests/ +# directory for helper.py and the other local imports there. + +del(sys.path[0]) +sys.path.insert(0, os.path.abspath('./Tests')) + +# if there's no test selected (mostly) choose a working default. +# Something is required, because if we import the tests from the local +# directory, once again, we've got the non-installed PIL in the way +if len(sys.argv) == 1: + sys.argv.extend(glob.glob('Tests/test*.py')) + +# Make sure that nose doesn't muck with our paths. +if ('--no-path-adjustment' not in sys.argv) and ('-P' not in sys.argv): + sys.argv.insert(1, '--no-path-adjustment') + +if 'NOSE_PROCESSES' not in os.environ: + for arg in sys.argv: + if '--processes' in arg: + break + else: # for + sys.argv.insert(1, '--processes=-1') # -1 == number of cores + sys.argv.insert(1, '--process-timeout=30') + +if __name__ == '__main__': + profile.run("nose.main()", sort=2)