react-native-mask-segment-canvas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +904 -0
  2. package/dist/components/MaskSegmentCanvas.d.ts +6 -0
  3. package/dist/components/MaskSegmentCanvas.d.ts.map +1 -0
  4. package/dist/components/MaskSegmentCanvas.js +2012 -0
  5. package/dist/components/MaskSegmentCanvas.js.map +1 -0
  6. package/dist/components/MaskSegmentCanvas.types.d.ts +189 -0
  7. package/dist/components/MaskSegmentCanvas.types.d.ts.map +1 -0
  8. package/dist/components/MaskSegmentCanvas.types.js +2 -0
  9. package/dist/components/MaskSegmentCanvas.types.js.map +1 -0
  10. package/dist/index.d.ts +6 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +5 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/shaders/regionPaint.sksl.d.ts +3 -0
  15. package/dist/shaders/regionPaint.sksl.d.ts.map +1 -0
  16. package/dist/shaders/regionPaint.sksl.js +72 -0
  17. package/dist/shaders/regionPaint.sksl.js.map +1 -0
  18. package/dist/utils/compositePaintedImage.d.ts +44 -0
  19. package/dist/utils/compositePaintedImage.d.ts.map +1 -0
  20. package/dist/utils/compositePaintedImage.js +146 -0
  21. package/dist/utils/compositePaintedImage.js.map +1 -0
  22. package/dist/utils/exportUtils.d.ts +20 -0
  23. package/dist/utils/exportUtils.d.ts.map +1 -0
  24. package/dist/utils/exportUtils.js +32 -0
  25. package/dist/utils/exportUtils.js.map +1 -0
  26. package/dist/utils/freqLayerPrep.d.ts +23 -0
  27. package/dist/utils/freqLayerPrep.d.ts.map +1 -0
  28. package/dist/utils/freqLayerPrep.js +168 -0
  29. package/dist/utils/freqLayerPrep.js.map +1 -0
  30. package/dist/utils/maskSegmentRuntime.d.ts +43 -0
  31. package/dist/utils/maskSegmentRuntime.d.ts.map +1 -0
  32. package/dist/utils/maskSegmentRuntime.js +181 -0
  33. package/dist/utils/maskSegmentRuntime.js.map +1 -0
  34. package/dist/utils/maskSegmentation.d.ts +133 -0
  35. package/dist/utils/maskSegmentation.d.ts.map +1 -0
  36. package/dist/utils/maskSegmentation.js +1600 -0
  37. package/dist/utils/maskSegmentation.js.map +1 -0
  38. package/dist/utils/maskSemanticPalette.d.ts +31 -0
  39. package/dist/utils/maskSemanticPalette.d.ts.map +1 -0
  40. package/dist/utils/maskSemanticPalette.js +125 -0
  41. package/dist/utils/maskSemanticPalette.js.map +1 -0
  42. package/dist/utils/opencvAdapter.d.ts +116 -0
  43. package/dist/utils/opencvAdapter.d.ts.map +1 -0
  44. package/dist/utils/opencvAdapter.js +353 -0
  45. package/dist/utils/opencvAdapter.js.map +1 -0
  46. package/dist/utils/paintColorMapTexture.d.ts +5 -0
  47. package/dist/utils/paintColorMapTexture.d.ts.map +1 -0
  48. package/dist/utils/paintColorMapTexture.js +203 -0
  49. package/dist/utils/paintColorMapTexture.js.map +1 -0
  50. package/dist/utils/paintShaderRuntime.d.ts +40 -0
  51. package/dist/utils/paintShaderRuntime.d.ts.map +1 -0
  52. package/dist/utils/paintShaderRuntime.js +76 -0
  53. package/dist/utils/paintShaderRuntime.js.map +1 -0
  54. package/dist/utils/pickMapTexture.d.ts +4 -0
  55. package/dist/utils/pickMapTexture.d.ts.map +1 -0
  56. package/dist/utils/pickMapTexture.js +24 -0
  57. package/dist/utils/pickMapTexture.js.map +1 -0
  58. package/dist/utils/pngImage.d.ts +49 -0
  59. package/dist/utils/pngImage.d.ts.map +1 -0
  60. package/dist/utils/pngImage.js +438 -0
  61. package/dist/utils/pngImage.js.map +1 -0
  62. package/dist/utils/resolveAssetPath.d.ts +3 -0
  63. package/dist/utils/resolveAssetPath.d.ts.map +1 -0
  64. package/dist/utils/resolveAssetPath.js +56 -0
  65. package/dist/utils/resolveAssetPath.js.map +1 -0
  66. package/dist/utils/resolveImageUrl.d.ts +3 -0
  67. package/dist/utils/resolveImageUrl.d.ts.map +1 -0
  68. package/dist/utils/resolveImageUrl.js +51 -0
  69. package/dist/utils/resolveImageUrl.js.map +1 -0
  70. package/dist/utils/skiaImage.d.ts +4 -0
  71. package/dist/utils/skiaImage.d.ts.map +1 -0
  72. package/dist/utils/skiaImage.js +12 -0
  73. package/dist/utils/skiaImage.js.map +1 -0
  74. package/package.json +100 -0
  75. package/patches/react-native-fast-opencv+0.4.8.patch +122 -0
  76. package/src/components/MaskSegmentCanvas.tsx +2832 -0
  77. package/src/components/MaskSegmentCanvas.types.ts +216 -0
  78. package/src/globals.d.ts +19 -0
  79. package/src/index.ts +45 -0
  80. package/src/shaders/regionPaint.sksl.ts +71 -0
  81. package/src/upng-js.d.ts +33 -0
  82. package/src/utils/compositePaintedImage.ts +201 -0
  83. package/src/utils/exportUtils.ts +40 -0
  84. package/src/utils/freqLayerPrep.ts +267 -0
  85. package/src/utils/maskSegmentRuntime.ts +257 -0
  86. package/src/utils/maskSegmentation.ts +2294 -0
  87. package/src/utils/maskSemanticPalette.ts +187 -0
  88. package/src/utils/opencvAdapter.ts +539 -0
  89. package/src/utils/paintColorMapTexture.ts +239 -0
  90. package/src/utils/paintShaderRuntime.tsx +150 -0
  91. package/src/utils/pickMapTexture.ts +37 -0
  92. package/src/utils/pngImage.ts +591 -0
  93. package/src/utils/resolveAssetPath.ts +64 -0
  94. package/src/utils/resolveImageUrl.ts +63 -0
  95. package/src/utils/skiaImage.ts +25 -0
