feat: Add RTL reading order and robust panel sorting
Browse filesImplement a right-to-left (RTL) reading order option for manga.
- app.py +12 -2
- image_processing/panel.py +87 -2
app.py
CHANGED
@@ -30,6 +30,7 @@ def process_images(
|
|
30 |
input_files,
|
31 |
method,
|
32 |
separate_folders,
|
|
|
33 |
# Traditional method params
|
34 |
merge_mode,
|
35 |
split_joint,
|
@@ -69,12 +70,14 @@ def process_images(
|
|
69 |
split_joint_panels=split_joint,
|
70 |
fallback=fallback,
|
71 |
mode=output_mode,
|
72 |
-
merge=merge_mode
|
|
|
73 |
)
|
74 |
elif method == "AI":
|
75 |
panel_blocks = generate_panel_blocks_by_ai(
|
76 |
image=image,
|
77 |
-
merge=merge_mode
|
|
|
78 |
)
|
79 |
else:
|
80 |
# Should not happen with Radio button selection
|
@@ -168,6 +171,12 @@ def main():
|
|
168 |
info="If unchecked, all panels will be in the root of the ZIP, with filenames prefixed by the original image name."
|
169 |
)
|
170 |
|
|
|
|
|
|
|
|
|
|
|
|
|
171 |
# --- Shared Parameters ---
|
172 |
gr.Markdown("### Shared Parameters")
|
173 |
merge_mode = gr.Dropdown(
|
@@ -219,6 +228,7 @@ def main():
|
|
219 |
input_files,
|
220 |
method,
|
221 |
separate_folders,
|
|
|
222 |
merge_mode,
|
223 |
split_joint,
|
224 |
fallback,
|
|
|
30 |
input_files,
|
31 |
method,
|
32 |
separate_folders,
|
33 |
+
rtl_order,
|
34 |
# Traditional method params
|
35 |
merge_mode,
|
36 |
split_joint,
|
|
|
70 |
split_joint_panels=split_joint,
|
71 |
fallback=fallback,
|
72 |
mode=output_mode,
|
73 |
+
merge=merge_mode,
|
74 |
+
rtl_order=rtl_order
|
75 |
)
|
76 |
elif method == "AI":
|
77 |
panel_blocks = generate_panel_blocks_by_ai(
|
78 |
image=image,
|
79 |
+
merge=merge_mode,
|
80 |
+
rtl_order=rtl_order
|
81 |
)
|
82 |
else:
|
83 |
# Should not happen with Radio button selection
|
|
|
171 |
info="If unchecked, all panels will be in the root of the ZIP, with filenames prefixed by the original image name."
|
172 |
)
|
173 |
|
174 |
+
rtl_order = gr.Checkbox(
|
175 |
+
label="Right-to-Left (RTL) Reading Order",
|
176 |
+
value=True, # Default to True for manga
|
177 |
+
info="Check this for manga that is read from right to left. Uncheck for western comics."
|
178 |
+
)
|
179 |
+
|
180 |
# --- Shared Parameters ---
|
181 |
gr.Markdown("### Shared Parameters")
|
182 |
merge_mode = gr.Dropdown(
|
|
|
228 |
input_files,
|
229 |
method,
|
230 |
separate_folders,
|
231 |
+
rtl_order,
|
232 |
merge_mode,
|
233 |
split_joint,
|
234 |
fallback,
|
image_processing/panel.py
CHANGED
@@ -308,6 +308,71 @@ def get_fallback_panels(
|
|
308 |
|
309 |
return panels
|
310 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
311 |
|
312 |
def generate_panel_blocks(
|
313 |
image: np.ndarray,
|
@@ -315,7 +380,8 @@ def generate_panel_blocks(
|
|
315 |
split_joint_panels: bool = False,
|
316 |
fallback: bool = True,
|
317 |
mode: str = OutputMode.BOUNDING,
|
318 |
-
merge: str = MergeMode.NONE
|
|
|
319 |
) -> list[np.ndarray]:
|
320 |
"""
|
321 |
Generates the separate panel images from the base image
|
@@ -324,6 +390,7 @@ def generate_panel_blocks(
|
|
324 |
- mode: The mode to use for extraction
|
325 |
- 'masked': Extracts the panels by cuting out only the inside of the contours
|
326 |
- 'bounding': Extracts the panels by using the bounding boxes of the contours
|
|
|
327 |
"""
|
328 |
|
329 |
grayscale_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
@@ -332,6 +399,12 @@ def generate_panel_blocks(
|
|
332 |
page_without_background = get_page_without_background(grayscale_image, background_mask, split_joint_panels)
|
333 |
contours, _ = cv2.findContours(page_without_background, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
334 |
contours = list(filter(lambda c: is_contour_sufficiently_big(c, image.shape[0], image.shape[1]), contours))
|
|
|
|
|
|
|
|
|
|
|
|
|
335 |
|
336 |
def get_panels(contours):
|
337 |
panels = extract_panels(image, contours, mode=mode)
|
@@ -353,9 +426,16 @@ def generate_panel_blocks(
|
|
353 |
return panels
|
354 |
|
355 |
|
356 |
-
def generate_panel_blocks_by_ai(
|
|
|
|
|
|
|
|
|
357 |
"""
|
358 |
Generates the separate panel images from the base image using AI with merge
|
|
|
|
|
|
|
359 |
"""
|
360 |
grayscale_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
361 |
processed_image = preprocess_image(grayscale_image)
|
@@ -369,6 +449,11 @@ def generate_panel_blocks_by_ai(image: np.ndarray, merge: str = MergeMode.NONE)
|
|
369 |
x1, y1, x2, y2, conf, cls = detection.tolist() # Convert to Python list
|
370 |
x1, y1, x2, y2 = map(int, [x1, y1, x2, y2])
|
371 |
bounding_boxes.append((x1, y1, x2 - x1, y2 - y1))
|
|
|
|
|
|
|
|
|
|
|
372 |
|
373 |
def get_panels(bounding_boxes):
|
374 |
panels = []
|
|
|
308 |
|
309 |
return panels
|
310 |
|
311 |
+
def _sort_items_by_reading_order(items: list, rtl_order: bool, image_height: int) -> list:
|
312 |
+
"""
|
313 |
+
Sorts contours or bounding boxes based on reading order (top-to-bottom, then LTR/RTL).
|
314 |
+
This function is robust against minor vertical misalignments by grouping items into rows.
|
315 |
+
|
316 |
+
Parameters:
|
317 |
+
- items: A list of contours (np.ndarray) or bounding boxes (tuple of x,y,w,h).
|
318 |
+
- rtl_order: If True, sort horizontally from right-to-left.
|
319 |
+
- image_height: The height of the original image, used for calculating tolerance.
|
320 |
+
|
321 |
+
Returns:
|
322 |
+
- A sorted list of the input items.
|
323 |
+
"""
|
324 |
+
if not items:
|
325 |
+
return []
|
326 |
+
|
327 |
+
# Unify items into a list of (item, bbox) tuples for consistent processing
|
328 |
+
item_bboxes = []
|
329 |
+
for item in items:
|
330 |
+
if isinstance(item, np.ndarray): # It's a contour
|
331 |
+
bbox = cv2.boundingRect(item)
|
332 |
+
else: # It's already a bbox tuple
|
333 |
+
bbox = item
|
334 |
+
item_bboxes.append((item, bbox))
|
335 |
+
|
336 |
+
# Initial sort by top y-coordinate
|
337 |
+
item_bboxes.sort(key=lambda x: x[1][1])
|
338 |
+
|
339 |
+
rows = []
|
340 |
+
current_row = []
|
341 |
+
if item_bboxes:
|
342 |
+
# Start the first row
|
343 |
+
current_row.append(item_bboxes[0])
|
344 |
+
first_item_in_row_bbox = item_bboxes[0][1]
|
345 |
+
|
346 |
+
# Define a dynamic tolerance based on the height of the first panel in a row.
|
347 |
+
# A panel can be considered in the same row if its top is not lower than
|
348 |
+
# the first panel's top + 30% of its height. This is a robust heuristic.
|
349 |
+
# We also add a minimum tolerance for very short panels.
|
350 |
+
y_tolerance = max(10, int(first_item_in_row_bbox[3] * 0.3))
|
351 |
+
|
352 |
+
for item, bbox in item_bboxes[1:]:
|
353 |
+
# If the current panel's y is within the tolerance of the current row's start y
|
354 |
+
if bbox[1] < first_item_in_row_bbox[1] + y_tolerance:
|
355 |
+
current_row.append((item, bbox))
|
356 |
+
else:
|
357 |
+
# Finish the current row
|
358 |
+
# Sort the completed row horizontally
|
359 |
+
current_row.sort(key=lambda x: -x[1][0] if rtl_order else x[1][0])
|
360 |
+
rows.append(current_row)
|
361 |
+
|
362 |
+
# Start a new row
|
363 |
+
current_row = [(item, bbox)]
|
364 |
+
first_item_in_row_bbox = bbox
|
365 |
+
y_tolerance = max(10, int(first_item_in_row_bbox[3] * 0.3))
|
366 |
+
|
367 |
+
# Add the last processed row
|
368 |
+
if current_row:
|
369 |
+
current_row.sort(key=lambda x: -x[1][0] if rtl_order else x[1][0])
|
370 |
+
rows.append(current_row)
|
371 |
+
|
372 |
+
# Flatten the rows and extract the original items in the correct order
|
373 |
+
sorted_items = [item for row in rows for item, bbox in row]
|
374 |
+
|
375 |
+
return sorted_items
|
376 |
|
377 |
def generate_panel_blocks(
|
378 |
image: np.ndarray,
|
|
|
380 |
split_joint_panels: bool = False,
|
381 |
fallback: bool = True,
|
382 |
mode: str = OutputMode.BOUNDING,
|
383 |
+
merge: str = MergeMode.NONE,
|
384 |
+
rtl_order: bool = False
|
385 |
) -> list[np.ndarray]:
|
386 |
"""
|
387 |
Generates the separate panel images from the base image
|
|
|
390 |
- mode: The mode to use for extraction
|
391 |
- 'masked': Extracts the panels by cuting out only the inside of the contours
|
392 |
- 'bounding': Extracts the panels by using the bounding boxes of the contours
|
393 |
+
- rtl_order: If True, sort panels from right-to-left. Otherwise, left-to-right.
|
394 |
"""
|
395 |
|
396 |
grayscale_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
|
|
399 |
page_without_background = get_page_without_background(grayscale_image, background_mask, split_joint_panels)
|
400 |
contours, _ = cv2.findContours(page_without_background, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
401 |
contours = list(filter(lambda c: is_contour_sufficiently_big(c, image.shape[0], image.shape[1]), contours))
|
402 |
+
|
403 |
+
# Sort by top-to-bottom (y-coordinate) first, then by horizontal order.
|
404 |
+
# For RTL, we sort by x-coordinate in descending order (by negating it).
|
405 |
+
if contours:
|
406 |
+
image_height = image.shape[0]
|
407 |
+
contours = _sort_items_by_reading_order(contours, rtl_order, image_height)
|
408 |
|
409 |
def get_panels(contours):
|
410 |
panels = extract_panels(image, contours, mode=mode)
|
|
|
426 |
return panels
|
427 |
|
428 |
|
429 |
+
def generate_panel_blocks_by_ai(
|
430 |
+
image: np.ndarray,
|
431 |
+
merge: str = MergeMode.NONE,
|
432 |
+
rtl_order: bool = False
|
433 |
+
) -> list[np.ndarray]:
|
434 |
"""
|
435 |
Generates the separate panel images from the base image using AI with merge
|
436 |
+
|
437 |
+
Parameters:
|
438 |
+
- rtl_order: If True, sort panels from right-to-left. Otherwise, left-to-right.
|
439 |
"""
|
440 |
grayscale_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
441 |
processed_image = preprocess_image(grayscale_image)
|
|
|
449 |
x1, y1, x2, y2, conf, cls = detection.tolist() # Convert to Python list
|
450 |
x1, y1, x2, y2 = map(int, [x1, y1, x2, y2])
|
451 |
bounding_boxes.append((x1, y1, x2 - x1, y2 - y1))
|
452 |
+
|
453 |
+
# Bounding boxes are already (x, y, w, h), so we access coordinates directly.
|
454 |
+
if bounding_boxes:
|
455 |
+
image_height = image.shape[0]
|
456 |
+
bounding_boxes = _sort_items_by_reading_order(bounding_boxes, rtl_order, image_height)
|
457 |
|
458 |
def get_panels(bounding_boxes):
|
459 |
panels = []
|