diff --git a/Tests/images/color_snakes.png b/Tests/images/color_snakes.png new file mode 100644 index 000000000..bf3a35196 Binary files /dev/null and b/Tests/images/color_snakes.png differ diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index ba65f211b..5164af015 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -5,6 +5,7 @@ from PIL import ImageOps from PIL import ImageFilter im = Image.open("Tests/images/hopper.ppm") +snakes = Image.open("Tests/images/color_snakes.png") class TestImageOpsUsm(PillowTestCase): @@ -16,7 +17,7 @@ class TestImageOpsUsm(PillowTestCase): self.assertEqual(i.size, (128, 128)) # i.save("blur.bmp") - i = ImageOps.usm(im, 2.0, 125, 8) + i = ImageOps.unsharp_mask(im, 2.0, 125, 8) self.assertEqual(i.mode, "RGB") self.assertEqual(i.size, (128, 128)) # i.save("usm.bmp") @@ -33,7 +34,7 @@ class TestImageOpsUsm(PillowTestCase): self.assertEqual(i.mode, "RGB") self.assertEqual(i.size, (128, 128)) - def test_usm(self): + def test_usm_formats(self): usm = ImageOps.unsharp_mask self.assertRaises(ValueError, lambda: usm(im.convert("1"))) @@ -45,7 +46,7 @@ class TestImageOpsUsm(PillowTestCase): usm(im.convert("CMYK")) self.assertRaises(ValueError, lambda: usm(im.convert("YCbCr"))) - def test_blur(self): + def test_blur_formats(self): blur = ImageOps.gaussian_blur self.assertRaises(ValueError, lambda: blur(im.convert("1"))) @@ -57,6 +58,33 @@ class TestImageOpsUsm(PillowTestCase): blur(im.convert("CMYK")) self.assertRaises(ValueError, lambda: blur(im.convert("YCbCr"))) + def test_usm_accuracy(self): + + i = snakes._new(ImageOps.unsharp_mask(snakes, 5, 1024, 0)) + # Image should not be changed because it have only 0 and 255 levels. + self.assertEqual(i.tobytes(), snakes.tobytes()) + + def test_blur_accuracy(self): + + i = snakes._new(ImageOps.gaussian_blur(snakes, 1)) + # Alpha channel must match whole. + self.assertEqual(i.split()[3], snakes.split()[3]) + # These pixels surrounded with pixels with 255 intensity. + # They must be 255. + for x, y, c in [(1, 0, 1), (2, 0, 1), (7, 8, 1), (8, 8, 1), (2, 9, 1), + (7, 3, 0), (8, 3, 0), (5, 8, 0), (5, 9, 0), (1, 3, 0), + (4, 3, 2), (4, 2, 2)]: + self.assertEqual(i.im.getpixel((x, y))[c], 255) + # Fuzzy match. + gp = lambda x, y: i.im.getpixel((x, y)) + self.assertTrue(211 <= gp(7, 4)[0] <= 213) + self.assertTrue(211 <= gp(7, 5)[2] <= 213) + self.assertTrue(211 <= gp(7, 6)[2] <= 213) + self.assertTrue(211 <= gp(7, 7)[1] <= 213) + self.assertTrue(211 <= gp(8, 4)[0] <= 213) + self.assertTrue(211 <= gp(8, 5)[2] <= 213) + self.assertTrue(211 <= gp(8, 6)[2] <= 213) + self.assertTrue(211 <= gp(8, 7)[1] <= 213) if __name__ == '__main__': unittest.main() diff --git a/libImaging/UnsharpMask.c b/libImaging/UnsharpMask.c index 231826245..76d630a8f 100644 --- a/libImaging/UnsharpMask.c +++ b/libImaging/UnsharpMask.c @@ -48,10 +48,10 @@ static inline UINT8 clip(double in) { if (in >= 255.0) - return (UINT8) 255; + return (UINT8) 255; if (in <= 0.0) - return (UINT8) 0; - return (UINT8) in; + return (UINT8) 0; + return (UINT8) (in + 0.5); } static Imaging @@ -79,6 +79,7 @@ gblur(Imaging im, Imaging imOut, float floatRadius, int channels, int padding) int radius = 0; float remainder = 0.0; + int hasAlpha = 0; int i; @@ -108,31 +109,31 @@ gblur(Imaging im, Imaging imOut, float floatRadius, int channels, int padding) maskData = malloc(radius * sizeof(float)); /* FIXME: error checking */ for (x = 0; x < radius; x++) { - z = ((float) (x + 2) / ((float) radius)); - dev = 0.5 + (((float) (radius * radius)) * 0.001); - /* you can adjust this factor to change the shape/center-weighting - of the gaussian */ - maskData[x] = (float) pow((1.0 / sqrt(2.0 * 3.14159265359 * dev)), - ((-(z - 1.0) * -(x - 1.0)) / - (2.0 * dev))); + z = ((float) (x + 2) / ((float) radius)); + dev = 0.5 + (((float) (radius * radius)) * 0.001); + /* you can adjust this factor to change the shape/center-weighting + of the gaussian */ + maskData[x] = (float) pow((1.0 / sqrt(2.0 * 3.14159265359 * dev)), + ((-(z - 1.0) * -(x - 1.0)) / + (2.0 * dev))); } /* if there's any remainder, multiply the first/last values in MaskData it. this allows us to support float radius values. */ if (remainder > 0.0) { - maskData[0] *= remainder; - maskData[radius - 1] *= remainder; + maskData[0] *= remainder; + maskData[radius - 1] *= remainder; } for (x = 0; x < radius; x++) { - /* this is done separately now due to the correction for float - radius values above */ - sum += maskData[x]; + /* this is done separately now due to the correction for float + radius values above */ + sum += maskData[x]; } for (i = 0; i < radius; i++) { - maskData[i] *= (1.0 / sum); - /* printf("%f\n", maskData[i]); */ + maskData[i] *= (1.0 / sum); + /* printf("%f\n", maskData[i]); */ } /* create a temporary memory buffer for the data for the first pass @@ -140,9 +141,9 @@ gblur(Imaging im, Imaging imOut, float floatRadius, int channels, int padding) /* don't bother about alpha/padding */ buffer = calloc((size_t) (im->xsize * im->ysize * channels), - sizeof(float)); + sizeof(float)); if (buffer == NULL) - return ImagingError_MemoryError(); + return ImagingError_MemoryError(); /* be nice to other threads while you go off to lala land */ ImagingSectionEnter(&cookie); @@ -153,94 +154,98 @@ gblur(Imaging im, Imaging imOut, float floatRadius, int channels, int padding) /* perform a blur on each line, and place in the temporary storage buffer */ for (y = 0; y < im->ysize; y++) { - if (channels == 1 && im->image8 != NULL) { - line8 = (UINT8 *) im->image8[y]; - } else { - line = im->image32[y]; - } - for (x = 0; x < im->xsize; x++) { - newPixel[0] = newPixel[1] = newPixel[2] = newPixel[3] = 0; - /* for each neighbor pixel, factor in its value/weighting to the - current pixel */ - for (pix = 0; pix < radius; pix++) { - /* figure the offset of this neighbor pixel */ - offset = - (int) ((-((float) radius / 2.0) + (float) pix) + 0.5); - if (x + offset < 0) - offset = -x; - else if (x + offset >= im->xsize) - offset = im->xsize - x - 1; + if (channels == 1 && im->image8 != NULL) { + line8 = (UINT8 *) im->image8[y]; + } else { + line = im->image32[y]; + } + for (x = 0; x < im->xsize; x++) { + newPixel[0] = newPixel[1] = newPixel[2] = newPixel[3] = 0; + /* for each neighbor pixel, factor in its value/weighting to the + current pixel */ + for (pix = 0; pix < radius; pix++) { + /* figure the offset of this neighbor pixel */ + offset = + (int) ((-((float) radius / 2.0) + (float) pix) + 0.5); + if (x + offset < 0) + offset = -x; + else if (x + offset >= im->xsize) + offset = im->xsize - x - 1; - /* add (neighbor pixel value * maskData[pix]) to the current - pixel value */ - if (channels == 1) { - buffer[(y * im->xsize) + x] += - ((float) ((UINT8 *) & line8[x + offset])[0]) * - (maskData[pix]); - } else { - for (channel = 0; channel < channels; channel++) { - buffer[(y * im->xsize * channels) + - (x * channels) + channel] += - ((float) ((UINT8 *) & line[x + offset]) - [channel]) * (maskData[pix]); - } - } - } - } + /* add (neighbor pixel value * maskData[pix]) to the current + pixel value */ + if (channels == 1) { + buffer[(y * im->xsize) + x] += + ((float) ((UINT8 *) & line8[x + offset])[0]) * + (maskData[pix]); + } else { + for (channel = 0; channel < channels; channel++) { + buffer[(y * im->xsize * channels) + + (x * channels) + channel] += + ((float) ((UINT8 *) & line[x + offset]) + [channel]) * (maskData[pix]); + } + } + } + } + } + + if (strcmp(im->mode, "RGBX") == 0 || strcmp(im->mode, "RGBA") == 0) { + hasAlpha = 1; } /* perform a blur on each column in the buffer, and place in the output image */ for (x = 0; x < im->xsize; x++) { - for (y = 0; y < im->ysize; y++) { - newPixel[0] = newPixel[1] = newPixel[2] = newPixel[3] = 0; - /* for each neighbor pixel, factor in its value/weighting to the - current pixel */ - for (pix = 0; pix < radius; pix++) { - /* figure the offset of this neighbor pixel */ - offset = - (int) (-((float) radius / 2.0) + (float) pix + 0.5); - if (y + offset < 0) - offset = -y; - else if (y + offset >= im->ysize) - offset = im->ysize - y - 1; - /* add (neighbor pixel value * maskData[pix]) to the current - pixel value */ - for (channel = 0; channel < channels; channel++) { - newPixel[channel] += - (buffer - [((y + offset) * im->xsize * channels) + - (x * channels) + channel]) * (maskData[pix]); - } - } - /* if the image is RGBX or RGBA, copy the 4th channel data to - newPixel, so it gets put in imOut */ - if (strcmp(im->mode, "RGBX") == 0 - || strcmp(im->mode, "RGBA") == 0) { - newPixel[3] = (float) ((UINT8 *) & line[x + offset])[3]; - } + for (y = 0; y < im->ysize; y++) { + newPixel[0] = newPixel[1] = newPixel[2] = newPixel[3] = 0; + /* for each neighbor pixel, factor in its value/weighting to the + current pixel */ + for (pix = 0; pix < radius; pix++) { + /* figure the offset of this neighbor pixel */ + offset = + (int) (-((float) radius / 2.0) + (float) pix + 0.5); + if (y + offset < 0) + offset = -y; + else if (y + offset >= im->ysize) + offset = im->ysize - y - 1; - /* pack the channels into an INT32 so we can put them back in - the PIL image */ - newPixelFinals = 0; - if (channels == 1) { - newPixelFinals = clip(newPixel[0]); - } else { - /* for RGB, the fourth channel isn't used anyways, so just - pack a 0 in there, this saves checking the mode for each - pixel. */ - /* this doesn't work on little-endian machines... fix it! */ - newPixelFinals = - clip(newPixel[0]) | clip(newPixel[1]) << 8 | - clip(newPixel[2]) << 16 | clip(newPixel[3]) << 24; - } - /* set the resulting pixel in imOut */ - if (channels == 1) { - imOut->image8[y][x] = (UINT8) newPixelFinals; - } else { - imOut->image32[y][x] = newPixelFinals; - } - } + /* add (neighbor pixel value * maskData[pix]) to the current + pixel value */ + for (channel = 0; channel < channels; channel++) { + newPixel[channel] += + (buffer + [((y + offset) * im->xsize * channels) + + (x * channels) + channel]) * (maskData[pix]); + } + } + /* if the image is RGBX or RGBA, copy the 4th channel data to + newPixel, so it gets put in imOut */ + if (hasAlpha) { + newPixel[3] = (float) ((UINT8 *) & im->image32[y][x])[3]; + } + + /* pack the channels into an INT32 so we can put them back in + the PIL image */ + newPixelFinals = 0; + if (channels == 1) { + newPixelFinals = clip(newPixel[0]); + } else { + /* for RGB, the fourth channel isn't used anyways, so just + pack a 0 in there, this saves checking the mode for each + pixel. */ + /* this doesn't work on little-endian machines... fix it! */ + newPixelFinals = + clip(newPixel[0]) | clip(newPixel[1]) << 8 | + clip(newPixel[2]) << 16 | clip(newPixel[3]) << 24; + } + /* set the resulting pixel in imOut */ + if (channels == 1) { + imOut->image8[y][x] = (UINT8) newPixelFinals; + } else { + imOut->image32[y][x] = newPixelFinals; + } + } } /* free the buffer */ @@ -258,29 +263,29 @@ Imaging ImagingGaussianBlur(Imaging im, Imaging imOut, float radius) int padding = 0; if (strcmp(im->mode, "RGB") == 0) { - channels = 3; - padding = 1; + channels = 3; + padding = 1; } else if (strcmp(im->mode, "RGBA") == 0) { - channels = 3; - padding = 1; + channels = 3; + padding = 1; } else if (strcmp(im->mode, "RGBX") == 0) { - channels = 3; - padding = 1; + channels = 3; + padding = 1; } else if (strcmp(im->mode, "CMYK") == 0) { - channels = 4; - padding = 0; + channels = 4; + padding = 0; } else if (strcmp(im->mode, "L") == 0) { - channels = 1; - padding = 0; + channels = 1; + padding = 0; } else - return ImagingError_ModeError(); + return ImagingError_ModeError(); return gblur(im, imOut, radius, channels, padding); } Imaging ImagingUnsharpMask(Imaging im, Imaging imOut, float radius, int percent, - int threshold) + int threshold) { ImagingSectionCookie cookie; @@ -288,6 +293,7 @@ ImagingUnsharpMask(Imaging im, Imaging imOut, float radius, int percent, int channel = 0; int channels = 0; int padding = 0; + int hasAlpha = 0; int x = 0; int y = 0; @@ -302,28 +308,28 @@ ImagingUnsharpMask(Imaging im, Imaging imOut, float radius, int percent, INT32 newPixel = 0; if (strcmp(im->mode, "RGB") == 0) { - channels = 3; - padding = 1; + channels = 3; + padding = 1; } else if (strcmp(im->mode, "RGBA") == 0) { - channels = 3; - padding = 1; + channels = 3; + padding = 1; } else if (strcmp(im->mode, "RGBX") == 0) { - channels = 3; - padding = 1; + channels = 3; + padding = 1; } else if (strcmp(im->mode, "CMYK") == 0) { - channels = 4; - padding = 0; + channels = 4; + padding = 0; } else if (strcmp(im->mode, "L") == 0) { - channels = 1; - padding = 0; + channels = 1; + padding = 0; } else - return ImagingError_ModeError(); + return ImagingError_ModeError(); /* first, do a gaussian blur on the image, putting results in imOut temporarily */ result = gblur(im, imOut, radius, channels, padding); if (!result) - return NULL; + return NULL; /* now, go through each pixel, compare "normal" pixel to blurred pixel. if the difference is more than threshold values, apply @@ -332,64 +338,67 @@ ImagingUnsharpMask(Imaging im, Imaging imOut, float radius, int percent, ImagingSectionEnter(&cookie); - for (y = 0; y < im->ysize; y++) { - if (channels == 1) { - lineIn8 = im->image8[y]; - lineOut8 = imOut->image8[y]; - } else { - lineIn = im->image32[y]; - lineOut = imOut->image32[y]; - } - for (x = 0; x < im->xsize; x++) { - newPixel = 0; - /* compare in/out pixels, apply sharpening */ - if (channels == 1) { - diff = - ((UINT8 *) & lineIn8[x])[0] - - ((UINT8 *) & lineOut8[x])[0]; - if (abs(diff) > threshold) { - /* add the diff*percent to the original pixel */ - imOut->image8[y][x] = - clip((((UINT8 *) & lineIn8[x])[0]) + - (diff * ((float) percent) / 100.0)); - } else { - /* newPixel is the same as imIn */ - imOut->image8[y][x] = ((UINT8 *) & lineIn8[x])[0]; - } - } + if (strcmp(im->mode, "RGBX") == 0 || strcmp(im->mode, "RGBA") == 0) { + hasAlpha = 1; + } - else { - for (channel = 0; channel < channels; channel++) { - diff = (int) ((((UINT8 *) & lineIn[x])[channel]) - - (((UINT8 *) & lineOut[x])[channel])); - if (abs(diff) > threshold) { - /* add the diff*percent to the original pixel - this may not work for little-endian systems, fix it! */ - newPixel = - newPixel | - clip((float) (((UINT8 *) & lineIn[x])[channel]) - + - (diff * - (((float) percent / - 100.0)))) << (channel * 8); - } else { - /* newPixel is the same as imIn - this may not work for little-endian systems, fix it! */ - newPixel = - newPixel | ((UINT8 *) & lineIn[x])[channel] << - (channel * 8); - } - } - if (strcmp(im->mode, "RGBX") == 0 - || strcmp(im->mode, "RGBA") == 0) { - /* preserve the alpha channel - this may not work for little-endian systems, fix it! */ - newPixel = - newPixel | ((UINT8 *) & lineIn[x])[channel] << 24; - } - imOut->image32[y][x] = newPixel; - } - } + for (y = 0; y < im->ysize; y++) { + if (channels == 1) { + lineIn8 = im->image8[y]; + lineOut8 = imOut->image8[y]; + } else { + lineIn = im->image32[y]; + lineOut = imOut->image32[y]; + } + for (x = 0; x < im->xsize; x++) { + newPixel = 0; + /* compare in/out pixels, apply sharpening */ + if (channels == 1) { + diff = + ((UINT8 *) & lineIn8[x])[0] - + ((UINT8 *) & lineOut8[x])[0]; + if (abs(diff) > threshold) { + /* add the diff*percent to the original pixel */ + imOut->image8[y][x] = + clip((((UINT8 *) & lineIn8[x])[0]) + + (diff * ((float) percent) / 100.0)); + } else { + /* newPixel is the same as imIn */ + imOut->image8[y][x] = ((UINT8 *) & lineIn8[x])[0]; + } + } + + else { + for (channel = 0; channel < channels; channel++) { + diff = (int) ((((UINT8 *) & lineIn[x])[channel]) - + (((UINT8 *) & lineOut[x])[channel])); + if (abs(diff) > threshold) { + /* add the diff*percent to the original pixel + this may not work for little-endian systems, fix it! */ + newPixel = + newPixel | + clip((float) (((UINT8 *) & lineIn[x])[channel]) + + + (diff * + (((float) percent / + 100.0)))) << (channel * 8); + } else { + /* newPixel is the same as imIn + this may not work for little-endian systems, fix it! */ + newPixel = + newPixel | ((UINT8 *) & lineIn[x])[channel] << + (channel * 8); + } + } + if (hasAlpha) { + /* preserve the alpha channel + this may not work for little-endian systems, fix it! */ + newPixel = + newPixel | ((UINT8 *) & lineIn[x])[channel] << 24; + } + imOut->image32[y][x] = newPixel; + } + } } ImagingSectionLeave(&cookie);