// 跳一跳图集自适应切片算法模块。 // 提供两种基于图像内容的自适应 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, /// 列边界位置 [width],长度 = cols + 1 pub col_boundaries: Vec, /// 使用的算法 #[allow(dead_code)] pub algorithm: AtlasSliceAlgorithm, } // ============================================================================ // Density 计算 // ============================================================================ /// 从 RGBA 像素计算行投影 density(每行非透明像素占比) pub(crate) fn compute_row_density(pixels: &[u8], width: u32, height: u32) -> Vec { 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 { 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 { 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 = (1..rows).map(|i| i * height / rows).collect(); let col_seeds: Vec = (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 { 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 { 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 { 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 = ¢ers[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, &'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::() / (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 = 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 { 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, 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 { 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 { 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 { 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::::new(); let mut best_comp = Vec::::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 { 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 { 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 { if grad.len() < 2 { return vec![]; } // 取绝对值后找局部极大 let abs_grad: Vec = grad.iter().map(|&g| g.abs()).collect(); let mut peaks: Vec = (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 { 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 { 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)?, }) }