Skip to main content

medical_cache/
multi_res.rs

1//! Multi-Resolution Image Generator
2//!
3//! Generates thumbnail (256×256) and mid-resolution (512×512) versions
4//! of DICOM slices for progressive loading and efficient caching.
5
6use std::sync::Arc;
7use serde::{Deserialize, Serialize};
8use tracing::debug;
9
10use crate::config::ImageQuality;
11
12/// Target dimensions for each quality level
13pub const THUMBNAIL_SIZE: usize = 256;
14pub const MID_RES_SIZE: usize = 512;
15
16/// Multi-resolution image set for a single slice
17#[derive(Debug, Clone)]
18pub struct MultiResImage {
19    /// Original image dimensions [width, height]
20    pub original_size: [usize; 2],
21    /// Thumbnail (256×256) - always available
22    pub thumbnail: Option<Arc<Vec<u8>>>,
23    /// Mid-resolution (512×512) - available based on config
24    pub mid_res: Option<Arc<Vec<u8>>>,
25    /// Full resolution - lazy loaded
26    pub full_res: Option<Arc<Vec<u8>>>,
27    /// Current highest quality available
28    pub current_quality: ImageQuality,
29    /// Whether full-res is being loaded
30    pub loading_full: bool,
31}
32
33impl MultiResImage {
34    /// Create empty multi-res image
35    pub fn new(original_size: [usize; 2]) -> Self {
36        Self {
37            original_size,
38            thumbnail: None,
39            mid_res: None,
40            full_res: None,
41            current_quality: ImageQuality::Thumbnail,
42            loading_full: false,
43        }
44    }
45
46    /// Get the best available image data
47    pub fn best_available(&self) -> Option<Arc<Vec<u8>>> {
48        if let Some(ref full) = self.full_res {
49            return Some(Arc::clone(full));
50        }
51        if let Some(ref mid) = self.mid_res {
52            return Some(Arc::clone(mid));
53        }
54        if let Some(ref thumb) = self.thumbnail {
55            return Some(Arc::clone(thumb));
56        }
57        None
58    }
59
60    /// Get image at specific quality level
61    pub fn at_quality(&self, quality: ImageQuality) -> Option<Arc<Vec<u8>>> {
62        match quality {
63            ImageQuality::Thumbnail => self.thumbnail.clone(),
64            ImageQuality::MidRes => self.mid_res.clone().or_else(|| self.thumbnail.clone()),
65            ImageQuality::Full => self.full_res.clone()
66                .or_else(|| self.mid_res.clone())
67                .or_else(|| self.thumbnail.clone()),
68        }
69    }
70
71    /// Check if we have the requested quality
72    pub fn has_quality(&self, quality: ImageQuality) -> bool {
73        match quality {
74            ImageQuality::Thumbnail => self.thumbnail.is_some(),
75            ImageQuality::MidRes => self.mid_res.is_some(),
76            ImageQuality::Full => self.full_res.is_some(),
77        }
78    }
79
80    /// Get memory usage in bytes
81    pub fn memory_bytes(&self) -> usize {
82        let mut total = 0;
83        if let Some(ref t) = self.thumbnail {
84            total += t.len();
85        }
86        if let Some(ref m) = self.mid_res {
87            total += m.len();
88        }
89        if let Some(ref f) = self.full_res {
90            total += f.len();
91        }
92        total
93    }
94}
95
96/// Image resizer using bilinear interpolation
97pub struct ImageResizer;
98
99impl ImageResizer {
100    /// Generate thumbnail from grayscale image data
101    ///
102    /// # Arguments
103    /// * `data` - Raw pixel data (grayscale, 8-bit or 16-bit)
104    /// * `width` - Original image width
105    /// * `height` - Original image height
106    /// * `bits_per_pixel` - 8 or 16
107    ///
108    /// # Returns
109    /// Resized image data as 8-bit grayscale
110    pub fn generate_thumbnail(
111        data: &[u8],
112        width: usize,
113        height: usize,
114        bits_per_pixel: usize,
115    ) -> Vec<u8> {
116        Self::resize(data, width, height, bits_per_pixel, THUMBNAIL_SIZE, THUMBNAIL_SIZE)
117    }
118
119    /// Generate mid-resolution image from grayscale image data
120    pub fn generate_mid_res(
121        data: &[u8],
122        width: usize,
123        height: usize,
124        bits_per_pixel: usize,
125    ) -> Vec<u8> {
126        Self::resize(data, width, height, bits_per_pixel, MID_RES_SIZE, MID_RES_SIZE)
127    }
128
129    /// Resize image to target dimensions using bilinear interpolation
130    ///
131    /// Maintains aspect ratio by fitting within target dimensions
132    pub fn resize(
133        data: &[u8],
134        src_width: usize,
135        src_height: usize,
136        bits_per_pixel: usize,
137        target_width: usize,
138        target_height: usize,
139    ) -> Vec<u8> {
140        // Calculate aspect-ratio-preserving dimensions
141        let (dst_width, dst_height) = Self::fit_dimensions(
142            src_width, src_height,
143            target_width, target_height,
144        );
145
146        // Convert 16-bit to normalized f32 for processing
147        let src_f32: Vec<f32> = if bits_per_pixel == 16 {
148            data.chunks(2)
149                .map(|chunk| {
150                    let val = u16::from_le_bytes([chunk[0], chunk.get(1).copied().unwrap_or(0)]);
151                    val as f32 / 65535.0
152                })
153                .collect()
154        } else {
155            data.iter().map(|&b| b as f32 / 255.0).collect()
156        };
157
158        // Perform bilinear interpolation
159        let mut result = vec![0u8; dst_width * dst_height];
160
161        let x_ratio = src_width as f32 / dst_width as f32;
162        let y_ratio = src_height as f32 / dst_height as f32;
163
164        for y in 0..dst_height {
165            for x in 0..dst_width {
166                let src_x = x as f32 * x_ratio;
167                let src_y = y as f32 * y_ratio;
168
169                let x0 = src_x.floor() as usize;
170                let y0 = src_y.floor() as usize;
171                let x1 = (x0 + 1).min(src_width - 1);
172                let y1 = (y0 + 1).min(src_height - 1);
173
174                let x_frac = src_x - x0 as f32;
175                let y_frac = src_y - y0 as f32;
176
177                // Get four neighboring pixels
178                let p00 = src_f32.get(y0 * src_width + x0).copied().unwrap_or(0.0);
179                let p10 = src_f32.get(y0 * src_width + x1).copied().unwrap_or(0.0);
180                let p01 = src_f32.get(y1 * src_width + x0).copied().unwrap_or(0.0);
181                let p11 = src_f32.get(y1 * src_width + x1).copied().unwrap_or(0.0);
182
183                // Bilinear interpolation
184                let top = p00 * (1.0 - x_frac) + p10 * x_frac;
185                let bottom = p01 * (1.0 - x_frac) + p11 * x_frac;
186                let value = top * (1.0 - y_frac) + bottom * y_frac;
187
188                result[y * dst_width + x] = (value * 255.0).clamp(0.0, 255.0) as u8;
189            }
190        }
191
192        result
193    }
194
195    /// Calculate dimensions that fit within target while preserving aspect ratio
196    pub fn fit_dimensions(
197        src_width: usize,
198        src_height: usize,
199        max_width: usize,
200        max_height: usize,
201    ) -> (usize, usize) {
202        let width_ratio = max_width as f32 / src_width as f32;
203        let height_ratio = max_height as f32 / src_height as f32;
204        let ratio = width_ratio.min(height_ratio);
205
206        let new_width = ((src_width as f32 * ratio).round() as usize).max(1);
207        let new_height = ((src_height as f32 * ratio).round() as usize).max(1);
208
209        (new_width, new_height)
210    }
211
212    /// Generate all resolution levels from full-res data
213    pub fn generate_all_levels(
214        data: &[u8],
215        width: usize,
216        height: usize,
217        bits_per_pixel: usize,
218    ) -> MultiResLevels {
219        debug!(
220            "Generating multi-res levels: {}x{} @ {}bpp",
221            width, height, bits_per_pixel
222        );
223
224        let thumbnail = Self::generate_thumbnail(data, width, height, bits_per_pixel);
225        let mid_res = Self::generate_mid_res(data, width, height, bits_per_pixel);
226
227        // For full-res, convert to 8-bit if needed
228        let full_res = if bits_per_pixel == 16 {
229            // Simple linear mapping to 8-bit
230            data.chunks(2)
231                .map(|chunk| {
232                    let val = u16::from_le_bytes([chunk[0], chunk.get(1).copied().unwrap_or(0)]);
233                    (val >> 8) as u8
234                })
235                .collect()
236        } else {
237            data.to_vec()
238        };
239
240        MultiResLevels {
241            thumbnail,
242            mid_res,
243            full_res,
244            original_size: [width, height],
245        }
246    }
247}
248
249/// Generated multi-resolution levels
250#[derive(Debug, Clone)]
251pub struct MultiResLevels {
252    /// 256×256 thumbnail
253    pub thumbnail: Vec<u8>,
254    /// 512×512 mid-resolution
255    pub mid_res: Vec<u8>,
256    /// Full resolution (8-bit)
257    pub full_res: Vec<u8>,
258    /// Original dimensions
259    pub original_size: [usize; 2],
260}
261
262impl MultiResLevels {
263    /// Get data for specific quality level
264    pub fn get(&self, quality: ImageQuality) -> &[u8] {
265        match quality {
266            ImageQuality::Thumbnail => &self.thumbnail,
267            ImageQuality::MidRes => &self.mid_res,
268            ImageQuality::Full => &self.full_res,
269        }
270    }
271
272    /// Get size for specific quality level
273    pub fn size_for(&self, quality: ImageQuality) -> [usize; 2] {
274        match quality {
275            ImageQuality::Thumbnail => {
276                let (w, h) = ImageResizer::fit_dimensions(
277                    self.original_size[0],
278                    self.original_size[1],
279                    THUMBNAIL_SIZE,
280                    THUMBNAIL_SIZE,
281                );
282                [w, h]
283            }
284            ImageQuality::MidRes => {
285                let (w, h) = ImageResizer::fit_dimensions(
286                    self.original_size[0],
287                    self.original_size[1],
288                    MID_RES_SIZE,
289                    MID_RES_SIZE,
290                );
291                [w, h]
292            }
293            ImageQuality::Full => self.original_size,
294        }
295    }
296
297    /// Total memory usage
298    pub fn total_bytes(&self) -> usize {
299        self.thumbnail.len() + self.mid_res.len() + self.full_res.len()
300    }
301}
302
303/// Quality tracking for a slice
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct QualityStatus {
306    /// Study ID
307    pub study_id: String,
308    /// Series ID
309    pub series_id: String,
310    /// Slice index
311    pub slice_index: usize,
312    /// Current displayed quality
313    pub current_quality: ImageQuality,
314    /// Highest available quality
315    pub available_quality: ImageQuality,
316    /// Whether higher quality is loading
317    pub is_loading: bool,
318    /// Progress (0-100) if loading
319    pub progress: u8,
320}
321
322impl QualityStatus {
323    /// Create new quality status
324    pub fn new(study_id: String, series_id: String, slice_index: usize) -> Self {
325        Self {
326            study_id,
327            series_id,
328            slice_index,
329            current_quality: ImageQuality::Thumbnail,
330            available_quality: ImageQuality::Thumbnail,
331            is_loading: false,
332            progress: 0,
333        }
334    }
335
336    /// Update when new quality becomes available
337    pub fn quality_ready(&mut self, quality: ImageQuality) {
338        self.available_quality = quality;
339        if self.is_loading && quality == ImageQuality::Full {
340            self.is_loading = false;
341            self.progress = 100;
342        }
343    }
344
345    /// Start loading higher quality
346    pub fn start_loading(&mut self) {
347        self.is_loading = true;
348        self.progress = 0;
349    }
350
351    /// Update loading progress
352    pub fn update_progress(&mut self, progress: u8) {
353        self.progress = progress.min(100);
354    }
355
356    /// Check if upgrade is needed
357    pub fn needs_upgrade(&self) -> bool {
358        self.current_quality < self.available_quality
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[test]
367    fn test_fit_dimensions_landscape() {
368        let (w, h) = ImageResizer::fit_dimensions(1024, 512, 256, 256);
369        assert_eq!(w, 256);
370        assert_eq!(h, 128);
371    }
372
373    #[test]
374    fn test_fit_dimensions_portrait() {
375        let (w, h) = ImageResizer::fit_dimensions(512, 1024, 256, 256);
376        assert_eq!(w, 128);
377        assert_eq!(h, 256);
378    }
379
380    #[test]
381    fn test_fit_dimensions_square() {
382        let (w, h) = ImageResizer::fit_dimensions(512, 512, 256, 256);
383        assert_eq!(w, 256);
384        assert_eq!(h, 256);
385    }
386
387    #[test]
388    fn test_resize_8bit() {
389        // Create a simple 4x4 gradient image
390        let data: Vec<u8> = (0..16).map(|i| (i * 17) as u8).collect();
391
392        let result = ImageResizer::resize(&data, 4, 4, 8, 2, 2);
393
394        assert_eq!(result.len(), 4); // 2x2 output
395        // Values should be interpolated
396        assert!(result[0] < result[3]); // Gradient preserved
397    }
398
399    #[test]
400    fn test_resize_16bit() {
401        // Create a simple 4x4 16-bit image
402        let mut data = Vec::new();
403        for i in 0..16 {
404            let val = (i * 4096) as u16;
405            data.extend_from_slice(&val.to_le_bytes());
406        }
407
408        let result = ImageResizer::resize(&data, 4, 4, 16, 2, 2);
409
410        assert_eq!(result.len(), 4); // 2x2 output
411    }
412
413    #[test]
414    fn test_generate_all_levels() {
415        // Create a 512x512 test image
416        let data: Vec<u8> = (0..512 * 512).map(|i| (i % 256) as u8).collect();
417
418        let levels = ImageResizer::generate_all_levels(&data, 512, 512, 8);
419
420        // Check sizes
421        assert_eq!(levels.thumbnail.len(), 256 * 256);
422        assert_eq!(levels.mid_res.len(), 512 * 512);
423        assert_eq!(levels.full_res.len(), 512 * 512);
424    }
425
426    #[test]
427    fn test_multi_res_image() {
428        let mut img = MultiResImage::new([512, 512]);
429
430        assert!(!img.has_quality(ImageQuality::Thumbnail));
431
432        img.thumbnail = Some(Arc::new(vec![0u8; 256 * 256]));
433        assert!(img.has_quality(ImageQuality::Thumbnail));
434        assert!(!img.has_quality(ImageQuality::Full));
435
436        assert_eq!(img.current_quality, ImageQuality::Thumbnail);
437    }
438
439    #[test]
440    fn test_quality_status() {
441        let mut status = QualityStatus::new(
442            "study1".to_string(),
443            "series1".to_string(),
444            0,
445        );
446
447        assert_eq!(status.current_quality, ImageQuality::Thumbnail);
448        assert!(!status.is_loading);
449
450        status.start_loading();
451        assert!(status.is_loading);
452
453        status.quality_ready(ImageQuality::Full);
454        assert!(!status.is_loading);
455        assert_eq!(status.available_quality, ImageQuality::Full);
456    }
457}