React+OpenCV.js绘制TensorFlow.js检测结果canvas无显示修复
问题根因
cv.imshow放在model.executeAsync()前后表现不一致,是三个问题叠加导致的:
- OpenCV.js的
cv.Mat存储在WebAssembly线性内存中,model.executeAsync()是调用WebGL/WebGPU的异步计算,await期间浏览器会切换事件循环、重绑绘图上下文:TFJS初始化推理上下文时会抢占canvas的GL绑定,如果你在异步调用前没完成帧渲染,后续的绘图指令会因为上下文切换丢失;同时WASM内存如果没有主动持久化,await期间可能被内存回收/数据覆写,等异步返回时Mat里已经是空数据,自然渲染不出内容。 - TensorFlow张量生命周期管理错误:你把跨异步周期使用的
videoTensor放在tf.tidy()里返回,tf.tidy()只会自动回收同步路径上的张量,跨await的张量会脱离tidy管理,加上你后续又手动重复dispose张量,会触发内存踩踏,直接影响同线程WASM内存的正常读写。 - 工具函数逻辑错误:
drawRect只要第一个检测框不满足置信度要求就会返回undefined,同时你搞反了cv.Mat.size的返回顺序——frame.size返回值是[height, width],你把第一个值当宽度用,坐标计算完全错位;另外所有你创建的cv.VideoCapture、cv.Mat、cv.Point对象从来没手动释放,跑不了几帧就会内存泄漏导致页面卡死。
修复步骤
按以下顺序修改代码即可解决问题:
- 调整渲染时序:在调用
model.executeAsync()异步方法前,先把捕获到的原始帧渲染到canvas,或者提前用frame.clone()拷贝一份独立的Mat副本做后续渲染,避免异步上下文切换导致数据丢失、渲染失效。 - 修正TF张量管理逻辑:移除包裹
videoTensor的tf.tidy(),tf.tidy仅适合同步执行场景,跨await的张量手动管理释放即可,不要重复dispose已经释放的张量。 - 修复绘制函数逻辑:纠正宽高取值顺序,处理无检测目标的边界情况,所有OpenCV创建的WASM对象用完必须手动调用
delete()释放,避免内存泄漏。
修正后的核心代码
首先修改App.js中的检测逻辑部分:
const detectInterior = async () => { if ( typeof webcamRef.current !== "undefined" && webcamRef.current !== null && webcamRef.current.video.readyState === 4 ) { const video = webcamRef.current.video; const videoHeight = video.videoHeight; const videoWidth = video.videoWidth; if (!video) return; // 设置canvas尺寸 canvasRef.current.width = videoWidth; canvasRef.current.height = videoHeight; // 捕获帧 const cap = new cv.VideoCapture(video); const frame = new cv.Mat(videoHeight, videoWidth, cv.CV_8UC4); cap.read(frame); // 异步推理前先渲染原始帧,避免上下文切换导致渲染失效 cv.imshow(canvasRef.current, frame); // 用完VideoCapture立即释放 cap.delete(); // 准备推理张量,不要用tf.tidy包裹跨await的张量 const tfimg = tf.browser.fromPixels(video); const resized = tfimg.resizeBilinear([videoHeight, videoWidth]).toInt(); const videoTensor = resized.expandDims(0); // 清理不再使用的中间张量 tfimg.dispose(); resized.dispose(); // 执行异步推理 const detection = await model.executeAsync(videoTensor); const data = [] for (let i = 0; i < 8; i++) { data.push(detection[i].arraySync()) } // 清理所有TF相关张量 videoTensor.dispose(); detection.forEach(t => t.dispose()); return {data: data, frame: frame} } }
修改requestAnimationFrame循环逻辑,确保所有OpenCV对象正确释放:
const nextTick = () => { handle = requestAnimationFrame(async () => { const detectionResults = await detectInterior() if (detectionResults) { const {data, frame} = detectionResults; const boxes = data[1] const classes = data[2] const scores = data[4] // 注意cv.Mat的size返回顺序是 [height, width] const imgHeight = frame.size[0] const imgWidth = frame.size[1] const img = drawRect(boxes[0], classes[0], scores[0], 0.85, imgWidth, imgHeight, frame) if (img) { cv.imshow(canvasRef.current, img) } // 用完Mat必须释放 frame.delete(); } nextTick(); }) };
最后修正drawRect绘制函数的逻辑问题:
export const drawRect = (boxes, classes, scores, threshold, imgWidth, imgHeight, frame) => { // 无检测结果直接返回原始帧 if (!boxes || !classes || !scores) return frame; const color = new cv.Scalar(0,255,0); for (let i=0; i<boxes.length; i++) { if (boxes[i] && classes[i] && scores[i] > threshold) { const [y, x, y1, x1] = boxes[i] const point1 = new cv.Point(x * imgWidth, y * imgHeight) const point2 = new cv.Point(x1 * imgWidth, y1 * imgHeight) cv.rectangle(frame, point1, point2, color, 3) // 释放临时创建的OpenCV对象 point1.delete(); point2.delete(); } } color.delete(); // 遍历完所有检测框后再返回帧,避免漏画多目标 return frame }
额外说明
- 如果不想在推理前渲染原始帧,可以在调用
model.executeAsync()前用frame.clone()创建一个独立的Mat副本,用副本做后续渲染操作,避免原Mat数据被异步流程破坏。 - OpenCV.js的WASM对象不会被JS垃圾回收自动处理,所有通过
new cv.XXX创建的对象必须手动调用delete()释放,否则内存会持续上涨直到页面崩溃。
内容的提问来源于stack exchange,提问作者Jakub Małecki
相关产品推荐
相关产品推荐

