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.
This commit is contained in:
Kovid Goyal 2013-08-13 22:20:04 +05:30
parent 2822ddfd5f
commit f5d400d364
4 changed files with 108 additions and 3 deletions

View File

@ -605,8 +605,13 @@ Future conversion of these books will use the default settings.</string>
</item> </item>
<item> <item>
<widget class="QRadioButton" name="cover_trim"> <widget class="QRadioButton" name="cover_trim">
<property name="toolTip">
<string>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.</string>
</property>
<property name="text"> <property name="text">
<string>&amp;Trim cover</string> <string>&amp;Trim cover (DANGEROUS)</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -1119,8 +1124,8 @@ not multiple and the destination field is multiple</string>
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>934</width> <width>203</width>
<height>256</height> <height>58</height>
</rect> </rect>
</property> </property>
<layout class="QGridLayout" name="testgrid"> <layout class="QGridLayout" name="testgrid">

View File

@ -902,6 +902,9 @@ class Cover(ImageView): # {{{
_('&Browse'), parent) _('&Browse'), parent)
self.trim_cover_button = QPushButton(QIcon(I('trim.png')), self.trim_cover_button = QPushButton(QIcon(I('trim.png')),
_('T&rim'), parent) _('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')), self.remove_cover_button = QPushButton(QIcon(I('trash.png')),
_('&Remove'), parent) _('&Remove'), parent)

View File

@ -225,6 +225,13 @@ class Image(_magick.Image): # {{{
_magick.Image.quantize(self, number_colors, colorspace, treedepth, dither, _magick.Image.quantize(self, number_colors, colorspace, treedepth, dither,
measure_error) 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'): def create_canvas(width, height, bgcolor='#ffffff'):

View File

@ -7,6 +7,9 @@
// Ensure that the underlying MagickWand has not been deleted // 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 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 {{{ // magick_set_exception {{{
PyObject* magick_set_exception(MagickWand *wand) { PyObject* magick_set_exception(MagickWand *wand) {
@ -17,6 +20,15 @@ PyObject* magick_set_exception(MagickWand *wand) {
desc = MagickRelinquishMemory(desc); desc = MagickRelinquishMemory(desc);
return NULL; 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 {{{ // 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 {{{ // Image.thumbnail {{{
static PyObject * static PyObject *
@ -1229,6 +1315,10 @@ static PyMethodDef magick_Image_methods[] = {
"trim(fuzz) \n\n Trim image." "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", (PyCFunction)magick_Image_crop, METH_VARARGS,
"crop(width, height, x, y) \n\n Crop image." "crop(width, height, x, y) \n\n Crop image."
}, },