事情的起因是这样的,每年的结婚纪念日我们都有拼乐高的传统。
但是拼的时候积木块又小又多经常都找不到对应的积木兼职,太痛苦了。😖
于是心生一计,想通过手机识别到我要找的积木,然后直接用框给我标出来,省时省力不费眼,岂不美哉。😎
恰巧之前写过浏览器上运行识别狗的一个功能 "🚫 为了防止狗上沙发,写了一个浏览器实时识别目标功能 📷",想着拿来改造一下应该就行了。但是 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>
大家如果还有什么好方法的话可以一起分享一下 😊
还没等摸鱼的时候写好功能,老婆已经拼完了。。。
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.