| from typing import List, Tuple |
| from pathlib import Path |
| from .config import Config, load_config |
|
|
| import numpy as np |
| import cv2 |
| from skimage.morphology import skeletonize, remove_small_objects |
| from skimage.measure import label |
| from skimage import measure |
| from tqdm import tqdm |
|
|
| from PIL import Image |
| import numpy as np |
| from sklearn.cluster import KMeans |
| import math |
|
|
| class ImageProcessor: |
| """Handles image preprocessing operations.""" |
| |
| def __init__(self, config: Config = None): |
| self.config = config or load_config() |
| self.index = 0 |
|
|
| def get_output_path(self, output_folder, file_name): |
| self.index += 1 |
| return f'{output_folder}/{self.index:02d}_{file_name}' |
|
|
| def mask_text_regions(self, input_path, bboxes: List[List[int]], output_filename: str = "1_text_removed.jpg", color: Tuple[int, int, int] = (0, 0, 0)) -> str: |
| """Mask text regions in the image to reduce panel extraction noise.""" |
| image = cv2.imread(input_path) |
| if image is None: |
| raise FileNotFoundError(f"Could not load image: {input_path}") |
|
|
| for bbox in bboxes: |
| x1, y1, x2, y2 = bbox |
| cv2.rectangle(image, (x1, y1), (x2, y2), color, thickness=-1) |
|
|
| output_path = f'{self.config.output_folder}/{output_filename}' |
| cv2.imwrite(output_path, image) |
| return str(output_path) |
| |
| def preprocess_image(self, processed_image_path) -> Tuple[str, str, str]: |
| """Preprocess image for panel extraction.""" |
| image = cv2.imread(processed_image_path) |
| if image is None: |
| raise FileNotFoundError(f"Could not load image: {processed_image_path}") |
|
|
| |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) |
|
|
| |
| blurred = cv2.GaussianBlur(gray, (3, 3), 0) |
|
|
| |
| edges = cv2.Canny(blurred, threshold1=50, threshold2=150, apertureSize=3) |
| kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) |
| dilated = cv2.dilate(edges, kernel, iterations=2) |
|
|
| |
| gray_path = self.get_output_path(self.config.output_folder, "gray.jpg") |
| binary_path = self.get_output_path(self.config.output_folder, "binary.jpg") |
| dilated_path = self.get_output_path(self.config.output_folder, "dilated.jpg") |
| |
| cv2.imwrite(str(gray_path), gray) |
| cv2.imwrite(str(binary_path), edges) |
| cv2.imwrite(str(dilated_path), dilated) |
| |
| return str(gray_path), str(binary_path), str(dilated_path) |
|
|
| def invert_if_black_dominates(self, binary): |
| |
| _, binary = cv2.threshold(binary, 127, 255, cv2.THRESH_BINARY) |
|
|
| |
| black_pixels = np.sum(binary == 0) |
| white_pixels = np.sum(binary == 255) |
|
|
| |
| if black_pixels > white_pixels: |
| print("🔄 Inverting image because black > white") |
| inverted = cv2.bitwise_not(binary) |
| else: |
| print("✅ No inversion needed, white >= black") |
| inverted = binary |
|
|
| |
| return inverted, black_pixels > white_pixels |
|
|
| def group_colors(self, processed_image_path, num_clusters: int = 5, file_name="group_colors.jpg", output_folder=None) -> Image.Image: |
| """ |
| Groups similar colors in an image using KMeans clustering. |
| |
| Args: |
| processed_image_path (str): Path to the image to be color-grouped. |
| num_clusters (int): Number of color clusters to form. |
| file_name (str): Name of the output image file. |
| output_folder (str): Optional output directory. |
| |
| Returns: |
| str: Path to the saved grouped-color image. |
| """ |
| output_folder = output_folder or self.config.output_folder |
| |
| image = Image.open(processed_image_path).convert("RGB") |
| np_image = np.array(image) |
| h, w = np_image.shape[:2] |
| pixels = np_image.reshape(-1, 3) |
|
|
| |
| kmeans = KMeans(n_clusters=num_clusters, random_state=42, n_init='auto') |
| labels = kmeans.fit_predict(pixels) |
| centers = kmeans.cluster_centers_.astype(np.uint8) |
|
|
| |
| clustered_pixels = centers[labels].reshape(h, w, 3) |
|
|
| |
| output_path = self.get_output_path(output_folder, file_name) |
| clustered_bgr = clustered_pixels[:, :, ::-1] |
| cv2.imwrite(output_path, clustered_bgr) |
|
|
| return str(output_path) |
|
|
| def thin_image_borders(self, processed_image_path: str, file_name="thin_border.jpg", output_folder=None) -> str: |
| """ |
| Clean dilated image by thinning thick borders and removing hanging clusters. |
| """ |
| output_folder = output_folder or self.config.output_folder |
| |
| img = cv2.imread(processed_image_path) |
| |
| gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) |
| |
|
|
| |
| blurred = cv2.GaussianBlur(gray, (3, 3), 0) |
|
|
| |
| edges = cv2.Canny(blurred, threshold1=50, threshold2=150, apertureSize=3) |
|
|
| |
| skeleton = skeletonize(edges).astype(np.uint8) |
|
|
| |
| labeled = label(skeleton, connectivity=2) |
| cleaned = remove_small_objects(labeled, min_size=150) |
|
|
| |
| final = (cleaned > 0).astype(np.uint8) * 255 |
|
|
| |
| result = 255 - final |
|
|
| |
| output_path = self.get_output_path(output_folder, file_name) |
| cv2.imwrite(output_path, result) |
| return str(output_path) |
|
|
| def remove_dangling_lines(self, image_path, file_name="dangling_lines_removed.jpg", output_folder=None): |
| output_folder = output_folder or self.config.output_folder |
| gray = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) |
|
|
| |
| binary = gray < 128 |
| binary = binary.astype(bool) |
|
|
| |
| labeled = label(binary, connectivity=2) |
|
|
| |
| cleaned = remove_small_objects(labeled, min_size=500) |
|
|
| |
| final_mask = (cleaned > 0).astype(np.uint8) * 255 |
|
|
| |
| final_image = 255 - final_mask |
| |
| output_path = self.get_output_path(output_folder, file_name) |
| cv2.imwrite(output_path, final_image) |
| return output_path |
|
|
| def remove_diagonal_lines(self, image_path, file_name="remove_diagonal_lines.jpg", output_folder=None): |
| output_folder = output_folder or self.config.output_folder |
| |
| |
| img = cv2.imread(image_path) |
| |
| |
| gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) |
| |
| |
| _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV) |
| |
| |
| |
| kernel_length = max(gray.shape[0], gray.shape[1]) // 30 |
| |
| |
| horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_length, 1)) |
| |
| vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, kernel_length)) |
| |
| |
| horizontal_lines = cv2.morphologyEx(binary, cv2.MORPH_OPEN, horizontal_kernel, iterations=2) |
| |
| |
| vertical_lines = cv2.morphologyEx(binary, cv2.MORPH_OPEN, vertical_kernel, iterations=2) |
| |
| |
| rect_lines = cv2.addWeighted(horizontal_lines, 1, vertical_lines, 1, 0) |
| |
| |
| result = np.ones_like(gray) * 255 |
| result[rect_lines > 0] = 0 |
|
|
| |
| output_path = self.get_output_path(output_folder, file_name) |
| cv2.imwrite(output_path, result) |
| return output_path |
|
|
| def thick_black(self, image_path, thickness=20, file_name="thick_black.jpg", output_folder=None): |
| output_folder = output_folder or self.config.output_folder |
| |
| img = cv2.imread(image_path) |
|
|
| |
| gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) |
|
|
| |
| _, binary = cv2.threshold(gray, 10, 255, cv2.THRESH_BINARY_INV) |
|
|
| |
| kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (thickness, thickness)) |
|
|
| |
| dilated = cv2.dilate(binary, kernel, iterations=1) |
|
|
| |
| |
|
|
| |
| result = img.copy() |
| result[np.where(dilated == 255)] = (0, 0, 0) |
|
|
| |
| output_path = self.get_output_path(output_folder, file_name) |
| cv2.imwrite(output_path, result) |
| return output_path |
|
|
| def to_int_box(self, line): |
| return map(int, line[0]) |
|
|
| def remove_diagonal_lines_and_set_white(self, image_path, file_name="remove_diagonal_lines_and_set_white.jpg", output_folder=None): |
| output_folder = output_folder or self.config.output_folder |
| |
| image = cv2.imread(image_path) |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) |
|
|
| |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) |
| blurred = cv2.GaussianBlur(gray, (3, 3), 0) |
| edges = cv2.Canny(blurred, 50, 150, apertureSize=3) |
|
|
| |
| kernel = np.ones((2, 2), np.uint8) |
| edges = cv2.dilate(edges, kernel, iterations=1) |
|
|
| |
| |
|
|
| |
| lsd = cv2.createLineSegmentDetector(0) |
| lines, _, _, _ = lsd.detect(gray) |
|
|
| |
| output = image.copy() |
|
|
| combined_lines = [] |
|
|
| if lines is not None: |
| combined_lines.extend(lines) |
|
|
| |
| |
|
|
| if combined_lines is not None: |
| for line in combined_lines: |
| x1, y1, x2, y2 = self.to_int_box(line) |
|
|
| |
| angle = np.abs(np.arctan2(y2 - y1, x2 - x1) * 180.0 / np.pi) |
|
|
| |
| if (80 < angle < 100) or (170 < angle < 190) or angle < 10 or angle > 350: |
| continue |
| else: |
| |
| padding = 2 |
| xmin = min(x1, x2) - padding |
| xmax = max(x1, x2) + padding |
| ymin = min(y1, y2) - padding |
| ymax = max(y1, y2) + padding |
|
|
| |
| cv2.rectangle(output, (xmin, ymin), (xmax, ymax), (255, 255, 255), thickness=-1) |
|
|
| |
| output_path = self.get_output_path(output_folder, file_name) |
| cv2.imwrite(output_path, output) |
| return output_path |
|
|
| def remove_small_regions(self, image_path, file_name="remove_small_regions.jpg", output_folder=None): |
| output_folder = output_folder or self.config.output_folder |
|
|
| |
| img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) |
| visual = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) |
|
|
| if img is None: |
| raise FileNotFoundError(f"Could not load image: {image_path}") |
|
|
| height_, width_ = img.shape |
| min_area = height_ * width_ * self.config.min_area_ratio |
|
|
| |
| _, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY_INV) |
|
|
| |
| labeled = measure.label(binary) |
| regions = measure.regionprops(labeled) |
|
|
| |
| clean_mask = np.copy(binary) |
|
|
| for region in regions: |
| area = region.area |
| minr, minc, maxr, maxc = region.bbox |
| width = maxc - minc |
| height = maxr - minr |
|
|
| |
| if width < width_ * self.config.min_width_ratio and height < height_ * self.config.min_height_ratio: |
| if (width/width_) < 0.9 and (height/height_) < 0.9: |
| clean_mask[labeled == region.label] = 0 |
| cv2.rectangle(visual, (minc, minr), (maxc, maxr), (0, 0, 255), 2) |
| continue |
|
|
| |
| region_crop = binary[minr:maxr, minc:maxc] |
| edges = cv2.Canny(region_crop, 50, 150, apertureSize=3) |
| lines = cv2.HoughLinesP(edges, 1, np.pi / 180, threshold=30, minLineLength=10, maxLineGap=5) |
|
|
| if lines is not None: |
| for line in lines: |
| x1, y1, x2, y2 = line[0] |
| angle = np.abs(np.arctan2(y2 - y1, x2 - x1) * 180.0 / np.pi) |
| |
| line_width = abs(x2 - x1) |
| line_height = abs(y2 - y1) |
|
|
| if line_height < height_ * self.config.min_height_ratio and line_width < width_ * self.config.min_width_ratio: |
| break |
| else: |
| |
| |
| clean_mask[labeled == region.label] = 0 |
| cv2.rectangle(visual, (minc, minr), (maxc, maxr), (0, 255, 255), 2) |
| elif width < width_ * self.config.min_width_ratio and height < height_ * self.config.min_height_ratio: |
| |
| clean_mask[labeled == region.label] = 0 |
| cv2.rectangle(visual, (minc, minr), (maxc, maxr), (255, 0, 0), 2) |
|
|
| |
| output_path = self.get_output_path(output_folder, f"debug_{file_name}") |
| cv2.imwrite(output_path, visual) |
|
|
| |
| cleaned = cv2.bitwise_not(clean_mask) |
| output_path = self.get_output_path(output_folder, file_name) |
| cv2.imwrite(output_path, cleaned) |
| return output_path |
|
|
|
|
| def thin_black(self, image_path, file_name="thin_black.jpg", output_folder=None): |
| output_folder = output_folder or self.config.output_folder |
| |
| img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) |
|
|
| |
| if img is None: |
| raise ValueError("Image not loaded. Check the file path.") |
|
|
| |
| _, binary = cv2.threshold(img, 128, 255, cv2.THRESH_BINARY_INV) |
|
|
| |
| try: |
| |
| thinned = cv2.ximgproc.thinning(binary) |
| except AttributeError: |
| |
| skel = np.zeros(binary.shape, np.uint8) |
| element = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3)) |
| while True: |
| eroded = cv2.erode(binary, element) |
| temp = cv2.dilate(eroded, element) |
| temp = cv2.subtract(binary, temp) |
| skel = cv2.bitwise_or(skel, temp) |
| binary = eroded.copy() |
| if cv2.countNonZero(binary) == 0: |
| break |
| thinned = skel |
|
|
| |
| thinned = 255 - thinned |
|
|
| |
| output_path = self.get_output_path(output_folder, file_name) |
| cv2.imwrite(output_path, thinned) |
| return output_path |
|
|
| def thin_lines_direct(self, image_path, file_name="thin_lines_direct.jpg", output_folder=None): |
| output_folder = output_folder or self.config.output_folder |
| |
| |
| img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) |
| if img is None: |
| raise ValueError("Could not load image") |
| |
| |
| _, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY) |
| |
| |
| result = np.full_like(binary, 255) |
| |
| height, width = binary.shape |
| print("Processing thick lines...") |
| |
| |
| print("Step 1: Thinning horizontal segments...") |
| for row in range(height): |
| col = 0 |
| while col < width: |
| |
| if binary[row, col] == 0: |
| |
| start_col = col |
| while col < width and binary[row, col] == 0: |
| col += 1 |
| end_col = col - 1 |
| |
| |
| segment_width = end_col - start_col + 1 |
| |
| if segment_width >= 1: |
| |
| mid_col = (start_col + end_col) // 2 |
| |
| |
| thickness = self.get_vertical_thickness(binary, row, mid_col) |
| |
| if thickness > 1: |
| |
| bottom_row = row + thickness - 1 |
| if bottom_row < height: |
| result[bottom_row, start_col:end_col+1] = 0 |
| else: |
| |
| result[row, start_col:end_col+1] = 0 |
| else: |
| col += 1 |
| |
| |
| |
| |
| |
| print("Step 2: Thinning vertical segments...") |
| |
| |
| result_v = np.full_like(binary, 255) |
| |
| for col in range(width): |
| row = 0 |
| while row < height: |
| |
| if binary[row, col] == 0: |
| |
| start_row = row |
| while row < height and binary[row, col] == 0: |
| row += 1 |
| end_row = row - 1 |
| |
| segment_height = end_row - start_row + 1 |
| |
| if segment_height >= 1: |
| |
| mid_row = (start_row + end_row) // 2 |
| |
| |
| thickness = self.get_horizontal_thickness(binary, mid_row, col) |
| |
| if thickness > 1: |
| |
| right_col = col + thickness - 1 |
| if right_col < width: |
| result_v[start_row:end_row+1, right_col] = 0 |
| else: |
| |
| result_v[start_row:end_row+1, col] = 0 |
| else: |
| row += 1 |
| |
| |
| |
| |
| |
| print("Step 3: Combining results...") |
| final_result = cv2.bitwise_and(result, result_v) |
| output_path = self.get_output_path(output_folder, file_name) |
| cv2.imwrite(output_path, final_result) |
| |
| return output_path |
|
|
| def get_vertical_thickness(self, binary, start_row, col): |
| """Get the vertical thickness of a black region starting from start_row, col""" |
| height = binary.shape[0] |
| thickness = 0 |
| |
| row = start_row |
| while row < height and binary[row, col] == 0: |
| thickness += 1 |
| row += 1 |
| |
| return thickness |
|
|
| def get_horizontal_thickness(self, binary, row, start_col): |
| """Get the horizontal thickness of a black region starting from row, start_col""" |
| width = binary.shape[1] |
| thickness = 0 |
| |
| col = start_col |
| while col < width and binary[row, col] == 0: |
| thickness += 1 |
| col += 1 |
| |
| return thickness |
|
|
| def remove_diagonal_only_cells(self, image_path, file_name="remove_diagonal_only_cells.jpg", output_folder=None): |
| output_folder = output_folder or self.config.output_folder |
| |
| img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) |
| if img is None: |
| raise ValueError("Unable to load the image. Check the file path.") |
| |
| |
| _, binary = cv2.threshold(img, 128, 255, cv2.THRESH_BINARY_INV) |
| |
| |
| padded = np.pad(binary, pad_width=1, mode='constant', constant_values=0) |
| rows, cols = binary.shape |
| output = padded.copy() |
| |
| |
| for r in range(1, rows + 1): |
| for c in range(1, cols + 1): |
| if padded[r, c] == 255: |
| |
| neighbors = { |
| 'top_left': padded[r-1, c-1], |
| 'top': padded[r-1, c], |
| 'top_right': padded[r-1, c+1], |
| 'left': padded[r, c-1], |
| 'right': padded[r, c+1], |
| 'bottom_left': padded[r+1, c-1], |
| 'bottom': padded[r+1, c], |
| 'bottom_right': padded[r+1, c+1] |
| } |
| |
| |
| active_count = sum(1 for v in neighbors.values() if v == 255) |
| |
| |
| |
| cond1 = (neighbors['top_left'] == 255 and neighbors['bottom_right'] == 255 and |
| active_count == 2) |
| |
| |
| cond2 = (neighbors['top_left'] == 255 and active_count == 1) |
| |
| |
| cond3 = (neighbors['bottom_right'] == 255 and active_count == 1) |
| |
| |
| cond4 = (neighbors['top_right'] == 255 and neighbors['bottom_left'] == 255 and |
| active_count == 2) |
| |
| |
| cond5 = (neighbors['top_right'] == 255 and active_count == 1) |
| |
| |
| cond6 = (neighbors['bottom_left'] == 255 and active_count == 1) |
| |
| |
| if cond1 or cond2 or cond3 or cond4 or cond5 or cond6: |
| output[r, c] = 0 |
| |
| |
| cleaned = output[1:-1, 1:-1] |
| result = cv2.bitwise_not(cleaned) |
| |
| |
| output_path = self.get_output_path(output_folder, file_name) |
| cv2.imwrite(output_path, result) |
| return output_path |
|
|
| def remove_small_continuity_components( |
| self, |
| image_path, |
| file_name="remove_small_continuity_components.jpg", |
| output_folder=None, |
| ): |
| output_folder = output_folder or self.config.output_folder |
|
|
| |
| img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) |
| if img is None: |
| raise ValueError("Unable to load the image. Check the file path.") |
|
|
| height, width = img.shape |
| min_height = height * self.config.min_height_ratio |
| min_width = width * self.config.min_width_ratio |
|
|
| |
| _, binary = cv2.threshold(img, 128, 255, cv2.THRESH_BINARY_INV) |
|
|
| |
| num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(binary, connectivity=8) |
|
|
| |
| cleaned_output = binary.copy() |
| debug_output = cv2.cvtColor(binary.copy(), cv2.COLOR_GRAY2BGR) |
|
|
| for label in tqdm(range(1, num_labels), desc="Processing labels"): |
| x, y, w, h, area = stats[label] |
|
|
| |
| if h < min_height and w < min_width: |
| cleaned_output[labels == label] = 0 |
| debug_output[labels == label] = [0, 0, 255] |
|
|
| |
| final_result = cv2.bitwise_not(cleaned_output) |
|
|
| |
| output_path = self.get_output_path(output_folder, file_name) |
| debug_path = self.get_output_path(output_folder, file_name.replace(".jpg", "_debug.jpg")) |
|
|
| cv2.imwrite(output_path, final_result) |
| cv2.imwrite(debug_path, debug_output) |
|
|
| return output_path |
|
|
|
|
| def connect_horizontal_vertical_gaps(self, image_path, file_name='connected_output.jpg', output_folder=None): |
| output_folder = output_folder or self.config.output_folder |
|
|
| image = cv2.imread(image_path) |
| height, width = image.shape[:2] |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) |
| edges = cv2.Canny(gray, 50, 150, apertureSize=3) |
|
|
| |
| lines = cv2.HoughLinesP(edges, 1, np.pi / 180, threshold=50, minLineLength=30, maxLineGap=10) |
|
|
| output = image.copy() |
|
|
| def angle_of_line(x1, y1, x2, y2): |
| return abs(math.degrees(math.atan2(y2 - y1, x2 - x1))) |
|
|
| |
| filtered_lines = [] |
| if lines is not None: |
| for line in lines: |
| x1, y1, x2, y2 = line[0] |
| angle = angle_of_line(x1, y1, x2, y2) |
| min_width = 0 |
| min_height = 0 |
|
|
| if angle < 5: |
| line_width = abs(x2 - x1) |
| if line_width >= min_width: |
| filtered_lines.append([x1, y1, x2, y2]) |
|
|
| elif 85 < angle < 95: |
| line_height = abs(y2 - y1) |
| if line_height >= min_height: |
| filtered_lines.append([x1, y1, x2, y2]) |
|
|
|
|
| |
| merged_lines = [] |
| used = [False] * len(filtered_lines) |
| horizontal_alignment_threshold = 5 |
| horizontal_distance_threshold = width * self.config.min_width_ratio |
| vertical_alignment_threshold = 5 |
| vertical_distance_threshold = height * self.config.min_height_ratio |
| overlap_allowance = 10 |
|
|
| for i in range(len(filtered_lines)): |
| if used[i]: |
| continue |
| x1a, y1a, x2a, y2a = filtered_lines[i] |
| merged = [x1a, y1a, x2a, y2a] |
| used[i] = True |
| for j in range(i + 1, len(filtered_lines)): |
| if used[j]: |
| continue |
| x1b, y1b, x2b, y2b = filtered_lines[j] |
|
|
| |
| if abs(y1a - y2a) < horizontal_alignment_threshold and abs(y1b - y2b) < horizontal_alignment_threshold and abs(y1a - y1b) < horizontal_distance_threshold: |
| if max(x1a, x2a) >= min(x1b, x2b) - overlap_allowance or max(x1b, x2b) >= min(x1a, x2a) - overlap_allowance: |
| merged = [ |
| min(merged[0], merged[2], x1b, x2b), |
| y1a, |
| max(merged[0], merged[2], x1b, x2b), |
| y1a |
| ] |
| used[j] = True |
|
|
| |
| elif abs(x1a - x2a) < vertical_alignment_threshold and abs(x1b - x2b) < vertical_alignment_threshold and abs(x1a - x1b) < vertical_distance_threshold: |
| if max(y1a, y2a) >= min(y1b, y2b) - overlap_allowance or max(y1b, y2b) >= min(y1a, y2a) - overlap_allowance: |
| merged = [ |
| x1a, |
| min(merged[1], merged[3], y1b, y2b), |
| x1a, |
| max(merged[1], merged[3], y1b, y2b) |
| ] |
| used[j] = True |
|
|
|
|
| merged_lines.append(merged) |
|
|
| |
| for x1, y1, x2, y2 in merged_lines: |
| cv2.line(output, (x1, y1), (x2, y2), (0, 0, 0), 20) |
| |
| |
| output_path = self.get_output_path(output_folder, file_name) |
| cv2.imwrite(output_path, output) |
| return output_path |
|
|
| def detect_small_objects_and_set_white(self, image_path, file_name="detect_small_objects_and_set_white.jpg", output_folder=None): |
| output_folder = output_folder or self.config.output_folder |
|
|
| |
| image = cv2.imread(image_path) |
| height, width = image.shape[:2] |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) |
|
|
| |
| _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV) |
|
|
| |
| contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) |
|
|
| |
| output = image.copy() |
| for cnt in contours: |
| x, y, w, h = cv2.boundingRect(cnt) |
|
|
| if h < height * self.config.min_height_ratio and w < width * self.config.min_width_ratio: |
| cv2.rectangle(output, (x, y), (x + w, y + h), (255, 255, 255), -1) |
|
|
| |
| output_path = self.get_output_path(output_folder, file_name) |
| cv2.imwrite(output_path, output) |
| return output_path |
|
|