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