react-native-mask-segment-c.../dist/utils/pngImage.js

438 lines
16 KiB
JavaScript
Raw Normal View History

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 需要 URIOpenCV / 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-bitfast-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