事情的起因是这样的,每年的结婚纪念日我们都有拼乐高的传统。
但是拼的时候积木块又小又多经常都找不到对应的积木兼职,太痛苦了。😖
于是心生一计,想通过手机识别到我要找的积木,然后直接用框给我标出来,省时省力不费眼,岂不美哉。😎
恰巧之前写过浏览器上运行识别狗的一个功能 "🚫 为了防止狗上沙发,写了一个浏览器实时识别目标功能 📷",想着拿来改造一下应该就行了。但是 coco-ssd 只能识别出日常的 80 多种物体。所以需要自己训练一个,或者找一个训练好的“识别积木模型”。 🤖
要找可直接运行的最终代码请直接拉到文末
这里我使用的是p5.js 一个流行的 JavaScript 库,简化了视觉和交互体验的创建,提供了易于使用的 API 用于绘图、处理用户输入以及处理如视频等媒体。
其中 setup() 和 draw() 是内置函数 😊 不需要调用
/**
 * 初始化函数,设置画布大小并配置视频捕获属性。
 * 该函数不接受参数,也不返回任何值。
 */
function setup() {
  // 创建画布并设置其尺寸
  canvas = createCanvas(640, 480);
  // 计算水平和垂直缩放因子,以保持捕获的视频与画布尺寸一致
  scaleX = 640 / +canvas.canvas.width || 640;
  scaleY = 480 / +canvas.canvas.height || 480;
  // 定义视频捕获的约束,指定使用后置摄像头
  let constraints = {
    video: {
      facingMode: "environment",
    },
  };
  // 创建视频元素并配置其大小,注册视频准备就绪的回调函数
  video = createCapture(constraints, videoReady);
  video.size(640, 480);
  video.hide(); // 隐藏视频元素,仅使用其捕获的视频数据
}
原本想要使用ml5.js 但是发现需要自己再训练乐高的模型且训练速度很慢,限制很多,作罢 😕。
目前使用的是 roboflow.js 同样是基于 tensorFlow.js 但是社区中有很多的训练好可直接使用的模型。
这里模型配置可信值我降低到了 0.15 ,因为发现高可信值的模型识别率太低了 😏。
/**
 * 异步函数 videoReady ,初始化视频处理模型并准备就绪。
 */
async function videoReady() {
  console.log("videoReady");
  // 等待模型加载
  model = await getModel();
  // 配置模型的阈值
  model.configure({ threshold: 0.15 });
  // 更新 UI ,表示模型已准备好
  loadText.innerHTML = "modelReady";
  console.log("modelReady");
  // 选择要识别的目标
  processSelect();
  // 开始检测
  detect();
}
/**
 * 异步函数 getModel ,从 roboflow 服务加载指定的模型。
 */
