Module wtracker.eval.error_calculator
View Source
from typing import Collection
import numpy as np
import cv2 as cv
from tqdm.auto import tqdm
from typing import Callable
from wtracker.utils.frame_reader import FrameReader
from wtracker.utils.bbox_utils import BoxUtils, BoxConverter, BoxFormat
class ErrorCalculator:
"""
The ErrorCalculator class provides methods to calculate different types of errors based on worm position and the microscope view.
"""
# TODO: Kinda a weird solution, but it works for now. Maybe find a better way to do this.
probe_hook: Callable[[np.ndarray, np.ndarray], None] = None # takes mask and view for testing
@staticmethod
def calculate_segmentation(
bbox: np.ndarray,
image: np.ndarray,
background: np.ndarray,
diff_thresh: float,
) -> np.ndarray:
"""
Calculates the segmentation error between a view and background image.
Args:
bbox (np.ndarray): The bounding box of the image, in the format (x, y, w, h).
image (np.ndarray): The image to calculate segmentation from.
background (np.ndarray): The background image.
diff_thresh (float): The difference threshold to distinguish foreground and background objects from.
Returns:
np.ndarray: The segmentation mask.
Raises:
ValueError: If the image is not grayscale or color.
"""
x, y, w, h = bbox
assert image.shape[:2] == (h, w)
bg_view = background[y : y + h, x : x + w]
diff = np.abs(image.astype(np.int32) - bg_view.astype(np.int32)).astype(np.uint8)
# if images are color, convert to grayscale
if diff.ndim == 3 and diff.shape[2] == 3:
diff = cv.cvtColor(diff, cv.COLOR_BGR2GRAY)
if diff.ndim != 2:
raise ValueError("Image must be either a gray or a color image.")
mask_wrm = diff > diff_thresh
return mask_wrm
# TODO: VERY FAST FOR ME, INVESTIGATE WHY IT'S SLOW IN THE LAB
# TODO: swap the FrameReader to another type. The only requirement is that accessing frame index returns the correct frame.
# we should probably use something like ImageLoader, which is implemented in the analysis_experimental.
@staticmethod
def calculate_precise(
background: np.ndarray,
worm_bboxes: np.ndarray,
mic_bboxes: np.ndarray,
frame_nums: np.ndarray,
worm_reader: FrameReader,
diff_thresh: float = 10,
) -> np.ndarray:
"""
Calculates the precise error for each frame in the given sequence.
This error is based on precise segmentation of the worm object from the frame, and
determining the exact proportion of worm's body outside the microscope view.
Args:
background (np.ndarray): The background image.
worm_bboxes: A numpy array of shape (N, 4) representing the bounding boxes of worms. The bounding boxes should be in the format (x, y, w, h).
mic_bboxes: A numpy array of shape (N, 4) representing the bounding boxes of the microscope. The bounding boxes should be in the format (x, y, w, h).
frame_nums (np.ndarray): An array of frame numbers to calculate the error for.
worm_reader (FrameReader): A frame reader containing segmented worm images for each frame. These worm images should match the shape of the worm bounding boxes.
Frames passed in frame_nums are read from this reader by index.
diff_thresh (float, optional): The difference threshold to distinguish foreground and background objects from.
A foreground object is detected if the pixel value difference with the background is greater than this threshold.
Returns:
np.ndarray: Array of errors of shape (N,) representing the precise segmentation error for each frame.
Raises:
AssertionError: If the length of frame_nums, worm_bboxes, and mic_bboxes do not match.
"""
assert frame_nums.ndim == 1
assert len(frame_nums) == worm_bboxes.shape[0] == mic_bboxes.shape[0]
errors = np.zeros(len(frame_nums), dtype=float)
bounds = background.shape[:2]
worm_bboxes, is_legal = BoxUtils.discretize(worm_bboxes, bounds=bounds, box_format=BoxFormat.XYWH)
mic_bboxes, _ = BoxUtils.discretize(mic_bboxes, bounds=bounds, box_format=BoxFormat.XYWH)
# filter out illegal bboxes, indicting no prediction or bad prediction.
errors[~is_legal] = np.nan
worm_bboxes = worm_bboxes[is_legal]
mic_bboxes = mic_bboxes[is_legal]
frame_nums = frame_nums[is_legal]
# convert to xyxy format for intersection calculation
worm_bboxes = BoxConverter.change_format(worm_bboxes, BoxFormat.XYWH, BoxFormat.XYXY)
mic_bboxes = BoxConverter.change_format(mic_bboxes, BoxFormat.XYWH, BoxFormat.XYXY)
wrm_left, wrm_top, wrm_right, wrm_bottom = BoxUtils.unpack(worm_bboxes)
mic_left, mic_top, mic_right, mic_bottom = BoxUtils.unpack(mic_bboxes)
# calculate intersection of worm and microscope bounding boxes
int_left = np.maximum(wrm_left, mic_left)
int_top = np.maximum(wrm_top, mic_top)
int_right = np.minimum(wrm_right, mic_right)
int_bottom = np.minimum(wrm_bottom, mic_bottom)
int_width = np.maximum(0, int_right - int_left)
int_height = np.maximum(0, int_bottom - int_top)
# shift the intersection to the worm view coordinates
int_left -= wrm_left
int_top -= wrm_top
# pack the intersection bounding boxes and convert to xywh format
int_bboxes = BoxUtils.pack(int_left, int_top, int_width, int_height)
worm_bboxes = BoxConverter.change_format(worm_bboxes, BoxFormat.XYXY, BoxFormat.XYWH)
mic_bboxes = BoxConverter.change_format(mic_bboxes, BoxFormat.XYXY, BoxFormat.XYWH)
for i, frame_num in tqdm(enumerate(frame_nums), total=len(frame_nums), desc="Calculating Error", unit="fr"):
wrm_bbox = worm_bboxes[i]
int_bbox = int_bboxes[i]
worm_view = worm_reader[frame_num]
mask_wrm = ErrorCalculator.calculate_segmentation(
bbox=wrm_bbox,
image=worm_view,
background=background,
diff_thresh=diff_thresh,
)
if ErrorCalculator.probe_hook is not None:
ErrorCalculator.probe_hook(worm_view, mask_wrm)
mask_mic = np.zeros_like(mask_wrm, dtype=bool)
mask_mic[int_bbox[1] : int_bbox[1] + int_bbox[3], int_bbox[0] : int_bbox[0] + int_bbox[2]] = True
total = mask_wrm.sum()
if total == 0:
errors[i] = 0.0
continue
intersection = np.logical_and(mask_wrm, mask_mic).sum()
error = 1.0 - intersection / total
errors[i] = error
return errors
@staticmethod
def calculate_bbox_error(worm_bboxes: np.ndarray, mic_bboxes: np.ndarray) -> np.ndarray:
"""
Calculate the bounding box error between worm bounding boxes and microscope bounding boxes.
This error calculates the proportion of the worm bounding box that is outside the microscope bounding box.
Args:
worm_bboxes: A numpy array of shape (N, 4) representing the bounding boxes of worms. The bounding boxes should be in the format (x, y, w, h).
mic_bboxes: A numpy array of shape (N, 4) representing the bounding boxes of the microscope. The bounding boxes should be in the format (x, y, w, h).
Returns:
np.ndarray: Array of errors of shape (N,) representing the bounding box error for each pair of worm and microscope bounding boxes.
"""
wrm_left, wrm_top, wrm_width, wrm_height = BoxUtils.unpack(worm_bboxes)
mic_left, mic_top, mic_width, mic_height = BoxUtils.unpack(mic_bboxes)
wrm_right, wrm_bottom = wrm_left + wrm_width, wrm_top + wrm_height
mic_right, mic_bottom = mic_left + mic_width, mic_top + mic_height
int_left = np.maximum(wrm_left, mic_left)
int_top = np.maximum(wrm_top, mic_top)
int_right = np.minimum(wrm_right, mic_right)
int_bottom = np.minimum(wrm_bottom, mic_bottom)
int_width = np.maximum(0, int_right - int_left)
int_height = np.maximum(0, int_bottom - int_top)
intersection = int_width * int_height
total = wrm_width * wrm_height
errors = 1.0 - intersection / total
errors[total == 0] = 0.0
return errors
@staticmethod
def calculate_mse_error(worm_bboxes: np.ndarray, mic_bboxes: np.ndarray) -> np.ndarray:
"""
Calculates the Mean Squared Error (MSE) error between the centers of worm bounding boxes and microscope bounding boxes.
Args:
worm_bboxes: A numpy array of shape (N, 4) representing the bounding boxes of worms. The bounding boxes should be in the format (x, y, w, h).
mic_bboxes: A numpy array of shape (N, 4) representing the bounding boxes of the microscope. The bounding boxes should be in the format (x, y, w, h).
Returns:
np.ndarray: Array of errors of shape (N,) representing the MSE error for each pair of worm and microscope bounding boxes.
"""
worm_centers = BoxUtils.center(worm_bboxes)
mic_centers = BoxUtils.center(mic_bboxes)
errors = np.mean((worm_centers - mic_centers) ** 2, axis=1)
return errors
Classes
ErrorCalculator
class ErrorCalculator(
/,
*args,
**kwargs
)
The ErrorCalculator class provides methods to calculate different types of errors based on worm position and the microscope view.
View Source
class ErrorCalculator:
"""
The ErrorCalculator class provides methods to calculate different types of errors based on worm position and the microscope view.
"""
# TODO: Kinda a weird solution, but it works for now. Maybe find a better way to do this.
probe_hook: Callable[[np.ndarray, np.ndarray], None] = None # takes mask and view for testing
@staticmethod
def calculate_segmentation(
bbox: np.ndarray,
image: np.ndarray,
background: np.ndarray,
diff_thresh: float,
) -> np.ndarray:
"""
Calculates the segmentation error between a view and background image.
Args:
bbox (np.ndarray): The bounding box of the image, in the format (x, y, w, h).
image (np.ndarray): The image to calculate segmentation from.
background (np.ndarray): The background image.
diff_thresh (float): The difference threshold to distinguish foreground and background objects from.
Returns:
np.ndarray: The segmentation mask.
Raises:
ValueError: If the image is not grayscale or color.
"""
x, y, w, h = bbox
assert image.shape[:2] == (h, w)
bg_view = background[y : y + h, x : x + w]
diff = np.abs(image.astype(np.int32) - bg_view.astype(np.int32)).astype(np.uint8)
# if images are color, convert to grayscale
if diff.ndim == 3 and diff.shape[2] == 3:
diff = cv.cvtColor(diff, cv.COLOR_BGR2GRAY)
if diff.ndim != 2:
raise ValueError("Image must be either a gray or a color image.")
mask_wrm = diff > diff_thresh
return mask_wrm
# TODO: VERY FAST FOR ME, INVESTIGATE WHY IT'S SLOW IN THE LAB
# TODO: swap the FrameReader to another type. The only requirement is that accessing frame index returns the correct frame.
# we should probably use something like ImageLoader, which is implemented in the analysis_experimental.
@staticmethod
def calculate_precise(
background: np.ndarray,
worm_bboxes: np.ndarray,
mic_bboxes: np.ndarray,
frame_nums: np.ndarray,
worm_reader: FrameReader,
diff_thresh: float = 10,
) -> np.ndarray:
"""
Calculates the precise error for each frame in the given sequence.
This error is based on precise segmentation of the worm object from the frame, and
determining the exact proportion of worm's body outside the microscope view.
Args:
background (np.ndarray): The background image.
worm_bboxes: A numpy array of shape (N, 4) representing the bounding boxes of worms. The bounding boxes should be in the format (x, y, w, h).
mic_bboxes: A numpy array of shape (N, 4) representing the bounding boxes of the microscope. The bounding boxes should be in the format (x, y, w, h).
frame_nums (np.ndarray): An array of frame numbers to calculate the error for.
worm_reader (FrameReader): A frame reader containing segmented worm images for each frame. These worm images should match the shape of the worm bounding boxes.
Frames passed in frame_nums are read from this reader by index.
diff_thresh (float, optional): The difference threshold to distinguish foreground and background objects from.
A foreground object is detected if the pixel value difference with the background is greater than this threshold.
Returns:
np.ndarray: Array of errors of shape (N,) representing the precise segmentation error for each frame.
Raises:
AssertionError: If the length of frame_nums, worm_bboxes, and mic_bboxes do not match.
"""
assert frame_nums.ndim == 1
assert len(frame_nums) == worm_bboxes.shape[0] == mic_bboxes.shape[0]
errors = np.zeros(len(frame_nums), dtype=float)
bounds = background.shape[:2]
worm_bboxes, is_legal = BoxUtils.discretize(worm_bboxes, bounds=bounds, box_format=BoxFormat.XYWH)
mic_bboxes, _ = BoxUtils.discretize(mic_bboxes, bounds=bounds, box_format=BoxFormat.XYWH)
# filter out illegal bboxes, indicting no prediction or bad prediction.
errors[~is_legal] = np.nan
worm_bboxes = worm_bboxes[is_legal]
mic_bboxes = mic_bboxes[is_legal]
frame_nums = frame_nums[is_legal]
# convert to xyxy format for intersection calculation
worm_bboxes = BoxConverter.change_format(worm_bboxes, BoxFormat.XYWH, BoxFormat.XYXY)
mic_bboxes = BoxConverter.change_format(mic_bboxes, BoxFormat.XYWH, BoxFormat.XYXY)
wrm_left, wrm_top, wrm_right, wrm_bottom = BoxUtils.unpack(worm_bboxes)
mic_left, mic_top, mic_right, mic_bottom = BoxUtils.unpack(mic_bboxes)
# calculate intersection of worm and microscope bounding boxes
int_left = np.maximum(wrm_left, mic_left)
int_top = np.maximum(wrm_top, mic_top)
int_right = np.minimum(wrm_right, mic_right)
int_bottom = np.minimum(wrm_bottom, mic_bottom)
int_width = np.maximum(0, int_right - int_left)
int_height = np.maximum(0, int_bottom - int_top)
# shift the intersection to the worm view coordinates
int_left -= wrm_left
int_top -= wrm_top
# pack the intersection bounding boxes and convert to xywh format
int_bboxes = BoxUtils.pack(int_left, int_top, int_width, int_height)
worm_bboxes = BoxConverter.change_format(worm_bboxes, BoxFormat.XYXY, BoxFormat.XYWH)
mic_bboxes = BoxConverter.change_format(mic_bboxes, BoxFormat.XYXY, BoxFormat.XYWH)
for i, frame_num in tqdm(enumerate(frame_nums), total=len(frame_nums), desc="Calculating Error", unit="fr"):
wrm_bbox = worm_bboxes[i]
int_bbox = int_bboxes[i]
worm_view = worm_reader[frame_num]
mask_wrm = ErrorCalculator.calculate_segmentation(
bbox=wrm_bbox,
image=worm_view,
background=background,
diff_thresh=diff_thresh,
)
if ErrorCalculator.probe_hook is not None:
ErrorCalculator.probe_hook(worm_view, mask_wrm)
mask_mic = np.zeros_like(mask_wrm, dtype=bool)
mask_mic[int_bbox[1] : int_bbox[1] + int_bbox[3], int_bbox[0] : int_bbox[0] + int_bbox[2]] = True
total = mask_wrm.sum()
if total == 0:
errors[i] = 0.0
continue
intersection = np.logical_and(mask_wrm, mask_mic).sum()
error = 1.0 - intersection / total
errors[i] = error
return errors
@staticmethod
def calculate_bbox_error(worm_bboxes: np.ndarray, mic_bboxes: np.ndarray) -> np.ndarray:
"""
Calculate the bounding box error between worm bounding boxes and microscope bounding boxes.
This error calculates the proportion of the worm bounding box that is outside the microscope bounding box.
Args:
worm_bboxes: A numpy array of shape (N, 4) representing the bounding boxes of worms. The bounding boxes should be in the format (x, y, w, h).
mic_bboxes: A numpy array of shape (N, 4) representing the bounding boxes of the microscope. The bounding boxes should be in the format (x, y, w, h).
Returns:
np.ndarray: Array of errors of shape (N,) representing the bounding box error for each pair of worm and microscope bounding boxes.
"""
wrm_left, wrm_top, wrm_width, wrm_height = BoxUtils.unpack(worm_bboxes)
mic_left, mic_top, mic_width, mic_height = BoxUtils.unpack(mic_bboxes)
wrm_right, wrm_bottom = wrm_left + wrm_width, wrm_top + wrm_height
mic_right, mic_bottom = mic_left + mic_width, mic_top + mic_height
int_left = np.maximum(wrm_left, mic_left)
int_top = np.maximum(wrm_top, mic_top)
int_right = np.minimum(wrm_right, mic_right)
int_bottom = np.minimum(wrm_bottom, mic_bottom)
int_width = np.maximum(0, int_right - int_left)
int_height = np.maximum(0, int_bottom - int_top)
intersection = int_width * int_height
total = wrm_width * wrm_height
errors = 1.0 - intersection / total
errors[total == 0] = 0.0
return errors
@staticmethod
def calculate_mse_error(worm_bboxes: np.ndarray, mic_bboxes: np.ndarray) -> np.ndarray:
"""
Calculates the Mean Squared Error (MSE) error between the centers of worm bounding boxes and microscope bounding boxes.
Args:
worm_bboxes: A numpy array of shape (N, 4) representing the bounding boxes of worms. The bounding boxes should be in the format (x, y, w, h).
mic_bboxes: A numpy array of shape (N, 4) representing the bounding boxes of the microscope. The bounding boxes should be in the format (x, y, w, h).
Returns:
np.ndarray: Array of errors of shape (N,) representing the MSE error for each pair of worm and microscope bounding boxes.
"""
worm_centers = BoxUtils.center(worm_bboxes)
mic_centers = BoxUtils.center(mic_bboxes)
errors = np.mean((worm_centers - mic_centers) ** 2, axis=1)
return errors
Class variables
probe_hook
Static methods
calculate_bbox_error
def calculate_bbox_error(
worm_bboxes: numpy.ndarray,
mic_bboxes: numpy.ndarray
) -> numpy.ndarray
Calculate the bounding box error between worm bounding boxes and microscope bounding boxes.
This error calculates the proportion of the worm bounding box that is outside the microscope bounding box.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
worm_bboxes | None | A numpy array of shape (N, 4) representing the bounding boxes of worms. The bounding boxes should be in the format (x, y, w, h). | None |
mic_bboxes | None | A numpy array of shape (N, 4) representing the bounding boxes of the microscope. The bounding boxes should be in the format (x, y, w, h). | None |
Returns:
Type | Description |
---|---|
np.ndarray | Array of errors of shape (N,) representing the bounding box error for each pair of worm and microscope bounding boxes. |
View Source
@staticmethod
def calculate_bbox_error(worm_bboxes: np.ndarray, mic_bboxes: np.ndarray) -> np.ndarray:
"""
Calculate the bounding box error between worm bounding boxes and microscope bounding boxes.
This error calculates the proportion of the worm bounding box that is outside the microscope bounding box.
Args:
worm_bboxes: A numpy array of shape (N, 4) representing the bounding boxes of worms. The bounding boxes should be in the format (x, y, w, h).
mic_bboxes: A numpy array of shape (N, 4) representing the bounding boxes of the microscope. The bounding boxes should be in the format (x, y, w, h).
Returns:
np.ndarray: Array of errors of shape (N,) representing the bounding box error for each pair of worm and microscope bounding boxes.
"""
wrm_left, wrm_top, wrm_width, wrm_height = BoxUtils.unpack(worm_bboxes)
mic_left, mic_top, mic_width, mic_height = BoxUtils.unpack(mic_bboxes)
wrm_right, wrm_bottom = wrm_left + wrm_width, wrm_top + wrm_height
mic_right, mic_bottom = mic_left + mic_width, mic_top + mic_height
int_left = np.maximum(wrm_left, mic_left)
int_top = np.maximum(wrm_top, mic_top)
int_right = np.minimum(wrm_right, mic_right)
int_bottom = np.minimum(wrm_bottom, mic_bottom)
int_width = np.maximum(0, int_right - int_left)
int_height = np.maximum(0, int_bottom - int_top)
intersection = int_width * int_height
total = wrm_width * wrm_height
errors = 1.0 - intersection / total
errors[total == 0] = 0.0
return errors
calculate_mse_error
def calculate_mse_error(
worm_bboxes: numpy.ndarray,
mic_bboxes: numpy.ndarray
) -> numpy.ndarray
Calculates the Mean Squared Error (MSE) error between the centers of worm bounding boxes and microscope bounding boxes.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
worm_bboxes | None | A numpy array of shape (N, 4) representing the bounding boxes of worms. The bounding boxes should be in the format (x, y, w, h). | None |
mic_bboxes | None | A numpy array of shape (N, 4) representing the bounding boxes of the microscope. The bounding boxes should be in the format (x, y, w, h). | None |
Returns:
Type | Description |
---|---|
np.ndarray | Array of errors of shape (N,) representing the MSE error for each pair of worm and microscope bounding boxes. |
View Source
@staticmethod
def calculate_mse_error(worm_bboxes: np.ndarray, mic_bboxes: np.ndarray) -> np.ndarray:
"""
Calculates the Mean Squared Error (MSE) error between the centers of worm bounding boxes and microscope bounding boxes.
Args:
worm_bboxes: A numpy array of shape (N, 4) representing the bounding boxes of worms. The bounding boxes should be in the format (x, y, w, h).
mic_bboxes: A numpy array of shape (N, 4) representing the bounding boxes of the microscope. The bounding boxes should be in the format (x, y, w, h).
Returns:
np.ndarray: Array of errors of shape (N,) representing the MSE error for each pair of worm and microscope bounding boxes.
"""
worm_centers = BoxUtils.center(worm_bboxes)
mic_centers = BoxUtils.center(mic_bboxes)
errors = np.mean((worm_centers - mic_centers) ** 2, axis=1)
return errors
calculate_precise
def calculate_precise(
background: numpy.ndarray,
worm_bboxes: numpy.ndarray,
mic_bboxes: numpy.ndarray,
frame_nums: numpy.ndarray,
worm_reader: wtracker.utils.frame_reader.FrameReader,
diff_thresh: float = 10
) -> numpy.ndarray
Calculates the precise error for each frame in the given sequence.
This error is based on precise segmentation of the worm object from the frame, and determining the exact proportion of worm's body outside the microscope view.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
background | np.ndarray | The background image. | None |
worm_bboxes | None | A numpy array of shape (N, 4) representing the bounding boxes of worms. The bounding boxes should be in the format (x, y, w, h). | None |
mic_bboxes | None | A numpy array of shape (N, 4) representing the bounding boxes of the microscope. The bounding boxes should be in the format (x, y, w, h). | None |
frame_nums | np.ndarray | An array of frame numbers to calculate the error for. | None |
worm_reader | FrameReader | A frame reader containing segmented worm images for each frame. These worm images should match the shape of the worm bounding boxes. Frames passed in frame_nums are read from this reader by index. |
None |
diff_thresh | float | The difference threshold to distinguish foreground and background objects from. A foreground object is detected if the pixel value difference with the background is greater than this threshold. |
None |
Returns:
Type | Description |
---|---|
np.ndarray | Array of errors of shape (N,) representing the precise segmentation error for each frame. |
Raises:
Type | Description |
---|---|
AssertionError | If the length of frame_nums, worm_bboxes, and mic_bboxes do not match. |
View Source
@staticmethod
def calculate_precise(
background: np.ndarray,
worm_bboxes: np.ndarray,
mic_bboxes: np.ndarray,
frame_nums: np.ndarray,
worm_reader: FrameReader,
diff_thresh: float = 10,
) -> np.ndarray:
"""
Calculates the precise error for each frame in the given sequence.
This error is based on precise segmentation of the worm object from the frame, and
determining the exact proportion of worm's body outside the microscope view.
Args:
background (np.ndarray): The background image.
worm_bboxes: A numpy array of shape (N, 4) representing the bounding boxes of worms. The bounding boxes should be in the format (x, y, w, h).
mic_bboxes: A numpy array of shape (N, 4) representing the bounding boxes of the microscope. The bounding boxes should be in the format (x, y, w, h).
frame_nums (np.ndarray): An array of frame numbers to calculate the error for.
worm_reader (FrameReader): A frame reader containing segmented worm images for each frame. These worm images should match the shape of the worm bounding boxes.
Frames passed in frame_nums are read from this reader by index.
diff_thresh (float, optional): The difference threshold to distinguish foreground and background objects from.
A foreground object is detected if the pixel value difference with the background is greater than this threshold.
Returns:
np.ndarray: Array of errors of shape (N,) representing the precise segmentation error for each frame.
Raises:
AssertionError: If the length of frame_nums, worm_bboxes, and mic_bboxes do not match.
"""
assert frame_nums.ndim == 1
assert len(frame_nums) == worm_bboxes.shape[0] == mic_bboxes.shape[0]
errors = np.zeros(len(frame_nums), dtype=float)
bounds = background.shape[:2]
worm_bboxes, is_legal = BoxUtils.discretize(worm_bboxes, bounds=bounds, box_format=BoxFormat.XYWH)
mic_bboxes, _ = BoxUtils.discretize(mic_bboxes, bounds=bounds, box_format=BoxFormat.XYWH)
# filter out illegal bboxes, indicting no prediction or bad prediction.
errors[~is_legal] = np.nan
worm_bboxes = worm_bboxes[is_legal]
mic_bboxes = mic_bboxes[is_legal]
frame_nums = frame_nums[is_legal]
# convert to xyxy format for intersection calculation
worm_bboxes = BoxConverter.change_format(worm_bboxes, BoxFormat.XYWH, BoxFormat.XYXY)
mic_bboxes = BoxConverter.change_format(mic_bboxes, BoxFormat.XYWH, BoxFormat.XYXY)
wrm_left, wrm_top, wrm_right, wrm_bottom = BoxUtils.unpack(worm_bboxes)
mic_left, mic_top, mic_right, mic_bottom = BoxUtils.unpack(mic_bboxes)
# calculate intersection of worm and microscope bounding boxes
int_left = np.maximum(wrm_left, mic_left)
int_top = np.maximum(wrm_top, mic_top)
int_right = np.minimum(wrm_right, mic_right)
int_bottom = np.minimum(wrm_bottom, mic_bottom)
int_width = np.maximum(0, int_right - int_left)
int_height = np.maximum(0, int_bottom - int_top)
# shift the intersection to the worm view coordinates
int_left -= wrm_left
int_top -= wrm_top
# pack the intersection bounding boxes and convert to xywh format
int_bboxes = BoxUtils.pack(int_left, int_top, int_width, int_height)
worm_bboxes = BoxConverter.change_format(worm_bboxes, BoxFormat.XYXY, BoxFormat.XYWH)
mic_bboxes = BoxConverter.change_format(mic_bboxes, BoxFormat.XYXY, BoxFormat.XYWH)
for i, frame_num in tqdm(enumerate(frame_nums), total=len(frame_nums), desc="Calculating Error", unit="fr"):
wrm_bbox = worm_bboxes[i]
int_bbox = int_bboxes[i]
worm_view = worm_reader[frame_num]
mask_wrm = ErrorCalculator.calculate_segmentation(
bbox=wrm_bbox,
image=worm_view,
background=background,
diff_thresh=diff_thresh,
)
if ErrorCalculator.probe_hook is not None:
ErrorCalculator.probe_hook(worm_view, mask_wrm)
mask_mic = np.zeros_like(mask_wrm, dtype=bool)
mask_mic[int_bbox[1] : int_bbox[1] + int_bbox[3], int_bbox[0] : int_bbox[0] + int_bbox[2]] = True
total = mask_wrm.sum()
if total == 0:
errors[i] = 0.0
continue
intersection = np.logical_and(mask_wrm, mask_mic).sum()
error = 1.0 - intersection / total
errors[i] = error
return errors
calculate_segmentation
def calculate_segmentation(
bbox: numpy.ndarray,
image: numpy.ndarray,
background: numpy.ndarray,
diff_thresh: float
) -> numpy.ndarray
Calculates the segmentation error between a view and background image.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
bbox | np.ndarray | The bounding box of the image, in the format (x, y, w, h). | None |
image | np.ndarray | The image to calculate segmentation from. | None |
background | np.ndarray | The background image. | None |
diff_thresh | float | The difference threshold to distinguish foreground and background objects from. | None |
Returns:
Type | Description |
---|---|
np.ndarray | The segmentation mask. |
Raises:
Type | Description |
---|---|
ValueError | If the image is not grayscale or color. |
View Source
@staticmethod
def calculate_segmentation(
bbox: np.ndarray,
image: np.ndarray,
background: np.ndarray,
diff_thresh: float,
) -> np.ndarray:
"""
Calculates the segmentation error between a view and background image.
Args:
bbox (np.ndarray): The bounding box of the image, in the format (x, y, w, h).
image (np.ndarray): The image to calculate segmentation from.
background (np.ndarray): The background image.
diff_thresh (float): The difference threshold to distinguish foreground and background objects from.
Returns:
np.ndarray: The segmentation mask.
Raises:
ValueError: If the image is not grayscale or color.
"""
x, y, w, h = bbox
assert image.shape[:2] == (h, w)
bg_view = background[y : y + h, x : x + w]
diff = np.abs(image.astype(np.int32) - bg_view.astype(np.int32)).astype(np.uint8)
# if images are color, convert to grayscale
if diff.ndim == 3 and diff.shape[2] == 3:
diff = cv.cvtColor(diff, cv.COLOR_BGR2GRAY)
if diff.ndim != 2:
raise ValueError("Image must be either a gray or a color image.")
mask_wrm = diff > diff_thresh
return mask_wrm