1#![allow(dead_code)]
2#[allow(clippy::cast_precision_loss)]
15type Coord = i32;
16
17#[derive(Debug, Clone, PartialEq)]
19pub struct RoiRegion {
20 pub x: Coord,
22 pub y: Coord,
24 pub width: u32,
26 pub height: u32,
28 pub priority: f64,
30 pub label: String,
32}
33
34impl RoiRegion {
35 pub fn new(x: Coord, y: Coord, width: u32, height: u32) -> Self {
37 Self {
38 x,
39 y,
40 width,
41 height,
42 priority: 1.0,
43 label: String::new(),
44 }
45 }
46
47 pub fn with_priority(x: Coord, y: Coord, width: u32, height: u32, priority: f64) -> Self {
49 Self {
50 x,
51 y,
52 width,
53 height,
54 priority,
55 label: String::new(),
56 }
57 }
58
59 pub fn set_label(&mut self, label: &str) {
61 self.label = label.to_string();
62 }
63
64 #[allow(clippy::cast_precision_loss)]
66 pub fn area(&self) -> u64 {
67 u64::from(self.width) * u64::from(self.height)
68 }
69
70 pub fn right(&self) -> Coord {
72 self.x + self.width as Coord
73 }
74
75 pub fn bottom(&self) -> Coord {
77 self.y + self.height as Coord
78 }
79
80 pub fn contains(&self, px: Coord, py: Coord) -> bool {
82 px >= self.x && px < self.right() && py >= self.y && py < self.bottom()
83 }
84
85 pub fn overlaps(&self, other: &Self) -> bool {
87 self.x < other.right()
88 && self.right() > other.x
89 && self.y < other.bottom()
90 && self.bottom() > other.y
91 }
92
93 pub fn intersection_area(&self, other: &Self) -> u64 {
95 let left = self.x.max(other.x);
96 let top = self.y.max(other.y);
97 let right = self.right().min(other.right());
98 let bottom = self.bottom().min(other.bottom());
99 if right > left && bottom > top {
100 (right - left) as u64 * (bottom - top) as u64
101 } else {
102 0
103 }
104 }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum QpAdjustMode {
110 AbsoluteOffset,
112 PriorityScale,
114 FixedQp,
116}
117
118#[derive(Debug, Clone)]
120pub struct RoiEncoderConfig {
121 pub frame_width: u32,
123 pub frame_height: u32,
125 pub ctu_size: u32,
127 pub base_qp: u8,
129 pub max_qp_reduction: u8,
131 pub max_qp_increase: u8,
133 pub adjust_mode: QpAdjustMode,
135}
136
137impl Default for RoiEncoderConfig {
138 fn default() -> Self {
139 Self {
140 frame_width: 1920,
141 frame_height: 1080,
142 ctu_size: 64,
143 base_qp: 28,
144 max_qp_reduction: 10,
145 max_qp_increase: 6,
146 adjust_mode: QpAdjustMode::PriorityScale,
147 }
148 }
149}
150
151#[derive(Debug, Clone)]
153pub struct QpDeltaMap {
154 pub cols: usize,
156 pub rows: usize,
158 pub deltas: Vec<i8>,
160}
161
162impl QpDeltaMap {
163 pub fn new(cols: usize, rows: usize) -> Self {
165 Self {
166 cols,
167 rows,
168 deltas: vec![0; cols * rows],
169 }
170 }
171
172 pub fn get(&self, col: usize, row: usize) -> i8 {
174 if col < self.cols && row < self.rows {
175 self.deltas[row * self.cols + col]
176 } else {
177 0
178 }
179 }
180
181 pub fn set(&mut self, col: usize, row: usize, delta: i8) {
183 if col < self.cols && row < self.rows {
184 self.deltas[row * self.cols + col] = delta;
185 }
186 }
187
188 #[allow(clippy::cast_precision_loss)]
190 pub fn average_delta(&self) -> f64 {
191 if self.deltas.is_empty() {
192 return 0.0;
193 }
194 let sum: i64 = self.deltas.iter().map(|&d| i64::from(d)).sum();
195 sum as f64 / self.deltas.len() as f64
196 }
197
198 pub fn active_ctu_count(&self) -> usize {
200 self.deltas.iter().filter(|&&d| d != 0).count()
201 }
202
203 pub fn merge_additive(&mut self, other: &Self, max_magnitude: i8) {
205 if self.cols != other.cols || self.rows != other.rows {
206 return;
207 }
208 for i in 0..self.deltas.len() {
209 let sum = i16::from(self.deltas[i]) + i16::from(other.deltas[i]);
210 self.deltas[i] = sum.clamp(i16::from(-max_magnitude), i16::from(max_magnitude)) as i8;
211 }
212 }
213}
214
215#[derive(Debug)]
217pub struct RoiEncoder {
218 config: RoiEncoderConfig,
220 regions: Vec<RoiRegion>,
222}
223
224impl RoiEncoder {
225 pub fn new(config: RoiEncoderConfig) -> Self {
227 Self {
228 config,
229 regions: Vec::new(),
230 }
231 }
232
233 pub fn add_region(&mut self, region: RoiRegion) {
235 self.regions.push(region);
236 }
237
238 pub fn clear_regions(&mut self) {
240 self.regions.clear();
241 }
242
243 pub fn region_count(&self) -> usize {
245 self.regions.len()
246 }
247
248 pub fn regions(&self) -> &[RoiRegion] {
250 &self.regions
251 }
252
253 pub fn config(&self) -> &RoiEncoderConfig {
255 &self.config
256 }
257
258 #[allow(clippy::cast_precision_loss)]
260 pub fn generate_qp_map(&self) -> QpDeltaMap {
261 let cols =
262 ((self.config.frame_width + self.config.ctu_size - 1) / self.config.ctu_size) as usize;
263 let rows =
264 ((self.config.frame_height + self.config.ctu_size - 1) / self.config.ctu_size) as usize;
265 let mut map = QpDeltaMap::new(cols, rows);
266
267 if self.regions.is_empty() {
268 return map;
269 }
270
271 for row in 0..rows {
272 for col in 0..cols {
273 let ctu_x = (col as u32 * self.config.ctu_size) as Coord;
274 let ctu_y = (row as u32 * self.config.ctu_size) as Coord;
275 let ctu_region =
276 RoiRegion::new(ctu_x, ctu_y, self.config.ctu_size, self.config.ctu_size);
277
278 let mut max_priority: f64 = 0.0;
279 for region in &self.regions {
280 if region.overlaps(&ctu_region) {
281 let overlap = region.intersection_area(&ctu_region);
282 let ctu_area = ctu_region.area();
283 let coverage = if ctu_area > 0 {
284 overlap as f64 / ctu_area as f64
285 } else {
286 0.0
287 };
288 let effective = region.priority * coverage;
289 if effective > max_priority {
290 max_priority = effective;
291 }
292 }
293 }
294
295 let delta = self.compute_delta(max_priority);
296 map.set(col, row, delta);
297 }
298 }
299
300 map
301 }
302
303 #[allow(clippy::cast_precision_loss)]
308 pub fn optimize_frame(&self) -> RoiOptimizeResult {
309 let map = self.generate_qp_map();
310 let analysis = analyze_qp_map(&map);
311
312 let quality_weights: Vec<f64> = map
314 .deltas
315 .iter()
316 .map(|&d| {
317 let w = 1.0 - f64::from(d) / f64::from(self.config.max_qp_reduction as i8);
319 w.clamp(0.5, 2.0)
320 })
321 .collect();
322
323 let bitrate_impact = self.estimate_bitrate_impact();
324
325 RoiOptimizeResult {
326 qp_map: map,
327 quality_weights,
328 analysis,
329 estimated_bitrate_change: bitrate_impact,
330 has_active_regions: !self.regions.is_empty(),
331 }
332 }
333
334 #[allow(clippy::cast_precision_loss)]
336 #[allow(clippy::cast_possible_truncation)]
337 fn compute_delta(&self, priority: f64) -> i8 {
338 if priority <= 0.0 {
339 return 0;
340 }
341 match self.config.adjust_mode {
342 QpAdjustMode::AbsoluteOffset => {
343 let offset = -(priority * f64::from(self.config.max_qp_reduction));
344 offset.round().max(-(i8::MAX as f64)).min(0.0) as i8
345 }
346 QpAdjustMode::PriorityScale => {
347 if priority > 1.0 {
348 let reduction =
349 (priority - 1.0).min(1.0) * f64::from(self.config.max_qp_reduction);
350 -(reduction
351 .round()
352 .min(f64::from(self.config.max_qp_reduction)) as i8)
353 } else if priority < 1.0 {
354 let increase = (1.0 - priority) * f64::from(self.config.max_qp_increase);
355 increase.round().min(f64::from(self.config.max_qp_increase)) as i8
356 } else {
357 0
358 }
359 }
360 QpAdjustMode::FixedQp => {
361 let target_qp = (f64::from(self.config.base_qp) / priority).round() as i16;
362 let delta = target_qp - i16::from(self.config.base_qp);
363 delta
364 .max(-i16::from(self.config.max_qp_reduction))
365 .min(i16::from(self.config.max_qp_increase)) as i8
366 }
367 }
368 }
369
370 #[allow(clippy::cast_precision_loss)]
372 pub fn estimate_bitrate_impact(&self) -> f64 {
373 let map = self.generate_qp_map();
374 let avg_delta = map.average_delta();
375 let factor = 2.0_f64.powf(-avg_delta / 6.0);
377 factor - 1.0
378 }
379}
380
381#[derive(Debug, Clone)]
383pub struct RoiOptimizeResult {
384 pub qp_map: QpDeltaMap,
386 pub quality_weights: Vec<f64>,
388 pub analysis: RoiAnalysisSummary,
390 pub estimated_bitrate_change: f64,
392 pub has_active_regions: bool,
394}
395
396impl RoiOptimizeResult {
397 pub fn empty(cols: usize, rows: usize) -> Self {
399 Self {
400 qp_map: QpDeltaMap::new(cols, rows),
401 quality_weights: vec![1.0; cols * rows],
402 analysis: RoiAnalysisSummary {
403 total_ctus: cols * rows,
404 roi_ctus: 0,
405 avg_delta: 0.0,
406 min_delta: 0,
407 max_delta: 0,
408 },
409 estimated_bitrate_change: 0.0,
410 has_active_regions: false,
411 }
412 }
413
414 pub fn qp_delta_at(&self, col: usize, row: usize) -> i8 {
416 self.qp_map.get(col, row)
417 }
418
419 #[allow(clippy::cast_precision_loss)]
421 pub fn quality_weight_at(&self, col: usize, row: usize) -> f64 {
422 if col < self.qp_map.cols && row < self.qp_map.rows {
423 let idx = row * self.qp_map.cols + col;
424 if idx < self.quality_weights.len() {
425 return self.quality_weights[idx];
426 }
427 }
428 1.0
429 }
430}
431
432#[derive(Debug, Clone)]
434pub struct RoiAnalysisSummary {
435 pub total_ctus: usize,
437 pub roi_ctus: usize,
439 pub avg_delta: f64,
441 pub min_delta: i8,
443 pub max_delta: i8,
445}
446
447pub fn analyze_qp_map(map: &QpDeltaMap) -> RoiAnalysisSummary {
449 let total_ctus = map.deltas.len();
450 let roi_ctus = map.active_ctu_count();
451 let avg_delta = map.average_delta();
452 let min_delta = map.deltas.iter().copied().min().unwrap_or(0);
453 let max_delta = map.deltas.iter().copied().max().unwrap_or(0);
454 RoiAnalysisSummary {
455 total_ctus,
456 roi_ctus,
457 avg_delta,
458 min_delta,
459 max_delta,
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466
467 #[test]
468 fn test_roi_region_new() {
469 let r = RoiRegion::new(10, 20, 100, 200);
470 assert_eq!(r.x, 10);
471 assert_eq!(r.y, 20);
472 assert_eq!(r.width, 100);
473 assert_eq!(r.height, 200);
474 assert!((r.priority - 1.0).abs() < f64::EPSILON);
475 }
476
477 #[test]
478 fn test_roi_region_with_priority() {
479 let r = RoiRegion::with_priority(0, 0, 50, 50, 2.5);
480 assert!((r.priority - 2.5).abs() < f64::EPSILON);
481 }
482
483 #[test]
484 fn test_roi_region_area() {
485 let r = RoiRegion::new(0, 0, 100, 200);
486 assert_eq!(r.area(), 20_000);
487 }
488
489 #[test]
490 fn test_roi_region_contains() {
491 let r = RoiRegion::new(10, 10, 50, 50);
492 assert!(r.contains(10, 10));
493 assert!(r.contains(30, 30));
494 assert!(!r.contains(60, 60));
495 assert!(!r.contains(9, 10));
496 }
497
498 #[test]
499 fn test_roi_region_overlaps() {
500 let a = RoiRegion::new(0, 0, 100, 100);
501 let b = RoiRegion::new(50, 50, 100, 100);
502 assert!(a.overlaps(&b));
503
504 let c = RoiRegion::new(200, 200, 10, 10);
505 assert!(!a.overlaps(&c));
506 }
507
508 #[test]
509 fn test_roi_region_intersection_area() {
510 let a = RoiRegion::new(0, 0, 100, 100);
511 let b = RoiRegion::new(50, 50, 100, 100);
512 assert_eq!(a.intersection_area(&b), 50 * 50);
513
514 let c = RoiRegion::new(200, 200, 10, 10);
515 assert_eq!(a.intersection_area(&c), 0);
516 }
517
518 #[test]
519 fn test_roi_region_label() {
520 let mut r = RoiRegion::new(0, 0, 10, 10);
521 r.set_label("face");
522 assert_eq!(r.label, "face");
523 }
524
525 #[test]
526 fn test_qp_delta_map_new() {
527 let map = QpDeltaMap::new(3, 2);
528 assert_eq!(map.cols, 3);
529 assert_eq!(map.rows, 2);
530 assert_eq!(map.deltas.len(), 6);
531 }
532
533 #[test]
534 fn test_qp_delta_map_get_set() {
535 let mut map = QpDeltaMap::new(4, 4);
536 map.set(1, 2, -5);
537 assert_eq!(map.get(1, 2), -5);
538 assert_eq!(map.get(0, 0), 0);
539 assert_eq!(map.get(10, 10), 0);
541 }
542
543 #[test]
544 fn test_qp_delta_map_average() {
545 let mut map = QpDeltaMap::new(2, 2);
546 map.set(0, 0, -4);
547 map.set(1, 0, -4);
548 map.set(0, 1, 0);
549 map.set(1, 1, 0);
550 assert!((map.average_delta() - (-2.0)).abs() < f64::EPSILON);
551 }
552
553 #[test]
554 fn test_qp_delta_map_active_count() {
555 let mut map = QpDeltaMap::new(3, 3);
556 map.set(0, 0, -2);
557 map.set(2, 2, 3);
558 assert_eq!(map.active_ctu_count(), 2);
559 }
560
561 #[test]
562 fn test_qp_delta_map_merge_additive() {
563 let mut map1 = QpDeltaMap::new(2, 2);
564 map1.set(0, 0, -3);
565 map1.set(1, 1, 2);
566
567 let mut map2 = QpDeltaMap::new(2, 2);
568 map2.set(0, 0, -4);
569 map2.set(1, 1, 3);
570
571 map1.merge_additive(&map2, 6);
572 assert_eq!(map1.get(0, 0), -6); assert_eq!(map1.get(1, 1), 5);
574 }
575
576 #[test]
577 fn test_roi_encoder_empty_regions() {
578 let config = RoiEncoderConfig {
579 frame_width: 128,
580 frame_height: 128,
581 ctu_size: 64,
582 ..Default::default()
583 };
584 let enc = RoiEncoder::new(config);
585 let map = enc.generate_qp_map();
586 assert_eq!(map.cols, 2);
587 assert_eq!(map.rows, 2);
588 assert_eq!(map.active_ctu_count(), 0);
589 }
590
591 #[test]
592 fn test_roi_encoder_with_region() {
593 let config = RoiEncoderConfig {
594 frame_width: 256,
595 frame_height: 256,
596 ctu_size: 64,
597 adjust_mode: QpAdjustMode::AbsoluteOffset,
598 max_qp_reduction: 6,
599 ..Default::default()
600 };
601 let mut enc = RoiEncoder::new(config);
602 enc.add_region(RoiRegion::with_priority(0, 0, 64, 64, 1.0));
603 let map = enc.generate_qp_map();
604 assert!(map.get(0, 0) < 0);
606 }
607
608 #[test]
609 fn test_analyze_qp_map() {
610 let mut map = QpDeltaMap::new(4, 4);
611 map.set(0, 0, -3);
612 map.set(3, 3, 2);
613 let summary = analyze_qp_map(&map);
614 assert_eq!(summary.total_ctus, 16);
615 assert_eq!(summary.roi_ctus, 2);
616 assert_eq!(summary.min_delta, -3);
617 assert_eq!(summary.max_delta, 2);
618 }
619
620 #[test]
621 fn test_roi_encoder_bitrate_impact_no_regions() {
622 let config = RoiEncoderConfig {
623 frame_width: 128,
624 frame_height: 128,
625 ctu_size: 64,
626 ..Default::default()
627 };
628 let enc = RoiEncoder::new(config);
629 let impact = enc.estimate_bitrate_impact();
630 assert!(impact.abs() < f64::EPSILON);
631 }
632
633 #[test]
634 fn test_roi_encoder_region_count() {
635 let config = RoiEncoderConfig::default();
636 let mut enc = RoiEncoder::new(config);
637 assert_eq!(enc.region_count(), 0);
638 enc.add_region(RoiRegion::new(0, 0, 100, 100));
639 enc.add_region(RoiRegion::new(200, 200, 50, 50));
640 assert_eq!(enc.region_count(), 2);
641 enc.clear_regions();
642 assert_eq!(enc.region_count(), 0);
643 }
644
645 #[test]
648 fn test_roi_optimize_frame_no_regions() {
649 let config = RoiEncoderConfig {
650 frame_width: 128,
651 frame_height: 128,
652 ctu_size: 64,
653 ..Default::default()
654 };
655 let enc = RoiEncoder::new(config);
656 let result = enc.optimize_frame();
657 assert!(!result.has_active_regions);
658 assert_eq!(result.analysis.roi_ctus, 0);
659 assert!((result.estimated_bitrate_change - 0.0).abs() < f64::EPSILON);
660 }
661
662 #[test]
663 fn test_roi_optimize_frame_with_regions() {
664 let config = RoiEncoderConfig {
665 frame_width: 256,
666 frame_height: 256,
667 ctu_size: 64,
668 adjust_mode: QpAdjustMode::AbsoluteOffset,
669 max_qp_reduction: 8,
670 ..Default::default()
671 };
672 let mut enc = RoiEncoder::new(config);
673 enc.add_region(RoiRegion::with_priority(0, 0, 128, 128, 1.5));
674 let result = enc.optimize_frame();
675 assert!(result.has_active_regions);
676 assert!(result.analysis.roi_ctus > 0);
677 let w = result.quality_weight_at(0, 0);
679 assert!(w > 1.0, "Quality weight in ROI should be > 1.0: {}", w);
680 }
681
682 #[test]
683 fn test_roi_optimize_result_empty() {
684 let result = RoiOptimizeResult::empty(4, 4);
685 assert!(!result.has_active_regions);
686 assert_eq!(result.quality_weights.len(), 16);
687 assert!((result.quality_weight_at(0, 0) - 1.0).abs() < f64::EPSILON);
688 assert_eq!(result.qp_delta_at(0, 0), 0);
689 }
690
691 #[test]
692 fn test_roi_optimize_result_accessors() {
693 let config = RoiEncoderConfig {
694 frame_width: 128,
695 frame_height: 128,
696 ctu_size: 64,
697 adjust_mode: QpAdjustMode::AbsoluteOffset,
698 max_qp_reduction: 6,
699 ..Default::default()
700 };
701 let mut enc = RoiEncoder::new(config);
702 enc.add_region(RoiRegion::with_priority(0, 0, 64, 64, 1.0));
703 let result = enc.optimize_frame();
704 assert!(result.qp_delta_at(0, 0) < 0);
706 assert_eq!(result.qp_delta_at(100, 100), 0);
708 assert!((result.quality_weight_at(100, 100) - 1.0).abs() < f64::EPSILON);
709 }
710
711 #[test]
712 fn test_roi_high_priority_gets_more_bits() {
713 let config = RoiEncoderConfig {
714 frame_width: 256,
715 frame_height: 256,
716 ctu_size: 64,
717 adjust_mode: QpAdjustMode::AbsoluteOffset,
718 max_qp_reduction: 10,
719 ..Default::default()
720 };
721 let mut enc = RoiEncoder::new(config);
722 enc.add_region(RoiRegion::with_priority(0, 0, 64, 64, 2.0));
724 let result = enc.optimize_frame();
725 let delta_roi = result.qp_delta_at(0, 0);
726 let delta_bg = result.qp_delta_at(3, 3);
727 assert!(
729 delta_roi < delta_bg,
730 "ROI delta ({}) should be less than background ({})",
731 delta_roi,
732 delta_bg
733 );
734 }
735
736 #[test]
737 fn test_roi_encoder_regions_accessor() {
738 let config = RoiEncoderConfig::default();
739 let mut enc = RoiEncoder::new(config);
740 enc.add_region(RoiRegion::new(0, 0, 100, 100));
741 enc.add_region(RoiRegion::with_priority(200, 200, 50, 50, 2.0));
742 assert_eq!(enc.regions().len(), 2);
743 assert_eq!(enc.regions()[0].width, 100);
744 assert!((enc.regions()[1].priority - 2.0).abs() < f64::EPSILON);
745 }
746
747 #[test]
748 fn test_merge_additive_different_sizes_noop() {
749 let mut map1 = QpDeltaMap::new(2, 2);
750 map1.set(0, 0, -3);
751 let map2 = QpDeltaMap::new(3, 3);
752 map1.merge_additive(&map2, 10);
753 assert_eq!(map1.get(0, 0), -3);
755 }
756}