File size: 3,307 Bytes
3680186 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | from __future__ import annotations
from dataclasses import dataclass
import numpy as np
@dataclass(frozen=True)
class IntensityRange:
minimum: float
maximum: float
@property
def span(self) -> float:
return max(self.maximum - self.minimum, 1e-6)
def to_float32(image: np.ndarray) -> np.ndarray:
return np.asarray(image, dtype=np.float32)
def normalize_with_range(image: np.ndarray) -> tuple[np.ndarray, IntensityRange]:
image = to_float32(image)
finite_mask = np.isfinite(image)
if not np.any(finite_mask):
raise ValueError("Input image does not contain finite pixel values.")
finite_values = image[finite_mask]
value_range = IntensityRange(
minimum=float(finite_values.min()),
maximum=float(finite_values.max()),
)
normalized = np.zeros_like(image, dtype=np.float32)
normalized[finite_mask] = (image[finite_mask] - value_range.minimum) / value_range.span
normalized = np.clip(normalized, 0.0, 1.0)
return normalized, value_range
def denormalize_image(normalized: np.ndarray, value_range: IntensityRange) -> np.ndarray:
normalized = np.clip(to_float32(normalized), 0.0, 1.0)
return normalized * value_range.span + value_range.minimum
def quantize_normalized(normalized: np.ndarray, bit_depth: int) -> tuple[np.ndarray, np.ndarray]:
if bit_depth < 1:
raise ValueError("Bit depth must be at least 1.")
normalized = np.clip(to_float32(normalized), 0.0, 1.0)
levels = 2**bit_depth
level_indices = np.rint(normalized * (levels - 1)).astype(np.int32)
quantized = level_indices.astype(np.float32) / float(levels - 1)
return quantized, level_indices
def apply_window(image: np.ndarray, level: float, width: float) -> tuple[np.ndarray, float, float]:
image = to_float32(image)
width = max(float(width), 1e-6)
low = float(level - width / 2.0)
high = float(level + width / 2.0)
windowed = np.clip((image - low) / (high - low), 0.0, 1.0)
return windowed.astype(np.float32), low, high
def bounds_to_level_width(low: float, high: float) -> tuple[float, float]:
width = max(float(high) - float(low), 1e-6)
level = float(low) + width / 2.0
return level, width
def compute_default_window(image: np.ndarray) -> tuple[float, float]:
image = to_float32(image)
finite_values = image[np.isfinite(image)]
if finite_values.size == 0:
raise ValueError("Input image does not contain finite pixel values.")
p5 = float(np.percentile(finite_values, 5))
p95 = float(np.percentile(finite_values, 95))
width = max(p95 - p5, 1e-3)
level = (p5 + p95) / 2.0
return level, width
def resize_for_display(image: np.ndarray, max_edge: int = 768) -> np.ndarray:
image = np.asarray(image)
if image.ndim != 2:
raise ValueError("Expected a 2D grayscale image.")
height, width = image.shape
current_max = max(height, width)
if current_max <= max_edge:
return image
scale = max_edge / float(current_max)
new_height = max(1, int(round(height * scale)))
new_width = max(1, int(round(width * scale)))
y_idx = np.linspace(0, height - 1, new_height).astype(np.int32)
x_idx = np.linspace(0, width - 1, new_width).astype(np.int32)
return image[np.ix_(y_idx, x_idx)]
|