Skip to main content

ass_renderer/debug/analyzer/
mod.rs

1//! Frame analysis tools for detailed debugging and profiling
2//!
3//! This module provides in-depth analysis capabilities for rendered frames,
4//! including pixel statistics, region detection, and text analysis.
5
6use crate::Frame;
7
8#[cfg(feature = "nostd")]
9use alloc::vec::Vec;
10
11mod report;
12
13pub use report::{AnalysisReport, PixelHistogram, Region};
14
15/// Frame analyzer for detailed text-based debugging
16pub struct FrameAnalyzer {
17    enable_pixel_histogram: bool,
18    enable_region_analysis: bool,
19    enable_text_detection: bool,
20}
21
22impl Default for FrameAnalyzer {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28impl FrameAnalyzer {
29    /// Create a new frame analyzer with all analysis features enabled
30    pub fn new() -> Self {
31        Self {
32            enable_pixel_histogram: true,
33            enable_region_analysis: true,
34            enable_text_detection: true,
35        }
36    }
37
38    /// Analyze a frame and generate a detailed report
39    pub fn analyze(&self, frame: &Frame) -> AnalysisReport {
40        let mut report = AnalysisReport::new(frame.width(), frame.height());
41
42        if self.enable_pixel_histogram {
43            report.pixel_histogram = self.calculate_pixel_histogram(frame);
44        }
45
46        if self.enable_region_analysis {
47            report.regions = self.detect_regions(frame);
48        }
49
50        if self.enable_text_detection {
51            report.text_areas = self.detect_text_areas(frame);
52        }
53
54        report.calculate_statistics(frame);
55        report
56    }
57
58    fn calculate_pixel_histogram(&self, frame: &Frame) -> PixelHistogram {
59        let pixels = frame.pixels();
60        let mut histogram = PixelHistogram::default();
61
62        for chunk in pixels.chunks(4) {
63            if chunk.len() == 4 {
64                let r = chunk[0];
65                let g = chunk[1];
66                let b = chunk[2];
67                let a = chunk[3];
68
69                histogram.red[r as usize] += 1;
70                histogram.green[g as usize] += 1;
71                histogram.blue[b as usize] += 1;
72                histogram.alpha[a as usize] += 1;
73
74                if a > 0 {
75                    histogram.non_transparent_count += 1;
76
77                    // Classify pixel
78                    if r == g && g == b {
79                        if r > 200 {
80                            histogram.white_pixels += 1;
81                        } else if r < 50 {
82                            histogram.black_pixels += 1;
83                        } else {
84                            histogram.gray_pixels += 1;
85                        }
86                    } else {
87                        histogram.colored_pixels += 1;
88                    }
89                }
90            }
91        }
92
93        histogram
94    }
95
96    fn detect_regions(&self, frame: &Frame) -> Vec<Region> {
97        let mut regions = Vec::new();
98        let pixels = frame.pixels();
99        let width = frame.width() as usize;
100        let height = frame.height() as usize;
101
102        // Simple region detection using connected components
103        let mut visited = vec![false; width * height];
104
105        for y in 0..height {
106            for x in 0..width {
107                let idx = y * width + x;
108                let pixel_idx = idx * 4;
109
110                if !visited[idx] && pixel_idx + 3 < pixels.len() && pixels[pixel_idx + 3] > 0 {
111                    // Found a non-transparent, unvisited pixel
112                    let region = self.flood_fill(frame, &mut visited, x, y);
113                    if region.pixel_count > 10 {
114                        // Filter out tiny regions
115                        regions.push(region);
116                    }
117                }
118            }
119        }
120
121        regions
122    }
123
124    fn flood_fill(
125        &self,
126        frame: &Frame,
127        visited: &mut [bool],
128        start_x: usize,
129        start_y: usize,
130    ) -> Region {
131        let pixels = frame.pixels();
132        let width = frame.width() as usize;
133        let height = frame.height() as usize;
134
135        let mut region = Region {
136            min_x: start_x as u32,
137            min_y: start_y as u32,
138            max_x: start_x as u32,
139            max_y: start_y as u32,
140            pixel_count: 0,
141            avg_color: [0, 0, 0, 0],
142        };
143
144        let mut stack = vec![(start_x, start_y)];
145        let mut color_sum = [0u64; 4];
146
147        while let Some((x, y)) = stack.pop() {
148            if x >= width || y >= height {
149                continue;
150            }
151
152            let idx = y * width + x;
153            if visited[idx] {
154                continue;
155            }
156
157            let pixel_idx = idx * 4;
158            if pixel_idx + 3 >= pixels.len() || pixels[pixel_idx + 3] == 0 {
159                continue;
160            }
161
162            visited[idx] = true;
163            region.pixel_count += 1;
164
165            // Update bounds
166            region.min_x = region.min_x.min(x as u32);
167            region.min_y = region.min_y.min(y as u32);
168            region.max_x = region.max_x.max(x as u32);
169            region.max_y = region.max_y.max(y as u32);
170
171            // Accumulate color
172            for i in 0..4 {
173                color_sum[i] += pixels[pixel_idx + i] as u64;
174            }
175
176            // Add neighbors
177            if x > 0 {
178                stack.push((x - 1, y));
179            }
180            if x + 1 < width {
181                stack.push((x + 1, y));
182            }
183            if y > 0 {
184                stack.push((x, y - 1));
185            }
186            if y + 1 < height {
187                stack.push((x, y + 1));
188            }
189        }
190
191        // Calculate average color
192        if region.pixel_count > 0 {
193            for (i, sum) in color_sum.iter().enumerate() {
194                region.avg_color[i] = (sum / region.pixel_count as u64) as u8;
195            }
196        }
197
198        region
199    }
200
201    fn detect_text_areas(&self, frame: &Frame) -> Vec<TextArea> {
202        let regions = self.detect_regions(frame);
203        let mut text_areas = Vec::new();
204
205        for region in regions {
206            // Heuristics to identify text regions:
207            // - Aspect ratio typical of text lines
208            // - High contrast colors (usually white/yellow text)
209            // - Reasonable size
210
211            let width = region.max_x - region.min_x + 1;
212            let height = region.max_y - region.min_y + 1;
213            let aspect_ratio = width as f32 / height as f32;
214
215            // Text typically has aspect ratio > 2 for horizontal text
216            // and reasonable height (20-200 pixels for typical subtitles)
217            if aspect_ratio > 1.5 && (15..200).contains(&height) {
218                // Check if it's high contrast (likely text)
219                let is_bright = region.avg_color[0] > 200
220                    || region.avg_color[1] > 200
221                    || region.avg_color[2] > 200;
222
223                if is_bright {
224                    text_areas.push(TextArea {
225                        x: region.min_x,
226                        y: region.min_y,
227                        width,
228                        height,
229                        confidence: calculate_text_confidence(&region),
230                        estimated_font_size: estimate_font_size(height),
231                    });
232                }
233            }
234        }
235
236        text_areas
237    }
238}
239
240#[derive(Debug, Clone)]
241pub struct TextArea {
242    pub x: u32,
243    pub y: u32,
244    pub width: u32,
245    pub height: u32,
246    pub confidence: f32,
247    pub estimated_font_size: u32,
248}
249
250fn calculate_text_confidence(region: &Region) -> f32 {
251    // Simple heuristic for text confidence
252    let width = region.max_x - region.min_x + 1;
253    let height = region.max_y - region.min_y + 1;
254    let aspect = width as f32 / height as f32;
255
256    let mut confidence: f32 = 0.0;
257
258    // Good aspect ratio for text
259    if (2.0..20.0).contains(&aspect) {
260        confidence += 0.3;
261    }
262
263    // High brightness (typical for subtitles)
264    let brightness =
265        (region.avg_color[0] as f32 + region.avg_color[1] as f32 + region.avg_color[2] as f32)
266            / 3.0;
267    if brightness > 200.0 {
268        confidence += 0.4;
269    }
270
271    // Reasonable size
272    if (20..150).contains(&height) {
273        confidence += 0.3;
274    }
275
276    confidence.min(1.0)
277}
278
279fn estimate_font_size(height: u32) -> u32 {
280    // Rough estimation: font size is about 75% of bounding box height
281    (height as f32 * 0.75) as u32
282}