diff --git a/Tests/test_image_entropy.py b/Tests/test_image_entropy.py new file mode 100644 index 000000000..3cf8561c4 --- /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 f4383b120..1ea7aa614 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 092cc8040..ea12bb7f3 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1361,6 +1361,7 @@ class Image(object): 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,36 @@ class Image(object): return self.im.histogram(extrema) return self.im.histogram() + def entropy(self, mask=None, extrema=None): + """ + Returns the histogram entropy. The histogram is returned as + a list of pixel counts, one for each pixel value in the source + image. If the image has more than one band, the histograms for + all bands are concatenated (for example, the histogram for an + "RGB" image contains 768 values). + + A bilevel image (mode "1") is treated as a greyscale ("L") image + by this method. + + If a mask is provided, the method returns a 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 measuring 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 ed4702d55..c209f4dc3 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1175,59 +1175,62 @@ _getpixel(ImagingObject* self, PyObject* args) return getpixel(self->image, self->access, x, y); } +#define HISTOGRAM_METHOD_PROLOGUE(HISTO) \ + ImagingHistogram HISTO; \ + 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; \ + break; \ + case IMAGING_TYPE_INT32: \ + if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) \ + return NULL; \ + extrema.i[0] = i0; \ + extrema.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; \ + break; \ + default: \ + ep = NULL; \ + break; \ + } \ + } else \ + ep = NULL; \ + \ + HISTO = ImagingGetHistogram(self->image, (maskp) ? maskp->image : NULL, ep); \ + \ + if (!HISTO) \ + return NULL; + static PyObject* _histogram(ImagingObject* self, PyObject* args) { - 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; - break; - case IMAGING_TYPE_INT32: - if (!PyArg_ParseTuple(extremap, "ii", &i0, &i1)) - return NULL; - extrema.i[0] = i0; - extrema.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; - break; - default: - ep = NULL; - break; - } - } else - ep = NULL; - - h = ImagingGetHistogram(self->image, (maskp) ? maskp->image : NULL, ep); - - if (!h) - return NULL; + HISTOGRAM_METHOD_PROLOGUE(h); /* Build an integer list containing the histogram */ list = PyList_New(h->bands * 256); @@ -1242,11 +1245,49 @@ _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) +{ + PyObject* entropy; + int idx, length; + long sum; + double fentropy, fsum, p; + HISTOGRAM_METHOD_PROLOGUE(h); + + /* 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 * log2(p)) : 0.0; + } + + /* Finally, allocate a PyObject* for return */ + entropy = PyFloat_FromDouble(-fentropy); + + /* Destroy the histogram structure */ + ImagingHistogramDelete(h); + + return entropy; +} + +#undef HISTOGRAM_METHOD_PROLOGUE + #ifdef WITH_MODEFILTER static PyObject* _modefilter(ImagingObject* self, PyObject* args) @@ -3191,6 +3232,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