package/README.md ADDED
@@ -0,0 +1,904 @@
1
+ # react-native-mask-segment-canvas
2
+
3
+ 基于 React Native **0.79** 的掩码分区交互库,核心导出 `MaskSegmentCanvas` 组件,可通过 **npm 包** 或 **npm link** 接入其它 RN 工程。
4
+
5
+ - **OpenCV**(`react-native-fast-opencv`):掩码语义布局、踢脚线修补、分区提取
6
+ - **Skia RuntimeEffect(SkSL)**:原图 + LAB 高低频纹理叠色(单次全屏 Shader)
7
+ - **Skia Path**:分区虚线轮廓高亮
8
+ - **交互**:底部笔刷选色(可选初始化)→ 点击分区上色;未选笔刷时点击分区会通过 `onPaintCallback` 提示;未选笔刷时长按预览分区虚线轮廓
9
+
10
+ 本仓库同时作为 **库源码**(`src/index.ts`)与 **自测 Demo**(根目录 `App.tsx`)维护。
11
+
12
+ **推荐的集成演示请查看 `example/` 目录**:它只使用公开 API,完整模拟业务项目接入方式(含 `package.json`、Metro 配置、完整可参考的 `App.tsx`)。
13
+
14
+ ---
15
+
16
+ ## 作为 npm 包接入其它工程
17
+
18
+ ### 安装依赖(宿主工程)
19
+
20
+ ```bash
21
+ npm install react-native-mask-segment-canvas
22
+ # 或本地联调
23
+ npm link ../MaskSegmentApp # 在库目录先执行 npm link
24
+ npm link react-native-mask-segment-canvas
25
+ ```
26
+
27
+ 宿主工程还需安装 **peerDependencies**(版本需与宿主 RN 对齐):
28
+
29
+ ```bash
30
+ npm install @shopify/react-native-skia react-native-reanimated react-native-fast-opencv react-native-fs buffer upng-js
31
+ # 若使用 showDebugPickers 相册选图
32
+ npm install react-native-image-picker
33
+ ```
34
+
35
+ ### 宿主工程 postinstall(必需)
36
+
37
+ 本库依赖 `patch-package` 修补 `react-native-fast-opencv`,宿主 `package.json` 需配置:
38
+
39
+ ```json
40
+ {
41
+ "scripts": {
42
+ "postinstall": "patch-package"
43
+ },
44
+ "devDependencies": {
45
+ "patch-package": "^8.0.1"
46
+ }
47
+ }
48
+ ```
49
+
50
+ 安装本库后,`node_modules/react-native-mask-segment-canvas/patches/` 中的补丁会在宿主 `postinstall` 时自动应用。
51
+
52
+ ### iOS / Android 原生依赖
53
+
54
+ ```bash
55
+ cd ios && pod install && cd ..
56
+ ```
57
+
58
+ 确保宿主已按各原生库文档完成 Skia、Reanimated、OpenCV 等配置。
59
+
60
+ ### Metro 配置(npm link 时)
61
+
62
+ 联调时若出现模块解析问题,在宿主 `metro.config.js` 中把本库加入 `watchFolders` / `extraNodeModules`(按宿主 Metro 版本调整):
63
+
64
+ ```js
65
+ const path = require('path');
66
+
67
+ module.exports = {
68
+ watchFolders: [path.resolve(__dirname, '../MaskSegmentApp')],
69
+ resolver: {
70
+ nodeModulesPaths: [path.resolve(__dirname, 'node_modules')],
71
+ },
72
+ };
73
+ ```
74
+
75
+ ### 业务侧引入
76
+
77
+ ```tsx
78
+ import MaskSegmentCanvas, {
79
+ type MaskSegmentCanvasRef,
80
+ type MaskSegmentSession,
81
+ type MaskSegmentWatchState,
82
+ type MaskSegmentWatchDetail,
83
+ type BgrColor,
84
+ type MaskSemanticColor,
85
+ type PaintCallbackPayload,
86
+ type PaintedRegionRecord,
87
+ type PipelineConfig,
88
+ type MaskSegmentConfig,
89
+ type PaintConfig,
90
+ type InteractionConfig,
91
+ type SavePaintResult,
92
+ MASK_SEMANTIC_COLORS,
93
+ BASEBOARD_SEMANTIC_NAME,
94
+ prewarmPngBgrCacheAsync,
95
+ DEFAULT_PIPELINE_CONFIG,
96
+ DEFAULT_MASK_CONFIG,
97
+ DEFAULT_PAINT_CONFIG,
98
+ DEFAULT_INTERACTION_CONFIG,
99
+ } from 'react-native-mask-segment-canvas';
100
+ ```
101
+
102
+ 主要导出一览:
103
+
104
+ | 类别 | 名称 |
105
+ | ---- | ---- |
106
+ | 组件 | `MaskSegmentCanvas`(default) |
107
+ | Ref / Props 类型 | `MaskSegmentCanvasRef`、`MaskSegmentCanvasProps` |
108
+ | 会话 / 回调类型 | `MaskSegmentSession`、`PaintCallbackPayload`、`PaintedRegionRecord`、`SavePaintResult` |
109
+ | Watch 类型 | `MaskSegmentWatchState`、`MaskSegmentWatchDetail` |
110
+ | 配置类型 | `PipelineConfig`、`MaskSegmentConfig`、`PaintConfig`、`InteractionConfig` |
111
+ | 语义色 | `MASK_SEMANTIC_COLORS`、`BASEBOARD_SEMANTIC_NAME` |
112
+ | 工具 | `prewarmPngBgrCacheAsync`、`prewarmPngBgrCache` |
113
+ | 运行时 | `DEFAULT_*_CONFIG`、`getMaskSegmentRuntimeConfig`、`setMaskSegmentRuntimeConfig` |
114
+
115
+ ---
116
+
117
+ ## 推荐:通过 example/ 目录学习集成
118
+
119
+ `example/` 是**专门为业务侧集成准备的演示文件夹**,它:
120
+
121
+ - 只通过 `import ... from 'react-native-mask-segment-canvas'` 使用公开 API(不碰内部 src)
122
+ - 提供了独立的 `package.json`(含 peer deps + 本地 file 依赖)
123
+ - 包含针对本地联调的 `metro.config.js`
124
+ - `App.tsx` 是一个可直接参考的完整页面,涵盖预热、状态管理、ref 操作、回调处理等
125
+
126
+ 建议:
127
+
128
+ 1. 直接阅读 `example/App.tsx` 获取最新可运行的集成写法。
129
+ 2. 按 `example/README.md` 的步骤在本机跑起来,验证安装、patch、Metro 配置是否正确。
130
+ 3. 把 `example/App.tsx` 中的核心逻辑复制到你自己的页面/组件中即可。
131
+
132
+ 这样可以确保你接入的是「库的公开契约」,而不是内部实现细节。
133
+
134
+ ---
135
+
136
+ ## 环境要求
137
+
138
+ - Node.js >= 18(推荐 20+)
139
+ - Xcode 15+(iOS)
140
+ - Android Studio + JDK 17(Android)
141
+ - CocoaPods(iOS)
142
+
143
+ ## 快速开始(本仓库 Demo)
144
+
145
+ 根目录 `App.tsx` 是库作者自测用的完整 Demo,内部直接 import `./src`。
146
+
147
+ ```bash
148
+ cd MaskSegmentApp
149
+
150
+ npm install
151
+
152
+ cd ios && bundle exec pod install && cd ..
153
+
154
+ npm start
155
+
156
+ # 另开终端
157
+ npm run ios
158
+ # 或
159
+ npm run android
160
+ ```
161
+
162
+ **想看「纯业务项目如何集成」**:请进入 `example/` 目录,按其 `README.md` 操作。它使用 `import from 'react-native-mask-segment-canvas'` + 标准的 `package.json` + Metro 配置,完全模拟消费者环境。
163
+
164
+ Demo 入口 `App.tsx` 通过 `./src`(即包入口 `src/index.ts`)引用组件,与业务侧 `import from 'react-native-mask-segment-canvas'` 等价。
165
+
166
+ ---
167
+
168
+ ## MaskSegmentCanvas 组件
169
+
170
+ ### 引入
171
+
172
+ ```tsx
173
+ import React, { useRef } from 'react';
174
+ import MaskSegmentCanvas, {
175
+ type MaskSegmentCanvasRef,
176
+ type MaskSegmentSession,
177
+ type MaskSegmentWatchState,
178
+ type MaskSegmentWatchDetail,
179
+ type BgrColor,
180
+ type MaskSemanticColor,
181
+ type PaintCallbackPayload,
182
+ MASK_SEMANTIC_COLORS,
183
+ prewarmPngBgrCacheAsync,
184
+ } from 'react-native-mask-segment-canvas';
185
+ ```
186
+
187
+ 也可按需导入运行时默认值(与组件 Props 合并使用):
188
+
189
+ ```tsx
190
+ import {
191
+ DEFAULT_PIPELINE_CONFIG,
192
+ DEFAULT_MASK_CONFIG,
193
+ DEFAULT_PAINT_CONFIG,
194
+ DEFAULT_INTERACTION_CONFIG,
195
+ } from 'react-native-mask-segment-canvas';
196
+ ```
197
+
198
+ ### 最小示例
199
+
200
+ 下面是一个可直接放进业务页面的完整示例,涵盖 **PNG 预热**、**state**、**配置**、**加载态**、**onWatch** 与 **ref** 常用操作。
201
+
202
+ ```tsx
203
+ import React, { useEffect, useRef, useState } from 'react';
204
+ import { ActivityIndicator, Text, View } from 'react-native';
205
+ import MaskSegmentCanvas, {
206
+ type MaskSegmentCanvasRef,
207
+ type MaskSegmentSession,
208
+ type MaskSegmentWatchState,
209
+ type BgrColor,
210
+ MASK_SEMANTIC_COLORS,
211
+ prewarmPngBgrCacheAsync,
212
+ } from 'react-native-mask-segment-canvas';
213
+
214
+ /** 业务侧准备好的图片地址(本地 file:// 或 http(s)://) */
215
+ type ImagePaths = {
216
+ origin: string;
217
+ mask: string;
218
+ };
219
+
220
+ const INTERACTIVE_STATES: MaskSegmentWatchState[] = [
221
+ 'interactive',
222
+ 'mask_paths_ready',
223
+ ];
224
+
225
+ export function PaintScreen() {
226
+ const canvasRef = useRef<MaskSegmentCanvasRef>(null);
227
+
228
+ const [imagePaths, setImagePaths] = useState<ImagePaths | null>(null);
229
+ const [pathsError, setPathsError] = useState('');
230
+ const [watchState, setWatchState] = useState<MaskSegmentWatchState | ''>('');
231
+ const [errorMessage, setErrorMessage] = useState('');
232
+ const [sessionDraft] = useState<MaskSegmentSession | null>(null);
233
+
234
+ const isInteractive = INTERACTIVE_STATES.includes(
235
+ watchState as MaskSegmentWatchState,
236
+ );
237
+ const isOutlineReady = watchState === 'mask_paths_ready';
238
+ const isCanvasLoading =
239
+ imagePaths != null &&
240
+ watchState !== '' &&
241
+ !INTERACTIVE_STATES.includes(watchState as MaskSegmentWatchState) &&
242
+ watchState !== 'error';
243
+
244
+ // 示例:接口下载完成后写入路径,并预热 PNG 解码缓存
245
+ useEffect(() => {
246
+ let cancelled = false;
247
+
248
+ (async () => {
249
+ try {
250
+ const origin = 'file:///path/to/origin.png';
251
+ const mask = 'file:///path/to/mask.png';
252
+ await prewarmPngBgrCacheAsync([origin, mask]);
253
+ if (!cancelled) {
254
+ setImagePaths({ origin, mask });
255
+ }
256
+ } catch (e) {
257
+ if (!cancelled) {
258
+ setPathsError(e instanceof Error ? e.message : String(e));
259
+ }
260
+ }
261
+ })();
262
+
263
+ return () => {
264
+ cancelled = true;
265
+ };
266
+ }, []);
267
+
268
+ const handleSave = async () => {
269
+ if (!isInteractive) return;
270
+ const result = await canvasRef.current?.save({ destDir: undefined });
271
+ console.log('saved', result?.filePath, result?.paintedCount);
272
+ };
273
+
274
+ if (pathsError) {
275
+ return <Text>{pathsError}</Text>;
276
+ }
277
+
278
+ if (!imagePaths) {
279
+ return (
280
+ <View>
281
+ <ActivityIndicator />
282
+ <Text>等待原图与掩码…</Text>
283
+ </View>
284
+ );
285
+ }
286
+
287
+ return (
288
+ <View style={{ flex: 1 }}>
289
+ {isCanvasLoading ? <Text>加载中:{watchState}</Text> : null}
290
+ {watchState === 'interactive' ? (
291
+ <Text>可上色(轮廓加载中…)</Text>
292
+ ) : null}
293
+ {isOutlineReady ? <Text>就绪</Text> : null}
294
+ {errorMessage ? <Text>{errorMessage}</Text> : null}
295
+
296
+ <MaskSegmentCanvas
297
+ ref={canvasRef}
298
+ style={{ flex: 1 }}
299
+ originUrl={imagePaths.origin}
300
+ maskUrl={imagePaths.mask}
301
+ semanticColors={MASK_SEMANTIC_COLORS}
302
+ regionOutlineColor="rgba(20, 120, 235, 0.58)"
303
+ maskConfig={{ blackThreshold: 30, maxRegionColors: 6 }}
304
+ pipelineConfig={{ maxImageLongSide: 720 }}
305
+ paintConfig={{ colorBaseOpacity: 0.88 }}
306
+ interactionConfig={{
307
+ initRegionFlashMs: 1000,
308
+ enableInitRegionFlash: true,
309
+ }}
310
+ initialSession={sessionDraft ?? undefined}
311
+ showDebugPickers={false}
312
+ showToolbar={false}
313
+ showColorBar
314
+ showStatusRow={false}
315
+ showOverlayButtons
316
+ disabled={!isInteractive}
317
+ onWatch={(state, durationMs, detail) => {
318
+ setWatchState(state);
319
+ // detail: regionCount, maskPathsReady, freqLayersReady, errorMessage
320
+ console.log('[onWatch]', state, durationMs, detail);
321
+ }}
322
+ onPaintCallback={payload => {
323
+ if (payload.kind === 'brush_required') {
324
+ toast(payload.hint);
325
+ return;
326
+ }
327
+ console.log('painted', payload.regionId, payload.regionName, payload.color, payload.configJson);
328
+ }}
329
+ onError={message => {
330
+ setErrorMessage(message);
331
+ setWatchState('error');
332
+ }}
333
+ />
334
+
335
+ {/* ref 示例:disabled={!isInteractive} 时可按需绑定 */}
336
+ {/* <Button title="保存" onPress={handleSave} disabled={!isInteractive} /> */}
337
+ {/* <Button title="撤销" onPress={() => canvasRef.current?.reset()} /> */}
338
+ {/* <Button title="对比" onPress={() => canvasRef.current?.swap()} /> */}
339
+ </View>
340
+ );
341
+ }
342
+ ```
343
+
344
+ #### 示例里涉及的 state 说明
345
+
346
+
347
+ | state | 类型 | 用途 |
348
+ | ------------------ | -------------------------- | ---- |
349
+ | `imagePaths` | `{ origin, mask } \| null` | 业务侧解析后的本地/远程图片路径 |
350
+ | `pathsError` | `string` | 路径解析或 PNG 预热失败文案 |
351
+ | `watchState` | `MaskSegmentWatchState \| ''` | `onWatch` 上报的初始化阶段 |
352
+ | `isInteractive` | 派生 | `interactive` 或 `mask_paths_ready` 时为 true,可开放操作 |
353
+ | `isOutlineReady` | 派生 | `mask_paths_ready` 时为 true,轮播虚线已就绪 |
354
+ | `isCanvasLoading` | 派生 | 画布初始化阻塞 Loading(不含 PNG 路径等待) |
355
+ | `errorMessage` | `string` | 由 `onError` 写入的分割/加载失败文案 |
356
+ | `sessionDraft` | `MaskSegmentSession \| null` | MMKV 等恢复的草稿 |
357
+
358
+
359
+ #### 配置项怎么选
360
+
361
+
362
+ | 配置 | 何时用顶层属性 | 何时用嵌套 Config |
363
+ | ---------- | -------------------------------- | ----------------------------------------- |
364
+ | 语义识别色 | `semanticColors={...}` 大多数场景 | `maskConfig.semanticColors` 需与更多掩码参数一起传时 |
365
+ | 虚线颜色 | `regionOutlineColor="..."` 大多数场景 | `paintConfig.regionOverlayFill` 需同时改笔刷盘等时 |
366
+ | 黑色阈值、最大分区数 | — | `maskConfig` |
367
+ | 图片处理尺寸 | — | `pipelineConfig` |
368
+ | 轮播间隔、点击容差 | — | `interactionConfig` |
369
+
370
+
371
+ 顶层属性与嵌套 Config **可同时传**,顶层 `semanticColors` / `regionOutlineColor` 优先级更高。
372
+
373
+ #### watchState 与 UI 建议
374
+
375
+ ```ts
376
+ // 阻塞式 Loading(分区 + 上色图层就绪前)
377
+ const isLoading = ![
378
+ 'interactive',
379
+ 'mask_paths_ready',
380
+ 'error',
381
+ '',
382
+ ].includes(watchState);
383
+
384
+ // 允许点击选区、选色、上色(不必等轮廓路径)
385
+ const canOperate =
386
+ watchState === 'interactive' || watchState === 'mask_paths_ready';
387
+
388
+ // 初始化轮播虚线已全部就绪(可选,用于收起「轮廓准备中」提示)
389
+ const isOutlineReady = watchState === 'mask_paths_ready';
390
+
391
+ // 显示错误页
392
+ const hasError = watchState === 'error';
393
+ ```
394
+
395
+ `interactive` 时 `detail.maskPathsReady` 一般为 `false`;`mask_paths_ready` 时为 `true`。两者通常相差约 100ms(异步构建 Skia 轮廓路径),不影响点击上色。
396
+
397
+ `originUrl` / `maskUrl` 支持:
398
+
399
+ - 本地路径:`file:///...` 或绝对路径
400
+ - 远程地址:`http(s)://...`(组件内部会下载/解析)
401
+
402
+ > 兼容旧属性 `originImgPath` / `maskImgPath`(已标记 deprecated,请改用 `originUrl` / `maskUrl`)。
403
+
404
+ ---
405
+
406
+ ## Props
407
+
408
+ ### 图片与初始化
409
+
410
+ | 属性 | 类型 | 必填 | 默认 | 说明 |
411
+ | ---- | ---- | ---- | ---- | ---- |
412
+ | `originUrl` | `string` | 是* | — | 原图地址(`file://`、绝对路径或 `http(s)://`) |
413
+ | `maskUrl` | `string` | 是* | — | 掩码图地址(语义色块图,建议与原图同尺寸) |
414
+ | `originImgPath` | `string` | — | — | **deprecated**,请用 `originUrl` |
415
+ | `maskImgPath` | `string` | — | — | **deprecated**,请用 `maskUrl` |
416
+ | `initialSession` | `MaskSegmentSession` | 否 | — | 从 MMKV 等恢复的草稿;分区就绪后自动 `loadSession` |
417
+ | `initialPaintColor` | `BgrColor` | 否 | — | **可选**。初始自定义笔刷色 `{ b, g, r }`;不传则默认无笔刷,需用户选色或 `ref.setPaintColor` |
418
+ | `initialPaintConfigJson` | `Record<string, unknown>` | 否 | — | **可选**。与 `initialPaintColor` 配套的笔刷配置,上色成功时随 `onPaintCallback` 回传 |
419
+
420
+ ### 识别色与虚线(顶层便捷配置)
421
+
422
+ | 属性 | 类型 | 默认 | 说明 |
423
+ | ---- | ---- | ---- | ---- |
424
+ | `semanticColors` | `MaskSemanticColor[]` | `MASK_SEMANTIC_COLORS` | 掩码语义识别色,等同 `maskConfig.semanticColors` |
425
+ | `regionOutlineColor` | `string` | `rgba(20, 120, 235, 0.58)` | 分区虚线高亮色,等同 `paintConfig.regionOverlayFill` |
426
+
427
+ 顶层属性优先级高于嵌套 `maskConfig` / `paintConfig`。
428
+
429
+ `MaskSemanticColor` 结构:
430
+
431
+ ```ts
432
+ {
433
+ name: string; // 语义名,如 wall / ceiling / baseboard
434
+ hex: string; // 展示用十六进制色
435
+ bgr: { b: number; g: number; r: number }; // 与掩码像素 BGR 通道一致
436
+ }
437
+ ```
438
+
439
+ 内置色表:`MASK_SEMANTIC_COLORS`(`src/utils/maskSemanticPalette.ts`)。
440
+
441
+ ### maskConfig
442
+
443
+ | 字段 | 类型 | 默认 | 说明 |
444
+ | ---- | ---- | ---- | ---- |
445
+ | `semanticColors` | `MaskSemanticColor[]` | 内置色表 | 掩码语义色(可被顶层 `semanticColors` 覆盖) |
446
+ | `blackThreshold` | `number` | `30` | BGR 最大值低于此值的像素视为黑色背景 |
447
+ | `maxRegionColors` | `number` | `6` | 最终保留的最大语义分区数 |
448
+ | `quantStep` | `number` | `64` | 踢脚线量化步长 |
449
+ | `baseboardMaxColorDist` | `number` | `42` | 踢脚线色距阈值 |
450
+ | `baseboardStripQuantKeys` | `string[]` | 内置键集 | 踢脚线条带量化键,格式 `"b,g,r"` |
451
+ | `wallQuantKeys` | `string[]` | 内置键集 | 墙面量化键 |
452
+ | `cabinetQuantKeys` | `string[]` | 内置键集 | 柜体量化键 |
453
+ | `secondarySemanticNames` | `string[]` | `garageDoor, roof, eave` | 次要语义名 |
454
+ | `secondaryMinPixelRatio` | `number` | `0.002` | 次要语义最小像素占比 |
455
+ | `junctionHRadiusPx` | `number` | `24` | 踢脚线交界水平半径 |
456
+ | `junctionVRadiusPx` | `number` | `2` | 踢脚线交界垂直半径 |
457
+ | `kickBridgeHalfWPx` | `number` | `6` | 踢脚线横向补缝半宽 |
458
+ | `baseboardJunctionRowMarginPx` | `number` | `1` | 踢脚线交界行边距 |
459
+ | `baseboardJunctionVReachPx` | `number` | `2` | 踢脚线交界纵向延伸 |
460
+ | `baseboardMinRunPx` | `number` | `2` | 蒙版条带最小 run 长度 |
461
+
462
+ ### pipelineConfig
463
+
464
+ | 字段 | 类型 | 默认 | 说明 |
465
+ | ---- | ---- | ---- | ---- |
466
+ | `maxImageLongSide` | `number` | `720` | 分割 / pickMap / 工作区缩放最长边 |
467
+ | `paintFreqMaxLongSide` | `number` | `480` | OpenCV LAB 高低频最长边 |
468
+ | `originPreviewMaxLongSide` | `number` | `360` | 预览最长边(主路径走工作区分辨率) |
469
+ | `maskPathMaxLongSide` | `number` | `480` | 虚线轮廓降采样最长边 |
470
+ | `minContourArea` | `number` | `100` | 最小轮廓面积(随缩放同比缩放) |
471
+ | `contourApproxEpsilon` | `number` | `0.003` | 轮廓多边形逼近系数 |
472
+ | `maxRegions` | `number` | `500` | 分割阶段最大区域数上限 |
473
+
474
+ ### paintConfig
475
+
476
+ | 字段 | 类型 | 默认 | 说明 |
477
+ | ---- | ---- | ---- | ---- |
478
+ | `palette` | `BgrColor[]` | 6 色内置盘 | 底部笔刷色条 |
479
+ | `colorBaseOpacity` | `number` | `0.88` | 底色不透明度 |
480
+ | `lLightOpacity` | `number` | `0.50` | L 通道叠加强度 |
481
+ | `textureOpacity` | `number` | `0.85` | 高频纹理叠加强度(纹理保留更强) |
482
+ | `lLowBlurKernel` | `number` | `7` | 低频高斯核(奇数) |
483
+ | `lLowContrast` | `number` | `1.15` | 低频对比度 |
484
+ | `lLowBrightness` | `number` | `0.9` | 低频亮度 |
485
+ | `lHighGain` | `number` | `1.22` | 高频增益 |
486
+ | `maskFeatherColor` | `number` | `1.6` | 上色边缘羽化(颜色)——软边 alpha 半径,像素 |
487
+ | `maskFeatherTexture` | `number` | `0.9` | 上色边缘羽化(纹理)——预留/辅助 |
488
+ | `regionOverlayFill` | `string` | `rgba(20,120,235,0.58)` | 虚线/高亮填充色 |
489
+ | `regionOutlineStrokeWidth` | `number` | `4` | 虚线描边宽度 |
490
+
491
+ ### interactionConfig
492
+
493
+ | 字段 | 类型 | 默认 | 说明 |
494
+ | ---- | ---- | ---- | ---- |
495
+ | `pickMapSearchRadiusPx` | `number` | `14` | 点击 pickMap 搜索半径(像素) |
496
+ | `kickMaskPickRadiusPx` | `number` | `36` | 踢脚线掩码拾取半径 |
497
+ | `thinStripPadding` | `number` | `0.008` | 细条带(踢脚线)点击扩展比例 |
498
+ | `regionPadding` | `number` | `0.003` | 普通分区点击扩展比例 |
499
+ | `initRegionFlashMs` | `number` | `1000` | 初始化轮播每条虚线停留毫秒 |
500
+ | `enableInitRegionFlash` | `boolean` | `true` | 是否启用初始化轮播 |
501
+
502
+ > 完整默认值常量:`DEFAULT_MASK_CONFIG`、`DEFAULT_PIPELINE_CONFIG`、`DEFAULT_PAINT_CONFIG`、`DEFAULT_INTERACTION_CONFIG`(自包入口导出)。
503
+
504
+ ### UI 开关与样式
505
+
506
+
507
+ | 属性 | 类型 | 默认 | 说明 |
508
+ | ------------------------------------------------ | ---------------------- | ------- | ---------------------- |
509
+ | `showToolbar` | `boolean` | `true` | 顶部「清空缓存重新分区」工具栏 |
510
+ | `showColorBar` | `boolean` | `true` | 底部笔刷色条 |
511
+ | `showStatusRow` | `boolean` | `true` | 分割/加载状态文案 |
512
+ | `showOverlayButtons` | `boolean` | `true` | 左下撤销、右下对比原图按钮 |
513
+ | `showDebugPickers` | `boolean` | `true` | 相册选图调试入口(生产建议 `false`) |
514
+ | `disabled` | `boolean` | `false` | 禁用上色交互 |
515
+ | `style` | `ViewStyle` | — | 外层容器样式 |
516
+ | `canvasStyle` | `ViewStyle` | — | 画布区域样式 |
517
+ | `undoButtonStyle` / `compareButtonStyle` | `ViewStyle` | — | 浮层按钮样式 |
518
+ | `undoButtonTextStyle` / `compareButtonTextStyle` | `TextStyle` | — | 浮层按钮文字样式 |
519
+ | `undoButtonText` | `string` | `撤销` | 撤销按钮文案 |
520
+ | `compareButtonText` | `string` | `对比原图` | 进入对比模式文案 |
521
+ | `compareExitButtonText` | `string` | `退出对比` | 退出对比模式文案 |
522
+ | `renderUndoButton` | `(props) => ReactNode` | — | 自定义撤销按钮 |
523
+ | `renderCompareButton` | `(props) => ReactNode` | — | 自定义对比按钮 |
524
+
525
+
526
+ ### 回调
527
+
528
+ | 属性 | 签名 | 说明 |
529
+ | ---- | ---- | ---- |
530
+ | `onWatch` | `(state, durationMs, detail?) => void` | 初始化阶段回调;`durationMs` 自本次 `init` 起算 |
531
+ | `onPaintCallback` | `(payload: PaintCallbackPayload) => void` | 上色成功或未选笔刷时点击分区 |
532
+ | `onError` | `(message, error?) => void` | 分割或加载失败 |
533
+
534
+ `PaintCallbackPayload`(判别联合,`payload.kind` 区分):
535
+
536
+ ```ts
537
+ // 上色成功
538
+ {
539
+ kind: 'painted';
540
+ regionId: number;
541
+ regionName: string;
542
+ color: BgrColor;
543
+ configJson?: Record<string, unknown>; // setPaintColor / initialPaintConfigJson 传入
544
+ }
545
+
546
+ // 未选笔刷时点击有效分区(不会上色)
547
+ {
548
+ kind: 'brush_required';
549
+ hint: string; // 如「请先选择笔刷颜色(底部色条或 ref.setPaintColor)」
550
+ regionId: number;
551
+ regionName: string;
552
+ }
553
+ ```
554
+
555
+ 示例:
556
+
557
+ ```tsx
558
+ onPaintCallback={payload => {
559
+ if (payload.kind === 'brush_required') {
560
+ showToast(payload.hint);
561
+ return;
562
+ }
563
+ savePaintRecord(payload.regionId, payload.color, payload.configJson);
564
+ }}
565
+ ```
566
+
567
+ `onWatch` 的 `detail`(`MaskSegmentWatchDetail`):
568
+
569
+ | 字段 | 类型 | 说明 |
570
+ | ---- | ---- | ---- |
571
+ | `regionCount` | `number` | 当前有效分区数 |
572
+ | `maskPathsReady` | `boolean` | 轮廓 Skia 路径是否就绪 |
573
+ | `freqLayersReady` | `boolean` | 高低频 Shader 纹理是否就绪 |
574
+ | `errorMessage` | `string` | `error` 状态下的失败说明 |
575
+
576
+ #### onWatch 状态流转
577
+
578
+ ```
579
+ init
580
+ → images_loaded 原图 + 掩码读取完成
581
+ → mask_aligned 掩码尺寸对齐完成
582
+ → mask_sampled 掩码像素采样完成
583
+ → regions_ready 分区提取成功
584
+ → layers_ready 上色纹理图层就绪(detail.maskPathsReady 可能仍为 false)
585
+ → interactive 可交互(可点击选区、选色、上色)
586
+ → mask_paths_ready 轮廓路径就绪(初始化轮播虚线可显示,detail.maskPathsReady 为 true)
587
+ → error 失败(detail.errorMessage 有说明)
588
+ ```
589
+
590
+ `layers_ready` / `interactive` 可能在轮廓路径算完之前触发;业务侧若以 `interactive` 关闭 Loading,用户即可操作,轮播虚线会在 `mask_paths_ready` 后自动出现。
591
+
592
+ ---
593
+
594
+ ## Ref 方法
595
+
596
+ 通过 `ref` 调用(类型 `MaskSegmentCanvasRef`):
597
+
598
+ | 方法 | 签名 | 说明 |
599
+ | ---- | ---- | ---- |
600
+ | `reset` | `() => void` | 撤销上一步上色(按 `paintHistory`) |
601
+ | `swap` | `(showOrigin?: boolean) => void` | 对比原图;不传参 toggle,传 `true`/`false` 显式开关 |
602
+ | `save` | `(options?) => Promise<SavePaintResult>` | 合成并保存 PNG;`options.destDir` 可选输出目录 |
603
+ | `session` | `() => MaskSegmentSession` | 导出可 JSON 序列化会话(存 MMKV) |
604
+ | `loadSession` | `(session) => void` | 恢复上色状态(也可通过 `initialSession`) |
605
+ | `setPaintColor` | `(color, configJson?) => void` | 设置当前笔刷色,清空底部色条选中 |
606
+ | `setMaskConfig` | `(config) => void` | 运行时更新掩码配置并**重新分割** |
607
+ | `clearAllPaint` | `() => void` | 清空全部上色记录 |
608
+ | `resegment` | `() => Promise<void>` | 清空 PNG 缓存并重新分割 |
609
+ | `getRegions` | `() => SegmentRegion[]` | 当前分区列表快照 |
610
+ | `getPaintedRegions` | `() => PaintedRegionRecord[]` | 当前上色记录快照 |
611
+
612
+ `SavePaintResult`:`{ filePath, width, height, paintedCount, previewPath? }`
613
+
614
+ 代码示例:
615
+
616
+ ```tsx
617
+ const ref = useRef<MaskSegmentCanvasRef>(null);
618
+
619
+ ref.current?.reset();
620
+ ref.current?.swap(); // toggle
621
+ ref.current?.swap(true); // 强制显示原图
622
+
623
+ const result = await ref.current?.save({ destDir: '/path/to/dir' });
624
+
625
+ const session = ref.current?.session();
626
+ ref.current?.loadSession(session);
627
+
628
+ ref.current?.setPaintColor({ b: 100, g: 120, r: 140 }, { sku: 'paint-001' });
629
+ ref.current?.setMaskConfig({ semanticColors: customColors });
630
+
631
+ ref.current?.clearAllPaint();
632
+ await ref.current?.resegment();
633
+
634
+ const regions = ref.current?.getRegions();
635
+ const painted = ref.current?.getPaintedRegions();
636
+ ```
637
+
638
+ > `save` 依赖工作区 buffer 与 pickMap 就绪(通常 `interactive` 之后);未就绪时 throw `'图像尚未就绪,无法保存'`。
639
+
640
+ ### 存储约定
641
+
642
+
643
+ | 能力 | 建议存储 | 内容 |
644
+ | --------------- | ------------------- | ----------------------- |
645
+ | `ref.save()` | 文件系统 | 大图 PNG 路径 |
646
+ | `ref.session()` | MMKV / AsyncStorage | JSON 元数据(URL、上色记录、笔刷色等) |
647
+
648
+
649
+ `MaskSegmentSession` 结构:
650
+
651
+ ```ts
652
+ {
653
+ version: 1;
654
+ originUrl: string;
655
+ maskUrl: string;
656
+ painted: PaintedRegionRecord[]; // { regionId, regionName, color, configJson? }
657
+ paintHistory: number[];
658
+ currentColor?: BgrColor;
659
+ currentColorConfigJson?: Record<string, unknown>;
660
+ savedAt: number;
661
+ }
662
+ ```
663
+
664
+ ---
665
+
666
+ ## 交互说明
667
+
668
+ 1. **初始化轮播**:分区就绪后,按 `initRegionFlashMs`(默认 1s)轮播各分区虚线轮廓;用户触摸画布后停止。
669
+ 2. **预览(未选笔刷)**:长按分区显示当前触摸点所在连通块的虚线轮廓;点击黑色区域不显示虚线。
670
+ 3. **上色(已选笔刷)**:先点底部色条或 `ref.setPaintColor`(也可通过 `initialPaintColor` 预设),再点击分区上色;同一分区重复点击会覆盖颜色。
671
+ 4. **未选笔刷点击分区**:不会上色;`onPaintCallback` 以 `kind: 'brush_required'` 回调,携带 `hint` 与目标分区信息,由业务侧 Toast / 弹窗提示用户选色。
672
+ 5. **撤销**:左下角按钮或 `ref.reset()`,按上色历史逐步撤回。
673
+ 6. **对比原图**:右下角按钮或 `ref.swap()`,隐藏上色层查看原图。
674
+
675
+ ---
676
+
677
+ ## 接入业务示例
678
+
679
+ ### 下载后预热再挂载(推荐)
680
+
681
+ ```tsx
682
+ import { prewarmPngBgrCacheAsync } from 'react-native-mask-segment-canvas';
683
+
684
+ async function openPaintScreen(originUrl: string, maskUrl: string) {
685
+ await prewarmPngBgrCacheAsync([originUrl, maskUrl]);
686
+ navigation.navigate('Paint', { originUrl, maskUrl });
687
+ }
688
+ ```
689
+
690
+ ### 接口下载后传入本地路径
691
+
692
+ ```tsx
693
+ <MaskSegmentCanvas
694
+ originUrl={localOriginPath}
695
+ maskUrl={localMaskPath}
696
+ showDebugPickers={false}
697
+ showToolbar={false}
698
+ semanticColors={MASK_SEMANTIC_COLORS}
699
+ regionOutlineColor="#1e96ff"
700
+ onWatch={(state, ms, detail) => {
701
+ if (state === 'interactive') hideBlockingLoader();
702
+ if (state === 'mask_paths_ready') hideOutlineHint();
703
+ }}
704
+ />
705
+ ```
706
+
707
+ ### 草稿恢复
708
+
709
+ ```tsx
710
+ const draft = JSON.parse(mmkv.getString('paint_draft'));
711
+
712
+ <MaskSegmentCanvas
713
+ originUrl={draft.originUrl}
714
+ maskUrl={draft.maskUrl}
715
+ initialSession={draft}
716
+ />
717
+ ```
718
+
719
+ ### 自定义语义色表
720
+
721
+ ```tsx
722
+ const gymColors: MaskSemanticColor[] = [
723
+ { name: 'wall', hex: '#4363D8', bgr: { b: 216, g: 99, r: 67 } },
724
+ { name: 'ceiling', hex: '#3CB44B', bgr: { b: 75, g: 180, r: 60 } },
725
+ // ...
726
+ ];
727
+
728
+ <MaskSegmentCanvas
729
+ semanticColors={gymColors}
730
+ maskConfig={{ blackThreshold: 30, maxRegionColors: 6 }}
731
+ />
732
+ ```
733
+
734
+ ---
735
+
736
+ ## 项目结构
737
+
738
+ ```
739
+ MaskSegmentApp/ # 仓库根目录(npm 包 react-native-mask-segment-canvas)
740
+ ├── App.tsx # 库自测 Demo(直接引用 ./src)
741
+ ├── src/
742
+ │ ├── index.ts # npm 包入口(业务 import 'react-native-mask-segment-canvas')
743
+ │ ├── components/
744
+ │ │ ├── MaskSegmentCanvas.tsx
745
+ │ │ └── MaskSegmentCanvas.types.ts
746
+ │ └── utils/
747
+ │ ├── maskSegmentation.ts
748
+ │ ├── maskSegmentRuntime.ts
749
+ │ ├── maskSemanticPalette.ts
750
+ │ └── ...
751
+ ├── example/ # ★ 推荐:业务集成演示(消费者视角)
752
+ │ ├── App.tsx # 只使用公开 API 的完整示例页面
753
+ │ ├── index.js / app.json
754
+ │ ├── package.json # 展示所需依赖 + "react-native-mask-segment-canvas": "file:.."
755
+ │ ├── metro.config.js / babel.config.js / tsconfig.json
756
+ │ └── README.md # 如何在真实项目中接入的说明
757
+ ├── patches/ # 随包发布,宿主 postinstall 应用
758
+ ├── ios/ # 根 Demo 原生工程(不发布到 npm)
759
+ └── android/
760
+ ```
761
+
762
+ ---
763
+
764
+ ## 依赖说明
765
+
766
+
767
+ | 包 | 用途 |
768
+ | -------------------------------- | ---------------------------- |
769
+ | `@shopify/react-native-skia` | Canvas 渲染、Path、虚线描边、Blend 混合 |
770
+ | `react-native-fast-opencv` | 掩码形态学、轮廓处理 |
771
+ | `react-native-fs` | 图层缓存、保存 PNG |
772
+ | `react-native-image-picker` | Demo 相册选图 |
773
+ | `react-native-reanimated` | Skia 动画依赖 |
774
+ | `react-native-safe-area-context` | 安全区适配 |
775
+
776
+
777
+ ---
778
+
779
+ ## 性能评测
780
+
781
+ 以下数据基于 Demo 测试图(`assets/test/origin.png` **1080×1920**、6 个语义分区)、**默认 `pipelineConfig`**,以及 `onWatch` 的 `durationMs`(从 `init` 起算)。为**经验区间**,非严格基准测试;真机因 CPU、存储、RN 版本会有波动。
782
+
783
+ ### 实测参考(开发环境 + PNG 预热)
784
+
785
+ Demo 在挂载画布前调用 `prewarmPngBgrCacheAsync([origin, mask])`,PNG 解码命中内存缓存。典型日志:
786
+
787
+ | 阶段 | watchState | 约耗时 | 说明 |
788
+ | ---- | ---------- | ------ | ---- |
789
+ | 掩码对齐 | `mask_aligned` | ~160ms | 掩码缩放到分割工作分辨率 |
790
+ | 分区完成 | `regions_ready` / `mask_sampled` | ~320ms | 布局扫描 + 踢脚线 + pickMap |
791
+ | **可交互** | **`interactive`** | **~320–450ms** | 可点击选区、选色、Shader 上色 |
792
+ | 轮廓就绪 | `mask_paths_ready` | ~430–550ms | 比 `interactive` 晚 **~100ms**,轮播虚线可显示 |
793
+
794
+ `interactive` **不等待**轮廓路径;`mask_paths_ready` 仅影响初始化轮播虚线与可选 UI 提示。
795
+
796
+ 同图各子步骤(`__DEV__` 日志,默认 pipeline)量级:
797
+
798
+ | 子步骤 | 约耗时 | 工作分辨率 |
799
+ | ------ | ------ | ---------- |
800
+ | OpenCV LAB 高低频 | ~10–40ms | 270×480 |
801
+ | 高低频 Skia 纹理 | ~20–30ms | 同上 |
802
+ | 布局扫描 + 踢脚线 + 点击查表 | ~90–120ms | 405×720(1080p 缩至 longSide 720) |
803
+ | 全量轮廓路径(异步,不阻塞交互) | ~80–150ms | 270×480 |
804
+
805
+ ### 分辨率与 `pipelineConfig` 的关系
806
+
807
+ 计算密集型步骤被 **最长边上限** 截断,**不随 4K/8K 原图线性放大**;**PNG 全图解码**仍随像素量线性增长。
808
+
809
+ | 步骤 | 配置项 | 1080×1920 实际处理尺寸 | 随原图像素增长 |
810
+ | ---- | ------ | ---------------------- | -------------- |
811
+ | PNG 解码 | — | 1080×1920 × 2 张 | **是** |
812
+ | 掩码分割 / pickMap | `maxImageLongSide: 720` | ~405×720 | **否**(长边 >720 时固定) |
813
+ | Shader 高低频 | `paintFreqMaxLongSide: 480` | ~270×480 | **否** |
814
+ | 工作区 Skia 原图 | 同 `maxImageLongSide` | ~405×720 | **否** |
815
+ | 虚线轮廓 | `maskPathMaxLongSide: 480` | ~270×480 | **否**(不阻塞 `interactive`) |
816
+
817
+ ### `interactive` 预估(默认 pipeline)
818
+
819
+ | 原图规格 | 相对 1080p 像素 | 有 PNG 预热 | 冷启动(无预热) |
820
+ | -------- | --------------- | ----------- | ---------------- |
821
+ | 1080×1920 | 1× | **320–450ms** | **450–700ms** |
822
+ | 1440×2560 (2K) | ~1.8× | **400–550ms** | **600–900ms** |
823
+ | 3840×2160 (4K) | ~4× | **500–750ms** | **800–1200ms** |
824
+ | 7680×4320 (8K) | ~16× | **0.8–1.5s** | **1.5–3s+** |
825
+
826
+ > **300ms 内可交互**:在 1080p + 预热 + 默认 pipeline + 中高端机上**接近但偏乐观**;不宜作为全机型 SLA。
827
+
828
+ ### 机型档位(1080p,默认 pipeline)
829
+
830
+ 相对上述开发环境 ~320ms 的量级:
831
+
832
+ | 档位 | 相对倍数 | 有预热 `interactive` | 冷启动 |
833
+ | ---- | -------- | -------------------- | ------ |
834
+ | 旗舰 iOS / 新旗舰 Android | 0.8–1.2× | 300–450ms | 500–800ms |
835
+ | 中端 Android | 1.5–2.5× | 500–800ms | 700ms–1.2s |
836
+ | 低端 Android(4GB、老 U) | 2.5–4× | 800ms–1.3s | 1–2s+ |
837
+
838
+ Android 额外开销主要来自:JS ↔ OpenCV bridge、内存带宽/GC、Skia 纹理上传。
839
+
840
+ ### 提高 `maxImageLongSide` 的影响
841
+
842
+ 若将 `pipelineConfig.maxImageLongSide` 设为 **1280**(高于默认 720),分割工作区约 **720×1280**,像素约为 720 档的 **3×**:
843
+
844
+ | 场景 | 默认 720 | 改为 1280 |
845
+ | ---- | -------- | --------- |
846
+ | 1080p `interactive`(中端机) | ~320–800ms | **500ms–1s+** |
847
+ | 分割 / pickMap 耗时 | ~90–120ms | ~250–350ms |
848
+
849
+ 更高精度换更长初始化;若目标仍是 **<500ms 可交互**,建议维持默认 **720**,必要时降至 **640**。
850
+
851
+ ### 优化建议
852
+
853
+ 1. **PNG 预热(推荐)**:下载或解压完成后、进入画面前调用 `prewarmPngBgrCacheAsync`,通常可省 **100–250ms**(低端机收益最大)。
854
+
855
+ ```tsx
856
+ import { prewarmPngBgrCacheAsync } from 'react-native-mask-segment-canvas';
857
+
858
+ await prewarmPngBgrCacheAsync([originPath, maskPath]);
859
+ // 再挂载 MaskSegmentCanvas
860
+ ```
861
+
862
+ 2. **Loading 时机**:阻塞式 Loading 在 `interactive` 关闭;「轮廓准备中」可选监听 `mask_paths_ready`。
863
+ 3. **大图 / 低端机**:保持默认 `maxImageLongSide: 720`;可再将 `paintFreqMaxLongSide` 降至 **360**。
864
+ 4. **4K 素材**:业务侧先下采样再传入,或接受 **0.8–1.5s** 量级的 `interactive`(预热后)。
865
+ 5. **观测**:开发环境关注 Metro 中 `[MaskSegment]`、`[⏱ ...]` 与 `onWatch` 的 `durationMs`。
866
+
867
+ ---
868
+
869
+ ## 注意事项
870
+
871
+ - 掩码图应为与原图同尺寸的语义色块图(黑底 + 纯色分区);黑色区域(`blackThreshold` 默认 30 以下)不参与分区。
872
+ - OpenCV 分割在 JS 线程执行,超大图可能卡顿;可通过 `pipelineConfig.maxImageLongSide` 限制处理尺寸。
873
+ - iOS 相册选图需相册权限(仅 `showDebugPickers` 开启时用到)。
874
+ - `semanticColors` 须与后端/标注掩码的语义色保持一致,否则识别会偏移。
875
+
876
+ ---
877
+
878
+ ## 故障排查
879
+
880
+ **iOS pod install 失败**
881
+
882
+ ```bash
883
+ cd ios
884
+ bundle install
885
+ bundle exec pod install --repo-update
886
+ ```
887
+
888
+ **Android 编译报错**
889
+
890
+ ```bash
891
+ cd android && ./gradlew clean && cd ..
892
+ ```
893
+
894
+ **分割失败 / 分区为空**
895
+
896
+ - 确认 `originUrl` / `maskUrl` 可访问
897
+ - 确认掩码语义色与 `semanticColors` 配置一致
898
+ - 查看 Metro 日志中的 `[MaskSegment]` / `[⏱ ...]` 输出
899
+
900
+ **虚线不贴边 / 出现多余轮廓**
901
+
902
+ - 虚线基于掩码像素外轮廓生成,长按仅显示触摸点所在连通块
903
+ - 初始化轮播仅显示该语义分区最大连通块
904
+