diff --git a/Tests/test_image_entropy.py b/Tests/test_image_entropy.py new file mode 100644 index 00000000000..3cf8561c40b --- /dev/null +++ b/Tests/test_image_entropy.py @@ -0,0 +1,23 @@ +from helper import unittest, PillowTestCase, hopper + + +class TestImageEntropy(PillowTestCase): + + def test_entropy(self): + + def entropy(mode): + return hopper(mode).entropy() + + self.assertAlmostEqual(entropy("1"), 0.9138803254693582) + self.assertAlmostEqual(entropy("L"), 7.06650513081286) + self.assertAlmostEqual(entropy("I"), 7.06650513081286) + self.assertAlmostEqual(entropy("F"), 7.06650513081286) + self.assertAlmostEqual(entropy("P"), 5.0530452472519745) + self.assertAlmostEqual(entropy("RGB"), 8.821286587714319) + self.assertAlmostEqual(entropy("RGBA"), 7.42724306524488) + self.assertAlmostEqual(entropy("CMYK"), 7.4272430652448795) + self.assertAlmostEqual(entropy("YCbCr"), 7.698360534903628) + + +if __name__ == '__main__': + unittest.main() diff --git a/selftest.py b/selftest.py index f4383b1206a..1ea7aa61416 100755 --- a/selftest.py +++ b/selftest.py @@ -90,6 +90,8 @@ def testimage(): 2 >>> len(im.histogram()) 768 + >>> '%.7f' % im.entropy() + '8.8212866' >>> _info(im.point(list(range(256))*3)) (None, 'RGB', (128, 128)) >>> _info(im.resize((64, 64))) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a67d3288f98..33daf961391 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1361,6 +1361,7 @@ def histogram(self, mask=None, extrema=None): bi-level image (mode "1") or a greyscale image ("L"). :param mask: An optional mask. + :param extrema: An optional tuple of manually-specified extrema. :returns: A list containing pixel counts. """ self.load() @@ -1373,6 +1374,32 @@ def histogram(self, mask=None, extrema=None): return self.im.histogram(extrema) return self.im.histogram() + def entropy(self, mask=None, extrema=None): + """ + Calculates and returns the entropy for the image. + + A bilevel image (mode "1") is treated as a greyscale ("L") + image by this method. + + If a mask is provided, the method employs the histogram for + those parts of the image where the mask image is non-zero. + The mask image must have the same size as the image, and be + either a bi-level image (mode "1") or a greyscale image ("L"). + + :param mask: An optional mask. + :param extrema: An optional tuple of manually-specified extrema. + :returns: A float value representing the image entropy + """ + self.load() + if mask: + mask.load() + return self.im.entropy((0, 0), mask.im) + if self.mode in ("I", "F"): + if extrema is None: + extrema = self.getextrema() + return self.im.entropy(extrema) + return self.im.entropy() + def offset(self, xoffset, yoffset=None): raise NotImplementedError("offset() has been removed. " "Please call ImageChops.offset() instead.") diff --git a/src/_imaging.c b/src/_imaging.c index ed4702d5565..6708d5cc491 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -85,6 +85,9 @@ #include "py3.h" +#define _USE_MATH_DEFINES +#include + /* Configuration stuff. Feel free to undef things you don't need. */ #define WITH_IMAGECHOPS /* ImageChops support */ #define WITH_IMAGEDRAW /* ImageDraw support */ @@ -1175,59 +1178,69 @@ _getpixel(ImagingObject* self, PyObject* args) return getpixel(self->image, self->access, x, y); } -static PyObject* -_histogram(ImagingObject* self, PyObject* args) +union hist_extrema { + UINT8 u[2]; + INT32 i[2]; + FLOAT32 f[2]; +}; + +static union hist_extrema* +parse_histogram_extremap(ImagingObject* self, PyObject* extremap, + union hist_extrema* ep) { - ImagingHistogram h; - PyObject* list; - int i; - union { - UINT8 u[2]; - INT32 i[2]; - FLOAT32 f[2]; - } extrema; - void* ep; int i0, i1; double f0, f1; - PyObject* extremap = NULL; - ImagingObject* maskp = NULL; - if (!PyArg_ParseTuple(args, "|OO!", &extremap, &Imaging_Type, &maskp)) - return NULL; - if (extremap) { - ep = &extrema; switch (self->image->type) { case IMAGING_TYPE_UINT8: if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) return NULL; /* FIXME: clip */ - extrema.u[0] = i0; - extrema.u[1] = i1; + ep->u[0] = i0; + ep->u[1] = i1; break; case IMAGING_TYPE_INT32: if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) return NULL; - extrema.i[0] = i0; - extrema.i[1] = i1; + ep->i[0] = i0; + ep->i[1] = i1; break; case IMAGING_TYPE_FLOAT32: if (!PyArg_ParseTuple(extremap, "dd", &f0, &f1)) return NULL; - extrema.f[0] = (FLOAT32) f0; - extrema.f[1] = (FLOAT32) f1; + ep->f[0] = (FLOAT32) f0; + ep->f[1] = (FLOAT32) f1; break; default: - ep = NULL; - break; + return NULL; } - } else - ep = NULL; + } else { + return NULL; + } + return ep; +} + +static PyObject* +_histogram(ImagingObject* self, PyObject* args) +{ + ImagingHistogram h; + PyObject* list; + int i; + union hist_extrema extrema; + union hist_extrema* ep; + + PyObject* extremap = NULL; + ImagingObject* maskp = NULL; + if (!PyArg_ParseTuple(args, "|OO!", &extremap, &Imaging_Type, &maskp)) + return NULL; + /* Using a var to avoid allocations. */ + ep = parse_histogram_extremap(self, extremap, &extrema); h = ImagingGetHistogram(self->image, (maskp) ? maskp->image : NULL, ep); if (!h) - return NULL; + return NULL; /* Build an integer list containing the histogram */ list = PyList_New(h->bands * 256); @@ -1242,11 +1255,61 @@ _histogram(ImagingObject* self, PyObject* args) PyList_SetItem(list, i, item); } + /* Destroy the histogram structure */ ImagingHistogramDelete(h); return list; } +static PyObject* +_entropy(ImagingObject* self, PyObject* args) +{ + ImagingHistogram h; + PyObject* entropy; + int idx, length; + long sum; + double fentropy, fsum, p; + union hist_extrema extrema; + union hist_extrema* ep; + + PyObject* extremap = NULL; + ImagingObject* maskp = NULL; + if (!PyArg_ParseTuple(args, "|OO!", &extremap, &Imaging_Type, &maskp)) + return NULL; + + /* Using a local var to avoid allocations. */ + ep = parse_histogram_extremap(self, extremap, &extrema); + h = ImagingGetHistogram(self->image, (maskp) ? maskp->image : NULL, ep); + + if (!h) + return NULL; + + /* Calculate the histogram entropy */ + /* First, sum the histogram data */ + length = h->bands * 256; + sum = 0; + for (idx = 0; idx < length; idx++) { + sum += h->histogram[idx]; + } + + /* Next, normalize the histogram data, */ + /* using the histogram sum value */ + fsum = (double)sum; + fentropy = 0.0; + for (idx = 0; idx < length; idx++) { + p = (double)h->histogram[idx] / fsum; + fentropy += p != 0.0 ? (p * log(p) * M_LOG2E) : 0.0; + } + + /* Finally, allocate a PyObject* for return */ + entropy = PyFloat_FromDouble(-fentropy); + + /* Destroy the histogram structure */ + ImagingHistogramDelete(h); + + return entropy; +} + #ifdef WITH_MODEFILTER static PyObject* _modefilter(ImagingObject* self, PyObject* args) @@ -3191,6 +3254,7 @@ static struct PyMethodDef methods[] = { {"expand", (PyCFunction)_expand_image, 1}, {"filter", (PyCFunction)_filter, 1}, {"histogram", (PyCFunction)_histogram, 1}, + {"entropy", (PyCFunction)_entropy, 1}, #ifdef WITH_MODEFILTER {"modefilter", (PyCFunction)_modefilter, 1}, #endif