You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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对象从来没手动释放,跑不了几帧就会内存泄漏导致页面卡死。
修复步骤

按以下顺序修改代码即可解决问题:

  1. 调整渲染时序:在调用model.executeAsync()异步方法前,先把捕获到的原始帧渲染到canvas,或者提前用frame.clone()拷贝一份独立的Mat副本做后续渲染,避免异步上下文切换导致数据丢失、渲染失效。
  2. 修正TF张量管理逻辑:移除包裹videoTensor的tf.tidy(),tf.tidy仅适合同步执行场景,跨await的张量手动管理释放即可,不要重复dispose已经释放的张量。
  3. 修复绘制函数逻辑:纠正宽高取值顺序,处理无检测目标的边界情况,所有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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 23:27:29