library.mask_processing
1from pathlib import Path 2import cv2 3import numpy as np 4import matplotlib.pyplot as plt 5 6from scipy.spatial.distance import cdist 7import skimage.measure 8 9 10 11 12def load_mask(mask_path): 13 """Load a binary mask from a PNG file. 14 15 Args: 16 mask_path: Path to the mask file (str or Path object). 17 18 Returns: 19 Binary mask as numpy array with values 0 (background) and 255 (foreground). 20 21 Raises: 22 FileNotFoundError: If the mask file cannot be loaded. 23 """ 24 mask = cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE) 25 if mask is None: 26 raise FileNotFoundError(f"Could not load mask from {mask_path}") 27 return mask 28 29def repair_root_mask_edges(mask_path, 30 min_component_size=20, 31 max_edge_distance=50, 32 max_iterations=10, 33 closing_kernel_height=5, 34 closing_kernel_width=3, 35 closing_iterations=2, 36 save_path=None, 37 visualize=False): 38 """Repair root mask gaps using edge-to-edge distance measurements. 39 40 Uses iterative edge-based gap filling to connect fragmented root components. 41 Measures minimum distance between component edges rather than centroids, 42 which is more accurate for irregular shapes like roots. 43 44 Args: 45 mask_path: Path to root mask file (str or Path). 46 min_component_size: Minimum component area in pixels to consider for connection. 47 max_edge_distance: Maximum pixel distance between edges to connect components. 48 max_iterations: Maximum number of gap-filling iterations. 49 closing_kernel_height: Height of initial morphological closing kernel. 50 closing_kernel_width: Width of initial morphological closing kernel. 51 closing_iterations: Number of initial closing iterations. 52 save_path: Optional path to save repaired mask. If None, does not save. 53 visualize: If True, displays before/after visualization with original image. 54 55 Returns: 56 Binary mask array (0 and 255) with repaired gaps. 57 58 Example: 59 repaired_mask = repair_root_mask_edges( 60 'root_mask.png', 61 max_edge_distance=50, 62 visualize=True 63 ) 64 """ 65 66 # Load mask 67 mask = load_mask(mask_path) 68 69 # Initial morphological closing 70 kernel = np.ones((closing_kernel_height, closing_kernel_width), np.uint8) 71 closed_mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, 72 iterations=closing_iterations) 73 74 repaired_mask = closed_mask.copy() 75 76 # Iterative edge-based gap filling 77 for iteration in range(max_iterations): 78 retval, labels = cv2.connectedComponents(repaired_mask) 79 regions = skimage.measure.regionprops(labels) 80 81 # Get substantial components with their edge pixels 82 components = [] 83 for region in regions: 84 if region.area < min_component_size: 85 continue 86 87 coords = region.coords 88 min_y = coords[:, 0].min() 89 max_y = coords[:, 0].max() 90 91 # Get bottom and top edge pixels 92 bottom_edge = coords[coords[:, 0] == max_y] 93 top_edge = coords[coords[:, 0] == min_y] 94 95 components.append({ 96 'label': region.label, 97 'area': region.area, 98 'min_y': min_y, 99 'max_y': max_y, 100 'bottom_edge': bottom_edge, 101 'top_edge': top_edge 102 }) 103 104 connections_made = 0 105 106 # Find and connect close component edges 107 for i, comp1 in enumerate(components): 108 for j, comp2 in enumerate(components): 109 if i >= j: 110 continue 111 112 # Check if comp2 is below comp1 113 if comp1['max_y'] >= comp2['min_y']: 114 continue 115 116 # Calculate minimum distance between edges 117 distances = cdist(comp1['bottom_edge'], comp2['top_edge']) 118 min_distance = distances.min() 119 min_idx = np.unravel_index(distances.argmin(), distances.shape) 120 121 # Get the closest points 122 closest_bottom = comp1['bottom_edge'][min_idx[0]] 123 closest_top = comp2['top_edge'][min_idx[1]] 124 125 if min_distance <= max_edge_distance: 126 # Draw line between closest points 127 cv2.line(repaired_mask, 128 (closest_bottom[1], closest_bottom[0]), 129 (closest_top[1], closest_top[0]), 130 255, 3) 131 connections_made += 1 132 133 # Apply closing to smooth connections 134 kernel_smooth = np.ones((5, 3), np.uint8) 135 repaired_mask = cv2.morphologyEx(repaired_mask, cv2.MORPH_CLOSE, 136 kernel_smooth, iterations=1) 137 138 # Stop if no connections were made 139 if connections_made == 0: 140 break 141 142 # Save if path provided 143 if save_path is not None: 144 cv2.imwrite(str(save_path), repaired_mask) 145 146 # Visualize if requested 147 if visualize: 148 mask_path_obj = Path(mask_path) 149 mask_name = mask_path_obj.stem 150 image_num = mask_name.replace('test_image_', '').replace('_root', '') 151 152 # Try to find original image 153 images_path = mask_path_obj.parent.parent.parent / 'Kaggle' 154 original_path = images_path / f'test_image_{image_num}.png' 155 156 retval_before = cv2.connectedComponents(closed_mask)[0] 157 retval_after, labels_after = cv2.connectedComponents(repaired_mask) 158 159 fig, axes = plt.subplots(2, 1, figsize=(15, 30)) 160 161 axes[0].imshow(labels_after, cmap='gist_ncar') 162 axes[0].set_title(f'After Edge-based Repair: {retval_after - 1} components\n' 163 f'(started with {retval_before - 1})', fontsize=16) 164 axes[0].axis('off') 165 166 if original_path.exists(): 167 original = cv2.imread(str(original_path)) 168 original_rgb = cv2.cvtColor(original, cv2.COLOR_BGR2RGB) 169 axes[1].imshow(original_rgb) 170 axes[1].imshow(labels_after, cmap='gist_ncar', alpha=0.4) 171 axes[1].set_title('Overlay with Original Image', fontsize=16) 172 else: 173 axes[1].imshow(labels_after, cmap='gist_ncar') 174 axes[1].set_title('Repaired Mask (no original image found)', fontsize=16) 175 176 axes[1].axis('off') 177 178 plt.tight_layout() 179 plt.show() 180 181 return repaired_mask
def
load_mask(mask_path):
13def load_mask(mask_path): 14 """Load a binary mask from a PNG file. 15 16 Args: 17 mask_path: Path to the mask file (str or Path object). 18 19 Returns: 20 Binary mask as numpy array with values 0 (background) and 255 (foreground). 21 22 Raises: 23 FileNotFoundError: If the mask file cannot be loaded. 24 """ 25 mask = cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE) 26 if mask is None: 27 raise FileNotFoundError(f"Could not load mask from {mask_path}") 28 return mask
Load a binary mask from a PNG file.
Arguments:
- mask_path: Path to the mask file (str or Path object).
Returns:
Binary mask as numpy array with values 0 (background) and 255 (foreground).
Raises:
- FileNotFoundError: If the mask file cannot be loaded.
def
repair_root_mask_edges( mask_path, min_component_size=20, max_edge_distance=50, max_iterations=10, closing_kernel_height=5, closing_kernel_width=3, closing_iterations=2, save_path=None, visualize=False):
30def repair_root_mask_edges(mask_path, 31 min_component_size=20, 32 max_edge_distance=50, 33 max_iterations=10, 34 closing_kernel_height=5, 35 closing_kernel_width=3, 36 closing_iterations=2, 37 save_path=None, 38 visualize=False): 39 """Repair root mask gaps using edge-to-edge distance measurements. 40 41 Uses iterative edge-based gap filling to connect fragmented root components. 42 Measures minimum distance between component edges rather than centroids, 43 which is more accurate for irregular shapes like roots. 44 45 Args: 46 mask_path: Path to root mask file (str or Path). 47 min_component_size: Minimum component area in pixels to consider for connection. 48 max_edge_distance: Maximum pixel distance between edges to connect components. 49 max_iterations: Maximum number of gap-filling iterations. 50 closing_kernel_height: Height of initial morphological closing kernel. 51 closing_kernel_width: Width of initial morphological closing kernel. 52 closing_iterations: Number of initial closing iterations. 53 save_path: Optional path to save repaired mask. If None, does not save. 54 visualize: If True, displays before/after visualization with original image. 55 56 Returns: 57 Binary mask array (0 and 255) with repaired gaps. 58 59 Example: 60 repaired_mask = repair_root_mask_edges( 61 'root_mask.png', 62 max_edge_distance=50, 63 visualize=True 64 ) 65 """ 66 67 # Load mask 68 mask = load_mask(mask_path) 69 70 # Initial morphological closing 71 kernel = np.ones((closing_kernel_height, closing_kernel_width), np.uint8) 72 closed_mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, 73 iterations=closing_iterations) 74 75 repaired_mask = closed_mask.copy() 76 77 # Iterative edge-based gap filling 78 for iteration in range(max_iterations): 79 retval, labels = cv2.connectedComponents(repaired_mask) 80 regions = skimage.measure.regionprops(labels) 81 82 # Get substantial components with their edge pixels 83 components = [] 84 for region in regions: 85 if region.area < min_component_size: 86 continue 87 88 coords = region.coords 89 min_y = coords[:, 0].min() 90 max_y = coords[:, 0].max() 91 92 # Get bottom and top edge pixels 93 bottom_edge = coords[coords[:, 0] == max_y] 94 top_edge = coords[coords[:, 0] == min_y] 95 96 components.append({ 97 'label': region.label, 98 'area': region.area, 99 'min_y': min_y, 100 'max_y': max_y, 101 'bottom_edge': bottom_edge, 102 'top_edge': top_edge 103 }) 104 105 connections_made = 0 106 107 # Find and connect close component edges 108 for i, comp1 in enumerate(components): 109 for j, comp2 in enumerate(components): 110 if i >= j: 111 continue 112 113 # Check if comp2 is below comp1 114 if comp1['max_y'] >= comp2['min_y']: 115 continue 116 117 # Calculate minimum distance between edges 118 distances = cdist(comp1['bottom_edge'], comp2['top_edge']) 119 min_distance = distances.min() 120 min_idx = np.unravel_index(distances.argmin(), distances.shape) 121 122 # Get the closest points 123 closest_bottom = comp1['bottom_edge'][min_idx[0]] 124 closest_top = comp2['top_edge'][min_idx[1]] 125 126 if min_distance <= max_edge_distance: 127 # Draw line between closest points 128 cv2.line(repaired_mask, 129 (closest_bottom[1], closest_bottom[0]), 130 (closest_top[1], closest_top[0]), 131 255, 3) 132 connections_made += 1 133 134 # Apply closing to smooth connections 135 kernel_smooth = np.ones((5, 3), np.uint8) 136 repaired_mask = cv2.morphologyEx(repaired_mask, cv2.MORPH_CLOSE, 137 kernel_smooth, iterations=1) 138 139 # Stop if no connections were made 140 if connections_made == 0: 141 break 142 143 # Save if path provided 144 if save_path is not None: 145 cv2.imwrite(str(save_path), repaired_mask) 146 147 # Visualize if requested 148 if visualize: 149 mask_path_obj = Path(mask_path) 150 mask_name = mask_path_obj.stem 151 image_num = mask_name.replace('test_image_', '').replace('_root', '') 152 153 # Try to find original image 154 images_path = mask_path_obj.parent.parent.parent / 'Kaggle' 155 original_path = images_path / f'test_image_{image_num}.png' 156 157 retval_before = cv2.connectedComponents(closed_mask)[0] 158 retval_after, labels_after = cv2.connectedComponents(repaired_mask) 159 160 fig, axes = plt.subplots(2, 1, figsize=(15, 30)) 161 162 axes[0].imshow(labels_after, cmap='gist_ncar') 163 axes[0].set_title(f'After Edge-based Repair: {retval_after - 1} components\n' 164 f'(started with {retval_before - 1})', fontsize=16) 165 axes[0].axis('off') 166 167 if original_path.exists(): 168 original = cv2.imread(str(original_path)) 169 original_rgb = cv2.cvtColor(original, cv2.COLOR_BGR2RGB) 170 axes[1].imshow(original_rgb) 171 axes[1].imshow(labels_after, cmap='gist_ncar', alpha=0.4) 172 axes[1].set_title('Overlay with Original Image', fontsize=16) 173 else: 174 axes[1].imshow(labels_after, cmap='gist_ncar') 175 axes[1].set_title('Repaired Mask (no original image found)', fontsize=16) 176 177 axes[1].axis('off') 178 179 plt.tight_layout() 180 plt.show() 181 182 return repaired_mask
Repair root mask gaps using edge-to-edge distance measurements.
Uses iterative edge-based gap filling to connect fragmented root components. Measures minimum distance between component edges rather than centroids, which is more accurate for irregular shapes like roots.
Arguments:
- mask_path: Path to root mask file (str or Path).
- min_component_size: Minimum component area in pixels to consider for connection.
- max_edge_distance: Maximum pixel distance between edges to connect components.
- max_iterations: Maximum number of gap-filling iterations.
- closing_kernel_height: Height of initial morphological closing kernel.
- closing_kernel_width: Width of initial morphological closing kernel.
- closing_iterations: Number of initial closing iterations.
- save_path: Optional path to save repaired mask. If None, does not save.
- visualize: If True, displays before/after visualization with original image.
Returns:
Binary mask array (0 and 255) with repaired gaps.
Example:
repaired_mask = repair_root_mask_edges( 'root_mask.png', max_edge_distance=50, visualize=True )