async function getModel() {
  return await roboflow
    .auth({
      publishable_key: "rf_lOpB8GQvE5Uwp6BAu66QfHyjPA13", // 使用 API 密钥进行授权
    })
    .load({
      model: "hex-lego", // 指定要加载的模型名称
      version: 3, // 指定要加载的模型版本
    });
}
绑定要选择的“目标积木”
function processSelect() {
  const { classes } = model.getMetadata();
  console.log("classes", classes);
  classes.forEach((className) => {
    const option = document.createElement("option");
    option.value = className;
    option.text = className;
    selectRef.appendChild(option);
  });
}
调用模型的 API 进行识别,以便于后续的绘制
const detect = async () => {
  if (!play || !model) {
    console.log("model is not available");
    timer = setTimeout(() => {
      requestAnimationFrame(detect);
      clearTimeout(timer);
    }, 2000);
    return;
  }
  detections = await model.detect(canvas.canvas);
  console.log("detections", detections);
  requestAnimationFrame(detect);
};
获取到模型返回的信息保存并将识别到的信息都用 canvas 绘制出来
function draw() {
  image(video, 0, 0);
  for (let i = 0; i < detections.length; i += 1) {
    const object = detections[i];
    let { x, y, width, height } = object.bbox;
    width *= scaleX;
    height *= scaleY;
    x = x * scaleX - width / 2;
    y = y * scaleY - height / 2;
    stroke(0, 0, 255);
    if (object.class.includes(selectVal)) stroke(0, 255, 0);
    strokeWeight(4);
    noFill();
    rect(Math.floor(x), Math.floor(y), Math.floor(width), Math.floor(height));
    noStroke();
    fill(255);
    textSize(24 * scaleX);
    text(object.class, Math.floor(x) + 10, Math.floor(y) + 24);
  }
}
<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- 定义文档类型和基本页面信息,包括字符编码、视口设置、标题和外部脚本引用 -->
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>find Lego</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js"></script>
    <script src="https://cdn.roboflow.com/0.2.26/roboflow.js"></script>
  </head>
  <body>
    <!-- 页面加载提示文本和分类选择下拉菜单以及开始和停止按钮 -->
    <div id="loadText">model is loading... please wait.</div>
    <select name="classify" id="selectRef"></select>
    <button id="start">start</button>
    <button id="stop">stop</button>
  </body>
  <script>
    // 定义全局变量
    let model;
    let video;
    let canvas;
    let play = true;
    let timer;
    let detections = [
      {
        bbox: {
          x: 0,
          y: 0,
          width: 100,
          height: 100,
        },
        class: "testing...",
      },
    ];
    let scaleX = 1;
    let scaleY = 1;
    let selectVal;
    const selectRef = document.getElementById("selectRef");
    // 监听下拉菜单选择变化事件
    selectRef.addEventListener("change", (e) => {
      selectVal = e.target.value;
      console.log("selectVal", e.target.value);
    });
    // 页面初始化设置函数
    function setup() {
      // 创建画布并调整缩放比例
      canvas = createCanvas(640, 480);
      scaleX = 640 / +canvas.canvas.width || 640;
      scaleY = 480 / +canvas.canvas.height || 480;
      // 设置视频捕获约束条件
      let constraints = {
        video: {
          facingMode: "environment",
        },
      };
      video = createCapture(constraints, videoReady);
      video.size(640, 480);
      video.hide();
    }
    // 页面持续绘制函数
    function draw() {
      // 显示视频并根据检测结果绘制边界框和类别文本
      image(video, 0, 0);
      for (let i = 0; i < detections.length; i += 1) {
        const object = detections[i];
        let { x, y, width, height } = object.bbox;
        width *= scaleX;
        height *= scaleY;
        x = x * scaleX - width / 2;
        y = y * scaleY - height / 2;
        stroke(0, 0, 255);
        if (object.class.includes(selectVal)) stroke(0, 255, 0);
        strokeWeight(4);
        noFill();
        rect(
          Math.floor(x),
          Math.floor(y),
          Math.floor(width),
          Math.floor(height)
        );
        noStroke();
        fill(255);
        textSize(24 * scaleX);
        text(object.class, Math.floor(x) + 10, Math.floor(y) + 24);
      }
    }
    // 视频准备就绪时的处理函数
    const loadText = document.getElementById("loadText");
    async function videoReady() {
      console.log("videoReady");
      model = await getModel();
      model.configure({ threshold: 0.15 });
      loadText.innerHTML = "modelReady";
      console.log("modelReady");
      processSelect();
      detect();
    }
    // 处理下拉菜单选项,基于模型支持的类别动态生成
    function processSelect() {
      const { classes } = model.getMetadata();
      console.log("classes", classes);
      classes.forEach((className) => {
        const option = document.createElement("option");
        option.value = className;
        option.text = className;
        selectRef.appendChild(option);
      });
    }
    // 异步加载模型
    async function getModel() {
      return await roboflow
        .auth({
          publishable_key: "rf_lOpB8GQvE5Uwp6BAu66QfHyjPA13",
        })
        .load({
          model: "hex-lego",
          version: 3, // <--- YOUR VERSION NUMBER
        });
    }
    // 异步检测函数,持续检测视频中的对象
    const detect = async () => {
      if (!play || !model) {
        console.log("model is not available");
        timer = setTimeout(() => {
          requestAnimationFrame(detect);
          clearTimeout(timer);
        }, 2000);
        return;
      }
      detections = await model.detect(canvas.canvas);
      console.log("detections", detections);
      requestAnimationFrame(detect);
    };
    // 停止按钮点击事件处理函数
    const stopBtn = document.getElementById("stop");
    stopBtn.addEventListener("click", () => {
      play = false;
      video.pause();
      // TODO
    });
    // 开始按钮点击事件处理函数
    const startBtn = document.getElementById("start");
    start.addEventListener("click", () => {
      play = true;
      video.play();
    });
  </script>
</html>
大家如果还有什么好方法的话可以一起分享一下 😊
还没等摸鱼的时候写好功能,老婆已经拼完了。。。
|  |      1581996      2024-05-10 16:10:43 +08:00 厉害 你的 key 泄露了 | 
|  |      2xiaowoli OP 还好公钥问题不大 嘿嘿 | 
|  |      3fengci      2024-05-10 16:55:30 +08:00 有共同爱好真好。 | 
|  |      4InDom      2024-05-10 16:58:49 +08:00 让我想到了,拿来拼图🧩上呢? | 
|  |      5LDa      2024-05-10 19:39:21 +08:00 老婆:就知道写代码 也不陪陪我 | 
|      6iX8NEGGn      2024-05-11 04:50:05 +08:00 via iPhone OP 看来 CV 玩得挺六的,想请教下两个问题。 一:我想识别一张包含钢琴的图片中的键盘区域。 二:然后识别键盘区域中每一个白键以及黑键的边框。 目前用 OpenCV 实现,运行得还算可以,但我总感觉用上 AI 会有黑魔法加持,这样那些非常模糊的或者光线过暗、过曝的图片也能准确识别,有什么推荐的项目或者框架吗? | 
|      7jpyl0423      2024-05-11 09:20:13 +08:00 | 
|  |      8xiao8276      2024-05-11 11:44:40 +08:00 666 | 
|  |      9xiaowoli OP  1 | 
|  |      10Leeeeex PRO 代码不能解决一切问题 每次你老婆都有更简单的解决思路 | 
|      11cPO3Im7cn3lD39cU      2024-05-11 14:19:07 +08:00 这就牛逼了啊 | 
|      12tbg      2024-05-11 14:21:48 +08:00 666 | 
|      13iX8NEGGn      2024-05-11 15:19:57 +08:00 #6 这都不用找图训练了,Cool 。 |