mirror of
https://github.com/ciromattia/kcc
synced 2025-12-12 17:26:23 +00:00
Inter-panel cropping method. (#810)
* Inter-panel cropping method. * 1. Save interpanelcrop option. 2. Update readme with the the new interpanelcrop argument. 3. Add a tooltip to the inter-panel crop box.
This commit is contained in:
committed by
GitHub
parent
4fb993b38b
commit
2f703ef92c
@@ -163,6 +163,8 @@ PROCESSING:
|
|||||||
Set cropping power [Default=1.0]
|
Set cropping power [Default=1.0]
|
||||||
--cm CROPPINGM, --croppingminimum CROPPINGM
|
--cm CROPPINGM, --croppingminimum CROPPINGM
|
||||||
Set cropping minimum area ratio [Default=0.0]
|
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
|
--blackborders Disable autodetection and force black borders
|
||||||
--whiteborders Disable autodetection and force white borders
|
--whiteborders Disable autodetection and force white borders
|
||||||
--forcecolor Don't convert images to grayscale
|
--forcecolor Don't convert images to grayscale
|
||||||
|
|||||||
13
gui/KCC.ui
13
gui/KCC.ui
@@ -237,6 +237,19 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="6" column="2">
|
||||||
|
<widget class="QCheckBox" name="interPanelCropBox">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><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></string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Inter-panel crop</string>
|
||||||
|
</property>
|
||||||
|
<property name="tristate">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ class WorkerThread(QThread):
|
|||||||
options.cropping = GUI.croppingBox.checkState().value
|
options.cropping = GUI.croppingBox.checkState().value
|
||||||
if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked:
|
if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked:
|
||||||
options.croppingp = float(GUI.croppingPowerValue)
|
options.croppingp = float(GUI.croppingPowerValue)
|
||||||
|
options.interpanelcrop = GUI.interPanelCropBox.checkState().value
|
||||||
if GUI.borderBox.checkState() == Qt.CheckState.PartiallyChecked:
|
if GUI.borderBox.checkState() == Qt.CheckState.PartiallyChecked:
|
||||||
options.white_borders = True
|
options.white_borders = True
|
||||||
elif GUI.borderBox.checkState() == Qt.CheckState.Checked:
|
elif GUI.borderBox.checkState() == Qt.CheckState.Checked:
|
||||||
@@ -789,6 +790,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
'gammaBox': GUI.gammaBox.checkState().value,
|
'gammaBox': GUI.gammaBox.checkState().value,
|
||||||
'croppingBox': GUI.croppingBox.checkState().value,
|
'croppingBox': GUI.croppingBox.checkState().value,
|
||||||
'croppingPowerSlider': float(self.croppingPowerValue) * 100,
|
'croppingPowerSlider': float(self.croppingPowerValue) * 100,
|
||||||
|
'interPanelCropBox': GUI.interPanelCropBox.checkState().value,
|
||||||
'upscaleBox': GUI.upscaleBox.checkState().value,
|
'upscaleBox': GUI.upscaleBox.checkState().value,
|
||||||
'borderBox': GUI.borderBox.checkState().value,
|
'borderBox': GUI.borderBox.checkState().value,
|
||||||
'webtoonBox': GUI.webtoonBox.checkState().value,
|
'webtoonBox': GUI.webtoonBox.checkState().value,
|
||||||
|
|||||||
@@ -138,6 +138,12 @@ class Ui_mainWindow(object):
|
|||||||
|
|
||||||
self.gridLayout_2.addWidget(self.authorEdit, 0, 0, 1, 1)
|
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)
|
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))
|
self.authorEdit.setToolTip(QCoreApplication.translate("mainWindow", u"Default Author is KCC", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.authorEdit.setPlaceholderText(QCoreApplication.translate("mainWindow", u"Default Author", None))
|
self.authorEdit.setPlaceholderText(QCoreApplication.translate("mainWindow", u"Default Author", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.interPanelCropBox.setToolTip(QCoreApplication.translate("mainWindow", u"<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>", 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.gammaLabel.setText(QCoreApplication.translate("mainWindow", u"Gamma: Auto", None))
|
||||||
self.croppingPowerLabel.setText(QCoreApplication.translate("mainWindow", u"Cropping power:", None))
|
self.croppingPowerLabel.setText(QCoreApplication.translate("mainWindow", u"Cropping power:", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
|
|||||||
@@ -612,6 +612,8 @@ def imgFileProcessing(work):
|
|||||||
img.cropPageNumber(opt.croppingp, opt.croppingm)
|
img.cropPageNumber(opt.croppingp, opt.croppingm)
|
||||||
if opt.cropping > 0 and not opt.webtoon:
|
if opt.cropping > 0 and not opt.webtoon:
|
||||||
img.cropMargin(opt.croppingp, opt.croppingm)
|
img.cropMargin(opt.croppingp, opt.croppingm)
|
||||||
|
if opt.interpanelcrop > 0:
|
||||||
|
img.cropInterPanelEmptySections("horizontal" if opt.interpanelcrop == 1 else "both")
|
||||||
img.autocontrastImage()
|
img.autocontrastImage()
|
||||||
img.resizeImage()
|
img.resizeImage()
|
||||||
if opt.forcepng and not opt.forcecolor:
|
if opt.forcepng and not opt.forcecolor:
|
||||||
@@ -1013,6 +1015,8 @@ def makeParser():
|
|||||||
help="Set cropping power [Default=1.0]")
|
help="Set cropping power [Default=1.0]")
|
||||||
processing_options.add_argument("--cm", "--croppingminimum", type=float, dest="croppingm", default="0.0",
|
processing_options.add_argument("--cm", "--croppingminimum", type=float, dest="croppingm", default="0.0",
|
||||||
help="Set cropping minimum area ratio [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,
|
processing_options.add_argument("--blackborders", action="store_true", dest="black_borders", default=False,
|
||||||
help="Disable autodetection and force black borders")
|
help="Disable autodetection and force black borders")
|
||||||
processing_options.add_argument("--whiteborders", action="store_true", dest="white_borders", default=False,
|
processing_options.add_argument("--whiteborders", action="store_true", dest="white_borders", default=False,
|
||||||
|
|||||||
28
kindlecomicconverter/common_crop.py
Normal file
28
kindlecomicconverter/common_crop.py
Normal file
@@ -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
|
||||||
@@ -24,6 +24,7 @@ import mozjpeg_lossless_optimization
|
|||||||
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter
|
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter
|
||||||
from .shared import md5Checksum
|
from .shared import md5Checksum
|
||||||
from .page_number_crop_alg import get_bbox_crop_margin_page_number, get_bbox_crop_margin
|
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
|
AUTO_CROP_THRESHOLD = 0.015
|
||||||
|
|
||||||
@@ -390,6 +391,8 @@ class ComicPage:
|
|||||||
if bbox:
|
if bbox:
|
||||||
self.maybeCrop(bbox, minimum)
|
self.maybeCrop(bbox, minimum)
|
||||||
|
|
||||||
|
def cropInterPanelEmptySections(self, direction):
|
||||||
|
self.image = crop_empty_inter_panel(self.image, direction, background_color=self.fill)
|
||||||
|
|
||||||
class Cover:
|
class Cover:
|
||||||
def __init__(self, source, target, opt, tomeid):
|
def __init__(self, source, target, opt, tomeid):
|
||||||
|
|||||||
76
kindlecomicconverter/inter_panel_crop_alg.py
Normal file
76
kindlecomicconverter/inter_panel_crop_alg.py
Normal file
@@ -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 []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
from PIL import ImageOps, ImageFilter
|
from PIL import ImageOps, ImageFilter
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from .common_crop import threshold_from_power, group_close_values
|
||||||
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Some assupmptions on the page number sizes
|
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)
|
threshold = threshold_from_power(power)
|
||||||
bw_img = img.point(lambda p: 255 if p <= threshold else 0)
|
bw_img = img.point(lambda p: 255 if p <= threshold else 0)
|
||||||
bw_bbox = bw_img.getbbox()
|
bw_bbox = bw_img.getbbox()
|
||||||
|
|
||||||
if not bw_bbox: # bbox cannot be found in case that the entire resulted image is black.
|
if not bw_bbox: # bbox cannot be found in case that the entire resulted image is black.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
left, top_y_pos, right, bot_y_pos = bw_bbox
|
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 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
|
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)
|
img_part_mat = np.array(img_part)
|
||||||
window_groups = []
|
window_groups = []
|
||||||
for i in range(img_part.size[1]):
|
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.extend(row_groups)
|
||||||
|
|
||||||
window_groups = np.array(window_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 = (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()
|
cropped_bbox = bw_img.crop(cropped_bbox).getbbox()
|
||||||
|
|
||||||
return cropped_bbox
|
return cropped_bbox
|
||||||
|
|
||||||
|
|
||||||
@@ -145,33 +145,6 @@ def get_bbox_crop_margin(img, power=1, background_color='white'):
|
|||||||
return bw_img.getbbox()
|
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):
|
def box_intersect(box1, box2, max_dist):
|
||||||
return not (box2[0]-max_dist[0] > box1[1]
|
return not (box2[0]-max_dist[0] > box1[1]
|
||||||
or box2[1]+max_dist[0] < box1[0]
|
or box2[1]+max_dist[0] < box1[0]
|
||||||
@@ -209,7 +182,3 @@ def merge_boxes(boxes, max_dist_tolerated):
|
|||||||
else:
|
else:
|
||||||
j += 1
|
j += 1
|
||||||
return boxes
|
return boxes
|
||||||
|
|
||||||
|
|
||||||
def threshold_from_power(power):
|
|
||||||
return 240-(power*64)
|
|
||||||
Reference in New Issue
Block a user