353 lines
14 KiB
JavaScript
353 lines
14 KiB
JavaScript
/**
|
||
* 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
|