Files
Genarrative/server-rs/crates/api-server/src/jump_hop_atlas_slicing.rs
Linghong 95df62fc82 跳一跳UV面切分改用blob+gradient自适应算法
重构alpha.rs洋红去背预设参数
新增jump_hop_atlas_slicing.rs独立切图模块
修复jump_hop.rs调用链接入新切图算法
2026-06-10 19:49:39 +08:00

929 lines
32 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 跳一跳图集自适应切片算法模块。
// 提供两种基于图像内容的自适应 cell 边界检测算法:
// - SeedRefinement: 种子点精修(默认),在固定网格分界线附近搜索 density 最低点。
// - ValleyDetection: 全谷检测,高斯平滑 + 自适应阈值 + 合并 + 滑窗选最优。
// 两种算法均参数化,支持任意 rows × cols 网格配置,默认 6×3。
use axum::http::StatusCode;
use image;
use serde_json::json;
use crate::{
http_error::AppError,
jump_hop::{
JumpHopTileAtlasSlice, JumpHopTileFaceSlice, JumpHopTileFaceSlices,
JUMP_HOP_CREATION_PROVIDER, JUMP_HOP_TILE_UV_FACE_COLS, JUMP_HOP_TILE_UV_FACE_ROWS,
crop_jump_hop_tile_texture_cell, jump_hop_tile_face_key_label,
jump_hop_tile_type_by_index,
},
openai_image_generation::DownloadedOpenAiImage,
};
use shared_contracts::jump_hop::JumpHopTileFaceKey;
/// 默认 tile 行数
pub(crate) const DEFAULT_TILE_ROWS: u32 = 6;
/// 默认 tile 列数
pub(crate) const DEFAULT_TILE_COLS: u32 = 3;
/// 自适应切片算法类型(控制 atlas 级 6×3 cell 网格检测)
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum AtlasSliceAlgorithm {
/// 种子点精修:在固定网格分界线附近搜索 density 最低点(默认)
SeedRefinement,
/// 全谷检测:高斯平滑 + 自适应阈值 + 合并 + 滑窗选最优
ValleyDetection,
}
impl Default for AtlasSliceAlgorithm {
fn default() -> Self {
Self::SeedRefinement
}
}
/// 自适应 cell 网格检测结果
#[derive(Clone, Debug)]
pub(crate) struct AdaptiveCellGrid {
/// 行边界位置 [height],长度 = rows + 1
pub row_boundaries: Vec<u32>,
/// 列边界位置 [width],长度 = cols + 1
pub col_boundaries: Vec<u32>,
/// 使用的算法
#[allow(dead_code)]
pub algorithm: AtlasSliceAlgorithm,
}
// ============================================================================
// Density 计算
// ============================================================================
/// 从 RGBA 像素计算行投影 density每行非透明像素占比
pub(crate) fn compute_row_density(pixels: &[u8], width: u32, height: u32) -> Vec<f32> {
let w = width as usize;
let h = height as usize;
let stride = w * 4;
let mut density = vec![0.0f32; h];
let total = w as f32;
for y in 0..h {
let row_start = y * stride;
let mut content = 0u32;
for x in 0..w {
if pixels[row_start + x * 4 + 3] > 0 {
content += 1;
}
}
density[y] = content as f32 / total;
}
density
}
/// 从 RGBA 像素计算列投影 density每列非透明像素占比
pub(crate) fn compute_col_density(pixels: &[u8], width: u32, height: u32) -> Vec<f32> {
let w = width as usize;
let h = height as usize;
let stride = w * 4;
let mut density = vec![0.0f32; w];
let total = h as f32;
for x in 0..w {
let mut content = 0u32;
for y in 0..h {
if pixels[y * stride + x * 4 + 3] > 0 {
content += 1;
}
}
density[x] = content as f32 / total;
}
density
}
// ============================================================================
// 共享工具
// ============================================================================
/// 在 [seed-radius, seed+radius] 范围内找 density 最小值的 index
fn find_min_density_position(density: &[f32], seed: u32, radius: u32) -> u32 {
let lo = seed.saturating_sub(radius) as usize;
let hi = (seed + radius).min(density.len().saturating_sub(1) as u32) as usize;
if lo >= density.len() || lo > hi {
return seed;
}
let mut best = seed as usize;
let mut best_val = density[best.min(density.len() - 1)];
for i in lo..=hi {
if density[i] < best_val {
best_val = density[i];
best = i;
}
}
best as u32
}
/// 保证边界单调递增(禁止交叉)
fn enforce_monotonic(boundaries: &mut [u32]) {
for i in 1..boundaries.len() {
if boundaries[i] <= boundaries[i - 1] {
boundaries[i] = boundaries[i - 1] + 1;
}
}
}
// ============================================================================
// 算法 A: 种子点精修 (Seed Refinement)
// ============================================================================
/// 种子点精修:对每条固定网格分界线,在 ±radius 搜索窗口内找 density 最低点。
///
/// * `density` - 一维投影 density 序列
/// * `seeds` - 固定网格分界线位置(不含 0 和 max
/// * `radius` - 搜索半径
///
/// 返回精修后的分界线位置(不含 0 和 max
pub(crate) fn refine_boundaries_seed(
density: &[f32],
seeds: &[u32],
radius: u32,
) -> Vec<u32> {
let mut refined = Vec::with_capacity(seeds.len());
for &seed in seeds {
let pos = find_min_density_position(density, seed, radius);
refined.push(pos);
}
enforce_monotonic(&mut refined);
refined
}
/// 种子点精修完整流程:计算 density → 生成种子 → 精修 → 组装边界
pub(crate) fn detect_cell_grid_seed(
pixels: &[u8],
width: u32,
height: u32,
rows: u32,
cols: u32,
) -> AdaptiveCellGrid {
let row_density = compute_row_density(pixels, width, height);
let col_density = compute_col_density(pixels, width, height);
let cell_height = (height / rows).max(1);
let cell_width = (width / cols).max(1);
let radius_row = (cell_height / 3).max(1);
let radius_col = (cell_width / 3).max(1);
let row_seeds: Vec<u32> = (1..rows).map(|i| i * height / rows).collect();
let col_seeds: Vec<u32> = (1..cols).map(|i| i * width / cols).collect();
let row_splits = refine_boundaries_seed(&row_density, &row_seeds, radius_row);
let col_splits = refine_boundaries_seed(&col_density, &col_seeds, radius_col);
let mut row_boundaries = vec![0u32];
row_boundaries.extend(row_splits);
row_boundaries.push(height);
let mut col_boundaries = vec![0u32];
col_boundaries.extend(col_splits);
col_boundaries.push(width);
AdaptiveCellGrid {
row_boundaries,
col_boundaries,
algorithm: AtlasSliceAlgorithm::SeedRefinement,
}
}
// ============================================================================
// 算法 B: 谷检测 (Valley Detection)
// ============================================================================
/// 一维高斯平滑核
fn gaussian_smooth_1d(signal: &[f32], sigma: f32) -> Vec<f32> {
let n = signal.len();
if n == 0 {
return vec![];
}
let radius = (sigma * 3.0).ceil() as isize;
let mut kernel = Vec::new();
let mut kernel_sum = 0.0f32;
for i in -radius..=radius {
let w = (-(i as f32).powi(2) / (2.0 * sigma * sigma)).exp();
kernel.push(w);
kernel_sum += w;
}
for w in &mut kernel {
*w /= kernel_sum;
}
let mut result = vec![0.0f32; n];
for i in 0..n {
let mut acc = 0.0f32;
let mut w_sum = 0.0f32;
for (k, &w) in kernel.iter().enumerate() {
let idx = i as isize + k as isize - radius;
if idx >= 0 && idx < n as isize {
acc += signal[idx as usize] * w;
w_sum += w;
}
}
if w_sum > 0.0 {
result[i] = acc / w_sum;
}
}
result
}
/// 低于阈值的连续区间 → 候选谷列表
fn extract_valleys_below_threshold(
signal: &[f32],
threshold: f32,
) -> Vec<(usize, usize)> {
let n = signal.len();
let mut valleys = Vec::new();
let mut in_valley = false;
let mut start = 0usize;
for i in 0..n {
if signal[i] <= threshold {
if !in_valley {
start = i;
in_valley = true;
}
} else if in_valley {
valleys.push((start, i - 1));
in_valley = false;
}
}
if in_valley {
valleys.push((start, n - 1));
}
valleys
}
/// 合并间距 < min_gap 的相邻谷
fn merge_close_valleys(
valleys: &[(usize, usize)],
min_gap: usize,
) -> Vec<(usize, usize)> {
if valleys.is_empty() {
return vec![];
}
let mut merged = Vec::new();
let mut cur_start = valleys[0].0;
let mut cur_end = valleys[0].1;
for &(s, e) in &valleys[1..] {
if s - cur_end <= min_gap {
cur_end = e;
} else {
merged.push((cur_start, cur_end));
cur_start = s;
cur_end = e;
}
}
merged.push((cur_start, cur_end));
merged
}
/// 谷的几何中心
fn valley_centers(valleys: &[(usize, usize)]) -> Vec<u32> {
valleys.iter().map(|&(s, e)| ((s + e) / 2) as u32).collect()
}
/// 滑窗选最优 target_count 个谷:枚举连续组合,选间距最均匀的一组
fn select_spaced_valleys(
centers: &[u32],
expected_spacing: f32,
target_count: usize,
) -> Vec<u32> {
if centers.len() <= target_count {
return centers.to_vec();
}
let mut best_score = f32::MAX;
let mut best_centers = vec![];
for start in 0..=centers.len() - target_count {
let window = &centers[start..start + target_count];
let mut score = 0.0f32;
for i in 1..window.len() {
let ratio = (window[i] - window[i - 1]) as f32 / expected_spacing;
score += (ratio - 1.0).powi(2);
}
if score < best_score {
best_score = score;
best_centers = window.to_vec();
}
}
best_centers
}
/// 谷检测完整流程
pub(crate) fn refine_boundaries_valley(
density: &[f32],
expected_cell_count: u32,
expected_cell_size: f32,
total_length: u32,
) -> Result<Vec<u32>, &'static str> {
if expected_cell_count <= 1 {
return Ok(vec![]);
}
let expected_valleys = (expected_cell_count - 1) as usize;
// 步骤1: 高斯平滑
let sigma = expected_cell_size / 4.0;
let smoothed = gaussian_smooth_1d(density, sigma);
// 步骤2: 自适应阈值
let peak = smoothed.iter().cloned().fold(0.0f32, f32::max);
let threshold = f32::max(peak * 0.15, 0.02);
// 步骤3: 提取候选谷
let raw_valleys = extract_valleys_below_threshold(&smoothed, threshold);
if raw_valleys.is_empty() {
return Err("未检测到候选谷");
}
// 步骤4: 合并相邻谷
let min_gap = (expected_cell_size * 0.5) as usize;
let merged = merge_close_valleys(&raw_valleys, min_gap);
// 步骤5: 过滤窄噪声谷(宽度 < 3px
let filtered: Vec<_> = merged
.into_iter()
.filter(|&(s, e)| e >= s && e - s >= 3)
.collect();
if filtered.is_empty() {
return Err("过滤后无有效谷");
}
let centers = valley_centers(&filtered);
// 步骤6: 候选太多时按谷深排序取 top
let candidates = if centers.len() > expected_valleys + 2 {
let mut scored: Vec<_> = filtered
.iter()
.map(|&(s, e)| {
let avg = smoothed[s..=e].iter().sum::<f32>() / (e - s + 1) as f32;
let depth = peak - avg;
(depth, (s + e) / 2)
})
.collect();
scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
scored.truncate(expected_valleys + 2);
let mut c: Vec<u32> = scored.into_iter().map(|(_, center)| center as u32).collect();
c.sort();
c
} else {
centers
};
// 步骤7: 滑窗选最优 expected_valleys 个
let selected = select_spaced_valleys(&candidates, expected_cell_size, expected_valleys);
// 步骤8: 校验间距合理性
let min_spacing = (expected_cell_size * 0.5) as u32;
let max_spacing = (expected_cell_size * 1.8) as u32;
for i in 1..selected.len() {
let gap = selected[i] - selected[i - 1];
if gap < min_spacing || gap > max_spacing {
return Err("谷间距异常");
}
}
// 首尾不能太靠边
let min_edge = (expected_cell_size / 3.0) as u32;
if selected[0] < min_edge || total_length - selected[selected.len() - 1] < min_edge {
return Err("谷太靠近边界");
}
Ok(selected)
}
/// 谷检测完整流程:计算 density → 谷检测 → 组装边界
pub(crate) fn detect_cell_grid_valley(
pixels: &[u8],
width: u32,
height: u32,
rows: u32,
cols: u32,
) -> Result<AdaptiveCellGrid, &'static str> {
let row_density = compute_row_density(pixels, width, height);
let col_density = compute_col_density(pixels, width, height);
let cell_height = (height / rows).max(1) as f32;
let cell_width = (width / cols).max(1) as f32;
let row_splits = refine_boundaries_valley(&row_density, rows, cell_height, height)?;
let col_splits = refine_boundaries_valley(&col_density, cols, cell_width, width)?;
let mut row_boundaries = vec![0u32];
row_boundaries.extend(row_splits);
row_boundaries.push(height);
let mut col_boundaries = vec![0u32];
col_boundaries.extend(col_splits);
col_boundaries.push(width);
Ok(AdaptiveCellGrid {
row_boundaries,
col_boundaries,
algorithm: AtlasSliceAlgorithm::ValleyDetection,
})
}
// ============================================================================
// 主入口:自适应切片
// ============================================================================
/// 使用自适应算法对洋红去背后的图集进行切片。
///
/// * `image` - 洋红去背后的图集图片
/// * `rows` - cell 行数(默认 6
/// * `cols` - cell 列数(默认 3
/// * `algorithm` - 自适应算法
pub(crate) fn slice_tile_atlas_adaptive(
image: &DownloadedOpenAiImage,
rows: u32,
cols: u32,
algorithm: AtlasSliceAlgorithm,
) -> Result<Vec<JumpHopTileAtlasSlice>, AppError> {
let source = image::load_from_memory(image.bytes.as_slice())
.map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": JUMP_HOP_CREATION_PROVIDER,
"message": format!("跳一跳地板贴图图集解码失败:{error}"),
}))
})?
.to_rgba8();
let width = source.width();
let height = source.height();
let pixels = source.as_raw();
// 自适应检测 cell 网格
let grid = match algorithm {
AtlasSliceAlgorithm::SeedRefinement => {
detect_cell_grid_seed(pixels, width, height, rows, cols)
}
AtlasSliceAlgorithm::ValleyDetection => {
detect_cell_grid_valley(pixels, width, height, rows, cols)
.unwrap_or_else(|_| {
// 谷检测失败时回退到种子点精修
detect_cell_grid_seed(pixels, width, height, rows, cols)
})
}
};
if grid.row_boundaries.len() != (rows + 1) as usize
|| grid.col_boundaries.len() != (cols + 1) as usize
{
return Err(AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": JUMP_HOP_CREATION_PROVIDER,
"message": format!(
"自适应网格检测结果异常:期望 {}×{},实际 {}×{}",
rows + 1,
cols + 1,
grid.row_boundaries.len(),
grid.col_boundaries.len(),
),
})));
}
let tile_count = (rows * cols) as usize;
let mut slices = Vec::with_capacity(tile_count);
let mut index = 0usize;
for row in 0..rows {
for col in 0..cols {
let x0 = grid.col_boundaries[col as usize];
let x1 = grid.col_boundaries[col as usize + 1];
let y0 = grid.row_boundaries[row as usize];
let y1 = grid.row_boundaries[row as usize + 1];
let tile_width = x1.saturating_sub(x0).max(1);
let tile_height = y1.saturating_sub(y0).max(1);
let faces = slice_jump_hop_tile_uv_faces_blob(
&source,
x0,
y0,
tile_width,
tile_height,
row,
col,
)?;
slices.push(JumpHopTileAtlasSlice {
tile_type: jump_hop_tile_type_by_index(index),
source_atlas_cell: format!("row-{}-col-{}", row + 1, col + 1),
faces,
});
index += 1;
}
}
Ok(slices)
}
// ============================================================================
// Cell 内 UV 面提取(与固定网格逻辑相同,接收 cell 边界参数)
// ============================================================================
fn slice_jump_hop_tile_uv_faces_adaptive(
source: &image::RgbaImage,
tile_x: u32,
tile_y: u32,
tile_width: u32,
tile_height: u32,
atlas_row: u32,
atlas_col: u32,
) -> Result<JumpHopTileFaceSlices, AppError> {
let face_side = (tile_width / JUMP_HOP_TILE_UV_FACE_COLS)
.min(tile_height / JUMP_HOP_TILE_UV_FACE_ROWS)
.max(1);
let uv_width = face_side.saturating_mul(JUMP_HOP_TILE_UV_FACE_COLS);
let uv_height = face_side.saturating_mul(JUMP_HOP_TILE_UV_FACE_ROWS);
let uv_x = tile_x.saturating_add(tile_width.saturating_sub(uv_width) / 2);
let uv_y = tile_y.saturating_add(tile_height.saturating_sub(uv_height) / 2);
Ok(JumpHopTileFaceSlices {
top: slice_jump_hop_tile_uv_face_adaptive(
source, uv_x, uv_y, face_side, atlas_row, atlas_col, JumpHopTileFaceKey::Top, 1, 0,
)?,
front: slice_jump_hop_tile_uv_face_adaptive(
source, uv_x, uv_y, face_side, atlas_row, atlas_col, JumpHopTileFaceKey::Front, 1, 1,
)?,
right: slice_jump_hop_tile_uv_face_adaptive(
source, uv_x, uv_y, face_side, atlas_row, atlas_col, JumpHopTileFaceKey::Right, 2, 1,
)?,
back: slice_jump_hop_tile_uv_face_adaptive(
source, uv_x, uv_y, face_side, atlas_row, atlas_col, JumpHopTileFaceKey::Back, 3, 1,
)?,
left: slice_jump_hop_tile_uv_face_adaptive(
source, uv_x, uv_y, face_side, atlas_row, atlas_col, JumpHopTileFaceKey::Left, 0, 1,
)?,
bottom: slice_jump_hop_tile_uv_face_adaptive(
source, uv_x, uv_y, face_side, atlas_row, atlas_col, JumpHopTileFaceKey::Bottom, 1, 2,
)?,
})
}
#[allow(clippy::too_many_arguments)]
fn slice_jump_hop_tile_uv_face_adaptive(
source: &image::RgbaImage,
uv_x: u32,
uv_y: u32,
face_side: u32,
atlas_row: u32,
atlas_col: u32,
face: JumpHopTileFaceKey,
face_col: u32,
face_row: u32,
) -> Result<JumpHopTileFaceSlice, AppError> {
let cleaned = crop_jump_hop_tile_texture_cell(
source,
uv_x.saturating_add(face_col.saturating_mul(face_side)),
uv_y.saturating_add(face_row.saturating_mul(face_side)),
face_side,
face_side,
);
let mut cursor = std::io::Cursor::new(Vec::new());
cleaned
.write_to(&mut cursor, image::ImageFormat::Png)
.map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": JUMP_HOP_CREATION_PROVIDER,
"message": format!("跳一跳地板 UV 面贴图切割失败:{error}"),
}))
})?;
let face_label = jump_hop_tile_face_key_label(&face);
Ok(JumpHopTileFaceSlice {
face,
source_atlas_cell: format!(
"row-{}-col-{}/{}",
atlas_row + 1,
atlas_col + 1,
face_label
),
bytes: cursor.into_inner(),
})
}
// ============================================================================
// Blob + Gradient 驱动 UV 面切分
//
// 1. BFS 找主 blob构造仅含 blob 的 cleaned 图像
// 2. 行/列 density → 平滑 → gradient → 8 边界
// 3. 3×3 block → 5 有效块 → Block(1,2) 拆分 → 6 块
// 4. 每块 max opaque rectangle → 缩放
// ============================================================================
const BLOB_ALPHA: u8 = 48;
const MIN_BLOB_AREA: usize = 64;
const GRAD_SMOOTH: usize = 3;
// ---- 1. BFS 主 blob + 构造 cleaned 图像 ----
fn build_cleaned_tile(
source: &image::RgbaImage,
tile_x: u32, tile_y: u32, tile_w: u32, tile_h: u32,
) -> Option<image::RgbaImage> {
let pixels = source.as_raw();
let sw = source.width() as usize;
let stride = sw * 4;
let total = (tile_w * tile_h) as usize;
let mut visited = vec![false; total];
let mut queue = Vec::<usize>::new();
let mut best_comp = Vec::<usize>::new();
let idx = |lx: u32, ly: u32| (ly * tile_w + lx) as usize;
for sy in 0..tile_h {
for sx in 0..tile_w {
let si = idx(sx, sy);
if visited[si] { continue; }
let go = (tile_y as usize + sy as usize) * stride + (tile_x as usize + sx as usize) * 4;
if pixels[go + 3] < BLOB_ALPHA { continue; }
queue.clear();
queue.push(si);
visited[si] = true;
let mut qi = 0;
while qi < queue.len() {
let cur = queue[qi]; qi += 1;
let cx = cur as u32 % tile_w;
let cy = cur as u32 / tile_w;
for (dx, dy) in [(1i32,0i32),(-1,0),(0,1),(0,-1)] {
let nx = cx as i32 + dx;
let ny = cy as i32 + dy;
if nx < 0 || nx >= tile_w as i32 || ny < 0 || ny >= tile_h as i32 { continue; }
let ni = idx(nx as u32, ny as u32);
if visited[ni] { continue; }
let ngo = (tile_y as usize + ny as usize) * stride + (tile_x as usize + nx as usize) * 4;
if pixels[ngo + 3] >= BLOB_ALPHA {
visited[ni] = true;
queue.push(ni);
}
}
}
let area = queue.len();
if area < MIN_BLOB_AREA { continue; }
if area > best_comp.len() {
best_comp = queue.clone();
}
}
}
if best_comp.is_empty() { return None; }
// 构造 cleaned 图像:仅含主 blob
let mut cleaned = image::RgbaImage::new(tile_w, tile_h);
for &pi in &best_comp {
let lx = pi as u32 % tile_w;
let ly = pi as u32 / tile_w;
let gx = tile_x + lx;
let gy = tile_y + ly;
let go = (gy as usize * sw + gx as usize) * 4;
cleaned.put_pixel(lx, ly, image::Rgba([pixels[go], pixels[go+1], pixels[go+2], pixels[go+3]]));
}
Some(cleaned)
}
// ---- 2. density + gradient → 边界 ----
fn smooth_1d(signal: &[f32], window: usize) -> Vec<f32> {
if signal.len() <= window { return signal.to_vec(); }
let hw = window / 2;
(0..signal.len()).map(|i| {
let lo = i.saturating_sub(hw);
let hi = (i + hw).min(signal.len() - 1);
let sum: f32 = signal[lo..=hi].iter().sum();
sum / (hi - lo + 1) as f32
}).collect()
}
fn gradient(signal: &[f32]) -> Vec<f32> {
if signal.len() < 2 { return vec![]; }
(0..signal.len()-1).map(|i| signal[i+1] - signal[i]).collect()
}
/// 在 gradient 中找最强上升沿 (positive) 和最强下降沿 (negative) 的位置。
/// 返回 (peak_idx_pos, peak_idx_neg) 中最显著的 4 个位置,按值大小排序。
struct GradPeak { idx: usize, val: f32 }
fn find_gradient_peaks(grad: &[f32], count: usize, min_sep: usize) -> Vec<GradPeak> {
if grad.len() < 2 { return vec![]; }
// 取绝对值后找局部极大
let abs_grad: Vec<f32> = grad.iter().map(|&g| g.abs()).collect();
let mut peaks: Vec<GradPeak> = (1..abs_grad.len()-1)
.filter(|&i| abs_grad[i] > abs_grad[i-1] && abs_grad[i] >= abs_grad[i+1] && abs_grad[i] > 0.0)
.map(|i| GradPeak { idx: i, val: grad[i] }) // 保留符号
.collect();
peaks.sort_by(|a, b| b.val.abs().partial_cmp(&a.val.abs()).unwrap_or(std::cmp::Ordering::Equal));
// 去重:间距小于 min_sep 的只保留最大者
let mut chosen = Vec::new();
for p in peaks {
if chosen.iter().all(|c: &GradPeak| (c.idx as isize - p.idx as isize).unsigned_abs() >= min_sep) {
chosen.push(p);
if chosen.len() >= count { break; }
}
}
chosen
}
/// 行 density → gradient → y₀,y₁,y₂,y₃
fn detect_row_boundaries(cleaned: &image::RgbaImage, tile_w: u32, tile_h: u32) -> Option<(u32,u32,u32,u32)> {
// 行 density
let mut row_density = Vec::with_capacity(tile_h as usize);
for y in 0..tile_h {
let mut cnt = 0u32;
for x in 0..tile_w {
if cleaned.get_pixel(x, y).0[3] >= BLOB_ALPHA { cnt += 1; }
}
row_density.push(cnt as f32 / tile_w as f32);
}
let smooth = smooth_1d(&row_density, GRAD_SMOOTH);
let grad = gradient(&smooth);
let peaks = find_gradient_peaks(&grad, 4, 4);
if peaks.len() < 2 { return None; }
// 分离正负
let pos: Vec<_> = peaks.iter().filter(|p| p.val > 0.0).collect();
let neg: Vec<_> = peaks.iter().filter(|p| p.val < 0.0).collect();
if pos.len() < 1 || neg.len() < 1 { return None; }
// y₁: 最强正峰(窄→宽); y₂: 最强负峰(宽→窄)
let y1 = pos[0].idx as u32;
let y2 = neg[0].idx as u32;
let y0 = row_density.iter().position(|&d| d > 0.0).unwrap_or(0) as u32;
let y3 = (row_density.len() as u32).saturating_sub(
1 + row_density.iter().rev().position(|&d| d > 0.0).unwrap_or(0) as u32
) + 1;
if y1 < y0 + 2 || y2 <= y1 + 6 || y2 > y3.saturating_sub(2) { return None; }
Some((y0, y1, y2, y3))
}
/// 列高度 profile每列 blob 的首次/末次行)→ gradient → x₀,x₁,x₂,x₃
fn detect_col_boundaries(cleaned: &image::RgbaImage, tile_w: u32, _tile_h: u32, y0: u32, y3: u32) -> Option<(u32,u32,u32,u32)> {
// 每列的 blob 高度
let mut col_height = Vec::with_capacity(tile_w as usize);
for x in 0..tile_w {
let first = (y0..y3).find(|&y| cleaned.get_pixel(x, y).0[3] >= BLOB_ALPHA);
let last = (y0..y3).rev().find(|&y| cleaned.get_pixel(x, y).0[3] >= BLOB_ALPHA);
col_height.push(
first.map_or(0.0, |f| {
let l = last.unwrap_or(f);
(l - f + 1) as f32 / (y3 - y0).max(1) as f32
})
);
}
let smooth = smooth_1d(&col_height, GRAD_SMOOTH);
let grad = gradient(&smooth);
let peaks = find_gradient_peaks(&grad, 4, 4);
if peaks.len() < 2 { return None; }
let pos: Vec<_> = peaks.iter().filter(|p| p.val > 0.0).collect();
let neg: Vec<_> = peaks.iter().filter(|p| p.val < 0.0).collect();
if pos.len() < 1 || neg.len() < 1 { return None; }
let x1 = pos[0].idx as u32;
let x2 = neg[0].idx as u32;
let x0 = col_height.iter().position(|&d| d > 0.0).unwrap_or(0) as u32;
let x3 = (tile_w as usize).saturating_sub(
1 + col_height.iter().rev().position(|&d| d > 0.0).unwrap_or(0)
) as u32 + 1;
if x1 < x0 + 2 || x2 <= x1 + 6 || x2 > x3.saturating_sub(2) { return None; }
Some((x0, x1, x2, x3))
}
// ---- 3. max opaque rectangle per block ----
/// 在 block 范围内基于 histogram 找最大全不透明矩形。
fn max_opaque_rect(
cleaned: &image::RgbaImage,
bx0: u32, by0: u32, bw: u32, bh: u32,
) -> Option<(u32, u32, u32, u32)> {
let mut heights = vec![0u32; bw as usize];
let mut best_area = 0u32;
let mut best = (0u32, 0u32, 1u32, 1u32);
for ly in 0..bh {
for lx in 0..bw {
if cleaned.get_pixel(bx0 + lx, by0 + ly).0[3] >= BLOB_ALPHA {
heights[lx as usize] += 1;
} else {
heights[lx as usize] = 0;
}
}
// histogram max rect
let mut stack: Vec<(u32, u32)> = Vec::new(); // (start_x, height)
for (x, &h) in heights.iter().enumerate() {
let x = x as u32;
let mut start = x;
while stack.last().map_or(false, |&(_, sh)| sh > h) {
let (sx, sh) = stack.pop().unwrap();
let area = sh * (x - sx);
if area > best_area {
best_area = area;
best = (bx0 + sx, by0 + ly - sh + 1, x - sx, sh);
}
start = sx;
}
if h > 0 && stack.last().map_or(true, |&(_, sh)| h > sh) {
stack.push((start, h));
}
}
let x = bw;
while let Some((sx, sh)) = stack.pop() {
let area = sh * (x - sx);
if area > best_area {
best_area = area;
best = (bx0 + sx, by0 + ly as u32 - sh + 1, x - sx, sh);
}
}
}
if best_area == 0 { None } else { Some(best) }
}
// ---- 4. 主编排 ----
fn slice_jump_hop_tile_uv_faces_blob(
source: &image::RgbaImage,
tile_x: u32, tile_y: u32, tile_w: u32, tile_h: u32,
atlas_row: u32, atlas_col: u32,
) -> Result<JumpHopTileFaceSlices, AppError> {
let fallback = || {
slice_jump_hop_tile_uv_faces_adaptive(source, tile_x, tile_y, tile_w, tile_h, atlas_row, atlas_col)
};
// Step 1: BFS 主 blob → cleaned 图像
let cleaned = match build_cleaned_tile(source, tile_x, tile_y, tile_w, tile_h) {
Some(c) => c,
None => return fallback(),
};
// Step 2: gradient 边界检测
let (y0, y1, y2, y3) = match detect_row_boundaries(&cleaned, tile_w, tile_h) {
Some(v) => v,
None => return fallback(),
};
let (x0, x1, x2, x3) = match detect_col_boundaries(&cleaned, tile_w, tile_h, y0, y3) {
Some(v) => v,
None => return fallback(),
};
// Step 3: 3×3 block → 5 有效块 + Block(1,2) 拆分 → 6 块
// blocks: (row, col): 0=Top, 1,0=Left, 1,1=Front, 1,2=Right+Back, 2=Bottom
let blocks: [(u32,u32,u32,u32); 5] = [
(x1, y0, x2 - x1, y1 - y0), // Top
(x0, y1, x1 - x0, y2 - y1), // Left
(x1, y1, x2 - x1, y2 - y1), // Front
(x2, y1, x3 - x2, y2 - y1), // Right+Back
(x1, y2, x2 - x1, y3 - y2), // Bottom
];
// Step 4: max opaque rectangle per block
let rect = |b: (u32,u32,u32,u32), name: &str| -> Result<(u32,u32,u32,u32), AppError> {
max_opaque_rect(&cleaned, b.0, b.1, b.2, b.3).ok_or_else(|| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": JUMP_HOP_CREATION_PROVIDER,
"message": format!("blob gradient: {name} 面无有效内容"),
}))
})
};
let top = rect(blocks[0], "Top")?;
let left = rect(blocks[1], "Left")?;
let front = rect(blocks[2], "Front")?;
// Right+Back → 从中点拆分
let (rb_x0, rb_y0, rb_w, rb_h) = blocks[3];
let mid = rb_x0 + rb_w / 2;
let right_rect = rect((rb_x0, rb_y0, mid - rb_x0, rb_h), "Right")?;
let back_rect = rect((mid, rb_y0, rb_x0 + rb_w - mid, rb_h), "Back")?;
let bottom = rect(blocks[4], "Bottom")?;
// Step 5: crop (tile_local → global)
let global = |r: (u32,u32,u32,u32)| (tile_x + r.0, tile_y + r.1, r.2, r.3);
let mk = |r: (u32,u32,u32,u32), face: JumpHopTileFaceKey| -> Result<JumpHopTileFaceSlice, AppError> {
let (gx, gy, gw, gh) = global(r);
let cleaned_dyn = crop_jump_hop_tile_texture_cell(source, gx, gy, gw, gh);
let mut cursor = std::io::Cursor::new(Vec::new());
cleaned_dyn.write_to(&mut cursor, image::ImageFormat::Png).map_err(|e| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": JUMP_HOP_CREATION_PROVIDER,
"message": format!("跳一跳地板 UV 面贴图切割失败:{e}"),
}))
})?;
let label = jump_hop_tile_face_key_label(&face);
Ok(JumpHopTileFaceSlice {
face,
source_atlas_cell: format!("row-{}-col-{}/{}", atlas_row + 1, atlas_col + 1, label),
bytes: cursor.into_inner(),
})
};
Ok(JumpHopTileFaceSlices {
top: mk(top, JumpHopTileFaceKey::Top)?,
left: mk(left, JumpHopTileFaceKey::Left)?,
front: mk(front, JumpHopTileFaceKey::Front)?,
right: mk(right_rect, JumpHopTileFaceKey::Right)?,
back: mk(back_rect, JumpHopTileFaceKey::Back)?,
bottom: mk(bottom, JumpHopTileFaceKey::Bottom)?,
})
}