Merge pull request #816 from wiredfool/hsv

HSV Support
This commit is contained in:
Hugo 2014-07-24 09:20:04 +03:00
commit 352f1fd41f
11 changed files with 355 additions and 3 deletions

View File

@ -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

View File

@ -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,

View File

@ -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):

View File

@ -9,6 +9,7 @@ modes = [
"RGB", "RGBA", "RGBa", "RGBX",
"CMYK",
"YCbCr",
"LAB", "HSV",
]

169
Tests/test_format_hsv.py Normal file
View File

@ -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

View File

@ -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

View File

@ -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 },

View File

@ -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},

View File

@ -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");

View File

@ -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},

34
profile-installed.py Executable file
View File

@ -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)