library.roi
Functions for detecting and working with regions of interest (ROI) in images.
1# library/roi.py 2"""Functions for detecting and working with regions of interest (ROI) in images.""" 3 4from pathlib import Path 5import cv2 6import numpy as np 7 8 9def ensure_grayscale(im): 10 """Convert an image to grayscale if it is not already. 11 12 Args: 13 im: Input image array. Can be either a 2D grayscale image or a 3D color 14 image with 3 channels (BGR format). 15 16 Returns: 17 Grayscale image as a 2D array. 18 19 Raises: 20 ValueError: If the input image has an unexpected shape (neither 2D nor 3D 21 with 3 channels). 22 """ 23 if len(im.shape) == 3 and im.shape[2] == 3: 24 gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY) 25 elif len(im.shape) == 2: 26 gray = im 27 else: 28 raise ValueError(f"Unexpected image shape: {im.shape}") 29 30 return gray 31 32 33def get_binary_mask(image, thresh=128, maxval=255): 34 """Create a binary mask using Otsu's thresholding. 35 36 Args: 37 image: Input image (can be color or grayscale). 38 thresh: Initial threshold value (ignored when using Otsu's method). 39 Default is 128. 40 maxval: Value to assign to pixels greater than the threshold. Default is 255. 41 42 Returns: 43 Tuple of (threshold_value, binary_mask) where threshold_value is the computed 44 threshold from Otsu's method and binary_mask is a uint8 array with values 45 in {0, maxval}. 46 """ 47 gray = ensure_grayscale(image) 48 ret, binary_mask = cv2.threshold( 49 gray, thresh=thresh, maxval=maxval, 50 type=cv2.THRESH_BINARY + cv2.THRESH_OTSU 51 ) 52 return ret, binary_mask 53 54 55def get_bounding_box(binary_mask): 56 """Compute a square bounding box around the largest contour in a binary mask. 57 58 Args: 59 binary_mask: 2D binary image (dtype uint8) where foreground pixels are 60 non-zero (e.g. 255). 61 62 Returns: 63 Tuple of ((x1, y1), (x2, y2)) giving the top-left and bottom-right 64 coordinates of a square bounding box that encloses the largest contour. 65 Coordinates are clamped to the image boundaries. 66 67 Raises: 68 ValueError: If no contours are found in the provided mask. 69 """ 70 contours, _ = cv2.findContours( 71 binary_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE 72 ) 73 74 if not contours: 75 raise ValueError("No contours found in binary mask") 76 77 # Get the biggest contour 78 max_contour = max(contours, key=cv2.contourArea) 79 80 # Get the x, y, width, height of the contour 81 x, y, w, h = cv2.boundingRect(max_contour) 82 83 # Get the longest side 84 size = max(w, h) 85 86 # Find the center of each side 87 center_x = x + w // 2 88 center_y = y + h // 2 89 90 # Calculate the new x, y based on the longest side 91 new_x = center_x - size // 2 92 new_y = center_y - size // 2 93 94 # Clamp to image boundaries 95 new_x = max(0, min(new_x, binary_mask.shape[1] - size)) 96 new_y = max(0, min(new_y, binary_mask.shape[0] - size)) 97 98 return ((new_x, new_y), (new_x + size, new_y + size)) 99 100 101def detect_roi(image): 102 """Detect the ROI (e.g., petri dish) bounding box in an image. 103 104 Args: 105 image: Input image (BGR color or grayscale). 106 107 Returns: 108 ROI bounding box as ((x1, y1), (x2, y2)). 109 """ 110 _, binary_mask = get_binary_mask(image) 111 roi_bbox = get_bounding_box(binary_mask) 112 return roi_bbox 113 114def crop_to_roi(image, roi_bbox): 115 """Crop an image to its ROI bounding box. 116 117 Args: 118 image: Input image (can be color or grayscale). 119 roi_bbox: ((x1, y1), (x2, y2)) from detect_roi. 120 121 Returns: 122 Cropped image. 123 """ 124 (x1, y1), (x2, y2) = roi_bbox 125 return image[y1:y2, x1:x2]
10def ensure_grayscale(im): 11 """Convert an image to grayscale if it is not already. 12 13 Args: 14 im: Input image array. Can be either a 2D grayscale image or a 3D color 15 image with 3 channels (BGR format). 16 17 Returns: 18 Grayscale image as a 2D array. 19 20 Raises: 21 ValueError: If the input image has an unexpected shape (neither 2D nor 3D 22 with 3 channels). 23 """ 24 if len(im.shape) == 3 and im.shape[2] == 3: 25 gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY) 26 elif len(im.shape) == 2: 27 gray = im 28 else: 29 raise ValueError(f"Unexpected image shape: {im.shape}") 30 31 return gray
Convert an image to grayscale if it is not already.
Arguments:
- im: Input image array. Can be either a 2D grayscale image or a 3D color image with 3 channels (BGR format).
Returns:
Grayscale image as a 2D array.
Raises:
- ValueError: If the input image has an unexpected shape (neither 2D nor 3D with 3 channels).
34def get_binary_mask(image, thresh=128, maxval=255): 35 """Create a binary mask using Otsu's thresholding. 36 37 Args: 38 image: Input image (can be color or grayscale). 39 thresh: Initial threshold value (ignored when using Otsu's method). 40 Default is 128. 41 maxval: Value to assign to pixels greater than the threshold. Default is 255. 42 43 Returns: 44 Tuple of (threshold_value, binary_mask) where threshold_value is the computed 45 threshold from Otsu's method and binary_mask is a uint8 array with values 46 in {0, maxval}. 47 """ 48 gray = ensure_grayscale(image) 49 ret, binary_mask = cv2.threshold( 50 gray, thresh=thresh, maxval=maxval, 51 type=cv2.THRESH_BINARY + cv2.THRESH_OTSU 52 ) 53 return ret, binary_mask
Create a binary mask using Otsu's thresholding.
Arguments:
- image: Input image (can be color or grayscale).
- thresh: Initial threshold value (ignored when using Otsu's method). Default is 128.
- maxval: Value to assign to pixels greater than the threshold. Default is 255.
Returns:
Tuple of (threshold_value, binary_mask) where threshold_value is the computed threshold from Otsu's method and binary_mask is a uint8 array with values in {0, maxval}.
56def get_bounding_box(binary_mask): 57 """Compute a square bounding box around the largest contour in a binary mask. 58 59 Args: 60 binary_mask: 2D binary image (dtype uint8) where foreground pixels are 61 non-zero (e.g. 255). 62 63 Returns: 64 Tuple of ((x1, y1), (x2, y2)) giving the top-left and bottom-right 65 coordinates of a square bounding box that encloses the largest contour. 66 Coordinates are clamped to the image boundaries. 67 68 Raises: 69 ValueError: If no contours are found in the provided mask. 70 """ 71 contours, _ = cv2.findContours( 72 binary_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE 73 ) 74 75 if not contours: 76 raise ValueError("No contours found in binary mask") 77 78 # Get the biggest contour 79 max_contour = max(contours, key=cv2.contourArea) 80 81 # Get the x, y, width, height of the contour 82 x, y, w, h = cv2.boundingRect(max_contour) 83 84 # Get the longest side 85 size = max(w, h) 86 87 # Find the center of each side 88 center_x = x + w // 2 89 center_y = y + h // 2 90 91 # Calculate the new x, y based on the longest side 92 new_x = center_x - size // 2 93 new_y = center_y - size // 2 94 95 # Clamp to image boundaries 96 new_x = max(0, min(new_x, binary_mask.shape[1] - size)) 97 new_y = max(0, min(new_y, binary_mask.shape[0] - size)) 98 99 return ((new_x, new_y), (new_x + size, new_y + size))
Compute a square bounding box around the largest contour in a binary mask.
Arguments:
- binary_mask: 2D binary image (dtype uint8) where foreground pixels are non-zero (e.g. 255).
Returns:
Tuple of ((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.
Raises:
- ValueError: If no contours are found in the provided mask.
102def detect_roi(image): 103 """Detect the ROI (e.g., petri dish) bounding box in an image. 104 105 Args: 106 image: Input image (BGR color or grayscale). 107 108 Returns: 109 ROI bounding box as ((x1, y1), (x2, y2)). 110 """ 111 _, binary_mask = get_binary_mask(image) 112 roi_bbox = get_bounding_box(binary_mask) 113 return roi_bbox
Detect the ROI (e.g., petri dish) bounding box in an image.
Arguments:
- image: Input image (BGR color or grayscale).
Returns:
ROI bounding box as ((x1, y1), (x2, y2)).
115def crop_to_roi(image, roi_bbox): 116 """Crop an image to its ROI bounding box. 117 118 Args: 119 image: Input image (can be color or grayscale). 120 roi_bbox: ((x1, y1), (x2, y2)) from detect_roi. 121 122 Returns: 123 Cropped image. 124 """ 125 (x1, y1), (x2, y2) = roi_bbox 126 return image[y1:y2, x1:x2]
Crop an image to its ROI bounding box.
Arguments:
- image: Input image (can be color or grayscale).
- roi_bbox: ((x1, y1), (x2, y2)) from detect_roi.
Returns:
Cropped image.