拼乐高找不到积木🤬破防了!写一个浏览器实时识别乐高积木块功能

41 天前
 xiaowoli

起源

事情的起因是这样的,每年的结婚纪念日我们都有拼乐高的传统。

但是拼的时候积木块又小又多经常都找不到对应的积木兼职,太痛苦了。😖

于是心生一计,想通过手机识别到我要找的积木,然后直接用框给我标出来,省时省力不费眼,岂不美哉。😎

恰巧之前写过浏览器上运行识别狗的一个功能 "🚫 为了防止狗上沙发,写了一个浏览器实时识别目标功能 📷",想着拿来改造一下应该就行了。但是 coco-ssd 只能识别出日常的 80 多种物体。所以需要自己训练一个,或者找一个训练好的“识别积木模型”。 🤖

要找可直接运行的最终代码请直接拉到文末

步骤

  1. 📷 调用手机摄像头,获取摄像头中的画面用 canvas 绘制
  2. 🔍 加载对应的识别乐高的模型
  3. 🧱 选择要识别的“目标积木”类型
  4. 🔎 让该模型识别图像并返回出识别对象的信息
  5. 🎨 通过识别出的对象信息在 canvas 上进行绘制
  6. 📲 部署在手机上

解决思路 📚:

  1. 📷 调用手机摄像头,获取摄像头中的画面用 canvas 绘制

这里我使用的是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(); // 隐藏视频元素,仅使用其捕获的视频数据
}

2. 🔍 加载对应的识别乐高的模型

原本想要使用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, // 指定要加载的模型版本
    });
}

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);
  });
}

4. 🔎 让该模型识别图像并返回出识别对象的信息

调用模型的 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);
};

5. 🎨 通过识别出的对象信息在 canvas 上进行绘制

获取到模型返回的信息保存并将识别到的信息都用 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);
  }
}

6. 📲 部署在手机上

最终代码

<!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>

总结 🎓

  1. 技术选型:
  1. 核心功能实现:
  1. 部署

改进方向 🚀

写在最后 😅

大家如果还有什么好方法的话可以一起分享一下 😊

还没等摸鱼的时候写好功能,老婆已经拼完了。。。

1905 次点击
所在节点    分享创造
13 条回复
iluolSNS
41 天前
厉害 你的 key 泄露了
xiaowoli
41 天前
还好公钥问题不大 嘿嘿
fengci
41 天前
有共同爱好真好。
InDom
41 天前
让我想到了,拿来拼图🧩上呢?
LDa
40 天前
老婆:就知道写代码 也不陪陪我
iX8NEGGn
40 天前
OP 看来 CV 玩得挺六的,想请教下两个问题。

一:我想识别一张包含钢琴的图片中的键盘区域。

二:然后识别键盘区域中每一个白键以及黑键的边框。

目前用 OpenCV 实现,运行得还算可以,但我总感觉用上 AI
会有黑魔法加持,这样那些非常模糊的或者光线过暗、过曝的图片也能准确识别,有什么推荐的项目或者框架吗?
jpyl0423
40 天前
没有灵魂
xiao8276
40 天前
666
xiaowoli
40 天前
chanChristin
40 天前
代码不能解决一切问题
每次你老婆都有更简单的解决思路
tpjaord
40 天前
这就牛逼了啊
tbg
40 天前
666
iX8NEGGn
40 天前
#6 这都不用找图训练了,Cool 。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1039503

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX