diff --git a/README.md b/README.md index 0a3d139..71f51e0 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,8 @@ PROCESSING: Set cropping power [Default=1.0] --cm CROPPINGM, --croppingminimum CROPPINGM Set cropping minimum area ratio [Default=0.0] + --ipc INTERPANELCROP, --interpanelcrop INTERPANELCROP + Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0] --blackborders Disable autodetection and force black borders --whiteborders Disable autodetection and force white borders --forcecolor Don't convert images to grayscale diff --git a/gui/KCC.ui b/gui/KCC.ui index 691acaf..824fd9d 100644 --- a/gui/KCC.ui +++ b/gui/KCC.ui @@ -237,6 +237,19 @@ + + + + <html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Disabled<br/></span>Disabled</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - Horizontal<br/></span>Crop empty horizontal lines.</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Both<br/></span>Crop empty horizontal and vertical lines.</p></body></html> + + + Inter-panel crop + + + true + + + diff --git a/kindlecomicconverter/KCC_gui.py b/kindlecomicconverter/KCC_gui.py index 28e4d00..da835a9 100644 --- a/kindlecomicconverter/KCC_gui.py +++ b/kindlecomicconverter/KCC_gui.py @@ -244,6 +244,7 @@ class WorkerThread(QThread): options.cropping = GUI.croppingBox.checkState().value if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked: options.croppingp = float(GUI.croppingPowerValue) + options.interpanelcrop = GUI.interPanelCropBox.checkState().value if GUI.borderBox.checkState() == Qt.CheckState.PartiallyChecked: options.white_borders = True elif GUI.borderBox.checkState() == Qt.CheckState.Checked: @@ -789,6 +790,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow): 'gammaBox': GUI.gammaBox.checkState().value, 'croppingBox': GUI.croppingBox.checkState().value, 'croppingPowerSlider': float(self.croppingPowerValue) * 100, + 'interPanelCropBox': GUI.interPanelCropBox.checkState().value, 'upscaleBox': GUI.upscaleBox.checkState().value, 'borderBox': GUI.borderBox.checkState().value, 'webtoonBox': GUI.webtoonBox.checkState().value, diff --git a/kindlecomicconverter/KCC_ui.py b/kindlecomicconverter/KCC_ui.py index 4d2c9af..7a9285b 100644 --- a/kindlecomicconverter/KCC_ui.py +++ b/kindlecomicconverter/KCC_ui.py @@ -138,6 +138,12 @@ class Ui_mainWindow(object): self.gridLayout_2.addWidget(self.authorEdit, 0, 0, 1, 1) + self.interPanelCropBox = QCheckBox(self.optionWidget) + self.interPanelCropBox.setObjectName(u"interPanelCropBox") + self.interPanelCropBox.setTristate(True) + + self.gridLayout_2.addWidget(self.interPanelCropBox, 6, 2, 1, 1) + self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2) @@ -444,6 +450,10 @@ class Ui_mainWindow(object): self.authorEdit.setToolTip(QCoreApplication.translate("mainWindow", u"Default Author is KCC", None)) #endif // QT_CONFIG(tooltip) self.authorEdit.setPlaceholderText(QCoreApplication.translate("mainWindow", u"Default Author", None)) +#if QT_CONFIG(tooltip) + self.interPanelCropBox.setToolTip(QCoreApplication.translate("mainWindow", u"

Unchecked - Disabled
Disabled

Indeterminate - Horizontal
Crop empty horizontal lines.

Checked - Both
Crop empty horizontal and vertical lines.

