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)]