原创 维克 2025-07-02 18:31 上海
得物ERP主要鉴别流程下,正品库59%的人力投入在线下商品借取/归还业务的操作端,而正品库可通过图库搭建,提升图库质量,节约线下用工和物流成本支出,本质上是根据当前的业务场景和诉求收益,深入业务剖析潜在业务价值,从工具思维转向价值。
📸 得物ERP鉴别流程中,正品库线下商品借取/归还业务耗费大量人力,且补图流程效率低下,难以满足快速建立正品库的需求,主要体现在图片上传途径繁琐、留档图质量压缩、借还操作单一和正品流转效率低等问题。
⚙️ 针对上述问题,技术团队基于WebRTC、HTML5 Canvas和Web Worker,构建了PWA相机应用,通过手持设备一站式完成补图、拍摄、上传和通知等操作,提升了效率和图片质量。
💡 为了解决高分辨率带来的内存压力、PWA内存分配限制、视频流与图像处理的资源竞争等问题,团队采用了Web Worker架构、ImageBitmap直接传输、绘制分块处理和资源管理优化等策略。
📈 优化后,单张图片处理峰值减少33%,单张图片持久占用减少61%,PWA应用整体内存优化16-26%,最终实现了高清图片绘制作业流程的流畅运行,并提升了日均拍摄件数330%。
原创 维克 2025-07-02 18:31 上海
得物ERP主要鉴别流程下,正品库59%的人力投入在线下商品借取/归还业务的操作端,而正品库可通过图库搭建,提升图库质量,节约线下用工和物流成本支出,本质上是根据当前的业务场景和诉求收益,深入业务剖析潜在业务价值,从工具思维转向价值。
如果单独拍摄一张图内存,粗略计算为如下(主要以iPhoneX的情况做解析):
const videoConstraints = useRef({
video: {
facingMode: 'environment',
width: {
min: 1280,
ideal: 4032,
max: 4032
},
height: {
min: 720,
ideal: 3024,
max: 3024
},
frameRate: {
ideal: 30, // 适当降低可以降低视频缓冲区的内存占用,我们先按照这样的场景来看。
min: 15
},
advanced: [
{ focusMode: "continuous" },
]
} as MediaTrackConstraints,
});
单张图的内存占用按照上文的视频约束条件,单帧大小:约 46.51MB,实际单张内存需要76.7M左右(15 + 15 + 46.5 + 0.2 「objectURL引用」),三五张图大概就会达到内存限制红线,这样的内存占用对移动设备来说太大了,实际上,在项目上线初期,业务使用也反馈:拍照几张手机发热严重,页面经常卡死。PWA相机应用内存占用情况// 视频流约束
const iphoneXStreamConfig = {
width: 4032,
height: 3024,
frameRate: 24,
format: 'RGBA' // 4字节/像素
};
// 单帧内存计算
const frameMemoryCalculation = {
// 单帧大小
pixelCount: 4032 * 3024, // = 12,192,768 像素
bytesPerFrame: 4032 * 3024 * 4, // = 48,771,072 字节
mbPerFrame: (4032 * 3024 * 4) / (1024 * 1024), // ≈ 46.51 MB
};
// 实际运行时内存占用
const runtimeMemoryUsage = {
// 视频流缓冲区 (至少3-4帧)
streamBuffer: {
frameCount: 4,
totalBytes: 48771072 * 4, // ≈ 186.04 MB
description: '视频流缓冲区(4帧)'
},
// 处理管道内存
processingPipeline: {
captureBuffer: 46.51, // 一帧的大小
processingBuffer: 46.51, // 处理缓冲
encoderBuffer: 46.51 * 0.5, // 编码缓冲(约半帧)
totalMB: 46.51 * 2.5, // ≈ 116.28 MB
description: '视频处理管道内存'
},
// 总体内存
total: {
peakMemoryMB: 186.04 + 116.28, // ≈ 302.32 MB
stableMemoryMB: 186.04 + 93.02, // ≈ 279.06 MB
description: '预估总内存占用'
}
};
createImageBitmap 实际上是:...
let imageBitmap: ImageBitmap | null = null;
// 判断是否为视频元素,如果是则尝试直接创建ImageBitmap
// 支持img 和 vedio
if ((source instanceof HTMLVideoElement || source instanceof HTMLImageElement) && supportsImageBitmap) {
try {
console.log('尝试直接从视频元素创建ImageBitmap');
// 直接从视频元素创建ImageBitmap,跳过Canvas中间步骤
if (source instanceof HTMLVideoElement) {
imageBitmap = await createImageBitmap(
source,
0, 0, sourceWidth, sourceHeight
);
} else {
// 支持img
imageBitmap = await createImageBitmap(source);
}
console.log('直接创建ImageBitmap成功!!');
} catch (directError) {
console.warn('这直接从视频创建ImageBitmap失败,回退到Canvas:', directError);
// 失败后将通过下面的Canvas方式创建
imageBitmap = null;
}
}
...
class ChunkedProcessStrategy extends ImageProcessStrategy {
readonly name = 'chunked';
protected async doProcess(imageData: ImageBitmap, options: ProcessOptions): Promise<Blob> {
const { width, height, quality } = options;
const optimalChunkSize = ResourceManager.calculateOptimalChunkSize(width, height);
const chunkConfig: ChunkConfig = {
size: optimalChunkSize,
cols: Math.ceil(width / optimalChunkSize),
rows: Math.ceil(height / optimalChunkSize),
};
const { canvas: finalCanvas, ctx: finalCtx } = ResourceManager.createCanvas(width, height);
const { canvas: tempCanvas, ctx: tempCtx } = ResourceManager.createCanvas(optimalChunkSize, optimalChunkSize);
try {
for (let row = 0; row < chunkConfig.rows; row++) {
for (let col = 0; col < chunkConfig.cols; col++) {
await this.processChunk(
imageData,
tempCanvas,
tempCtx,
finalCtx,
row,
col,
chunkConfig,
width,
height
);
await new Promise(resolve => setTimeout(resolve, 0));
}
}
return await finalCanvas.convertToBlob({
type: 'image/jpeg',
quality,
});
} finally {
ResourceManager.releaseResources(tempCanvas, tempCtx);
ResourceManager.releaseResources(finalCanvas, finalCtx);
}
}
private async processChunk(
imageData: ImageBitmap,
tempCanvas: OffscreenCanvas,
tempCtx: OffscreenCanvasRenderingContext2D,
finalCtx: OffscreenCanvasRenderingContext2D,
row: number,
col: number,
chunkConfig: ChunkConfig,
width: number,
height: number
): Promise<void> {
const x = col * chunkConfig.size;
const y = row * chunkConfig.size;
const chunkWidth = Math.min(chunkConfig.size, width - x);
const chunkHeight = Math.min(chunkConfig.size, height - y);
tempCtx.clearRect(0, 0, chunkConfig.size, chunkConfig.size);
tempCtx.drawImage(
imageData,
x, y, chunkWidth, chunkHeight,
0, 0, chunkWidth, chunkHeight
);
finalCtx.drawImage(
tempCanvas,
0, 0, chunkWidth, chunkHeight,
x, y, chunkWidth, chunkHeight
);
}
}
...
// 分块转化 最终返回
class ChunkedProcessStrategy extends ImageProcessStrategy {
readonly name = 'chunked';
protected async doProcess(imageData: ImageBitmap, options: ProcessOptions): Promise<Blob> {
const { width, height, quality } = options;
const optimalChunkSize = ResourceManager.calculateOptimalChunkSize(width, height);
const chunkConfig: ChunkConfig = {
size: optimalChunkSize,
cols: Math.ceil(width / optimalChunkSize),
rows: Math.ceil(height / optimalChunkSize),
};
const { canvas: finalCanvas, ctx: finalCtx } = ResourceManager.createCanvas(width, height);
const { canvas: tempCanvas, ctx: tempCtx } = ResourceManager.createCanvas(optimalChunkSize, optimalChunkSize);
try {
for (let row = 0; row < chunkConfig.rows; row++) {
for (let col = 0; col < chunkConfig.cols; col++) {
await this.processChunk(
imageData,
tempCanvas,
tempCtx,
finalCtx,
row,
col,
chunkConfig,
width,
height
);
// 给GC机会
await new Promise(resolve => setTimeout(resolve, 0));
}
}
return await finalCanvas.convertToBlob({
type: 'image/jpeg',
quality,
});
} finally {
ResourceManager.releaseResources(tempCanvas, tempCtx);
ResourceManager.releaseResources(finalCanvas, finalCtx);
}
}
private async processChunk(
imageData: ImageBitmap,
tempCanvas: OffscreenCanvas,
tempCtx: OffscreenCanvasRenderingContext2D,
finalCtx: OffscreenCanvasRenderingContext2D,
row: number,
col: number,
chunkConfig: ChunkConfig,
width: number,
height: number
): Promise<void> {
const x = col * chunkConfig.size;
const y = row * chunkConfig.size;
const chunkWidth = Math.min(chunkConfig.size, width - x);
const chunkHeight = Math.min(chunkConfig.size, height - y);
tempCtx.clearRect(0, 0, chunkConfig.size, chunkConfig.size);
tempCtx.drawImage(
imageData,
x, y, chunkWidth, chunkHeight,
0, 0, chunkWidth, chunkHeight
);
finalCtx.drawImage(
tempCanvas,
0, 0, chunkWidth, chunkHeight,
x, y, chunkWidth, chunkHeight
);
}
}
...
...
class ChunkedConvertStrategy extends ImageProcessStrategy {
readonly name = 'chunkedConvert';
protected async doProcess(imageData: ImageBitmap, options: ProcessOptions): Promise<Blob> {
const { width, height, quality } = options;
const config = WorkerConfig.getInstance();
const chunks: Array<{
blob: Blob;
x: number;
y: number;
width: number;
height: number;
}> = [];
// 分块处理
for (let y = 0; y < height; y += config.chunkSize) {
for (let x = 0; x < width; x += config.chunkSize) {
const chunkWidth = Math.min(config.chunkSize, width - x);
const chunkHeight = Math.min(config.chunkSize, height - y);
const chunk = await this.processSingleChunk(
imageData, x, y, chunkWidth, chunkHeight, quality
);
chunks.push({ ...chunk, x, y, width: chunkWidth, height: chunkHeight });
await new Promise(resolve => setTimeout(resolve, 0));
}
}
// 合并块
return chunks.length === 1 ? chunks[0].blob : await this.mergeChunks(chunks, width, height, quality);
}
private async processSingleChunk(
imageData: ImageBitmap,
x: number,
y: number,
width: number,
height: number,
quality: number
): Promise<{ blob: Blob }> {
const { canvas, ctx } = ResourceManager.createCanvas(width, height);
try {
ctx.drawImage(imageData, x, y, width, height, 0, 0, width, height);
const blob = await canvas.convertToBlob({
type: 'image/jpeg',
quality,
});
return { blob };
} finally {
ResourceManager.releaseResources(canvas, ctx);
}
}
private async mergeChunks(
chunks: Array<{ blob: Blob; x: number; y: number; width: number; height: number }>,
width: number,
height: number,
quality: number
): Promise<Blob> {
const { canvas: finalCanvas, ctx: finalCtx } = ResourceManager.createCanvas(width, height);
try {
for (const chunk of chunks) {
const imgBitmap = await createImageBitmap(chunk.blob);
try {
finalCtx.drawImage(
imgBitmap,
0, 0, chunk.width, chunk.height,
chunk.x, chunk.y, chunk.width, chunk.height
);
} finally {
imgBitmap.close();
}
await new Promise(resolve => setTimeout(resolve, 0));
}
return await finalCanvas.convertToBlob({
type: 'image/jpeg',
quality,
});
} finally {
ResourceManager.releaseResources(finalCanvas, finalCtx);
}
}
}
AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。
鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