567 lines
23 KiB
Python
567 lines
23 KiB
Python
import cv2
|
|
import numpy as np
|
|
|
|
|
|
# AutoslicerDetectionMixin: class definition for ScanSlicer behavior.
|
|
class AutoslicerDetectionMixin:
|
|
# Find all bbox related to n
|
|
# get_box_relatives: function definition for ScanSlicer behavior.
|
|
def get_box_relatives(self, hierarchy, n):
|
|
relatives = []
|
|
|
|
while 1:
|
|
father = hierarchy[n][3]
|
|
if father >= 0:
|
|
relatives.append(father)
|
|
n = father
|
|
else:
|
|
return relatives
|
|
|
|
# _poly_iou_prepared: function definition for ScanSlicer behavior.
|
|
def _poly_iou_prepared(self, a, b, area_a=None, area_b=None):
|
|
if area_a is None:
|
|
area_a = cv2.contourArea(a)
|
|
if area_b is None:
|
|
area_b = cv2.contourArea(b)
|
|
if area_a <= 0 or area_b <= 0:
|
|
return 0.0
|
|
|
|
try:
|
|
inter_area, _ = cv2.intersectConvexConvex(a, b)
|
|
except cv2.error:
|
|
return 0.0
|
|
|
|
union_area = area_a + area_b - inter_area
|
|
if union_area <= 0:
|
|
return 0.0
|
|
return float(inter_area / union_area)
|
|
|
|
# poly_iou: function definition for ScanSlicer behavior.
|
|
def poly_iou(self, poly_a, poly_b):
|
|
a = np.asarray(poly_a, dtype=np.float32).reshape(-1, 2)
|
|
b = np.asarray(poly_b, dtype=np.float32).reshape(-1, 2)
|
|
return self._poly_iou_prepared(a, b)
|
|
|
|
# suppress_border_bar_artifacts: function definition for ScanSlicer behavior.
|
|
def suppress_border_bar_artifacts(self, mask, update_status_callback=None, status_prefix=""):
|
|
if mask is None or mask.ndim != 2:
|
|
return mask
|
|
|
|
h, w = mask.shape[:2]
|
|
if h < 20 or w < 20:
|
|
return mask
|
|
|
|
binary = (mask > 0).astype(np.uint8)
|
|
num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(binary, connectivity=8)
|
|
if num_labels <= 1:
|
|
return mask
|
|
|
|
max_thin_h = max(2, int(round(h * 0.02)))
|
|
max_thin_w = max(2, int(round(w * 0.02)))
|
|
min_long_w = max(16, int(round(w * 0.25)))
|
|
min_long_h = max(16, int(round(h * 0.25)))
|
|
|
|
cleaned = binary.copy()
|
|
removed = 0
|
|
for idx in range(1, num_labels):
|
|
x, y, width, height, _area = stats[idx]
|
|
near_top = y <= max_thin_h
|
|
near_bottom = (y + height) >= (h - max_thin_h)
|
|
near_left = x <= max_thin_w
|
|
near_right = (x + width) >= (w - max_thin_w)
|
|
|
|
horizontal_bar = height <= max_thin_h and width >= min_long_w and (near_top or near_bottom)
|
|
vertical_bar = width <= max_thin_w and height >= min_long_h and (near_left or near_right)
|
|
if horizontal_bar or vertical_bar:
|
|
cleaned[labels == idx] = 0
|
|
removed += 1
|
|
|
|
# Also trim thin foreground strips inside narrow border bands.
|
|
# This handles bars connected to a larger photo component.
|
|
band_h = max(2, min(20, int(round(h * 0.008))))
|
|
band_w = max(2, min(20, int(round(w * 0.008))))
|
|
min_band_long_w = max(20, int(round(w * 0.2)))
|
|
min_band_long_h = max(20, int(round(h * 0.2)))
|
|
trimmed = 0
|
|
|
|
# trim_horizontal_band: function definition for ScanSlicer behavior.
|
|
def trim_horizontal_band(row_start, row_end):
|
|
nonlocal cleaned, trimmed
|
|
band = cleaned[row_start:row_end, :]
|
|
if band.size == 0:
|
|
return
|
|
n, band_labels, band_stats, _ = cv2.connectedComponentsWithStats(band, connectivity=8)
|
|
if n <= 1:
|
|
return
|
|
for label_idx in range(1, n):
|
|
x, y, width, height, area = band_stats[label_idx]
|
|
if width < min_band_long_w:
|
|
continue
|
|
if height > band_h:
|
|
continue
|
|
fill = float(area) / float(max(1, width * height))
|
|
if fill < 0.25:
|
|
continue
|
|
mask_sel = band_labels == label_idx
|
|
if np.any(mask_sel):
|
|
band[mask_sel] = 0
|
|
trimmed += 1
|
|
cleaned[row_start:row_end, :] = band
|
|
|
|
# trim_vertical_band: function definition for ScanSlicer behavior.
|
|
def trim_vertical_band(col_start, col_end):
|
|
nonlocal cleaned, trimmed
|
|
band = cleaned[:, col_start:col_end]
|
|
if band.size == 0:
|
|
return
|
|
n, band_labels, band_stats, _ = cv2.connectedComponentsWithStats(band, connectivity=8)
|
|
if n <= 1:
|
|
return
|
|
for label_idx in range(1, n):
|
|
x, y, width, height, area = band_stats[label_idx]
|
|
if height < min_band_long_h:
|
|
continue
|
|
if width > band_w:
|
|
continue
|
|
fill = float(area) / float(max(1, width * height))
|
|
if fill < 0.25:
|
|
continue
|
|
mask_sel = band_labels == label_idx
|
|
if np.any(mask_sel):
|
|
band[mask_sel] = 0
|
|
trimmed += 1
|
|
cleaned[:, col_start:col_end] = band
|
|
|
|
trim_horizontal_band(0, min(h, band_h))
|
|
trim_horizontal_band(max(0, h - band_h), h)
|
|
trim_vertical_band(0, min(w, band_w))
|
|
trim_vertical_band(max(0, w - band_w), w)
|
|
|
|
if removed > 0:
|
|
self.update_status(
|
|
update_status_callback,
|
|
status_prefix + f"Suppressed {removed} border artifact component(s).",
|
|
)
|
|
if trimmed > 0:
|
|
self.update_status(
|
|
update_status_callback,
|
|
status_prefix + f"Trimmed {trimmed} border strip artifact(s).",
|
|
)
|
|
|
|
if removed > 0 or trimmed > 0:
|
|
return (cleaned * 255).astype(np.uint8)
|
|
|
|
return mask
|
|
|
|
# extract_boxes_from_mask: function definition for ScanSlicer behavior.
|
|
def extract_boxes_from_mask(self, mask, update_status_callback=None, status_prefix=""):
|
|
mask = self.suppress_border_bar_artifacts(mask, update_status_callback, status_prefix)
|
|
self.update_status(update_status_callback, status_prefix + "Finding contours...")
|
|
contours, hierarchy = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
|
|
|
|
if hierarchy is None:
|
|
return []
|
|
|
|
hierarchy = hierarchy[0]
|
|
|
|
# Calculate total image area and minimum box thresh
|
|
h, w, _ = self.image.shape
|
|
img_area = h * w
|
|
min_area = self.params.bbox_min_size.get() / 100 * img_area
|
|
max_area = self.params.bbox_max_size.get() / 100 * img_area
|
|
min_aspect = self.params.aspect_ratio_min.get() / 100.0
|
|
max_aspect = max(min_aspect, self.params.aspect_ratio_max.get() / 100.0)
|
|
border_margin_x = w * self.params.border_ignore.get() / 100.0
|
|
border_margin_y = h * self.params.border_ignore.get() / 100.0
|
|
merge_iou = self.params.iou_merge_thresh.get() / 100.0
|
|
approx_pct = self.params.contour_approx_pct.get() / 1000.0
|
|
|
|
boxes = []
|
|
box_polys_f32 = []
|
|
box_areas = []
|
|
good_ids = set()
|
|
last_contour = len(contours) - 1
|
|
for n, contour in enumerate(contours):
|
|
if self.abort_flag:
|
|
return []
|
|
|
|
# No triangles
|
|
if len(contour) < 4:
|
|
continue
|
|
|
|
if n % 25 == 0 or n == last_contour:
|
|
self.update_status(
|
|
update_status_callback,
|
|
status_prefix + "Processing contour " + str(n) + "/" + str(len(contours)),
|
|
)
|
|
|
|
# If child of a selected bbox, then it's discarded (it's inside)
|
|
ancestor = hierarchy[n][3]
|
|
skip = False
|
|
while ancestor >= 0:
|
|
if ancestor in good_ids:
|
|
skip = True
|
|
break
|
|
ancestor = hierarchy[ancestor][3]
|
|
if skip:
|
|
continue
|
|
|
|
contour_for_rect = contour
|
|
if approx_pct > 0:
|
|
perimeter = cv2.arcLength(contour, True)
|
|
epsilon = perimeter * approx_pct
|
|
approximated = cv2.approxPolyDP(contour, epsilon, True)
|
|
if len(approximated) >= 4:
|
|
contour_for_rect = approximated
|
|
|
|
# Find bounding box
|
|
bbox_rot_rect = cv2.minAreaRect(contour_for_rect)
|
|
bounding_box_f32 = cv2.boxPoints(bbox_rot_rect).astype(np.float32)
|
|
bounding_box = bounding_box_f32.astype(np.intp)
|
|
shape_area = cv2.contourArea(contour)
|
|
bbox_area = cv2.contourArea(bounding_box_f32)
|
|
|
|
# Too small or too big
|
|
if shape_area < 1 or bbox_area < min_area or bbox_area > max_area:
|
|
continue
|
|
|
|
# Fill ratio
|
|
fill_ratio = shape_area / bbox_area * 100
|
|
if fill_ratio < self.params.bbox_fill_thresh.get():
|
|
continue
|
|
|
|
# Aspect ratio (long/short, orientation-agnostic)
|
|
bbox_w, bbox_h = bbox_rot_rect[1]
|
|
long_edge = max(bbox_w, bbox_h)
|
|
short_edge = min(bbox_w, bbox_h)
|
|
if short_edge <= 0:
|
|
continue
|
|
aspect_ratio = long_edge / short_edge
|
|
if aspect_ratio < min_aspect or aspect_ratio > max_aspect:
|
|
continue
|
|
|
|
# Ignore contours touching image border by configurable margin.
|
|
if border_margin_x > 0 or border_margin_y > 0:
|
|
min_x = float(np.min(bounding_box[:, 0]))
|
|
max_x = float(np.max(bounding_box[:, 0]))
|
|
min_y = float(np.min(bounding_box[:, 1]))
|
|
max_y = float(np.max(bounding_box[:, 1]))
|
|
if (
|
|
min_x <= border_margin_x
|
|
or min_y <= border_margin_y
|
|
or max_x >= (w - 1 - border_margin_x)
|
|
or max_y >= (h - 1 - border_margin_y)
|
|
):
|
|
continue
|
|
|
|
# Merge near-duplicates by overlap threshold.
|
|
merged = False
|
|
if merge_iou > 0:
|
|
for idx, existing_poly in enumerate(box_polys_f32):
|
|
if self._poly_iou_prepared(existing_poly, bounding_box_f32, box_areas[idx], bbox_area) >= merge_iou:
|
|
existing_area = box_areas[idx]
|
|
if bbox_area > existing_area:
|
|
boxes[idx] = bounding_box
|
|
box_polys_f32[idx] = bounding_box_f32
|
|
box_areas[idx] = bbox_area
|
|
merged = True
|
|
break
|
|
if merged:
|
|
good_ids.add(n)
|
|
continue
|
|
|
|
# It's good
|
|
good_ids.add(n)
|
|
boxes.append(bounding_box)
|
|
box_polys_f32.append(bounding_box_f32)
|
|
box_areas.append(bbox_area)
|
|
|
|
if len(boxes) > self.MAX_RETURNED_BOXES:
|
|
ranked = sorted(
|
|
zip(box_areas, boxes),
|
|
key=lambda item: item[0],
|
|
reverse=True,
|
|
)[: self.MAX_RETURNED_BOXES]
|
|
boxes = [box for _, box in ranked]
|
|
self.update_status(
|
|
update_status_callback,
|
|
status_prefix + f"Too many candidates; keeping top {self.MAX_RETURNED_BOXES}.",
|
|
)
|
|
|
|
return boxes
|
|
|
|
# apply_morphology: function definition for ScanSlicer behavior.
|
|
def apply_morphology(self, mask, update_status_callback=None):
|
|
operation = self.params.morph_op.get()
|
|
kernel_size = self.params.dilate_kernel.get()
|
|
if operation == 0 or kernel_size <= 0:
|
|
return mask
|
|
|
|
kernel = np.ones((kernel_size, kernel_size), np.uint8)
|
|
if operation == 1:
|
|
self.update_status(update_status_callback, "Morphology: dilate...")
|
|
return cv2.dilate(mask, kernel)
|
|
if operation == 2:
|
|
self.update_status(update_status_callback, "Morphology: erode...")
|
|
return cv2.erode(mask, kernel)
|
|
if operation == 3:
|
|
self.update_status(update_status_callback, "Morphology: open...")
|
|
return cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
|
|
if operation == 4:
|
|
self.update_status(update_status_callback, "Morphology: close...")
|
|
return cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
|
|
return mask
|
|
|
|
# apply_threshold_pipeline: function definition for ScanSlicer behavior.
|
|
def apply_threshold_pipeline(self, image_gray, update_status_callback=None):
|
|
# Binary filter
|
|
if self.params.bw_method.get() == 0:
|
|
self.update_status(update_status_callback, "Simple binary thresholding...")
|
|
_, mask = cv2.threshold(image_gray, self.params.bw_thresh.get(), 255, cv2.THRESH_BINARY)
|
|
return mask
|
|
|
|
# Adaptive thresh
|
|
if self.params.bw_method.get() == 1:
|
|
self.update_status(update_status_callback, "Adaptive Gaussian thresholding...")
|
|
# OpenCV requires blockSize to be odd and > 1.
|
|
block = self.params.bw_gauss.get()
|
|
if block <= 1:
|
|
block = 3
|
|
elif block % 2 == 0:
|
|
block += 1
|
|
return cv2.adaptiveThreshold(
|
|
image_gray,
|
|
255,
|
|
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
|
cv2.THRESH_BINARY,
|
|
block,
|
|
self.params.bw_adaptive_c.get(),
|
|
)
|
|
|
|
# Otsu thresh
|
|
self.update_status(update_status_callback, "Otsu thresholding...")
|
|
_, mask = cv2.threshold(
|
|
image_gray,
|
|
0,
|
|
255,
|
|
cv2.THRESH_BINARY + cv2.THRESH_OTSU,
|
|
)
|
|
return mask
|
|
|
|
# apply_canny_pipeline: function definition for ScanSlicer behavior.
|
|
def apply_canny_pipeline(self, image_gray, update_status_callback=None):
|
|
self.update_status(update_status_callback, "Canny edge detection...")
|
|
low = min(self.params.canny_low.get(), self.params.canny_high.get())
|
|
high = max(self.params.canny_low.get(), self.params.canny_high.get())
|
|
return cv2.Canny(image_gray, low, high)
|
|
|
|
# extract_boxes_from_mask_relaxed: function definition for ScanSlicer behavior.
|
|
def extract_boxes_from_mask_relaxed(self, mask, update_status_callback=None, status_prefix="", return_areas=False):
|
|
mask = self.suppress_border_bar_artifacts(mask, update_status_callback, status_prefix)
|
|
self.update_status(update_status_callback, status_prefix + "Finding contours...")
|
|
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
|
|
if not contours:
|
|
if return_areas:
|
|
return [], []
|
|
return []
|
|
|
|
h, w, _ = self.image.shape
|
|
img_area = h * w
|
|
min_area = max(1.0, (self.params.bbox_min_size.get() / 100.0) * img_area * 0.5)
|
|
max_area = min(float(img_area), (self.params.bbox_max_size.get() / 100.0) * img_area * 1.05)
|
|
min_aspect = max(1.0, self.params.aspect_ratio_min.get() / 100.0)
|
|
max_aspect = max(10.0, self.params.aspect_ratio_max.get() / 100.0)
|
|
merge_iou = max(0.2, self.params.iou_merge_thresh.get() / 100.0)
|
|
|
|
boxes = []
|
|
box_polys_f32 = []
|
|
box_areas = []
|
|
last_contour = len(contours) - 1
|
|
for n, contour in enumerate(contours):
|
|
if self.abort_flag:
|
|
if return_areas:
|
|
return [], []
|
|
return []
|
|
if len(contour) < 4:
|
|
continue
|
|
|
|
if n % 25 == 0 or n == last_contour:
|
|
self.update_status(
|
|
update_status_callback,
|
|
status_prefix + "Processing contour " + str(n) + "/" + str(len(contours)),
|
|
)
|
|
|
|
rect = cv2.minAreaRect(contour)
|
|
box_f32 = cv2.boxPoints(rect).astype(np.float32)
|
|
box = box_f32.astype(np.intp)
|
|
area = cv2.contourArea(box_f32)
|
|
if area < min_area or area > max_area:
|
|
continue
|
|
|
|
bw, bh = rect[1]
|
|
long_edge = max(bw, bh)
|
|
short_edge = min(bw, bh)
|
|
if short_edge <= 0:
|
|
continue
|
|
aspect_ratio = long_edge / short_edge
|
|
if aspect_ratio < min_aspect or aspect_ratio > max_aspect:
|
|
continue
|
|
|
|
merged = False
|
|
for idx, existing in enumerate(box_polys_f32):
|
|
if self._poly_iou_prepared(existing, box_f32, box_areas[idx], area) >= merge_iou:
|
|
existing_area = box_areas[idx]
|
|
if area > existing_area:
|
|
boxes[idx] = box
|
|
box_polys_f32[idx] = box_f32
|
|
box_areas[idx] = area
|
|
merged = True
|
|
break
|
|
if not merged:
|
|
boxes.append(box)
|
|
box_polys_f32.append(box_f32)
|
|
box_areas.append(area)
|
|
|
|
if len(boxes) > self.MAX_RETURNED_BOXES:
|
|
ranked = sorted(
|
|
zip(box_areas, boxes),
|
|
key=lambda item: item[0],
|
|
reverse=True,
|
|
)[: self.MAX_RETURNED_BOXES]
|
|
box_areas = [area for area, _ in ranked]
|
|
boxes = [box for _, box in ranked]
|
|
if return_areas:
|
|
return boxes, box_areas
|
|
return boxes
|
|
|
|
# white_background_fallback: function definition for ScanSlicer behavior.
|
|
def white_background_fallback(self, image_gray, update_status_callback=None):
|
|
self.update_status(update_status_callback, "Fallback: trying white-background recovery...")
|
|
|
|
h, w = image_gray.shape[:2]
|
|
# Keep cleanup kernels conservative so separate photos are not merged
|
|
# into one connected component.
|
|
close_k = int(np.clip(round(min(h, w) * 0.0025), 3, 15))
|
|
if close_k % 2 == 0:
|
|
close_k += 1
|
|
open_k = max(3, close_k // 2)
|
|
if open_k % 2 == 0:
|
|
open_k += 1
|
|
close_kernel = np.ones((close_k, close_k), np.uint8)
|
|
open_kernel = np.ones((open_k, open_k), np.uint8)
|
|
|
|
candidates = []
|
|
_, otsu_inv = cv2.threshold(image_gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
|
|
candidates.append(("Otsu inverted", otsu_inv))
|
|
for threshold in (220, 235, 245):
|
|
_, mask = cv2.threshold(image_gray, threshold, 255, cv2.THRESH_BINARY_INV)
|
|
candidates.append((f"Inverted threshold {threshold}", mask))
|
|
|
|
median = float(np.median(image_gray))
|
|
low = int(max(0.0, 0.66 * median))
|
|
high = int(min(255.0, 1.33 * median))
|
|
edge_mask = cv2.Canny(image_gray, low, high)
|
|
candidates.append(("Edges", edge_mask))
|
|
|
|
best_boxes = []
|
|
best_mask = None
|
|
best_score = (-1, -1, -1.0, -999999)
|
|
|
|
for name, raw_mask in candidates:
|
|
if self.abort_flag:
|
|
return [], None
|
|
|
|
self.update_status(update_status_callback, f"Fallback: {name}...")
|
|
mask_variants = [("raw", raw_mask)]
|
|
cleaned = cv2.morphologyEx(raw_mask, cv2.MORPH_CLOSE, close_kernel)
|
|
cleaned = cv2.morphologyEx(cleaned, cv2.MORPH_OPEN, open_kernel)
|
|
if name == "Edges":
|
|
cleaned = cv2.dilate(cleaned, open_kernel)
|
|
mask_variants.append(("cleaned", cleaned))
|
|
|
|
for variant_name, candidate_mask in mask_variants:
|
|
boxes, box_areas = self.extract_boxes_from_mask_relaxed(
|
|
candidate_mask,
|
|
update_status_callback,
|
|
status_prefix=f"Fallback ({name}/{variant_name}): ",
|
|
return_areas=True,
|
|
)
|
|
if len(boxes) == 0:
|
|
continue
|
|
|
|
total_area = float(sum(box_areas))
|
|
|
|
# Prefer plausible multi-photo detections and use area as tie-breaker.
|
|
plausible = 1 if len(boxes) <= 12 else 0
|
|
multi = 1 if len(boxes) >= 2 else 0
|
|
score = (plausible, multi, len(boxes), float(total_area))
|
|
if score > best_score:
|
|
best_score = score
|
|
best_boxes = boxes
|
|
best_mask = candidate_mask
|
|
|
|
return best_boxes, best_mask
|
|
|
|
# autodetect_slices: function definition for ScanSlicer behavior.
|
|
def autodetect_slices(self, update_status_callback=None, preview_filter_output=False, fast_mode=False):
|
|
filter_out = self.image_gray
|
|
self.abort_flag = False
|
|
|
|
if self.params.clahe_clip.get() > 0:
|
|
self.update_status(update_status_callback, "Auto contrast (CLAHE)...")
|
|
clip_limit = max(0.1, self.params.clahe_clip.get() / 10.0)
|
|
clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=(8, 8))
|
|
filter_out = clahe.apply(filter_out)
|
|
|
|
# Gaussian blur
|
|
if self.params.gaussian.get() > 0:
|
|
self.update_status(update_status_callback, "Gaussian blur...")
|
|
|
|
if self.params.gaussian.get() % 2 == 0:
|
|
block = self.params.gaussian.get() + 1
|
|
else:
|
|
block = self.params.gaussian.get()
|
|
filter_out = cv2.GaussianBlur(filter_out, (block, block), 0)
|
|
|
|
processing_gray = filter_out.copy()
|
|
|
|
if self.params.detect_pipeline.get() == 0:
|
|
filter_out = self.apply_threshold_pipeline(filter_out, update_status_callback)
|
|
else:
|
|
filter_out = self.apply_canny_pipeline(filter_out, update_status_callback)
|
|
|
|
invert_mode = self.params.invert_mode.get()
|
|
preview_filter = filter_out
|
|
if invert_mode == 2:
|
|
# Force inverted polarity first, then apply morphology on the same polarity
|
|
# that contours are extracted from.
|
|
inverted_filter = cv2.bitwise_not(filter_out)
|
|
preview_filter = self.apply_morphology(inverted_filter, update_status_callback)
|
|
boxes = self.extract_boxes_from_mask(preview_filter, update_status_callback)
|
|
else:
|
|
preview_filter = self.apply_morphology(filter_out, update_status_callback)
|
|
boxes = self.extract_boxes_from_mask(preview_filter, update_status_callback)
|
|
|
|
# Fallback for the opposite polarity (dark objects on bright background or vice versa).
|
|
if invert_mode == 0 and len(boxes) == 0 and not self.abort_flag:
|
|
inverted_filter = cv2.bitwise_not(filter_out)
|
|
inverted_filter = self.apply_morphology(inverted_filter, update_status_callback)
|
|
inverted_boxes = self.extract_boxes_from_mask(
|
|
inverted_filter,
|
|
update_status_callback,
|
|
status_prefix="Fallback (inverted): ",
|
|
)
|
|
if len(inverted_boxes) > 0:
|
|
boxes = inverted_boxes
|
|
preview_filter = inverted_filter
|
|
|
|
if len(boxes) == 0 and not self.abort_flag and not fast_mode:
|
|
fallback_boxes, fallback_mask = self.white_background_fallback(processing_gray, update_status_callback)
|
|
if len(fallback_boxes) > 0:
|
|
boxes = fallback_boxes
|
|
preview_filter = fallback_mask
|
|
|
|
if preview_filter_output:
|
|
return boxes, cv2.cvtColor(preview_filter, cv2.COLOR_GRAY2RGB)
|
|
return boxes, cv2.cvtColor(self.image, cv2.COLOR_BGR2RGB)
|