"""Quality metrics for ultrasound images."""
import numpy as np
from zea.internal.registry import metrics_registry
[docs]
def get_metric(name):
"""Get metric function given name."""
return metrics_registry[name]
[docs]
@metrics_registry(name="cnr", framework="numpy", supervised=True)
def cnr(x, y):
"""Calculate contrast to noise ratio"""
mu_x = np.mean(x)
mu_y = np.mean(y)
var_x = np.var(x)
var_y = np.var(y)
return 20 * np.log10(np.abs(mu_x - mu_y) / np.sqrt((var_x + var_y) / 2))
[docs]
@metrics_registry(name="contrast", framework="numpy", supervised=True)
def contrast(x, y):
"""Contrast ratio"""
return 20 * np.log10(x.mean() / y.mean())
[docs]
@metrics_registry(name="gcnr", framework="numpy", supervised=True)
def gcnr(x, y, bins=256):
"""Generalized contrast-to-noise-ratio"""
x = x.flatten()
y = y.flatten()
_, bins = np.histogram(np.concatenate((x, y)), bins=bins)
f, _ = np.histogram(x, bins=bins, density=True)
g, _ = np.histogram(y, bins=bins, density=True)
f /= f.sum()
g /= g.sum()
return 1 - np.sum(np.minimum(f, g))
[docs]
@metrics_registry(name="fwhm", framework="numpy", supervised=False)
def fwhm(img):
"""Resolution full width half maxima"""
mask = np.nonzero(img >= 0.5 * np.amax(img))[0]
return mask[-1] - mask[0]
[docs]
@metrics_registry(name="speckle_res", framework="numpy", supervised=False)
def speckle_res(img):
"""TODO: Write speckle edge-spread function resolution code"""
raise NotImplementedError
[docs]
@metrics_registry(name="snr", framework="numpy", supervised=False)
def snr(img):
"""Signal to noise ratio"""
return img.mean() / img.std()
[docs]
@metrics_registry(name="wopt_mae", framework="numpy", supervised=True)
def wopt_mae(ref, img):
"""Find the optimal weight that minimizes the mean absolute error"""
wopt = np.median(ref / img)
return wopt
[docs]
@metrics_registry(name="wopt_mse", framework="numpy", supervised=True)
def wopt_mse(ref, img):
"""Find the optimal weight that minimizes the mean squared error"""
wopt = np.sum(ref * img) / np.sum(img * img)
return wopt
[docs]
@metrics_registry(name="l1loss", framework="numpy", supervised=True)
def l1loss(x, y):
"""L1 loss"""
return np.abs(x - y).mean()
[docs]
@metrics_registry(name="l2loss", framework="numpy", supervised=True)
def l2loss(x, y):
"""L2 loss"""
return np.sqrt(((x - y) ** 2).mean())
[docs]
@metrics_registry(name="psnr", framework="numpy", supervised=True)
def psnr(x, y):
"""Peak signal to noise ratio"""
dynamic_range = max(x.max(), y.max()) - min(x.min(), y.min())
return 20 * np.log10(dynamic_range / l2loss(x, y))
[docs]
@metrics_registry(name="ncc", framework="numpy", supervised=True)
def ncc(x, y):
"""Normalized cross correlation"""
return (x * y).sum() / np.sqrt((x**2).sum() * (y**2).sum())
[docs]
@metrics_registry(name="image_entropy", framework="numpy", supervised=False)
def image_entropy(image):
"""Calculate the entropy of the image
Args:
image (ndarray): The image for which the entropy is calculated
Returns:
float: The entropy of the image
"""
marg = np.histogramdd(np.ravel(image), bins=256)[0] / image.size
marg = list(filter(lambda p: p > 0, np.ravel(marg)))
entropy = -np.sum(np.multiply(marg, np.log2(marg)))
return entropy
[docs]
@metrics_registry(name="image_sharpness", framework="numpy", supervised=False)
def image_sharpness(image):
"""Calculate the sharpness of the image
Args:
image (ndarray): The image for which the sharpness is calculated
Returns:
float: The sharpness of the image
"""
return np.mean(np.abs(np.gradient(image)))
def _sector_reweight_image(image, sector_angle, axis):
"""
Reweights image according to the amount of area each
row of pixels will occupy if that image is scan converted
with angle sector_angle.
This 'image' could be e.g. a pixelwise loss or metric.
We can compute this by viewing the scan converted image as the sector
of a circle with a known central angle, and radius given by depth.
See: https://en.wikipedia.org/wiki/Circular_sector
Params:
image (ndarray or Tensor): image to be re-weighted, any shape
sector_angle (float | int): angle in degrees
axis (int): axis corresponding to the height/depth dimension.
Returns:
reweighted_image (ndarray): image with pixels reweighted to area occupied by each
pixel post-scan-conversion.
"""
height = image.shape[axis]
depths = np.arange(height) + 0.5 # center of the pixel as its depth
reweighting_factors = (sector_angle / 360) * 2 * np.pi * depths
# Reshape reweighting_factors to broadcast along the specified axis
shape = [1] * image.ndim
shape[axis] = height
reweighting_factors = np.reshape(reweighting_factors, shape)
return reweighting_factors * image