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]]
|
[[package]]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.26.3"
|
version = "0.26.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -1692,7 +1692,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.26.3"
|
version = "0.26.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
@ -1704,7 +1704,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.26.3"
|
version = "0.26.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.26.3"
|
version = "0.26.4"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.26.3"
|
version = "0.26.4"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.26.3"
|
version = "0.26.4"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
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_DELTA: u32 = 28;
|
||||||
const DEFAULT_PIXEL_GUARD_PARTIAL_SEAM_COVERAGE_PCT: u32 = 32;
|
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_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_DELTA: u32 = 28;
|
||||||
const DEFAULT_PIXEL_GUARD_CHROMA_COVERAGE_PCT: u32 = 34;
|
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
|
.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.
|
/// Resolve the chroma-seam threshold.
|
||||||
///
|
///
|
||||||
/// Inputs: optional pixel-guard tuning. Output: chroma delta threshold. Why:
|
/// 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)
|
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.
|
/// Detect visible luma/chroma tile seams in a sampled MJPEG frame.
|
||||||
///
|
///
|
||||||
/// Inputs: decoded sampled frame. Output: the strongest seam reason, if any.
|
/// 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_luma_threshold = partial_seam_delta_threshold();
|
||||||
let partial_coverage_threshold = partial_seam_coverage_pct_threshold();
|
let partial_coverage_threshold = partial_seam_coverage_pct_threshold();
|
||||||
let partial_run_threshold = partial_seam_run_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_threshold = chroma_delta_threshold();
|
||||||
let chroma_coverage_threshold = chroma_coverage_pct_threshold();
|
let chroma_coverage_threshold = chroma_coverage_pct_threshold();
|
||||||
let mut best_luma: Option<(usize, usize, u16)> = None;
|
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 luma_hits = 0usize;
|
||||||
let mut partial_luma_hits = 0usize;
|
let mut partial_luma_hits = 0usize;
|
||||||
let mut partial_run = 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 longest_partial_run = 0usize;
|
||||||
let mut chroma_hits = 0usize;
|
let mut chroma_hits = 0usize;
|
||||||
let mut luma_total = 0u32;
|
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 {
|
if luma_delta >= partial_luma_threshold as u16 {
|
||||||
partial_luma_hits += 1;
|
partial_luma_hits += 1;
|
||||||
partial_luma_total += u32::from(luma_delta);
|
partial_luma_total += u32::from(luma_delta);
|
||||||
|
if partial_run == 0 {
|
||||||
|
partial_run_start = col;
|
||||||
|
}
|
||||||
partial_run += 1;
|
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 {
|
} else {
|
||||||
partial_run = 0;
|
partial_run = 0;
|
||||||
}
|
}
|
||||||
@ -200,6 +276,13 @@ pub(super) fn seam_reason(frame: &SampledFrame) -> Option<MjpegVisualArtifactRea
|
|||||||
&& partial_luma_coverage >= partial_coverage_threshold
|
&& partial_luma_coverage >= partial_coverage_threshold
|
||||||
&& partial_luma_run_pct >= partial_run_threshold
|
&& partial_luma_run_pct >= partial_run_threshold
|
||||||
&& partial_luma_avg >= partial_luma_threshold as u16
|
&& 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);
|
partial_candidate_rows.push(row);
|
||||||
match best_partial {
|
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]
|
#[test]
|
||||||
/// Verifies low-detail gray slabs freeze out before UVC handoff.
|
/// Verifies low-detail gray slabs freeze out before UVC handoff.
|
||||||
fn pixel_guard_rejects_flat_lower_block_frames() {
|
fn pixel_guard_rejects_flat_lower_block_frames() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user