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]
def ensure_grayscale(im):
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).
def get_binary_mask(image, thresh=128, maxval=255):
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}.

def get_bounding_box(binary_mask):
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.
def detect_roi(image):
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)).

def crop_to_roi(image, roi_bbox):
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.