fix: allow synthetic grid frames through UVC guard
This commit is contained in:
parent
5f2c2efa13
commit
cf1c1fa47d
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1658,7 +1658,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_client"
|
||||
version = "0.25.1"
|
||||
version = "0.25.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@ -1692,7 +1692,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_common"
|
||||
version = "0.25.1"
|
||||
version = "0.25.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@ -1704,7 +1704,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_server"
|
||||
version = "0.25.1"
|
||||
version = "0.25.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.25.1"
|
||||
version = "0.25.2"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.25.1"
|
||||
version = "0.25.2"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.25.1"
|
||||
version = "0.25.2"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -339,6 +339,9 @@ fn seam_reason(frame: &SampledFrame) -> Option<MjpegVisualArtifactReason> {
|
||||
let mut best_luma: Option<(usize, usize, u16)> = None;
|
||||
let mut best_partial: Option<(usize, usize, usize, u16)> = None;
|
||||
let mut best_chroma: Option<(usize, usize, u16)> = None;
|
||||
let mut luma_candidate_rows = Vec::new();
|
||||
let mut partial_candidate_rows = Vec::new();
|
||||
let mut chroma_candidate_rows = Vec::new();
|
||||
|
||||
for row in row_bounds(frame).skip(1) {
|
||||
let prev_start = (row - 1) * frame.width;
|
||||
@ -385,6 +388,7 @@ fn seam_reason(frame: &SampledFrame) -> Option<MjpegVisualArtifactReason> {
|
||||
let chroma_avg = (chroma_total / frame.width.max(1) as u32) as u16;
|
||||
|
||||
if luma_coverage >= luma_coverage_threshold && luma_avg >= luma_threshold as u16 {
|
||||
luma_candidate_rows.push(row);
|
||||
match best_luma {
|
||||
Some((_, best_coverage, best_avg))
|
||||
if best_coverage > luma_coverage
|
||||
@ -397,6 +401,7 @@ fn seam_reason(frame: &SampledFrame) -> Option<MjpegVisualArtifactReason> {
|
||||
&& partial_luma_run_pct >= partial_run_threshold
|
||||
&& partial_luma_avg >= partial_luma_threshold as u16
|
||||
{
|
||||
partial_candidate_rows.push(row);
|
||||
match best_partial {
|
||||
Some((_, best_coverage, best_run, best_avg))
|
||||
if best_coverage > partial_luma_coverage
|
||||
@ -415,6 +420,7 @@ fn seam_reason(frame: &SampledFrame) -> Option<MjpegVisualArtifactReason> {
|
||||
}
|
||||
}
|
||||
if chroma_coverage >= chroma_coverage_threshold && chroma_avg >= chroma_threshold as u16 {
|
||||
chroma_candidate_rows.push(row);
|
||||
match best_chroma {
|
||||
Some((_, best_coverage, best_avg))
|
||||
if best_coverage > chroma_coverage
|
||||
@ -425,6 +431,9 @@ fn seam_reason(frame: &SampledFrame) -> Option<MjpegVisualArtifactReason> {
|
||||
}
|
||||
|
||||
if let Some((row, coverage, delta)) = best_chroma {
|
||||
if regular_texture_seam_rows(frame, &chroma_candidate_rows) {
|
||||
return None;
|
||||
}
|
||||
return Some(MjpegVisualArtifactReason::ChromaTileSeam {
|
||||
row_pct: pct(row, frame.height),
|
||||
coverage_pct: coverage.min(100) as u8,
|
||||
@ -432,6 +441,9 @@ fn seam_reason(frame: &SampledFrame) -> Option<MjpegVisualArtifactReason> {
|
||||
});
|
||||
}
|
||||
if let Some((row, coverage, run_pct, delta)) = best_partial {
|
||||
if regular_texture_seam_rows(frame, &partial_candidate_rows) {
|
||||
return None;
|
||||
}
|
||||
return Some(MjpegVisualArtifactReason::PartialTileSeam {
|
||||
row_pct: pct(row, frame.height),
|
||||
coverage_pct: coverage.min(100) as u8,
|
||||
@ -439,6 +451,9 @@ fn seam_reason(frame: &SampledFrame) -> Option<MjpegVisualArtifactReason> {
|
||||
delta,
|
||||
});
|
||||
}
|
||||
if regular_texture_seam_rows(frame, &luma_candidate_rows) {
|
||||
return None;
|
||||
}
|
||||
best_luma.map(|(row, coverage, delta)| MjpegVisualArtifactReason::HorizontalTileSeam {
|
||||
row_pct: pct(row, frame.height),
|
||||
coverage_pct: coverage.min(100) as u8,
|
||||
@ -446,6 +461,52 @@ fn seam_reason(frame: &SampledFrame) -> Option<MjpegVisualArtifactReason> {
|
||||
})
|
||||
}
|
||||
|
||||
fn regular_texture_seam_rows(frame: &SampledFrame, rows: &[usize]) -> bool {
|
||||
// Synthetic calibration grids create many evenly spaced hard edges; unlike a
|
||||
// tear, they are regular across the guard window rather than isolated.
|
||||
if rows.len() < 6 {
|
||||
return false;
|
||||
}
|
||||
let mut unique_rows = rows.to_vec();
|
||||
unique_rows.sort_unstable();
|
||||
unique_rows.dedup();
|
||||
if unique_rows.len() < 6 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let bounds = row_bounds(frame);
|
||||
let row_span = unique_rows
|
||||
.last()
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
.saturating_sub(unique_rows[0]);
|
||||
let guard_span = bounds.end.saturating_sub(bounds.start).max(1);
|
||||
if row_span.saturating_mul(100) < guard_span.saturating_mul(30) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut gaps: Vec<usize> = unique_rows
|
||||
.windows(2)
|
||||
.map(|pair| pair[1].saturating_sub(pair[0]))
|
||||
.filter(|gap| *gap > 0)
|
||||
.collect();
|
||||
if gaps.len() < 5 {
|
||||
return false;
|
||||
}
|
||||
gaps.sort_unstable();
|
||||
let median_gap = gaps[gaps.len() / 2];
|
||||
if median_gap == 0 || median_gap > frame.height.saturating_div(8).max(2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let tolerance = median_gap.saturating_div(3).max(1);
|
||||
let consistent_gaps = gaps
|
||||
.iter()
|
||||
.filter(|gap| gap.abs_diff(median_gap) <= tolerance)
|
||||
.count();
|
||||
consistent_gaps.saturating_mul(100) >= gaps.len().saturating_mul(70)
|
||||
}
|
||||
|
||||
fn inspect_sampled_frame_for_artifacts(frame: &SampledFrame) -> Option<MjpegVisualArtifactReason> {
|
||||
flat_block_reason(frame).or_else(|| seam_reason(frame))
|
||||
}
|
||||
|
||||
@ -206,6 +206,20 @@ fn smooth_rgb_frame(width: usize, height: usize) -> Vec<u8> {
|
||||
pixels
|
||||
}
|
||||
|
||||
fn checker_grid_rgb_frame(width: usize, height: usize) -> Vec<u8> {
|
||||
let mut pixels = Vec::with_capacity(width * height * 3);
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
let block = ((x / 16) + (y / 16)) % 2;
|
||||
let base = if block == 0 { 42 } else { 214 };
|
||||
pixels.push((base + (x % 17)) as u8);
|
||||
pixels.push((base + (y % 19)) as u8);
|
||||
pixels.push((base + ((x + y) % 13)) as u8);
|
||||
}
|
||||
}
|
||||
pixels
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pixel_guard_accepts_smooth_frames() {
|
||||
temp_env::with_vars(
|
||||
@ -220,6 +234,35 @@ fn pixel_guard_accepts_smooth_frames() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pixel_guard_accepts_regular_synthetic_checker_grids() {
|
||||
temp_env::with_vars(
|
||||
[
|
||||
("LESAVKA_UVC_MJPEG_PIXEL_GUARD", Some("1")),
|
||||
("LESAVKA_UVC_MJPEG_PIXEL_GUARD_SAMPLE_WIDTH", Some("160")),
|
||||
(
|
||||
"LESAVKA_UVC_MJPEG_PIXEL_GUARD_PARTIAL_SEAM_DELTA",
|
||||
Some("28"),
|
||||
),
|
||||
(
|
||||
"LESAVKA_UVC_MJPEG_PIXEL_GUARD_PARTIAL_SEAM_COVERAGE_PCT",
|
||||
Some("32"),
|
||||
),
|
||||
(
|
||||
"LESAVKA_UVC_MJPEG_PIXEL_GUARD_PARTIAL_SEAM_RUN_PCT",
|
||||
Some("12"),
|
||||
),
|
||||
],
|
||||
|| {
|
||||
let frame = checker_grid_rgb_frame(320, 180);
|
||||
assert_eq!(
|
||||
super::mjpeg_visual_guard::sampled_rgb_frame_for_test(320, 180, &frame),
|
||||
None
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pixel_guard_rejects_flat_lower_block_frames() {
|
||||
temp_env::with_vars(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user