fix(design): 混合透明度不再被误判为「全透明」(复审 r1 p2-1)
先按 alpha 过滤像素、再做块平均(整块全透明则跳过)。原实现先平均再比门槛, 块内 255 与 0 的整数除法得 127 < 128 → 512×512 里 131072 个不透明像素被报 NoOpaquePixels,UI 会告诉用户「背景图几乎全透明」,与事实相反。 - 新增 half_transparent_image_keeps_its_opaque_colour(红) - 新增 mostly_transparent_image_still_samples_the_visible_part - 模块文档加一行「混合透明度」的处置与踩坑说明
This commit is contained in:
1 parent
36c1cce00d
commit
2ce394fbfc
2 files changed
+59
-11
No files matched your search
@@ -26,6 +26,7 @@
|
||||
//! | 纯色图(有彩度) | 正常取色 | 单色图只要够鲜艳就是合格的种子(不做特殊化) |
|
||||
//! | 灰度/灰白图 | [`Failure::LowChroma`](**不是**返回青灰色) | D40 实测:零彩度种子的 CAM16 色相恒为 209.49°,硬取色会把灰白壁纸「凭空上色」成青色 `#006874`;极低彩度段色相还是噪声放大器(`#808080` → 209.5°、`#7A7C7E` → 231.2°,摆 22°) |
|
||||
//! | 超大图(>65536 像素) | 先按 4^n 块做**块平均**降采样,再量化 | 取色只需要「色彩分布」,不需要分辨率;块平均能保住细纹理的平均色,避免等距抽样在条纹/网格图上走样 |
|
||||
//! | 混合透明度(一半不透明一半全透明) | **先按 alpha 过滤、再块平均**(整块全透明则跳过) | 「先平均再比门槛」会把 `255/2 = 127 < 128` 判成全透明,于是 13 万个不透明像素被报成 `NoOpaquePixels`——复审实测踩到的坑 |
|
||||
//! | 解码失败 | 不在本 crate:见 `crates/palette-extract` | D40 分层 |
|
||||
//!
|
||||
//! 阈值 [`MIN_SEED_CHROMA`] = 8 是 **D40 的定案值**(`seed chroma < ~8 视为无信息量`),
|
||||
@@ -138,16 +139,13 @@ pub fn analyze_rgba(pixels: &[u8], width: u32, height: u32) -> Result<Seed, Fail
|
||||
});
|
||||
}
|
||||
|
||||
// 采样点已经是「只由不透明像素算出的块平均」(见 [`sample_blocks`]),这里不再按 alpha 过滤。
|
||||
let samples = sample_blocks(pixels, width as usize, height as usize);
|
||||
let opaque: Vec<Color> = samples
|
||||
.into_iter()
|
||||
.filter(|color| color.a >= ALPHA_THRESHOLD)
|
||||
.collect();
|
||||
if opaque.is_empty() {
|
||||
if samples.is_empty() {
|
||||
return Err(Failure::NoOpaquePixels);
|
||||
}
|
||||
|
||||
let clusters = quantize::quantize(&opaque);
|
||||
let clusters = quantize::quantize(&samples);
|
||||
let ranked = score::rank(&clusters);
|
||||
match ranked.first() {
|
||||
Some(best) if best.chroma >= MIN_SEED_CHROMA => Ok(Seed {
|
||||
@@ -183,6 +181,15 @@ fn most_populated(clusters: &[Cluster]) -> Option<&Cluster> {
|
||||
}
|
||||
|
||||
/// 块平均降采样:块边长按「不超过 [`MAX_SAMPLES`] 个采样点」反推,小图不降采样(块边长 1)。
|
||||
///
|
||||
/// **先按 alpha 过滤、再求平均**:块内只累加 `a >= ALPHA_THRESHOLD` 的像素,整块没有有效像素
|
||||
/// 就直接跳过(不产出采样点)。
|
||||
///
|
||||
/// 为什么不能「先平均、再拿平均 alpha 跟门槛比」:块内 alpha 用整数除法截断,
|
||||
/// 一半不透明一半透明的块平均下来是 `255/2 = 127 < 128`,于是**整张图被判成「没有不透明像素」**
|
||||
/// (复审实测:512×512 里 131072 个完全不透明像素 → `Err(NoOpaquePixels)`)。
|
||||
/// 副作用是「半透明像素的颜色」也被排除在外——这是有意的:半透明像素的 RGB 常常是
|
||||
/// 未预乘的原始值(有些解码器给的是 0,0,0),混进来会把种子拽向黑色。
|
||||
fn sample_blocks(pixels: &[u8], width: usize, height: usize) -> Vec<Color> {
|
||||
let mut block = 1usize;
|
||||
while width.div_ceil(block) * height.div_ceil(block) > MAX_SAMPLES {
|
||||
@@ -196,19 +203,24 @@ fn sample_blocks(pixels: &[u8], width: usize, height: usize) -> Vec<Color> {
|
||||
let mut left = 0usize;
|
||||
while left < width {
|
||||
let right = (left + block).min(width);
|
||||
let mut sum = [0u64; 4];
|
||||
let mut sum = [0u64; 3];
|
||||
let mut count = 0u64;
|
||||
for y in top..bottom {
|
||||
for x in left..right {
|
||||
let offset = (y * width + x) * 4;
|
||||
for (channel, slot) in sum.iter_mut().enumerate() {
|
||||
*slot += u64::from(pixels[offset + channel]);
|
||||
if pixels[offset + 3] < ALPHA_THRESHOLD {
|
||||
continue;
|
||||
}
|
||||
for channel in 0..3 {
|
||||
sum[channel] += u64::from(pixels[offset + channel]);
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
let average = |channel: usize| (sum[channel] / count) as u8;
|
||||
samples.push(Color::rgba(average(0), average(1), average(2), average(3)));
|
||||
if count > 0 {
|
||||
let average = |channel: usize| (sum[channel] / count) as u8;
|
||||
samples.push(Color::rgb(average(0), average(1), average(2)));
|
||||
}
|
||||
left = right;
|
||||
}
|
||||
top = bottom;
|
||||
|
||||
@@ -137,6 +137,42 @@ fn fully_transparent_image_has_no_seed() {
|
||||
assert_eq!(analyze_rgba(&pixels, 4, 4), Err(Failure::NoOpaquePixels));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn half_transparent_image_keeps_its_opaque_colour() {
|
||||
// 复审 p2-1 的反证:512×512、一半完全不透明砖红、一半全透明(RGB=0)交错。
|
||||
// 修复前「先块平均、再拿平均 alpha 比 128」:块内 255 与 0 平均被整数除法截成 127 < 128,
|
||||
// 于是全部 131072 个不透明像素被判成「没有不透明像素」→ Err(NoOpaquePixels)。
|
||||
// 修复后(先过滤再平均)必须取到砖红本身。
|
||||
let (width, height) = (512usize, 512usize);
|
||||
let mut pixels = Vec::with_capacity(width * height * 4);
|
||||
for index in 0..width * height {
|
||||
if index % 2 == 0 {
|
||||
pixels.extend_from_slice(&[0xC0, 0x39, 0x2B, 0xFF]);
|
||||
} else {
|
||||
pixels.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
|
||||
}
|
||||
}
|
||||
let seed = analyze_rgba(&pixels, 512, 512).expect("一半不透明像素必须能取色");
|
||||
assert_eq!(seed.color, Color::rgb(0xC0, 0x39, 0x2B));
|
||||
assert!(seed.chroma > 40.0, "彩度 {}", seed.chroma);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mostly_transparent_image_still_samples_the_visible_part() {
|
||||
// 90% 全透明 + 10% 不透明绿:可见部分够鲜艳就该取到它,而不是报「没有不透明像素」。
|
||||
let (width, height) = (64usize, 64usize);
|
||||
let mut pixels = Vec::with_capacity(width * height * 4);
|
||||
for index in 0..width * height {
|
||||
if index % 10 == 0 {
|
||||
pixels.extend_from_slice(&[0x2E, 0x7D, 0x4F, 0xFF]);
|
||||
} else {
|
||||
pixels.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
|
||||
}
|
||||
}
|
||||
let seed = analyze_rgba(&pixels, 64, 64).expect("10% 可见像素必须能取色");
|
||||
assert_eq!(seed.color, Color::rgb(0x2E, 0x7D, 0x4F));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_image_is_downsampled_and_still_deterministic() {
|
||||
let (width, height) = (512_u32, 512_u32);
|
||||
|
||||
Reference in new issue
Block a user