avans06 commited on
Commit
a627433
·
1 Parent(s): e9366fc

feat: Add RTL reading order and robust panel sorting

Browse files

Implement a right-to-left (RTL) reading order option for manga.

Files changed (2) hide show
  1. app.py +12 -2
  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(image: np.ndarray, merge: str = MergeMode.NONE) -> list[np.ndarray]:
 
 
 
 
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 = []