library.helpers
1from pathlib import Path 2 3import cv2 4 5def ensure_grayscale(im): 6 """ 7 Convert an image to grayscale if it is not already. 8 9 Parameters 10 ---------- 11 im : numpy.ndarray 12 Input image array. Can be either a 2D grayscale image or a 3D color 13 image with 3 channels (BGR/BGRA format). 14 15 Returns 16 ------- 17 numpy.ndarray 18 Grayscale image as a 2D array. 19 20 Raises 21 ------ 22 ValueError 23 If the input image has an unexpected shape (neither 2D nor 3D with 3 channels). 24 25 Examples 26 -------- 27 >>> gray_img = ensure_grayscale(color_image) 28 >>> gray_img = ensure_grayscale(already_gray_image) # Returns unchanged 29 """ 30 if len(im.shape) == 3 and im.shape[2] == 3: 31 gray = cv2.cvtColor(im, cv2.COLOR_BGRA2GRAY) 32 elif len(im.shape) == 2: 33 gray = im 34 else: 35 raise ValueError(f"Unexpected image shape: {image.shape}") 36 37 return gray 38 39def crop_and_save(bbox, image_path, output_path, show=False, dpi=100): 40 """Crop and save a square region from an image. 41 42 Parameters 43 ---------- 44 bbox : tuple 45 ((x1, y1), (x2, y2)) Coordinates of the top-left and bottom-right 46 corners of the crop rectangle. 47 image_path : str or pathlib.Path 48 Path to the source image. 49 output_path : str or pathlib.Path 50 Path where the cropped image will be saved. 51 show : bool, optional 52 If True, display the cropped image. 53 dpi : int, optional 54 DPI to use when displaying the image. 55 56 Returns 57 ------- 58 pathlib.Path 59 Path to the saved cropped image. 60 61 Raises 62 ------ 63 ValueError 64 If bbox is not a valid rectangle (e.g. zero or negative area). 65 """ 66 67 im = cv2.imread(str(image_path)) 68 69 (x1, y1), (x2, y2) = bbox 70 71 cropped = im[y1:y2, x1:x2] 72 73 output_path = Path(output_path) 74 output_path.parent.mkdir(exist_ok=True) 75 cv2.imwrite(str(output_path), cropped) 76 77 if show: 78 plt.figure(dpi=dpi) 79 plt.imshow(cropped) 80 plt.title(str(output_path)) 81 plt.show() 82 return output_path 83 84 85def get_mask(img_path, thresh=128, maxval=255, show=False, dpi=300): 86 """Return a binary mask for an image using Otsu's thresholding. 87 88 Parameters 89 ---------- 90 img_path : str or pathlib.Path 91 Path to the input image. 92 thresh : int, optional 93 Initial threshold value (ignored when using Otsu's method). 94 maxval : int, optional 95 Value to assign to pixels greater than the threshold. 96 show : bool, optional 97 If True, display the resulting mask. 98 dpi : int, optional 99 DPI to use when displaying the image. 100 101 Returns 102 ------- 103 ret : float 104 Computed threshold value (from Otsu's method when requested). 105 binary_mask : numpy.ndarray 106 Binary mask image (dtype uint8) with values in {0, maxval}. 107 """ 108 109 im = cv2.imread(img_path) 110 im = ensure_grayscale(im) 111 ret, binary_mask = cv2.threshold(im, thresh=thresh, maxval=maxval, type=cv2.THRESH_BINARY + cv2.THRESH_OTSU) 112 if show: 113 plt.figure(dpi=dpi) 114 plt.imshow(binary_mask, cmap='gray') 115 plt.show() 116 return ret, binary_mask 117 118 119# + 120def get_bounding_box(binary_mask): 121 """Compute a square bounding box around the largest contour in a binary mask. 122 123 Parameters 124 ---------- 125 binary_mask : numpy.ndarray 126 2D binary image (dtype uint8) where foreground pixels are non-zero (e.g. 255). 127 128 Returns 129 ------- 130 tuple 131 A pair of 2-tuples ((x1, y1), (x2, y2)) giving the top-left and 132 bottom-right coordinates of a square bounding box that encloses the 133 largest contour. Coordinates are clamped to the image boundaries (the 134 function uses the module-level `im` array for image shape). 135 136 Raises 137 ------ 138 ValueError 139 If no contours are found in the provided mask. 140 """ 141 contours, hiearchy = cv2.findContours(binary_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 142 143 # get the biggest contour 144 max_contour = max(contours, key=cv2.contourArea) 145 146 # get the x, y, width, height of the contour 147 x, y, w, h = cv2.boundingRect(max_contour) 148 149 # get the longest side 150 size = max(w, h) 151 152 # find the center of each side 153 center_x = x + w // 2 154 center_y = y + h // 2 155 156 # calculate the new x, y based on the longest side 157 new_x = center_x - size // 2 158 new_y = center_y - size // 2 159 160 # make sure it is inside the borders of the image 161 new_x = min(new_x, binary_mask.shape[1] - size) 162 new_y = min(new_y, binary_mask.shape[0] - size) 163 164 return((new_x, new_y), (new_x + size, new_y + size))
6def ensure_grayscale(im): 7 """ 8 Convert an image to grayscale if it is not already. 9 10 Parameters 11 ---------- 12 im : numpy.ndarray 13 Input image array. Can be either a 2D grayscale image or a 3D color 14 image with 3 channels (BGR/BGRA format). 15 16 Returns 17 ------- 18 numpy.ndarray 19 Grayscale image as a 2D array. 20 21 Raises 22 ------ 23 ValueError 24 If the input image has an unexpected shape (neither 2D nor 3D with 3 channels). 25 26 Examples 27 -------- 28 >>> gray_img = ensure_grayscale(color_image) 29 >>> gray_img = ensure_grayscale(already_gray_image) # Returns unchanged 30 """ 31 if len(im.shape) == 3 and im.shape[2] == 3: 32 gray = cv2.cvtColor(im, cv2.COLOR_BGRA2GRAY) 33 elif len(im.shape) == 2: 34 gray = im 35 else: 36 raise ValueError(f"Unexpected image shape: {image.shape}") 37 38 return gray
Convert an image to grayscale if it is not already.
Parameters
im : numpy.ndarray Input image array. Can be either a 2D grayscale image or a 3D color image with 3 channels (BGR/BGRA format).
Returns
numpy.ndarray Grayscale image as a 2D array.
Raises
ValueError If the input image has an unexpected shape (neither 2D nor 3D with 3 channels).
Examples
>>> gray_img = ensure_grayscale(color_image)
>>> gray_img = ensure_grayscale(already_gray_image) # Returns unchanged
40def crop_and_save(bbox, image_path, output_path, show=False, dpi=100): 41 """Crop and save a square region from an image. 42 43 Parameters 44 ---------- 45 bbox : tuple 46 ((x1, y1), (x2, y2)) Coordinates of the top-left and bottom-right 47 corners of the crop rectangle. 48 image_path : str or pathlib.Path 49 Path to the source image. 50 output_path : str or pathlib.Path 51 Path where the cropped image will be saved. 52 show : bool, optional 53 If True, display the cropped image. 54 dpi : int, optional 55 DPI to use when displaying the image. 56 57 Returns 58 ------- 59 pathlib.Path 60 Path to the saved cropped image. 61 62 Raises 63 ------ 64 ValueError 65 If bbox is not a valid rectangle (e.g. zero or negative area). 66 """ 67 68 im = cv2.imread(str(image_path)) 69 70 (x1, y1), (x2, y2) = bbox 71 72 cropped = im[y1:y2, x1:x2] 73 74 output_path = Path(output_path) 75 output_path.parent.mkdir(exist_ok=True) 76 cv2.imwrite(str(output_path), cropped) 77 78 if show: 79 plt.figure(dpi=dpi) 80 plt.imshow(cropped) 81 plt.title(str(output_path)) 82 plt.show() 83 return output_path
Crop and save a square region from an image.
Parameters
bbox : tuple ((x1, y1), (x2, y2)) Coordinates of the top-left and bottom-right corners of the crop rectangle. image_path : str or pathlib.Path Path to the source image. output_path : str or pathlib.Path Path where the cropped image will be saved. show : bool, optional If True, display the cropped image. dpi : int, optional DPI to use when displaying the image.
Returns
pathlib.Path Path to the saved cropped image.
Raises
ValueError If bbox is not a valid rectangle (e.g. zero or negative area).
86def get_mask(img_path, thresh=128, maxval=255, show=False, dpi=300): 87 """Return a binary mask for an image using Otsu's thresholding. 88 89 Parameters 90 ---------- 91 img_path : str or pathlib.Path 92 Path to the input image. 93 thresh : int, optional 94 Initial threshold value (ignored when using Otsu's method). 95 maxval : int, optional 96 Value to assign to pixels greater than the threshold. 97 show : bool, optional 98 If True, display the resulting mask. 99 dpi : int, optional 100 DPI to use when displaying the image. 101 102 Returns 103 ------- 104 ret : float 105 Computed threshold value (from Otsu's method when requested). 106 binary_mask : numpy.ndarray 107 Binary mask image (dtype uint8) with values in {0, maxval}. 108 """ 109 110 im = cv2.imread(img_path) 111 im = ensure_grayscale(im) 112 ret, binary_mask = cv2.threshold(im, thresh=thresh, maxval=maxval, type=cv2.THRESH_BINARY + cv2.THRESH_OTSU) 113 if show: 114 plt.figure(dpi=dpi) 115 plt.imshow(binary_mask, cmap='gray') 116 plt.show() 117 return ret, binary_mask
Return a binary mask for an image using Otsu's thresholding.
Parameters
img_path : str or pathlib.Path Path to the input image. thresh : int, optional Initial threshold value (ignored when using Otsu's method). maxval : int, optional Value to assign to pixels greater than the threshold. show : bool, optional If True, display the resulting mask. dpi : int, optional DPI to use when displaying the image.
Returns
ret : float Computed threshold value (from Otsu's method when requested). binary_mask : numpy.ndarray Binary mask image (dtype uint8) with values in {0, maxval}.
121def get_bounding_box(binary_mask): 122 """Compute a square bounding box around the largest contour in a binary mask. 123 124 Parameters 125 ---------- 126 binary_mask : numpy.ndarray 127 2D binary image (dtype uint8) where foreground pixels are non-zero (e.g. 255). 128 129 Returns 130 ------- 131 tuple 132 A pair of 2-tuples ((x1, y1), (x2, y2)) giving the top-left and 133 bottom-right coordinates of a square bounding box that encloses the 134 largest contour. Coordinates are clamped to the image boundaries (the 135 function uses the module-level `im` array for image shape). 136 137 Raises 138 ------ 139 ValueError 140 If no contours are found in the provided mask. 141 """ 142 contours, hiearchy = cv2.findContours(binary_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 143 144 # get the biggest contour 145 max_contour = max(contours, key=cv2.contourArea) 146 147 # get the x, y, width, height of the contour 148 x, y, w, h = cv2.boundingRect(max_contour) 149 150 # get the longest side 151 size = max(w, h) 152 153 # find the center of each side 154 center_x = x + w // 2 155 center_y = y + h // 2 156 157 # calculate the new x, y based on the longest side 158 new_x = center_x - size // 2 159 new_y = center_y - size // 2 160 161 # make sure it is inside the borders of the image 162 new_x = min(new_x, binary_mask.shape[1] - size) 163 new_y = min(new_y, binary_mask.shape[0] - size) 164 165 return((new_x, new_y), (new_x + size, new_y + size))
Compute a square bounding box around the largest contour in a binary mask.
Parameters
binary_mask : numpy.ndarray 2D binary image (dtype uint8) where foreground pixels are non-zero (e.g. 255).
Returns
tuple
A pair of 2-tuples ((x1, y1), (x2, y2)) giving the top-left and
bottom-right coordinates of a square bounding box that encloses the
largest contour. Coordinates are clamped to the image boundaries (the
function uses the module-level im array for image shape).
Raises
ValueError If no contours are found in the provided mask.