mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
parent
2822ddfd5f
commit
f5d400d364
@ -605,8 +605,13 @@ Future conversion of these books will use the default settings.</string>
|
||||
</item>
|
||||
<item>
|
||||
<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">
|
||||
<string>&Trim cover</string>
|
||||
<string>&Trim cover (DANGEROUS)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -1119,8 +1124,8 @@ not multiple and the destination field is multiple</string>
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>934</width>
|
||||
<height>256</height>
|
||||
<width>203</width>
|
||||
<height>58</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="testgrid">
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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'):
|
||||
|
@ -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."
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user