From d34fd8868b8fe86d197a88c717b86c8ba37c1d80 Mon Sep 17 00:00:00 2001 From: hugovk Date: Mon, 21 Jul 2014 12:15:23 +0300 Subject: [PATCH 1/8] Workaround to test PyQt: https://github.com/travis-ci/travis-ci/issues/2219#issuecomment-41804942 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 09e523c39..b37588843 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ python: - "pypy" - 2.6 - 2.7 + - "2.7_with_system_site_packages" # For PyQt4 - 3.2 - 3.3 - 3.4 From ee4793a806f34c428f059f0e4256c61bad351f60 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 21 Jul 2014 22:29:38 -0700 Subject: [PATCH 2/8] More detail when assert_image_similar fails --- Tests/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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): From 625ff24358b3705791450fd68b0f9ad8e8afca98 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 21 Jul 2014 22:31:32 -0700 Subject: [PATCH 3/8] Storage, packing and access for HSV format images --- PIL/Image.py | 1 + PIL/PyAccess.py | 1 + Tests/make_hash.py | 1 + libImaging/Access.c | 5 +++-- libImaging/Pack.c | 6 ++++++ libImaging/Storage.c | 7 +++++++ libImaging/Unpack.c | 6 ++++++ 7 files changed, 25 insertions(+), 2 deletions(-) diff --git a/PIL/Image.py b/PIL/Image.py index ea8cc6155..a61aaa62b 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/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/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/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}, From 0bb1cd398fc5ba14e69795eb26f709a7b0bf2436 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 21 Jul 2014 22:36:40 -0700 Subject: [PATCH 4/8] Conversion between RGB and HSV images --- Tests/test_format_hsv.py | 177 +++++++++++++++++++++++++++++++++++++++ libImaging/Convert.c | 126 ++++++++++++++++++++++++++++ 2 files changed, 303 insertions(+) create mode 100644 Tests/test_format_hsv.py diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py new file mode 100644 index 000000000..1a9c46bd7 --- /dev/null +++ b/Tests/test_format_hsv.py @@ -0,0 +1,177 @@ +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 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: + f_r = map(self.str_to_float,r.tobytes()) + f_g = map(self.str_to_float,g.tobytes()) + f_b = map(self.str_to_float,b.tobytes()) + else: + f_r = map(self.int_to_float,r.tobytes()) + f_g = map(self.int_to_float,g.tobytes()) + f_b = map(self.int_to_float,b.tobytes()) + + f_h = []; + f_s = []; + f_v = []; + + if hasattr(itertools, 'izip'): + iter_helper = itertools.izip + else: + iter_helper = itertools.zip_longest + + for (_r, _g, _b) in iter_helper(f_r, f_g, f_b): + _h, _s, _v = func(_r, _g, _b) + f_h.append(_h) + f_s.append(_s) + f_v.append(_v) + + h = Image.new('L', r.size) + h.putdata(list(map(self.to_int, f_h))) + s = Image.new('L', r.size) + s.putdata(list(map(self.to_int, f_s))) + v = Image.new('L', r.size) + v.putdata(list(map(self.to_int, f_v))) + + hsv = Image.merge(mode, (h, s, v)) + + 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): + im = self.wedge().convert('HSV') + comparable = self.to_hsv_colorsys(self.wedge()) + + #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 = self.wedge() + 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/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 }, From ffe8887cc6ab2d45bf366b2b039f73e7aa8a5d5f Mon Sep 17 00:00:00 2001 From: wiredfool Date: Wed, 23 Jul 2014 08:48:55 -0700 Subject: [PATCH 5/8] profiler for testing --- profile-installed.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100755 profile-installed.py 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) From 67c235b7c084c8fed6284cd1b7f43f8d67754d19 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Wed, 23 Jul 2014 08:49:19 -0700 Subject: [PATCH 6/8] Don't DOS pypy --- Tests/test_format_hsv.py | 48 ++++++++++++++++------------------------ 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index 1a9c46bd7..be9c86e1c 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -12,7 +12,10 @@ class TestFormatHSV(PillowTestCase): 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)) @@ -49,38 +52,24 @@ class TestFormatHSV(PillowTestCase): (r,g,b) = im.split() if bytes is str: - f_r = map(self.str_to_float,r.tobytes()) - f_g = map(self.str_to_float,g.tobytes()) - f_b = map(self.str_to_float,b.tobytes()) + conv_func = self.str_to_float else: - f_r = map(self.int_to_float,r.tobytes()) - f_g = map(self.int_to_float,g.tobytes()) - f_b = map(self.int_to_float,b.tobytes()) - - f_h = []; - f_s = []; - f_v = []; + 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())] + + + new_bytes = b''.join(chr(h)+chr(s)+chr(v) for (h,s,v) in converted) + + hsv = Image.frombytes(mode,r.size, new_bytes) - for (_r, _g, _b) in iter_helper(f_r, f_g, f_b): - _h, _s, _v = func(_r, _g, _b) - f_h.append(_h) - f_s.append(_s) - f_v.append(_v) - - h = Image.new('L', r.size) - h.putdata(list(map(self.to_int, f_h))) - s = Image.new('L', r.size) - s.putdata(list(map(self.to_int, f_s))) - v = Image.new('L', r.size) - v.putdata(list(map(self.to_int, f_v))) - - hsv = Image.merge(mode, (h, s, v)) - return hsv def to_hsv_colorsys(self, im): @@ -90,8 +79,9 @@ class TestFormatHSV(PillowTestCase): return self.to_xxx_colorsys(im, colorsys.hsv_to_rgb, 'RGB') def test_wedge(self): - im = self.wedge().convert('HSV') - comparable = self.to_hsv_colorsys(self.wedge()) + 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))) @@ -111,7 +101,7 @@ class TestFormatHSV(PillowTestCase): #print (im.getpixel((192, 64))) - comparable = self.wedge() + comparable = src im = im.convert('RGB') #im.split()[0].show() From e14e3593d98056fb86811e4613f221009320d5fe Mon Sep 17 00:00:00 2001 From: wiredfool Date: Wed, 23 Jul 2014 09:08:28 -0700 Subject: [PATCH 7/8] And now for something completely different. Py3 compatibility --- Tests/test_format_hsv.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index be9c86e1c..03603aa9b 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -65,8 +65,10 @@ class TestFormatHSV(PillowTestCase): 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())] - - new_bytes = b''.join(chr(h)+chr(s)+chr(v) for (h,s,v) in converted) + 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) From 13bd1d6006cf93c0b3f016a3efd502c8dcef3b95 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 24 Jul 2014 09:21:09 +0300 Subject: [PATCH 8/8] Update CHANGES.rst [CI skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) 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]