", None)) +#endif // QT_CONFIG(tooltip) + self.interPanelCropBox.setText(QCoreApplication.translate("mainWindow", u"Inter-panel crop", None)) self.gammaLabel.setText(QCoreApplication.translate("mainWindow", u"Gamma: Auto", None)) self.croppingPowerLabel.setText(QCoreApplication.translate("mainWindow", u"Cropping power:", None)) #if QT_CONFIG(tooltip) diff --git a/kindlecomicconverter/comic2ebook.py b/kindlecomicconverter/comic2ebook.py index 81cf130..fa2403e 100755 --- a/kindlecomicconverter/comic2ebook.py +++ b/kindlecomicconverter/comic2ebook.py @@ -612,6 +612,8 @@ def imgFileProcessing(work): img.cropPageNumber(opt.croppingp, opt.croppingm) if opt.cropping > 0 and not opt.webtoon: img.cropMargin(opt.croppingp, opt.croppingm) + if opt.interpanelcrop > 0: + img.cropInterPanelEmptySections("horizontal" if opt.interpanelcrop == 1 else "both") img.autocontrastImage() img.resizeImage() if opt.forcepng and not opt.forcecolor: @@ -1013,6 +1015,8 @@ def makeParser(): help="Set cropping power [Default=1.0]") processing_options.add_argument("--cm", "--croppingminimum", type=float, dest="croppingm", default="0.0", help="Set cropping minimum area ratio [Default=0.0]") + processing_options.add_argument("--ipc", "--interpanelcrop", type=int, dest="interpanelcrop", default="0", + help="Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0]") processing_options.add_argument("--blackborders", action="store_true", dest="black_borders", default=False, help="Disable autodetection and force black borders") processing_options.add_argument("--whiteborders", action="store_true", dest="white_borders", default=False, diff --git a/kindlecomicconverter/common_crop.py b/kindlecomicconverter/common_crop.py new file mode 100644 index 0000000..5f831db --- /dev/null +++ b/kindlecomicconverter/common_crop.py @@ -0,0 +1,28 @@ +def threshold_from_power(power): + return 240-(power*64) + + +''' +Groups close values together +''' +def group_close_values(vals, max_dist_tolerated): + groups = [] + + group_start = -1 + group_end = 0 + for i in range(len(vals)): + dist = vals[i] - group_end + if group_start == -1: + group_start = vals[i] + group_end = vals[i] + elif dist <= max_dist_tolerated: + group_end = vals[i] + else: + groups.append((group_start, group_end)) + group_start = -1 + group_end = -1 + + if group_start != -1: + groups.append((group_start, group_end)) + + return groups diff --git a/kindlecomicconverter/image.py b/kindlecomicconverter/image.py index 4370303..171a0ea 100755 --- a/kindlecomicconverter/image.py +++ b/kindlecomicconverter/image.py @@ -24,6 +24,7 @@ import mozjpeg_lossless_optimization from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter from .shared import md5Checksum from .page_number_crop_alg import get_bbox_crop_margin_page_number, get_bbox_crop_margin +from .inter_panel_crop_alg import crop_empty_inter_panel AUTO_CROP_THRESHOLD = 0.015 @@ -390,6 +391,8 @@ class ComicPage: if bbox: self.maybeCrop(bbox, minimum) + def cropInterPanelEmptySections(self, direction): + self.image = crop_empty_inter_panel(self.image, direction, background_color=self.fill) class Cover: def __init__(self, source, target, opt, tomeid): diff --git a/kindlecomicconverter/inter_panel_crop_alg.py b/kindlecomicconverter/inter_panel_crop_alg.py new file mode 100644 index 0000000..cf8211e --- /dev/null +++ b/kindlecomicconverter/inter_panel_crop_alg.py @@ -0,0 +1,76 @@ +from PIL import Image, ImageFilter, ImageOps +import numpy as np +from typing import Literal +from .common_crop import threshold_from_power, group_close_values + + +''' +Crops inter-panel empty spaces (ignores empty spaces near borders - for that use crop margins). + + Parameters: + img (PIL image): A PIL image. + direction (horizontal or vertical or both): To crop rows (horizontal), cols (vertical) or both. + keep (float): Distance to keep between panels after cropping (in percentage relative to the original distance). + background_color (string): 'white' for white background, anything else for black. + Returns: + img (PIL image): A PIL image after cropping empty sections. +''' +def crop_empty_inter_panel(img, direction: Literal["horizontal", "vertical", "both"], keep=0.04, background_color='white'): + img_temp = img + + if img.mode != 'L': + img_temp = ImageOps.grayscale(img) + + if background_color != 'white': + img_temp = ImageOps.invert(img) + + img_mat = np.array(img) + + power = 1 + img_temp = ImageOps.autocontrast(img_temp, 1).filter(ImageFilter.BoxBlur(1)) + img_temp = img_temp.point(lambda p: 255 if p <= threshold_from_power(power) else 0) + + if direction in ["horizontal", "both"]: + rows_idx_to_remove = empty_sections(img_temp, keep, horizontal=True) + img_mat = np.delete(img_mat, rows_idx_to_remove, 0) + + if direction in ["vertical", "both"]: + cols_idx_to_remove = empty_sections(img_temp, keep, horizontal=False) + img_mat = np.delete(img_mat, cols_idx_to_remove, 1) + + return Image.fromarray(img_mat) + + +''' +Finds empty sections (excluding near borders). + + Parameters: + img (PIL image): A PIL image. + keep (float): Distance to keep between panels after cropping (in percentage relative to the original distance). + horizontal (boolean): True to find empty rows, False to find empty columns. + Returns: + Itertable (list or NumPy array): indices of rows or columns to remove. +''' +def empty_sections(img, keep, horizontal=True): + axis = 1 if horizontal else 0 + + img_mat = np.array(img) + img_mat_max = np.max(img_mat, axis=axis) + img_mat_empty_idx = np.where(img_mat_max == 0)[0] + + empty_sections = group_close_values(img_mat_empty_idx, 1) + sections_to_remove = [] + for section in empty_sections: + if section[1] < img.size[1] * 0.99 and section[0] > img.size[1] * 0.01: # if not near borders + sections_to_remove.append(section) + + if len(sections_to_remove) != 0: + sections_to_remove_after_keep = [(int(x1+(keep/2)*(x2-x1)), int(x2-(keep/2)*(x2-x1))) for x1,x2 in sections_to_remove] + idx_to_remove = np.concatenate([np.arange(x1, x2) for x1,x2 in sections_to_remove_after_keep]) + + return idx_to_remove + + return [] + + + diff --git a/kindlecomicconverter/page_number_crop_alg.py b/kindlecomicconverter/page_number_crop_alg.py index fd95100..21254f1 100644 --- a/kindlecomicconverter/page_number_crop_alg.py +++ b/kindlecomicconverter/page_number_crop_alg.py @@ -1,5 +1,7 @@ from PIL import ImageOps, ImageFilter import numpy as np +from .common_crop import threshold_from_power, group_close_values + ''' Some assupmptions on the page number sizes @@ -51,12 +53,11 @@ def get_bbox_crop_margin_page_number(img, power=1, background_color='white'): threshold = threshold_from_power(power) bw_img = img.point(lambda p: 255 if p <= threshold else 0) bw_bbox = bw_img.getbbox() - if not bw_bbox: # bbox cannot be found in case that the entire resulted image is black. return None left, top_y_pos, right, bot_y_pos = bw_bbox - + ''' We inspect the lower bottom part of the image where we suspect might be a page number. We assume that page number consist of 1 to 3 digits and the total min and max size of the number @@ -73,7 +74,7 @@ def get_bbox_crop_margin_page_number(img, power=1, background_color='white'): img_part_mat = np.array(img_part) window_groups = [] for i in range(img_part.size[1]): - row_groups = [(g[0], g[1], i, i) for g in group_pixels(img_part_mat[i], img.size[0]*max_dist_size[0], threshold)] + row_groups = [(g[0], g[1], i, i) for g in group_close_values(np.where(img_part_mat[i] <= threshold)[0], img.size[0]*max_dist_size[0])] window_groups.extend(row_groups) window_groups = np.array(window_groups) @@ -109,7 +110,6 @@ def get_bbox_crop_margin_page_number(img, power=1, background_color='white'): cropped_bbox = (0, 0, img.size[0], bot_y_pos-(window_h-boxes_in_same_y_range[0][2]+1)) cropped_bbox = bw_img.crop(cropped_bbox).getbbox() - return cropped_bbox @@ -145,33 +145,6 @@ def get_bbox_crop_margin(img, power=1, background_color='white'): return bw_img.getbbox() -''' -Groups close pixels together (x axis) -''' -def group_pixels(row, max_dist_tolerated, threshold): - groups = [] - idx = np.where(row <= threshold)[0] - - group_start = -1 - group_end = 0 - for i in range(len(idx)): - dist = idx[i] - group_end - if group_start == -1: - group_start = idx[i] - group_end = idx[i] - elif dist <= max_dist_tolerated: - group_end = idx[i] - else: - groups.append((group_start, group_end)) - group_start = -1 - group_end = -1 - - if group_start != -1: - groups.append((group_start, group_end)) - - return groups - - def box_intersect(box1, box2, max_dist): return not (box2[0]-max_dist[0] > box1[1] or box2[1]+max_dist[0] < box1[0] @@ -209,7 +182,3 @@ def merge_boxes(boxes, max_dist_tolerated): else: j += 1 return boxes - - -def threshold_from_power(power): - return 240-(power*64) \ No newline at end of file