File size: 6,961 Bytes
69066c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
"""
DICOM processing utilities for NeuroSAM 3 application.
Handles DICOM file reading, windowing, and image preprocessing.
"""

from typing import Tuple, Optional
import numpy as np
import pydicom
from pydicom.errors import InvalidDicomError
from PIL import Image
from logger_config import logger
from config import CT_WINDOW_PRESETS, OUTPUT_DPI


def get_window_params(window_type: str, modality: str) -> Tuple[float, float]:
    """
    Get window level and width parameters based on window type and modality.
    
    Args:
        window_type: Window type name (e.g., "Brain (Grey Matter)")
        modality: Imaging modality ("CT" or "MRI")
    
    Returns:
        Tuple of (level, width)
    """
    if modality == "CT":
        preset = CT_WINDOW_PRESETS.get(window_type, CT_WINDOW_PRESETS["Default"])
        return preset["level"], preset["width"]
    else:
        # MRI doesn't use windowing presets
        return 0.0, 0.0


def apply_ct_windowing(img_hu: np.ndarray, level: float, width: float) -> np.ndarray:
    """
    Apply CT windowing to Hounsfield units.
    
    Args:
        img_hu: Image in Hounsfield units
        level: Window level
        width: Window width
    
    Returns:
        Windowed image array (0-1 normalized)
    """
    img_min = level - (width / 2)
    img_max = level + (width / 2)
    
    img_range = img_max - img_min
    if img_range <= 0:
        # Fallback to full range
        img_min = np.min(img_hu)
        img_max = np.max(img_hu)
        img_range = img_max - img_min
        if img_range <= 0:
            raise ValueError("Invalid image range for windowing")
    
    img_windowed = (img_hu - img_min) / img_range
    img_windowed = np.clip(img_windowed, 0, 1)
    
    return img_windowed


def apply_mri_normalization(img_array: np.ndarray) -> np.ndarray:
    """
    Apply percentile-based normalization for MRI images.
    
    Args:
        img_array: Image array
    
    Returns:
        Normalized image array (0-1 normalized)
    """
    img_min = np.percentile(img_array, 1)
    img_max = np.percentile(img_array, 99)
    
    img_range = img_max - img_min
    if img_range <= 0:
        # Fallback to full range
        img_min = np.min(img_array)
        img_max = np.max(img_array)
        img_range = img_max - img_min
        if img_range <= 0:
            raise ValueError("Invalid image range for normalization")
    
    img_normalized = (img_array - img_min) / img_range
    img_normalized = np.clip(img_normalized, 0, 1)
    
    return img_normalized


def read_dicom_file(file_path: str) -> Tuple[np.ndarray, Optional[pydicom.Dataset]]:
    """
    Read DICOM file and extract pixel data.
    
    Args:
        file_path: Path to DICOM file
    
    Returns:
        Tuple of (pixel_array, dataset) or raises exception
    
    Raises:
        InvalidDicomError: If file is not a valid DICOM file
        ValueError: If DICOM file doesn't contain pixel data
    """
    try:
        ds = pydicom.dcmread(file_path)
        
        if not hasattr(ds, 'pixel_array'):
            raise ValueError("DICOM file does not contain pixel data")
        
        raw = ds.pixel_array.astype(np.float32)
        
        # Apply rescale slope and intercept
        slope = getattr(ds, 'RescaleSlope', 1)
        intercept = getattr(ds, 'RescaleIntercept', 0)
        img_hu = raw * slope + intercept
        
        logger.debug(f"DICOM file read: {file_path}, shape={img_hu.shape}")
        
        return img_hu, ds
        
    except InvalidDicomError as e:
        logger.error(f"Invalid DICOM file format: {file_path}, error: {e}")
        raise
    except Exception as e:
        logger.error(f"Error reading DICOM file: {file_path}, error: {e}")
        raise


def process_dicom_to_pil(
    file_path: str,
    modality: str,
    window_type: str
) -> Image.Image:
    """
    Process DICOM file and convert to PIL Image.
    
    Args:
        file_path: Path to DICOM file
        modality: Imaging modality ("CT" or "MRI")
        window_type: Window type for CT images
    
    Returns:
        PIL Image ready for processing
    
    Raises:
        InvalidDicomError: If file is not a valid DICOM file
        ValueError: If processing fails
    """
    img_hu, ds = read_dicom_file(file_path)
    
    # Apply windowing/normalization based on modality
    if modality == "CT":
        level, width = get_window_params(window_type, modality)
        img_windowed = apply_ct_windowing(img_hu, level, width)
    else:  # MRI
        img_windowed = apply_mri_normalization(img_hu)
    
    # Convert to uint8
    img_uint8 = (img_windowed * 255).astype(np.uint8)
    
    # Convert to PIL Image
    if len(img_uint8.shape) == 2:
        pil_image = Image.fromarray(img_uint8).convert('RGB')
    else:
        pil_image = Image.fromarray(img_uint8)
    
    logger.debug(f"DICOM processed to PIL Image: shape={img_uint8.shape}")
    
    return pil_image


def process_standard_image_to_pil(
    file_path: str,
    modality: str,
    window_type: str
) -> Image.Image:
    """
    Process standard image file (PNG, JPG, etc.) and convert to PIL Image.
    
    Args:
        file_path: Path to image file
        modality: Imaging modality ("CT" or "MRI")
        window_type: Window type for CT images
    
    Returns:
        PIL Image ready for processing
    
    Raises:
        ValueError: If processing fails
    """
    pil_image = Image.open(file_path)
    
    # Convert to RGB if needed
    if pil_image.mode != 'RGB':
        pil_image = pil_image.convert('RGB')
    
    # Convert to numpy for normalization
    img_array = np.array(pil_image)
    
    # Handle grayscale images
    if len(img_array.shape) == 2:
        img_array = np.stack([img_array] * 3, axis=-1)
    
    # Normalize image based on modality
    img_float = img_array.astype(np.float32)
    
    if modality == "CT":
        # For CT-like processing, use windowing
        level, width = get_window_params(window_type, modality)
        # Apply windowing to each channel
        img_normalized = np.zeros_like(img_float)
        for c in range(img_float.shape[2]):
            channel_hu = img_float[:, :, c]
            img_normalized[:, :, c] = apply_ct_windowing(channel_hu, level, width)
    else:  # MRI - use percentile normalization
        img_normalized = apply_mri_normalization(img_float)
    
    # Convert back to uint8
    img_uint8 = (img_normalized * 255).astype(np.uint8)
    
    pil_image = Image.fromarray(img_uint8.astype(np.uint8))
    
    logger.debug(f"Standard image processed to PIL Image: shape={img_uint8.shape}")
    
    return pil_image


def is_dicom_file(file_path: str) -> bool:
    """
    Check if file is a DICOM file based on extension.
    
    Args:
        file_path: Path to file
    
    Returns:
        True if file is DICOM, False otherwise
    """
    import os
    ext = os.path.splitext(file_path)[1].lower()
    return ext == '.dcm'