diff --git a/README.md b/README.md index 584fa95..093688d 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,7 @@ import '@shopify/react-native-skia'; **几乎总是**因为 Metro 同时解析到多份 reanimated / skia / gesture-handler / fast-opencv / safe-area 等包。 **最佳实践**: + - 直接复制 `example/metro.config.js` 里的 `singletonPackages` + extraNodeModules + blockList 写法 - 在你的 `index.js` 最顶部加入 gesture-handler → reanimated → skia 三个 import - 重启 Metro (`--reset-cache`) + 重装 app @@ -141,16 +142,18 @@ import MaskSegmentCanvas, { 主要导出一览: -| 类别 | 名称 | -| ---- | ---- | -| 组件 | `MaskSegmentCanvas`(default) | -| Ref / Props 类型 | `MaskSegmentCanvasRef`、`MaskSegmentCanvasProps` | -| 会话 / 回调类型 | `MaskSegmentSession`、`PaintCallbackPayload`、`PaintedRegionRecord`、`SavePaintResult` | -| Watch 类型 | `MaskSegmentWatchState`、`MaskSegmentWatchDetail` | -| 配置类型 | `PipelineConfig`、`MaskSegmentConfig`、`PaintConfig`、`InteractionConfig` | -| 语义色 | `MASK_SEMANTIC_COLORS`、`BASEBOARD_SEMANTIC_NAME` | -| 工具 | `prewarmPngBgrCacheAsync`、`prewarmPngBgrCache` | -| 运行时 | `DEFAULT_*_CONFIG`、`getMaskSegmentRuntimeConfig`、`setMaskSegmentRuntimeConfig` | + +| 类别 | 名称 | +| -------------- | ----------------------------------------------------------------------------------- | +| 组件 | `MaskSegmentCanvas`(default) | +| Ref / Props 类型 | `MaskSegmentCanvasRef`、`MaskSegmentCanvasProps` | +| 会话 / 回调类型 | `MaskSegmentSession`、`PaintCallbackPayload`、`PaintedRegionRecord`、`SavePaintResult` | +| Watch 类型 | `MaskSegmentWatchState`、`MaskSegmentWatchDetail` | +| 配置类型 | `PipelineConfig`、`MaskSegmentConfig`、`PaintConfig`、`InteractionConfig` | +| 语义色 | `MASK_SEMANTIC_COLORS`、`BASEBOARD_SEMANTIC_NAME` | +| 工具 | `prewarmPngBgrCacheAsync`、`prewarmPngBgrCache` | +| 运行时 | `DEFAULT_*_CONFIG`、`getMaskSegmentRuntimeConfig`、`setMaskSegmentRuntimeConfig` | + --- @@ -384,16 +387,16 @@ export function PaintScreen() { #### 示例里涉及的 state 说明 -| state | 类型 | 用途 | -| ------------------ | -------------------------- | ---- | -| `imagePaths` | `{ origin, mask } \| null` | 业务侧解析后的本地/远程图片路径 | -| `pathsError` | `string` | 路径解析或 PNG 预热失败文案 | -| `watchState` | `MaskSegmentWatchState \| ''` | `onWatch` 上报的初始化阶段 | -| `isInteractive` | 派生 | `interactive` 或 `mask_paths_ready` 时为 true,可开放操作 | -| `isOutlineReady` | 派生 | `mask_paths_ready` 时为 true,轮播虚线已就绪 | -| `isCanvasLoading` | 派生 | 画布初始化阻塞 Loading(不含 PNG 路径等待) | -| `errorMessage` | `string` | 由 `onError` 写入的分割/加载失败文案 | -| `sessionDraft` | `MaskSegmentSession \| null` | MMKV 等恢复的草稿 | +| state | 类型 | 用途 | +| ----------------- | ---------------------------- | ------------------------------------------------ | +| `imagePaths` | `{ origin, mask } | null` | 业务侧解析后的本地/远程图片路径 | +| `pathsError` | `string` | 路径解析或 PNG 预热失败文案 | +| `watchState` | `MaskSegmentWatchState | ''` | `onWatch` 上报的初始化阶段 | +| `isInteractive` | 派生 | `interactive` 或 `mask_paths_ready` 时为 true,可开放操作 | +| `isOutlineReady` | 派生 | `mask_paths_ready` 时为 true,轮播虚线已就绪 | +| `isCanvasLoading` | 派生 | 画布初始化阻塞 Loading(不含 PNG 路径等待) | +| `errorMessage` | `string` | 由 `onError` 写入的分割/加载失败文案 | +| `sessionDraft` | `MaskSegmentSession | null` | MMKV 等恢复的草稿 | #### 配置项怎么选 @@ -447,22 +450,26 @@ const hasError = watchState === 'error'; ### 图片与初始化 -| 属性 | 类型 | 必填 | 默认 | 说明 | -| ---- | ---- | ---- | ---- | ---- | -| `originUrl` | `string` | 是* | — | 原图地址(`file://`、绝对路径或 `http(s)://`) | -| `maskUrl` | `string` | 是* | — | 掩码图地址(语义色块图,建议与原图同尺寸) | -| `originImgPath` | `string` | — | — | **deprecated**,请用 `originUrl` | -| `maskImgPath` | `string` | — | — | **deprecated**,请用 `maskUrl` | -| `initialSession` | `MaskSegmentSession` | 否 | — | 从 MMKV 等恢复的草稿;分区就绪后自动 `loadSession` | -| `initialPaintColor` | `BgrColor` | 否 | — | **可选**。初始自定义笔刷色 `{ b, g, r }`;不传则默认无笔刷,需用户选色或 `ref.setPaintColor` | -| `initialPaintConfigJson` | `Record` | 否 | — | **可选**。与 `initialPaintColor` 配套的笔刷配置,上色成功时随 `onPaintCallback` 回传 | + +| 属性 | 类型 | 必填 | 默认 | 说明 | +| ------------------------ | ------------------------- | --- | --- | ----------------------------------------------------------------- | +| `originUrl` | `string` | 是* | — | 原图地址(`file://`、绝对路径或 `http(s)://`) | +| `maskUrl` | `string` | 是* | — | 掩码图地址(语义色块图,建议与原图同尺寸) | +| `originImgPath` | `string` | — | — | **deprecated**,请用 `originUrl` | +| `maskImgPath` | `string` | — | — | **deprecated**,请用 `maskUrl` | +| `initialSession` | `MaskSegmentSession` | 否 | — | 从 MMKV 等恢复的草稿;分区就绪后自动 `loadSession` | +| `initialPaintColor` | `BgrColor` | 否 | — | **可选**。初始自定义笔刷色 `{ b, g, r }`;不传则默认无笔刷,需用户选色或 `ref.setPaintColor` | +| `initialPaintConfigJson` | `Record` | 否 | — | **可选**。与 `initialPaintColor` 配套的笔刷配置,上色成功时随 `onPaintCallback` 回传 | + ### 识别色与虚线(顶层便捷配置) -| 属性 | 类型 | 默认 | 说明 | -| ---- | ---- | ---- | ---- | -| `semanticColors` | `MaskSemanticColor[]` | `MASK_SEMANTIC_COLORS` | 掩码语义识别色,等同 `maskConfig.semanticColors` | -| `regionOutlineColor` | `string` | `rgba(20, 120, 235, 0.58)` | 分区虚线高亮色,等同 `paintConfig.regionOverlayFill` | + +| 属性 | 类型 | 默认 | 说明 | +| -------------------- | --------------------- | -------------------------- | ------------------------------------------ | +| `semanticColors` | `MaskSemanticColor[]` | `MASK_SEMANTIC_COLORS` | 掩码语义识别色,等同 `maskConfig.semanticColors` | +| `regionOutlineColor` | `string` | `rgba(20, 120, 235, 0.58)` | 分区虚线高亮色,等同 `paintConfig.regionOverlayFill` | + 顶层属性优先级高于嵌套 `maskConfig` / `paintConfig`。 @@ -480,72 +487,80 @@ const hasError = watchState === 'error'; ### maskConfig -| 字段 | 类型 | 默认 | 说明 | -| ---- | ---- | ---- | ---- | -| `semanticColors` | `MaskSemanticColor[]` | 内置色表 | 掩码语义色(可被顶层 `semanticColors` 覆盖) | -| `blackThreshold` | `number` | `30` | BGR 最大值低于此值的像素视为黑色背景 | -| `maxRegionColors` | `number` | `6` | 最终保留的最大语义分区数 | -| `quantStep` | `number` | `64` | 踢脚线量化步长 | -| `baseboardMaxColorDist` | `number` | `42` | 踢脚线色距阈值 | -| `baseboardStripQuantKeys` | `string[]` | 内置键集 | 踢脚线条带量化键,格式 `"b,g,r"` | -| `wallQuantKeys` | `string[]` | 内置键集 | 墙面量化键 | -| `cabinetQuantKeys` | `string[]` | 内置键集 | 柜体量化键 | -| `secondarySemanticNames` | `string[]` | `garageDoor, roof, eave` | 次要语义名 | -| `secondaryMinPixelRatio` | `number` | `0.002` | 次要语义最小像素占比 | -| `junctionHRadiusPx` | `number` | `24` | 踢脚线交界水平半径 | -| `junctionVRadiusPx` | `number` | `2` | 踢脚线交界垂直半径 | -| `kickBridgeHalfWPx` | `number` | `6` | 踢脚线横向补缝半宽 | -| `baseboardJunctionRowMarginPx` | `number` | `1` | 踢脚线交界行边距 | -| `baseboardJunctionVReachPx` | `number` | `2` | 踢脚线交界纵向延伸 | -| `baseboardMinRunPx` | `number` | `2` | 蒙版条带最小 run 长度 | -| `splitWalls` | `boolean` | `false` | 在 wall 掩码内按纹理边界细分为 `wall-1`、`wall-2`… | -| `splitWallsMaxCount` | `number` | `8` | 墙壁子区最大数量 | -| `splitWallsMinAreaRatio` | `number` | `0.002` | 碎块最小面积比(相对 seg 总像素) | -| `splitWallsColorDistSq` | `number` | `1400` | 连通域色度均值距离平方阈值(墙内光影容忍,材质间更严) | -| `splitWallsChromaBlurRadius` | `number` | `5` | 预留:色度平滑半径 | -| `splitWallsNeutralChromaMax` | `number` | `14` | 白/灰墙低饱和判定半径;与有色墙强制分界 | + +| 字段 | 类型 | 默认 | 说明 | +| ------------------------------ | --------------------- | ------------------------ | ------------------------------------- | +| `semanticColors` | `MaskSemanticColor[]` | 内置色表 | 掩码语义色(可被顶层 `semanticColors` 覆盖) | +| `blackThreshold` | `number` | `30` | BGR 最大值低于此值的像素视为黑色背景 | +| `maxRegionColors` | `number` | `6` | 最终保留的最大语义分区数 | +| `quantStep` | `number` | `64` | 踢脚线量化步长 | +| `baseboardMaxColorDist` | `number` | `42` | 踢脚线色距阈值 | +| `baseboardStripQuantKeys` | `string[]` | 内置键集 | 踢脚线条带量化键,格式 `"b,g,r"` | +| `wallQuantKeys` | `string[]` | 内置键集 | 墙面量化键 | +| `cabinetQuantKeys` | `string[]` | 内置键集 | 柜体量化键 | +| `secondarySemanticNames` | `string[]` | `garageDoor, roof, eave` | 次要语义名 | +| `secondaryMinPixelRatio` | `number` | `0.002` | 次要语义最小像素占比 | +| `junctionHRadiusPx` | `number` | `24` | 踢脚线交界水平半径 | +| `junctionVRadiusPx` | `number` | `2` | 踢脚线交界垂直半径 | +| `kickBridgeHalfWPx` | `number` | `6` | 踢脚线横向补缝半宽 | +| `baseboardJunctionRowMarginPx` | `number` | `1` | 踢脚线交界行边距 | +| `baseboardJunctionVReachPx` | `number` | `2` | 踢脚线交界纵向延伸 | +| `baseboardMinRunPx` | `number` | `2` | 蒙版条带最小 run 长度 | +| `splitWalls` | `boolean` | `false` | 在 wall 掩码内按纹理边界细分为 `wall-1`、`wall-2`… | +| `splitWallsMaxCount` | `number` | `8` | 墙壁子区最大数量 | +| `splitWallsMinAreaRatio` | `number` | `0.002` | 碎块最小面积比(相对 seg 总像素) | +| `splitWallsColorDistSq` | `number` | `1400` | 连通域色度均值距离平方阈值(墙内光影容忍,材质间更严) | +| `splitWallsChromaBlurRadius` | `number` | `5` | 预留:色度平滑半径 | +| `splitWallsNeutralChromaMax` | `number` | `14` | 白/灰墙低饱和判定半径;与有色墙强制分界 | + 开启 `splitWalls` 后,原有单一 `wall` 区域会被替换为多个 `wall-N` 子区,各自独立上色与撤销。旧 Session 中 `regionName: 'wall'` 无法映射到新子区名,需重新上色。 ### pipelineConfig -| 字段 | 类型 | 默认 | 说明 | -| ---- | ---- | ---- | ---- | -| `maxImageLongSide` | `number` | `720` | 分割 / pickMap / 工作区缩放最长边 | -| `paintFreqMaxLongSide` | `number` | `480` | OpenCV LAB 高低频最长边 | -| `originPreviewMaxLongSide` | `number` | `360` | 预览最长边(主路径走工作区分辨率) | -| `maskPathMaxLongSide` | `number` | `480` | 虚线轮廓降采样最长边 | -| `minContourArea` | `number` | `100` | 最小轮廓面积(随缩放同比缩放) | -| `contourApproxEpsilon` | `number` | `0.003` | 轮廓多边形逼近系数 | -| `maxRegions` | `number` | `500` | 分割阶段最大区域数上限 | + +| 字段 | 类型 | 默认 | 说明 | +| -------------------------- | -------- | ------- | ----------------------- | +| `maxImageLongSide` | `number` | `720` | 分割 / pickMap / 工作区缩放最长边 | +| `paintFreqMaxLongSide` | `number` | `480` | OpenCV LAB 高低频最长边 | +| `originPreviewMaxLongSide` | `number` | `360` | 预览最长边(主路径走工作区分辨率) | +| `maskPathMaxLongSide` | `number` | `480` | 虚线轮廓降采样最长边 | +| `minContourArea` | `number` | `100` | 最小轮廓面积(随缩放同比缩放) | +| `contourApproxEpsilon` | `number` | `0.003` | 轮廓多边形逼近系数 | +| `maxRegions` | `number` | `500` | 分割阶段最大区域数上限 | + ### paintConfig -| 字段 | 类型 | 默认 | 说明 | -| ---- | ---- | ---- | ---- | -| `palette` | `BgrColor[]` | 6 色内置盘 | 底部笔刷色条 | -| `colorBaseOpacity` | `number` | `0.88` | 底色不透明度 | -| `lLightOpacity` | `number` | `0.50` | L 通道叠加强度 | -| `textureOpacity` | `number` | `0.85` | 高频纹理叠加强度(纹理保留更强) | -| `lLowBlurKernel` | `number` | `7` | 低频高斯核(奇数) | -| `lLowContrast` | `number` | `1.15` | 低频对比度 | -| `lLowBrightness` | `number` | `0.9` | 低频亮度 | -| `lHighGain` | `number` | `1.22` | 高频增益 | -| `maskFeatherColor` | `number` | `1.6` | 上色边缘羽化(颜色)——软边 alpha 半径,像素 | -| `maskFeatherTexture` | `number` | `0.9` | 上色边缘羽化(纹理)——预留/辅助 | -| `regionOverlayFill` | `string` | `rgba(20,120,235,0.58)` | 虚线/高亮填充色 | -| `regionOutlineStrokeWidth` | `number` | `4` | 虚线描边宽度 | + +| 字段 | 类型 | 默认 | 说明 | +| -------------------------- | ------------ | ----------------------- | -------------------------- | +| `palette` | `BgrColor[]` | 6 色内置盘 | 底部笔刷色条 | +| `colorBaseOpacity` | `number` | `0.88` | 底色不透明度 | +| `lLightOpacity` | `number` | `0.50` | L 通道叠加强度 | +| `textureOpacity` | `number` | `0.85` | 高频纹理叠加强度(纹理保留更强) | +| `lLowBlurKernel` | `number` | `7` | 低频高斯核(奇数) | +| `lLowContrast` | `number` | `1.15` | 低频对比度 | +| `lLowBrightness` | `number` | `0.9` | 低频亮度 | +| `lHighGain` | `number` | `1.22` | 高频增益 | +| `maskFeatherColor` | `number` | `1.6` | 上色边缘羽化(颜色)——软边 alpha 半径,像素 | +| `maskFeatherTexture` | `number` | `0.9` | 上色边缘羽化(纹理)——预留/辅助 | +| `regionOverlayFill` | `string` | `rgba(20,120,235,0.58)` | 虚线/高亮填充色 | +| `regionOutlineStrokeWidth` | `number` | `4` | 虚线描边宽度 | + ### interactionConfig -| 字段 | 类型 | 默认 | 说明 | -| ---- | ---- | ---- | ---- | -| `pickMapSearchRadiusPx` | `number` | `14` | 点击 pickMap 搜索半径(像素) | -| `kickMaskPickRadiusPx` | `number` | `36` | 踢脚线掩码拾取半径 | -| `thinStripPadding` | `number` | `0.008` | 细条带(踢脚线)点击扩展比例 | -| `regionPadding` | `number` | `0.003` | 普通分区点击扩展比例 | -| `initRegionFlashMs` | `number` | `1000` | 初始化轮播每条虚线停留毫秒 | -| `enableInitRegionFlash` | `boolean` | `true` | 是否启用初始化轮播 | + +| 字段 | 类型 | 默认 | 说明 | +| ----------------------- | --------- | ------- | ------------------- | +| `pickMapSearchRadiusPx` | `number` | `14` | 点击 pickMap 搜索半径(像素) | +| `kickMaskPickRadiusPx` | `number` | `36` | 踢脚线掩码拾取半径 | +| `thinStripPadding` | `number` | `0.008` | 细条带(踢脚线)点击扩展比例 | +| `regionPadding` | `number` | `0.003` | 普通分区点击扩展比例 | +| `initRegionFlashMs` | `number` | `1000` | 初始化轮播每条虚线停留毫秒 | +| `enableInitRegionFlash` | `boolean` | `true` | 是否启用初始化轮播 | + > 完整默认值常量:`DEFAULT_MASK_CONFIG`、`DEFAULT_PIPELINE_CONFIG`、`DEFAULT_PAINT_CONFIG`、`DEFAULT_INTERACTION_CONFIG`(自包入口导出)。 @@ -573,11 +588,13 @@ const hasError = watchState === 'error'; ### 回调 -| 属性 | 签名 | 说明 | -| ---- | ---- | ---- | -| `onWatch` | `(state, durationMs, detail?) => void` | 初始化阶段回调;`durationMs` 自本次 `init` 起算 | -| `onPaintCallback` | `(payload: PaintCallbackPayload) => void` | 上色成功或未选笔刷时点击分区 | -| `onError` | `(message, error?) => void` | 分割或加载失败 | + +| 属性 | 签名 | 说明 | +| ----------------- | ----------------------------------------- | ---------------------------------- | +| `onWatch` | `(state, durationMs, detail?) => void` | 初始化阶段回调;`durationMs` 自本次 `init` 起算 | +| `onPaintCallback` | `(payload: PaintCallbackPayload) => void` | 上色成功或未选笔刷时点击分区 | +| `onError` | `(message, error?) => void` | 分割或加载失败 | + `PaintCallbackPayload`(判别联合,`payload.kind` 区分): @@ -614,12 +631,14 @@ onPaintCallback={payload => { `onWatch` 的 `detail`(`MaskSegmentWatchDetail`): -| 字段 | 类型 | 说明 | -| ---- | ---- | ---- | -| `regionCount` | `number` | 当前有效分区数 | -| `maskPathsReady` | `boolean` | 轮廓 Skia 路径是否就绪 | + +| 字段 | 类型 | 说明 | +| ----------------- | --------- | ----------------- | +| `regionCount` | `number` | 当前有效分区数 | +| `maskPathsReady` | `boolean` | 轮廓 Skia 路径是否就绪 | | `freqLayersReady` | `boolean` | 高低频 Shader 纹理是否就绪 | -| `errorMessage` | `string` | `error` 状态下的失败说明 | +| `errorMessage` | `string` | `error` 状态下的失败说明 | + #### onWatch 状态流转 @@ -643,19 +662,21 @@ init 通过 `ref` 调用(类型 `MaskSegmentCanvasRef`): -| 方法 | 签名 | 说明 | -| ---- | ---- | ---- | -| `reset` | `() => void` | 撤销上一步上色(按 `paintHistory`) | -| `swap` | `(showOrigin?: boolean) => void` | 对比原图;不传参 toggle,传 `true`/`false` 显式开关 | -| `save` | `(options?) => Promise` | 合成并保存 PNG;`options.destDir` 可选输出目录 | -| `session` | `() => MaskSegmentSession` | 导出可 JSON 序列化会话(存 MMKV) | -| `loadSession` | `(session) => void` | 恢复上色状态(也可通过 `initialSession`) | -| `setPaintColor` | `(color, configJson?) => void` | 设置当前笔刷色,清空底部色条选中 | -| `setMaskConfig` | `(config) => void` | 运行时更新掩码配置并**重新分割** | -| `clearAllPaint` | `() => void` | 清空全部上色记录 | -| `resegment` | `() => Promise` | 清空 PNG 缓存并重新分割 | -| `getRegions` | `() => SegmentRegion[]` | 当前分区列表快照 | -| `getPaintedRegions` | `() => PaintedRegionRecord[]` | 当前上色记录快照 | + +| 方法 | 签名 | 说明 | +| ------------------- | ---------------------------------------- | ------------------------------------- | +| `reset` | `() => void` | 撤销上一步上色(按 `paintHistory`) | +| `swap` | `(showOrigin?: boolean) => void` | 对比原图;不传参 toggle,传 `true`/`false` 显式开关 | +| `save` | `(options?) => Promise` | 合成并保存 PNG;`options.destDir` 可选输出目录 | +| `session` | `() => MaskSegmentSession` | 导出可 JSON 序列化会话(存 MMKV) | +| `loadSession` | `(session) => void` | 恢复上色状态(也可通过 `initialSession`) | +| `setPaintColor` | `(color, configJson?) => void` | 设置当前笔刷色,清空底部色条选中 | +| `setMaskConfig` | `(config) => void` | 运行时更新掩码配置并**重新分割** | +| `clearAllPaint` | `() => void` | 清空全部上色记录 | +| `resegment` | `() => Promise` | 清空 PNG 缓存并重新分割 | +| `getRegions` | `() => SegmentRegion[]` | 当前分区列表快照 | +| `getPaintedRegions` | `() => PaintedRegionRecord[]` | 当前上色记录快照 | + `SavePaintResult`:`{ filePath, width, height, paintedCount, previewPath? }` @@ -832,44 +853,52 @@ MaskSegmentApp/ # 仓库根目录(npm 包 react-n Demo 在挂载画布前调用 `prewarmPngBgrCacheAsync([origin, mask])`,PNG 解码命中内存缓存。典型日志: -| 阶段 | watchState | 约耗时 | 说明 | -| ---- | ---------- | ------ | ---- | -| 掩码对齐 | `mask_aligned` | ~160ms | 掩码缩放到分割工作分辨率 | -| 分区完成 | `regions_ready` / `mask_sampled` | ~320ms | 布局扫描 + 踢脚线 + pickMap | -| **可交互** | **`interactive`** | **~320–450ms** | 可点击选区、选色、Shader 上色 | -| 轮廓就绪 | `mask_paths_ready` | ~430–550ms | 比 `interactive` 晚 **~100ms**,轮播虚线可显示 | + +| 阶段 | watchState | 约耗时 | 说明 | +| ------- | -------------------------------- | -------------- | ------------------------------------ | +| 掩码对齐 | `mask_aligned` | ~160ms | 掩码缩放到分割工作分辨率 | +| 分区完成 | `regions_ready` / `mask_sampled` | ~320ms | 布局扫描 + 踢脚线 + pickMap | +| **可交互** | `**interactive`** | **~320–450ms** | 可点击选区、选色、Shader 上色 | +| 轮廓就绪 | `mask_paths_ready` | ~430–550ms | 比 `interactive` 晚 **~100ms**,轮播虚线可显示 | + `interactive` **不等待**轮廓路径;`mask_paths_ready` 仅影响初始化轮播虚线与可选 UI 提示。 同图各子步骤(`__DEV__` 日志,默认 pipeline)量级: -| 子步骤 | 约耗时 | 工作分辨率 | -| ------ | ------ | ---------- | -| OpenCV LAB 高低频 | ~10–40ms | 270×480 | -| 高低频 Skia 纹理 | ~20–30ms | 同上 | + +| 子步骤 | 约耗时 | 工作分辨率 | +| ----------------- | --------- | ------------------------------ | +| OpenCV LAB 高低频 | ~10–40ms | 270×480 | +| 高低频 Skia 纹理 | ~20–30ms | 同上 | | 布局扫描 + 踢脚线 + 点击查表 | ~90–120ms | 405×720(1080p 缩至 longSide 720) | -| 全量轮廓路径(异步,不阻塞交互) | ~80–150ms | 270×480 | +| 全量轮廓路径(异步,不阻塞交互) | ~80–150ms | 270×480 | + ### 分辨率与 `pipelineConfig` 的关系 计算密集型步骤被 **最长边上限** 截断,**不随 4K/8K 原图线性放大**;**PNG 全图解码**仍随像素量线性增长。 -| 步骤 | 配置项 | 1080×1920 实际处理尺寸 | 随原图像素增长 | -| ---- | ------ | ---------------------- | -------------- | -| PNG 解码 | — | 1080×1920 × 2 张 | **是** | -| 掩码分割 / pickMap | `maxImageLongSide: 720` | ~405×720 | **否**(长边 >720 时固定) | -| Shader 高低频 | `paintFreqMaxLongSide: 480` | ~270×480 | **否** | -| 工作区 Skia 原图 | 同 `maxImageLongSide` | ~405×720 | **否** | -| 虚线轮廓 | `maskPathMaxLongSide: 480` | ~270×480 | **否**(不阻塞 `interactive`) | + +| 步骤 | 配置项 | 1080×1920 实际处理尺寸 | 随原图像素增长 | +| -------------- | --------------------------- | ---------------- | ------------------------ | +| PNG 解码 | — | 1080×1920 × 2 张 | **是** | +| 掩码分割 / pickMap | `maxImageLongSide: 720` | ~405×720 | **否**(长边 >720 时固定) | +| Shader 高低频 | `paintFreqMaxLongSide: 480` | ~270×480 | **否** | +| 工作区 Skia 原图 | 同 `maxImageLongSide` | ~405×720 | **否** | +| 虚线轮廓 | `maskPathMaxLongSide: 480` | ~270×480 | **否**(不阻塞 `interactive`) | + ### `interactive` 预估(默认 pipeline) -| 原图规格 | 相对 1080p 像素 | 有 PNG 预热 | 冷启动(无预热) | -| -------- | --------------- | ----------- | ---------------- | -| 1080×1920 | 1× | **320–450ms** | **450–700ms** | -| 1440×2560 (2K) | ~1.8× | **400–550ms** | **600–900ms** | -| 3840×2160 (4K) | ~4× | **500–750ms** | **800–1200ms** | -| 7680×4320 (8K) | ~16× | **0.8–1.5s** | **1.5–3s+** | + +| 原图规格 | 相对 1080p 像素 | 有 PNG 预热 | 冷启动(无预热) | +| -------------- | ----------- | ------------- | -------------- | +| 1080×1920 | 1× | **320–450ms** | **450–700ms** | +| 1440×2560 (2K) | ~1.8× | **400–550ms** | **600–900ms** | +| 3840×2160 (4K) | ~4× | **500–750ms** | **800–1200ms** | +| 7680×4320 (8K) | ~16× | **0.8–1.5s** | **1.5–3s+** | + > **300ms 内可交互**:在 1080p + 预热 + 默认 pipeline + 中高端机上**接近但偏乐观**;不宜作为全机型 SLA。 @@ -877,11 +906,13 @@ Demo 在挂载画布前调用 `prewarmPngBgrCacheAsync([origin, mask])`,PNG 相对上述开发环境 ~320ms 的量级: -| 档位 | 相对倍数 | 有预热 `interactive` | 冷启动 | -| ---- | -------- | -------------------- | ------ | -| 旗舰 iOS / 新旗舰 Android | 0.8–1.2× | 300–450ms | 500–800ms | -| 中端 Android | 1.5–2.5× | 500–800ms | 700ms–1.2s | -| 低端 Android(4GB、老 U) | 2.5–4× | 800ms–1.3s | 1–2s+ | + +| 档位 | 相对倍数 | 有预热 `interactive` | 冷启动 | +| -------------------- | -------- | ----------------- | ---------- | +| 旗舰 iOS / 新旗舰 Android | 0.8–1.2× | 300–450ms | 500–800ms | +| 中端 Android | 1.5–2.5× | 500–800ms | 700ms–1.2s | +| 低端 Android(4GB、老 U) | 2.5–4× | 800ms–1.3s | 1–2s+ | + Android 额外开销主要来自:JS ↔ OpenCV bridge、内存带宽/GC、Skia 纹理上传。 @@ -889,10 +920,12 @@ Android 额外开销主要来自:JS ↔ OpenCV bridge、内存带宽/GC、Skia 若将 `pipelineConfig.maxImageLongSide` 设为 **1280**(高于默认 720),分割工作区约 **720×1280**,像素约为 720 档的 **3×**: -| 场景 | 默认 720 | 改为 1280 | -| ---- | -------- | --------- | + +| 场景 | 默认 720 | 改为 1280 | +| ------------------------ | ---------- | ------------- | | 1080p `interactive`(中端机) | ~320–800ms | **500ms–1s+** | -| 分割 / pickMap 耗时 | ~90–120ms | ~250–350ms | +| 分割 / pickMap 耗时 | ~90–120ms | ~250–350ms | + 更高精度换更长初始化;若目标仍是 **<500ms 可交互**,建议维持默认 **720**,必要时降至 **640**。 @@ -907,10 +940,10 @@ await prewarmPngBgrCacheAsync([originPath, maskPath]); // 再挂载 MaskSegmentCanvas ``` -2. **Loading 时机**:阻塞式 Loading 在 `interactive` 关闭;「轮廓准备中」可选监听 `mask_paths_ready`。 -3. **大图 / 低端机**:保持默认 `maxImageLongSide: 720`;可再将 `paintFreqMaxLongSide` 降至 **360**。 -4. **4K 素材**:业务侧先下采样再传入,或接受 **0.8–1.5s** 量级的 `interactive`(预热后)。 -5. **观测**:开发环境关注 Metro 中 `[MaskSegment]`、`[⏱ ...]` 与 `onWatch` 的 `durationMs`。 +1. **Loading 时机**:阻塞式 Loading 在 `interactive` 关闭;「轮廓准备中」可选监听 `mask_paths_ready`。 +2. **大图 / 低端机**:保持默认 `maxImageLongSide: 720`;可再将 `paintFreqMaxLongSide` 降至 **360**。 +3. **4K 素材**:业务侧先下采样再传入,或接受 **0.8–1.5s** 量级的 `interactive`(预热后)。 +4. **观测**:开发环境关注 Metro 中 `[MaskSegment]`、`[⏱ ...]` 与 `onWatch` 的 `durationMs`。 ---