From f5d400d364659706884b4e4a708690e4d66d74f3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 13 Aug 2013 22:20:04 +0530 Subject: [PATCH] An improved cover trimming algorithm An improved cover trimming algorithm to automatically detect and remove borders and extra space from the edge of cover images. To try it use the "Trim" button in the edit metadata dialog. --- src/calibre/gui2/dialogs/metadata_bulk.ui | 11 ++- src/calibre/gui2/metadata/basic_widgets.py | 3 + src/calibre/utils/magick/__init__.py | 7 ++ src/calibre/utils/magick/magick.c | 90 ++++++++++++++++++++++ 4 files changed, 108 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index afc5e55449..741f8cae54 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -605,8 +605,13 @@ Future conversion of these books will use the default settings. + + Try to automatically detect and remove borders and extra space + from the edges of cover images. This can sometimes remove too + much, so use with care. + - &Trim cover + &Trim cover (DANGEROUS) @@ -1119,8 +1124,8 @@ not multiple and the destination field is multiple 0 0 - 934 - 256 + 203 + 58 diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index a596e43f35..6238d0c08a 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -902,6 +902,9 @@ class Cover(ImageView): # {{{ _('&Browse'), parent) self.trim_cover_button = QPushButton(QIcon(I('trim.png')), _('T&rim'), parent) + self.trim_cover_button.setToolTip(_( + 'Automatically detect and remove extra space at the cover\'s edges.\n' + 'Pressing it repeatedly can sometimes remove stubborn borders.')) self.remove_cover_button = QPushButton(QIcon(I('trash.png')), _('&Remove'), parent) diff --git a/src/calibre/utils/magick/__init__.py b/src/calibre/utils/magick/__init__.py index 99ed2d98be..0c1315eb71 100644 --- a/src/calibre/utils/magick/__init__.py +++ b/src/calibre/utils/magick/__init__.py @@ -225,6 +225,13 @@ class Image(_magick.Image): # {{{ _magick.Image.quantize(self, number_colors, colorspace, treedepth, dither, measure_error) + def trim(self, fuzz): + try: + _magick.Image.remove_border(self, fuzz) + except AttributeError: + _magick.Image.trim(self, fuzz) + + # }}} def create_canvas(width, height, bgcolor='#ffffff'): diff --git a/src/calibre/utils/magick/magick.c b/src/calibre/utils/magick/magick.c index 0a4c3bd866..e306ad9065 100644 --- a/src/calibre/utils/magick/magick.c +++ b/src/calibre/utils/magick/magick.c @@ -7,6 +7,9 @@ // Ensure that the underlying MagickWand has not been deleted #define NULL_CHECK(x) if(self->wand == NULL) {PyErr_SetString(PyExc_ValueError, "Underlying ImageMagick Wand has been destroyed"); return x; } +#define MAX(x, y) ((x > y) ? x: y) +#define ABS(x) ((x < 0) ? -x : x) +#define SQUARE(x) x*x // magick_set_exception {{{ PyObject* magick_set_exception(MagickWand *wand) { @@ -17,6 +20,15 @@ PyObject* magick_set_exception(MagickWand *wand) { desc = MagickRelinquishMemory(desc); return NULL; } + +PyObject* pw_iterator_set_exception(PixelIterator *pi) { + ExceptionType ext; + char *desc = PixelGetIteratorException(pi, &ext); + PyErr_SetString(PyExc_Exception, desc); + PixelClearIteratorException(pi); + desc = MagickRelinquishMemory(desc); + return NULL; +} // }}} // PixelWand object definition {{{ @@ -840,6 +852,80 @@ magick_Image_trim(magick_Image *self, PyObject *args) { } // }}} +// Image.remove_border {{{ + +static size_t +magick_find_border(PixelIterator *pi, double fuzz, size_t img_width, double *reds, PixelWand** (*next)(PixelIterator*, size_t*)) { + size_t band = 0, width = 0, c = 0; + double *greens = NULL, *blues = NULL, red_average, green_average, blue_average, distance, first_row[3] = {0.0, 0.0, 0.0}; + PixelWand **pixels = NULL; + + greens = reds + img_width + 1; blues = greens + img_width + 1; + + while ( (pixels = next(pi, &width)) != NULL ) { + red_average = 0; green_average = 0; blue_average = 0; + for (c = 0; c < width; c++) { + reds[c] = PixelGetRed(pixels[c]); greens[c] = PixelGetGreen(pixels[c]); blues[c] = PixelGetBlue(pixels[c]); + /* PixelGetHSL(pixels[c], reds + c, greens + c, blues + c); */ + red_average += reds[c]; green_average += greens[c]; blue_average += blues[c]; + } + red_average /= MAX(1, width); green_average /= MAX(1, width); blue_average /= MAX(1, width); + distance = 0; + for (c = 0; c < width && distance < fuzz; c++) + distance = MAX(distance, SQUARE((reds[c] - red_average)) + SQUARE((greens[c] - green_average)) + SQUARE((blues[c] - blue_average))); + if (distance > fuzz) break; // row is not homogeneous + if (band == 0) {first_row[0] = red_average; first_row[1] = blue_average; first_row[2] = green_average; } + else { + distance = SQUARE((first_row[0] - red_average)) + SQUARE((first_row[1] - green_average)) + SQUARE((first_row[2] - blue_average)); + if (distance > fuzz) break; // this row's average color is far from the previous rows average color + } + band += 1; + } + return band; +} + +static PyObject * +magick_Image_remove_border(magick_Image *self, PyObject *args) { + double fuzz, *buf = NULL; + PixelIterator *pi = NULL; + size_t width, height, iwidth, iheight; + size_t top_band = 0, bottom_band = 0, left_band = 0, right_band = 0; + + NULL_CHECK(NULL) + + if (!PyArg_ParseTuple(args, "d", &fuzz)) return NULL; + fuzz /= 255; + + height = iwidth = MagickGetImageHeight(self->wand); + width = iheight = MagickGetImageWidth(self->wand); + buf = PyMem_New(double, 3*(MAX(width, height)+1)); + pi = NewPixelIterator(self->wand); + if (buf == NULL || pi == NULL) { PyErr_NoMemory(); goto end; } + top_band = magick_find_border(pi, fuzz, width, buf, &PixelGetNextIteratorRow); + if (top_band >= height) goto end; + PixelSetLastIteratorRow(pi); + bottom_band = magick_find_border(pi, fuzz, width, buf, &PixelGetPreviousIteratorRow); + if (bottom_band >= height) goto end; + if (!MagickTransposeImage(self->wand)) { magick_set_exception(self->wand); goto end; } + pi = DestroyPixelIterator(pi); + pi = NewPixelIterator(self->wand); + if (pi == NULL) { PyErr_NoMemory(); goto end; } + left_band = magick_find_border(pi, fuzz, iwidth, buf, &PixelGetNextIteratorRow); + if (left_band >= iheight) goto end; + PixelSetLastIteratorRow(pi); + right_band = magick_find_border(pi, fuzz, iwidth, buf, &PixelGetPreviousIteratorRow); + if (right_band >= iheight) goto end; + if (!MagickTransposeImage(self->wand)) { magick_set_exception(self->wand); goto end; } + if (!MagickCropImage(self->wand, width - left_band - right_band, height - top_band - bottom_band, left_band, top_band)) { magick_set_exception(self->wand); goto end; } +end: + if (pi != NULL) pi = DestroyPixelIterator(pi); + if (buf != NULL) PyMem_Free(buf); + if (PyErr_Occurred() != NULL) return NULL; + + return Py_BuildValue("kkkk", left_band, top_band, right_band, bottom_band); +} +// }}} + // Image.thumbnail {{{ static PyObject * @@ -1229,6 +1315,10 @@ static PyMethodDef magick_Image_methods[] = { "trim(fuzz) \n\n Trim image." }, + {"remove_border", (PyCFunction)magick_Image_remove_border, METH_VARARGS, + "remove_border(fuzz) \n\n Try to detect and remove borders from the image, better than the ImageMagick trim() method. Detects rows of the same color at each image edge. Where color similarity testing is based on the fuzz factor (a number between 0 and 255). Returns the number of columns/rows removed from the left, top, right and bottom edges of the image." + }, + {"crop", (PyCFunction)magick_Image_crop, METH_VARARGS, "crop(width, height, x, y) \n\n Crop image." },