feat: add MaskSegmentApp source code and config
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
08280f38e3
commit
56738b1f06
34
.npmignore
Normal file
34
.npmignore
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Demo app
|
||||||
|
example/
|
||||||
|
App.tsx
|
||||||
|
index.js
|
||||||
|
app.json
|
||||||
|
metro.config.js
|
||||||
|
babel.config.js
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
__tests__/
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Native demo projects
|
||||||
|
ios/
|
||||||
|
android/
|
||||||
|
|
||||||
|
# Config files (not needed in published package)
|
||||||
|
tsconfig.json
|
||||||
|
tsconfig.build.json
|
||||||
|
.eslintrc.js
|
||||||
|
.prettierrc.js
|
||||||
|
|
||||||
|
# Development
|
||||||
|
.cursor/
|
||||||
|
agent-transcripts/
|
||||||
|
assets/
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
290
App.tsx
290
App.tsx
@ -1,130 +1,220 @@
|
|||||||
/**
|
/**
|
||||||
* Sample React Native App
|
* Mask Segment Demo App
|
||||||
* https://github.com/facebook/react-native
|
* 基于 OpenCV + Skia 的掩码分区交互画布
|
||||||
*
|
*
|
||||||
* @format
|
* 注意:这是库开发自测用的 Demo,直接引用 ./src。
|
||||||
|
* 业务项目集成演示请参考 example/ 目录(使用公开包名导入)。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import type {PropsWithChildren} from 'react';
|
|
||||||
import {
|
import {
|
||||||
ScrollView,
|
ActivityIndicator,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
useColorScheme,
|
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import MaskSegmentCanvas, {
|
||||||
|
type MaskSegmentCanvasRef,
|
||||||
|
type MaskSegmentSession,
|
||||||
|
type MaskSegmentWatchState,
|
||||||
|
MASK_SEMANTIC_COLORS,
|
||||||
|
} from './src';
|
||||||
|
import { resolveAssetPath } from './src/utils/resolveAssetPath';
|
||||||
|
import { prewarmPngBgrCacheAsync } from './src/utils/pngImage';
|
||||||
|
|
||||||
import {
|
const TEST_ORIGIN = require('./assets/test/origin.png');
|
||||||
Colors,
|
const TEST_MASK = require('./assets/test/mask.png');
|
||||||
DebugInstructions,
|
|
||||||
Header,
|
|
||||||
LearnMoreLinks,
|
|
||||||
ReloadInstructions,
|
|
||||||
} from 'react-native/Libraries/NewAppScreen';
|
|
||||||
|
|
||||||
type SectionProps = PropsWithChildren<{
|
const INTERACTIVE_WATCH_STATES: MaskSegmentWatchState[] = [
|
||||||
title: string;
|
'interactive',
|
||||||
}>;
|
'mask_paths_ready',
|
||||||
|
];
|
||||||
|
|
||||||
function Section({children, title}: SectionProps): React.JSX.Element {
|
function formatWatchStatus(state: MaskSegmentWatchState | ''): string {
|
||||||
const isDarkMode = useColorScheme() === 'dark';
|
if (!state) {
|
||||||
return (
|
return '';
|
||||||
<View style={styles.sectionContainer}>
|
}
|
||||||
<Text
|
if (state === 'interactive') {
|
||||||
style={[
|
return '可上色(轮廓加载中…)';
|
||||||
styles.sectionTitle,
|
}
|
||||||
{
|
if (state === 'mask_paths_ready') {
|
||||||
color: isDarkMode ? Colors.white : Colors.black,
|
return '就绪';
|
||||||
},
|
}
|
||||||
]}>
|
if (state === 'error') {
|
||||||
{title}
|
return '失败';
|
||||||
</Text>
|
}
|
||||||
<Text
|
return `加载中:${state}`;
|
||||||
style={[
|
|
||||||
styles.sectionDescription,
|
|
||||||
{
|
|
||||||
color: isDarkMode ? Colors.light : Colors.dark,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
{children}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function App(): React.JSX.Element {
|
function App(): React.JSX.Element {
|
||||||
const isDarkMode = useColorScheme() === 'dark';
|
const canvasRef = useRef<MaskSegmentCanvasRef>(null);
|
||||||
|
const [testPaths, setTestPaths] = useState<{
|
||||||
|
origin: string;
|
||||||
|
mask: string;
|
||||||
|
} | null>(null);
|
||||||
|
const [loadError, setLoadError] = useState('');
|
||||||
|
const [watchState, setWatchState] = useState<MaskSegmentWatchState | ''>('');
|
||||||
|
const [sessionDraft, setSessionDraft] = useState<MaskSegmentSession | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const isFullyReady = watchState === 'mask_paths_ready';
|
||||||
|
const isInitLoading =
|
||||||
|
testPaths != null &&
|
||||||
|
watchState !== '' &&
|
||||||
|
!INTERACTIVE_WATCH_STATES.includes(watchState as MaskSegmentWatchState) &&
|
||||||
|
watchState !== 'error';
|
||||||
|
|
||||||
const backgroundStyle = {
|
useEffect(() => {
|
||||||
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
|
let cancelled = false;
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
(async () => {
|
||||||
* To keep the template simple and small we're adding padding to prevent view
|
try {
|
||||||
* from rendering under the System UI.
|
const [origin, mask] = await Promise.all([
|
||||||
* For bigger apps the recommendation is to use `react-native-safe-area-context`:
|
resolveAssetPath(TEST_ORIGIN, 'gym_test_origin.png'),
|
||||||
* https://github.com/AppAndFlow/react-native-safe-area-context
|
resolveAssetPath(TEST_MASK, 'gym_test_mask.png'),
|
||||||
*
|
]);
|
||||||
* You can read more about it here:
|
await prewarmPngBgrCacheAsync([origin, mask]);
|
||||||
* https://github.com/react-native-community/discussions-and-proposals/discussions/827
|
if (!cancelled) {
|
||||||
*/
|
setTestPaths({ origin, mask });
|
||||||
const safePadding = '5%';
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!cancelled) {
|
||||||
|
setLoadError(e instanceof Error ? e.message : String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={backgroundStyle}>
|
<SafeAreaProvider>
|
||||||
<StatusBar
|
<SafeAreaView style={styles.root}>
|
||||||
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
|
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
|
||||||
backgroundColor={backgroundStyle.backgroundColor}
|
{loadError ? (
|
||||||
/>
|
<View style={styles.center}>
|
||||||
<ScrollView
|
<Text style={styles.errorText}>{loadError}</Text>
|
||||||
style={backgroundStyle}>
|
</View>
|
||||||
<View style={{paddingRight: safePadding}}>
|
) : testPaths ? (
|
||||||
<Header/>
|
<>
|
||||||
</View>
|
{watchState ? (
|
||||||
<View
|
<Text style={styles.watchText}>
|
||||||
style={{
|
状态: {formatWatchStatus(watchState)}
|
||||||
backgroundColor: isDarkMode ? Colors.black : Colors.white,
|
{isFullyReady ? ' · 轮播虚线已就绪' : null}
|
||||||
paddingHorizontal: safePadding,
|
</Text>
|
||||||
paddingBottom: safePadding,
|
) : null}
|
||||||
}}>
|
<View style={styles.canvasHost}>
|
||||||
<Section title="Step One">
|
<MaskSegmentCanvas
|
||||||
Edit <Text style={styles.highlight}>App.tsx</Text> to change this
|
ref={canvasRef}
|
||||||
screen and then come back to see your edits.
|
originUrl={testPaths.origin}
|
||||||
</Section>
|
maskUrl={testPaths.mask}
|
||||||
<Section title="See Your Changes">
|
semanticColors={MASK_SEMANTIC_COLORS}
|
||||||
<ReloadInstructions />
|
regionOutlineColor="rgba(20, 120, 235, 0.58)"
|
||||||
</Section>
|
showDebugPickers
|
||||||
<Section title="Debug">
|
initialSession={sessionDraft ?? undefined}
|
||||||
<DebugInstructions />
|
onWatch={(state, durationMs, detail) => {
|
||||||
</Section>
|
setWatchState(state);
|
||||||
<Section title="Learn More">
|
if (__DEV__) {
|
||||||
Read the docs to discover what to do next:
|
const extra =
|
||||||
</Section>
|
detail && Object.keys(detail).length > 0
|
||||||
<LearnMoreLinks />
|
? ` ${JSON.stringify(detail)}`
|
||||||
</View>
|
: '';
|
||||||
</ScrollView>
|
console.log(
|
||||||
</View>
|
`[Demo onWatch] ${state} ${durationMs.toFixed(0)}ms${extra}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onPaintCallback={payload => {
|
||||||
|
if (__DEV__) {
|
||||||
|
if (payload.kind === 'brush_required') {
|
||||||
|
console.log('[Demo onPaint]', payload.hint, payload.regionName);
|
||||||
|
} else {
|
||||||
|
console.log('[Demo onPaint]', payload.regionName, payload.color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onError={message => {
|
||||||
|
setLoadError(message);
|
||||||
|
setWatchState('error');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{isInitLoading ? (
|
||||||
|
<View style={styles.initOverlay} pointerEvents="none">
|
||||||
|
<ActivityIndicator size="small" color="#333" />
|
||||||
|
<Text style={styles.initOverlayText}>
|
||||||
|
{formatWatchStatus(watchState)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
{sessionDraft ? (
|
||||||
|
<Text style={styles.sessionText}>
|
||||||
|
已恢复 MMKV 草稿({sessionDraft.painted.length} 区域)
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<View style={styles.center}>
|
||||||
|
<ActivityIndicator size="large" color="#333" />
|
||||||
|
<Text style={styles.loadingText}>加载健身房测试图…</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</SafeAreaView>
|
||||||
|
</SafeAreaProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
sectionContainer: {
|
root: {
|
||||||
marginTop: 32,
|
flex: 1,
|
||||||
paddingHorizontal: 24,
|
backgroundColor: '#fff',
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
center: {
|
||||||
fontSize: 24,
|
flex: 1,
|
||||||
fontWeight: '600',
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: 24,
|
||||||
},
|
},
|
||||||
sectionDescription: {
|
loadingText: {
|
||||||
marginTop: 8,
|
marginTop: 12,
|
||||||
fontSize: 18,
|
color: '#666',
|
||||||
fontWeight: '400',
|
fontSize: 14,
|
||||||
},
|
},
|
||||||
highlight: {
|
errorText: {
|
||||||
fontWeight: '700',
|
color: '#c33',
|
||||||
|
fontSize: 14,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
watchText: {
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingTop: 8,
|
||||||
|
color: '#555',
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
canvasHost: {
|
||||||
|
flex: 1,
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
initOverlay: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.72)',
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
initOverlayText: {
|
||||||
|
color: '#666',
|
||||||
|
fontSize: 13,
|
||||||
|
},
|
||||||
|
sessionText: {
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingBottom: 8,
|
||||||
|
color: '#2a7',
|
||||||
|
fontSize: 12,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
949
README.md
949
README.md
@ -1,97 +1,904 @@
|
|||||||
This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli).
|
# react-native-mask-segment-canvas
|
||||||
|
|
||||||
# Getting Started
|
基于 React Native **0.79** 的掩码分区交互库,核心导出 `MaskSegmentCanvas` 组件,可通过 **npm 包** 或 **npm link** 接入其它 RN 工程。
|
||||||
|
|
||||||
> **Note**: Make sure you have completed the [Set Up Your Environment](https://reactnative.dev/docs/set-up-your-environment) guide before proceeding.
|
- **OpenCV**(`react-native-fast-opencv`):掩码语义布局、踢脚线修补、分区提取
|
||||||
|
- **Skia RuntimeEffect(SkSL)**:原图 + LAB 高低频纹理叠色(单次全屏 Shader)
|
||||||
|
- **Skia Path**:分区虚线轮廓高亮
|
||||||
|
- **交互**:底部笔刷选色(可选初始化)→ 点击分区上色;未选笔刷时点击分区会通过 `onPaintCallback` 提示;未选笔刷时长按预览分区虚线轮廓
|
||||||
|
|
||||||
## Step 1: Start Metro
|
本仓库同时作为 **库源码**(`src/index.ts`)与 **自测 Demo**(根目录 `App.tsx`)维护。
|
||||||
|
|
||||||
First, you will need to run **Metro**, the JavaScript build tool for React Native.
|
**推荐的集成演示请查看 `example/` 目录**:它只使用公开 API,完整模拟业务项目接入方式(含 `package.json`、Metro 配置、完整可参考的 `App.tsx`)。
|
||||||
|
|
||||||
To start the Metro dev server, run the following command from the root of your React Native project:
|
---
|
||||||
|
|
||||||
|
## 作为 npm 包接入其它工程
|
||||||
|
|
||||||
|
### 安装依赖(宿主工程)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install react-native-mask-segment-canvas
|
||||||
|
# 或本地联调
|
||||||
|
npm link ../MaskSegmentApp # 在库目录先执行 npm link
|
||||||
|
npm link react-native-mask-segment-canvas
|
||||||
|
```
|
||||||
|
|
||||||
|
宿主工程还需安装 **peerDependencies**(版本需与宿主 RN 对齐):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @shopify/react-native-skia react-native-reanimated react-native-fast-opencv react-native-fs buffer upng-js
|
||||||
|
# 若使用 showDebugPickers 相册选图
|
||||||
|
npm install react-native-image-picker
|
||||||
|
```
|
||||||
|
|
||||||
|
### 宿主工程 postinstall(必需)
|
||||||
|
|
||||||
|
本库依赖 `patch-package` 修补 `react-native-fast-opencv`,宿主 `package.json` 需配置:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"postinstall": "patch-package"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"patch-package": "^8.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
安装本库后,`node_modules/react-native-mask-segment-canvas/patches/` 中的补丁会在宿主 `postinstall` 时自动应用。
|
||||||
|
|
||||||
|
### iOS / Android 原生依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ios && pod install && cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
确保宿主已按各原生库文档完成 Skia、Reanimated、OpenCV 等配置。
|
||||||
|
|
||||||
|
### Metro 配置(npm link 时)
|
||||||
|
|
||||||
|
联调时若出现模块解析问题,在宿主 `metro.config.js` 中把本库加入 `watchFolders` / `extraNodeModules`(按宿主 Metro 版本调整):
|
||||||
|
|
||||||
|
```js
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
watchFolders: [path.resolve(__dirname, '../MaskSegmentApp')],
|
||||||
|
resolver: {
|
||||||
|
nodeModulesPaths: [path.resolve(__dirname, 'node_modules')],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 业务侧引入
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import MaskSegmentCanvas, {
|
||||||
|
type MaskSegmentCanvasRef,
|
||||||
|
type MaskSegmentSession,
|
||||||
|
type MaskSegmentWatchState,
|
||||||
|
type MaskSegmentWatchDetail,
|
||||||
|
type BgrColor,
|
||||||
|
type MaskSemanticColor,
|
||||||
|
type PaintCallbackPayload,
|
||||||
|
type PaintedRegionRecord,
|
||||||
|
type PipelineConfig,
|
||||||
|
type MaskSegmentConfig,
|
||||||
|
type PaintConfig,
|
||||||
|
type InteractionConfig,
|
||||||
|
type SavePaintResult,
|
||||||
|
MASK_SEMANTIC_COLORS,
|
||||||
|
BASEBOARD_SEMANTIC_NAME,
|
||||||
|
prewarmPngBgrCacheAsync,
|
||||||
|
DEFAULT_PIPELINE_CONFIG,
|
||||||
|
DEFAULT_MASK_CONFIG,
|
||||||
|
DEFAULT_PAINT_CONFIG,
|
||||||
|
DEFAULT_INTERACTION_CONFIG,
|
||||||
|
} from 'react-native-mask-segment-canvas';
|
||||||
|
```
|
||||||
|
|
||||||
|
主要导出一览:
|
||||||
|
|
||||||
|
| 类别 | 名称 |
|
||||||
|
| ---- | ---- |
|
||||||
|
| 组件 | `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` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 推荐:通过 example/ 目录学习集成
|
||||||
|
|
||||||
|
`example/` 是**专门为业务侧集成准备的演示文件夹**,它:
|
||||||
|
|
||||||
|
- 只通过 `import ... from 'react-native-mask-segment-canvas'` 使用公开 API(不碰内部 src)
|
||||||
|
- 提供了独立的 `package.json`(含 peer deps + 本地 file 依赖)
|
||||||
|
- 包含针对本地联调的 `metro.config.js`
|
||||||
|
- `App.tsx` 是一个可直接参考的完整页面,涵盖预热、状态管理、ref 操作、回调处理等
|
||||||
|
|
||||||
|
建议:
|
||||||
|
|
||||||
|
1. 直接阅读 `example/App.tsx` 获取最新可运行的集成写法。
|
||||||
|
2. 按 `example/README.md` 的步骤在本机跑起来,验证安装、patch、Metro 配置是否正确。
|
||||||
|
3. 把 `example/App.tsx` 中的核心逻辑复制到你自己的页面/组件中即可。
|
||||||
|
|
||||||
|
这样可以确保你接入的是「库的公开契约」,而不是内部实现细节。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
|
||||||
|
- Node.js >= 18(推荐 20+)
|
||||||
|
- Xcode 15+(iOS)
|
||||||
|
- Android Studio + JDK 17(Android)
|
||||||
|
- CocoaPods(iOS)
|
||||||
|
|
||||||
|
## 快速开始(本仓库 Demo)
|
||||||
|
|
||||||
|
根目录 `App.tsx` 是库作者自测用的完整 Demo,内部直接 import `./src`。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd MaskSegmentApp
|
||||||
|
|
||||||
|
npm install
|
||||||
|
|
||||||
|
cd ios && bundle exec pod install && cd ..
|
||||||
|
|
||||||
```sh
|
|
||||||
# Using npm
|
|
||||||
npm start
|
npm start
|
||||||
|
|
||||||
# OR using Yarn
|
# 另开终端
|
||||||
yarn start
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step 2: Build and run your app
|
|
||||||
|
|
||||||
With Metro running, open a new terminal window/pane from the root of your React Native project, and use one of the following commands to build and run your Android or iOS app:
|
|
||||||
|
|
||||||
### Android
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Using npm
|
|
||||||
npm run android
|
|
||||||
|
|
||||||
# OR using Yarn
|
|
||||||
yarn android
|
|
||||||
```
|
|
||||||
|
|
||||||
### iOS
|
|
||||||
|
|
||||||
For iOS, remember to install CocoaPods dependencies (this only needs to be run on first clone or after updating native deps).
|
|
||||||
|
|
||||||
The first time you create a new project, run the Ruby bundler to install CocoaPods itself:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
bundle install
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, and every time you update your native dependencies, run:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
bundle exec pod install
|
|
||||||
```
|
|
||||||
|
|
||||||
For more information, please visit [CocoaPods Getting Started guide](https://guides.cocoapods.org/using/getting-started.html).
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Using npm
|
|
||||||
npm run ios
|
npm run ios
|
||||||
|
# 或
|
||||||
# OR using Yarn
|
npm run android
|
||||||
yarn ios
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If everything is set up correctly, you should see your new app running in the Android Emulator, iOS Simulator, or your connected device.
|
**想看「纯业务项目如何集成」**:请进入 `example/` 目录,按其 `README.md` 操作。它使用 `import from 'react-native-mask-segment-canvas'` + 标准的 `package.json` + Metro 配置,完全模拟消费者环境。
|
||||||
|
|
||||||
This is one way to run your app — you can also build it directly from Android Studio or Xcode.
|
Demo 入口 `App.tsx` 通过 `./src`(即包入口 `src/index.ts`)引用组件,与业务侧 `import from 'react-native-mask-segment-canvas'` 等价。
|
||||||
|
|
||||||
## Step 3: Modify your app
|
---
|
||||||
|
|
||||||
Now that you have successfully run the app, let's make changes!
|
## MaskSegmentCanvas 组件
|
||||||
|
|
||||||
Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](https://reactnative.dev/docs/fast-refresh).
|
### 引入
|
||||||
|
|
||||||
When you want to forcefully reload, for example to reset the state of your app, you can perform a full reload:
|
```tsx
|
||||||
|
import React, { useRef } from 'react';
|
||||||
|
import MaskSegmentCanvas, {
|
||||||
|
type MaskSegmentCanvasRef,
|
||||||
|
type MaskSegmentSession,
|
||||||
|
type MaskSegmentWatchState,
|
||||||
|
type MaskSegmentWatchDetail,
|
||||||
|
type BgrColor,
|
||||||
|
type MaskSemanticColor,
|
||||||
|
type PaintCallbackPayload,
|
||||||
|
MASK_SEMANTIC_COLORS,
|
||||||
|
prewarmPngBgrCacheAsync,
|
||||||
|
} from 'react-native-mask-segment-canvas';
|
||||||
|
```
|
||||||
|
|
||||||
- **Android**: Press the <kbd>R</kbd> key twice or select **"Reload"** from the **Dev Menu**, accessed via <kbd>Ctrl</kbd> + <kbd>M</kbd> (Windows/Linux) or <kbd>Cmd ⌘</kbd> + <kbd>M</kbd> (macOS).
|
也可按需导入运行时默认值(与组件 Props 合并使用):
|
||||||
- **iOS**: Press <kbd>R</kbd> in iOS Simulator.
|
|
||||||
|
|
||||||
## Congratulations! :tada:
|
```tsx
|
||||||
|
import {
|
||||||
|
DEFAULT_PIPELINE_CONFIG,
|
||||||
|
DEFAULT_MASK_CONFIG,
|
||||||
|
DEFAULT_PAINT_CONFIG,
|
||||||
|
DEFAULT_INTERACTION_CONFIG,
|
||||||
|
} from 'react-native-mask-segment-canvas';
|
||||||
|
```
|
||||||
|
|
||||||
You've successfully run and modified your React Native App. :partying_face:
|
### 最小示例
|
||||||
|
|
||||||
### Now what?
|
下面是一个可直接放进业务页面的完整示例,涵盖 **PNG 预热**、**state**、**配置**、**加载态**、**onWatch** 与 **ref** 常用操作。
|
||||||
|
|
||||||
- If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps).
|
```tsx
|
||||||
- If you're curious to learn more about React Native, check out the [docs](https://reactnative.dev/docs/getting-started).
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { ActivityIndicator, Text, View } from 'react-native';
|
||||||
|
import MaskSegmentCanvas, {
|
||||||
|
type MaskSegmentCanvasRef,
|
||||||
|
type MaskSegmentSession,
|
||||||
|
type MaskSegmentWatchState,
|
||||||
|
type BgrColor,
|
||||||
|
MASK_SEMANTIC_COLORS,
|
||||||
|
prewarmPngBgrCacheAsync,
|
||||||
|
} from 'react-native-mask-segment-canvas';
|
||||||
|
|
||||||
# Troubleshooting
|
/** 业务侧准备好的图片地址(本地 file:// 或 http(s)://) */
|
||||||
|
type ImagePaths = {
|
||||||
|
origin: string;
|
||||||
|
mask: string;
|
||||||
|
};
|
||||||
|
|
||||||
If you're having issues getting the above steps to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page.
|
const INTERACTIVE_STATES: MaskSegmentWatchState[] = [
|
||||||
|
'interactive',
|
||||||
|
'mask_paths_ready',
|
||||||
|
];
|
||||||
|
|
||||||
# Learn More
|
export function PaintScreen() {
|
||||||
|
const canvasRef = useRef<MaskSegmentCanvasRef>(null);
|
||||||
|
|
||||||
To learn more about React Native, take a look at the following resources:
|
const [imagePaths, setImagePaths] = useState<ImagePaths | null>(null);
|
||||||
|
const [pathsError, setPathsError] = useState('');
|
||||||
|
const [watchState, setWatchState] = useState<MaskSegmentWatchState | ''>('');
|
||||||
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
|
const [sessionDraft] = useState<MaskSegmentSession | null>(null);
|
||||||
|
|
||||||
|
const isInteractive = INTERACTIVE_STATES.includes(
|
||||||
|
watchState as MaskSegmentWatchState,
|
||||||
|
);
|
||||||
|
const isOutlineReady = watchState === 'mask_paths_ready';
|
||||||
|
const isCanvasLoading =
|
||||||
|
imagePaths != null &&
|
||||||
|
watchState !== '' &&
|
||||||
|
!INTERACTIVE_STATES.includes(watchState as MaskSegmentWatchState) &&
|
||||||
|
watchState !== 'error';
|
||||||
|
|
||||||
|
// 示例:接口下载完成后写入路径,并预热 PNG 解码缓存
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const origin = 'file:///path/to/origin.png';
|
||||||
|
const mask = 'file:///path/to/mask.png';
|
||||||
|
await prewarmPngBgrCacheAsync([origin, mask]);
|
||||||
|
if (!cancelled) {
|
||||||
|
setImagePaths({ origin, mask });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!cancelled) {
|
||||||
|
setPathsError(e instanceof Error ? e.message : String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!isInteractive) return;
|
||||||
|
const result = await canvasRef.current?.save({ destDir: undefined });
|
||||||
|
console.log('saved', result?.filePath, result?.paintedCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pathsError) {
|
||||||
|
return <Text>{pathsError}</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!imagePaths) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<ActivityIndicator />
|
||||||
|
<Text>等待原图与掩码…</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
{isCanvasLoading ? <Text>加载中:{watchState}</Text> : null}
|
||||||
|
{watchState === 'interactive' ? (
|
||||||
|
<Text>可上色(轮廓加载中…)</Text>
|
||||||
|
) : null}
|
||||||
|
{isOutlineReady ? <Text>就绪</Text> : null}
|
||||||
|
{errorMessage ? <Text>{errorMessage}</Text> : null}
|
||||||
|
|
||||||
|
<MaskSegmentCanvas
|
||||||
|
ref={canvasRef}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
originUrl={imagePaths.origin}
|
||||||
|
maskUrl={imagePaths.mask}
|
||||||
|
semanticColors={MASK_SEMANTIC_COLORS}
|
||||||
|
regionOutlineColor="rgba(20, 120, 235, 0.58)"
|
||||||
|
maskConfig={{ blackThreshold: 30, maxRegionColors: 6 }}
|
||||||
|
pipelineConfig={{ maxImageLongSide: 720 }}
|
||||||
|
paintConfig={{ colorBaseOpacity: 0.88 }}
|
||||||
|
interactionConfig={{
|
||||||
|
initRegionFlashMs: 1000,
|
||||||
|
enableInitRegionFlash: true,
|
||||||
|
}}
|
||||||
|
initialSession={sessionDraft ?? undefined}
|
||||||
|
showDebugPickers={false}
|
||||||
|
showToolbar={false}
|
||||||
|
showColorBar
|
||||||
|
showStatusRow={false}
|
||||||
|
showOverlayButtons
|
||||||
|
disabled={!isInteractive}
|
||||||
|
onWatch={(state, durationMs, detail) => {
|
||||||
|
setWatchState(state);
|
||||||
|
// detail: regionCount, maskPathsReady, freqLayersReady, errorMessage
|
||||||
|
console.log('[onWatch]', state, durationMs, detail);
|
||||||
|
}}
|
||||||
|
onPaintCallback={payload => {
|
||||||
|
if (payload.kind === 'brush_required') {
|
||||||
|
toast(payload.hint);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('painted', payload.regionId, payload.regionName, payload.color, payload.configJson);
|
||||||
|
}}
|
||||||
|
onError={message => {
|
||||||
|
setErrorMessage(message);
|
||||||
|
setWatchState('error');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* ref 示例:disabled={!isInteractive} 时可按需绑定 */}
|
||||||
|
{/* <Button title="保存" onPress={handleSave} disabled={!isInteractive} /> */}
|
||||||
|
{/* <Button title="撤销" onPress={() => canvasRef.current?.reset()} /> */}
|
||||||
|
{/* <Button title="对比" onPress={() => canvasRef.current?.swap()} /> */}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 示例里涉及的 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 等恢复的草稿 |
|
||||||
|
|
||||||
|
|
||||||
|
#### 配置项怎么选
|
||||||
|
|
||||||
|
|
||||||
|
| 配置 | 何时用顶层属性 | 何时用嵌套 Config |
|
||||||
|
| ---------- | -------------------------------- | ----------------------------------------- |
|
||||||
|
| 语义识别色 | `semanticColors={...}` 大多数场景 | `maskConfig.semanticColors` 需与更多掩码参数一起传时 |
|
||||||
|
| 虚线颜色 | `regionOutlineColor="..."` 大多数场景 | `paintConfig.regionOverlayFill` 需同时改笔刷盘等时 |
|
||||||
|
| 黑色阈值、最大分区数 | — | `maskConfig` |
|
||||||
|
| 图片处理尺寸 | — | `pipelineConfig` |
|
||||||
|
| 轮播间隔、点击容差 | — | `interactionConfig` |
|
||||||
|
|
||||||
|
|
||||||
|
顶层属性与嵌套 Config **可同时传**,顶层 `semanticColors` / `regionOutlineColor` 优先级更高。
|
||||||
|
|
||||||
|
#### watchState 与 UI 建议
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 阻塞式 Loading(分区 + 上色图层就绪前)
|
||||||
|
const isLoading = ![
|
||||||
|
'interactive',
|
||||||
|
'mask_paths_ready',
|
||||||
|
'error',
|
||||||
|
'',
|
||||||
|
].includes(watchState);
|
||||||
|
|
||||||
|
// 允许点击选区、选色、上色(不必等轮廓路径)
|
||||||
|
const canOperate =
|
||||||
|
watchState === 'interactive' || watchState === 'mask_paths_ready';
|
||||||
|
|
||||||
|
// 初始化轮播虚线已全部就绪(可选,用于收起「轮廓准备中」提示)
|
||||||
|
const isOutlineReady = watchState === 'mask_paths_ready';
|
||||||
|
|
||||||
|
// 显示错误页
|
||||||
|
const hasError = watchState === 'error';
|
||||||
|
```
|
||||||
|
|
||||||
|
`interactive` 时 `detail.maskPathsReady` 一般为 `false`;`mask_paths_ready` 时为 `true`。两者通常相差约 100ms(异步构建 Skia 轮廓路径),不影响点击上色。
|
||||||
|
|
||||||
|
`originUrl` / `maskUrl` 支持:
|
||||||
|
|
||||||
|
- 本地路径:`file:///...` 或绝对路径
|
||||||
|
- 远程地址:`http(s)://...`(组件内部会下载/解析)
|
||||||
|
|
||||||
|
> 兼容旧属性 `originImgPath` / `maskImgPath`(已标记 deprecated,请改用 `originUrl` / `maskUrl`)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
### 图片与初始化
|
||||||
|
|
||||||
|
| 属性 | 类型 | 必填 | 默认 | 说明 |
|
||||||
|
| ---- | ---- | ---- | ---- | ---- |
|
||||||
|
| `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<string, unknown>` | 否 | — | **可选**。与 `initialPaintColor` 配套的笔刷配置,上色成功时随 `onPaintCallback` 回传 |
|
||||||
|
|
||||||
|
### 识别色与虚线(顶层便捷配置)
|
||||||
|
|
||||||
|
| 属性 | 类型 | 默认 | 说明 |
|
||||||
|
| ---- | ---- | ---- | ---- |
|
||||||
|
| `semanticColors` | `MaskSemanticColor[]` | `MASK_SEMANTIC_COLORS` | 掩码语义识别色,等同 `maskConfig.semanticColors` |
|
||||||
|
| `regionOutlineColor` | `string` | `rgba(20, 120, 235, 0.58)` | 分区虚线高亮色,等同 `paintConfig.regionOverlayFill` |
|
||||||
|
|
||||||
|
顶层属性优先级高于嵌套 `maskConfig` / `paintConfig`。
|
||||||
|
|
||||||
|
`MaskSemanticColor` 结构:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
name: string; // 语义名,如 wall / ceiling / baseboard
|
||||||
|
hex: string; // 展示用十六进制色
|
||||||
|
bgr: { b: number; g: number; r: number }; // 与掩码像素 BGR 通道一致
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
内置色表:`MASK_SEMANTIC_COLORS`(`src/utils/maskSemanticPalette.ts`)。
|
||||||
|
|
||||||
|
### 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 长度 |
|
||||||
|
|
||||||
|
### 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` | 分割阶段最大区域数上限 |
|
||||||
|
|
||||||
|
### 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` | 虚线描边宽度 |
|
||||||
|
|
||||||
|
### interactionConfig
|
||||||
|
|
||||||
|
| 字段 | 类型 | 默认 | 说明 |
|
||||||
|
| ---- | ---- | ---- | ---- |
|
||||||
|
| `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`(自包入口导出)。
|
||||||
|
|
||||||
|
### UI 开关与样式
|
||||||
|
|
||||||
|
|
||||||
|
| 属性 | 类型 | 默认 | 说明 |
|
||||||
|
| ------------------------------------------------ | ---------------------- | ------- | ---------------------- |
|
||||||
|
| `showToolbar` | `boolean` | `true` | 顶部「清空缓存重新分区」工具栏 |
|
||||||
|
| `showColorBar` | `boolean` | `true` | 底部笔刷色条 |
|
||||||
|
| `showStatusRow` | `boolean` | `true` | 分割/加载状态文案 |
|
||||||
|
| `showOverlayButtons` | `boolean` | `true` | 左下撤销、右下对比原图按钮 |
|
||||||
|
| `showDebugPickers` | `boolean` | `true` | 相册选图调试入口(生产建议 `false`) |
|
||||||
|
| `disabled` | `boolean` | `false` | 禁用上色交互 |
|
||||||
|
| `style` | `ViewStyle` | — | 外层容器样式 |
|
||||||
|
| `canvasStyle` | `ViewStyle` | — | 画布区域样式 |
|
||||||
|
| `undoButtonStyle` / `compareButtonStyle` | `ViewStyle` | — | 浮层按钮样式 |
|
||||||
|
| `undoButtonTextStyle` / `compareButtonTextStyle` | `TextStyle` | — | 浮层按钮文字样式 |
|
||||||
|
| `undoButtonText` | `string` | `撤销` | 撤销按钮文案 |
|
||||||
|
| `compareButtonText` | `string` | `对比原图` | 进入对比模式文案 |
|
||||||
|
| `compareExitButtonText` | `string` | `退出对比` | 退出对比模式文案 |
|
||||||
|
| `renderUndoButton` | `(props) => ReactNode` | — | 自定义撤销按钮 |
|
||||||
|
| `renderCompareButton` | `(props) => ReactNode` | — | 自定义对比按钮 |
|
||||||
|
|
||||||
|
|
||||||
|
### 回调
|
||||||
|
|
||||||
|
| 属性 | 签名 | 说明 |
|
||||||
|
| ---- | ---- | ---- |
|
||||||
|
| `onWatch` | `(state, durationMs, detail?) => void` | 初始化阶段回调;`durationMs` 自本次 `init` 起算 |
|
||||||
|
| `onPaintCallback` | `(payload: PaintCallbackPayload) => void` | 上色成功或未选笔刷时点击分区 |
|
||||||
|
| `onError` | `(message, error?) => void` | 分割或加载失败 |
|
||||||
|
|
||||||
|
`PaintCallbackPayload`(判别联合,`payload.kind` 区分):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 上色成功
|
||||||
|
{
|
||||||
|
kind: 'painted';
|
||||||
|
regionId: number;
|
||||||
|
regionName: string;
|
||||||
|
color: BgrColor;
|
||||||
|
configJson?: Record<string, unknown>; // setPaintColor / initialPaintConfigJson 传入
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未选笔刷时点击有效分区(不会上色)
|
||||||
|
{
|
||||||
|
kind: 'brush_required';
|
||||||
|
hint: string; // 如「请先选择笔刷颜色(底部色条或 ref.setPaintColor)」
|
||||||
|
regionId: number;
|
||||||
|
regionName: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
onPaintCallback={payload => {
|
||||||
|
if (payload.kind === 'brush_required') {
|
||||||
|
showToast(payload.hint);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
savePaintRecord(payload.regionId, payload.color, payload.configJson);
|
||||||
|
}}
|
||||||
|
```
|
||||||
|
|
||||||
|
`onWatch` 的 `detail`(`MaskSegmentWatchDetail`):
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| ---- | ---- | ---- |
|
||||||
|
| `regionCount` | `number` | 当前有效分区数 |
|
||||||
|
| `maskPathsReady` | `boolean` | 轮廓 Skia 路径是否就绪 |
|
||||||
|
| `freqLayersReady` | `boolean` | 高低频 Shader 纹理是否就绪 |
|
||||||
|
| `errorMessage` | `string` | `error` 状态下的失败说明 |
|
||||||
|
|
||||||
|
#### onWatch 状态流转
|
||||||
|
|
||||||
|
```
|
||||||
|
init
|
||||||
|
→ images_loaded 原图 + 掩码读取完成
|
||||||
|
→ mask_aligned 掩码尺寸对齐完成
|
||||||
|
→ mask_sampled 掩码像素采样完成
|
||||||
|
→ regions_ready 分区提取成功
|
||||||
|
→ layers_ready 上色纹理图层就绪(detail.maskPathsReady 可能仍为 false)
|
||||||
|
→ interactive 可交互(可点击选区、选色、上色)
|
||||||
|
→ mask_paths_ready 轮廓路径就绪(初始化轮播虚线可显示,detail.maskPathsReady 为 true)
|
||||||
|
→ error 失败(detail.errorMessage 有说明)
|
||||||
|
```
|
||||||
|
|
||||||
|
`layers_ready` / `interactive` 可能在轮廓路径算完之前触发;业务侧若以 `interactive` 关闭 Loading,用户即可操作,轮播虚线会在 `mask_paths_ready` 后自动出现。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ref 方法
|
||||||
|
|
||||||
|
通过 `ref` 调用(类型 `MaskSegmentCanvasRef`):
|
||||||
|
|
||||||
|
| 方法 | 签名 | 说明 |
|
||||||
|
| ---- | ---- | ---- |
|
||||||
|
| `reset` | `() => void` | 撤销上一步上色(按 `paintHistory`) |
|
||||||
|
| `swap` | `(showOrigin?: boolean) => void` | 对比原图;不传参 toggle,传 `true`/`false` 显式开关 |
|
||||||
|
| `save` | `(options?) => Promise<SavePaintResult>` | 合成并保存 PNG;`options.destDir` 可选输出目录 |
|
||||||
|
| `session` | `() => MaskSegmentSession` | 导出可 JSON 序列化会话(存 MMKV) |
|
||||||
|
| `loadSession` | `(session) => void` | 恢复上色状态(也可通过 `initialSession`) |
|
||||||
|
| `setPaintColor` | `(color, configJson?) => void` | 设置当前笔刷色,清空底部色条选中 |
|
||||||
|
| `setMaskConfig` | `(config) => void` | 运行时更新掩码配置并**重新分割** |
|
||||||
|
| `clearAllPaint` | `() => void` | 清空全部上色记录 |
|
||||||
|
| `resegment` | `() => Promise<void>` | 清空 PNG 缓存并重新分割 |
|
||||||
|
| `getRegions` | `() => SegmentRegion[]` | 当前分区列表快照 |
|
||||||
|
| `getPaintedRegions` | `() => PaintedRegionRecord[]` | 当前上色记录快照 |
|
||||||
|
|
||||||
|
`SavePaintResult`:`{ filePath, width, height, paintedCount, previewPath? }`
|
||||||
|
|
||||||
|
代码示例:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const ref = useRef<MaskSegmentCanvasRef>(null);
|
||||||
|
|
||||||
|
ref.current?.reset();
|
||||||
|
ref.current?.swap(); // toggle
|
||||||
|
ref.current?.swap(true); // 强制显示原图
|
||||||
|
|
||||||
|
const result = await ref.current?.save({ destDir: '/path/to/dir' });
|
||||||
|
|
||||||
|
const session = ref.current?.session();
|
||||||
|
ref.current?.loadSession(session);
|
||||||
|
|
||||||
|
ref.current?.setPaintColor({ b: 100, g: 120, r: 140 }, { sku: 'paint-001' });
|
||||||
|
ref.current?.setMaskConfig({ semanticColors: customColors });
|
||||||
|
|
||||||
|
ref.current?.clearAllPaint();
|
||||||
|
await ref.current?.resegment();
|
||||||
|
|
||||||
|
const regions = ref.current?.getRegions();
|
||||||
|
const painted = ref.current?.getPaintedRegions();
|
||||||
|
```
|
||||||
|
|
||||||
|
> `save` 依赖工作区 buffer 与 pickMap 就绪(通常 `interactive` 之后);未就绪时 throw `'图像尚未就绪,无法保存'`。
|
||||||
|
|
||||||
|
### 存储约定
|
||||||
|
|
||||||
|
|
||||||
|
| 能力 | 建议存储 | 内容 |
|
||||||
|
| --------------- | ------------------- | ----------------------- |
|
||||||
|
| `ref.save()` | 文件系统 | 大图 PNG 路径 |
|
||||||
|
| `ref.session()` | MMKV / AsyncStorage | JSON 元数据(URL、上色记录、笔刷色等) |
|
||||||
|
|
||||||
|
|
||||||
|
`MaskSegmentSession` 结构:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
version: 1;
|
||||||
|
originUrl: string;
|
||||||
|
maskUrl: string;
|
||||||
|
painted: PaintedRegionRecord[]; // { regionId, regionName, color, configJson? }
|
||||||
|
paintHistory: number[];
|
||||||
|
currentColor?: BgrColor;
|
||||||
|
currentColorConfigJson?: Record<string, unknown>;
|
||||||
|
savedAt: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 交互说明
|
||||||
|
|
||||||
|
1. **初始化轮播**:分区就绪后,按 `initRegionFlashMs`(默认 1s)轮播各分区虚线轮廓;用户触摸画布后停止。
|
||||||
|
2. **预览(未选笔刷)**:长按分区显示当前触摸点所在连通块的虚线轮廓;点击黑色区域不显示虚线。
|
||||||
|
3. **上色(已选笔刷)**:先点底部色条或 `ref.setPaintColor`(也可通过 `initialPaintColor` 预设),再点击分区上色;同一分区重复点击会覆盖颜色。
|
||||||
|
4. **未选笔刷点击分区**:不会上色;`onPaintCallback` 以 `kind: 'brush_required'` 回调,携带 `hint` 与目标分区信息,由业务侧 Toast / 弹窗提示用户选色。
|
||||||
|
5. **撤销**:左下角按钮或 `ref.reset()`,按上色历史逐步撤回。
|
||||||
|
6. **对比原图**:右下角按钮或 `ref.swap()`,隐藏上色层查看原图。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 接入业务示例
|
||||||
|
|
||||||
|
### 下载后预热再挂载(推荐)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { prewarmPngBgrCacheAsync } from 'react-native-mask-segment-canvas';
|
||||||
|
|
||||||
|
async function openPaintScreen(originUrl: string, maskUrl: string) {
|
||||||
|
await prewarmPngBgrCacheAsync([originUrl, maskUrl]);
|
||||||
|
navigation.navigate('Paint', { originUrl, maskUrl });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 接口下载后传入本地路径
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<MaskSegmentCanvas
|
||||||
|
originUrl={localOriginPath}
|
||||||
|
maskUrl={localMaskPath}
|
||||||
|
showDebugPickers={false}
|
||||||
|
showToolbar={false}
|
||||||
|
semanticColors={MASK_SEMANTIC_COLORS}
|
||||||
|
regionOutlineColor="#1e96ff"
|
||||||
|
onWatch={(state, ms, detail) => {
|
||||||
|
if (state === 'interactive') hideBlockingLoader();
|
||||||
|
if (state === 'mask_paths_ready') hideOutlineHint();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 草稿恢复
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const draft = JSON.parse(mmkv.getString('paint_draft'));
|
||||||
|
|
||||||
|
<MaskSegmentCanvas
|
||||||
|
originUrl={draft.originUrl}
|
||||||
|
maskUrl={draft.maskUrl}
|
||||||
|
initialSession={draft}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义语义色表
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const gymColors: MaskSemanticColor[] = [
|
||||||
|
{ name: 'wall', hex: '#4363D8', bgr: { b: 216, g: 99, r: 67 } },
|
||||||
|
{ name: 'ceiling', hex: '#3CB44B', bgr: { b: 75, g: 180, r: 60 } },
|
||||||
|
// ...
|
||||||
|
];
|
||||||
|
|
||||||
|
<MaskSegmentCanvas
|
||||||
|
semanticColors={gymColors}
|
||||||
|
maskConfig={{ blackThreshold: 30, maxRegionColors: 6 }}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
MaskSegmentApp/ # 仓库根目录(npm 包 react-native-mask-segment-canvas)
|
||||||
|
├── App.tsx # 库自测 Demo(直接引用 ./src)
|
||||||
|
├── src/
|
||||||
|
│ ├── index.ts # npm 包入口(业务 import 'react-native-mask-segment-canvas')
|
||||||
|
│ ├── components/
|
||||||
|
│ │ ├── MaskSegmentCanvas.tsx
|
||||||
|
│ │ └── MaskSegmentCanvas.types.ts
|
||||||
|
│ └── utils/
|
||||||
|
│ ├── maskSegmentation.ts
|
||||||
|
│ ├── maskSegmentRuntime.ts
|
||||||
|
│ ├── maskSemanticPalette.ts
|
||||||
|
│ └── ...
|
||||||
|
├── example/ # ★ 推荐:业务集成演示(消费者视角)
|
||||||
|
│ ├── App.tsx # 只使用公开 API 的完整示例页面
|
||||||
|
│ ├── index.js / app.json
|
||||||
|
│ ├── package.json # 展示所需依赖 + "react-native-mask-segment-canvas": "file:.."
|
||||||
|
│ ├── metro.config.js / babel.config.js / tsconfig.json
|
||||||
|
│ └── README.md # 如何在真实项目中接入的说明
|
||||||
|
├── patches/ # 随包发布,宿主 postinstall 应用
|
||||||
|
├── ios/ # 根 Demo 原生工程(不发布到 npm)
|
||||||
|
└── android/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 依赖说明
|
||||||
|
|
||||||
|
|
||||||
|
| 包 | 用途 |
|
||||||
|
| -------------------------------- | ---------------------------- |
|
||||||
|
| `@shopify/react-native-skia` | Canvas 渲染、Path、虚线描边、Blend 混合 |
|
||||||
|
| `react-native-fast-opencv` | 掩码形态学、轮廓处理 |
|
||||||
|
| `react-native-fs` | 图层缓存、保存 PNG |
|
||||||
|
| `react-native-image-picker` | Demo 相册选图 |
|
||||||
|
| `react-native-reanimated` | Skia 动画依赖 |
|
||||||
|
| `react-native-safe-area-context` | 安全区适配 |
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 性能评测
|
||||||
|
|
||||||
|
以下数据基于 Demo 测试图(`assets/test/origin.png` **1080×1920**、6 个语义分区)、**默认 `pipelineConfig`**,以及 `onWatch` 的 `durationMs`(从 `init` 起算)。为**经验区间**,非严格基准测试;真机因 CPU、存储、RN 版本会有波动。
|
||||||
|
|
||||||
|
### 实测参考(开发环境 + PNG 预热)
|
||||||
|
|
||||||
|
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**,轮播虚线可显示 |
|
||||||
|
|
||||||
|
`interactive` **不等待**轮廓路径;`mask_paths_ready` 仅影响初始化轮播虚线与可选 UI 提示。
|
||||||
|
|
||||||
|
同图各子步骤(`__DEV__` 日志,默认 pipeline)量级:
|
||||||
|
|
||||||
|
| 子步骤 | 约耗时 | 工作分辨率 |
|
||||||
|
| ------ | ------ | ---------- |
|
||||||
|
| OpenCV LAB 高低频 | ~10–40ms | 270×480 |
|
||||||
|
| 高低频 Skia 纹理 | ~20–30ms | 同上 |
|
||||||
|
| 布局扫描 + 踢脚线 + 点击查表 | ~90–120ms | 405×720(1080p 缩至 longSide 720) |
|
||||||
|
| 全量轮廓路径(异步,不阻塞交互) | ~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`) |
|
||||||
|
|
||||||
|
### `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+** |
|
||||||
|
|
||||||
|
> **300ms 内可交互**:在 1080p + 预热 + 默认 pipeline + 中高端机上**接近但偏乐观**;不宜作为全机型 SLA。
|
||||||
|
|
||||||
|
### 机型档位(1080p,默认 pipeline)
|
||||||
|
|
||||||
|
相对上述开发环境 ~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+ |
|
||||||
|
|
||||||
|
Android 额外开销主要来自:JS ↔ OpenCV bridge、内存带宽/GC、Skia 纹理上传。
|
||||||
|
|
||||||
|
### 提高 `maxImageLongSide` 的影响
|
||||||
|
|
||||||
|
若将 `pipelineConfig.maxImageLongSide` 设为 **1280**(高于默认 720),分割工作区约 **720×1280**,像素约为 720 档的 **3×**:
|
||||||
|
|
||||||
|
| 场景 | 默认 720 | 改为 1280 |
|
||||||
|
| ---- | -------- | --------- |
|
||||||
|
| 1080p `interactive`(中端机) | ~320–800ms | **500ms–1s+** |
|
||||||
|
| 分割 / pickMap 耗时 | ~90–120ms | ~250–350ms |
|
||||||
|
|
||||||
|
更高精度换更长初始化;若目标仍是 **<500ms 可交互**,建议维持默认 **720**,必要时降至 **640**。
|
||||||
|
|
||||||
|
### 优化建议
|
||||||
|
|
||||||
|
1. **PNG 预热(推荐)**:下载或解压完成后、进入画面前调用 `prewarmPngBgrCacheAsync`,通常可省 **100–250ms**(低端机收益最大)。
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { prewarmPngBgrCacheAsync } from 'react-native-mask-segment-canvas';
|
||||||
|
|
||||||
|
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`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 掩码图应为与原图同尺寸的语义色块图(黑底 + 纯色分区);黑色区域(`blackThreshold` 默认 30 以下)不参与分区。
|
||||||
|
- OpenCV 分割在 JS 线程执行,超大图可能卡顿;可通过 `pipelineConfig.maxImageLongSide` 限制处理尺寸。
|
||||||
|
- iOS 相册选图需相册权限(仅 `showDebugPickers` 开启时用到)。
|
||||||
|
- `semanticColors` 须与后端/标注掩码的语义色保持一致,否则识别会偏移。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
**iOS pod install 失败**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ios
|
||||||
|
bundle install
|
||||||
|
bundle exec pod install --repo-update
|
||||||
|
```
|
||||||
|
|
||||||
|
**Android 编译报错**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd android && ./gradlew clean && cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
**分割失败 / 分区为空**
|
||||||
|
|
||||||
|
- 确认 `originUrl` / `maskUrl` 可访问
|
||||||
|
- 确认掩码语义色与 `semanticColors` 配置一致
|
||||||
|
- 查看 Metro 日志中的 `[MaskSegment]` / `[⏱ ...]` 输出
|
||||||
|
|
||||||
|
**虚线不贴边 / 出现多余轮廓**
|
||||||
|
|
||||||
|
- 虚线基于掩码像素外轮廓生成,长按仅显示触摸点所在连通块
|
||||||
|
- 初始化轮播仅显示该语义分区最大连通块
|
||||||
|
|
||||||
- [React Native Website](https://reactnative.dev) - learn more about React Native.
|
|
||||||
- [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment.
|
|
||||||
- [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**.
|
|
||||||
- [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts.
|
|
||||||
- [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native.
|
|
||||||
|
|||||||
@ -2,12 +2,80 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import type { MaskSegmentSession } from '../src/components/MaskSegmentCanvas.types';
|
||||||
import ReactTestRenderer from 'react-test-renderer';
|
import { createRuntimeConfig } from '../src/utils/maskSegmentRuntime';
|
||||||
import App from '../App';
|
|
||||||
|
|
||||||
test('renders correctly', async () => {
|
jest.mock('react-native', () => ({
|
||||||
await ReactTestRenderer.act(() => {
|
View: 'View',
|
||||||
ReactTestRenderer.create(<App />);
|
Text: 'Text',
|
||||||
|
StyleSheet: { create: (s: object) => s, hairlineWidth: 1 },
|
||||||
|
ActivityIndicator: 'ActivityIndicator',
|
||||||
|
StatusBar: 'StatusIndicator',
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('react-native-safe-area-context', () => ({
|
||||||
|
SafeAreaProvider: ({ children }: { children: unknown }) => children,
|
||||||
|
SafeAreaView: ({ children }: { children: unknown }) => children,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../src/components/MaskSegmentCanvas', () => {
|
||||||
|
const React = require('react');
|
||||||
|
const { forwardRef } = React;
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
default: forwardRef(() => null),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('../src/utils/pngImage', () => ({
|
||||||
|
prewarmPngBgrCacheAsync: jest.fn(() => Promise.resolve()),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../src/utils/resolveAssetPath', () => ({
|
||||||
|
resolveAssetPath: jest.fn((_asset: number, name: string) =>
|
||||||
|
Promise.resolve(`file:///mock/${name}`),
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
test('session round-trip shape', () => {
|
||||||
|
const session: MaskSegmentSession = {
|
||||||
|
version: 1,
|
||||||
|
originUrl: 'file:///mock/origin.png',
|
||||||
|
maskUrl: 'file:///mock/mask.png',
|
||||||
|
painted: [
|
||||||
|
{
|
||||||
|
regionId: 0,
|
||||||
|
regionName: 'wall',
|
||||||
|
color: { b: 100, g: 120, r: 140 },
|
||||||
|
configJson: { sku: 'paint-001' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
paintHistory: [0],
|
||||||
|
currentColor: { b: 100, g: 120, r: 140 },
|
||||||
|
currentColorConfigJson: { sku: 'paint-001' },
|
||||||
|
savedAt: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const restored = JSON.parse(JSON.stringify(session)) as MaskSegmentSession;
|
||||||
|
expect(restored.version).toBe(1);
|
||||||
|
expect(restored.painted).toHaveLength(1);
|
||||||
|
expect(restored.painted[0].configJson).toEqual({ sku: 'paint-001' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runtime config defaults', () => {
|
||||||
|
const runtime = createRuntimeConfig();
|
||||||
|
expect(runtime.pipeline.maxImageLongSide).toBe(720);
|
||||||
|
expect(runtime.paint.palette).toHaveLength(6);
|
||||||
|
expect(runtime.mask.semanticColors.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders app shell', async () => {
|
||||||
|
const React = require('react');
|
||||||
|
const ReactTestRenderer = require('react-test-renderer');
|
||||||
|
const App = require('../App').default;
|
||||||
|
|
||||||
|
await ReactTestRenderer.act(async () => {
|
||||||
|
ReactTestRenderer.create(React.createElement(App));
|
||||||
|
await Promise.resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
79
__tests__/paintShader.test.ts
Normal file
79
__tests__/paintShader.test.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
jest.mock('../src/utils/opencvAdapter', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildPaintColorMapImage,
|
||||||
|
} from '../src/utils/paintColorMapTexture';
|
||||||
|
import {
|
||||||
|
bgrBufferToRgbaBuffer,
|
||||||
|
bgrToLabL,
|
||||||
|
prepareFreqLayersFromBgrBuffer,
|
||||||
|
} from '../src/utils/freqLayerPrep';
|
||||||
|
import { getRegionPaintEffect } from '../src/utils/paintShaderRuntime';
|
||||||
|
|
||||||
|
jest.mock('@shopify/react-native-skia', () => {
|
||||||
|
const makeImage = jest.fn(() => ({
|
||||||
|
dispose: jest.fn(),
|
||||||
|
width: () => 2,
|
||||||
|
height: () => 2,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
Skia: {
|
||||||
|
Data: { fromBytes: jest.fn(() => ({})) },
|
||||||
|
Image: { MakeImage: makeImage },
|
||||||
|
RuntimeEffect: {
|
||||||
|
Make: jest.fn(() => ({
|
||||||
|
getUniformCount: () => 4,
|
||||||
|
getUniformName: (i: number) =>
|
||||||
|
['colorBaseOpacity', 'lLightOpacity', 'textureOpacity', 'showOrigin'][
|
||||||
|
i
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AlphaType: { Opaque: 1, Unpremul: 3 },
|
||||||
|
ColorType: { RGBA_8888: 4 },
|
||||||
|
TileMode: { Clamp: 0 },
|
||||||
|
ImageFormat: { PNG: 4 },
|
||||||
|
Canvas: 'Canvas',
|
||||||
|
Fill: 'Fill',
|
||||||
|
Shader: 'Shader',
|
||||||
|
ImageShader: 'ImageShader',
|
||||||
|
Group: 'Group',
|
||||||
|
drawAsImage: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('bgrToLabL returns clamped 0-255', () => {
|
||||||
|
expect(bgrToLabL(0, 0, 0)).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(bgrToLabL(255, 255, 255)).toBeLessThanOrEqual(255);
|
||||||
|
expect(bgrToLabL(128, 128, 128)).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('bgrBufferToRgbaBuffer swaps BGR to RGBA', () => {
|
||||||
|
const bgr = new Uint8Array([10, 20, 30]);
|
||||||
|
const rgba = bgrBufferToRgbaBuffer(bgr, 1, 1);
|
||||||
|
expect(Array.from(rgba)).toEqual([30, 20, 10, 255]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildPaintColorMapImage marks painted pixels', () => {
|
||||||
|
const pick = new Uint8Array([0, 1, 2]);
|
||||||
|
const painted = new Map([
|
||||||
|
[0, { b: 1, g: 2, r: 3 }],
|
||||||
|
[1, { b: 4, g: 5, r: 6 }],
|
||||||
|
]);
|
||||||
|
const image = buildPaintColorMapImage(pick, 3, 1, painted);
|
||||||
|
expect(image).toBeTruthy();
|
||||||
|
expect(require('@shopify/react-native-skia').Skia.Image.MakeImage).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('prepareFreqLayersFromBgrBuffer rejects invalid buffer size', async () => {
|
||||||
|
await expect(prepareFreqLayersFromBgrBuffer(new Uint8Array(2), 1, 1)).resolves.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('regionPaint SkSL compiles via RuntimeEffect', () => {
|
||||||
|
const effect = getRegionPaintEffect();
|
||||||
|
expect(effect.getUniformCount()).toBe(4);
|
||||||
|
});
|
||||||
@ -1,6 +1,8 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".MainApplication"
|
android:name=".MainApplication"
|
||||||
|
|||||||
BIN
assets/test/mask.png
Normal file
BIN
assets/test/mask.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 695 KiB |
BIN
assets/test/origin.png
Normal file
BIN
assets/test/origin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@ -1,3 +1,4 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: ['module:@react-native/babel-preset'],
|
presets: ['module:@react-native/babel-preset'],
|
||||||
|
plugins: ['react-native-reanimated/plugin'],
|
||||||
};
|
};
|
||||||
|
|||||||
6
dist/components/MaskSegmentCanvas.d.ts
vendored
Normal file
6
dist/components/MaskSegmentCanvas.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { MaskSegmentCanvasProps, MaskSegmentCanvasRef } from './MaskSegmentCanvas.types';
|
||||||
|
export type { MaskSegmentCanvasProps, MaskSegmentCanvasRef, MaskSegmentSession, MaskSegmentWatchState, PaintedRegionRecord, BgrColor, MaskSemanticColor, } from './MaskSegmentCanvas.types';
|
||||||
|
declare const MaskSegmentCanvas: React.ForwardRefExoticComponent<MaskSegmentCanvasProps & React.RefAttributes<MaskSegmentCanvasRef>>;
|
||||||
|
export default MaskSegmentCanvas;
|
||||||
|
//# sourceMappingURL=MaskSegmentCanvas.d.ts.map
|
||||||
1
dist/components/MaskSegmentCanvas.d.ts.map
vendored
Normal file
1
dist/components/MaskSegmentCanvas.d.ts.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"MaskSegmentCanvas.d.ts","sourceRoot":"","sources":["../../src/components/MaskSegmentCanvas.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AAmDf,OAAO,KAAK,EAEV,sBAAsB,EACtB,oBAAoB,EAMrB,MAAM,2BAA2B,CAAC;AAcnC,YAAY,EACV,sBAAsB,EACtB,oBAAoB,EACpB,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,QAAQ,EACR,iBAAiB,GAClB,MAAM,2BAA2B,CAAC;AA4ZnC,QAAA,MAAM,iBAAiB,qGA2pErB,CAAC;AA8HH,eAAe,iBAAiB,CAAC"}
|
||||||
2012
dist/components/MaskSegmentCanvas.js
vendored
Normal file
2012
dist/components/MaskSegmentCanvas.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
dist/components/MaskSegmentCanvas.js.map
vendored
Normal file
1
dist/components/MaskSegmentCanvas.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
189
dist/components/MaskSegmentCanvas.types.d.ts
vendored
Normal file
189
dist/components/MaskSegmentCanvas.types.d.ts
vendored
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
|
||||||
|
import type { SegmentRegion } from '../utils/maskSegmentation';
|
||||||
|
import type { MaskSemanticColor } from '../utils/maskSemanticPalette';
|
||||||
|
export type BgrColor = {
|
||||||
|
b: number;
|
||||||
|
g: number;
|
||||||
|
r: number;
|
||||||
|
};
|
||||||
|
export type MaskSegmentWatchState = 'init' | 'images_loaded' | 'mask_aligned' | 'mask_sampled' | 'regions_ready' | 'layers_ready' | 'interactive' | 'mask_paths_ready' | 'error';
|
||||||
|
export type MaskSegmentWatchDetail = {
|
||||||
|
regionCount?: number;
|
||||||
|
maskPathsReady?: boolean;
|
||||||
|
freqLayersReady?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
};
|
||||||
|
export type PipelinePreset = 'high' | 'medium' | 'low';
|
||||||
|
export type PipelineConfig = {
|
||||||
|
maxImageLongSide?: number;
|
||||||
|
/** 高低频 LAB 处理最长边(可低于 maxImageLongSide,Shader 拉伸采样) */
|
||||||
|
paintFreqMaxLongSide?: number;
|
||||||
|
originPreviewMaxLongSide?: number;
|
||||||
|
maskPathMaxLongSide?: number;
|
||||||
|
minContourArea?: number;
|
||||||
|
contourApproxEpsilon?: number;
|
||||||
|
maxRegions?: number;
|
||||||
|
};
|
||||||
|
export type MaskSegmentConfig = {
|
||||||
|
semanticColors?: MaskSemanticColor[];
|
||||||
|
baseboardMaxColorDist?: number;
|
||||||
|
blackThreshold?: number;
|
||||||
|
quantStep?: number;
|
||||||
|
baseboardStripQuantKeys?: string[];
|
||||||
|
wallQuantKeys?: string[];
|
||||||
|
cabinetQuantKeys?: string[];
|
||||||
|
maxRegionColors?: number;
|
||||||
|
secondarySemanticNames?: string[];
|
||||||
|
secondaryMinPixelRatio?: number;
|
||||||
|
junctionHRadiusPx?: number;
|
||||||
|
junctionVRadiusPx?: number;
|
||||||
|
kickBridgeHalfWPx?: number;
|
||||||
|
baseboardJunctionRowMarginPx?: number;
|
||||||
|
baseboardJunctionVReachPx?: number;
|
||||||
|
baseboardMinRunPx?: number;
|
||||||
|
};
|
||||||
|
export type PaintConfig = {
|
||||||
|
palette?: BgrColor[];
|
||||||
|
colorBaseOpacity?: number;
|
||||||
|
lLightOpacity?: number;
|
||||||
|
textureOpacity?: number;
|
||||||
|
lLowBlurKernel?: number;
|
||||||
|
lLowContrast?: number;
|
||||||
|
lLowBrightness?: number;
|
||||||
|
lHighGain?: number;
|
||||||
|
maskFeatherColor?: number;
|
||||||
|
maskFeatherTexture?: number;
|
||||||
|
regionOverlayFill?: string;
|
||||||
|
regionOutlineStrokeWidth?: number;
|
||||||
|
};
|
||||||
|
export type InteractionConfig = {
|
||||||
|
kickMaskPickRadiusPx?: number;
|
||||||
|
pickMapSearchRadiusPx?: number;
|
||||||
|
thinStripPadding?: number;
|
||||||
|
regionPadding?: number;
|
||||||
|
initRegionFlashMs?: number;
|
||||||
|
enableInitRegionFlash?: boolean;
|
||||||
|
};
|
||||||
|
export type PaintedRegionRecord = {
|
||||||
|
regionId: number;
|
||||||
|
regionName: string;
|
||||||
|
color: BgrColor;
|
||||||
|
configJson?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
export type MaskSegmentSession = {
|
||||||
|
version: 1;
|
||||||
|
originUrl: string;
|
||||||
|
maskUrl: string;
|
||||||
|
painted: PaintedRegionRecord[];
|
||||||
|
paintHistory: number[];
|
||||||
|
currentColor?: BgrColor;
|
||||||
|
currentColorConfigJson?: Record<string, unknown>;
|
||||||
|
savedAt: number;
|
||||||
|
};
|
||||||
|
export type SavePaintResult = {
|
||||||
|
filePath: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
paintedCount: number;
|
||||||
|
previewPath?: string;
|
||||||
|
};
|
||||||
|
export type SavePaintOptions = {
|
||||||
|
destDir?: string;
|
||||||
|
};
|
||||||
|
export type PaintSuccessPayload = {
|
||||||
|
kind: 'painted';
|
||||||
|
regionId: number;
|
||||||
|
regionName: string;
|
||||||
|
color: BgrColor;
|
||||||
|
configJson?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
export type PaintBrushRequiredPayload = {
|
||||||
|
kind: 'brush_required';
|
||||||
|
/** 未选笔刷时的提示文案 */
|
||||||
|
hint: string;
|
||||||
|
regionId: number;
|
||||||
|
regionName: string;
|
||||||
|
};
|
||||||
|
export type PaintCallbackPayload = PaintSuccessPayload | PaintBrushRequiredPayload;
|
||||||
|
export type OverlayButtonRenderProps = {
|
||||||
|
onPress: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
export type MaskSegmentCanvasRef = {
|
||||||
|
reset: () => void;
|
||||||
|
swap: (showOrigin?: boolean) => void;
|
||||||
|
save: (options?: SavePaintOptions) => Promise<SavePaintResult>;
|
||||||
|
session: () => MaskSegmentSession;
|
||||||
|
loadSession: (session: MaskSegmentSession) => void;
|
||||||
|
setPaintColor: (color: BgrColor, configJson?: Record<string, unknown>) => void;
|
||||||
|
setMaskConfig: (config: MaskSegmentConfig) => void;
|
||||||
|
clearAllPaint: () => void;
|
||||||
|
/** Undo the most recent single coloring (paint) step. Distinct from clearAllPaint (full reset). */
|
||||||
|
undoSelection?: () => void;
|
||||||
|
resegment: () => Promise<void>;
|
||||||
|
getRegions: () => SegmentRegion[];
|
||||||
|
getPaintedRegions: () => PaintedRegionRecord[];
|
||||||
|
/** Returns the most recent auto-export or save() result, if any. */
|
||||||
|
getLastExport?: () => SavePaintResult | null;
|
||||||
|
};
|
||||||
|
export type MaskSegmentCanvasProps = {
|
||||||
|
originUrl?: string;
|
||||||
|
maskUrl?: string;
|
||||||
|
/** @deprecated 使用 originUrl */
|
||||||
|
originImgPath?: string;
|
||||||
|
/** @deprecated 使用 maskUrl */
|
||||||
|
maskImgPath?: string;
|
||||||
|
/** 掩码语义识别色,初始化配置;等同 maskConfig.semanticColors */
|
||||||
|
semanticColors?: MaskSemanticColor[];
|
||||||
|
/** 分区虚线高亮色,初始化配置;等同 paintConfig.regionOverlayFill */
|
||||||
|
regionOutlineColor?: string;
|
||||||
|
maskConfig?: MaskSegmentConfig;
|
||||||
|
/** Performance preset (high / medium / low). Merged with pipelineConfig overrides. */
|
||||||
|
pipelinePreset?: PipelinePreset;
|
||||||
|
pipelineConfig?: PipelineConfig;
|
||||||
|
paintConfig?: PaintConfig;
|
||||||
|
interactionConfig?: InteractionConfig;
|
||||||
|
initialSession?: MaskSegmentSession;
|
||||||
|
initialPaintColor?: BgrColor;
|
||||||
|
initialPaintConfigJson?: Record<string, unknown>;
|
||||||
|
showDebugPickers?: boolean;
|
||||||
|
showToolbar?: boolean;
|
||||||
|
showColorBar?: boolean;
|
||||||
|
showStatusRow?: boolean;
|
||||||
|
showOverlayButtons?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
|
canvasStyle?: StyleProp<ViewStyle>;
|
||||||
|
/**
|
||||||
|
* Max container height available for this component (px). When set, the SDK
|
||||||
|
* computes canvas dimensions as a fit-contain within (screenWidth - 20, maxHeight)
|
||||||
|
* instead of using the full image aspect at full screen width. This prevents
|
||||||
|
* internal ScrollView scrolling for tall images.
|
||||||
|
*/
|
||||||
|
maxHeight?: number;
|
||||||
|
undoButtonStyle?: StyleProp<ViewStyle>;
|
||||||
|
compareButtonStyle?: StyleProp<ViewStyle>;
|
||||||
|
undoButtonTextStyle?: StyleProp<TextStyle>;
|
||||||
|
compareButtonTextStyle?: StyleProp<TextStyle>;
|
||||||
|
undoButtonText?: string;
|
||||||
|
compareButtonText?: string;
|
||||||
|
compareExitButtonText?: string;
|
||||||
|
renderUndoButton?: (props: OverlayButtonRenderProps) => ReactNode;
|
||||||
|
renderCompareButton?: (props: OverlayButtonRenderProps) => ReactNode;
|
||||||
|
onWatch?: (state: MaskSegmentWatchState, durationMs: number, detail?: MaskSegmentWatchDetail) => void;
|
||||||
|
onPaintCallback?: (payload: PaintCallbackPayload) => void;
|
||||||
|
onError?: (message: string, error?: unknown) => void;
|
||||||
|
/**
|
||||||
|
* When true, once the canvas reaches a ready interactive state (segmentation complete
|
||||||
|
* + any initialSession / painted colors applied), the SDK will automatically call its
|
||||||
|
* internal save pipeline to produce the recolored result image and fire onExported.
|
||||||
|
* This moves "auto-generate After preview" capability inside the SDK.
|
||||||
|
*/
|
||||||
|
autoExportOnReady?: boolean;
|
||||||
|
/** Fired by SDK when autoExportOnReady produced a result (the recolored file). */
|
||||||
|
onExported?: (result: SavePaintResult) => void;
|
||||||
|
};
|
||||||
|
export type { SegmentRegion, MaskSemanticColor };
|
||||||
|
//# sourceMappingURL=MaskSegmentCanvas.types.d.ts.map
|
||||||
1
dist/components/MaskSegmentCanvas.types.d.ts.map
vendored
Normal file
1
dist/components/MaskSegmentCanvas.types.d.ts.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
dist/components/MaskSegmentCanvas.types.js
vendored
Normal file
2
dist/components/MaskSegmentCanvas.types.js
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export {};
|
||||||
|
//# sourceMappingURL=MaskSegmentCanvas.types.js.map
|
||||||
1
dist/components/MaskSegmentCanvas.types.js.map
vendored
Normal file
1
dist/components/MaskSegmentCanvas.types.js.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"MaskSegmentCanvas.types.js","sourceRoot":"","sources":["../../src/components/MaskSegmentCanvas.types.ts"],"names":[],"mappings":""}
|
||||||
6
dist/index.d.ts
vendored
Normal file
6
dist/index.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export { default } from './components/MaskSegmentCanvas';
|
||||||
|
export type { BgrColor, InteractionConfig, MaskSegmentCanvasProps, MaskSegmentCanvasRef, MaskSegmentConfig, MaskSegmentSession, MaskSegmentWatchDetail, MaskSegmentWatchState, MaskSemanticColor, OverlayButtonRenderProps, PaintBrushRequiredPayload, PaintCallbackPayload, PaintSuccessPayload, PaintConfig, PaintedRegionRecord, PipelineConfig, PipelinePreset, SavePaintOptions, SavePaintResult, SegmentRegion, } from './components/MaskSegmentCanvas.types';
|
||||||
|
export { BASEBOARD_SEMANTIC_NAME, MASK_SEMANTIC_COLORS, } from './utils/maskSemanticPalette';
|
||||||
|
export { createRuntimeConfig, DEFAULT_INTERACTION_CONFIG, DEFAULT_MASK_CONFIG, DEFAULT_PAINT_CONFIG, DEFAULT_PIPELINE_CONFIG, PIPELINE_HIGH, PIPELINE_LOW, PIPELINE_MEDIUM, PIPELINE_PRESETS, getMaskSegmentRuntimeConfig, resolvePipelineConfig, setMaskSegmentRuntimeConfig, } from './utils/maskSegmentRuntime';
|
||||||
|
export { prewarmPngBgrCache, prewarmPngBgrCacheAsync, } from './utils/pngImage';
|
||||||
|
//# sourceMappingURL=index.d.ts.map
|
||||||
1
dist/index.d.ts.map
vendored
Normal file
1
dist/index.d.ts.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AACzD,YAAY,EACV,QAAQ,EACR,iBAAiB,EACjB,sBAAsB,EACtB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,iBAAiB,EACjB,wBAAwB,EACxB,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACnB,WAAW,EACX,mBAAmB,EACnB,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,aAAa,GACd,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EACL,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,mBAAmB,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,EACvB,aAAa,EACb,YAAY,EACZ,eAAe,EACf,gBAAgB,EAChB,2BAA2B,EAC3B,qBAAqB,EACrB,2BAA2B,GAC5B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,kBAAkB,CAAC"}
|
||||||
5
dist/index.js
vendored
Normal file
5
dist/index.js
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export { default } from './components/MaskSegmentCanvas';
|
||||||
|
export { BASEBOARD_SEMANTIC_NAME, MASK_SEMANTIC_COLORS, } from './utils/maskSemanticPalette';
|
||||||
|
export { createRuntimeConfig, DEFAULT_INTERACTION_CONFIG, DEFAULT_MASK_CONFIG, DEFAULT_PAINT_CONFIG, DEFAULT_PIPELINE_CONFIG, PIPELINE_HIGH, PIPELINE_LOW, PIPELINE_MEDIUM, PIPELINE_PRESETS, getMaskSegmentRuntimeConfig, resolvePipelineConfig, setMaskSegmentRuntimeConfig, } from './utils/maskSegmentRuntime';
|
||||||
|
export { prewarmPngBgrCache, prewarmPngBgrCacheAsync, } from './utils/pngImage';
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
1
dist/index.js.map
vendored
Normal file
1
dist/index.js.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAuBzD,OAAO,EACL,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,mBAAmB,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,EACvB,aAAa,EACb,YAAY,EACZ,eAAe,EACf,gBAAgB,EAChB,2BAA2B,EAC3B,qBAAqB,EACrB,2BAA2B,GAC5B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,kBAAkB,CAAC"}
|
||||||
3
dist/shaders/regionPaint.sksl.d.ts
vendored
Normal file
3
dist/shaders/regionPaint.sksl.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/** SkSL:分区上色(保留原图明暗与纹理,按 paintColorMap 叠色) */
|
||||||
|
export declare const REGION_PAINT_SKSL = "\nuniform shader originTex;\nuniform shader paintColorTex;\nuniform shader lowFreqTex;\nuniform shader highFreqTex;\n\nuniform float colorBaseOpacity;\nuniform float lLightOpacity;\nuniform float textureOpacity;\nuniform float showOrigin;\n\nfloat luminance(half3 c) {\n return dot(c, half3(0.2126, 0.7152, 0.0722));\n}\n\nhalf3 setLuminance(float lum, half3 base) {\n float diff = lum - luminance(base);\n return base + diff;\n}\n\nhalf3 luminosityBlend(half3 base, half3 blend) {\n return setLuminance(luminance(blend), base);\n}\n\nhalf3 overlayBlend(half3 base, half3 blend) {\n half3 low = 2.0 * base * blend;\n half3 high = 1.0 - 2.0 * (1.0 - base) * (1.0 - blend);\n return mix(low, high, step(half3(0.5), base));\n}\n\nhalf4 main(float2 coord) {\n half4 origin = originTex.eval(coord);\n if (showOrigin > 0.5) {\n return origin;\n }\n\n half4 paintEntry = paintColorTex.eval(coord);\n // The paintColorMap uses Unpremul alpha: painted pixels \u2192 (R,G,B,255),\n // transparent pixels \u2192 (0,0,0,0). GPU bilinear sampling interpolates\n // straight-alpha values, so at boundaries rgb = trueColor * sampled.a\n // (contaminated with black from the transparent neighbour).\n //\n // Unpremultiply to recover the true paint color:\n // trueColor = sampled.rgb / sampled.a\n // This eliminates dark fringing at region boundaries.\n float pa = paintEntry.a + 0.0001;\n paintEntry.rgb /= pa;\n\n // Gate sub-pixel alpha to kill residual sampling noise.\n // Thresholds are deliberately low (\u22481.3\u20133.8 in byte space) because\n // post-unpremul the RGB is correct at any alpha \u2265 0 \u2014 we only need\n // to suppress samples that contribute negligibly to the final blend.\n // Using *= preserves the smooth edge; higher-alpha samples pass through.\n paintEntry.a *= smoothstep(0.005, 0.015, paintEntry.a);\n\n half3 paintRgb = paintEntry.rgb;\n half lowL = lowFreqTex.eval(coord).r;\n half highL = highFreqTex.eval(coord).r;\n\n half3 base = paintRgb * colorBaseOpacity;\n half3 lit = luminosityBlend(base, half3(lowL));\n half3 withLight = mix(base, lit, lLightOpacity);\n half3 tex = overlayBlend(withLight, half3(highL));\n half3 finalRgb = mix(withLight, tex, textureOpacity);\n\n // Soft edge blend using the (feathered) alpha from the paint color map as coverage.\n half3 blended = mix(origin.rgb, finalRgb, paintEntry.a);\n return half4(blended, 1.0);\n}\n";
|
||||||
|
//# sourceMappingURL=regionPaint.sksl.d.ts.map
|
||||||
1
dist/shaders/regionPaint.sksl.d.ts.map
vendored
Normal file
1
dist/shaders/regionPaint.sksl.d.ts.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"regionPaint.sksl.d.ts","sourceRoot":"","sources":["../../src/shaders/regionPaint.sksl.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAC9C,eAAO,MAAM,iBAAiB,83EAqE7B,CAAC"}
|
||||||
72
dist/shaders/regionPaint.sksl.js
vendored
Normal file
72
dist/shaders/regionPaint.sksl.js
vendored
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/** SkSL:分区上色(保留原图明暗与纹理,按 paintColorMap 叠色) */
|
||||||
|
export const REGION_PAINT_SKSL = `
|
||||||
|
uniform shader originTex;
|
||||||
|
uniform shader paintColorTex;
|
||||||
|
uniform shader lowFreqTex;
|
||||||
|
uniform shader highFreqTex;
|
||||||
|
|
||||||
|
uniform float colorBaseOpacity;
|
||||||
|
uniform float lLightOpacity;
|
||||||
|
uniform float textureOpacity;
|
||||||
|
uniform float showOrigin;
|
||||||
|
|
||||||
|
float luminance(half3 c) {
|
||||||
|
return dot(c, half3(0.2126, 0.7152, 0.0722));
|
||||||
|
}
|
||||||
|
|
||||||
|
half3 setLuminance(float lum, half3 base) {
|
||||||
|
float diff = lum - luminance(base);
|
||||||
|
return base + diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
half3 luminosityBlend(half3 base, half3 blend) {
|
||||||
|
return setLuminance(luminance(blend), base);
|
||||||
|
}
|
||||||
|
|
||||||
|
half3 overlayBlend(half3 base, half3 blend) {
|
||||||
|
half3 low = 2.0 * base * blend;
|
||||||
|
half3 high = 1.0 - 2.0 * (1.0 - base) * (1.0 - blend);
|
||||||
|
return mix(low, high, step(half3(0.5), base));
|
||||||
|
}
|
||||||
|
|
||||||
|
half4 main(float2 coord) {
|
||||||
|
half4 origin = originTex.eval(coord);
|
||||||
|
if (showOrigin > 0.5) {
|
||||||
|
return origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
half4 paintEntry = paintColorTex.eval(coord);
|
||||||
|
// The paintColorMap uses Unpremul alpha: painted pixels → (R,G,B,255),
|
||||||
|
// transparent pixels → (0,0,0,0). GPU bilinear sampling interpolates
|
||||||
|
// straight-alpha values, so at boundaries rgb = trueColor * sampled.a
|
||||||
|
// (contaminated with black from the transparent neighbour).
|
||||||
|
//
|
||||||
|
// Unpremultiply to recover the true paint color:
|
||||||
|
// trueColor = sampled.rgb / sampled.a
|
||||||
|
// This eliminates dark fringing at region boundaries.
|
||||||
|
float pa = paintEntry.a + 0.0001;
|
||||||
|
paintEntry.rgb /= pa;
|
||||||
|
|
||||||
|
// Gate sub-pixel alpha to kill residual sampling noise.
|
||||||
|
// Thresholds are deliberately low (≈1.3–3.8 in byte space) because
|
||||||
|
// post-unpremul the RGB is correct at any alpha ≥ 0 — we only need
|
||||||
|
// to suppress samples that contribute negligibly to the final blend.
|
||||||
|
// Using *= preserves the smooth edge; higher-alpha samples pass through.
|
||||||
|
paintEntry.a *= smoothstep(0.005, 0.015, paintEntry.a);
|
||||||
|
|
||||||
|
half3 paintRgb = paintEntry.rgb;
|
||||||
|
half lowL = lowFreqTex.eval(coord).r;
|
||||||
|
half highL = highFreqTex.eval(coord).r;
|
||||||
|
|
||||||
|
half3 base = paintRgb * colorBaseOpacity;
|
||||||
|
half3 lit = luminosityBlend(base, half3(lowL));
|
||||||
|
half3 withLight = mix(base, lit, lLightOpacity);
|
||||||
|
half3 tex = overlayBlend(withLight, half3(highL));
|
||||||
|
half3 finalRgb = mix(withLight, tex, textureOpacity);
|
||||||
|
|
||||||
|
// Soft edge blend using the (feathered) alpha from the paint color map as coverage.
|
||||||
|
half3 blended = mix(origin.rgb, finalRgb, paintEntry.a);
|
||||||
|
return half4(blended, 1.0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
//# sourceMappingURL=regionPaint.sksl.js.map
|
||||||
1
dist/shaders/regionPaint.sksl.js.map
vendored
Normal file
1
dist/shaders/regionPaint.sksl.js.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"regionPaint.sksl.js","sourceRoot":"","sources":["../../src/shaders/regionPaint.sksl.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAC9C,MAAM,CAAC,MAAM,iBAAiB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqEhC,CAAC"}
|
||||||
44
dist/utils/compositePaintedImage.d.ts
vendored
Normal file
44
dist/utils/compositePaintedImage.d.ts
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import type { BgrColor, SavePaintResult } from '../components/MaskSegmentCanvas.types';
|
||||||
|
import type { SkImage } from '@shopify/react-native-skia';
|
||||||
|
export type CompositePaintInput = {
|
||||||
|
originBuffer: Uint8Array;
|
||||||
|
cols: number;
|
||||||
|
rows: number;
|
||||||
|
pickBuffer: Uint8Array;
|
||||||
|
paintedRegions: Map<number, BgrColor>;
|
||||||
|
destDir?: string;
|
||||||
|
/**
|
||||||
|
* Preferred path for rich export: PNG base64 from makeImageSnapshot() — written
|
||||||
|
* directly to disk without an extra decode/re-encode round trip.
|
||||||
|
*/
|
||||||
|
exportPngBase64?: string;
|
||||||
|
/**
|
||||||
|
* Preferred path for rich export: if the caller (MaskSegmentCanvas) provides bytes
|
||||||
|
* that were produced by makeImageSnapshot() on a high-resolution Canvas rendering the
|
||||||
|
* exact same PaintShaderLayer + regionPaint SkSL at work resolution, we write them
|
||||||
|
* directly. This captures the live editor 质感 (lighting + high/low-freq texture)
|
||||||
|
* without CPU pixel math and without a second declarative drawAsImage.
|
||||||
|
*/
|
||||||
|
exportPngBytes?: Uint8Array;
|
||||||
|
/**
|
||||||
|
* Fallback rich path (when no pre-captured snapshot bytes): pass the live textures
|
||||||
|
* so we can try renderPaintedImageOffscreen (drawAsImage with the shader tree).
|
||||||
|
*/
|
||||||
|
shaderTextures?: {
|
||||||
|
originImage: SkImage;
|
||||||
|
paintColorMap: SkImage;
|
||||||
|
lowFreqImage: SkImage;
|
||||||
|
highFreqImage: SkImage;
|
||||||
|
};
|
||||||
|
/** The logical size at which to render the shader tree for export (typically the work image res). */
|
||||||
|
renderWidth?: number;
|
||||||
|
renderHeight?: number;
|
||||||
|
};
|
||||||
|
/** 将上色区域导出为 recolored PNG。
|
||||||
|
* 优先级(从好到保底):
|
||||||
|
* 1. exportPngBytes(调用方用 makeImageSnapshot 在高分辨率 Canvas 上捕获的完整 shader 结果)—— 推荐的“保存快照”路径,无 CPU 逐像素,无二次 drawAsImage。
|
||||||
|
* 2. shaderTextures + render*(通过 renderPaintedImageOffscreen / drawAsImage 重建同一套 PaintShaderLayer + SkSL)。
|
||||||
|
* 3. CPU 逐像素 recolor(flat,无光照/纹理,仅作最后兜底,保证保存不中断)。
|
||||||
|
*/
|
||||||
|
export declare function compositePaintedImage(input: CompositePaintInput): Promise<SavePaintResult>;
|
||||||
|
//# sourceMappingURL=compositePaintedImage.d.ts.map
|
||||||
1
dist/utils/compositePaintedImage.d.ts.map
vendored
Normal file
1
dist/utils/compositePaintedImage.d.ts.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"compositePaintedImage.d.ts","sourceRoot":"","sources":["../../src/utils/compositePaintedImage.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,uCAAuC,CAAC;AACvF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAM1D,MAAM,MAAM,mBAAmB,GAAG;IAChC,YAAY,EAAE,UAAU,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,UAAU,CAAC;IACvB,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,UAAU,CAAC;IAC5B;;;OAGG;IACH,cAAc,CAAC,EAAE;QACf,WAAW,EAAE,OAAO,CAAC;QACrB,aAAa,EAAE,OAAO,CAAC;QACvB,YAAY,EAAE,OAAO,CAAC;QACtB,aAAa,EAAE,OAAO,CAAC;KACxB,CAAC;IACF,qGAAqG;IACrG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAyCF;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,eAAe,CAAC,CA6G1B"}
|
||||||
146
dist/utils/compositePaintedImage.js
vendored
Normal file
146
dist/utils/compositePaintedImage.js
vendored
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { Buffer } from 'buffer';
|
||||||
|
import RNFS from 'react-native-fs';
|
||||||
|
// upng-js: used for PNG encode of CPU recolor.
|
||||||
|
import UPNG from 'upng-js';
|
||||||
|
import { renderPaintedImageOffscreen } from './paintShaderRuntime';
|
||||||
|
import { writePngBase64ToFile, writePngBytesToFile } from './exportUtils';
|
||||||
|
/**
|
||||||
|
* CPU recolor: directly map pick codes to painted BGR colors (or copy origin).
|
||||||
|
* Produces RGBA PNG bytes via upng-js. This is the *fallback* path when rich shader offscreen
|
||||||
|
* is not available or fails. It produces flat colors without the editor's lighting + freq texture.
|
||||||
|
*/
|
||||||
|
function cpuRecolorToPngBytes(originBgr, pickBuffer, paintedRegions, cols, rows) {
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
const rgba = new Uint8Array(pixelCount * 4);
|
||||||
|
const colorByPickCode = new Map();
|
||||||
|
for (const [regionId, color] of paintedRegions) {
|
||||||
|
colorByPickCode.set(regionId + 1, color);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
const code = pickBuffer[i];
|
||||||
|
const color = code > 0 ? colorByPickCode.get(code) : undefined;
|
||||||
|
const d = i * 4;
|
||||||
|
if (color) {
|
||||||
|
rgba[d] = color.r;
|
||||||
|
rgba[d + 1] = color.g;
|
||||||
|
rgba[d + 2] = color.b;
|
||||||
|
rgba[d + 3] = 255;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const s = i * 3;
|
||||||
|
rgba[d] = originBgr[s + 2]; // RGB <- BGR
|
||||||
|
rgba[d + 1] = originBgr[s + 1];
|
||||||
|
rgba[d + 2] = originBgr[s];
|
||||||
|
rgba[d + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const png = UPNG.encode([rgba.buffer], cols, rows, 0);
|
||||||
|
return new Uint8Array(png);
|
||||||
|
}
|
||||||
|
/** 将上色区域导出为 recolored PNG。
|
||||||
|
* 优先级(从好到保底):
|
||||||
|
* 1. exportPngBytes(调用方用 makeImageSnapshot 在高分辨率 Canvas 上捕获的完整 shader 结果)—— 推荐的“保存快照”路径,无 CPU 逐像素,无二次 drawAsImage。
|
||||||
|
* 2. shaderTextures + render*(通过 renderPaintedImageOffscreen / drawAsImage 重建同一套 PaintShaderLayer + SkSL)。
|
||||||
|
* 3. CPU 逐像素 recolor(flat,无光照/纹理,仅作最后兜底,保证保存不中断)。
|
||||||
|
*/
|
||||||
|
export async function compositePaintedImage(input) {
|
||||||
|
const { originBuffer, cols, rows, pickBuffer, paintedRegions, destDir, exportPngBase64, exportPngBytes, shaderTextures, renderWidth, renderHeight, } = input;
|
||||||
|
if (paintedRegions.size === 0) {
|
||||||
|
throw new Error('No painted regions, cannot save');
|
||||||
|
}
|
||||||
|
if (pickBuffer.length !== cols * rows) {
|
||||||
|
const msg = 'pickMap size does not match image';
|
||||||
|
console.error('[VIZ-SAVE] composite will throw:', msg, { pickLen: pickBuffer.length, expected: cols * rows, cols, rows });
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
if (originBuffer.length !== cols * rows * 3) {
|
||||||
|
const msg = 'Original buffer size does not match image';
|
||||||
|
console.error('[VIZ-SAVE] composite will throw:', msg, { originLen: originBuffer.length, expected: cols * rows * 3, cols, rows });
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
let pngBytesForWrite;
|
||||||
|
let pngBase64ForWrite;
|
||||||
|
let usedSnapshot = false;
|
||||||
|
let usedRichShader = false;
|
||||||
|
// 1) Highest priority: pre-encoded PNG base64 from makeImageSnapshot (no extra conversion).
|
||||||
|
if (exportPngBase64 && exportPngBase64.length > 0) {
|
||||||
|
pngBase64ForWrite = exportPngBase64;
|
||||||
|
usedSnapshot = true;
|
||||||
|
}
|
||||||
|
// 1b) Snapshot bytes fallback (legacy callers).
|
||||||
|
else if (exportPngBytes && exportPngBytes.length > 0) {
|
||||||
|
pngBytesForWrite = exportPngBytes;
|
||||||
|
usedSnapshot = true;
|
||||||
|
}
|
||||||
|
// 2) 回退 rich:用 live 纹理 + drawAsImage 重建与编辑器一致的 shader 结果(带光照 + 频率纹理)。
|
||||||
|
else if (shaderTextures && renderWidth && renderHeight) {
|
||||||
|
try {
|
||||||
|
const offImg = await renderPaintedImageOffscreen({
|
||||||
|
originImage: shaderTextures.originImage,
|
||||||
|
paintColorMap: shaderTextures.paintColorMap,
|
||||||
|
lowFreqImage: shaderTextures.lowFreqImage,
|
||||||
|
highFreqImage: shaderTextures.highFreqImage,
|
||||||
|
width: renderWidth,
|
||||||
|
height: renderHeight,
|
||||||
|
showOrigin: false,
|
||||||
|
});
|
||||||
|
if (offImg) {
|
||||||
|
let b64 = '';
|
||||||
|
try {
|
||||||
|
const enc = offImg.encodeToBase64;
|
||||||
|
if (typeof enc === 'function') {
|
||||||
|
b64 = enc.call(offImg) || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (encErr) {
|
||||||
|
console.warn('[VIZ-SAVE] offscreen encodeToBase64 failed:', encErr);
|
||||||
|
}
|
||||||
|
if (b64 && b64.length > 0) {
|
||||||
|
pngBytesForWrite = new Uint8Array(Buffer.from(b64, 'base64'));
|
||||||
|
usedRichShader = true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const disp = offImg.dispose;
|
||||||
|
if (typeof disp === 'function')
|
||||||
|
disp.call(offImg);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.warn('[VIZ-SAVE] rich shader offscreen for export failed (will fallback):', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 3) 最后兜底:CPU 逐像素(flat 颜色,无 editor 质感)。
|
||||||
|
if (!pngBase64ForWrite && !pngBytesForWrite) {
|
||||||
|
try {
|
||||||
|
pngBytesForWrite = cpuRecolorToPngBytes(originBuffer, pickBuffer, paintedRegions, cols, rows);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
throw new Error('CPU recolor PNG decoding failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const dir = destDir ?? RNFS.CachesDirectoryPath;
|
||||||
|
const filePath = `${dir}/painted_${Date.now()}.png`;
|
||||||
|
try {
|
||||||
|
if (pngBase64ForWrite) {
|
||||||
|
await writePngBase64ToFile(filePath, pngBase64ForWrite);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await writePngBytesToFile(filePath, pngBytesForWrite);
|
||||||
|
}
|
||||||
|
void usedSnapshot;
|
||||||
|
void usedRichShader;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error('[VIZ-SAVE] composite writeFile threw:', e, { filePath, dir });
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
filePath,
|
||||||
|
width: cols,
|
||||||
|
height: rows,
|
||||||
|
paintedCount: paintedRegions.size,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=compositePaintedImage.js.map
|
||||||
1
dist/utils/compositePaintedImage.js.map
vendored
Normal file
1
dist/utils/compositePaintedImage.js.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"compositePaintedImage.js","sourceRoot":"","sources":["../../src/utils/compositePaintedImage.ts"],"names":[],"mappings":"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"}
|
||||||
20
dist/utils/exportUtils.d.ts
vendored
Normal file
20
dist/utils/exportUtils.d.ts
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import type { BgrColor } from '../components/MaskSegmentCanvas.types';
|
||||||
|
/** Stable fingerprint for painted region colors — used to reuse cached exports. */
|
||||||
|
export declare function paintedRegionsFingerprint(painted: Map<number, BgrColor>): string;
|
||||||
|
export declare function writePngBase64ToFile(filePath: string, base64: string): Promise<void>;
|
||||||
|
export declare function writePngBytesToFile(filePath: string, bytes: Uint8Array): Promise<void>;
|
||||||
|
export declare function stripFilePrefix(uri: string): string;
|
||||||
|
export declare function resolveExportResultForDestDir(cached: {
|
||||||
|
filePath: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
paintedCount: number;
|
||||||
|
previewPath?: string;
|
||||||
|
}, destDir?: string): Promise<{
|
||||||
|
filePath: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
paintedCount: number;
|
||||||
|
previewPath?: string;
|
||||||
|
}>;
|
||||||
|
//# sourceMappingURL=exportUtils.d.ts.map
|
||||||
1
dist/utils/exportUtils.d.ts.map
vendored
Normal file
1
dist/utils/exportUtils.d.ts.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"exportUtils.d.ts","sourceRoot":"","sources":["../../src/utils/exportUtils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uCAAuC,CAAC;AAEtE,mFAAmF;AACnF,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG,MAAM,CAMhF;AAED,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1F;AAED,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5F;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAsB,6BAA6B,CACjD,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,EACvG,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAW1G"}
|
||||||
32
dist/utils/exportUtils.js
vendored
Normal file
32
dist/utils/exportUtils.js
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Buffer } from 'buffer';
|
||||||
|
import RNFS from 'react-native-fs';
|
||||||
|
/** Stable fingerprint for painted region colors — used to reuse cached exports. */
|
||||||
|
export function paintedRegionsFingerprint(painted) {
|
||||||
|
if (painted.size === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const entries = [...painted.entries()].sort((a, b) => a[0] - b[0]);
|
||||||
|
return entries.map(([id, c]) => `${id}:${c.r},${c.g},${c.b}`).join('|');
|
||||||
|
}
|
||||||
|
export async function writePngBase64ToFile(filePath, base64) {
|
||||||
|
await RNFS.writeFile(filePath, base64, 'base64');
|
||||||
|
}
|
||||||
|
export async function writePngBytesToFile(filePath, bytes) {
|
||||||
|
await RNFS.writeFile(filePath, Buffer.from(bytes).toString('base64'), 'base64');
|
||||||
|
}
|
||||||
|
export function stripFilePrefix(uri) {
|
||||||
|
return uri.startsWith('file://') ? uri.slice('file://'.length) : uri;
|
||||||
|
}
|
||||||
|
export async function resolveExportResultForDestDir(cached, destDir) {
|
||||||
|
if (!destDir) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
const src = stripFilePrefix(cached.filePath);
|
||||||
|
if (src.startsWith(destDir)) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
const filePath = `${destDir}/painted_${Date.now()}.png`;
|
||||||
|
await RNFS.copyFile(src, filePath);
|
||||||
|
return { ...cached, filePath };
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=exportUtils.js.map
|
||||||
1
dist/utils/exportUtils.js.map
vendored
Normal file
1
dist/utils/exportUtils.js.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"exportUtils.js","sourceRoot":"","sources":["../../src/utils/exportUtils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,IAAI,MAAM,iBAAiB,CAAC;AAGnC,mFAAmF;AACnF,MAAM,UAAU,yBAAyB,CAAC,OAA8B;IACtE,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC,EAAE;QACtB,OAAO,EAAE,CAAC;KACX;IACD,MAAM,OAAO,GAAG,CAAC,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACnE,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC1E,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,QAAgB,EAAE,MAAc;IACzE,MAAM,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;AACnD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,QAAgB,EAAE,KAAiB;IAC3E,MAAM,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,CAAC;AAClF,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,OAAO,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;AACvE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,6BAA6B,CACjD,MAAuG,EACvG,OAAgB;IAEhB,IAAI,CAAC,OAAO,EAAE;QACZ,OAAO,MAAM,CAAC;KACf;IACD,MAAM,GAAG,GAAG,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC7C,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE;QAC3B,OAAO,MAAM,CAAC;KACf;IACD,MAAM,QAAQ,GAAG,GAAG,OAAO,YAAY,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC;IACxD,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IACnC,OAAO,EAAE,GAAG,MAAM,EAAE,QAAQ,EAAE,CAAC;AACjC,CAAC"}
|
||||||
23
dist/utils/freqLayerPrep.d.ts
vendored
Normal file
23
dist/utils/freqLayerPrep.d.ts
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { type WrappedMat } from './opencvAdapter';
|
||||||
|
import type { SkImage } from '@shopify/react-native-skia';
|
||||||
|
export type FreqLayerImages = {
|
||||||
|
lowFreqImage: SkImage;
|
||||||
|
highFreqImage: SkImage;
|
||||||
|
};
|
||||||
|
export type PaintResourceBatch = {
|
||||||
|
originImage: SkImage;
|
||||||
|
layers: FreqLayerImages;
|
||||||
|
};
|
||||||
|
/** OpenCV 8-bit Lab L 通道(BGR 输入,供单测与近似对照) */
|
||||||
|
export declare function bgrToLabL(b: number, g: number, r: number): number;
|
||||||
|
export declare function bgrBufferToRgbaBuffer(bgr: Uint8Array, cols: number, rows: number): Uint8Array;
|
||||||
|
export declare function releaseFreqLayerImages(layers: FreqLayerImages | null): void;
|
||||||
|
/** 复用已上传的 BGR Mat,避免重复 bufferToMat + JS↔原生往返 */
|
||||||
|
export declare function prepareFreqLayersFromWorkMat(workMat: WrappedMat, cols: number, rows: number): Promise<FreqLayerImages | null>;
|
||||||
|
/** 单次 Mat 上传 → 高低频 + 原图 Skia(并行,高低频先就绪时可回调) */
|
||||||
|
export declare function preparePaintResourcesFromWorkBuffer(bgrBuffer: Uint8Array, cols: number, rows: number, onFreqLayersReady?: (layers: FreqLayerImages) => void): Promise<PaintResourceBatch | null>;
|
||||||
|
/** @deprecated 测试兼容;生产路径请用 preparePaintResourcesFromWorkBuffer */
|
||||||
|
export declare function prepareFreqLayersFromBgrBuffer(bgrBuffer: Uint8Array, cols: number, rows: number): Promise<FreqLayerImages | null>;
|
||||||
|
/** 原图 BGR → Skia RGBA(OpenCV cvtColor,与 freq 并行) */
|
||||||
|
export declare function originBgrBufferToSkiaImage(bgrBuffer: Uint8Array, cols: number, rows: number): Promise<SkImage | null>;
|
||||||
|
//# sourceMappingURL=freqLayerPrep.d.ts.map
|
||||||
1
dist/utils/freqLayerPrep.d.ts.map
vendored
Normal file
1
dist/utils/freqLayerPrep.d.ts.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"freqLayerPrep.d.ts","sourceRoot":"","sources":["../../src/utils/freqLayerPrep.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAG1D,MAAM,MAAM,eAAe,GAAG;IAC5B,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,eAAe,CAAC;CACzB,CAAC;AAEF,6CAA6C;AAC7C,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CA2BjE;AAED,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,UAAU,EACf,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,UAAU,CAYZ;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,QAGpE;AAyDD,gDAAgD;AAChD,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,UAAU,EACnB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAyEjC;AAED,+CAA+C;AAC/C,wBAAsB,mCAAmC,CACvD,SAAS,EAAE,UAAU,EACrB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,GACpD,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CA8BpC;AAED,kEAAkE;AAClE,wBAAsB,8BAA8B,CAClD,SAAS,EAAE,UAAU,EACrB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAWjC;AAED,oDAAoD;AACpD,wBAAsB,0BAA0B,CAC9C,SAAS,EAAE,UAAU,EACrB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAEzB"}
|
||||||
168
dist/utils/freqLayerPrep.js
vendored
Normal file
168
dist/utils/freqLayerPrep.js
vendored
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import cv from './opencvAdapter';
|
||||||
|
import { getMaskSegmentRuntimeConfig } from './maskSegmentRuntime';
|
||||||
|
/** OpenCV 8-bit Lab L 通道(BGR 输入,供单测与近似对照) */
|
||||||
|
export function bgrToLabL(b, g, r) {
|
||||||
|
let rf = r / 255;
|
||||||
|
let gf = g / 255;
|
||||||
|
let bf = b / 255;
|
||||||
|
rf = rf > 0.04045 ? Math.pow((rf + 0.055) / 1.055, 2.4) : rf / 12.92;
|
||||||
|
gf = gf > 0.04045 ? Math.pow((gf + 0.055) / 1.055, 2.4) : gf / 12.92;
|
||||||
|
bf = bf > 0.04045 ? Math.pow((bf + 0.055) / 1.055, 2.4) : bf / 12.92;
|
||||||
|
const x = rf * 0.412453 + gf * 0.35758 + bf * 0.180423;
|
||||||
|
const y = rf * 0.212671 + gf * 0.71516 + bf * 0.072169;
|
||||||
|
const z = rf * 0.019334 + gf * 0.119193 + bf * 0.950227;
|
||||||
|
const xn = 0.950456;
|
||||||
|
const yn = 1;
|
||||||
|
const zn = 1.088754;
|
||||||
|
const delta = 6 / 29;
|
||||||
|
const delta3 = delta * delta * delta;
|
||||||
|
let fx = x / xn;
|
||||||
|
let fy = y / yn;
|
||||||
|
let fz = z / zn;
|
||||||
|
fx = fx > delta3 ? Math.cbrt(fx) : fx / (3 * delta * delta) + 4 / 29;
|
||||||
|
fy = fy > delta3 ? Math.cbrt(fy) : fy / (3 * delta * delta) + 4 / 29;
|
||||||
|
fz = fz > delta3 ? Math.cbrt(fz) : fz / (3 * delta * delta) + 4 / 29;
|
||||||
|
const L = fy * 116 - 16;
|
||||||
|
return Math.max(0, Math.min(255, Math.round((L * 255) / 100)));
|
||||||
|
}
|
||||||
|
export function bgrBufferToRgbaBuffer(bgr, cols, rows) {
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
const rgba = new Uint8Array(pixelCount * 4);
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
const s = i * 3;
|
||||||
|
const d = i * 4;
|
||||||
|
rgba[d] = bgr[s + 2];
|
||||||
|
rgba[d + 1] = bgr[s + 1];
|
||||||
|
rgba[d + 2] = bgr[s];
|
||||||
|
rgba[d + 3] = 255;
|
||||||
|
}
|
||||||
|
return rgba;
|
||||||
|
}
|
||||||
|
export function releaseFreqLayerImages(layers) {
|
||||||
|
layers?.lowFreqImage.dispose();
|
||||||
|
layers?.highFreqImage.dispose();
|
||||||
|
}
|
||||||
|
/** 16-bit 有符号差分 → 8-bit 高频层(detail * gain + 128) */
|
||||||
|
async function buildHighFreqMatNative(lMat, lLowMat, cols, rows, gain) {
|
||||||
|
const l16 = cv.createMat(cols, rows, 1);
|
||||||
|
const lLow16 = cv.createMat(cols, rows, 1);
|
||||||
|
const diff16 = cv.createMat(cols, rows, 1);
|
||||||
|
const high8 = cv.createMat(cols, rows, 1);
|
||||||
|
try {
|
||||||
|
cv.convertTo(lMat, l16, cv.CV_16SC1);
|
||||||
|
cv.convertTo(lLowMat, lLow16, cv.CV_16SC1);
|
||||||
|
await cv.subtract(l16, lLow16, diff16);
|
||||||
|
await cv.addWeighted(diff16, gain, null, 0, 128, diff16);
|
||||||
|
cv.convertTo(diff16, high8, cv.CV_8UC1);
|
||||||
|
return high8;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
l16.release();
|
||||||
|
lLow16.release();
|
||||||
|
diff16.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function downscaleMatForFreq(workMat, cols, rows) {
|
||||||
|
const maxLongSide = getMaskSegmentRuntimeConfig().pipeline.paintFreqMaxLongSide;
|
||||||
|
const longSide = Math.max(cols, rows);
|
||||||
|
if (longSide <= maxLongSide) {
|
||||||
|
return { mat: workMat, cols, rows, owned: false };
|
||||||
|
}
|
||||||
|
const scale = maxLongSide / longSide;
|
||||||
|
const freqCols = Math.floor(cols * scale);
|
||||||
|
const freqRows = Math.floor(rows * scale);
|
||||||
|
const resized = cv.createMat(freqCols, freqRows, 3);
|
||||||
|
await cv.resize(workMat, resized, { width: freqCols, height: freqRows }, cv.INTER_LINEAR);
|
||||||
|
return { mat: resized, cols: freqCols, rows: freqRows, owned: true };
|
||||||
|
}
|
||||||
|
/** 复用已上传的 BGR Mat,避免重复 bufferToMat + JS↔原生往返 */
|
||||||
|
export async function prepareFreqLayersFromWorkMat(workMat, cols, rows) {
|
||||||
|
const paintCfg = getMaskSegmentRuntimeConfig().paint;
|
||||||
|
const blurStart = __DEV__ ? performance.now() : 0;
|
||||||
|
const scaled = await downscaleMatForFreq(workMat, cols, rows);
|
||||||
|
const freqMat = scaled.mat;
|
||||||
|
const freqCols = scaled.cols;
|
||||||
|
const freqRows = scaled.rows;
|
||||||
|
let labMat = null;
|
||||||
|
let lMat = null;
|
||||||
|
let lLowMat = null;
|
||||||
|
let lHighMat = null;
|
||||||
|
try {
|
||||||
|
labMat = cv.cvtColorBgr(freqMat, cv.COLOR_BGR2Lab);
|
||||||
|
lMat = cv.createMat(freqCols, freqRows, 1);
|
||||||
|
cv.extractChannel(labMat, lMat, 0);
|
||||||
|
labMat.release();
|
||||||
|
labMat = null;
|
||||||
|
lLowMat = cv.createMat(freqCols, freqRows, 1);
|
||||||
|
const kernel = paintCfg.lLowBlurKernel;
|
||||||
|
await cv.GaussianBlur(lMat, lLowMat, { width: kernel, height: kernel }, 0);
|
||||||
|
lHighMat = await buildHighFreqMatNative(lMat, lLowMat, freqCols, freqRows, paintCfg.lHighGain);
|
||||||
|
await cv.addWeighted(lLowMat, paintCfg.lLowContrast, null, 0, 128 * (1 - paintCfg.lLowContrast), lLowMat);
|
||||||
|
await cv.addWeighted(lLowMat, paintCfg.lLowBrightness, null, 0, 128 * (1 - paintCfg.lLowBrightness), lLowMat);
|
||||||
|
const lowFreqImage = cv.grayMatToSkiaImage(lLowMat);
|
||||||
|
const highFreqImage = cv.grayMatToSkiaImage(lHighMat);
|
||||||
|
if (!lowFreqImage || !highFreqImage) {
|
||||||
|
lowFreqImage?.dispose();
|
||||||
|
highFreqImage?.dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { lowFreqImage, highFreqImage };
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (scaled.owned) {
|
||||||
|
freqMat.release();
|
||||||
|
}
|
||||||
|
labMat?.release();
|
||||||
|
lMat?.release();
|
||||||
|
lLowMat?.release();
|
||||||
|
lHighMat?.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** 单次 Mat 上传 → 高低频 + 原图 Skia(并行,高低频先就绪时可回调) */
|
||||||
|
export async function preparePaintResourcesFromWorkBuffer(bgrBuffer, cols, rows, onFreqLayersReady) {
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
if (bgrBuffer.length !== pixelCount * 3) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const prepStart = __DEV__ ? performance.now() : 0;
|
||||||
|
const workMat = cv.bgrBufferToMat(bgrBuffer, cols, rows);
|
||||||
|
try {
|
||||||
|
const originPromise = cv.matToSkiaImage(workMat);
|
||||||
|
const freqPromise = prepareFreqLayersFromWorkMat(workMat, cols, rows);
|
||||||
|
const layers = await freqPromise;
|
||||||
|
if (!layers) {
|
||||||
|
const originImage = await originPromise;
|
||||||
|
originImage?.dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
onFreqLayersReady?.(layers);
|
||||||
|
const originImage = await originPromise;
|
||||||
|
if (!originImage) {
|
||||||
|
releaseFreqLayerImages(layers);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { originImage, layers };
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
workMat.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** @deprecated 测试兼容;生产路径请用 preparePaintResourcesFromWorkBuffer */
|
||||||
|
export async function prepareFreqLayersFromBgrBuffer(bgrBuffer, cols, rows) {
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
if (bgrBuffer.length !== pixelCount * 3) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const workMat = cv.bgrBufferToMat(bgrBuffer, cols, rows);
|
||||||
|
try {
|
||||||
|
return await prepareFreqLayersFromWorkMat(workMat, cols, rows);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
workMat.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** 原图 BGR → Skia RGBA(OpenCV cvtColor,与 freq 并行) */
|
||||||
|
export async function originBgrBufferToSkiaImage(bgrBuffer, cols, rows) {
|
||||||
|
return cv.bgrBufferToSkiaImage(bgrBuffer, cols, rows);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=freqLayerPrep.js.map
|
||||||
1
dist/utils/freqLayerPrep.js.map
vendored
Normal file
1
dist/utils/freqLayerPrep.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
43
dist/utils/maskSegmentRuntime.d.ts
vendored
Normal file
43
dist/utils/maskSegmentRuntime.d.ts
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { type MaskSemanticColor } from './maskSemanticPalette';
|
||||||
|
import type { InteractionConfig, MaskSegmentConfig, PaintConfig, PipelineConfig, PipelinePreset } from '../components/MaskSegmentCanvas.types';
|
||||||
|
/** high */
|
||||||
|
export declare const PIPELINE_HIGH: Required<PipelineConfig>;
|
||||||
|
/** middle */
|
||||||
|
export declare const PIPELINE_MEDIUM: Required<PipelineConfig>;
|
||||||
|
/** low */
|
||||||
|
export declare const PIPELINE_LOW: Required<PipelineConfig>;
|
||||||
|
export declare const PIPELINE_PRESETS: Record<PipelinePreset, Required<PipelineConfig>>;
|
||||||
|
export declare const DEFAULT_PIPELINE_CONFIG: Required<PipelineConfig>;
|
||||||
|
export declare function resolvePipelineConfig(preset?: PipelinePreset, overrides?: PipelineConfig): Required<PipelineConfig>;
|
||||||
|
export declare const DEFAULT_PAINT_CONFIG: Required<PaintConfig>;
|
||||||
|
export declare const DEFAULT_INTERACTION_CONFIG: Required<InteractionConfig>;
|
||||||
|
export declare const DEFAULT_MASK_CONFIG: Required<Omit<MaskSegmentConfig, 'baseboardStripQuantKeys' | 'wallQuantKeys' | 'cabinetQuantKeys' | 'secondarySemanticNames'>> & {
|
||||||
|
baseboardStripQuantKeys: Set<string>;
|
||||||
|
wallQuantKeys: Set<string>;
|
||||||
|
cabinetQuantKeys: Set<string>;
|
||||||
|
secondarySemanticNames: Set<string>;
|
||||||
|
semanticColors: MaskSemanticColor[];
|
||||||
|
};
|
||||||
|
export type ResolvedMaskSegmentRuntime = {
|
||||||
|
pipeline: Required<PipelineConfig>;
|
||||||
|
mask: typeof DEFAULT_MASK_CONFIG;
|
||||||
|
paint: Required<PaintConfig>;
|
||||||
|
interaction: Required<InteractionConfig>;
|
||||||
|
};
|
||||||
|
export declare function mergeMaskConfig(partial?: MaskSegmentConfig): typeof DEFAULT_MASK_CONFIG;
|
||||||
|
export declare function createRuntimeConfig(input?: {
|
||||||
|
pipelineConfig?: PipelineConfig;
|
||||||
|
maskConfig?: MaskSegmentConfig;
|
||||||
|
paintConfig?: PaintConfig;
|
||||||
|
interactionConfig?: InteractionConfig;
|
||||||
|
}): ResolvedMaskSegmentRuntime;
|
||||||
|
export declare function getMaskRuntimeRevision(): number;
|
||||||
|
export declare function setMaskSegmentRuntimeConfig(input?: {
|
||||||
|
pipelineConfig?: PipelineConfig;
|
||||||
|
maskConfig?: MaskSegmentConfig;
|
||||||
|
paintConfig?: PaintConfig;
|
||||||
|
interactionConfig?: InteractionConfig;
|
||||||
|
}): ResolvedMaskSegmentRuntime;
|
||||||
|
export declare function getMaskSegmentRuntimeConfig(): ResolvedMaskSegmentRuntime;
|
||||||
|
export declare function resetMaskSegmentRuntimeConfig(): ResolvedMaskSegmentRuntime;
|
||||||
|
//# sourceMappingURL=maskSegmentRuntime.d.ts.map
|
||||||
1
dist/utils/maskSegmentRuntime.d.ts.map
vendored
Normal file
1
dist/utils/maskSegmentRuntime.d.ts.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"maskSegmentRuntime.d.ts","sourceRoot":"","sources":["../../src/utils/maskSegmentRuntime.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,iBAAiB,EACvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EACV,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACX,cAAc,EACd,cAAc,EAEf,MAAM,uCAAuC,CAAC;AAC/C,WAAW;AACX,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,cAAc,CAQlD,CAAC;AAEF,aAAa;AACb,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,cAAc,CAQpD,CAAC;AAEF,UAAU;AACV,eAAO,MAAM,YAAY,EAAE,QAAQ,CAAC,cAAc,CAQjD,CAAC;AACF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,cAAc,EAAE,QAAQ,CAAC,cAAc,CAAC,CAI7E,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,cAAc,CAAiB,CAAC;AAE/E,wBAAgB,qBAAqB,CACnC,MAAM,CAAC,EAAE,cAAc,EACvB,SAAS,CAAC,EAAE,cAAc,GACzB,QAAQ,CAAC,cAAc,CAAC,CAI1B;AAWD,eAAO,MAAM,oBAAoB,EAAE,QAAQ,CAAC,WAAW,CAkBtD,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,QAAQ,CAAC,iBAAiB,CAOlE,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CACxC,IAAI,CACF,iBAAiB,EACf,yBAAyB,GACzB,eAAe,GACf,kBAAkB,GAClB,wBAAwB,CAC3B,CACF,GAAG;IACF,uBAAuB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACrC,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,sBAAsB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACpC,cAAc,EAAE,iBAAiB,EAAE,CAAC;CAkBrC,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,QAAQ,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC;IACnC,IAAI,EAAE,OAAO,mBAAmB,CAAC;IACjC,KAAK,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC7B,WAAW,EAAE,QAAQ,CAAC,iBAAiB,CAAC,CAAC;CAC1C,CAAC;AAMF,wBAAgB,eAAe,CAC7B,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,mBAAmB,CA2C5B;AAED,wBAAgB,mBAAmB,CAAC,KAAK,CAAC,EAAE;IAC1C,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;CACvC,GAAG,0BAA0B,CAiB7B;AAKD,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AAED,wBAAgB,2BAA2B,CAAC,KAAK,CAAC,EAAE;IAClD,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;CACvC,GAAG,0BAA0B,CAiB7B;AAED,wBAAgB,2BAA2B,IAAI,0BAA0B,CAExE;AAED,wBAAgB,6BAA6B,IAAI,0BAA0B,CAI1E"}
|
||||||
181
dist/utils/maskSegmentRuntime.js
vendored
Normal file
181
dist/utils/maskSegmentRuntime.js
vendored
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import { BASEBOARD_STRIP_QUANT_KEYS, CABINET_QUANT_KEYS, MASK_SEMANTIC_COLORS, WALL_QUANT_KEYS, } from './maskSemanticPalette';
|
||||||
|
/** high */
|
||||||
|
export const PIPELINE_HIGH = {
|
||||||
|
maxImageLongSide: 1440,
|
||||||
|
paintFreqMaxLongSide: 960,
|
||||||
|
originPreviewMaxLongSide: 720,
|
||||||
|
maskPathMaxLongSide: 960,
|
||||||
|
minContourArea: 50,
|
||||||
|
contourApproxEpsilon: 0.002,
|
||||||
|
maxRegions: 800,
|
||||||
|
};
|
||||||
|
/** middle */
|
||||||
|
export const PIPELINE_MEDIUM = {
|
||||||
|
maxImageLongSide: 720,
|
||||||
|
paintFreqMaxLongSide: 480,
|
||||||
|
originPreviewMaxLongSide: 360,
|
||||||
|
maskPathMaxLongSide: 480,
|
||||||
|
minContourArea: 100,
|
||||||
|
contourApproxEpsilon: 0.003,
|
||||||
|
maxRegions: 500,
|
||||||
|
};
|
||||||
|
/** low */
|
||||||
|
export const PIPELINE_LOW = {
|
||||||
|
maxImageLongSide: 360,
|
||||||
|
paintFreqMaxLongSide: 240,
|
||||||
|
originPreviewMaxLongSide: 180,
|
||||||
|
maskPathMaxLongSide: 240,
|
||||||
|
minContourArea: 200,
|
||||||
|
contourApproxEpsilon: 0.005,
|
||||||
|
maxRegions: 300,
|
||||||
|
};
|
||||||
|
export const PIPELINE_PRESETS = {
|
||||||
|
high: PIPELINE_HIGH,
|
||||||
|
medium: PIPELINE_MEDIUM,
|
||||||
|
low: PIPELINE_LOW,
|
||||||
|
};
|
||||||
|
export const DEFAULT_PIPELINE_CONFIG = PIPELINE_HIGH;
|
||||||
|
export function resolvePipelineConfig(preset, overrides) {
|
||||||
|
const base = preset != null ? PIPELINE_PRESETS[preset] : DEFAULT_PIPELINE_CONFIG;
|
||||||
|
return { ...base, ...overrides };
|
||||||
|
}
|
||||||
|
const DEFAULT_PAINT_PALETTE = [
|
||||||
|
{ b: 138, g: 126, r: 110 },
|
||||||
|
{ b: 92, g: 124, r: 86 },
|
||||||
|
{ b: 70, g: 80, r: 158 },
|
||||||
|
{ b: 54, g: 134, r: 182 },
|
||||||
|
{ b: 128, g: 98, r: 142 },
|
||||||
|
{ b: 76, g: 120, r: 138 },
|
||||||
|
];
|
||||||
|
export const DEFAULT_PAINT_CONFIG = {
|
||||||
|
palette: DEFAULT_PAINT_PALETTE,
|
||||||
|
// Optimized coloring: slightly stronger base color fidelity while preserving natural lighting.
|
||||||
|
colorBaseOpacity: 0.88,
|
||||||
|
lLightOpacity: 0.50,
|
||||||
|
// Strengthened texture retention (high-freq detail overlay) for richer surface appearance after recolor.
|
||||||
|
textureOpacity: 0.85,
|
||||||
|
// Slightly tighter low-freq lighting kernel for cleaner wall/ceiling shading without over-smoothing.
|
||||||
|
lLowBlurKernel: 7,
|
||||||
|
lLowContrast: 1.10,
|
||||||
|
lLowBrightness: 0.92,
|
||||||
|
lHighGain: 1.22,
|
||||||
|
// Edge handling: small positive feather produces soft transitions at painted region boundaries.
|
||||||
|
// color feather drives the paintColorMap alpha softness (used by shader for blend-to-origin).
|
||||||
|
maskFeatherColor: 1.6,
|
||||||
|
maskFeatherTexture: 0.9,
|
||||||
|
regionOverlayFill: '#FFC14D',
|
||||||
|
regionOutlineStrokeWidth: 4,
|
||||||
|
};
|
||||||
|
export const DEFAULT_INTERACTION_CONFIG = {
|
||||||
|
kickMaskPickRadiusPx: 36,
|
||||||
|
pickMapSearchRadiusPx: 14,
|
||||||
|
thinStripPadding: 0.008,
|
||||||
|
regionPadding: 0.003,
|
||||||
|
initRegionFlashMs: 1000,
|
||||||
|
enableInitRegionFlash: true,
|
||||||
|
};
|
||||||
|
export const DEFAULT_MASK_CONFIG = {
|
||||||
|
semanticColors: MASK_SEMANTIC_COLORS,
|
||||||
|
baseboardMaxColorDist: 42,
|
||||||
|
blackThreshold: 30,
|
||||||
|
quantStep: 64,
|
||||||
|
baseboardStripQuantKeys: new Set(BASEBOARD_STRIP_QUANT_KEYS),
|
||||||
|
wallQuantKeys: new Set(WALL_QUANT_KEYS),
|
||||||
|
cabinetQuantKeys: new Set(CABINET_QUANT_KEYS),
|
||||||
|
maxRegionColors: 6,
|
||||||
|
secondarySemanticNames: new Set(['garageDoor', 'roof', 'eave']),
|
||||||
|
secondaryMinPixelRatio: 0.002,
|
||||||
|
junctionHRadiusPx: 24,
|
||||||
|
junctionVRadiusPx: 2,
|
||||||
|
kickBridgeHalfWPx: 6,
|
||||||
|
baseboardJunctionRowMarginPx: 1,
|
||||||
|
baseboardJunctionVReachPx: 2,
|
||||||
|
baseboardMinRunPx: 2,
|
||||||
|
};
|
||||||
|
function toStringSet(values) {
|
||||||
|
return new Set(values ?? []);
|
||||||
|
}
|
||||||
|
export function mergeMaskConfig(partial) {
|
||||||
|
if (!partial) {
|
||||||
|
return { ...DEFAULT_MASK_CONFIG };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
semanticColors: partial.semanticColors ?? DEFAULT_MASK_CONFIG.semanticColors,
|
||||||
|
baseboardMaxColorDist: partial.baseboardMaxColorDist ?? DEFAULT_MASK_CONFIG.baseboardMaxColorDist,
|
||||||
|
blackThreshold: partial.blackThreshold ?? DEFAULT_MASK_CONFIG.blackThreshold,
|
||||||
|
quantStep: partial.quantStep ?? DEFAULT_MASK_CONFIG.quantStep,
|
||||||
|
baseboardStripQuantKeys: partial.baseboardStripQuantKeys
|
||||||
|
? toStringSet(partial.baseboardStripQuantKeys)
|
||||||
|
: new Set(DEFAULT_MASK_CONFIG.baseboardStripQuantKeys),
|
||||||
|
wallQuantKeys: partial.wallQuantKeys
|
||||||
|
? toStringSet(partial.wallQuantKeys)
|
||||||
|
: new Set(DEFAULT_MASK_CONFIG.wallQuantKeys),
|
||||||
|
cabinetQuantKeys: partial.cabinetQuantKeys
|
||||||
|
? toStringSet(partial.cabinetQuantKeys)
|
||||||
|
: new Set(DEFAULT_MASK_CONFIG.cabinetQuantKeys),
|
||||||
|
maxRegionColors: partial.maxRegionColors ?? DEFAULT_MASK_CONFIG.maxRegionColors,
|
||||||
|
secondarySemanticNames: partial.secondarySemanticNames
|
||||||
|
? toStringSet(partial.secondarySemanticNames)
|
||||||
|
: new Set(DEFAULT_MASK_CONFIG.secondarySemanticNames),
|
||||||
|
secondaryMinPixelRatio: partial.secondaryMinPixelRatio ??
|
||||||
|
DEFAULT_MASK_CONFIG.secondaryMinPixelRatio,
|
||||||
|
junctionHRadiusPx: partial.junctionHRadiusPx ?? DEFAULT_MASK_CONFIG.junctionHRadiusPx,
|
||||||
|
junctionVRadiusPx: partial.junctionVRadiusPx ?? DEFAULT_MASK_CONFIG.junctionVRadiusPx,
|
||||||
|
kickBridgeHalfWPx: partial.kickBridgeHalfWPx ?? DEFAULT_MASK_CONFIG.kickBridgeHalfWPx,
|
||||||
|
baseboardJunctionRowMarginPx: partial.baseboardJunctionRowMarginPx ??
|
||||||
|
DEFAULT_MASK_CONFIG.baseboardJunctionRowMarginPx,
|
||||||
|
baseboardJunctionVReachPx: partial.baseboardJunctionVReachPx ??
|
||||||
|
DEFAULT_MASK_CONFIG.baseboardJunctionVReachPx,
|
||||||
|
baseboardMinRunPx: partial.baseboardMinRunPx ?? DEFAULT_MASK_CONFIG.baseboardMinRunPx,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export function createRuntimeConfig(input) {
|
||||||
|
return {
|
||||||
|
pipeline: {
|
||||||
|
...DEFAULT_PIPELINE_CONFIG,
|
||||||
|
...input?.pipelineConfig,
|
||||||
|
},
|
||||||
|
mask: mergeMaskConfig(input?.maskConfig),
|
||||||
|
paint: {
|
||||||
|
...DEFAULT_PAINT_CONFIG,
|
||||||
|
...input?.paintConfig,
|
||||||
|
palette: input?.paintConfig?.palette ?? DEFAULT_PAINT_CONFIG.palette,
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
...DEFAULT_INTERACTION_CONFIG,
|
||||||
|
...input?.interactionConfig,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let activeRuntime = createRuntimeConfig();
|
||||||
|
let runtimeRevision = 0;
|
||||||
|
export function getMaskRuntimeRevision() {
|
||||||
|
return runtimeRevision;
|
||||||
|
}
|
||||||
|
export function setMaskSegmentRuntimeConfig(input) {
|
||||||
|
activeRuntime = {
|
||||||
|
pipeline: input?.pipelineConfig
|
||||||
|
? { ...activeRuntime.pipeline, ...input.pipelineConfig }
|
||||||
|
: activeRuntime.pipeline,
|
||||||
|
mask: input?.maskConfig
|
||||||
|
? mergeMaskConfig(input.maskConfig)
|
||||||
|
: activeRuntime.mask,
|
||||||
|
paint: input?.paintConfig
|
||||||
|
? { ...activeRuntime.paint, ...input.paintConfig }
|
||||||
|
: activeRuntime.paint,
|
||||||
|
interaction: input?.interactionConfig
|
||||||
|
? { ...activeRuntime.interaction, ...input.interactionConfig }
|
||||||
|
: activeRuntime.interaction,
|
||||||
|
};
|
||||||
|
runtimeRevision += 1;
|
||||||
|
return activeRuntime;
|
||||||
|
}
|
||||||
|
export function getMaskSegmentRuntimeConfig() {
|
||||||
|
return activeRuntime;
|
||||||
|
}
|
||||||
|
export function resetMaskSegmentRuntimeConfig() {
|
||||||
|
activeRuntime = createRuntimeConfig();
|
||||||
|
runtimeRevision += 1;
|
||||||
|
return activeRuntime;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=maskSegmentRuntime.js.map
|
||||||
1
dist/utils/maskSegmentRuntime.js.map
vendored
Normal file
1
dist/utils/maskSegmentRuntime.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
133
dist/utils/maskSegmentation.d.ts
vendored
Normal file
133
dist/utils/maskSegmentation.d.ts
vendored
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { type WrappedMat } from './opencvAdapter';
|
||||||
|
import { type SkPath } from '@shopify/react-native-skia';
|
||||||
|
export type RegionPickMap = {
|
||||||
|
buffer: Uint8Array;
|
||||||
|
cols: number;
|
||||||
|
rows: number;
|
||||||
|
};
|
||||||
|
export type SegmentMaskResult = {
|
||||||
|
regions: SegmentRegion[];
|
||||||
|
pickMap: RegionPickMap;
|
||||||
|
labels: Uint8Array;
|
||||||
|
baseboardBinary: Uint8Array;
|
||||||
|
segCols: number;
|
||||||
|
segRows: number;
|
||||||
|
};
|
||||||
|
export type SegmentRegion = {
|
||||||
|
id: number;
|
||||||
|
/** 语义分区名(door / cabinet / baseboard …) */
|
||||||
|
name: string;
|
||||||
|
/** 参考色 hex */
|
||||||
|
hex: string;
|
||||||
|
/** 参考色(BGR) */
|
||||||
|
color: {
|
||||||
|
b: number;
|
||||||
|
g: number;
|
||||||
|
r: number;
|
||||||
|
};
|
||||||
|
polygons: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}[][];
|
||||||
|
/** 上色/高亮蒙版:严格像素条带,不填充黑色空洞 */
|
||||||
|
maskPolygons?: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}[][];
|
||||||
|
hitPolygons?: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}[][];
|
||||||
|
outlinePolygons?: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}[][];
|
||||||
|
bbox: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
};
|
||||||
|
area: number;
|
||||||
|
/** 踢脚线等细条区域,点击检测需加宽容差 */
|
||||||
|
thinStrip?: boolean;
|
||||||
|
};
|
||||||
|
export declare function buildRegionOutlinePolygons(reg: SegmentRegion): NormPoint[][];
|
||||||
|
export declare function buildRegionOutlinePathForRegion(regionId: number, regions: SegmentRegion[], maskData: RegionMaskData, rect: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
}, normSeed?: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}): SkPath;
|
||||||
|
export declare function buildAllRegionOutlinePaths(regions: SegmentRegion[], maskData: RegionMaskData, rect: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
}): Map<number, SkPath>;
|
||||||
|
/** 从二值图逐行条带构建蒙版(供 Skia PathBuilder 使用) */
|
||||||
|
export declare function appendMaskBinaryToPathBuilder(binary: Uint8Array, cols: number, rows: number, rect: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
}, builder: {
|
||||||
|
moveTo: (x: number, y: number) => unknown;
|
||||||
|
lineTo: (x: number, y: number) => unknown;
|
||||||
|
close: () => unknown;
|
||||||
|
}, minRunPx?: number): void;
|
||||||
|
/** 从语义标签逐行条带构建蒙版(避免维护多张二值图) */
|
||||||
|
export declare function appendLabelMaskToPathBuilder(labels: Uint8Array, semanticIndex: number, cols: number, rows: number, rect: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
}, builder: {
|
||||||
|
moveTo: (x: number, y: number) => unknown;
|
||||||
|
lineTo: (x: number, y: number) => unknown;
|
||||||
|
close: () => unknown;
|
||||||
|
}, minRunPx?: number): void;
|
||||||
|
export type RegionMaskData = {
|
||||||
|
labels: Uint8Array;
|
||||||
|
baseboardBinary: Uint8Array;
|
||||||
|
cols: number;
|
||||||
|
rows: number;
|
||||||
|
};
|
||||||
|
/** 蒙版路径构建降采样(屏幕显示不需要分割分辨率,点击仍用全分辨率 pickMap) */
|
||||||
|
export declare function downsampleMaskDataForPaths(maskData: RegionMaskData, maxLongSide: number): RegionMaskData;
|
||||||
|
/** 单次扫描构建所有分区 Skia 蒙版路径(单 label pass,避免每像素 × 语义数循环) */
|
||||||
|
export declare function buildAllRegionMaskPaths(regions: SegmentRegion[], maskData: RegionMaskData, rect: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
}): Map<number, SkPath>;
|
||||||
|
type NormPoint = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
export declare function buildBaseboardBinaryFromMask(buffer: Uint8Array, cols: number, rows: number): Uint8Array;
|
||||||
|
/** 分割分辨率踢脚线二值图最近邻放大到点击查表分辨率(避免全图 junction 重算) */
|
||||||
|
export declare function upscaleBinaryMask(src: Uint8Array, srcCols: number, srcRows: number, dstCols: number, dstRows: number): Uint8Array;
|
||||||
|
export declare function isBaseboardMaskPixel(buffer: Uint8Array, cols: number, rows: number, x: number, y: number, baseboardBinary?: Uint8Array | null): boolean;
|
||||||
|
export { isStrictBaseboardPixel as isBaseboardPixel } from './maskSemanticPalette';
|
||||||
|
export declare function getMaskQuantKey(b: number, g: number, r: number): string;
|
||||||
|
/** @deprecated 请使用 isBaseboardMaskPixel */
|
||||||
|
export declare function isKickPlatePixel(b: number, g: number, r: number): boolean;
|
||||||
|
export declare function extractRegionsFromMaskBuffer(buffer: Uint8Array, cols: number, rows: number, _options: {
|
||||||
|
minArea: number;
|
||||||
|
approxEpsilon: number;
|
||||||
|
}): Promise<SegmentMaskResult>;
|
||||||
|
export declare function extractRegionsFromMaskBufferSync(buffer: Uint8Array, cols: number, rows: number, _options: {
|
||||||
|
minArea: number;
|
||||||
|
approxEpsilon: number;
|
||||||
|
}): SegmentMaskResult;
|
||||||
|
/** @deprecated 请使用 extractRegionsFromMaskBuffer */
|
||||||
|
export declare function extractRegionsFromMask(maskMat: WrappedMat, options: {
|
||||||
|
minArea: number;
|
||||||
|
approxEpsilon: number;
|
||||||
|
}): Promise<SegmentRegion[]>;
|
||||||
|
//# sourceMappingURL=maskSegmentation.d.ts.map
|
||||||
1
dist/utils/maskSegmentation.d.ts.map
vendored
Normal file
1
dist/utils/maskSegmentation.d.ts.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"maskSegmentation.d.ts","sourceRoot":"","sources":["../../src/utils/maskSegmentation.ts"],"names":[],"mappings":"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"}
|
||||||
1600
dist/utils/maskSegmentation.js
vendored
Normal file
1600
dist/utils/maskSegmentation.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
dist/utils/maskSegmentation.js.map
vendored
Normal file
1
dist/utils/maskSegmentation.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
31
dist/utils/maskSemanticPalette.d.ts
vendored
Normal file
31
dist/utils/maskSemanticPalette.d.ts
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
export type MaskSemanticColor = {
|
||||||
|
name: string;
|
||||||
|
hex: string;
|
||||||
|
/** 参考色(BGR,与掩码 buffer 通道一致) */
|
||||||
|
bgr: {
|
||||||
|
b: number;
|
||||||
|
g: number;
|
||||||
|
r: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** 掩码语义色表(与后端分区颜色参考一致) */
|
||||||
|
export declare const MASK_SEMANTIC_COLORS: MaskSemanticColor[];
|
||||||
|
export declare const BASEBOARD_SEMANTIC_NAME = "baseboard";
|
||||||
|
/** 将掩码像素归类到最近的语义色(baseboard 仅严格橙色命中) */
|
||||||
|
export declare function classifyBgrPixelToSemantic(b: number, g: number, r: number): string;
|
||||||
|
export declare function getSemanticColorByName(name: string): MaskSemanticColor | undefined;
|
||||||
|
/**
|
||||||
|
* 踢脚线须更接近 #F58231 且明显优于黄柜 / 蓝墙,避免整块黄区被误判。
|
||||||
|
*/
|
||||||
|
export declare function isStrictBaseboardPixel(b: number, g: number, r: number): boolean;
|
||||||
|
export declare function isBaseboardPixel(b: number, g: number, r: number): boolean;
|
||||||
|
/** 掩码上墙/柜交界细条的量化色 */
|
||||||
|
export declare const BASEBOARD_STRIP_QUANT_KEYS: Set<string>;
|
||||||
|
/** 掩码上墙面量化色 */
|
||||||
|
export declare const WALL_QUANT_KEYS: Set<string>;
|
||||||
|
/** 掩码上柜/地面量化色 */
|
||||||
|
export declare const CABINET_QUANT_KEYS: Set<string>;
|
||||||
|
export declare function getBaseboardStripQuantKeys(): Set<string>;
|
||||||
|
export declare function getWallQuantKeys(): Set<string>;
|
||||||
|
export declare function getCabinetQuantKeys(): Set<string>;
|
||||||
|
//# sourceMappingURL=maskSemanticPalette.d.ts.map
|
||||||
1
dist/utils/maskSemanticPalette.d.ts.map
vendored
Normal file
1
dist/utils/maskSemanticPalette.d.ts.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"maskSemanticPalette.d.ts","sourceRoot":"","sources":["../../src/utils/maskSemanticPalette.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,+BAA+B;IAC/B,GAAG,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1C,CAAC;AAEF,0BAA0B;AAC1B,eAAO,MAAM,oBAAoB,EAAE,iBAAiB,EAUnD,CAAC;AAEF,eAAO,MAAM,uBAAuB,cAAc,CAAC;AAiEnD,wCAAwC;AACxC,wBAAgB,0BAA0B,CACxC,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,GACR,MAAM,CA8BR;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAGlF;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,GACR,OAAO,CAYT;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAEzE;AAED,qBAAqB;AACrB,eAAO,MAAM,0BAA0B,aAAuC,CAAC;AAE/E,eAAe;AACf,eAAO,MAAM,eAAe,aAM1B,CAAC;AAEH,iBAAiB;AACjB,eAAO,MAAM,kBAAkB,aAI7B,CAAC;AAEH,wBAAgB,0BAA0B,IAAI,GAAG,CAAC,MAAM,CAAC,CAExD;AAED,wBAAgB,gBAAgB,IAAI,GAAG,CAAC,MAAM,CAAC,CAE9C;AAED,wBAAgB,mBAAmB,IAAI,GAAG,CAAC,MAAM,CAAC,CAEjD"}
|
||||||
125
dist/utils/maskSemanticPalette.js
vendored
Normal file
125
dist/utils/maskSemanticPalette.js
vendored
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import { getMaskRuntimeRevision, getMaskSegmentRuntimeConfig, } from './maskSegmentRuntime';
|
||||||
|
/** 掩码语义色表(与后端分区颜色参考一致) */
|
||||||
|
export const MASK_SEMANTIC_COLORS = [
|
||||||
|
{ name: 'door', hex: '#E6194B', bgr: { b: 75, g: 25, r: 230 } },
|
||||||
|
{ name: 'ceiling', hex: '#3CB44B', bgr: { b: 75, g: 180, r: 60 } },
|
||||||
|
{ name: 'cabinet', hex: '#FFE119', bgr: { b: 25, g: 225, r: 255 } },
|
||||||
|
{ name: 'wall', hex: '#4363D8', bgr: { b: 216, g: 99, r: 67 } },
|
||||||
|
{ name: 'baseboard', hex: '#F58231', bgr: { b: 49, g: 130, r: 245 } },
|
||||||
|
{ name: 'windowFrame', hex: '#911EB4', bgr: { b: 180, g: 30, r: 145 } },
|
||||||
|
{ name: 'garageDoor', hex: '#46F0F0', bgr: { b: 240, g: 240, r: 70 } },
|
||||||
|
{ name: 'roof', hex: '#F032E6', bgr: { b: 230, g: 50, r: 240 } },
|
||||||
|
{ name: 'eave', hex: '#BCF60C', bgr: { b: 12, g: 246, r: 188 } },
|
||||||
|
];
|
||||||
|
export const BASEBOARD_SEMANTIC_NAME = 'baseboard';
|
||||||
|
let contextRevision = -1;
|
||||||
|
let cachedContext = null;
|
||||||
|
function buildSemanticRgb(colors) {
|
||||||
|
return colors.map(entry => ({
|
||||||
|
name: entry.name,
|
||||||
|
rgb: {
|
||||||
|
r: entry.bgr.r,
|
||||||
|
g: entry.bgr.g,
|
||||||
|
b: entry.bgr.b,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
function getSemanticContext() {
|
||||||
|
const revision = getMaskRuntimeRevision();
|
||||||
|
if (contextRevision === revision && cachedContext) {
|
||||||
|
return cachedContext;
|
||||||
|
}
|
||||||
|
const mask = getMaskSegmentRuntimeConfig().mask;
|
||||||
|
const semanticRgb = buildSemanticRgb(mask.semanticColors);
|
||||||
|
const baseboardRgb = semanticRgb.find(entry => entry.name === BASEBOARD_SEMANTIC_NAME);
|
||||||
|
const cabinetRgb = semanticRgb.find(entry => entry.name === 'cabinet');
|
||||||
|
const wallRgb = semanticRgb.find(entry => entry.name === 'wall');
|
||||||
|
const maxDist = mask.baseboardMaxColorDist;
|
||||||
|
cachedContext = {
|
||||||
|
baseboardMaxColorDistSq: maxDist * maxDist,
|
||||||
|
semanticRgb,
|
||||||
|
baseboardRgb,
|
||||||
|
cabinetRgb,
|
||||||
|
wallRgb,
|
||||||
|
};
|
||||||
|
contextRevision = revision;
|
||||||
|
return cachedContext;
|
||||||
|
}
|
||||||
|
function colorDistanceSq(a, b) {
|
||||||
|
const dr = a.r - b.r;
|
||||||
|
const dg = a.g - b.g;
|
||||||
|
const db = a.b - b.b;
|
||||||
|
return dr * dr + dg * dg + db * db;
|
||||||
|
}
|
||||||
|
/** 将掩码像素归类到最近的语义色(baseboard 仅严格橙色命中) */
|
||||||
|
export function classifyBgrPixelToSemantic(b, g, r) {
|
||||||
|
const ctx = getSemanticContext();
|
||||||
|
const pixel = { r, g, b };
|
||||||
|
const distToBaseboard = colorDistanceSq(pixel, ctx.baseboardRgb.rgb);
|
||||||
|
const distToCabinet = colorDistanceSq(pixel, ctx.cabinetRgb.rgb);
|
||||||
|
const distToWall = colorDistanceSq(pixel, ctx.wallRgb.rgb);
|
||||||
|
if (distToBaseboard <= ctx.baseboardMaxColorDistSq &&
|
||||||
|
distToBaseboard < distToCabinet &&
|
||||||
|
distToBaseboard < distToWall) {
|
||||||
|
return BASEBOARD_SEMANTIC_NAME;
|
||||||
|
}
|
||||||
|
let best = ctx.semanticRgb[0];
|
||||||
|
let bestDist = Number.POSITIVE_INFINITY;
|
||||||
|
for (const entry of ctx.semanticRgb) {
|
||||||
|
if (entry.name === BASEBOARD_SEMANTIC_NAME) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const dist = colorDistanceSq(pixel, entry.rgb);
|
||||||
|
if (dist < bestDist) {
|
||||||
|
bestDist = dist;
|
||||||
|
best = entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best.name;
|
||||||
|
}
|
||||||
|
export function getSemanticColorByName(name) {
|
||||||
|
const colors = getMaskSegmentRuntimeConfig().mask.semanticColors;
|
||||||
|
return colors.find(entry => entry.name === name);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 踢脚线须更接近 #F58231 且明显优于黄柜 / 蓝墙,避免整块黄区被误判。
|
||||||
|
*/
|
||||||
|
export function isStrictBaseboardPixel(b, g, r) {
|
||||||
|
const ctx = getSemanticContext();
|
||||||
|
const pixel = { r, g, b };
|
||||||
|
const distToBaseboard = colorDistanceSq(pixel, ctx.baseboardRgb.rgb);
|
||||||
|
const distToCabinet = colorDistanceSq(pixel, ctx.cabinetRgb.rgb);
|
||||||
|
const distToWall = colorDistanceSq(pixel, ctx.wallRgb.rgb);
|
||||||
|
return (distToBaseboard <= ctx.baseboardMaxColorDistSq &&
|
||||||
|
distToBaseboard < distToCabinet &&
|
||||||
|
distToBaseboard < distToWall);
|
||||||
|
}
|
||||||
|
export function isBaseboardPixel(b, g, r) {
|
||||||
|
return isStrictBaseboardPixel(b, g, r);
|
||||||
|
}
|
||||||
|
/** 掩码上墙/柜交界细条的量化色 */
|
||||||
|
export const BASEBOARD_STRIP_QUANT_KEYS = new Set(['0,255,255', '64,255,255']);
|
||||||
|
/** 掩码上墙面量化色 */
|
||||||
|
export const WALL_QUANT_KEYS = new Set([
|
||||||
|
'192,128,64',
|
||||||
|
'192,64,64',
|
||||||
|
'128,64,64',
|
||||||
|
'192,192,128',
|
||||||
|
'128,128,64',
|
||||||
|
]);
|
||||||
|
/** 掩码上柜/地面量化色 */
|
||||||
|
export const CABINET_QUANT_KEYS = new Set([
|
||||||
|
'0,192,255',
|
||||||
|
'64,192,255',
|
||||||
|
'128,192,255',
|
||||||
|
]);
|
||||||
|
export function getBaseboardStripQuantKeys() {
|
||||||
|
return getMaskSegmentRuntimeConfig().mask.baseboardStripQuantKeys;
|
||||||
|
}
|
||||||
|
export function getWallQuantKeys() {
|
||||||
|
return getMaskSegmentRuntimeConfig().mask.wallQuantKeys;
|
||||||
|
}
|
||||||
|
export function getCabinetQuantKeys() {
|
||||||
|
return getMaskSegmentRuntimeConfig().mask.cabinetQuantKeys;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=maskSemanticPalette.js.map
|
||||||
1
dist/utils/maskSemanticPalette.js.map
vendored
Normal file
1
dist/utils/maskSemanticPalette.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
116
dist/utils/opencvAdapter.d.ts
vendored
Normal file
116
dist/utils/opencvAdapter.d.ts
vendored
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { DataTypes, ColorConversionCodes, ThresholdTypes, MorphShapes, MorphTypes, RetrievalModes, ContourApproximationModes, InterpolationFlags, type Mat, type PointVector } from 'react-native-fast-opencv';
|
||||||
|
import type { SkImage } from '@shopify/react-native-skia';
|
||||||
|
type Point = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
type BBox = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
type BgrColor = {
|
||||||
|
b: number;
|
||||||
|
g: number;
|
||||||
|
r: number;
|
||||||
|
};
|
||||||
|
export declare class WrappedMat {
|
||||||
|
readonly mat: Mat;
|
||||||
|
cols: number;
|
||||||
|
rows: number;
|
||||||
|
readonly channels: number;
|
||||||
|
constructor(mat: Mat, cols: number, rows: number, channels?: number);
|
||||||
|
release(): void;
|
||||||
|
clone(): Promise<WrappedMat>;
|
||||||
|
}
|
||||||
|
export declare class ContourWrapper {
|
||||||
|
readonly pointVector: PointVector;
|
||||||
|
constructor(pointVector: PointVector);
|
||||||
|
release(): void;
|
||||||
|
}
|
||||||
|
declare const cv: {
|
||||||
|
IMREAD_GRAYSCALE: number;
|
||||||
|
THRESH_BINARY: ThresholdTypes;
|
||||||
|
MORPH_RECT: MorphShapes;
|
||||||
|
MORPH_ELLIPSE: MorphShapes;
|
||||||
|
MORPH_OPEN: MorphTypes;
|
||||||
|
MORPH_CLOSE: MorphTypes;
|
||||||
|
RETR_EXTERNAL: RetrievalModes;
|
||||||
|
CHAIN_APPROX_SIMPLE: ContourApproximationModes;
|
||||||
|
CHAIN_APPROX_NONE: ContourApproximationModes;
|
||||||
|
COLOR_BGR2GRAY: ColorConversionCodes;
|
||||||
|
COLOR_BGR2Lab: ColorConversionCodes;
|
||||||
|
COLOR_Lab2BGR: ColorConversionCodes;
|
||||||
|
COLOR_GRAY2BGR: ColorConversionCodes;
|
||||||
|
CV_8UC1: DataTypes;
|
||||||
|
CV_16SC1: DataTypes;
|
||||||
|
INTER_LINEAR: InterpolationFlags;
|
||||||
|
INTER_NEAREST: InterpolationFlags;
|
||||||
|
ensurePngPath(path: string, cacheFileName?: string): Promise<string>;
|
||||||
|
imread(path: string, flags?: number): Promise<WrappedMat>;
|
||||||
|
createMat(cols: number, rows: number, channels?: 1 | 3 | 4): WrappedMat;
|
||||||
|
cvtColor(src: WrappedMat, code: ColorConversionCodes): Promise<WrappedMat>;
|
||||||
|
/** 三通道色彩空间转换(BGR/Lab 等) */
|
||||||
|
cvtColorBgr(src: WrappedMat, code: ColorConversionCodes): WrappedMat;
|
||||||
|
/** 灰度 Mat → 三通道 BGR(供 Skia 显示) */
|
||||||
|
grayToBgr(src: WrappedMat): WrappedMat;
|
||||||
|
/** 掩码统一为三通道 BGR;已是 3 通道则原样返回,色序由分割侧 swapBr 检测 */
|
||||||
|
ensureBgr3(src: WrappedMat): Promise<WrappedMat>;
|
||||||
|
/** JS 二值缓冲(0/255)→ 单通道 Mat */
|
||||||
|
binaryBufferToMat(buffer: Uint8Array, cols: number, rows: number): WrappedMat;
|
||||||
|
/** 连续 BGR 缓冲 → 三通道 Mat */
|
||||||
|
bgrBufferToMat(buffer: Uint8Array, cols: number, rows: number): WrappedMat;
|
||||||
|
/** 将 JS 侧生成的灰度二值图写入临时 PGM 并读回 Mat */
|
||||||
|
grayBufferToMat(gray: Uint8Array, cols: number, rows: number): Promise<WrappedMat>;
|
||||||
|
/**
|
||||||
|
* 导出 Mat 像素。先 clone 保证内存连续,避免原生 matToBuffer 忽略 step 导致行错位。
|
||||||
|
*/
|
||||||
|
matToBuffer(src: WrappedMat): {
|
||||||
|
buffer: Uint8Array;
|
||||||
|
cols: number;
|
||||||
|
rows: number;
|
||||||
|
channels: number;
|
||||||
|
};
|
||||||
|
inRangeBgr(src: WrappedMat, color: BgrColor, tolerance: number, dst: WrappedMat): Promise<void>;
|
||||||
|
resize(src: WrappedMat, dst: WrappedMat, size: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}, interpolation?: InterpolationFlags): Promise<void>;
|
||||||
|
/** BGR 缓冲原生缩放(掩码用语义色,默认最近邻) */
|
||||||
|
resizeBgrBuffer(buffer: Uint8Array, srcCols: number, srcRows: number, dstCols: number, dstRows: number, interpolation?: InterpolationFlags): Promise<Uint8Array>;
|
||||||
|
/** BGR Mat → RGBA 连续缓冲(供 Skia 直传) */
|
||||||
|
matToRgbaBuffer(src: WrappedMat): Promise<{
|
||||||
|
buffer: Uint8Array;
|
||||||
|
cols: number;
|
||||||
|
rows: number;
|
||||||
|
}>;
|
||||||
|
/** BGR Mat → Skia 图像(跳过低频/高频 PNG 编码) */
|
||||||
|
matToSkiaImage(src: WrappedMat): Promise<SkImage | null>;
|
||||||
|
/** 单通道灰度 Mat → Skia RGBA(跳过 BGR 伪彩 + 四通道 matToBuffer) */
|
||||||
|
grayMatToSkiaImage(src: WrappedMat): SkImage | null;
|
||||||
|
/** 连续 BGR 缓冲 → Skia 图像(工作分辨率原图 / 高低频,复用 OpenCV 解码结果) */
|
||||||
|
bgrBufferToSkiaImage(buffer: Uint8Array, cols: number, rows: number): Promise<SkImage | null>;
|
||||||
|
threshold(src: WrappedMat, dst: WrappedMat, thresh: number, maxval: number, type: number): Promise<void>;
|
||||||
|
getStructuringElement(shape: MorphShapes, ksize: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}): Promise<WrappedMat>;
|
||||||
|
morphologyEx(src: WrappedMat, dst: WrappedMat, op: MorphTypes, kernel: WrappedMat): Promise<void>;
|
||||||
|
findContours(image: WrappedMat, mode: RetrievalModes, method: ContourApproximationModes): Promise<ContourWrapper[]>;
|
||||||
|
contourArea(contour: ContourWrapper): Promise<number>;
|
||||||
|
boundingRect(contour: ContourWrapper): Promise<BBox>;
|
||||||
|
arcLength(contour: ContourWrapper, closed: boolean): Promise<number>;
|
||||||
|
approxPolyDP(contour: ContourWrapper, epsilon: number, closed: boolean): Promise<Point[]>;
|
||||||
|
GaussianBlur(src: WrappedMat, dst: WrappedMat, ksize: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}, sigma: number): Promise<void>;
|
||||||
|
extractChannel(src: WrappedMat, dst: WrappedMat, channel: number): void;
|
||||||
|
convertTo(src: WrappedMat, dst: WrappedMat, rtype: number, alpha?: number, beta?: number): void;
|
||||||
|
subtract(src1: WrappedMat, src2: WrappedMat, dst: WrappedMat): Promise<void>;
|
||||||
|
addWeighted(src1: WrappedMat, alpha: number, src2: WrappedMat | null, beta: number, gamma: number, dst: WrappedMat): Promise<void>;
|
||||||
|
imwrite(path: string, mat: WrappedMat): Promise<void>;
|
||||||
|
};
|
||||||
|
export default cv;
|
||||||
|
//# sourceMappingURL=opencvAdapter.d.ts.map
|
||||||
1
dist/utils/opencvAdapter.d.ts.map
vendored
Normal file
1
dist/utils/opencvAdapter.d.ts.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"opencvAdapter.d.ts","sourceRoot":"","sources":["../../src/utils/opencvAdapter.ts"],"names":[],"mappings":"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"}
|
||||||
353
dist/utils/opencvAdapter.js
vendored
Normal file
353
dist/utils/opencvAdapter.js
vendored
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
/**
|
||||||
|
* OpenCV 便捷适配层
|
||||||
|
* 将 react-native-fast-opencv 的 invoke API 封装为 MaskSegmentCanvas 所需的 async 风格接口
|
||||||
|
*/
|
||||||
|
import RNFS from 'react-native-fs';
|
||||||
|
import { OpenCV, ObjectType, DataTypes, ColorConversionCodes, ThresholdTypes, MorphShapes, MorphTypes, RetrievalModes, ContourApproximationModes, InterpolationFlags, } from 'react-native-fast-opencv';
|
||||||
|
import { PNG_COMPRESSION, ensureMat8U, ensurePngFile, isPngPath, normalizePath, pngCacheName, readPngBgrBuffer, readPngHeaderFromBase64, } from './pngImage';
|
||||||
|
import { rgbaBufferToSkiaImage } from './skiaImage';
|
||||||
|
const IMREAD_GRAYSCALE = 0;
|
||||||
|
export class WrappedMat {
|
||||||
|
mat;
|
||||||
|
cols;
|
||||||
|
rows;
|
||||||
|
channels;
|
||||||
|
constructor(mat, cols, rows, channels = 3) {
|
||||||
|
this.mat = mat;
|
||||||
|
this.cols = cols;
|
||||||
|
this.rows = rows;
|
||||||
|
this.channels = channels;
|
||||||
|
}
|
||||||
|
release() {
|
||||||
|
OpenCV.releaseBuffers([this.mat.id]);
|
||||||
|
}
|
||||||
|
async clone() {
|
||||||
|
const cloned = OpenCV.invoke('clone', this.mat);
|
||||||
|
return new WrappedMat(cloned, this.cols, this.rows, this.channels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class ContourWrapper {
|
||||||
|
pointVector;
|
||||||
|
constructor(pointVector) {
|
||||||
|
this.pointVector = pointVector;
|
||||||
|
}
|
||||||
|
release() {
|
||||||
|
OpenCV.releaseBuffers([this.pointVector.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function createScalar(a, b, c) {
|
||||||
|
if (b === undefined) {
|
||||||
|
return OpenCV.createObject(ObjectType.Scalar, a);
|
||||||
|
}
|
||||||
|
if (c === undefined) {
|
||||||
|
return OpenCV.createObject(ObjectType.Scalar, a, b, 0);
|
||||||
|
}
|
||||||
|
return OpenCV.createObject(ObjectType.Scalar, a, b, c);
|
||||||
|
}
|
||||||
|
async function readImageFromPath(path, grayscale = false) {
|
||||||
|
const filePath = normalizePath(path);
|
||||||
|
const base64 = await RNFS.readFile(filePath, 'base64');
|
||||||
|
const srcMat = OpenCV.base64ToMat(base64);
|
||||||
|
// 16-bit PNG 降级: toJSValue 可能崩溃,先解析 PNG 头
|
||||||
|
let pngHeader;
|
||||||
|
try {
|
||||||
|
pngHeader = readPngHeaderFromBase64(base64);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// 非 PNG 不传
|
||||||
|
}
|
||||||
|
const { mat, extraReleaseIds } = ensureMat8U(srcMat, pngHeader);
|
||||||
|
const info = OpenCV.toJSValue(mat);
|
||||||
|
if (grayscale) {
|
||||||
|
const gray = OpenCV.createObject(ObjectType.Mat, info.rows, info.cols, DataTypes.CV_8UC1);
|
||||||
|
OpenCV.invoke('cvtColor', mat, gray, ColorConversionCodes.COLOR_BGR2GRAY);
|
||||||
|
OpenCV.releaseBuffers([
|
||||||
|
...new Set([srcMat.id, mat.id, ...extraReleaseIds]),
|
||||||
|
]);
|
||||||
|
return new WrappedMat(gray, info.cols, info.rows, 1);
|
||||||
|
}
|
||||||
|
const channels = info.type === DataTypes.CV_8UC1
|
||||||
|
? 1
|
||||||
|
: info.type === DataTypes.CV_8UC4
|
||||||
|
? 4
|
||||||
|
: 3;
|
||||||
|
if (mat.id !== srcMat.id) {
|
||||||
|
OpenCV.releaseBuffers([srcMat.id]);
|
||||||
|
}
|
||||||
|
return new WrappedMat(mat, info.cols, info.rows, channels);
|
||||||
|
}
|
||||||
|
function createSize(width, height) {
|
||||||
|
return OpenCV.createObject(ObjectType.Size, width, height);
|
||||||
|
}
|
||||||
|
/** OpenCV GaussianBlur 要求核宽高为正奇数 */
|
||||||
|
function normalizeGaussianKernel(size) {
|
||||||
|
const n = Math.max(1, Math.round(size));
|
||||||
|
return n % 2 === 0 ? n + 1 : n;
|
||||||
|
}
|
||||||
|
const cv = {
|
||||||
|
IMREAD_GRAYSCALE,
|
||||||
|
THRESH_BINARY: ThresholdTypes.THRESH_BINARY,
|
||||||
|
MORPH_RECT: MorphShapes.MORPH_RECT,
|
||||||
|
MORPH_ELLIPSE: MorphShapes.MORPH_ELLIPSE,
|
||||||
|
MORPH_OPEN: MorphTypes.MORPH_OPEN,
|
||||||
|
MORPH_CLOSE: MorphTypes.MORPH_CLOSE,
|
||||||
|
RETR_EXTERNAL: RetrievalModes.RETR_EXTERNAL,
|
||||||
|
CHAIN_APPROX_SIMPLE: ContourApproximationModes.CHAIN_APPROX_SIMPLE,
|
||||||
|
CHAIN_APPROX_NONE: ContourApproximationModes.CHAIN_APPROX_NONE,
|
||||||
|
COLOR_BGR2GRAY: ColorConversionCodes.COLOR_BGR2GRAY,
|
||||||
|
COLOR_BGR2Lab: ColorConversionCodes.COLOR_BGR2Lab,
|
||||||
|
COLOR_Lab2BGR: ColorConversionCodes.COLOR_Lab2BGR,
|
||||||
|
COLOR_GRAY2BGR: ColorConversionCodes.COLOR_GRAY2BGR,
|
||||||
|
CV_8UC1: DataTypes.CV_8UC1,
|
||||||
|
CV_16SC1: DataTypes.CV_16SC1,
|
||||||
|
INTER_LINEAR: InterpolationFlags.INTER_LINEAR,
|
||||||
|
INTER_NEAREST: InterpolationFlags.INTER_NEAREST,
|
||||||
|
async ensurePngPath(path, cacheFileName) {
|
||||||
|
const name = cacheFileName ?? pngCacheName(path, 'img');
|
||||||
|
return ensurePngFile(path, name);
|
||||||
|
},
|
||||||
|
async imread(path, flags) {
|
||||||
|
const filePath = normalizePath(path);
|
||||||
|
if (isPngPath(filePath) && (await RNFS.exists(filePath))) {
|
||||||
|
if (flags === IMREAD_GRAYSCALE) {
|
||||||
|
return readImageFromPath(filePath, true);
|
||||||
|
}
|
||||||
|
const { buffer, cols, rows } = await readPngBgrBuffer(filePath);
|
||||||
|
return cv.bgrBufferToMat(buffer, cols, rows);
|
||||||
|
}
|
||||||
|
const pngPath = await ensurePngFile(path, pngCacheName(path, 'imread'));
|
||||||
|
if (flags === IMREAD_GRAYSCALE) {
|
||||||
|
return readImageFromPath(pngPath, true);
|
||||||
|
}
|
||||||
|
const { buffer, cols, rows } = await readPngBgrBuffer(pngPath);
|
||||||
|
return cv.bgrBufferToMat(buffer, cols, rows);
|
||||||
|
},
|
||||||
|
createMat(cols, rows, channels = 1) {
|
||||||
|
const type = channels === 1
|
||||||
|
? DataTypes.CV_8UC1
|
||||||
|
: channels === 3
|
||||||
|
? DataTypes.CV_8UC3
|
||||||
|
: DataTypes.CV_8UC4;
|
||||||
|
const mat = OpenCV.createObject(ObjectType.Mat, rows, cols, type);
|
||||||
|
return new WrappedMat(mat, cols, rows, channels);
|
||||||
|
},
|
||||||
|
async cvtColor(src, code) {
|
||||||
|
const dst = cv.createMat(src.cols, src.rows, 1);
|
||||||
|
OpenCV.invoke('cvtColor', src.mat, dst.mat, code);
|
||||||
|
return dst;
|
||||||
|
},
|
||||||
|
/** 三通道色彩空间转换(BGR/Lab 等) */
|
||||||
|
cvtColorBgr(src, code) {
|
||||||
|
const dst = cv.createMat(src.cols, src.rows, 3);
|
||||||
|
OpenCV.invoke('cvtColor', src.mat, dst.mat, code);
|
||||||
|
return dst;
|
||||||
|
},
|
||||||
|
/** 灰度 Mat → 三通道 BGR(供 Skia 显示) */
|
||||||
|
grayToBgr(src) {
|
||||||
|
const dst = cv.createMat(src.cols, src.rows, 3);
|
||||||
|
OpenCV.invoke('cvtColor', src.mat, dst.mat, ColorConversionCodes.COLOR_GRAY2BGR);
|
||||||
|
return dst;
|
||||||
|
},
|
||||||
|
/** 掩码统一为三通道 BGR;已是 3 通道则原样返回,色序由分割侧 swapBr 检测 */
|
||||||
|
async ensureBgr3(src) {
|
||||||
|
if (src.channels === 3) {
|
||||||
|
return src;
|
||||||
|
}
|
||||||
|
const dst = cv.createMat(src.cols, src.rows, 3);
|
||||||
|
const code = src.channels === 4
|
||||||
|
? ColorConversionCodes.COLOR_BGRA2BGR
|
||||||
|
: ColorConversionCodes.COLOR_GRAY2BGR;
|
||||||
|
OpenCV.invoke('cvtColor', src.mat, dst.mat, code);
|
||||||
|
return dst;
|
||||||
|
},
|
||||||
|
/** JS 二值缓冲(0/255)→ 单通道 Mat */
|
||||||
|
binaryBufferToMat(buffer, cols, rows) {
|
||||||
|
const mat = OpenCV.bufferToMat('uint8', rows, cols, 1, buffer);
|
||||||
|
return new WrappedMat(mat, cols, rows, 1);
|
||||||
|
},
|
||||||
|
/** 连续 BGR 缓冲 → 三通道 Mat */
|
||||||
|
bgrBufferToMat(buffer, cols, rows) {
|
||||||
|
const mat = OpenCV.bufferToMat('uint8', rows, cols, 3, buffer);
|
||||||
|
return new WrappedMat(mat, cols, rows, 3);
|
||||||
|
},
|
||||||
|
/** 将 JS 侧生成的灰度二值图写入临时 PGM 并读回 Mat */
|
||||||
|
async grayBufferToMat(gray, cols, rows) {
|
||||||
|
const path = `${RNFS.CachesDirectoryPath}/seg_bin_${Date.now()}.pgm`;
|
||||||
|
const header = `P5\n${cols} ${rows}\n255\n`;
|
||||||
|
const headerBytes = new TextEncoder().encode(header);
|
||||||
|
const fileBytes = new Uint8Array(headerBytes.length + gray.length);
|
||||||
|
fileBytes.set(headerBytes, 0);
|
||||||
|
fileBytes.set(gray, headerBytes.length);
|
||||||
|
let binary = '';
|
||||||
|
const chunkSize = 8192;
|
||||||
|
for (let i = 0; i < fileBytes.length; i += chunkSize) {
|
||||||
|
const slice = fileBytes.subarray(i, i + chunkSize);
|
||||||
|
binary += String.fromCharCode(...slice);
|
||||||
|
}
|
||||||
|
await RNFS.writeFile(path, btoa(binary), 'base64');
|
||||||
|
try {
|
||||||
|
return readImageFromPath(path, true);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (await RNFS.exists(path)) {
|
||||||
|
await RNFS.unlink(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 导出 Mat 像素。先 clone 保证内存连续,避免原生 matToBuffer 忽略 step 导致行错位。
|
||||||
|
*/
|
||||||
|
matToBuffer(src) {
|
||||||
|
const continuous = OpenCV.invoke('clone', src.mat);
|
||||||
|
try {
|
||||||
|
const { buffer, cols, rows, channels } = OpenCV.matToBuffer(continuous, 'uint8');
|
||||||
|
return { buffer, cols, rows, channels };
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
OpenCV.releaseBuffers([continuous.id]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async inRangeBgr(src, color, tolerance, dst) {
|
||||||
|
const lower = createScalar(Math.max(0, color.b - tolerance), Math.max(0, color.g - tolerance), Math.max(0, color.r - tolerance));
|
||||||
|
const upper = createScalar(Math.min(255, color.b + tolerance), Math.min(255, color.g + tolerance), Math.min(255, color.r + tolerance));
|
||||||
|
OpenCV.invoke('inRange', src.mat, lower, upper, dst.mat);
|
||||||
|
},
|
||||||
|
async resize(src, dst, size, interpolation = InterpolationFlags.INTER_LINEAR) {
|
||||||
|
const dsize = createSize(size.width, size.height);
|
||||||
|
OpenCV.invoke('resize', src.mat, dst.mat, dsize, 0, 0, interpolation);
|
||||||
|
dst.cols = size.width;
|
||||||
|
dst.rows = size.height;
|
||||||
|
},
|
||||||
|
/** BGR 缓冲原生缩放(掩码用语义色,默认最近邻) */
|
||||||
|
async resizeBgrBuffer(buffer, srcCols, srcRows, dstCols, dstRows, interpolation = InterpolationFlags.INTER_NEAREST) {
|
||||||
|
if (srcCols === dstCols && srcRows === dstRows) {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
const srcMat = cv.bgrBufferToMat(buffer, srcCols, srcRows);
|
||||||
|
const dstMat = cv.createMat(dstCols, dstRows, 3);
|
||||||
|
try {
|
||||||
|
await cv.resize(srcMat, dstMat, { width: dstCols, height: dstRows }, interpolation);
|
||||||
|
return cv.matToBuffer(dstMat).buffer;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
srcMat.release();
|
||||||
|
dstMat.release();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** BGR Mat → RGBA 连续缓冲(供 Skia 直传) */
|
||||||
|
async matToRgbaBuffer(src) {
|
||||||
|
const dst = cv.createMat(src.cols, src.rows, 4);
|
||||||
|
try {
|
||||||
|
OpenCV.invoke('cvtColor', src.mat, dst.mat, ColorConversionCodes.COLOR_BGR2RGBA);
|
||||||
|
const { buffer, cols, rows } = cv.matToBuffer(dst);
|
||||||
|
return { buffer, cols, rows };
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
dst.release();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** BGR Mat → Skia 图像(跳过低频/高频 PNG 编码) */
|
||||||
|
async matToSkiaImage(src) {
|
||||||
|
const { buffer, cols, rows } = await cv.matToRgbaBuffer(src);
|
||||||
|
return rgbaBufferToSkiaImage(buffer, cols, rows);
|
||||||
|
},
|
||||||
|
/** 单通道灰度 Mat → Skia RGBA(跳过 BGR 伪彩 + 四通道 matToBuffer) */
|
||||||
|
grayMatToSkiaImage(src) {
|
||||||
|
const { buffer, cols, rows } = cv.matToBuffer(src);
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
const rgba = new Uint8Array(pixelCount * 4);
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
const value = buffer[i];
|
||||||
|
const offset = i * 4;
|
||||||
|
rgba[offset] = value;
|
||||||
|
rgba[offset + 1] = value;
|
||||||
|
rgba[offset + 2] = value;
|
||||||
|
rgba[offset + 3] = 255;
|
||||||
|
}
|
||||||
|
return rgbaBufferToSkiaImage(rgba, cols, rows);
|
||||||
|
},
|
||||||
|
/** 连续 BGR 缓冲 → Skia 图像(工作分辨率原图 / 高低频,复用 OpenCV 解码结果) */
|
||||||
|
async bgrBufferToSkiaImage(buffer, cols, rows) {
|
||||||
|
const mat = cv.bgrBufferToMat(buffer, cols, rows);
|
||||||
|
try {
|
||||||
|
return await cv.matToSkiaImage(mat);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
mat.release();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async threshold(src, dst, thresh, maxval, type) {
|
||||||
|
OpenCV.invoke('threshold', src.mat, dst.mat, thresh, maxval, type);
|
||||||
|
},
|
||||||
|
async getStructuringElement(shape, ksize) {
|
||||||
|
const size = createSize(ksize.width, ksize.height);
|
||||||
|
const kernel = OpenCV.invoke('getStructuringElement', shape, size);
|
||||||
|
return new WrappedMat(kernel, ksize.width, ksize.height, 1);
|
||||||
|
},
|
||||||
|
async morphologyEx(src, dst, op, kernel) {
|
||||||
|
OpenCV.invoke('morphologyEx', src.mat, dst.mat, op, kernel.mat);
|
||||||
|
},
|
||||||
|
async findContours(image, mode, method) {
|
||||||
|
const contours = OpenCV.createObject(ObjectType.PointVectorOfVectors);
|
||||||
|
OpenCV.invoke('findContours', image.mat, contours, mode, method);
|
||||||
|
const data = OpenCV.toJSValue(contours);
|
||||||
|
const wrappers = data.array.map((_, index) => {
|
||||||
|
const pv = OpenCV.copyObjectFromVector(contours, index);
|
||||||
|
return new ContourWrapper(pv);
|
||||||
|
});
|
||||||
|
OpenCV.releaseBuffers([contours.id]);
|
||||||
|
return wrappers;
|
||||||
|
},
|
||||||
|
async contourArea(contour) {
|
||||||
|
const result = OpenCV.invoke('contourArea', contour.pointVector, false);
|
||||||
|
return result.value;
|
||||||
|
},
|
||||||
|
async boundingRect(contour) {
|
||||||
|
const rect = OpenCV.invoke('boundingRect', contour.pointVector);
|
||||||
|
const js = OpenCV.toJSValue(rect);
|
||||||
|
return { x: js.x, y: js.y, width: js.width, height: js.height };
|
||||||
|
},
|
||||||
|
async arcLength(contour, closed) {
|
||||||
|
const result = OpenCV.invoke('arcLength', contour.pointVector, closed);
|
||||||
|
return result.value;
|
||||||
|
},
|
||||||
|
async approxPolyDP(contour, epsilon, closed) {
|
||||||
|
const approx = OpenCV.createObject(ObjectType.PointVector);
|
||||||
|
try {
|
||||||
|
OpenCV.invoke('approxPolyDP', contour.pointVector, approx, epsilon, closed);
|
||||||
|
return OpenCV.toJSValue(approx).array;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
OpenCV.releaseBuffers([approx.id]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async GaussianBlur(src, dst, ksize, sigma) {
|
||||||
|
const width = normalizeGaussianKernel(ksize.width);
|
||||||
|
const height = normalizeGaussianKernel(ksize.height);
|
||||||
|
const size = createSize(width, height);
|
||||||
|
OpenCV.invoke('GaussianBlur', src.mat, dst.mat, size, sigma);
|
||||||
|
},
|
||||||
|
extractChannel(src, dst, channel) {
|
||||||
|
OpenCV.invoke('extractChannel', src.mat, dst.mat, channel);
|
||||||
|
},
|
||||||
|
convertTo(src, dst, rtype, alpha = 1, beta = 0) {
|
||||||
|
OpenCV.invoke('convertTo', src.mat, dst.mat, rtype, alpha, beta);
|
||||||
|
},
|
||||||
|
async subtract(src1, src2, dst) {
|
||||||
|
OpenCV.invoke('subtract', src1.mat, src2.mat, dst.mat);
|
||||||
|
},
|
||||||
|
async addWeighted(src1, alpha, src2, beta, gamma, dst) {
|
||||||
|
if (src2) {
|
||||||
|
OpenCV.invoke('addWeighted', src1.mat, alpha, src2.mat, beta, gamma, dst.mat);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
OpenCV.invoke('addWeighted', src1.mat, alpha, src1.mat, 0, gamma, dst.mat);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async imwrite(path, mat) {
|
||||||
|
const filePath = normalizePath(path);
|
||||||
|
OpenCV.saveMatToFile(mat.mat, filePath, 'png', PNG_COMPRESSION);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default cv;
|
||||||
|
//# sourceMappingURL=opencvAdapter.js.map
|
||||||
1
dist/utils/opencvAdapter.js.map
vendored
Normal file
1
dist/utils/opencvAdapter.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
5
dist/utils/paintColorMapTexture.d.ts
vendored
Normal file
5
dist/utils/paintColorMapTexture.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { type SkImage } from '@shopify/react-native-skia';
|
||||||
|
import type { BgrColor } from '../components/MaskSegmentCanvas.types';
|
||||||
|
/** 按 pickMap 展开的上色颜色图(与 pick 同尺寸,未上色像素 a=0)。支持 maskFeather 产生软边缘 alpha。 */
|
||||||
|
export declare function buildPaintColorMapImage(pickBuffer: Uint8Array, cols: number, rows: number, paintedRegions: Map<number, BgrColor>, featherRadius?: number): SkImage;
|
||||||
|
//# sourceMappingURL=paintColorMapTexture.d.ts.map
|
||||||
1
dist/utils/paintColorMapTexture.d.ts.map
vendored
Normal file
1
dist/utils/paintColorMapTexture.d.ts.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"paintColorMapTexture.d.ts","sourceRoot":"","sources":["../../src/utils/paintColorMapTexture.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,OAAO,EACb,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uCAAuC,CAAC;AA4ItE,2EAA2E;AAC3E,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,UAAU,EACtB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,EACrC,aAAa,SAAI,GAChB,OAAO,CAqFT"}
|
||||||
203
dist/utils/paintColorMapTexture.js
vendored
Normal file
203
dist/utils/paintColorMapTexture.js
vendored
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
import { Skia, AlphaType, ColorType, } from '@shopify/react-native-skia';
|
||||||
|
/**
|
||||||
|
* Separable box blur on premultiplied RGBA (4-channel 0-255).
|
||||||
|
* Feather radius in pixels (at the working pick buffer resolution).
|
||||||
|
* Works on premultiplied values so that color naturally diffuses alongside alpha
|
||||||
|
* into the feather region — preventing "scatter dots" at resolution-mismatched boundaries.
|
||||||
|
*/
|
||||||
|
function boxBlurRgbaPremul(rgba, cols, rows, radius) {
|
||||||
|
const r = Math.max(1, Math.min(16, Math.round(radius)));
|
||||||
|
const tmp = new Uint8Array(cols * rows * 4);
|
||||||
|
const dst = new Uint8Array(cols * rows * 4);
|
||||||
|
// Horizontal pass
|
||||||
|
for (let y = 0; y < rows; y++) {
|
||||||
|
for (let x = 0; x < cols; x++) {
|
||||||
|
let sumR = 0, sumG = 0, sumB = 0, sumA = 0;
|
||||||
|
let cnt = 0;
|
||||||
|
const base = y * cols * 4;
|
||||||
|
for (let dx = -r; dx <= r; dx++) {
|
||||||
|
const xx = x + dx;
|
||||||
|
if (xx >= 0 && xx < cols) {
|
||||||
|
const idx = base + xx * 4;
|
||||||
|
sumR += rgba[idx];
|
||||||
|
sumG += rgba[idx + 1];
|
||||||
|
sumB += rgba[idx + 2];
|
||||||
|
sumA += rgba[idx + 3];
|
||||||
|
cnt++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const o = base + x * 4;
|
||||||
|
tmp[o] = Math.round(sumR / cnt);
|
||||||
|
tmp[o + 1] = Math.round(sumG / cnt);
|
||||||
|
tmp[o + 2] = Math.round(sumB / cnt);
|
||||||
|
tmp[o + 3] = Math.round(sumA / cnt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Vertical pass
|
||||||
|
for (let x = 0; x < cols; x++) {
|
||||||
|
for (let y = 0; y < rows; y++) {
|
||||||
|
let sumR = 0, sumG = 0, sumB = 0, sumA = 0;
|
||||||
|
let cnt = 0;
|
||||||
|
for (let dy = -r; dy <= r; dy++) {
|
||||||
|
const yy = y + dy;
|
||||||
|
if (yy >= 0 && yy < rows) {
|
||||||
|
const idx = yy * cols * 4 + x * 4;
|
||||||
|
sumR += tmp[idx];
|
||||||
|
sumG += tmp[idx + 1];
|
||||||
|
sumB += tmp[idx + 2];
|
||||||
|
sumA += tmp[idx + 3];
|
||||||
|
cnt++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const o = y * cols * 4 + x * 4;
|
||||||
|
dst[o] = Math.round(sumR / cnt);
|
||||||
|
dst[o + 1] = Math.round(sumG / cnt);
|
||||||
|
dst[o + 2] = Math.round(sumB / cnt);
|
||||||
|
dst[o + 3] = Math.round(sumA / cnt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Separable dilation (max filter) on premultiplied RGBA.
|
||||||
|
* Fills small holes (alpha=0) inside painted regions by taking the
|
||||||
|
* per-channel maximum in the neighborhood. Applied after boxBlur to
|
||||||
|
* eliminate scatter dots caused by segmentation gaps in the pick buffer.
|
||||||
|
*/
|
||||||
|
function dilateRgbaPremul(rgba, cols, rows, radius) {
|
||||||
|
const r = Math.max(1, Math.min(8, Math.round(radius)));
|
||||||
|
const tmp = new Uint8Array(cols * rows * 4);
|
||||||
|
const dst = new Uint8Array(cols * rows * 4);
|
||||||
|
// Horizontal pass
|
||||||
|
for (let y = 0; y < rows; y++) {
|
||||||
|
for (let x = 0; x < cols; x++) {
|
||||||
|
let maxR = 0, maxG = 0, maxB = 0, maxA = 0;
|
||||||
|
const base = y * cols * 4;
|
||||||
|
for (let dx = -r; dx <= r; dx++) {
|
||||||
|
const xx = x + dx;
|
||||||
|
if (xx >= 0 && xx < cols) {
|
||||||
|
const idx = base + xx * 4;
|
||||||
|
if (rgba[idx + 3] > maxA) {
|
||||||
|
maxR = rgba[idx];
|
||||||
|
maxG = rgba[idx + 1];
|
||||||
|
maxB = rgba[idx + 2];
|
||||||
|
maxA = rgba[idx + 3];
|
||||||
|
}
|
||||||
|
else if (rgba[idx + 3] === maxA && maxA > 0) {
|
||||||
|
maxR = Math.max(maxR, rgba[idx]);
|
||||||
|
maxG = Math.max(maxG, rgba[idx + 1]);
|
||||||
|
maxB = Math.max(maxB, rgba[idx + 2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const o = base + x * 4;
|
||||||
|
tmp[o] = maxR;
|
||||||
|
tmp[o + 1] = maxG;
|
||||||
|
tmp[o + 2] = maxB;
|
||||||
|
tmp[o + 3] = maxA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Vertical pass
|
||||||
|
for (let x = 0; x < cols; x++) {
|
||||||
|
for (let y = 0; y < rows; y++) {
|
||||||
|
let maxR = 0, maxG = 0, maxB = 0, maxA = 0;
|
||||||
|
for (let dy = -r; dy <= r; dy++) {
|
||||||
|
const yy = y + dy;
|
||||||
|
if (yy >= 0 && yy < rows) {
|
||||||
|
const idx = yy * cols * 4 + x * 4;
|
||||||
|
if (tmp[idx + 3] > maxA) {
|
||||||
|
maxR = tmp[idx];
|
||||||
|
maxG = tmp[idx + 1];
|
||||||
|
maxB = tmp[idx + 2];
|
||||||
|
maxA = tmp[idx + 3];
|
||||||
|
}
|
||||||
|
else if (tmp[idx + 3] === maxA && maxA > 0) {
|
||||||
|
maxR = Math.max(maxR, tmp[idx]);
|
||||||
|
maxG = Math.max(maxG, tmp[idx + 1]);
|
||||||
|
maxB = Math.max(maxB, tmp[idx + 2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const o = y * cols * 4 + x * 4;
|
||||||
|
dst[o] = maxR;
|
||||||
|
dst[o + 1] = maxG;
|
||||||
|
dst[o + 2] = maxB;
|
||||||
|
dst[o + 3] = maxA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
/** 按 pickMap 展开的上色颜色图(与 pick 同尺寸,未上色像素 a=0)。支持 maskFeather 产生软边缘 alpha。 */
|
||||||
|
export function buildPaintColorMapImage(pickBuffer, cols, rows, paintedRegions, featherRadius = 0) {
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
const colorByPickCode = new Map();
|
||||||
|
for (const [regionId, color] of paintedRegions) {
|
||||||
|
colorByPickCode.set(regionId + 1, color);
|
||||||
|
}
|
||||||
|
// ── Perf fast-path: no feather → 1-pass direct unpremul RGBA (O(N)) ──
|
||||||
|
// Skips premul buffer, boxBlur H+V, dilate H+V, and unpremul conversion.
|
||||||
|
// The GPU shader handles boundary noise via unpremultiply (rgb /= a) +
|
||||||
|
// smoothstep gate, so no CPU-side dilate/filter is needed.
|
||||||
|
if (featherRadius <= 0.1 && featherRadius >= -0.1) {
|
||||||
|
const rgba = new Uint8Array(pixelCount * 4);
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
const code = pickBuffer[i];
|
||||||
|
const color = code > 0 ? colorByPickCode.get(code) : undefined;
|
||||||
|
const o = i * 4;
|
||||||
|
if (color) {
|
||||||
|
rgba[o] = color.r;
|
||||||
|
rgba[o + 1] = color.g;
|
||||||
|
rgba[o + 2] = color.b;
|
||||||
|
rgba[o + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const data = Skia.Data.fromBytes(rgba);
|
||||||
|
return Skia.Image.MakeImage({ width: cols, height: rows, alphaType: AlphaType.Unpremul, colorType: ColorType.RGBA_8888 }, data, cols * 4);
|
||||||
|
}
|
||||||
|
// ── Feathered path: premul + boxBlur + dilate + unpremul ──
|
||||||
|
// Build hard premultiplied RGBA: where a pick code maps to a painted color,
|
||||||
|
// store (R, G, B, 255) in premul space. Unpainted pixels stay (0,0,0,0).
|
||||||
|
const hardPremul = new Uint8Array(pixelCount * 4);
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
const code = pickBuffer[i];
|
||||||
|
const color = code > 0 ? colorByPickCode.get(code) : undefined;
|
||||||
|
const o = i * 4;
|
||||||
|
if (color) {
|
||||||
|
hardPremul[o] = color.r;
|
||||||
|
hardPremul[o + 1] = color.g;
|
||||||
|
hardPremul[o + 2] = color.b;
|
||||||
|
hardPremul[o + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Box-blur premultiplied RGBA so color naturally diffuses alongside alpha
|
||||||
|
// into the feather region. This prevents scatter dots from bilinear
|
||||||
|
// interpolation at resolution-mismatched boundaries (where unpainted=black
|
||||||
|
// would otherwise leak into boundary samples).
|
||||||
|
const blurredPremul = boxBlurRgbaPremul(hardPremul, cols, rows, featherRadius);
|
||||||
|
// Dilation pass: fill small holes (alpha=0) inside painted regions that
|
||||||
|
// survive the box blur due to segmentation gaps in the pick buffer.
|
||||||
|
const filledPremul = dilateRgbaPremul(blurredPremul, cols, rows, featherRadius);
|
||||||
|
// Unpremultiply and store as RGBA for the final SkImage.
|
||||||
|
const rgba = new Uint8Array(pixelCount * 4);
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
const o = i * 4;
|
||||||
|
const a = filledPremul[o + 3];
|
||||||
|
if (a > 0) {
|
||||||
|
rgba[o] = Math.min(255, Math.round((filledPremul[o] * 255) / a));
|
||||||
|
rgba[o + 1] = Math.min(255, Math.round((filledPremul[o + 1] * 255) / a));
|
||||||
|
rgba[o + 2] = Math.min(255, Math.round((filledPremul[o + 2] * 255) / a));
|
||||||
|
rgba[o + 3] = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const data = Skia.Data.fromBytes(rgba);
|
||||||
|
return Skia.Image.MakeImage({
|
||||||
|
width: cols,
|
||||||
|
height: rows,
|
||||||
|
// Unpremul: rgb stores the target paint color (unmodulated), alpha stores coverage (soft edge feather).
|
||||||
|
// This ensures ImageShader sampling returns the intended paint color + separate coverage alpha,
|
||||||
|
// without premultiplication assumptions that could cause dark fringes on soft edges.
|
||||||
|
alphaType: AlphaType.Unpremul,
|
||||||
|
colorType: ColorType.RGBA_8888,
|
||||||
|
}, data, cols * 4);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=paintColorMapTexture.js.map
|
||||||
1
dist/utils/paintColorMapTexture.js.map
vendored
Normal file
1
dist/utils/paintColorMapTexture.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
40
dist/utils/paintShaderRuntime.d.ts
vendored
Normal file
40
dist/utils/paintShaderRuntime.d.ts
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { type SkImage, type SkRuntimeEffect } from '@shopify/react-native-skia';
|
||||||
|
import type { BgrColor } from '../components/MaskSegmentCanvas.types';
|
||||||
|
export declare function getRegionPaintEffect(): SkRuntimeEffect;
|
||||||
|
export type PaintShaderTextures = {
|
||||||
|
originImage: SkImage;
|
||||||
|
paintColorMap: SkImage;
|
||||||
|
lowFreqImage: SkImage;
|
||||||
|
highFreqImage: SkImage;
|
||||||
|
};
|
||||||
|
export declare function buildPaintShaderUniforms(showOrigin: boolean): {
|
||||||
|
colorBaseOpacity: number;
|
||||||
|
lLightOpacity: number;
|
||||||
|
textureOpacity: number;
|
||||||
|
showOrigin: number;
|
||||||
|
};
|
||||||
|
export type PaintShaderLayerProps = PaintShaderTextures & {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
showOrigin?: boolean;
|
||||||
|
};
|
||||||
|
/** Canvas 内全屏上色 Shader 层 */
|
||||||
|
export declare function PaintShaderLayer(props: PaintShaderLayerProps): React.JSX.Element;
|
||||||
|
export declare function createPaintColorMapForPaint(pickBuffer: Uint8Array, cols: number, rows: number, paintedRegions: Map<number, BgrColor>): SkImage;
|
||||||
|
export type OffscreenPaintInput = PaintShaderTextures & {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
showOrigin?: boolean;
|
||||||
|
};
|
||||||
|
/** 离屏渲染与预览同源的 shader 合成图 */
|
||||||
|
export declare function renderPaintedImageOffscreen(input: OffscreenPaintInput): Promise<SkImage | null>;
|
||||||
|
export declare function releasePaintShaderTextures(textures: {
|
||||||
|
originImage?: SkImage | null;
|
||||||
|
paintColorMap?: SkImage | null;
|
||||||
|
lowFreqImage?: SkImage | null;
|
||||||
|
highFreqImage?: SkImage | null;
|
||||||
|
}): void;
|
||||||
|
//# sourceMappingURL=paintShaderRuntime.d.ts.map
|
||||||
1
dist/utils/paintShaderRuntime.d.ts.map
vendored
Normal file
1
dist/utils/paintShaderRuntime.d.ts.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"paintShaderRuntime.d.ts","sourceRoot":"","sources":["../../src/utils/paintShaderRuntime.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAOL,KAAK,OAAO,EACZ,KAAK,eAAe,EACrB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uCAAuC,CAAC;AAMtE,wBAAgB,oBAAoB,IAAI,eAAe,CAUtD;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,OAAO;;;;;EAQ3D;AAED,MAAM,MAAM,qBAAqB,GAAG,mBAAmB,GAAG;IACxD,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAmCF,4BAA4B;AAC5B,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,qBAE5D;AAED,wBAAgB,2BAA2B,CACzC,UAAU,EAAE,UAAU,EACtB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,GACpC,OAAO,CAMT;AAED,MAAM,MAAM,mBAAmB,GAAG,mBAAmB,GAAG;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,4BAA4B;AAC5B,wBAAsB,2BAA2B,CAC/C,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsBzB;AAED,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE;IACnD,WAAW,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC7B,aAAa,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B,YAAY,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC9B,aAAa,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAChC,QAKA"}
|
||||||
76
dist/utils/paintShaderRuntime.js
vendored
Normal file
76
dist/utils/paintShaderRuntime.js
vendored
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||||
|
import { Skia, Fill, Shader, ImageShader, Group, drawAsImage, } from '@shopify/react-native-skia';
|
||||||
|
import { REGION_PAINT_SKSL } from '../shaders/regionPaint.sksl';
|
||||||
|
import { getMaskSegmentRuntimeConfig } from './maskSegmentRuntime';
|
||||||
|
import { buildPaintColorMapImage } from './paintColorMapTexture';
|
||||||
|
let cachedEffect = null;
|
||||||
|
export function getRegionPaintEffect() {
|
||||||
|
if (cachedEffect) {
|
||||||
|
return cachedEffect;
|
||||||
|
}
|
||||||
|
const effect = Skia.RuntimeEffect.Make(REGION_PAINT_SKSL);
|
||||||
|
if (!effect) {
|
||||||
|
throw new Error('regionPaint SkSL compile failed');
|
||||||
|
}
|
||||||
|
cachedEffect = effect;
|
||||||
|
return effect;
|
||||||
|
}
|
||||||
|
export function buildPaintShaderUniforms(showOrigin) {
|
||||||
|
const paintCfg = getMaskSegmentRuntimeConfig().paint;
|
||||||
|
return {
|
||||||
|
colorBaseOpacity: paintCfg.colorBaseOpacity,
|
||||||
|
lLightOpacity: paintCfg.lLightOpacity,
|
||||||
|
textureOpacity: paintCfg.textureOpacity,
|
||||||
|
showOrigin: showOrigin ? 1 : 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function createPaintShaderTree(props) {
|
||||||
|
const { originImage, paintColorMap, lowFreqImage, highFreqImage, x, y, width, height, showOrigin = false, } = props;
|
||||||
|
const effect = getRegionPaintEffect();
|
||||||
|
const uniforms = buildPaintShaderUniforms(showOrigin);
|
||||||
|
const imageShaderProps = {
|
||||||
|
fit: 'fill',
|
||||||
|
tx: 'clamp',
|
||||||
|
ty: 'clamp',
|
||||||
|
rect: { x, y, width, height },
|
||||||
|
};
|
||||||
|
return (_jsx(Fill, { children: _jsxs(Shader, { source: effect, uniforms: uniforms, children: [_jsx(ImageShader, { image: originImage, ...imageShaderProps }), _jsx(ImageShader, { image: paintColorMap, ...imageShaderProps }), _jsx(ImageShader, { image: lowFreqImage, ...imageShaderProps }), _jsx(ImageShader, { image: highFreqImage, ...imageShaderProps })] }) }));
|
||||||
|
}
|
||||||
|
/** Canvas 内全屏上色 Shader 层 */
|
||||||
|
export function PaintShaderLayer(props) {
|
||||||
|
return createPaintShaderTree(props);
|
||||||
|
}
|
||||||
|
export function createPaintColorMapForPaint(pickBuffer, cols, rows, paintedRegions) {
|
||||||
|
const paintCfg = getMaskSegmentRuntimeConfig().paint;
|
||||||
|
// Prefer color feather for the recolor application alpha (controls soft edge blend for both base color and texture overlay).
|
||||||
|
// texture feather kept in config for future differentiation (e.g. high-freq strength roll-off).
|
||||||
|
const feather = paintCfg.maskFeatherColor ?? 0;
|
||||||
|
return buildPaintColorMapImage(pickBuffer, cols, rows, paintedRegions, feather);
|
||||||
|
}
|
||||||
|
/** 离屏渲染与预览同源的 shader 合成图 */
|
||||||
|
export async function renderPaintedImageOffscreen(input) {
|
||||||
|
const { width, height, showOrigin = false, ...textures } = input;
|
||||||
|
if (!textures.originImage || !textures.paintColorMap || !textures.lowFreqImage || !textures.highFreqImage) {
|
||||||
|
console.warn('[VIZ-SAVE] renderPaintedImageOffscreen: missing one or more shader textures, will fallback');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// drawAsImage expects the *scene content*, not a <Canvas> host component.
|
||||||
|
// Passing a <Canvas> could cause the internal player to construct paints with
|
||||||
|
// undefined values (leading to k.charAt errors in color/enum code).
|
||||||
|
const element = (_jsx(Group, { children: createPaintShaderTree({
|
||||||
|
...textures,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
showOrigin,
|
||||||
|
}) }));
|
||||||
|
return drawAsImage(element, { width, height });
|
||||||
|
}
|
||||||
|
export function releasePaintShaderTextures(textures) {
|
||||||
|
textures.originImage?.dispose();
|
||||||
|
textures.paintColorMap?.dispose();
|
||||||
|
textures.lowFreqImage?.dispose();
|
||||||
|
textures.highFreqImage?.dispose();
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=paintShaderRuntime.js.map
|
||||||
1
dist/utils/paintShaderRuntime.js.map
vendored
Normal file
1
dist/utils/paintShaderRuntime.js.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"paintShaderRuntime.js","sourceRoot":"","sources":["../../src/utils/paintShaderRuntime.tsx"],"names":[],"mappings":"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"}
|
||||||
4
dist/utils/pickMapTexture.d.ts
vendored
Normal file
4
dist/utils/pickMapTexture.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { type SkImage } from '@shopify/react-native-skia';
|
||||||
|
/** pickMap 像素值 regionId+1 → RGBA 纹理(R 通道存查表码) */
|
||||||
|
export declare function pickBufferToSkImage(pickBuffer: Uint8Array, cols: number, rows: number): SkImage | null;
|
||||||
|
//# sourceMappingURL=pickMapTexture.d.ts.map
|
||||||
1
dist/utils/pickMapTexture.d.ts.map
vendored
Normal file
1
dist/utils/pickMapTexture.d.ts.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"pickMapTexture.d.ts","sourceRoot":"","sources":["../../src/utils/pickMapTexture.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,OAAO,EACb,MAAM,4BAA4B,CAAC;AAEpC,iDAAiD;AACjD,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,UAAU,EACtB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,GAAG,IAAI,CAwBhB"}
|
||||||
24
dist/utils/pickMapTexture.js
vendored
Normal file
24
dist/utils/pickMapTexture.js
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Skia, AlphaType, ColorType, } from '@shopify/react-native-skia';
|
||||||
|
/** pickMap 像素值 regionId+1 → RGBA 纹理(R 通道存查表码) */
|
||||||
|
export function pickBufferToSkImage(pickBuffer, cols, rows) {
|
||||||
|
if (pickBuffer.length !== cols * rows) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const rgba = new Uint8Array(cols * rows * 4);
|
||||||
|
for (let i = 0; i < pickBuffer.length; i++) {
|
||||||
|
const o = i * 4;
|
||||||
|
const code = pickBuffer[i];
|
||||||
|
rgba[o] = code;
|
||||||
|
rgba[o + 1] = code;
|
||||||
|
rgba[o + 2] = code;
|
||||||
|
rgba[o + 3] = 255;
|
||||||
|
}
|
||||||
|
const data = Skia.Data.fromBytes(rgba);
|
||||||
|
return Skia.Image.MakeImage({
|
||||||
|
width: cols,
|
||||||
|
height: rows,
|
||||||
|
alphaType: AlphaType.Opaque,
|
||||||
|
colorType: ColorType.RGBA_8888,
|
||||||
|
}, data, cols * 4);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=pickMapTexture.js.map
|
||||||
1
dist/utils/pickMapTexture.js.map
vendored
Normal file
1
dist/utils/pickMapTexture.js.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"pickMapTexture.js","sourceRoot":"","sources":["../../src/utils/pickMapTexture.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,SAAS,EACT,SAAS,GAEV,MAAM,4BAA4B,CAAC;AAEpC,iDAAiD;AACjD,MAAM,UAAU,mBAAmB,CACjC,UAAsB,EACtB,IAAY,EACZ,IAAY;IAEZ,IAAI,UAAU,CAAC,MAAM,KAAK,IAAI,GAAG,IAAI,EAAE;QACrC,OAAO,IAAI,CAAC;KACb;IACD,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC;IAC7C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QAC1C,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAChB,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;QAC3B,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;QACf,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC;KACnB;IACD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACvC,OAAO,IAAI,CAAC,KAAK,CAAC,SAAS,CACzB;QACE,KAAK,EAAE,IAAI;QACX,MAAM,EAAE,IAAI;QACZ,SAAS,EAAE,SAAS,CAAC,MAAM;QAC3B,SAAS,EAAE,SAAS,CAAC,SAAS;KAC/B,EACD,IAAI,EACJ,IAAI,GAAG,CAAC,CACT,CAAC;AACJ,CAAC"}
|
||||||
49
dist/utils/pngImage.d.ts
vendored
Normal file
49
dist/utils/pngImage.d.ts
vendored
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { type Mat } from 'react-native-fast-opencv';
|
||||||
|
export declare const PNG_EXT = ".png";
|
||||||
|
/** PNG 压缩级别 0 = 无损 */
|
||||||
|
export declare const PNG_COMPRESSION = 0;
|
||||||
|
export declare function normalizePath(path: string): string;
|
||||||
|
/** Skia useImage 需要 URI;OpenCV / RNFS 使用裸路径 */
|
||||||
|
export declare function toSkiaUri(path: string | null | undefined): string | null;
|
||||||
|
export declare function toPngFileName(name: string): string;
|
||||||
|
export declare function isPngPath(path: string): boolean;
|
||||||
|
/** 按文件元数据生成指纹,避免整文件读入(原 base64 全量哈希在 1.5MB 图上极慢) */
|
||||||
|
export declare function fileContentFingerprint(path: string): Promise<string>;
|
||||||
|
/** 任意图片路径 → 缓存目录下的 PNG 文件路径(已是 PNG 则复制,否则解码转存) */
|
||||||
|
export declare function ensurePngFile(sourcePath: string, cacheFileName: string): Promise<string>;
|
||||||
|
/** 根据源路径生成稳定 PNG 缓存名 */
|
||||||
|
export declare function pngCacheName(sourcePath: string, prefix: string): string;
|
||||||
|
/** 清理分割/OpenCV 派生缓存,保留原图与掩码源文件 */
|
||||||
|
export declare function clearDerivedImageCache(): Promise<number>;
|
||||||
|
type PngHeader = {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
bitDepth: number;
|
||||||
|
colorType: number;
|
||||||
|
};
|
||||||
|
/** 从 base64 解析 PNG IHDR(不依赖 OpenCV),用于 16-bit Mat toJSValue 崩溃时的降级通路 */
|
||||||
|
export declare function readPngHeaderFromBase64(base64: string): PngHeader;
|
||||||
|
/** 16-bit / float Mat → 8-bit(fast-opencv 在 patch 前 toJSValue 会截断高位)。
|
||||||
|
* Semantic mask PNGs use 16-bit RGB where the label (0-255) is stored as value×257;
|
||||||
|
* use 1/257 scaling for 3+ channel 16-bit cases so the resulting 8-bit channels contain
|
||||||
|
* the original semantic label values (consistent with the native ensure8U patch).
|
||||||
|
*
|
||||||
|
* pngHeader 可选:当 toJSValue 因 16-bit Mat 崩溃时,用 PNG 文件头信息做降级转换,
|
||||||
|
* 绕过原生 toJSValue 调用。
|
||||||
|
*/
|
||||||
|
export declare function ensureMat8U(srcMat: Mat, pngHeader?: PngHeader): {
|
||||||
|
mat: Mat;
|
||||||
|
extraReleaseIds: string[];
|
||||||
|
};
|
||||||
|
export type PngBgrBuffer = {
|
||||||
|
buffer: Uint8Array;
|
||||||
|
cols: number;
|
||||||
|
rows: number;
|
||||||
|
};
|
||||||
|
export declare function prewarmPngBgrCache(paths: string[]): void;
|
||||||
|
export declare function prewarmPngBgrCacheAsync(paths: string[]): Promise<void>;
|
||||||
|
export declare function pngContentCacheKey(path: string): Promise<string>;
|
||||||
|
export declare function readPngBgrBuffer(path: string): Promise<PngBgrBuffer>;
|
||||||
|
export declare function resizeBgrBuffer(buffer: Uint8Array, srcCols: number, srcRows: number, dstCols: number, dstRows: number): Uint8Array;
|
||||||
|
export {};
|
||||||
|
//# sourceMappingURL=pngImage.d.ts.map
|
||||||
1
dist/utils/pngImage.d.ts.map
vendored
Normal file
1
dist/utils/pngImage.d.ts.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"pngImage.d.ts","sourceRoot":"","sources":["../../src/utils/pngImage.ts"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,GAAG,EACT,MAAM,0BAA0B,CAAC;AAElC,eAAO,MAAM,OAAO,SAAS,CAAC;AAC9B,sBAAsB;AACtB,eAAO,MAAM,eAAe,IAAI,CAAC;AAEjC,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,+CAA+C;AAC/C,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAcxE;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGlD;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE/C;AAcD,oDAAoD;AACpD,wBAAsB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI1E;AAgCD,kDAAkD;AAClD,wBAAsB,aAAa,CACjC,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,CA6BjB;AAED,wBAAwB;AACxB,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEvE;AAUD,kCAAkC;AAClC,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC,CAgB9D;AAYD,KAAK,SAAS,GAAG;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAkBF,wEAAwE;AACxE,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAkCjE;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,GAAG,EACX,SAAS,CAAC,EAAE,SAAS,GACpB;IACD,GAAG,EAAE,GAAG,CAAC;IACT,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B,CA2EA;AAsKD,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAKF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAKxD;AAED,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,MAAM,EAAE,GACd,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGtE;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAoD1E;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd,UAAU,CAkBZ"}
|
||||||
438
dist/utils/pngImage.js
vendored
Normal file
438
dist/utils/pngImage.js
vendored
Normal file
@ -0,0 +1,438 @@
|
|||||||
|
import RNFS from 'react-native-fs';
|
||||||
|
import UPNG from 'upng-js';
|
||||||
|
import { OpenCV, ObjectType, DataTypes, ColorConversionCodes, } from 'react-native-fast-opencv';
|
||||||
|
export const PNG_EXT = '.png';
|
||||||
|
/** PNG 压缩级别 0 = 无损 */
|
||||||
|
export const PNG_COMPRESSION = 0;
|
||||||
|
export function normalizePath(path) {
|
||||||
|
return path.startsWith('file://') ? path.slice(7) : path;
|
||||||
|
}
|
||||||
|
/** Skia useImage 需要 URI;OpenCV / RNFS 使用裸路径 */
|
||||||
|
export function toSkiaUri(path) {
|
||||||
|
if (!path) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (path.startsWith('http://') ||
|
||||||
|
path.startsWith('https://') ||
|
||||||
|
path.startsWith('data:') ||
|
||||||
|
path.startsWith('file://')) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
const normalized = normalizePath(path);
|
||||||
|
return `file://${normalized}`;
|
||||||
|
}
|
||||||
|
export function toPngFileName(name) {
|
||||||
|
const base = name.replace(/\.(jpe?g|webp|gif|bmp|heic|heif)$/i, '');
|
||||||
|
return base.toLowerCase().endsWith(PNG_EXT) ? base : `${base}${PNG_EXT}`;
|
||||||
|
}
|
||||||
|
export function isPngPath(path) {
|
||||||
|
return normalizePath(path).toLowerCase().endsWith(PNG_EXT);
|
||||||
|
}
|
||||||
|
function hashString(value) {
|
||||||
|
let hash = 5381;
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
hash = Math.imul(hash, 33) ^ value.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return (hash >>> 0).toString(16);
|
||||||
|
}
|
||||||
|
function hashPath(path) {
|
||||||
|
return hashString(path);
|
||||||
|
}
|
||||||
|
/** 按文件元数据生成指纹,避免整文件读入(原 base64 全量哈希在 1.5MB 图上极慢) */
|
||||||
|
export async function fileContentFingerprint(path) {
|
||||||
|
const normalized = normalizePath(path);
|
||||||
|
const stat = await RNFS.stat(normalized);
|
||||||
|
return hashString(`${stat.size}:${stat.mtime ?? stat.ctime}`);
|
||||||
|
}
|
||||||
|
function versionedCachePath(cacheFileName, fingerprint) {
|
||||||
|
const baseName = toPngFileName(cacheFileName).replace(/\.png$/i, '');
|
||||||
|
return `${RNFS.CachesDirectoryPath}/${baseName}_${fingerprint}${PNG_EXT}`;
|
||||||
|
}
|
||||||
|
async function cleanupStaleVersionedCache(cacheFileName, keepDest) {
|
||||||
|
const baseName = toPngFileName(cacheFileName).replace(/\.png$/i, '');
|
||||||
|
const legacyDest = `${RNFS.CachesDirectoryPath}/${toPngFileName(cacheFileName)}`;
|
||||||
|
const prefix = `${baseName}_`;
|
||||||
|
if (legacyDest !== keepDest && (await RNFS.exists(legacyDest))) {
|
||||||
|
await RNFS.unlink(legacyDest);
|
||||||
|
}
|
||||||
|
const files = await RNFS.readDir(RNFS.CachesDirectoryPath);
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.isFile() &&
|
||||||
|
file.name.startsWith(prefix) &&
|
||||||
|
file.name.endsWith(PNG_EXT) &&
|
||||||
|
file.path !== keepDest) {
|
||||||
|
await RNFS.unlink(file.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** 任意图片路径 → 缓存目录下的 PNG 文件路径(已是 PNG 则复制,否则解码转存) */
|
||||||
|
export async function ensurePngFile(sourcePath, cacheFileName) {
|
||||||
|
const src = normalizePath(sourcePath);
|
||||||
|
const fingerprint = await fileContentFingerprint(src);
|
||||||
|
const dest = versionedCachePath(cacheFileName, fingerprint);
|
||||||
|
if (!(await RNFS.exists(src))) {
|
||||||
|
throw new Error(`Image does not exist: ${src}`);
|
||||||
|
}
|
||||||
|
if (src === dest) {
|
||||||
|
return dest;
|
||||||
|
}
|
||||||
|
if (await RNFS.exists(dest)) {
|
||||||
|
return dest;
|
||||||
|
}
|
||||||
|
if (isPngPath(src)) {
|
||||||
|
await RNFS.copyFile(src, dest);
|
||||||
|
await cleanupStaleVersionedCache(cacheFileName, dest);
|
||||||
|
return dest;
|
||||||
|
}
|
||||||
|
const base64 = await RNFS.readFile(src, 'base64');
|
||||||
|
const mat = OpenCV.base64ToMat(base64);
|
||||||
|
OpenCV.saveMatToFile(mat, dest, 'png', PNG_COMPRESSION);
|
||||||
|
OpenCV.releaseBuffers([mat.id]);
|
||||||
|
await cleanupStaleVersionedCache(cacheFileName, dest);
|
||||||
|
return dest;
|
||||||
|
}
|
||||||
|
/** 根据源路径生成稳定 PNG 缓存名 */
|
||||||
|
export function pngCacheName(sourcePath, prefix) {
|
||||||
|
return `${prefix}_${hashPath(normalizePath(sourcePath))}${PNG_EXT}`;
|
||||||
|
}
|
||||||
|
const DERIVED_CACHE_PREFIXES = [
|
||||||
|
'seg_lowfreq_',
|
||||||
|
'seg_highfreq_',
|
||||||
|
'imread_',
|
||||||
|
'img_',
|
||||||
|
'tmp_',
|
||||||
|
];
|
||||||
|
/** 清理分割/OpenCV 派生缓存,保留原图与掩码源文件 */
|
||||||
|
export async function clearDerivedImageCache() {
|
||||||
|
const files = await RNFS.readDir(RNFS.CachesDirectoryPath);
|
||||||
|
let removed = 0;
|
||||||
|
for (const file of files) {
|
||||||
|
if (!file.isFile() || !file.name.endsWith(PNG_EXT)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!DERIVED_CACHE_PREFIXES.some(prefix => file.name.startsWith(prefix))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await RNFS.unlink(file.path);
|
||||||
|
removed += 1;
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
const CV_MAT_DEPTH_MASK = 7;
|
||||||
|
function matDepth(type) {
|
||||||
|
return type & CV_MAT_DEPTH_MASK;
|
||||||
|
}
|
||||||
|
function matChannelsFromType(type) {
|
||||||
|
return (type >> 3) + 1;
|
||||||
|
}
|
||||||
|
function pngColorTypeToChannels(colorType) {
|
||||||
|
switch (colorType) {
|
||||||
|
case 0:
|
||||||
|
return 1;
|
||||||
|
case 2:
|
||||||
|
case 3:
|
||||||
|
return 3;
|
||||||
|
case 4:
|
||||||
|
return 2;
|
||||||
|
case 6:
|
||||||
|
return 4;
|
||||||
|
default:
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** 从 base64 解析 PNG IHDR(不依赖 OpenCV),用于 16-bit Mat toJSValue 崩溃时的降级通路 */
|
||||||
|
export function readPngHeaderFromBase64(base64) {
|
||||||
|
// PNG 签名 8 字节 + IHDR 长度 4 + "IHDR" 4 + 宽 4 + 高 4 + 位深 1 + 颜色类型 1 = 26 字节
|
||||||
|
// base64 每 3 字节 → 4 字符,取前 40 字符覆盖 ~30 字节
|
||||||
|
const headerPart = base64.slice(0, 40);
|
||||||
|
const binary = atob(headerPart);
|
||||||
|
function readUint32BE(offset) {
|
||||||
|
return (((binary.charCodeAt(offset) << 24) |
|
||||||
|
(binary.charCodeAt(offset + 1) << 16) |
|
||||||
|
(binary.charCodeAt(offset + 2) << 8) |
|
||||||
|
binary.charCodeAt(offset + 3)) >>>
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
// 校验 PNG 签名
|
||||||
|
const sig = [137, 80, 78, 71, 13, 10, 26, 10];
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
if (binary.charCodeAt(i) !== sig[i]) {
|
||||||
|
throw new Error('Invalid PNG file (signature mismatch)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const width = readUint32BE(16);
|
||||||
|
const height = readUint32BE(20);
|
||||||
|
const bitDepth = binary.charCodeAt(24);
|
||||||
|
const colorType = binary.charCodeAt(25);
|
||||||
|
if (width === 0 || height === 0) {
|
||||||
|
throw new Error(`Invalid PNG size: ${width}x${height}`);
|
||||||
|
}
|
||||||
|
return { width, height, bitDepth, colorType };
|
||||||
|
}
|
||||||
|
/** 16-bit / float Mat → 8-bit(fast-opencv 在 patch 前 toJSValue 会截断高位)。
|
||||||
|
* Semantic mask PNGs use 16-bit RGB where the label (0-255) is stored as value×257;
|
||||||
|
* use 1/257 scaling for 3+ channel 16-bit cases so the resulting 8-bit channels contain
|
||||||
|
* the original semantic label values (consistent with the native ensure8U patch).
|
||||||
|
*
|
||||||
|
* pngHeader 可选:当 toJSValue 因 16-bit Mat 崩溃时,用 PNG 文件头信息做降级转换,
|
||||||
|
* 绕过原生 toJSValue 调用。
|
||||||
|
*/
|
||||||
|
export function ensureMat8U(srcMat, pngHeader) {
|
||||||
|
let info;
|
||||||
|
try {
|
||||||
|
info = OpenCV.toJSValue(srcMat);
|
||||||
|
}
|
||||||
|
catch (toJsError) {
|
||||||
|
if (!pngHeader) {
|
||||||
|
throw new Error(`OpenCV toJSValue failed (mat id=${srcMat?.id ?? 'unknown'}): ${toJsError.message}`);
|
||||||
|
}
|
||||||
|
// 降级通路: 16-bit PNG 的 Mat 无法通过 toJSValue 读取元数据,
|
||||||
|
// 直接使用 PNG 文件头中的宽高和位深信息做 convertTo
|
||||||
|
const { width: cols, height: rows, bitDepth, colorType } = pngHeader;
|
||||||
|
const ch = pngColorTypeToChannels(colorType);
|
||||||
|
if (bitDepth <= 8) {
|
||||||
|
// 8-bit 或更低,不需要 convertTo,直接返回
|
||||||
|
return { mat: srcMat, extraReleaseIds: [] };
|
||||||
|
}
|
||||||
|
const outType = ch === 1
|
||||||
|
? DataTypes.CV_8UC1
|
||||||
|
: ch === 4
|
||||||
|
? DataTypes.CV_8UC4
|
||||||
|
: DataTypes.CV_8UC3;
|
||||||
|
const dstMat = OpenCV.createObject(ObjectType.Mat, rows, cols, outType);
|
||||||
|
// 16-bit RGB 语义掩码: 标签值 = 原始值 / 257
|
||||||
|
const alpha = ch >= 3 ? 1 / 257 : 255 / 65535;
|
||||||
|
OpenCV.invoke('convertTo', srcMat, dstMat, outType, alpha, 0);
|
||||||
|
return { mat: dstMat, extraReleaseIds: [dstMat.id] };
|
||||||
|
}
|
||||||
|
if (matDepth(info.type) === DataTypes.CV_8U) {
|
||||||
|
return { mat: srcMat, extraReleaseIds: [] };
|
||||||
|
}
|
||||||
|
const rows = info.rows;
|
||||||
|
const cols = info.cols;
|
||||||
|
const ch = matChannelsFromType(info.type);
|
||||||
|
const outType = ch === 1
|
||||||
|
? DataTypes.CV_8UC1
|
||||||
|
: ch === 4
|
||||||
|
? DataTypes.CV_8UC4
|
||||||
|
: ch === 2
|
||||||
|
? DataTypes.CV_8UC2
|
||||||
|
: DataTypes.CV_8UC3;
|
||||||
|
const dstMat = OpenCV.createObject(ObjectType.Mat, rows, cols, outType);
|
||||||
|
const depth = matDepth(info.type);
|
||||||
|
let alpha = 1;
|
||||||
|
if (depth === DataTypes.CV_16U || depth === DataTypes.CV_16S) {
|
||||||
|
// Special case for semantic mask 16-bit "RGB label" encoding (value × 257)
|
||||||
|
alpha = ch >= 3 ? 1 / 257 : 255 / 65535;
|
||||||
|
}
|
||||||
|
else if (depth === DataTypes.CV_32F || depth === DataTypes.CV_64F) {
|
||||||
|
alpha = 255;
|
||||||
|
}
|
||||||
|
OpenCV.invoke('convertTo', srcMat, dstMat, outType, alpha, 0);
|
||||||
|
return { mat: dstMat, extraReleaseIds: [dstMat.id] };
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* JS fallback PNG decoder (OpenCV base64ToMat may throw HostFunction for 16-bit PNGs).
|
||||||
|
* 16-bit RGB semantic masks use value×257 encoding → UPNG.toRGBA8 high byte = original 8-bit label.
|
||||||
|
*/
|
||||||
|
function decodePngToBgrJS(base64) {
|
||||||
|
const atobFn = globalThis.atob;
|
||||||
|
if (!atobFn) {
|
||||||
|
throw new Error('JS PNG fallback: atob unavailable');
|
||||||
|
}
|
||||||
|
const binary = atobFn(base64);
|
||||||
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
for (let i = 0; i < binary.length; i++) {
|
||||||
|
bytes[i] = binary.charCodeAt(i);
|
||||||
|
}
|
||||||
|
const decoded = UPNG.decode(bytes);
|
||||||
|
const rgbaFrames = UPNG.toRGBA8(decoded);
|
||||||
|
const rgbaBuf = new Uint8Array(rgbaFrames[0]);
|
||||||
|
const w = decoded.width;
|
||||||
|
const h = decoded.height;
|
||||||
|
const pixelCount = w * h;
|
||||||
|
// RGBA → BGR (drop alpha)
|
||||||
|
const bgr = new Uint8Array(pixelCount * 3);
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
const si = i * 4;
|
||||||
|
const di = i * 3;
|
||||||
|
bgr[di] = rgbaBuf[si + 2]; // B ← R
|
||||||
|
bgr[di + 1] = rgbaBuf[si + 1]; // G
|
||||||
|
bgr[di + 2] = rgbaBuf[si]; // R ← B
|
||||||
|
}
|
||||||
|
return { buffer: bgr, cols: w, rows: h };
|
||||||
|
}
|
||||||
|
/** Only check PNG magic bytes (89 50 4E 47) — agnostic to bit depth.
|
||||||
|
* OpenCV bridge may throw HostFunction for both 8-bit and 16-bit PNGs.
|
||||||
|
* Fall back to UPNG pure-JS decode for any valid PNG (supports all variants). */
|
||||||
|
function isPngByBase64Magic(base64) {
|
||||||
|
if (!base64 || base64.length < 8) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const atobFn = globalThis.atob;
|
||||||
|
if (!atobFn) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const binary = atobFn(base64.slice(0, 12)); // 8 bytes → 12 base64 chars
|
||||||
|
return (binary.charCodeAt(0) === 0x89 &&
|
||||||
|
binary.charCodeAt(1) === 0x50 &&
|
||||||
|
binary.charCodeAt(2) === 0x4e &&
|
||||||
|
binary.charCodeAt(3) === 0x47);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** base64 PNG → 连续 BGR 缓冲(OpenCV 原生解码,跳过 JS atob + upng) */
|
||||||
|
function decodeBase64PngToBgr(base64) {
|
||||||
|
let srcMat;
|
||||||
|
try {
|
||||||
|
srcMat = OpenCV.base64ToMat(base64);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
// OpenCV bridge may throw HostFunction for any PNG (8-bit or 16-bit).
|
||||||
|
// Fall back to pure JS decode whenever the file header is PNG.
|
||||||
|
if (isPngByBase64Magic(base64)) {
|
||||||
|
try {
|
||||||
|
return decodePngToBgrJS(base64);
|
||||||
|
}
|
||||||
|
catch (jsError) {
|
||||||
|
throw new Error(`JS PNG fallback also failed (base64 length ${base64?.length ?? 0}): ${jsError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`OpenCV base64ToMat failed (base64 length ${base64?.length ?? 0}): ${e.message}`);
|
||||||
|
}
|
||||||
|
if (!srcMat || typeof srcMat.id !== 'string' || !srcMat.id) {
|
||||||
|
throw new Error(`OpenCV base64ToMat returned invalid Mat (base64 length ${base64?.length ?? 0})`);
|
||||||
|
}
|
||||||
|
const releaseIds = [srcMat.id];
|
||||||
|
try {
|
||||||
|
// 先解析 PNG 文件头(宽高、位深),供 ensureMat8U 在 16-bit toJSValue 崩溃时降级使用
|
||||||
|
let pngHeader;
|
||||||
|
try {
|
||||||
|
pngHeader = readPngHeaderFromBase64(base64);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// 非 PNG 或解析失败,不传 header,确保 retain 原有行为
|
||||||
|
}
|
||||||
|
let workMat;
|
||||||
|
let extraReleaseIds;
|
||||||
|
try {
|
||||||
|
const result = ensureMat8U(srcMat, pngHeader);
|
||||||
|
workMat = result.mat;
|
||||||
|
extraReleaseIds = result.extraReleaseIds;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
throw new Error(`ensureMat8U failed: ${e.message}`);
|
||||||
|
}
|
||||||
|
releaseIds.push(...extraReleaseIds);
|
||||||
|
const info = OpenCV.toJSValue(workMat);
|
||||||
|
const cols = info.cols;
|
||||||
|
const rows = info.rows;
|
||||||
|
let bgrMat = workMat;
|
||||||
|
if (info.type === DataTypes.CV_8UC4) {
|
||||||
|
bgrMat = OpenCV.createObject(ObjectType.Mat, rows, cols, DataTypes.CV_8UC3);
|
||||||
|
releaseIds.push(bgrMat.id);
|
||||||
|
OpenCV.invoke('cvtColor', workMat, bgrMat, ColorConversionCodes.COLOR_BGRA2BGR);
|
||||||
|
}
|
||||||
|
else if (info.type === DataTypes.CV_8UC1) {
|
||||||
|
bgrMat = OpenCV.createObject(ObjectType.Mat, rows, cols, DataTypes.CV_8UC3);
|
||||||
|
releaseIds.push(bgrMat.id);
|
||||||
|
OpenCV.invoke('cvtColor', workMat, bgrMat, ColorConversionCodes.COLOR_GRAY2BGR);
|
||||||
|
}
|
||||||
|
const continuous = OpenCV.invoke('clone', bgrMat);
|
||||||
|
releaseIds.push(continuous.id);
|
||||||
|
const { buffer, cols: outCols, rows: outRows } = OpenCV.matToBuffer(continuous, 'uint8');
|
||||||
|
return {
|
||||||
|
buffer: new Uint8Array(buffer),
|
||||||
|
cols: outCols,
|
||||||
|
rows: outRows,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
OpenCV.releaseBuffers(releaseIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const pngBgrCache = new Map();
|
||||||
|
const prewarmInFlight = new Map();
|
||||||
|
export function prewarmPngBgrCache(paths) {
|
||||||
|
for (const path of paths) {
|
||||||
|
const filePath = normalizePath(path);
|
||||||
|
void readPngBgrBuffer(filePath).catch(() => { });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function prewarmPngBgrCacheAsync(paths) {
|
||||||
|
await Promise.all(paths.map(path => readPngBgrBuffer(normalizePath(path))));
|
||||||
|
}
|
||||||
|
export async function pngContentCacheKey(path) {
|
||||||
|
const stat = await RNFS.stat(normalizePath(path));
|
||||||
|
return `${stat.size}:${stat.mtime ?? stat.ctime}`;
|
||||||
|
}
|
||||||
|
export async function readPngBgrBuffer(path) {
|
||||||
|
const filePath = normalizePath(path);
|
||||||
|
const cacheKey = await pngContentCacheKey(filePath);
|
||||||
|
const cached = pngBgrCache.get(cacheKey);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
const inflight = prewarmInFlight.get(cacheKey);
|
||||||
|
if (inflight) {
|
||||||
|
return inflight;
|
||||||
|
}
|
||||||
|
const loadPromise = (async () => {
|
||||||
|
const exists = await RNFS.exists(filePath);
|
||||||
|
if (!exists) {
|
||||||
|
throw new Error(`Image file does not exist: ${filePath}`);
|
||||||
|
}
|
||||||
|
const stat = await RNFS.stat(filePath);
|
||||||
|
if (stat.size === 0) {
|
||||||
|
throw new Error(`Image file is empty (0 bytes): ${filePath}`);
|
||||||
|
}
|
||||||
|
const base64 = await RNFS.readFile(filePath, 'base64');
|
||||||
|
if (!base64 || base64.length === 0) {
|
||||||
|
throw new Error(`Read base64 is empty: ${filePath}`);
|
||||||
|
}
|
||||||
|
let decoded;
|
||||||
|
try {
|
||||||
|
decoded = decodeBase64PngToBgr(base64);
|
||||||
|
}
|
||||||
|
catch (decodeError) {
|
||||||
|
throw new Error(`PNG decode failed: ${filePath} (${stat.size} bytes) - ${decodeError.message}`);
|
||||||
|
}
|
||||||
|
const entry = {
|
||||||
|
buffer: decoded.buffer,
|
||||||
|
cols: decoded.cols,
|
||||||
|
rows: decoded.rows,
|
||||||
|
};
|
||||||
|
pngBgrCache.set(cacheKey, entry);
|
||||||
|
return entry;
|
||||||
|
})();
|
||||||
|
prewarmInFlight.set(cacheKey, loadPromise);
|
||||||
|
try {
|
||||||
|
const result = await loadPromise;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
prewarmInFlight.delete(cacheKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function resizeBgrBuffer(buffer, srcCols, srcRows, dstCols, dstRows) {
|
||||||
|
if (srcCols === dstCols && srcRows === dstRows) {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
const out = new Uint8Array(dstCols * dstRows * 3);
|
||||||
|
for (let y = 0; y < dstRows; y++) {
|
||||||
|
const sy = Math.min(srcRows - 1, Math.floor((y * srcRows) / dstRows));
|
||||||
|
for (let x = 0; x < dstCols; x++) {
|
||||||
|
const sx = Math.min(srcCols - 1, Math.floor((x * srcCols) / dstCols));
|
||||||
|
const si = (sy * srcCols + sx) * 3;
|
||||||
|
const di = (y * dstCols + x) * 3;
|
||||||
|
out[di] = buffer[si];
|
||||||
|
out[di + 1] = buffer[si + 1];
|
||||||
|
out[di + 2] = buffer[si + 2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=pngImage.js.map
|
||||||
1
dist/utils/pngImage.js.map
vendored
Normal file
1
dist/utils/pngImage.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
3
dist/utils/resolveAssetPath.d.ts
vendored
Normal file
3
dist/utils/resolveAssetPath.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/** 将 require() 资源解析为 PNG 本地路径(OpenCV / RNFS 可读) */
|
||||||
|
export declare function resolveAssetPath(assetModule: number, cacheFileName: string): Promise<string>;
|
||||||
|
//# sourceMappingURL=resolveAssetPath.d.ts.map
|
||||||
1
dist/utils/resolveAssetPath.d.ts.map
vendored
Normal file
1
dist/utils/resolveAssetPath.d.ts.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"resolveAssetPath.d.ts","sourceRoot":"","sources":["../../src/utils/resolveAssetPath.ts"],"names":[],"mappings":"AAIA,mDAAmD;AACnD,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,CAuDjB"}
|
||||||
56
dist/utils/resolveAssetPath.js
vendored
Normal file
56
dist/utils/resolveAssetPath.js
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { Image, Platform } from 'react-native';
|
||||||
|
import RNFS from 'react-native-fs';
|
||||||
|
import { ensurePngFile, toPngFileName } from './pngImage';
|
||||||
|
/** 将 require() 资源解析为 PNG 本地路径(OpenCV / RNFS 可读) */
|
||||||
|
export async function resolveAssetPath(assetModule, cacheFileName) {
|
||||||
|
const pngCacheName = toPngFileName(cacheFileName);
|
||||||
|
const source = Image.resolveAssetSource(assetModule);
|
||||||
|
if (!source?.uri) {
|
||||||
|
throw new Error('Cannot resolve image resource');
|
||||||
|
}
|
||||||
|
const { uri } = source;
|
||||||
|
if (uri.startsWith('file://')) {
|
||||||
|
return ensurePngFile(uri, pngCacheName);
|
||||||
|
}
|
||||||
|
if (Platform.OS === 'ios' && uri.startsWith('/')) {
|
||||||
|
return ensurePngFile(uri, pngCacheName);
|
||||||
|
}
|
||||||
|
if (Platform.OS === 'ios' && uri.startsWith('/')) {
|
||||||
|
return ensurePngFile(uri, pngCacheName);
|
||||||
|
}
|
||||||
|
if (uri.startsWith('http://') || uri.startsWith('https://')) {
|
||||||
|
const tmpDest = `${RNFS.CachesDirectoryPath}/tmp_${Date.now()}_${pngCacheName}`;
|
||||||
|
const { statusCode } = await RNFS.downloadFile({
|
||||||
|
fromUrl: uri,
|
||||||
|
toFile: tmpDest,
|
||||||
|
}).promise;
|
||||||
|
if (statusCode !== 200) {
|
||||||
|
throw new Error(`Download resource failed: ${pngCacheName}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await ensurePngFile(tmpDest, pngCacheName);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (await RNFS.exists(tmpDest)) {
|
||||||
|
await RNFS.unlink(tmpDest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
const assetPath = uri
|
||||||
|
.replace('asset:/', '')
|
||||||
|
.replace('file:///android_asset/', '');
|
||||||
|
const tmpDest = `${RNFS.CachesDirectoryPath}/tmp_${Date.now()}_${pngCacheName}`;
|
||||||
|
await RNFS.copyFileAssets(assetPath, tmpDest);
|
||||||
|
try {
|
||||||
|
return await ensurePngFile(tmpDest, pngCacheName);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (await RNFS.exists(tmpDest)) {
|
||||||
|
await RNFS.unlink(tmpDest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ensurePngFile(uri, pngCacheName);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=resolveAssetPath.js.map
|
||||||
1
dist/utils/resolveAssetPath.js.map
vendored
Normal file
1
dist/utils/resolveAssetPath.js.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"resolveAssetPath.js","sourceRoot":"","sources":["../../src/utils/resolveAssetPath.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,IAAI,MAAM,iBAAiB,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE1D,mDAAmD;AACnD,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,WAAmB,EACnB,aAAqB;IAErB,MAAM,YAAY,GAAG,aAAa,CAAC,aAAa,CAAC,CAAC;IAClD,MAAM,MAAM,GAAG,KAAK,CAAC,kBAAkB,CAAC,WAAW,CAAC,CAAC;IACrD,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE;QAChB,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;KAClD;IAED,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,CAAC;IAEvB,IAAI,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE;QAC7B,OAAO,aAAa,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;KACzC;IAED,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE;QAChD,OAAO,aAAa,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;KACzC;IAED,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE;QAChD,OAAO,aAAa,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;KACzC;IAED,IAAI,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE;QAC3D,MAAM,OAAO,GAAG,GAAG,IAAI,CAAC,mBAAmB,QAAQ,IAAI,CAAC,GAAG,EAAE,IAAI,YAAY,EAAE,CAAC;QAChF,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC;YAC7C,OAAO,EAAE,GAAG;YACZ,MAAM,EAAE,OAAO;SAChB,CAAC,CAAC,OAAO,CAAC;QACX,IAAI,UAAU,KAAK,GAAG,EAAE;YACtB,MAAM,IAAI,KAAK,CAAC,6BAA6B,YAAY,EAAE,CAAC,CAAC;SAC9D;QACD,IAAI;YACF,OAAO,MAAM,aAAa,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;SACnD;gBAAS;YACR,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE;gBAC9B,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;aAC5B;SACF;KACF;IAED,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,EAAE;QAC7B,MAAM,SAAS,GAAG,GAAG;aAClB,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC;aACtB,OAAO,CAAC,wBAAwB,EAAE,EAAE,CAAC,CAAC;QACzC,MAAM,OAAO,GAAG,GAAG,IAAI,CAAC,mBAAmB,QAAQ,IAAI,CAAC,GAAG,EAAE,IAAI,YAAY,EAAE,CAAC;QAChF,MAAM,IAAI,CAAC,cAAc,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QAC9C,IAAI;YACF,OAAO,MAAM,aAAa,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;SACnD;gBAAS;YACR,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE;gBAC9B,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;aAC5B;SACF;KACF;IAED,OAAO,aAAa,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;AAC1C,CAAC"}
|
||||||
3
dist/utils/resolveImageUrl.d.ts
vendored
Normal file
3
dist/utils/resolveImageUrl.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/** 将本地路径或远程 URL 解析为 OpenCV / RNFS 可读的 PNG 本地路径 */
|
||||||
|
export declare function resolveImageUrl(source: string, cacheFileName?: string): Promise<string>;
|
||||||
|
//# sourceMappingURL=resolveImageUrl.d.ts.map
|
||||||
1
dist/utils/resolveImageUrl.d.ts.map
vendored
Normal file
1
dist/utils/resolveImageUrl.d.ts.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"resolveImageUrl.d.ts","sourceRoot":"","sources":["../../src/utils/resolveImageUrl.ts"],"names":[],"mappings":"AAYA,kDAAkD;AAClD,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,MAAM,CAAC,CA8CjB"}
|
||||||
51
dist/utils/resolveImageUrl.js
vendored
Normal file
51
dist/utils/resolveImageUrl.js
vendored
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { Platform } from 'react-native';
|
||||||
|
import RNFS from 'react-native-fs';
|
||||||
|
import { ensurePngFile, isPngPath, normalizePath, toPngFileName } from './pngImage';
|
||||||
|
function hashUrl(url) {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < url.length; i++) {
|
||||||
|
hash = (hash * 31 + url.charCodeAt(i)) | 0;
|
||||||
|
}
|
||||||
|
return Math.abs(hash).toString(36);
|
||||||
|
}
|
||||||
|
/** 将本地路径或远程 URL 解析为 OpenCV / RNFS 可读的 PNG 本地路径 */
|
||||||
|
export async function resolveImageUrl(source, cacheFileName) {
|
||||||
|
const trimmed = source.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
throw new Error('Image URL is empty');
|
||||||
|
}
|
||||||
|
const pngCacheName = toPngFileName(cacheFileName ?? `img_${hashUrl(trimmed)}.png`);
|
||||||
|
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
||||||
|
const tmpDest = `${RNFS.CachesDirectoryPath}/tmp_${Date.now()}_${pngCacheName}`;
|
||||||
|
const { statusCode } = await RNFS.downloadFile({
|
||||||
|
fromUrl: trimmed,
|
||||||
|
toFile: tmpDest,
|
||||||
|
}).promise;
|
||||||
|
if (statusCode !== 200) {
|
||||||
|
throw new Error(`Download image failed: ${trimmed}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await ensurePngFile(tmpDest, pngCacheName);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (await RNFS.exists(tmpDest)) {
|
||||||
|
await RNFS.unlink(tmpDest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const normalized = normalizePath(trimmed);
|
||||||
|
if (await RNFS.exists(normalized) && isPngPath(normalized)) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
if (normalized.startsWith('file://')) {
|
||||||
|
return ensurePngFile(normalized, pngCacheName);
|
||||||
|
}
|
||||||
|
if (Platform.OS === 'ios' && normalized.startsWith('/')) {
|
||||||
|
return ensurePngFile(normalized, pngCacheName);
|
||||||
|
}
|
||||||
|
if (await RNFS.exists(normalized)) {
|
||||||
|
return ensurePngFile(normalized, pngCacheName);
|
||||||
|
}
|
||||||
|
return ensurePngFile(trimmed, pngCacheName);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=resolveImageUrl.js.map
|
||||||
1
dist/utils/resolveImageUrl.js.map
vendored
Normal file
1
dist/utils/resolveImageUrl.js.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"resolveImageUrl.js","sourceRoot":"","sources":["../../src/utils/resolveImageUrl.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,IAAI,MAAM,iBAAiB,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEpF,SAAS,OAAO,CAAC,GAAW;IAC1B,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QACnC,IAAI,GAAG,CAAC,IAAI,GAAG,EAAE,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;KAC5C;IACD,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AACrC,CAAC;AAED,kDAAkD;AAClD,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAc,EACd,aAAsB;IAEtB,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAC9B,IAAI,CAAC,OAAO,EAAE;QACZ,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;KACvC;IAED,MAAM,YAAY,GAAG,aAAa,CAChC,aAAa,IAAI,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,CAC/C,CAAC;IAEF,IAAI,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE;QACnE,MAAM,OAAO,GAAG,GAAG,IAAI,CAAC,mBAAmB,QAAQ,IAAI,CAAC,GAAG,EAAE,IAAI,YAAY,EAAE,CAAC;QAChF,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC;YAC7C,OAAO,EAAE,OAAO;YAChB,MAAM,EAAE,OAAO;SAChB,CAAC,CAAC,OAAO,CAAC;QACX,IAAI,UAAU,KAAK,GAAG,EAAE;YACtB,MAAM,IAAI,KAAK,CAAC,0BAA0B,OAAO,EAAE,CAAC,CAAC;SACtD;QACD,IAAI;YACF,OAAO,MAAM,aAAa,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;SACnD;gBAAS;YACR,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE;gBAC9B,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;aAC5B;SACF;KACF;IAED,MAAM,UAAU,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IAC1C,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,SAAS,CAAC,UAAU,CAAC,EAAE;QAC1D,OAAO,UAAU,CAAC;KACnB;IAED,IAAI,UAAU,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE;QACpC,OAAO,aAAa,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;KAChD;IAED,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE;QACvD,OAAO,aAAa,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;KAChD;IAED,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE;QACjC,OAAO,aAAa,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;KAChD;IAED,OAAO,aAAa,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;AAC9C,CAAC"}
|
||||||
4
dist/utils/skiaImage.d.ts
vendored
Normal file
4
dist/utils/skiaImage.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { type SkImage } from '@shopify/react-native-skia';
|
||||||
|
/** 连续 RGBA 缓冲 → Skia 图像(高低频 / 工作分辨率原图内存直传,避免 PNG 落盘) */
|
||||||
|
export declare function rgbaBufferToSkiaImage(buffer: Uint8Array, cols: number, rows: number): SkImage | null;
|
||||||
|
//# sourceMappingURL=skiaImage.d.ts.map
|
||||||
1
dist/utils/skiaImage.d.ts.map
vendored
Normal file
1
dist/utils/skiaImage.d.ts.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"skiaImage.d.ts","sourceRoot":"","sources":["../../src/utils/skiaImage.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,OAAO,EACb,MAAM,4BAA4B,CAAC;AAEpC,wDAAwD;AACxD,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,GAAG,IAAI,CAYhB"}
|
||||||
12
dist/utils/skiaImage.js
vendored
Normal file
12
dist/utils/skiaImage.js
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Skia, AlphaType, ColorType, } from '@shopify/react-native-skia';
|
||||||
|
/** 连续 RGBA 缓冲 → Skia 图像(高低频 / 工作分辨率原图内存直传,避免 PNG 落盘) */
|
||||||
|
export function rgbaBufferToSkiaImage(buffer, cols, rows) {
|
||||||
|
const data = Skia.Data.fromBytes(buffer);
|
||||||
|
return Skia.Image.MakeImage({
|
||||||
|
width: cols,
|
||||||
|
height: rows,
|
||||||
|
alphaType: AlphaType.Opaque,
|
||||||
|
colorType: ColorType.RGBA_8888,
|
||||||
|
}, data, cols * 4);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=skiaImage.js.map
|
||||||
1
dist/utils/skiaImage.js.map
vendored
Normal file
1
dist/utils/skiaImage.js.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"skiaImage.js","sourceRoot":"","sources":["../../src/utils/skiaImage.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,SAAS,EACT,SAAS,GAEV,MAAM,4BAA4B,CAAC;AAEpC,wDAAwD;AACxD,MAAM,UAAU,qBAAqB,CACnC,MAAkB,EAClB,IAAY,EACZ,IAAY;IAEZ,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACzC,OAAO,IAAI,CAAC,KAAK,CAAC,SAAS,CACzB;QACE,KAAK,EAAE,IAAI;QACX,MAAM,EAAE,IAAI;QACZ,SAAS,EAAE,SAAS,CAAC,MAAM;QAC3B,SAAS,EAAE,SAAS,CAAC,SAAS;KAC/B,EACD,IAAI,EACJ,IAAI,GAAG,CAAC,CACT,CAAC;AACJ,CAAC"}
|
||||||
6
index.js
6
index.js
@ -2,6 +2,12 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import 'react-native-gesture-handler';
|
||||||
|
|
||||||
|
import { Buffer } from 'buffer';
|
||||||
|
|
||||||
|
global.Buffer = global.Buffer || Buffer;
|
||||||
|
|
||||||
import {AppRegistry} from 'react-native';
|
import {AppRegistry} from 'react-native';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import {name as appName} from './app.json';
|
import {name as appName} from './app.json';
|
||||||
|
|||||||
@ -9,22 +9,12 @@
|
|||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
0C80B921A6F3F58F76C31292 /* libPods-MaskSegmentApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-MaskSegmentApp.a */; };
|
0C80B921A6F3F58F76C31292 /* libPods-MaskSegmentApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-MaskSegmentApp.a */; };
|
||||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||||
|
2EC56833612FED5731DD12F7 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; };
|
||||||
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
|
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
|
||||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
|
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
|
||||||
00E356F41AD99517003FC87E /* PBXContainerItemProxy */ = {
|
|
||||||
isa = PBXContainerItemProxy;
|
|
||||||
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
|
||||||
proxyType = 1;
|
|
||||||
remoteGlobalIDString = 13B07F861A680F5B00A75B9A;
|
|
||||||
remoteInfo = MaskSegmentApp;
|
|
||||||
};
|
|
||||||
/* End PBXContainerItemProxy section */
|
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
|
||||||
13B07F961A680F5B00A75B9A /* MaskSegmentApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MaskSegmentApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
13B07F961A680F5B00A75B9A /* MaskSegmentApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MaskSegmentApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = MaskSegmentApp/Images.xcassets; sourceTree = "<group>"; };
|
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = MaskSegmentApp/Images.xcassets; sourceTree = "<group>"; };
|
||||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = MaskSegmentApp/Info.plist; sourceTree = "<group>"; };
|
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = MaskSegmentApp/Info.plist; sourceTree = "<group>"; };
|
||||||
@ -49,14 +39,6 @@
|
|||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
00E356F01AD99517003FC87E /* Supporting Files */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
00E356F11AD99517003FC87E /* Info.plist */,
|
|
||||||
);
|
|
||||||
name = "Supporting Files";
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
13B07FAE1A68108700A75B9A /* MaskSegmentApp */ = {
|
13B07FAE1A68108700A75B9A /* MaskSegmentApp */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -172,19 +154,13 @@
|
|||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
00E356EC1AD99517003FC87E /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
13B07F8E1A680F5B00A75B9A /* Resources */ = {
|
13B07F8E1A680F5B00A75B9A /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
|
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
|
||||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||||
|
2EC56833612FED5731DD12F7 /* PrivacyInfo.xcprivacy in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -276,14 +252,6 @@
|
|||||||
};
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXTargetDependency section */
|
|
||||||
00E356F51AD99517003FC87E /* PBXTargetDependency */ = {
|
|
||||||
isa = PBXTargetDependency;
|
|
||||||
target = 13B07F861A680F5B00A75B9A /* MaskSegmentApp */;
|
|
||||||
targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */;
|
|
||||||
};
|
|
||||||
/* End PBXTargetDependency section */
|
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
13B07F941A680F5B00A75B9A /* Debug */ = {
|
13B07F941A680F5B00A75B9A /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
@ -408,7 +376,14 @@
|
|||||||
"-DFOLLY_CFG_NO_COROUTINES=1",
|
"-DFOLLY_CFG_NO_COROUTINES=1",
|
||||||
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
|
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
|
||||||
);
|
);
|
||||||
|
OTHER_LDFLAGS = (
|
||||||
|
"$(inherited)",
|
||||||
|
" ",
|
||||||
|
);
|
||||||
|
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||||
|
USE_HERMES = true;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@ -473,7 +448,13 @@
|
|||||||
"-DFOLLY_CFG_NO_COROUTINES=1",
|
"-DFOLLY_CFG_NO_COROUTINES=1",
|
||||||
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
|
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
|
||||||
);
|
);
|
||||||
|
OTHER_LDFLAGS = (
|
||||||
|
"$(inherited)",
|
||||||
|
" ",
|
||||||
|
);
|
||||||
|
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
USE_HERMES = true;
|
||||||
VALIDATE_PRODUCT = YES;
|
VALIDATE_PRODUCT = YES;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
|
|||||||
10
ios/MaskSegmentApp.xcworkspace/contents.xcworkspacedata
generated
Normal file
10
ios/MaskSegmentApp.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:MaskSegmentApp.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@ -34,6 +34,10 @@
|
|||||||
</dict>
|
</dict>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string></string>
|
<string></string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>需要访问相册以选择原图和掩码图</string>
|
||||||
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
|
<string>需要保存处理后的图片</string>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
<array>
|
<array>
|
||||||
<string>C617.1</string>
|
<string>C617.1</string>
|
||||||
|
<string>3B52.1</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
|
|||||||
@ -5,6 +5,13 @@ require Pod::Executable.execute_command('node', ['-p',
|
|||||||
{paths: [process.argv[1]]},
|
{paths: [process.argv[1]]},
|
||||||
)', __dir__]).strip
|
)', __dir__]).strip
|
||||||
|
|
||||||
|
# 使用 GitHub 镜像,避免 pod install clone 失败
|
||||||
|
require_relative 'offline_pods'
|
||||||
|
|
||||||
|
# 避免 cdn.cocoapods.org SSL 问题时,可改用 Specs 仓库(体积较大,首次较慢)
|
||||||
|
# source 'https://github.com/CocoaPods/Specs.git'
|
||||||
|
source 'https://cdn.cocoapods.org/'
|
||||||
|
|
||||||
platform :ios, min_ios_version_supported
|
platform :ios, min_ios_version_supported
|
||||||
prepare_react_native_project!
|
prepare_react_native_project!
|
||||||
|
|
||||||
|
|||||||
2259
ios/Podfile.lock
Normal file
2259
ios/Podfile.lock
Normal file
File diff suppressed because it is too large
Load Diff
27
ios/offline_pods.rb
Normal file
27
ios/offline_pods.rb
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# CocoaPods 拉取 React Native 第三方库时走 GitHub 镜像,解决 github.com SSL/连接失败
|
||||||
|
require_relative '../node_modules/react-native/scripts/cocoapods/helpers.rb'
|
||||||
|
|
||||||
|
github_mirror = ENV.fetch('COCOAPODS_GITHUB_MIRROR', 'https://ghproxy.net/https://github.com')
|
||||||
|
|
||||||
|
def mirror_git(base, repo_path)
|
||||||
|
"#{base}/#{repo_path}.git"
|
||||||
|
end
|
||||||
|
|
||||||
|
Helpers::Constants.set_double_conversion_config(
|
||||||
|
:git => mirror_git(github_mirror, 'google/double-conversion'),
|
||||||
|
)
|
||||||
|
Helpers::Constants.set_glog_config(
|
||||||
|
:git => mirror_git(github_mirror, 'google/glog'),
|
||||||
|
)
|
||||||
|
Helpers::Constants.set_folly_config(
|
||||||
|
:git => mirror_git(github_mirror, 'facebook/folly'),
|
||||||
|
)
|
||||||
|
Helpers::Constants.set_boost_config(
|
||||||
|
:git => mirror_git(github_mirror, 'react-native-community/boost-for-react-native'),
|
||||||
|
)
|
||||||
|
Helpers::Constants.set_fmt_config(
|
||||||
|
:git => mirror_git(github_mirror, 'fmtlib/fmt'),
|
||||||
|
)
|
||||||
|
Helpers::Constants.set_fast_float_config(
|
||||||
|
:git => mirror_git(github_mirror, 'fastfloat/fast_float'),
|
||||||
|
)
|
||||||
15
ios/scripts/setup_vendor_pods.sh
Executable file
15
ios/scripts/setup_vendor_pods.sh
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 可选:预检 GitHub 镜像是否可用(默认镜像见 ios/offline_pods.rb)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MIRROR="${COCOAPODS_GITHUB_MIRROR:-https://ghproxy.net/https://github.com}"
|
||||||
|
TEST_URL="${MIRROR}/google/double-conversion.git"
|
||||||
|
|
||||||
|
echo "Checking CocoaPods GitHub mirror..."
|
||||||
|
if git ls-remote "$TEST_URL" HEAD >/dev/null 2>&1; then
|
||||||
|
echo "✓ Mirror OK: $MIRROR"
|
||||||
|
else
|
||||||
|
echo "✗ Mirror unreachable: $MIRROR"
|
||||||
|
echo " Try: export COCOAPODS_GITHUB_MIRROR='https://mirror.ghproxy.com/https://github.com'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
13350
package-lock.json
generated
Normal file
13350
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
85
package.json
85
package.json
@ -1,17 +1,56 @@
|
|||||||
{
|
{
|
||||||
"name": "MaskSegmentApp",
|
"name": "react-native-mask-segment-canvas",
|
||||||
"version": "0.0.1",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"description": "React Native 掩码分区交互库:OpenCV 语义分割 + SkSL Shader 上色",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"src",
|
||||||
|
"patches",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
"ios": "react-native run-ios",
|
"ios": "react-native run-ios",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
|
"pod:check": "bash ios/scripts/setup_vendor_pods.sh",
|
||||||
|
"pod:install": "cd ios && pod install",
|
||||||
|
"postinstall": "patch-package",
|
||||||
"start": "react-native start",
|
"start": "react-native start",
|
||||||
"test": "jest"
|
"test": "jest",
|
||||||
|
"build": "tsc -p tsconfig.build.json",
|
||||||
|
"prepare": "npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "19.0.0",
|
"buffer": "^6.0.3",
|
||||||
"react-native": "0.79.0"
|
"upng-js": "^2.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@shopify/react-native-skia": ">=2.0.0",
|
||||||
|
"react": ">=18",
|
||||||
|
"react-native": ">=0.74",
|
||||||
|
"react-native-fast-opencv": ">=0.4.8",
|
||||||
|
"react-native-fs": ">=2.20.0",
|
||||||
|
"react-native-gesture-handler": ">=2.16.0",
|
||||||
|
"react-native-image-picker": ">=7.0.0",
|
||||||
|
"react-native-reanimated": ">=3.0.0",
|
||||||
|
"react-native-safe-area-context": ">=4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-native-image-picker": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native-safe-area-context": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
@ -20,20 +59,42 @@
|
|||||||
"@react-native-community/cli": "18.0.0",
|
"@react-native-community/cli": "18.0.0",
|
||||||
"@react-native-community/cli-platform-android": "18.0.0",
|
"@react-native-community/cli-platform-android": "18.0.0",
|
||||||
"@react-native-community/cli-platform-ios": "18.0.0",
|
"@react-native-community/cli-platform-ios": "18.0.0",
|
||||||
"@react-native/babel-preset": "0.79.0",
|
"@react-native/babel-preset": "0.79.4",
|
||||||
"@react-native/eslint-config": "0.79.0",
|
"@react-native/eslint-config": "0.79.4",
|
||||||
"@react-native/metro-config": "0.79.0",
|
"@react-native/metro-config": "0.79.4",
|
||||||
"@react-native/typescript-config": "0.79.0",
|
"@react-native/typescript-config": "0.79.4",
|
||||||
|
"@shopify/react-native-skia": "^2.6.4",
|
||||||
"@types/jest": "^29.5.13",
|
"@types/jest": "^29.5.13",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-test-renderer": "^19.0.0",
|
"@types/react-test-renderer": "^19.0.0",
|
||||||
"eslint": "^8.19.0",
|
"eslint": "^8.19.0",
|
||||||
"jest": "^29.6.3",
|
"jest": "^29.6.3",
|
||||||
|
"patch-package": "^8.0.1",
|
||||||
"prettier": "2.8.8",
|
"prettier": "2.8.8",
|
||||||
|
"react": "19.0.0",
|
||||||
|
"react-native": "0.79.4",
|
||||||
|
"react-native-fast-opencv": "^0.4.8",
|
||||||
|
"react-native-fs": "^2.20.0",
|
||||||
|
"react-native-gesture-handler": "^2.16.0",
|
||||||
|
"react-native-image-picker": "^8.2.1",
|
||||||
|
"react-native-reanimated": "^3.19.5",
|
||||||
|
"react-native-safe-area-context": "^5.8.0",
|
||||||
"react-test-renderer": "19.0.0",
|
"react-test-renderer": "19.0.0",
|
||||||
"typescript": "5.0.4"
|
"typescript": "5.0.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
},
|
||||||
}
|
"keywords": [
|
||||||
|
"react-native",
|
||||||
|
"mask-segmentation",
|
||||||
|
"semantic-segmentation",
|
||||||
|
"opencv",
|
||||||
|
"skia",
|
||||||
|
"shader",
|
||||||
|
"image-editing",
|
||||||
|
"canvas",
|
||||||
|
"paint"
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
|||||||
122
patches/react-native-fast-opencv+0.4.8.patch
Normal file
122
patches/react-native-fast-opencv+0.4.8.patch
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
diff --git a/node_modules/react-native-fast-opencv/cpp/ConvertImage.cpp b/node_modules/react-native-fast-opencv/cpp/ConvertImage.cpp
|
||||||
|
index ea50bc0..729d648 100644
|
||||||
|
--- a/node_modules/react-native-fast-opencv/cpp/ConvertImage.cpp
|
||||||
|
+++ b/node_modules/react-native-fast-opencv/cpp/ConvertImage.cpp
|
||||||
|
@@ -149,6 +149,39 @@ string ImageConverter::mat2str(const Mat& m, std::string &format)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
+Mat ImageConverter::ensure8U(const Mat& img)
|
||||||
|
+{
|
||||||
|
+ if (img.empty() || img.depth() == CV_8U) {
|
||||||
|
+ return img;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ // Semantic mask PNG: 16-bit RGB stores 8-bit labels as value×257.
|
||||||
|
+ if (img.depth() == CV_16U && img.channels() >= 3) {
|
||||||
|
+ cv::Mat img8;
|
||||||
|
+ if (img.channels() == 4) {
|
||||||
|
+ cv::Mat bgr16;
|
||||||
|
+ cv::cvtColor(img, bgr16, cv::COLOR_BGRA2BGR);
|
||||||
|
+ bgr16.convertTo(img8, CV_8U, 1.0 / 257.0);
|
||||||
|
+ } else {
|
||||||
|
+ img.convertTo(img8, CV_8U, 1.0 / 257.0);
|
||||||
|
+ }
|
||||||
|
+ return img8;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ Mat dst;
|
||||||
|
+ const int outType = CV_MAKETYPE(CV_8U, img.channels());
|
||||||
|
+ double alpha = 1.0;
|
||||||
|
+
|
||||||
|
+ if (img.depth() == CV_16U || img.depth() == CV_16S) {
|
||||||
|
+ alpha = 255.0 / 65535.0;
|
||||||
|
+ } else if (img.depth() == CV_32F || img.depth() == CV_64F) {
|
||||||
|
+ alpha = 255.0;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ img.convertTo(dst, outType, alpha);
|
||||||
|
+ return dst;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
Mat ImageConverter::str2mat(const string& s)
|
||||||
|
{
|
||||||
|
// Decode data
|
||||||
|
@@ -157,5 +190,5 @@ Mat ImageConverter::str2mat(const string& s)
|
||||||
|
|
||||||
|
cv::Mat img = cv::imdecode(data, IMREAD_UNCHANGED);
|
||||||
|
|
||||||
|
- return img;
|
||||||
|
+ return ensure8U(img);
|
||||||
|
}
|
||||||
|
diff --git a/node_modules/react-native-fast-opencv/cpp/ConvertImage.hpp b/node_modules/react-native-fast-opencv/cpp/ConvertImage.hpp
|
||||||
|
index 79df697..9bd84f2 100644
|
||||||
|
--- a/node_modules/react-native-fast-opencv/cpp/ConvertImage.hpp
|
||||||
|
+++ b/node_modules/react-native-fast-opencv/cpp/ConvertImage.hpp
|
||||||
|
@@ -23,6 +23,8 @@ class ImageConverter {
|
||||||
|
public:
|
||||||
|
static cv::Mat str2mat(const string& imageBase64);
|
||||||
|
static string mat2str(const Mat& img, std::string &format);
|
||||||
|
+ /** 16-bit / float PNG 等非常规 depth 统一缩放到 8-bit,避免 matToBuffer(uint8) 误读 */
|
||||||
|
+ static cv::Mat ensure8U(const Mat& img);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::string base64_encode(uchar const* bytesToEncode, unsigned int inLen);
|
||||||
|
diff --git a/node_modules/react-native-fast-opencv/cpp/FOCV_Object.cpp b/node_modules/react-native-fast-opencv/cpp/FOCV_Object.cpp
|
||||||
|
index e83166d..5d3268e 100644
|
||||||
|
--- a/node_modules/react-native-fast-opencv/cpp/FOCV_Object.cpp
|
||||||
|
+++ b/node_modules/react-native-fast-opencv/cpp/FOCV_Object.cpp
|
||||||
|
@@ -183,20 +183,20 @@ jsi::Object FOCV_Object::convertToJSI(jsi::Runtime& runtime, const jsi::Value* a
|
||||||
|
|
||||||
|
switch(hashString(objectType.c_str(), objectType.size())) {
|
||||||
|
case hashString("mat", 3): {
|
||||||
|
- auto mat = *FOCV_Storage::get<cv::Mat>(id);
|
||||||
|
+ const cv::Mat& stored = *FOCV_Storage::get<cv::Mat>(id);
|
||||||
|
std::string format = "jpeg";
|
||||||
|
|
||||||
|
if(arguments[1].isString()) {
|
||||||
|
format = arguments[1].asString(runtime).utf8(runtime);
|
||||||
|
}
|
||||||
|
|
||||||
|
- mat.convertTo(mat, CV_8U);
|
||||||
|
+ const cv::Mat exportMat = ImageConverter::ensure8U(stored);
|
||||||
|
|
||||||
|
- value.setProperty(runtime, "base64", jsi::String::createFromUtf8(runtime, ImageConverter::mat2str(mat, format)));
|
||||||
|
- value.setProperty(runtime, "size", jsi::Value(mat.size));
|
||||||
|
- value.setProperty(runtime, "cols", jsi::Value(mat.cols));
|
||||||
|
- value.setProperty(runtime, "rows", jsi::Value(mat.rows));
|
||||||
|
- value.setProperty(runtime, "type", jsi::Value(mat.type()));
|
||||||
|
+ value.setProperty(runtime, "base64", jsi::String::createFromUtf8(runtime, ImageConverter::mat2str(exportMat, format)));
|
||||||
|
+ value.setProperty(runtime, "size", jsi::Value(stored.size));
|
||||||
|
+ value.setProperty(runtime, "cols", jsi::Value(stored.cols));
|
||||||
|
+ value.setProperty(runtime, "rows", jsi::Value(stored.rows));
|
||||||
|
+ value.setProperty(runtime, "type", jsi::Value(stored.type()));
|
||||||
|
} break;
|
||||||
|
case hashString("mat_vector", 10): {
|
||||||
|
auto mats = *FOCV_Storage::get<std::vector<cv::Mat>>(id);
|
||||||
|
diff --git a/node_modules/react-native-fast-opencv/cpp/react-native-fast-opencv.cpp b/node_modules/react-native-fast-opencv/cpp/react-native-fast-opencv.cpp
|
||||||
|
index f0186e5..1289d57 100644
|
||||||
|
--- a/node_modules/react-native-fast-opencv/cpp/react-native-fast-opencv.cpp
|
||||||
|
+++ b/node_modules/react-native-fast-opencv/cpp/react-native-fast-opencv.cpp
|
||||||
|
@@ -160,7 +160,8 @@ jsi::Value OpenCVPlugin::get(jsi::Runtime& runtime, const jsi::PropNameID& propN
|
||||||
|
size_t count) -> jsi::Object {
|
||||||
|
|
||||||
|
auto id = FOCV_JsiObject::id_from_wrap(runtime, arguments[0]);
|
||||||
|
- auto mat = *FOCV_Storage::get<cv::Mat>(id);
|
||||||
|
+ const cv::Mat& stored = *FOCV_Storage::get<cv::Mat>(id);
|
||||||
|
+ cv::Mat mat = stored.isContinuous() ? stored : stored.clone();
|
||||||
|
|
||||||
|
jsi::Object value(runtime);
|
||||||
|
|
||||||
|
@@ -169,6 +170,11 @@ jsi::Value OpenCVPlugin::get(jsi::Runtime& runtime, const jsi::PropNameID& propN
|
||||||
|
value.setProperty(runtime, "channels", jsi::Value(mat.channels()));
|
||||||
|
|
||||||
|
auto type = arguments[1].asString(runtime).utf8(runtime);
|
||||||
|
+
|
||||||
|
+ if(type == "uint8" && mat.depth() != CV_8U) {
|
||||||
|
+ mat = ImageConverter::ensure8U(mat);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
auto size = mat.cols * mat.rows * mat.channels();
|
||||||
|
|
||||||
|
if(type == "uint8") {
|
||||||
2832
src/components/MaskSegmentCanvas.tsx
Normal file
2832
src/components/MaskSegmentCanvas.tsx
Normal file
File diff suppressed because it is too large
Load Diff
216
src/components/MaskSegmentCanvas.types.ts
Normal file
216
src/components/MaskSegmentCanvas.types.ts
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
|
||||||
|
import type { SegmentRegion } from '../utils/maskSegmentation';
|
||||||
|
import type { MaskSemanticColor } from '../utils/maskSemanticPalette';
|
||||||
|
|
||||||
|
export type BgrColor = { b: number; g: number; r: number };
|
||||||
|
|
||||||
|
export type MaskSegmentWatchState =
|
||||||
|
| 'init'
|
||||||
|
| 'images_loaded'
|
||||||
|
| 'mask_aligned'
|
||||||
|
| 'mask_sampled'
|
||||||
|
| 'regions_ready'
|
||||||
|
| 'layers_ready'
|
||||||
|
| 'interactive'
|
||||||
|
| 'mask_paths_ready'
|
||||||
|
| 'error';
|
||||||
|
|
||||||
|
export type MaskSegmentWatchDetail = {
|
||||||
|
regionCount?: number;
|
||||||
|
maskPathsReady?: boolean;
|
||||||
|
freqLayersReady?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PipelinePreset = 'high' | 'medium' | 'low';
|
||||||
|
|
||||||
|
export type PipelineConfig = {
|
||||||
|
maxImageLongSide?: number;
|
||||||
|
/** 高低频 LAB 处理最长边(可低于 maxImageLongSide,Shader 拉伸采样) */
|
||||||
|
paintFreqMaxLongSide?: number;
|
||||||
|
originPreviewMaxLongSide?: number;
|
||||||
|
maskPathMaxLongSide?: number;
|
||||||
|
minContourArea?: number;
|
||||||
|
contourApproxEpsilon?: number;
|
||||||
|
maxRegions?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MaskSegmentConfig = {
|
||||||
|
semanticColors?: MaskSemanticColor[];
|
||||||
|
baseboardMaxColorDist?: number;
|
||||||
|
blackThreshold?: number;
|
||||||
|
quantStep?: number;
|
||||||
|
baseboardStripQuantKeys?: string[];
|
||||||
|
wallQuantKeys?: string[];
|
||||||
|
cabinetQuantKeys?: string[];
|
||||||
|
maxRegionColors?: number;
|
||||||
|
secondarySemanticNames?: string[];
|
||||||
|
secondaryMinPixelRatio?: number;
|
||||||
|
junctionHRadiusPx?: number;
|
||||||
|
junctionVRadiusPx?: number;
|
||||||
|
kickBridgeHalfWPx?: number;
|
||||||
|
baseboardJunctionRowMarginPx?: number;
|
||||||
|
baseboardJunctionVReachPx?: number;
|
||||||
|
baseboardMinRunPx?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PaintConfig = {
|
||||||
|
palette?: BgrColor[];
|
||||||
|
colorBaseOpacity?: number;
|
||||||
|
lLightOpacity?: number;
|
||||||
|
textureOpacity?: number;
|
||||||
|
lLowBlurKernel?: number;
|
||||||
|
lLowContrast?: number;
|
||||||
|
lLowBrightness?: number;
|
||||||
|
lHighGain?: number;
|
||||||
|
maskFeatherColor?: number;
|
||||||
|
maskFeatherTexture?: number;
|
||||||
|
regionOverlayFill?: string;
|
||||||
|
regionOutlineStrokeWidth?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InteractionConfig = {
|
||||||
|
kickMaskPickRadiusPx?: number;
|
||||||
|
pickMapSearchRadiusPx?: number;
|
||||||
|
thinStripPadding?: number;
|
||||||
|
regionPadding?: number;
|
||||||
|
initRegionFlashMs?: number;
|
||||||
|
enableInitRegionFlash?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PaintedRegionRecord = {
|
||||||
|
regionId: number;
|
||||||
|
regionName: string;
|
||||||
|
color: BgrColor;
|
||||||
|
configJson?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MaskSegmentSession = {
|
||||||
|
version: 1;
|
||||||
|
originUrl: string;
|
||||||
|
maskUrl: string;
|
||||||
|
painted: PaintedRegionRecord[];
|
||||||
|
paintHistory: number[];
|
||||||
|
currentColor?: BgrColor;
|
||||||
|
currentColorConfigJson?: Record<string, unknown>;
|
||||||
|
savedAt: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SavePaintResult = {
|
||||||
|
filePath: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
paintedCount: number;
|
||||||
|
previewPath?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SavePaintOptions = {
|
||||||
|
destDir?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PaintSuccessPayload = {
|
||||||
|
kind: 'painted';
|
||||||
|
regionId: number;
|
||||||
|
regionName: string;
|
||||||
|
color: BgrColor;
|
||||||
|
configJson?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PaintBrushRequiredPayload = {
|
||||||
|
kind: 'brush_required';
|
||||||
|
/** 未选笔刷时的提示文案 */
|
||||||
|
hint: string;
|
||||||
|
regionId: number;
|
||||||
|
regionName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PaintCallbackPayload = PaintSuccessPayload | PaintBrushRequiredPayload;
|
||||||
|
|
||||||
|
export type OverlayButtonRenderProps = {
|
||||||
|
onPress: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MaskSegmentCanvasRef = {
|
||||||
|
reset: () => void;
|
||||||
|
swap: (showOrigin?: boolean) => void;
|
||||||
|
save: (options?: SavePaintOptions) => Promise<SavePaintResult>;
|
||||||
|
session: () => MaskSegmentSession;
|
||||||
|
loadSession: (session: MaskSegmentSession) => void;
|
||||||
|
setPaintColor: (color: BgrColor, configJson?: Record<string, unknown>) => void;
|
||||||
|
setMaskConfig: (config: MaskSegmentConfig) => void;
|
||||||
|
clearAllPaint: () => void;
|
||||||
|
/** Undo the most recent single coloring (paint) step. Distinct from clearAllPaint (full reset). */
|
||||||
|
undoSelection?: () => void;
|
||||||
|
resegment: () => Promise<void>;
|
||||||
|
getRegions: () => SegmentRegion[];
|
||||||
|
getPaintedRegions: () => PaintedRegionRecord[];
|
||||||
|
/** Returns the most recent auto-export or save() result, if any. */
|
||||||
|
getLastExport?: () => SavePaintResult | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MaskSegmentCanvasProps = {
|
||||||
|
originUrl?: string;
|
||||||
|
maskUrl?: string;
|
||||||
|
/** @deprecated 使用 originUrl */
|
||||||
|
originImgPath?: string;
|
||||||
|
/** @deprecated 使用 maskUrl */
|
||||||
|
maskImgPath?: string;
|
||||||
|
/** 掩码语义识别色,初始化配置;等同 maskConfig.semanticColors */
|
||||||
|
semanticColors?: MaskSemanticColor[];
|
||||||
|
/** 分区虚线高亮色,初始化配置;等同 paintConfig.regionOverlayFill */
|
||||||
|
regionOutlineColor?: string;
|
||||||
|
maskConfig?: MaskSegmentConfig;
|
||||||
|
/** Performance preset (high / medium / low). Merged with pipelineConfig overrides. */
|
||||||
|
pipelinePreset?: PipelinePreset;
|
||||||
|
pipelineConfig?: PipelineConfig;
|
||||||
|
paintConfig?: PaintConfig;
|
||||||
|
interactionConfig?: InteractionConfig;
|
||||||
|
initialSession?: MaskSegmentSession;
|
||||||
|
initialPaintColor?: BgrColor;
|
||||||
|
initialPaintConfigJson?: Record<string, unknown>;
|
||||||
|
showDebugPickers?: boolean;
|
||||||
|
showToolbar?: boolean;
|
||||||
|
showColorBar?: boolean;
|
||||||
|
showStatusRow?: boolean;
|
||||||
|
showOverlayButtons?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
|
canvasStyle?: StyleProp<ViewStyle>;
|
||||||
|
/**
|
||||||
|
* Max container height available for this component (px). When set, the SDK
|
||||||
|
* computes canvas dimensions as a fit-contain within (screenWidth - 20, maxHeight)
|
||||||
|
* instead of using the full image aspect at full screen width. This prevents
|
||||||
|
* internal ScrollView scrolling for tall images.
|
||||||
|
*/
|
||||||
|
maxHeight?: number;
|
||||||
|
undoButtonStyle?: StyleProp<ViewStyle>;
|
||||||
|
compareButtonStyle?: StyleProp<ViewStyle>;
|
||||||
|
undoButtonTextStyle?: StyleProp<TextStyle>;
|
||||||
|
compareButtonTextStyle?: StyleProp<TextStyle>;
|
||||||
|
undoButtonText?: string;
|
||||||
|
compareButtonText?: string;
|
||||||
|
compareExitButtonText?: string;
|
||||||
|
renderUndoButton?: (props: OverlayButtonRenderProps) => ReactNode;
|
||||||
|
renderCompareButton?: (props: OverlayButtonRenderProps) => ReactNode;
|
||||||
|
onWatch?: (
|
||||||
|
state: MaskSegmentWatchState,
|
||||||
|
durationMs: number,
|
||||||
|
detail?: MaskSegmentWatchDetail,
|
||||||
|
) => void;
|
||||||
|
onPaintCallback?: (payload: PaintCallbackPayload) => void;
|
||||||
|
onError?: (message: string, error?: unknown) => void;
|
||||||
|
/**
|
||||||
|
* When true, once the canvas reaches a ready interactive state (segmentation complete
|
||||||
|
* + any initialSession / painted colors applied), the SDK will automatically call its
|
||||||
|
* internal save pipeline to produce the recolored result image and fire onExported.
|
||||||
|
* This moves "auto-generate After preview" capability inside the SDK.
|
||||||
|
*/
|
||||||
|
autoExportOnReady?: boolean;
|
||||||
|
/** Fired by SDK when autoExportOnReady produced a result (the recolored file). */
|
||||||
|
onExported?: (result: SavePaintResult) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { SegmentRegion, MaskSemanticColor };
|
||||||
19
src/globals.d.ts
vendored
Normal file
19
src/globals.d.ts
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// 补充 React Native 运行时可用的全局 API 类型声明
|
||||||
|
|
||||||
|
declare var performance: {
|
||||||
|
now(): number;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare function btoa(data: string): string;
|
||||||
|
declare function atob(data: string): string;
|
||||||
|
|
||||||
|
declare var TextEncoder: {
|
||||||
|
prototype: TextEncoder;
|
||||||
|
new (): TextEncoder;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare interface TextEncoder {
|
||||||
|
encode(input?: string): Uint8Array;
|
||||||
|
encodeInto(input: string, dest: Uint8Array): { read: number; written: number };
|
||||||
|
readonly encoding: string;
|
||||||
|
}
|
||||||
45
src/index.ts
Normal file
45
src/index.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
export { default } from './components/MaskSegmentCanvas';
|
||||||
|
export type {
|
||||||
|
BgrColor,
|
||||||
|
InteractionConfig,
|
||||||
|
MaskSegmentCanvasProps,
|
||||||
|
MaskSegmentCanvasRef,
|
||||||
|
MaskSegmentConfig,
|
||||||
|
MaskSegmentSession,
|
||||||
|
MaskSegmentWatchDetail,
|
||||||
|
MaskSegmentWatchState,
|
||||||
|
MaskSemanticColor,
|
||||||
|
OverlayButtonRenderProps,
|
||||||
|
PaintBrushRequiredPayload,
|
||||||
|
PaintCallbackPayload,
|
||||||
|
PaintSuccessPayload,
|
||||||
|
PaintConfig,
|
||||||
|
PaintedRegionRecord,
|
||||||
|
PipelineConfig,
|
||||||
|
PipelinePreset,
|
||||||
|
SavePaintOptions,
|
||||||
|
SavePaintResult,
|
||||||
|
SegmentRegion,
|
||||||
|
} from './components/MaskSegmentCanvas.types';
|
||||||
|
export {
|
||||||
|
BASEBOARD_SEMANTIC_NAME,
|
||||||
|
MASK_SEMANTIC_COLORS,
|
||||||
|
} from './utils/maskSemanticPalette';
|
||||||
|
export {
|
||||||
|
createRuntimeConfig,
|
||||||
|
DEFAULT_INTERACTION_CONFIG,
|
||||||
|
DEFAULT_MASK_CONFIG,
|
||||||
|
DEFAULT_PAINT_CONFIG,
|
||||||
|
DEFAULT_PIPELINE_CONFIG,
|
||||||
|
PIPELINE_HIGH,
|
||||||
|
PIPELINE_LOW,
|
||||||
|
PIPELINE_MEDIUM,
|
||||||
|
PIPELINE_PRESETS,
|
||||||
|
getMaskSegmentRuntimeConfig,
|
||||||
|
resolvePipelineConfig,
|
||||||
|
setMaskSegmentRuntimeConfig,
|
||||||
|
} from './utils/maskSegmentRuntime';
|
||||||
|
export {
|
||||||
|
prewarmPngBgrCache,
|
||||||
|
prewarmPngBgrCacheAsync,
|
||||||
|
} from './utils/pngImage';
|
||||||
71
src/shaders/regionPaint.sksl.ts
Normal file
71
src/shaders/regionPaint.sksl.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/** SkSL:分区上色(保留原图明暗与纹理,按 paintColorMap 叠色) */
|
||||||
|
export const REGION_PAINT_SKSL = `
|
||||||
|
uniform shader originTex;
|
||||||
|
uniform shader paintColorTex;
|
||||||
|
uniform shader lowFreqTex;
|
||||||
|
uniform shader highFreqTex;
|
||||||
|
|
||||||
|
uniform float colorBaseOpacity;
|
||||||
|
uniform float lLightOpacity;
|
||||||
|
uniform float textureOpacity;
|
||||||
|
uniform float showOrigin;
|
||||||
|
|
||||||
|
float luminance(half3 c) {
|
||||||
|
return dot(c, half3(0.2126, 0.7152, 0.0722));
|
||||||
|
}
|
||||||
|
|
||||||
|
half3 setLuminance(float lum, half3 base) {
|
||||||
|
float diff = lum - luminance(base);
|
||||||
|
return base + diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
half3 luminosityBlend(half3 base, half3 blend) {
|
||||||
|
return setLuminance(luminance(blend), base);
|
||||||
|
}
|
||||||
|
|
||||||
|
half3 overlayBlend(half3 base, half3 blend) {
|
||||||
|
half3 low = 2.0 * base * blend;
|
||||||
|
half3 high = 1.0 - 2.0 * (1.0 - base) * (1.0 - blend);
|
||||||
|
return mix(low, high, step(half3(0.5), base));
|
||||||
|
}
|
||||||
|
|
||||||
|
half4 main(float2 coord) {
|
||||||
|
half4 origin = originTex.eval(coord);
|
||||||
|
if (showOrigin > 0.5) {
|
||||||
|
return origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
half4 paintEntry = paintColorTex.eval(coord);
|
||||||
|
// The paintColorMap uses Unpremul alpha: painted pixels → (R,G,B,255),
|
||||||
|
// transparent pixels → (0,0,0,0). GPU bilinear sampling interpolates
|
||||||
|
// straight-alpha values, so at boundaries rgb = trueColor * sampled.a
|
||||||
|
// (contaminated with black from the transparent neighbour).
|
||||||
|
//
|
||||||
|
// Unpremultiply to recover the true paint color:
|
||||||
|
// trueColor = sampled.rgb / sampled.a
|
||||||
|
// This eliminates dark fringing at region boundaries.
|
||||||
|
float pa = paintEntry.a + 0.0001;
|
||||||
|
paintEntry.rgb /= pa;
|
||||||
|
|
||||||
|
// Gate sub-pixel alpha to kill residual sampling noise.
|
||||||
|
// Thresholds are deliberately low (≈1.3–3.8 in byte space) because
|
||||||
|
// post-unpremul the RGB is correct at any alpha ≥ 0 — we only need
|
||||||
|
// to suppress samples that contribute negligibly to the final blend.
|
||||||
|
// Using *= preserves the smooth edge; higher-alpha samples pass through.
|
||||||
|
paintEntry.a *= smoothstep(0.005, 0.015, paintEntry.a);
|
||||||
|
|
||||||
|
half3 paintRgb = paintEntry.rgb;
|
||||||
|
half lowL = lowFreqTex.eval(coord).r;
|
||||||
|
half highL = highFreqTex.eval(coord).r;
|
||||||
|
|
||||||
|
half3 base = paintRgb * colorBaseOpacity;
|
||||||
|
half3 lit = luminosityBlend(base, half3(lowL));
|
||||||
|
half3 withLight = mix(base, lit, lLightOpacity);
|
||||||
|
half3 tex = overlayBlend(withLight, half3(highL));
|
||||||
|
half3 finalRgb = mix(withLight, tex, textureOpacity);
|
||||||
|
|
||||||
|
// Soft edge blend using the (feathered) alpha from the paint color map as coverage.
|
||||||
|
half3 blended = mix(origin.rgb, finalRgb, paintEntry.a);
|
||||||
|
return half4(blended, 1.0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
33
src/upng-js.d.ts
vendored
Normal file
33
src/upng-js.d.ts
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
declare module 'upng-js' {
|
||||||
|
export interface UPNGImage {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
depth: number;
|
||||||
|
ctype: number;
|
||||||
|
frames: number;
|
||||||
|
tabs: Record<string, string>;
|
||||||
|
data: ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decode(buffer: ArrayBuffer): UPNGImage;
|
||||||
|
|
||||||
|
export function toRGBA8(img: UPNGImage): ArrayBuffer[];
|
||||||
|
|
||||||
|
export function encode(
|
||||||
|
imgs: ArrayBuffer[],
|
||||||
|
w: number,
|
||||||
|
h: number,
|
||||||
|
cnum: number,
|
||||||
|
dels?: number[],
|
||||||
|
): ArrayBuffer;
|
||||||
|
|
||||||
|
export function encodeLL(
|
||||||
|
imgs: ArrayBuffer[],
|
||||||
|
w: number,
|
||||||
|
h: number,
|
||||||
|
cc: number,
|
||||||
|
ac: number,
|
||||||
|
depth: number,
|
||||||
|
dels?: number[],
|
||||||
|
): ArrayBuffer;
|
||||||
|
}
|
||||||
201
src/utils/compositePaintedImage.ts
Normal file
201
src/utils/compositePaintedImage.ts
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import { Buffer } from 'buffer';
|
||||||
|
import RNFS from 'react-native-fs';
|
||||||
|
import type { BgrColor, SavePaintResult } from '../components/MaskSegmentCanvas.types';
|
||||||
|
import type { SkImage } from '@shopify/react-native-skia';
|
||||||
|
// upng-js: used for PNG encode of CPU recolor.
|
||||||
|
import UPNG from 'upng-js';
|
||||||
|
import { renderPaintedImageOffscreen } from './paintShaderRuntime';
|
||||||
|
import { writePngBase64ToFile, writePngBytesToFile } from './exportUtils';
|
||||||
|
|
||||||
|
export type CompositePaintInput = {
|
||||||
|
originBuffer: Uint8Array;
|
||||||
|
cols: number;
|
||||||
|
rows: number;
|
||||||
|
pickBuffer: Uint8Array;
|
||||||
|
paintedRegions: Map<number, BgrColor>;
|
||||||
|
destDir?: string;
|
||||||
|
/**
|
||||||
|
* Preferred path for rich export: PNG base64 from makeImageSnapshot() — written
|
||||||
|
* directly to disk without an extra decode/re-encode round trip.
|
||||||
|
*/
|
||||||
|
exportPngBase64?: string;
|
||||||
|
/**
|
||||||
|
* Preferred path for rich export: if the caller (MaskSegmentCanvas) provides bytes
|
||||||
|
* that were produced by makeImageSnapshot() on a high-resolution Canvas rendering the
|
||||||
|
* exact same PaintShaderLayer + regionPaint SkSL at work resolution, we write them
|
||||||
|
* directly. This captures the live editor 质感 (lighting + high/low-freq texture)
|
||||||
|
* without CPU pixel math and without a second declarative drawAsImage.
|
||||||
|
*/
|
||||||
|
exportPngBytes?: Uint8Array;
|
||||||
|
/**
|
||||||
|
* Fallback rich path (when no pre-captured snapshot bytes): pass the live textures
|
||||||
|
* so we can try renderPaintedImageOffscreen (drawAsImage with the shader tree).
|
||||||
|
*/
|
||||||
|
shaderTextures?: {
|
||||||
|
originImage: SkImage;
|
||||||
|
paintColorMap: SkImage;
|
||||||
|
lowFreqImage: SkImage;
|
||||||
|
highFreqImage: SkImage;
|
||||||
|
};
|
||||||
|
/** The logical size at which to render the shader tree for export (typically the work image res). */
|
||||||
|
renderWidth?: number;
|
||||||
|
renderHeight?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CPU recolor: directly map pick codes to painted BGR colors (or copy origin).
|
||||||
|
* Produces RGBA PNG bytes via upng-js. This is the *fallback* path when rich shader offscreen
|
||||||
|
* is not available or fails. It produces flat colors without the editor's lighting + freq texture.
|
||||||
|
*/
|
||||||
|
function cpuRecolorToPngBytes(
|
||||||
|
originBgr: Uint8Array,
|
||||||
|
pickBuffer: Uint8Array,
|
||||||
|
paintedRegions: Map<number, BgrColor>,
|
||||||
|
cols: number,
|
||||||
|
rows: number,
|
||||||
|
): Uint8Array {
|
||||||
|
const pixelCount = cols * rows;
|
||||||
|
const rgba = new Uint8Array(pixelCount * 4);
|
||||||
|
const colorByPickCode = new Map<number, BgrColor>();
|
||||||
|
for (const [regionId, color] of paintedRegions) {
|
||||||
|
colorByPickCode.set(regionId + 1, color);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
const code = pickBuffer[i];
|
||||||
|
const color = code > 0 ? colorByPickCode.get(code) : undefined;
|
||||||
|
const d = i * 4;
|
||||||
|
if (color) {
|
||||||
|
rgba[d] = color.r;
|
||||||
|
rgba[d + 1] = color.g;
|
||||||
|
rgba[d + 2] = color.b;
|
||||||
|
rgba[d + 3] = 255;
|
||||||
|
} else {
|
||||||
|
const s = i * 3;
|
||||||
|
rgba[d] = originBgr[s + 2]; // RGB <- BGR
|
||||||
|
rgba[d + 1] = originBgr[s + 1];
|
||||||
|
rgba[d + 2] = originBgr[s];
|
||||||
|
rgba[d + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const png = UPNG.encode([rgba.buffer], cols, rows, 0);
|
||||||
|
return new Uint8Array(png as ArrayBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将上色区域导出为 recolored PNG。
|
||||||
|
* 优先级(从好到保底):
|
||||||
|
* 1. exportPngBytes(调用方用 makeImageSnapshot 在高分辨率 Canvas 上捕获的完整 shader 结果)—— 推荐的“保存快照”路径,无 CPU 逐像素,无二次 drawAsImage。
|
||||||
|
* 2. shaderTextures + render*(通过 renderPaintedImageOffscreen / drawAsImage 重建同一套 PaintShaderLayer + SkSL)。
|
||||||
|
* 3. CPU 逐像素 recolor(flat,无光照/纹理,仅作最后兜底,保证保存不中断)。
|
||||||
|
*/
|
||||||
|
export async function compositePaintedImage(
|
||||||
|
input: CompositePaintInput,
|
||||||
|
): Promise<SavePaintResult> {
|
||||||
|
const {
|
||||||
|
originBuffer,
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
pickBuffer,
|
||||||
|
paintedRegions,
|
||||||
|
destDir,
|
||||||
|
exportPngBase64,
|
||||||
|
exportPngBytes,
|
||||||
|
shaderTextures,
|
||||||
|
renderWidth,
|
||||||
|
renderHeight,
|
||||||
|
} = input;
|
||||||
|
if (paintedRegions.size === 0) {
|
||||||
|
throw new Error('No painted regions, cannot save');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pickBuffer.length !== cols * rows) {
|
||||||
|
const msg = 'pickMap size does not match image';
|
||||||
|
console.error('[VIZ-SAVE] composite will throw:', msg, { pickLen: pickBuffer.length, expected: cols * rows, cols, rows });
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
if (originBuffer.length !== cols * rows * 3) {
|
||||||
|
const msg = 'Original buffer size does not match image';
|
||||||
|
console.error('[VIZ-SAVE] composite will throw:', msg, { originLen: originBuffer.length, expected: cols * rows * 3, cols, rows });
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let pngBytesForWrite: Uint8Array | undefined;
|
||||||
|
let pngBase64ForWrite: string | undefined;
|
||||||
|
let usedSnapshot = false;
|
||||||
|
let usedRichShader = false;
|
||||||
|
|
||||||
|
// 1) Highest priority: pre-encoded PNG base64 from makeImageSnapshot (no extra conversion).
|
||||||
|
if (exportPngBase64 && exportPngBase64.length > 0) {
|
||||||
|
pngBase64ForWrite = exportPngBase64;
|
||||||
|
usedSnapshot = true;
|
||||||
|
}
|
||||||
|
// 1b) Snapshot bytes fallback (legacy callers).
|
||||||
|
else if (exportPngBytes && exportPngBytes.length > 0) {
|
||||||
|
pngBytesForWrite = exportPngBytes;
|
||||||
|
usedSnapshot = true;
|
||||||
|
}
|
||||||
|
// 2) 回退 rich:用 live 纹理 + drawAsImage 重建与编辑器一致的 shader 结果(带光照 + 频率纹理)。
|
||||||
|
else if (shaderTextures && renderWidth && renderHeight) {
|
||||||
|
try {
|
||||||
|
const offImg = await renderPaintedImageOffscreen({
|
||||||
|
originImage: shaderTextures.originImage,
|
||||||
|
paintColorMap: shaderTextures.paintColorMap,
|
||||||
|
lowFreqImage: shaderTextures.lowFreqImage,
|
||||||
|
highFreqImage: shaderTextures.highFreqImage,
|
||||||
|
width: renderWidth,
|
||||||
|
height: renderHeight,
|
||||||
|
showOrigin: false,
|
||||||
|
});
|
||||||
|
if (offImg) {
|
||||||
|
let b64 = '';
|
||||||
|
try {
|
||||||
|
const enc = (offImg as any).encodeToBase64;
|
||||||
|
if (typeof enc === 'function') {
|
||||||
|
b64 = enc.call(offImg) || '';
|
||||||
|
}
|
||||||
|
} catch (encErr) {
|
||||||
|
console.warn('[VIZ-SAVE] offscreen encodeToBase64 failed:', encErr);
|
||||||
|
}
|
||||||
|
if (b64 && b64.length > 0) {
|
||||||
|
pngBytesForWrite = new Uint8Array(Buffer.from(b64, 'base64'));
|
||||||
|
usedRichShader = true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const disp = (offImg as any).dispose;
|
||||||
|
if (typeof disp === 'function') disp.call(offImg);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[VIZ-SAVE] rich shader offscreen for export failed (will fallback):', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) 最后兜底:CPU 逐像素(flat 颜色,无 editor 质感)。
|
||||||
|
if (!pngBase64ForWrite && !pngBytesForWrite) {
|
||||||
|
try {
|
||||||
|
pngBytesForWrite = cpuRecolorToPngBytes(originBuffer, pickBuffer, paintedRegions, cols, rows);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('CPU recolor PNG decoding failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const dir = destDir ?? RNFS.CachesDirectoryPath;
|
||||||
|
const filePath = `${dir}/painted_${Date.now()}.png`;
|
||||||
|
try {
|
||||||
|
if (pngBase64ForWrite) {
|
||||||
|
await writePngBase64ToFile(filePath, pngBase64ForWrite);
|
||||||
|
} else {
|
||||||
|
await writePngBytesToFile(filePath, pngBytesForWrite!);
|
||||||
|
}
|
||||||
|
void usedSnapshot;
|
||||||
|
void usedRichShader;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[VIZ-SAVE] composite writeFile threw:', e, { filePath, dir });
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
filePath,
|
||||||
|
width: cols,
|
||||||
|
height: rows,
|
||||||
|
paintedCount: paintedRegions.size,
|
||||||
|
};
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user