fix: avoid false positive UVC seam freezes
This commit is contained in:
parent
e62023573c
commit
b82694125e
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1658,7 +1658,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_client"
|
||||
version = "0.26.3"
|
||||
version = "0.26.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@ -1692,7 +1692,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_common"
|
||||
version = "0.26.3"
|
||||
version = "0.26.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@ -1704,7 +1704,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_server"
|
||||
version = "0.26.3"
|
||||
version = "0.26.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.26.3"
|
||||
version = "0.26.4"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.26.3"
|
||||
version = "0.26.4"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.26.3"
|
||||
version = "0.26.4"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ const DEFAULT_PIXEL_GUARD_SEAM_COVERAGE_PCT: u32 = 42;
|
||||
const DEFAULT_PIXEL_GUARD_PARTIAL_SEAM_DELTA: u32 = 28;
|
||||
const DEFAULT_PIXEL_GUARD_PARTIAL_SEAM_COVERAGE_PCT: u32 = 32;
|
||||
const DEFAULT_PIXEL_GUARD_PARTIAL_SEAM_RUN_PCT: u32 = 12;
|
||||
const DEFAULT_PIXEL_GUARD_PARTIAL_SEAM_BLOCK_STDDEV: u32 = 14;
|
||||
const DEFAULT_PIXEL_GUARD_CHROMA_DELTA: u32 = 28;
|
||||
const DEFAULT_PIXEL_GUARD_CHROMA_COVERAGE_PCT: u32 = 34;
|
||||
|
||||
@ -73,6 +74,21 @@ fn partial_seam_run_pct_threshold() -> usize {
|
||||
.clamp(4, 60) as usize
|
||||
}
|
||||
|
||||
/// Resolve the partial-seam block-texture threshold.
|
||||
///
|
||||
/// Inputs: optional pixel-guard tuning. Output: luma stddev threshold. Why:
|
||||
/// natural shelf/chair/table edges can look like a partial seam on one row,
|
||||
/// but damaged codec slabs usually have a low-texture block on one side.
|
||||
fn partial_seam_block_stddev_threshold() -> f64 {
|
||||
f64::from(
|
||||
env_u32(
|
||||
"LESAVKA_UVC_MJPEG_PIXEL_GUARD_PARTIAL_SEAM_BLOCK_STDDEV",
|
||||
DEFAULT_PIXEL_GUARD_PARTIAL_SEAM_BLOCK_STDDEV,
|
||||
)
|
||||
.clamp(2, 60),
|
||||
)
|
||||
}
|
||||
|
||||
/// Resolve the chroma-seam threshold.
|
||||
///
|
||||
/// Inputs: optional pixel-guard tuning. Output: chroma delta threshold. Why:
|
||||
@ -122,6 +138,55 @@ fn row_bounds(frame: &SampledFrame) -> std::ops::Range<usize> {
|
||||
start..end.min(frame.height)
|
||||
}
|
||||
|
||||
fn region_luma_stddev(
|
||||
frame: &SampledFrame,
|
||||
row_start: usize,
|
||||
row_end: usize,
|
||||
col_start: usize,
|
||||
col_end: usize,
|
||||
) -> Option<f64> {
|
||||
if row_start >= row_end || col_start >= col_end || row_end > frame.height || col_end > frame.width
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let mut sum = 0f64;
|
||||
let mut sum_sq = 0f64;
|
||||
let mut count = 0usize;
|
||||
for row in row_start..row_end {
|
||||
let start = row.saturating_mul(frame.width);
|
||||
for col in col_start..col_end {
|
||||
let value = f64::from(frame.pixels[start + col].luma);
|
||||
sum += value;
|
||||
sum_sq += value * value;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
if count == 0 {
|
||||
return None;
|
||||
}
|
||||
let count_f = count as f64;
|
||||
let mean = sum / count_f;
|
||||
Some((sum_sq / count_f - mean * mean).max(0.0).sqrt())
|
||||
}
|
||||
|
||||
fn partial_seam_block_stddev(
|
||||
frame: &SampledFrame,
|
||||
row: usize,
|
||||
col_start: usize,
|
||||
col_end: usize,
|
||||
) -> Option<f64> {
|
||||
let window = frame.height.saturating_div(12).max(3);
|
||||
let above_start = row.saturating_sub(window);
|
||||
let above = region_luma_stddev(frame, above_start, row, col_start, col_end);
|
||||
let below_end = row.saturating_add(window).min(frame.height);
|
||||
let below = region_luma_stddev(frame, row, below_end, col_start, col_end);
|
||||
match (above, below) {
|
||||
(Some(above), Some(below)) => Some(above.min(below)),
|
||||
(Some(value), None) | (None, Some(value)) => Some(value),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect visible luma/chroma tile seams in a sampled MJPEG frame.
|
||||
///
|
||||
/// Inputs: decoded sampled frame. Output: the strongest seam reason, if any.
|
||||
@ -136,6 +201,7 @@ pub(super) fn seam_reason(frame: &SampledFrame) -> Option<MjpegVisualArtifactRea
|
||||
let partial_luma_threshold = partial_seam_delta_threshold();
|
||||
let partial_coverage_threshold = partial_seam_coverage_pct_threshold();
|
||||
let partial_run_threshold = partial_seam_run_pct_threshold();
|
||||
let partial_block_stddev_threshold = partial_seam_block_stddev_threshold();
|
||||
let chroma_threshold = chroma_delta_threshold();
|
||||
let chroma_coverage_threshold = chroma_coverage_pct_threshold();
|
||||
let mut best_luma: Option<(usize, usize, u16)> = None;
|
||||
@ -151,6 +217,9 @@ pub(super) fn seam_reason(frame: &SampledFrame) -> Option<MjpegVisualArtifactRea
|
||||
let mut luma_hits = 0usize;
|
||||
let mut partial_luma_hits = 0usize;
|
||||
let mut partial_run = 0usize;
|
||||
let mut partial_run_start = 0usize;
|
||||
let mut longest_partial_run_start = 0usize;
|
||||
let mut longest_partial_run_end = 0usize;
|
||||
let mut longest_partial_run = 0usize;
|
||||
let mut chroma_hits = 0usize;
|
||||
let mut luma_total = 0u32;
|
||||
@ -170,8 +239,15 @@ pub(super) fn seam_reason(frame: &SampledFrame) -> Option<MjpegVisualArtifactRea
|
||||
if luma_delta >= partial_luma_threshold as u16 {
|
||||
partial_luma_hits += 1;
|
||||
partial_luma_total += u32::from(luma_delta);
|
||||
if partial_run == 0 {
|
||||
partial_run_start = col;
|
||||
}
|
||||
partial_run += 1;
|
||||
longest_partial_run = longest_partial_run.max(partial_run);
|
||||
if partial_run > longest_partial_run {
|
||||
longest_partial_run = partial_run;
|
||||
longest_partial_run_start = partial_run_start;
|
||||
longest_partial_run_end = col + 1;
|
||||
}
|
||||
} else {
|
||||
partial_run = 0;
|
||||
}
|
||||
@ -200,6 +276,13 @@ pub(super) fn seam_reason(frame: &SampledFrame) -> Option<MjpegVisualArtifactRea
|
||||
&& partial_luma_coverage >= partial_coverage_threshold
|
||||
&& partial_luma_run_pct >= partial_run_threshold
|
||||
&& partial_luma_avg >= partial_luma_threshold as u16
|
||||
&& partial_seam_block_stddev(
|
||||
frame,
|
||||
row,
|
||||
longest_partial_run_start,
|
||||
longest_partial_run_end,
|
||||
)
|
||||
.is_some_and(|stddev| stddev <= partial_block_stddev_threshold)
|
||||
{
|
||||
partial_candidate_rows.push(row);
|
||||
match best_partial {
|
||||
|
||||
@ -280,6 +280,53 @@ fn pixel_guard_accepts_regular_synthetic_checker_grids() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Verifies textured scene edges are not misclassified as partial tile tears.
|
||||
fn pixel_guard_accepts_textured_partial_scene_edges() {
|
||||
temp_env::with_vars(
|
||||
[
|
||||
("LESAVKA_UVC_MJPEG_PIXEL_GUARD", Some("1")),
|
||||
(
|
||||
"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"),
|
||||
),
|
||||
(
|
||||
"LESAVKA_UVC_MJPEG_PIXEL_GUARD_PARTIAL_SEAM_BLOCK_STDDEV",
|
||||
Some("14"),
|
||||
),
|
||||
],
|
||||
|| {
|
||||
let width = 320;
|
||||
let height = 180;
|
||||
let mut frame = smooth_rgb_frame(width, height);
|
||||
let start_x = width / 4;
|
||||
let end_x = start_x + width / 3;
|
||||
for y in height / 2..height {
|
||||
for x in start_x..end_x {
|
||||
let offset = (y * width + x) * 3;
|
||||
let texture = ((x * 17 + y * 11) % 96) as u8;
|
||||
let value = 56u8.saturating_add(texture);
|
||||
frame[offset] = value;
|
||||
frame[offset + 1] = value.saturating_add((x % 23) as u8);
|
||||
frame[offset + 2] = value.saturating_sub((y % 19) as u8);
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
super::mjpeg_visual_guard::sampled_rgb_frame_for_test(width, height, &frame),
|
||||
None
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Verifies low-detail gray slabs freeze out before UVC handoff.
|
||||
fn pixel_guard_rejects_flat_lower_block_frames() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user