@@ -0,0 +1,928 @@
// 跳一跳图集自适应切片算法模块。
// 提供两种基于图像内容的自适应 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.0 f32 ; h ] ;
let total = w as f32 ;
for y in 0 .. h {
let row_start = y * stride ;
let mut content = 0 u32 ;
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.0 f32 ; w ] ;
let total = h as f32 ;
for x in 0 .. w {
let mut content = 0 u32 ;
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! [ 0 u32 ] ;
row_boundaries . extend ( row_splits ) ;
row_boundaries . push ( height ) ;
let mut col_boundaries = vec! [ 0 u32 ] ;
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.0 f32 ;
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.0 f32 ; n ] ;
for i in 0 .. n {
let mut acc = 0.0 f32 ;
let mut w_sum = 0.0 f32 ;
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 = 0 usize ;
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.0 f32 ;
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.0 f32 , 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! [ 0 u32 ] ;
row_boundaries . extend ( row_splits ) ;
row_boundaries . push ( height ) ;
let mut col_boundaries = vec! [ 0 u32 ] ;
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 = 0 usize ;
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 [ ( 1 i32 , 0 i32 ) , ( - 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 = 0 u32 ;
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! [ 0 u32 ; bw as usize ] ;
let mut best_area = 0 u32 ;
let mut best = ( 0 u32 , 0 u32 , 1 u32 , 1 u32 ) ;
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 ) ? ,
} )
}