ESP32-CAM Веб-сервер с OpenCV.js: Обнаружение и отслеживание цвета
Это руководство знакомит с OpenCV.js и инструментами OpenCV для среды веб-сервера камеры ESP32. В качестве примера мы создадим простой веб-сервер ESP32-CAM, который включает обнаружение цвета и отслеживание движущегося объекта.
Этот урок ни в коем случае не является исчерпывающим описанием всех возможностей OpenCV для веб-серверов камер ESP32. Ожидается, что это введение вдохновит на дальнейшую работу с OpenCV и камерами ESP32.
Этот проект/урок был создан на основе проекта Andrew R. Sass и отредактирован Сарой Сантос.
Введение
ESP32 может выступать в роли сервера для браузерного клиента, а некоторые модели включают камеру (например, ESP32-CAM), которая позволяет клиенту просматривать фотографии или видео в браузере. HTML, JavaScript и другие языки браузера могут использовать обширные возможности ESP32 и его камеры.
Для тех, у кого мало или совсем нет опыта работы с камерами ESP32, можно начать со следующего урока:
OpenCV.js
Как описано в docs.opencv.org, «OpenCV.js — это JavaScript-привязка для выбранного подмножества функций OpenCV для веб-платформы». OpenCV.js использует Emscripten, компилятор LLVM-to-JavaScript, для компиляции функций OpenCV в API-библиотеку, которая продолжает расти.
OpenCV.js работает в браузере, что позволяет быстро опробовать функции OpenCV, имея лишь скромный опыт в HTML и JavaScript. Те, кто имеет опыт работы с приложениями камеры ESP32, уже обладают таким опытом.
Обзор проекта
Проект, который мы создадим в этом уроке, представляет собой веб-сервер, который позволяет отслеживать цвет движущегося объекта. На интерфейсе веб-сервера вы можете настраивать различные параметры для правильного выбора цвета, который вы хотите отслеживать. Затем браузер отправляет координаты x и y центра масс движущегося объекта в реальном времени на плату ESP32.
Вот предварительный просмотр веб-сервера.
Необходимые условия
Перед тем, как приступить к этому проекту, убедитесь, что вы выполнили следующие предварительные условия.
Arduino IDE
Мы будем программировать плату ESP32 с помощью Arduino IDE. Поэтому вам нужна установленная Arduino IDE, а также дополнение ESP32:
VS Code (опционально)
Если вы предпочитаете использовать VS Code + PlatformIO для программирования платы, вы можете следовать следующему уроку, чтобы узнать, как настроить VS Code для работы с платами ESP32.
Выбор камеры ESP32
Этот проект совместим с любой платой камеры ESP32, оснащённой камерой OV2640. Существует несколько моделей камер ESP32. Для сравнения наиболее популярных камер обратитесь к следующей статье:
Убедитесь, что вы знаете распиновку используемой платы камеры. Для распиновки наиболее популярных плат ознакомьтесь с этой статьёй:
Код — ESP32-CAM с OpenCV.js
Программа состоит из двух частей:
серверная программа, которая работает на камере ESP32
клиентская программа, которая работает в браузере Chrome
Программа разделена на два файла: файл OCV_ColorTrack_P.ino, содержащий серверную программу, и заголовочный файл index_OCV_ColorTrack.h, содержащий клиентскую программу (HTML, CSS и JavaScript с OpenCV.js).
Создайте новый скетч Arduino с именем OCV_ColorTrack_P и скопируйте следующий код.
/*********
The include file, index_OCV_ColorTrack.h, the Client, is an intoduction of OpenCV.js to the ESP32 Camera environment. The Client was
developed and written by Andrew R. Sass. Permission to reproduce the index_OCV_ColorTrack.h file is granted free of charge if this
entire copyright notice is included in all copies of the index_OCV_ColorTrack.h file.
Complete instructions at https://RandomNerdTutorials.com/esp32-cam-opencv-js-color-detection-tracking/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*********/
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include "esp_camera.h"
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
#include "index_OCV_ColorTrack.h"
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
String Feedback="";
String Command="",cmd="",P1="",P2="",P3="",P4="",P5="",P6="",P7="",P8="",P9="";
byte ReceiveState=0,cmdState=1,strState=1,questionstate=0,equalstate=0,semicolonstate=0;
//ANN:0
// AI-Thinker
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
WiFiServer server(80);
//ANN:2
void ExecuteCommand() {
if (cmd!="colorDetect") { //Omit printout
//Serial.println("cmd= "+cmd+" ,P1= "+P1+" ,P2= "+P2+" ,P3= "+P3+" ,P4= "+P4+" ,P5= "+P5+" ,P6= "+P6+" ,P7= "+P7+" ,P8= "+P8+" ,P9= "+P9);
//Serial.println("");
}
if (cmd=="resetwifi") {
WiFi.begin(P1.c_str(), P2.c_str());
Serial.print("Connecting to ");
Serial.println(P1);
long int StartTime=millis();
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
if ((StartTime+5000) < millis()) break;
}
Serial.println("");
Serial.println("STAIP: "+WiFi.localIP().toString());
Feedback="STAIP: "+WiFi.localIP().toString();
}
else if (cmd=="restart") {
ESP.restart();
}
else if (cmd=="cm"){
int XcmVal = P1.toInt();
int YcmVal = P2.toInt();
Serial.println("cmd= "+cmd+" ,VALXCM= "+XcmVal);
Serial.println("cmd= "+cmd+" ,VALYCM= "+YcmVal);
}
else if (cmd=="quality") {
sensor_t * s = esp_camera_sensor_get();
int val = P1.toInt();
s->set_quality(s, val);
}
else if (cmd=="contrast") {
sensor_t * s = esp_camera_sensor_get();
int val = P1.toInt();
s->set_contrast(s, val);
}
else if (cmd=="brightness") {
sensor_t * s = esp_camera_sensor_get();
int val = P1.toInt();
s->set_brightness(s, val);
}
else {
Feedback="Command is not defined.";
}
if (Feedback=="") {
Feedback=Command;
}
}
void setup() {
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
Serial.begin(115200);
Serial.setDebugOutput(true);
Serial.println();
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sccb_sda = SIOD_GPIO_NUM;
config.pin_sccb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;
//init with high specs to pre-allocate larger buffers
if(psramFound()){
config.frame_size = FRAMESIZE_UXGA;
config.jpeg_quality = 10; //0-63 lower number means higher quality
config.fb_count = 2;
} else {
config.frame_size = FRAMESIZE_SVGA;
config.jpeg_quality = 12; //0-63 lower number means higher quality
config.fb_count = 1;
}
// camera init
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x", err);
delay(1000);
ESP.restart();
}
//drop down frame size for higher initial frame rate
sensor_t * s = esp_camera_sensor_get();
s->set_framesize(s, FRAMESIZE_CIF); //UXGA|SXGA|XGA|SVGA|VGA|CIF|QVGA|HQVGA|QQVGA
WiFi.mode(WIFI_AP_STA);
WiFi.begin(ssid, password);
delay(1000);
long int StartTime=millis();
while (WiFi.status() != WL_CONNECTED) {
delay(500);
if ((StartTime+10000) < millis())
break;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.print("ESP IP Address: http://");
Serial.println(WiFi.localIP());
}
server.begin();
}
void loop() {
Feedback="";Command="";cmd="";P1="";P2="";P3="";P4="";P5="";P6="";P7="";P8="";P9="";
ReceiveState=0,cmdState=1,strState=1,questionstate=0,equalstate=0,semicolonstate=0;
WiFiClient client = server.available();
if (client) {
String currentLine = "";
while (client.connected()) {
if (client.available()) {
char c = client.read();
getCommand(c);
if (c == '\n') {
if (currentLine.length() == 0) {
if (cmd=="colorDetect") {
camera_fb_t * fb = NULL;
fb = esp_camera_fb_get();
if(!fb) {
Serial.println("Camera capture failed");
delay(1000);
ESP.restart();
}
//ANN:1
client.println("HTTP/1.1 200 OK");
client.println("Access-Control-Allow-Origin: *");
client.println("Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept");
client.println("Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS");
client.println("Content-Type: image/jpeg");
client.println("Content-Disposition: form-data; name=\"imageFile\"; filename=\"picture.jpg\"");
client.println("Content-Length: " + String(fb->len));
client.println("Connection: close");
client.println();
uint8_t *fbBuf = fb->buf;
size_t fbLen = fb->len;
for (size_t n=0;n<fbLen;n=n+1024) {
if (n+1024<fbLen) {
client.write(fbBuf, 1024);
fbBuf += 1024;
}
else if (fbLen%1024>0) {
size_t remainder = fbLen%1024;
client.write(fbBuf, remainder);
}
}
esp_camera_fb_return(fb);
}
else {
//ANN:1
client.println("HTTP/1.1 200 OK");
client.println("Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept");
client.println("Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS");
client.println("Content-Type: text/html; charset=utf-8");
client.println("Access-Control-Allow-Origin: *");
client.println("Connection: close");
client.println();
String Data="";
if (cmd!="")
Data = Feedback;
else {
Data = String((const char *)INDEX_HTML);
}
int Index;
for (Index = 0; Index < Data.length(); Index = Index+1000) {
client.print(Data.substring(Index, Index+1000));
}
client.println();
}
Feedback="";
break;
} else {
currentLine = "";
}
}
else if (c != '\r') {
currentLine += c;
}
if ((currentLine.indexOf("/?")!=-1)&&(currentLine.indexOf(" HTTP")!=-1)) {
if (Command.indexOf("stop")!=-1) {
client.println();
client.println();
client.stop();
}
currentLine="";
Feedback="";
ExecuteCommand();
}
}
}
delay(1);
client.stop();
}
}
void getCommand(char c){
if (c=='?') ReceiveState=1;
if ((c==' ')||(c=='\r')||(c=='\n')) ReceiveState=0;
if (ReceiveState==1) {
Command=Command+String(c);
if (c=='=') cmdState=0;
if (c==';') strState++;
if ((cmdState==1)&&((c!='?')||(questionstate==1))) cmd=cmd+String(c);
if ((cmdState==0)&&(strState==1)&&((c!='=')||(equalstate==1))) P1=P1+String(c);
if ((cmdState==0)&&(strState==2)&&(c!=';')) P2=P2+String(c);
if ((cmdState==0)&&(strState==3)&&(c!=';')) P3=P3+String(c);
if ((cmdState==0)&&(strState==4)&&(c!=';')) P4=P4+String(c);
if ((cmdState==0)&&(strState==5)&&(c!=';')) P5=P5+String(c);
if ((cmdState==0)&&(strState==6)&&(c!=';')) P6=P6+String(c);
if ((cmdState==0)&&(strState==7)&&(c!=';')) P7=P7+String(c);
if ((cmdState==0)&&(strState==8)&&(c!=';')) P8=P8+String(c);
if ((cmdState==0)&&(strState>=9)&&((c!=';')||(semicolonstate==1))) P9=P9+String(c);
if (c=='?') questionstate=1;
if (c=='=') equalstate=1;
if ((strState>=9)&&(c==';')) semicolonstate=1;
}
}
Сохраните этот файл.
index_OCV_ColorTrack.h
Затем откройте новую вкладку в Arduino IDE, как показано на следующем изображении.
Назовите её index_OCV_ColorTrack.h.
Скопируйте следующий код в этот файл.
/****************************
This include file, index_OCV_ColorTrack.h, the Client, is an intoduction of OpenCV.js to the ESP32 Camera environment. The Client was
developed and written by Andrew R. Sass. Permission to reproduce the index_OCV_ColorTrack.h file is granted free of charge if this
entire copyright notice is included in all copies of the index_OCV_ColorTrack.h file.
Complete instructions at https://RandomNerdTutorials.com/esp32-cam-opencv-js-color-detection-tracking/
*******************************/
static const char PROGMEM INDEX_HTML[] = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>ESP32-CAMERA COLOR DETECTION</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<!----ANN:3--->
<script async src=" https://docs.opencv.org/master/opencv.js" type="text/javascript"></script>
</head>
<style>
html {
font-family: Arial, Helvetica, sans-serif;
}
body {
background-color: #F7F7F2;
margin: 0px;
}
h1 {
font-size: 1.6rem;
color:white;
text-align: center;
}
.topnav {
overflow: hidden;
background-color: #0A1128;
}
.main-controls{
padding-top: 5px;
}
h2 {
color: #0A1128;
font-size: 1rem;
}
.section {
margin: 2px;
padding: 10px;
}
.column{
float: left;
width: 50%
}
table {
margin: 0;
width: 90%;
border-collapse: collapse;
}
th{
text-align: center;
}
.row{
margin-right:50px;
margin-left:50px;
}
#colorDetect{
border: none;
color: #FEFCFB;
background-color: #0A1128;
padding: 15px;
text-align: center;
display: inline-block;
font-size: 16px;
border-radius: 4px;
}
#restart{
border: none;
color: #FEFCFB;
background-color: #7B0828;
padding: 15px;
text-align: center;
display: inline-block;
font-size: 16px;
border-radius: 4px;
}
button{
border: none;
color: #FEFCFB;
background-color: #0A1128;
padding: 10px;
text-align: center;
display: inline-block;
border-radius: 4px;
}
</style>
<body>
<div class="topnav">
<h1>ESP32-CAM Color Detection and Tracking</h1>
</div>
<div class="main-controls">
<table>
<tr>
<td><center><input type="button" id="colorDetect" value="COLOR DETECTION"></center></td>
<td><center><input type="button" id="restart" value="RESET BOARD"></center></td>
</tr>
</table>
</div>
<div class="container">
<div class = "row">
<div class = "column">
<div class="section">
<div class ="video-container">
<h2>Video Streaming</h2>
<center><img id="ShowImage" src="" style="display:none"></center>
<center><canvas id="canvas" style="display:none"></canvas></center>
</div>
</div>
<div class="section">
<table>
<tr>
<td>Quality</td>
<td><input type="range" id="quality" min="10" max="63" value="10"></td>
</tr>
<tr>
<td>Brightness</td>
<td><input type="range" id="brightness" min="-2" max="2" value="0"></td>
</tr>
<tr>
<td>Contrast</td>
<td><input type="range" id="contrast" min="-2" max="2" value="0"></td>
</tr>
</table>
</div>
<!-----ANN:5---->
<div class="section">
<h2>RGB Color Trackbars</h2>
<table>
<tr>
<td>R min:   <span id="RMINdemo"></span></td>
<td><input type="range" id="rmin" min="0" max="255" value="0" class = "slider"></td>
<td>R max:   <span id="RMAXdemo"></span></td>
<td><input type="range" id="rmax" min="0" max="255" value="50" class = "slider"></td>
</tr>
<tr>
<td>G min:   <span id="GMINdemo"></span></td>
<td><input type="range" id="gmin" min="0" max="255" value="0" class = "slider"></td>
<td>G max:   <span id="GMAXdemo"></span></td>
<td><input type="range" id="gmax" min="0" max="255" value="50" class = "slider"></td>
</tr>
<tr>
<td>B min:   <span id ="BMINdemo"></span></td>
<td><input type="range" id="bmin" min="0" max="255" value="0" class = "slider"> </td>
<td>B max:   <span id="BMAXdemo"></span></td>
<td> <input type="range" id="bmax" min="0" max="255" value="50" class = "slider"> </td>
</tr>
</table>
</div>
<div class="section">
<h2>Threshold Minimum-Binary Image</h2>
<table>
<tr>
<td>Minimum Threshold:   <span id="THRESH_MINdemo"></span></td>
<td><input type="range" id="thresh_min" min="0" max="255" value="120" class = "slider"> </td>
</tr>
</table>
</div>
<!----ANN:9--->
<div class="section">
<h2>Color Probe</h2>
<table>
<tr>
<td>X probe:   <span id="X_PROBEdemo"></span></td>
<td><input type="range" id="x_probe" min="0" max="400" value="200" class = "slider"></td>
<td>Y probe:   <span id="Y_PROBEdemo"></span></td>
<td> <input type="range" id="y_probe" min="0" max="296" value="148" class = "slider"></td>
</tr>
</table>
</div>
</div>
<div class = "column">
<div class="section">
<h2>Image Mask</h2>
<canvas id="imageMask"></canvas>
</div>
<div class="section">
<h2>Image Canvas</h2>
<canvas id="imageCanvas"></canvas>
</div>
<div class="section">
<table>
<tr>
<td><button type="button" id="invertButton" class="btn btn-primary">INVERT</button></td>
<td><button type="button" id="contourButton" class="btn btn-primary">SHOW CONTOUR</button></td>
<td><button type="button" id="trackButton" class="btn btn-primary">TRACKING</button></td>
</tr>
<tr>
<td>Invert: <span id="INVERTdemo"></span></td>
<td>Contour: <span id="CONTOURdemo"></span></td>
<td>Track: <span id="TRACKdemo"></span>
</td>
</tr>
</table>
</div>
<div class="section">
<table>
<tr>
<td><strong>XCM:</strong> <span id="XCMdemo"></span></td>
<td><strong>YCM:</strong> <span id="YCMdemo"></span></td>
</tr>
</table>
</div>
<div class="section">
<canvas id="textCanvas" width="480" height="180" style= "border: 1px solid #black;"></canvas>
<iframe id="ifr" style="display:none"></iframe>
<div id="message"></div>
</div>
</div>
</div>
</div>
<div class="modal"></div>
<script>
var colorDetect = document.getElementById('colorDetect');
var ShowImage = document.getElementById('ShowImage');
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var imageMask = document.getElementById("imageMask");
var imageMaskContext = imageMask.getContext("2d");
var imageCanvas = document.getElementById("imageCanvas");
var imageContext = imageCanvas.getContext("2d");
var txtcanvas = document.getElementById("textCanvas");
var ctx = txtcanvas.getContext("2d");
var message = document.getElementById('message');
var ifr = document.getElementById('ifr');
var myTimer;
var restartCount=0;
const modelPath = 'https://ruisantosdotme.github.io/face-api.js/weights/';
let currentStream;
let displaySize = { width:400, height: 296 }
let faceDetection;
let b_tracker = false;
let x_cm = 0;
let y_cm = 0;
let b_invert = false;
let b_contour = false;
var RMAX=50;
var RMIN=0;
var GMAX=50;
var GMIN=0;
var BMAX=50;
var BMIN=0;
var THRESH_MIN=120;
var X_PROBE=200;
var Y_PROBE=196;
var R=0;
var G=0;
var B=0;
var A=0;
colorDetect.onclick = function (event) {
clearInterval(myTimer);
myTimer = setInterval(function(){error_handle();},5000);
ShowImage.src=location.origin+'/?colorDetect='+Math.random();
}
var Module = {
onRuntimeInitialized(){onOpenCvReady();}
}
function onOpenCvReady(){
console.log("OpenCV IS READY!!!");
drawReadyText();
document.body.classList.remove("loading");
}
function error_handle() {
restartCount++;
clearInterval(myTimer);
if (restartCount<=2) {
message.textContent = "Get still error. Restart ESP32-CAM "+restartCount+" times.";
myTimer = setInterval(function(){colorDetect.click();},10000);
ifr.src = document.location.origin+'?restart';
}
else
message.textContent = "Get still error. Please close the page and check ESP32-CAM.";
}
colorDetect.style.display = "block";
ShowImage.onload = function (event) {
console.log("SHOW iMAGE");
clearInterval(myTimer);
restartCount=0;
canvas.setAttribute("width", ShowImage.width);
canvas.setAttribute("height", ShowImage.height);
canvas.style.display = "block";
imageCanvas.setAttribute("width", ShowImage.width);
imageCanvas.setAttribute("height", ShowImage.height);
imageCanvas.style.display = "block";
imageMask.setAttribute("width", ShowImage.width);
imageMask.setAttribute("height", ShowImage.height);
imageMask.style.display = "block";
context.drawImage(ShowImage,0,0,ShowImage.width,ShowImage.height);
DetectImage();
}
restart.onclick = function (event) {
fetch(location.origin+'/?restart=stop');
}
quality.onclick = function (event) {
fetch(document.location.origin+'/?quality='+this.value+';stop');
}
brightness.onclick = function (event) {
fetch(document.location.origin+'/?brightness='+this.value+';stop');
}
contrast.onclick = function (event) {
fetch(document.location.origin+'/?contrast='+this.value+';stop');
}
async function DetectImage() {
console.log("DETECT IMAGE");
let src = cv.imread(ShowImage);
arows = src.rows;
acols = src.cols;
aarea = arows*acols;
adepth = src.depth();
atype = src.type();
achannels = src.channels();
console.log("rows = " + arows);
console.log("cols = " + acols);
console.log("pic area = " + aarea);
console.log("depth = " + adepth);
console.log("type = " + atype);
console.log("channels = " + achannels);
var RMAXslider = document.getElementById("rmax");
var RMAXoutput = document.getElementById("RMAXdemo");
RMAXoutput.textContent = RMAXslider.value;
RMAXslider.oninput = function(){
RMAXoutput.textContent = this.value;
RMAX = parseInt(RMAXoutput.textContent,10);
console.log("RMAX=" + RMAX);
}
console.log("RMAX=" + RMAX);
var RMINslider = document.getElementById("rmin");
var RMINoutput = document.getElementById("RMINdemo");
RMINoutput.textContent = RMINslider.value;
RMINslider.oninput = function(){
RMINoutput.textContent = this.value;
RMIN = parseInt(RMINoutput.textContent,10);
console.log("RMIN=" + RMIN);
}
console.log("RMIN=" + RMIN);
var GMAXslider = document.getElementById("gmax");
var GMAXoutput = document.getElementById("GMAXdemo");
GMAXoutput.textContent = GMAXslider.value;
GMAXslider.oninput = function(){
GMAXoutput.textContent = this.value;
GMAX = parseInt(GMAXoutput.textContent,10);
}
console.log("GMAX=" + GMAX);
var GMINslider = document.getElementById("gmin");
var GMINoutput = document.getElementById("GMINdemo");
GMINoutput.textContent = GMINslider.value;
GMINslider.oninput = function(){
GMINoutput.textContent = this.value;
GMIN = parseInt(GMINoutput.textContent,10);
}
console.log("GMIN=" + GMIN);
var BMAXslider = document.getElementById("bmax");
var BMAXoutput = document.getElementById("BMAXdemo");
BMAXoutput.textContent = BMAXslider.value;
BMAXslider.oninput = function(){
BMAXoutput.textContent = this.value;
BMAX = parseInt(BMAXoutput.textContent,10);
}
console.log("BMAX=" + BMAX);
var BMINslider = document.getElementById("bmin");
var BMINoutput = document.getElementById("BMINdemo");
BMINoutput.textContent = BMINslider.value;
BMINslider.oninput = function(){
BMINoutput.textContent = this.value;
BMIN = parseInt(BMINoutput.textContent,10);
}
console.log("BMIN=" + BMIN);
var THRESH_MINslider = document.getElementById("thresh_min");
var THRESH_MINoutput = document.getElementById("THRESH_MINdemo");
THRESH_MINoutput.textContent = THRESH_MINslider.value;
THRESH_MINslider.oninput = function(){
THRESH_MINoutput.textContent = this.value;
THRESH_MIN = parseInt(THRESH_MINoutput.textContent,10);
}
console.log("THRESHOLD MIN=" + THRESH_MIN);
var X_PROBEslider = document.getElementById("x_probe");
var X_PROBEoutput = document.getElementById("X_PROBEdemo");
X_PROBEoutput.textContent = X_PROBEslider.value;
X_PROBEslider.oninput = function(){
X_PROBEoutput.textContent = this.value;
X_PROBE = parseInt(X_PROBEoutput.textContent,10);
}
console.log("X_PROBE=" + X_PROBE);
var Y_PROBEslider = document.getElementById("y_probe");
var Y_PROBEoutput = document.getElementById("Y_PROBEdemo");
Y_PROBEoutput.textContent = Y_PROBEslider.value;
Y_PROBEslider.oninput = function(){
Y_PROBEoutput.textContent = this.value;
Y_PROBE = parseInt(Y_PROBEoutput.textContent,10);
}
console.log("Y_PROBE=" + Y_PROBE);
document.getElementById('trackButton').onclick = function(){
b_tracker = (true && !b_tracker)
console.log("TRACKER = " + b_tracker );
var TRACKoutput = document.getElementById("TRACKdemo");
TRACKoutput.textContent = b_tracker;
}
document.getElementById('invertButton').onclick = function(){
b_invert = (true && !b_invert)
console.log("TRACKER = " + b_invert );
var INVERToutput = document.getElementById("INVERTdemo");
INVERToutput.textContent = b_invert;
}
document.getElementById('contourButton').onclick = function(){
b_contour = (true && !b_contour)
console.log("TRACKER = " + b_contour );
var CONTOURoutput = document.getElementById("CONTOURdemo");
CONTOURoutput.textContent = b_contour;
}
let tracker = 0;
var TRACKoutput = document.getElementById("TRACKdemo");
TRACKoutput.textContent = b_tracker;
var XCMoutput = document.getElementById("XCMdemo");
var YCMoutput = document.getElementById("YCMdemo");
XCMoutput.textContent = 0;
YCMoutput.textContent = 0;
var INVERToutput = document.getElementById("INVERTdemo");
INVERToutput.textContent = b_invert;
var CONTOURoutput = document.getElementById("CONTOURdemo");
CONTOURoutput.textContent = b_contour;
let M00Array = [0,];
let orig = new cv.Mat();
let mask = new cv.Mat();
let mask1 = new cv.Mat();
let mask2 = new cv.Mat();
let contours = new cv.MatVector();
let hierarchy = new cv.Mat();
let rgbaPlanes = new cv.MatVector();
let color = new cv.Scalar(0,0,0);
clear_canvas();
orig = cv.imread(ShowImage);
cv.split(orig,rgbaPlanes);
let BP = rgbaPlanes.get(2);
let GP = rgbaPlanes.get(1);
let RP = rgbaPlanes.get(0);
cv.merge(rgbaPlanes,orig);
let row = Y_PROBE
let col = X_PROBE
drawColRowText(acols,arows);
console.log("ISCONTINUOUS = " + orig.isContinuous());
R = src.data[row * src.cols * src.channels() + col * src.channels()];
G = src.data[row * src.cols * src.channels() + col * src.channels() + 1];
B = src.data[row * src.cols * src.channels() + col * src.channels() + 2];
A = src.data[row * src.cols * src.channels() + col * src.channels() + 3];
console.log("RDATA = " + R);
console.log("GDATA = " + G);
console.log("BDATA = " + B);
console.log("ADATA = " + A);
drawRGB_PROBE_Text();
let point4 = new cv.Point(col,row);
cv.circle(src,point4,5,[255,255,255,255],2,cv.LINE_AA,0);
let high = new cv.Mat(src.rows,src.cols,src.type(),[RMAX,GMAX,BMAX,255]);
let low = new cv.Mat(src.rows,src.cols,src.type(),[RMIN,GMIN,BMIN,0]);
cv.inRange(src,low,high,mask1);
cv.threshold(mask1,mask,THRESH_MIN,255,cv.THRESH_BINARY);
if(b_invert==true){
cv.bitwise_not(mask,mask2);
}
if(b_tracker == true){
try{
if(b_invert==false){
cv.findContours(mask,contours,hierarchy,cv.RETR_CCOMP,cv.CHAIN_APPROX_SIMPLE);
}
else{
cv.findContours(mask2,contours,hierarchy,cv.RETR_CCOMP,cv.CHAIN_APPROX_SIMPLE);
}
console.log("CONTOUR_SIZE = " + contours.size());
if(b_contour==true){
for(let i = 0; i < contours.size(); i++){
cv.drawContours(src,contours,i,[0,0,0,255],2,cv.LINE_8,hierarchy,100)
}
}
let cnt;
let Moments;
let M00;
let M10;
for(let k = 0; k < contours.size(); k++){
cnt = contours.get(k);
Moments = cv.moments(cnt,false);
M00Array[k] = Moments.m00;
}
let max_area_arg = MaxAreaArg(M00Array);
console.log("MAXAREAARG = "+max_area_arg);
let ArgMaxArea = MaxAreaArg(M00Array);
if(ArgMaxArea >= 0){
cnt = contours.get(MaxAreaArg(M00Array));
Moments = cv.moments(cnt,false);
M00 = Moments.m00;
M10 = Moments.m10;
M01 = Moments.m01;
x_cm = M10/M00;
y_cm = M01/M00;
XCMoutput.textContent = Math.round(x_cm);
YCMoutput.textContent = Math.round(y_cm);
console.log("M00 = "+M00);
console.log("XCM = "+Math.round(x_cm));
console.log("YCM = "+Math.round(y_cm));
fetch(document.location.origin+'/?cm='+Math.round(x_cm)+';'+Math.round(y_cm)+';stop');
console.log("M00ARRAY = " + M00Array);
let rect = cv.boundingRect(cnt);
let point1 = new cv.Point(rect.x,rect.y);
let point2 = new cv.Point(rect.x+rect.width,rect.y+rect.height);
cv.rectangle(src,point1,point2,[0,0,255,255],2,cv.LINE_AA,0);
let point3 = new cv.Point(x_cm,y_cm);
cv.circle(src,point3,2,[0,0,255,255],2,cv.LINE_AA,0);
}
else{
if(ArgMaxArea==-1){
console.log("ZERO ARRAY LENGTH");
}
else{
console.log("DUPLICATE MAX ARRAY-ELEMENT");
}
}
cnt.delete();
drawXCM_YCM_Text();
}
catch{
console.log("ERROR TRACKER NO CONTOUR");
clear_canvas();
drawErrorTracking_Text();
}
}
else{
XCMoutput.textContent = 0;
YCMoutput.textContent = 0;
}
if(b_invert==false){
cv.imshow('imageMask', mask);
}
else{
cv.imshow('imageMask', mask2);
}
cv.imshow('imageCanvas', src);
src.delete();
high.delete();
low.delete();
orig.delete();
mask1.delete();
mask2.delete();
mask.delete();
contours.delete();
hierarchy.delete();
RP.delete();
setTimeout(function(){colorDetect.click();},500);
}
function MaxAreaArg(arr){
if (arr.length == 0) {
return -1;
}
var max = arr[0];
var maxIndex = 0;
var dupIndexCount = 0;
if(arr[0] >= .90*aarea){
max = 0;
}
for (var i = 1; i < arr.length; i++) {
if (arr[i] > max && arr[i] < .99*aarea) {
maxIndex = i;
max = arr[i];
dupIndexCount = 0;
}
else if(arr[i]==max && arr[i]!=0){
dupIndexCount++;
}
}
if(dupIndexCount==0){
return maxIndex;
}
else{
return -2;
}
}
function clear_canvas(){
ctx.clearRect(0,0,txtcanvas.width,txtcanvas.height);
ctx.rect(0,0,txtcanvas.width,txtcanvas.height);
ctx.fillStyle="red";
ctx.fill();
}
function drawReadyText(){
ctx.fillStyle = 'black';
ctx.font = '20px serif';
ctx.fillText('OpenCV.JS READY',txtcanvas.width/4,txtcanvas.height/10);
}
function drawColRowText(x,y){
ctx.fillStyle = 'black';
ctx.font = '20px serif';
ctx.fillText('ImageCols='+x,0,txtcanvas.height/10);
ctx.fillText('ImageRows='+y,txtcanvas.width/2,txtcanvas.height/10);
}
function drawRGB_PROBE_Text(){
ctx.fillStyle = 'black';
ctx.font = '20px serif';
ctx.fillText('Rp='+R,0,2*txtcanvas.height/10);
ctx.fillText('Gp='+G,txtcanvas.width/4,2*txtcanvas.height/10);
ctx.fillText('Bp='+B,txtcanvas.width/2,2*txtcanvas.height/10);
ctx.fillText('Ap='+A,3*txtcanvas.width/4,2*txtcanvas.height/10);
}
function drawXCM_YCM_Text(){
ctx.fillStyle = 'black';
ctx.font = '20px serif';
ctx.fillText('XCM='+Math.round(x_cm),0,3*txtcanvas.height/10);
ctx.fillText('YCM='+Math.round(y_cm),txtcanvas.width/4,3*txtcanvas.height/10);
}
function drawErrorTracking_Text(){
ctx.fillStyle = 'black';
ctx.font = '20px serif';
ctx.fillText('ERROR TRACKING-NO CONTOUR',0,3*txtcanvas.height/10);
}
</script>
</body>
</html>
)rawliteral";
Сохраните файл.
Сетевые учётные данные
Для правильной работы программы необходимо вставить свои сетевые учётные данные в следующие переменные в файле OCV_ColorTrack_P.ino:
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Распиновка камеры
По умолчанию код использует распиновку для модуля ESP32-CAM AI-Thinker.
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
Если вы используете другую плату камеры, не забудьте вставить правильную распиновку. Вы можете перейти к следующей статье, чтобы найти распиновку для вашей платы:
Как работает код
Продолжайте читать, чтобы узнать, как работает код, или переходите к следующему разделу.
Для удобства чтения и понимания программы в код были добавлены комментарии — АННОТАЦИИ (ANNOTATIONS).
Например, распиновка для ESP32-CAM указана под аннотацией ANN:0, расположенной в файле .ino. ANN:0 можно найти с помощью команды Edit/Find в Arduino IDE.
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
Серверный скетч
Серверная программа OCV_ColorTrack.ino взята из ESP32-CAM Projects, Модуль 5 авторства Руи Сантоса и Сары Сантос. Она содержит стандартную функцию setup() для камеры ESP32, которая настраивает камеру, IP-адрес сервера и пароль.
Аннотация 1 (ANN:1)
Однако, что не является стандартным в этой серверной программе — это инструкции, имеющие жизненно важное значение, которые разрешают контроль доступа (Access-Control). Смотрите код в ANN:1.
//ANN:1
client.println("HTTP/1.1 200 OK");
client.println("Access-Control-Allow-Origin: *");
client.println("Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept");
client.println("Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS");
client.println("Content-Type: image/jpeg");
client.println("Content-Disposition: form-data; name=\"imageFile\"; filename=\"picture.jpg\"");
client.println("Content-Length: " + String(fb->len));
client.println("Connection: close");
client.println();
Это инструктирует браузер разрешить совместную работу изображения камеры и OpenCV.js, которые имеют разные источники, в программе. Без этих инструкций браузер Chrome выдаёт ошибки.
Аннотация 2 (ANN:2)
Серверный цикл loop() отслеживает сообщения клиента и декодирует их через ExecuteCommand(), который находится в ANN:2.
//ANN:2
void ExecuteCommand() {
if (cmd!="colorDetect") { //Omit printout
//Serial.println("cmd= "+cmd+" ,P1= "+P1+" ,P2= "+P2+" ,P3= "+P3+" ,P4= "+P4+" ,P5= "+P5+" ,P6= "+P6+" ,P7= "+P7+" ,P8= "+P8+" ,P9= "+P9);
//Serial.println("");
}
if (cmd=="resetwifi") {
WiFi.begin(P1.c_str(), P2.c_str());
Serial.print("Connecting to ");
Serial.println(P1);
long int StartTime=millis();
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
if ((StartTime+5000) < millis()) break;
}
Serial.println("");
Serial.println("STAIP: "+WiFi.localIP().toString());
Feedback="STAIP: "+WiFi.localIP().toString();
}
else if (cmd=="restart") {
ESP.restart();
}
else if (cmd=="cm"){
int XcmVal = P1.toInt();
int YcmVal = P2.toInt();
Serial.println("cmd= "+cmd+" ,VALXCM= "+XcmVal);
Serial.println("cmd= "+cmd+" ,VALYCM= "+YcmVal);
}
else if (cmd=="quality") {
sensor_t * s = esp_camera_sensor_get();
int val = P1.toInt();
s->set_quality(s, val);
}
else if (cmd=="contrast") {
sensor_t * s = esp_camera_sensor_get();
int val = P1.toInt();
s->set_contrast(s, val);
}
else if (cmd=="brightness") {
sensor_t * s = esp_camera_sensor_get();
int val = P1.toInt();
s->set_brightness(s, val);
}
else {
Feedback="Command is not defined.";
}
if (Feedback=="") {
Feedback=Command;
}
}
Исходная программа использует эту функцию для приёма и выполнения команд от ползунков в клиенте, которые управляют характеристиками изображения и передаются клиентом через инструкцию «fetch».
В нашей текущей программе эта функция, описание которой будет дано далее, используется для передачи «центра масс» цветовой цели, обнаруженной клиентом, на сервер ESP32 — функция, жизненно важная для робототехнических приложений.
Кроме изменения, связанного с извлечением координат x и y центра масс и их выводом, в серверной программе нет других изменений.
Клиентский скетч (OpenCV.js)
Помимо подпрограмм ползунков характеристик изображения и их передачи данных на сервер через «fetch», а также подпрограммы обработки ошибок в исходной клиентской программе электронной книги, упомянутой выше, клиентская программа здесь является новой и содержит код, посвящённый применению OpenCV.js к изображению камеры ESP32, передаваемому в браузер (как упоминалось ранее, «fetch» используется для передачи данных о цветовой цели на сервер).
Клиентский код обильно снабжён инструкциями console.log, которые позволяют пользователю видеть результаты работы кода. Консоль Chrome console.log открывается нажатием CTRL + SHIFT + J одновременно.
Аннотация 3 (ANN:3)
ANN:3 подключает последнюю версию OpenCV.js в нашу веб-страницу. Нажмите здесь, чтобы узнать больше.
<script async src=" https://docs.opencv.org/master/opencv.js" type="text/javascript"></script>
ANN:READY
ANN:READY отмечает модуль, который сигнализирует о том, что OpenCV.js инициализирован. После завершения инициализации можно нажать кнопку Color Detection. Хотя более быстрые компьютеры не требуют этой возможности, она включена для полноты.
Аннотация 4 (ANN:4)
Скриншот клиентской программы, работающей в Chrome, показывает две колонки, созданные HTML-частью кода. В левой колонке отображается исходное изображение камеры, которое передаётся примерно с частотой 1 кадр в секунду. Это изображение с идентификатором ShowImage является исходным изображением для подпрограммы OpenCV в программе.
ANN:4 отмечает создание src и его характеристик: rows, cols и т.д.
//ANN:4
let src = cv.imread(ShowImage);
arows = src.rows;
acols = src.cols;
aarea = arows*acols;
adepth = src.depth();
atype = src.type();
achannels = src.channels();
console.log("rows = " + arows);
console.log("cols = " + acols);
console.log("pic area = " + aarea);
console.log("depth = " + adepth);
console.log("type = " + atype);
console.log("channels = " + achannels);
Ползунки RGB Color Trackbars
Под исходным изображением находятся три оригинальных ползунка характеристик изображения (Quality, Brightness и Contrast), а также RGB Color Trackbars.
Они используются для установки пределов диапазона цветов, допустимых в «обработанном» изображении в CV-приложении. Код для ползунков находится в ANN:5, ANN:6.
<!-----ANN:5---->
<div class="section">
<h2>RGB Color Trackbars</h2>
<table>
<tr>
<td>R min:   <span id="RMINdemo"></span></td>
<td><input type="range" id="rmin" min="0" max="255" value="0" class = "slider"></td>
<td>R max:   <span id="RMAXdemo"></span></td>
<td><input type="range" id="rmax" min="0" max="255" value="50" class = "slider"></td>
</tr>
<tr>
<td>G min:   <span id="GMINdemo"></span></td>
<td><input type="range" id="gmin" min="0" max="255" value="0" class = "slider"></td>
<td>G max:   <span id="GMAXdemo"></span></td>
<td><input type="range" id="gmax" min="0" max="255" value="50" class = "slider"></td>
</tr>
<tr>
<td>B min:   <span id ="BMINdemo"></span></td>
<td><input type="range" id="bmin" min="0" max="255" value="0" class = "slider"></td>
<td>B max:<span id="BMAXdemo"></span></td>
<td> <input type="range" id="bmax" min="0" max="255" value="50" class = "slider"></td>
</tr>
</table>
</div>
//ANN:6
var RMAXslider = document.getElementById("rmax");
var RMAXoutput = document.getElementById("RMAXdemo");
RMAXoutput.textContent = RMAXslider.value;
RMAXslider.oninput = function() {
RMAXoutput.textContent = this.value;
RMAX = parseInt(RMAXoutput.textContent,10);
console.log("RMAX=" + RMAX);
}
console.log("RMAX=" + RMAX);
var RMINslider = document.getElementById("rmin");
var RMINoutput = document.getElementById("RMINdemo");
RMINoutput.textContent = RMINslider.value;
RMINslider.oninput = function(){
RMINoutput.textContent = this.value;
RMIN = parseInt(RMINoutput.textContent,10);
console.log("RMIN=" + RMIN);
}
console.log("RMIN=" + RMIN);
var GMAXslider = document.getElementById("gmax");
var GMAXoutput = document.getElementById("GMAXdemo");
GMAXoutput.textContent = GMAXslider.value;
GMAXslider.oninput = function(){
GMAXoutput.textContent = this.value;
GMAX = parseInt(GMAXoutput.textContent,10);
}
console.log("GMAX=" + GMAX);
var GMINslider = document.getElementById("gmin");
var GMINoutput = document.getElementById("GMINdemo");
GMINoutput.textContent = GMINslider.value;
GMINslider.oninput = function(){
GMINoutput.textContent = this.value;
GMIN = parseInt(GMINoutput.textContent,10);
}
console.log("GMIN=" + GMIN);
var BMAXslider = document.getElementById("bmax");
var BMAXoutput = document.getElementById("BMAXdemo");
BMAXoutput.textContent = BMAXslider.value;
BMAXslider.oninput = function(){
BMAXoutput.textContent = this.value;
BMAX = parseInt(BMAXoutput.textContent,10);
}
console.log("BMAX=" + BMAX);
var BMINslider = document.getElementById("bmin");
var BMINoutput = document.getElementById("BMINdemo");
BMINoutput.textContent = BMINslider.value;
BMINslider.oninput = function(){
BMINoutput.textContent = this.value;
BMIN = parseInt(BMINoutput.textContent,10);
}
console.log("BMIN=" + BMIN);
Максимальные и минимальные значения красного, зелёного и синего (RGB) применяются к функции OpenCV inRange() в ANN:7.
let high = new cv.Mat(src.rows,src.cols,src.type(),[RMAX,GMAX,BMAX,255]);
let low = new cv.Mat(src.rows,src.cols,src.type(),[RMIN,GMIN,BMIN,0]);
cv.inRange(src,low,high,mask1);
//inRange(source image, lower limit, higher limit, destination image)
cv.threshold(mask1,mask,THRESH_MIN,255,cv.THRESH_BINARY);
//threshold(source image,destination image,threshold,255,threshold method);
Изображение является 4-канальным: RGBA, где A — уровень прозрачности. В этом уроке A будет установлен на 100% непрозрачности, то есть 255. Код основан на том, что, помимо плоскости A, изображение имеет 3 цветовые плоскости RGB, каждый пиксель в каждой плоскости имеет значение от 0 до 255. Верхние/нижние пределы применяются к соответствующим цветовым плоскостям для каждого пикселя.
Обратите внимание, что inRange() имеет выходное изображение, которое было создано ранее в программе (ANN:8).
let M00Array = [0,];
let orig = new cv.Mat();
let mask = new cv.Mat();
let mask1 = new cv.Mat();
let mask2 = new cv.Mat();
let contours = new cv.MatVector();
let hierarchy = new cv.Mat();
let rgbaPlanes = new cv.MatVector();
let color = new cv.Scalar(0,0,0);
clear_canvas();
orig = cv.imread(ShowImage);
cv.split(orig,rgbaPlanes); //SPLIT
let BP = rgbaPlanes.get(2); // SELECTED COLOR PLANE
let GP = rgbaPlanes.get(1);
let RP = rgbaPlanes.get(0);
cv.merge(rgbaPlanes,orig);
Важно: каждое изображение, созданное в программе OpenCV, должно быть удалено, чтобы избежать утечки памяти компьютера (ANN:8A).
src.delete();
high.delete();
low.delete();
orig.delete();
mask1.delete();
mask2.delete();
mask.delete();
contours.delete();
hierarchy.delete();
//cnt.delete();
RP.delete();
Выходное изображение Mask1 не отображается в программе, хотя могло бы. Однако оно используется функцией threshold(), следующей сразу за inRange().
Функция threshold() проверяет составное значение пикселя исходного изображения и устанавливает соответствующее значение назначения на 0 или 255 в зависимости от того, меньше или больше исходное значение порога. Верхнее изображение в правой колонке показывает это бинарное изображение.
Для полноты была добавлена функция инвертирования бинарного изображения. При нажатии кнопки INVERT на веб-странице бинарное изображение инвертируется (чёрное становится белым, белое — чёрным), и последующая обработка выполняется над новым изображением. Кнопка бистабильная, поэтому второе нажатие возвращает бинарное изображение в исходное состояние.
Зонд целевого цвета
На скриншоте красная крышка является целью в обычной комнатной обстановке с обычной 60-ваттной люминесцентной лампой. Лампа излучает красный, зелёный и синий свет. Красная крышка отражает красный, зелёный и синий свет, но преимущественно красный. Метод определения количества каждого отражённого цвета будет описан сейчас. Этот метод позволяет установить ползунки RGB с минимальными усилиями. Его использование настоятельно рекомендуется.
Изображение предоставлено Andrew R. Sass
Метод включает использование ползунков Color Probe. Эти два ползунка, X и Y Probe, используются для размещения маленького белого кружка зонда в нужной позиции на нижнем изображении правой колонки. Значения RGB в этой позиции зонда измеряются и используются для установки максимумов и минимумов inRange() RGB, описанных ранее.
Смотрите ANN:9, 9A, 9B, 9C для кода, связанного с этим зондом.
Когда оптимальные значения для нужной цели найдены с помощью зонда X, Y и установлены ползунками, цель в бинарном изображении становится белой, а остальная часть изображения — чёрной, в идеале, как показано на скриншоте.
Этот идеал обычно может быть реализован только при тщательном контроле условий освещения. Стандартное комнатное освещение является приемлемым. Для оптимальных результатов можно использовать фильтры, но здесь они не использовались.
Вот ещё один пример:
Отслеживание
Когда бинарное изображение сочтено приемлемым, можно нажать бистабильную кнопку TRACKING. ANN:10 отмечает начало подпрограммы отслеживания.
//ANN:10
if(b_tracker == true){
try{
if(b_invert==false){
Поскольку, как упоминалось выше, эта статья не касается функции INVERT, интерес представляет только случай b_invert равный false.
ANN:11 Первый шаг в отслеживании — findContours, алгоритм OpenCV, который находит контуры всех белых объектов в бинарном изображении.
//ANN:11
cv.findContours(mask,contours,hierarchy,cv.RETR_CCOMP,cv.CHAIN_APPROX_SIMPLE);
//findContours(source image, array of contours found, hierarchy of contours
// if contours are inside other contours, method of contour data retrieval,
//algorithm method)
}
else{
cv.findContours(mask2,contours,hierarchy,cv.RETR_CCOMP,cv.CHAIN_APPROX_SIMPLE);
}
console.log("CONTOUR_SIZE = " + contours.size());
//draw contours
if(b_contour==true){
for(let i = 0; i < contours.size(); i++){
cv.drawContours(src,contours,i,[0,0,0,255],2,cv.LINE_8,hierarchy,100)
}
}
Если кнопка отслеживания нажата, когда бинарное изображение полностью чёрное, инструкции, зависящие от вывода findContours, выбросят исключения; try-catch позволяет программе безопасно продолжить работу, выводя сообщение в console.log и текстовое поле.
Contours.size() — это выход findContours и представляет собой массив контуров белых объектов, найденных в бинарном изображении. Contours.size() определяет количество элементов в массиве. Иерархия (контуры внутри других контуров) здесь не представляет интереса, так как белых объектов (обведённых чёрным) внутри других белых объектов не будет.
ANN:12 Отмечает начало нахождения моментов найденных контуров.
//ANN:12
let cnt;
let Moments;
let M00;
let M10;
M00 — это нулевой момент — «площадь», ограниченная контуром. В OpenCV это фактически количество пикселей, заключённых в контуре. M10 и M01 — это взвешенное по координатам x и y количество пикселей.
Как обычно, начало координатной системы x,y находится в верхнем левом углу изображения. X положителен горизонтально вправо, а Y положителен вертикально вниз. Поэтому M10/M00 и M01/M00 — это координаты x,y центроида контура в массиве.
ANN:13, 13A отмечает нахождение контура с наибольшей площадью в массиве контуров с помощью функции MaxAreaArg и передачу центроида x_cm, y_cm на ESP32 через инструкцию fetch.
//ANN:13
for(let k = 0; k < contours.size(); k++){
cnt = contours.get(k);
Moments = cv.moments(cnt,false);
M00Array[k] = Moments.m00;
// cnt.delete();
}
//ANN13A
let max_area_arg = MaxAreaArg(M00Array);
console.log("MAXAREAARG = "+max_area_arg);
//let TestArray = [0,0,0,15,4,15,2];
//let TestArray0 = [];
//let max_test_area_arg = MaxAreaArg(TestArray0);
//console.log("MAXTESTAREAARG = "+max_test_area_arg);
let ArgMaxArea = MaxAreaArg(M00Array);
if(ArgMaxArea >= 0){
cnt = contours.get(MaxAreaArg(M00Array)); //use the contour with biggest MOO
//cnt = contours.get(54);
Moments = cv.moments(cnt,false);
M00 = Moments.m00;
M10 = Moments.m10;
M01 = Moments.m01;
x_cm = M10/M00; // 75 for circle_9.jpg
y_cm = M01/M00; // 41 for circle_9.jpg
XCMoutput.textContent = Math.round(x_cm);
YCMoutput.textContent = Math.round(y_cm);
console.log("M00 = "+M00);
console.log("XCM = "+Math.round(x_cm));
console.log("YCM = "+Math.round(y_cm));
//fetch(document.location.origin+'/?xcm='+Math.round(x_cm)+';stop');
fetch(document.location.origin+'/?cm='+Math.round(x_cm)+';'+Math.round(y_cm)+';stop');
console.log("M00ARRAY = " + M00Array);
Во время работы программы координаты центроида отображаются в Serial Monitor, а также в console.log и в текстовом поле на экране браузера. ESP32 может использовать данные центроида для целей отслеживания в робототехнических приложениях.
ANN:14 Отмечает код для синего ограничивающего прямоугольника, который ограничивает контур с наибольшей площадью, и центроид этого контура. Их можно увидеть на нижнем изображении в правой колонке экрана браузера.
//ANN:14
//**************min area bounding rect********************
//let rotatedRect=cv.minAreaRect(cnt);
//let vertices = cv.RotatedRect.points(rotatedRect);
//for(let j=0;j<4;j++){
// cv.line(src,vertices[j],
// vertices[(j+1)%4],[0,0,255,255],2,cv.LINE_AA,0);
//}
//***************end min area bounding rect*************************************
//***************bounding rect***************************
let rect = cv.boundingRect(cnt);
let point1 = new cv.Point(rect.x,rect.y);
let point2 = new cv.Point(rect.x+rect.width,rect.y+rect.height);
cv.rectangle(src,point1,point2,[0,0,255,255],2,cv.LINE_AA,0);
//*************end bounding rect***************************
//*************draw center point*********************
let point3 = new cv.Point(x_cm,y_cm);
cv.circle(src,point3,2,[0,0,255,255],2,cv.LINE_AA,0);
//***********end draw center point*********************
}//end if(ArgMaxArea >= 0)
else{
if(ArgMaxArea==-1){
console.log("ZERO ARRAY LENGTH");
}
else{ //ArgMaxArea=-2
console.log("DUPLICATE MAX ARRAY-ELEMENT");
}
}
cnt.delete();
Под нижним изображением в правой колонке текстовое поле содержит выбранные выходные данные программы, включая данные зонда X, Y, координаты центроида и вывод catch, если генерируется исключение, как упоминалось выше.
Загрузка кода
После ввода сетевых учётных данных и распиновки для вашей камеры вы можете загрузить код.
В меню Tools выберите следующие настройки перед загрузкой кода на плату.
BOARD: ESP32 Wrover Module
Flash Mode: «QIO»
PARTITION SCHEME: «Huge App (3Mb No OTA/1MB SPIFFS)»
Flash Frequency: «80 Mhz»
Upload Speed: «115200»
Core Debug Level: «None»
Тестирование программы
После загрузки кода откройте Serial Monitor на скорости 115200 бод. Нажмите встроенную кнопку RST, и IP-адрес ESP должен быть напечатан. В данном случае IP-адрес — 192.168.1.95.
Откройте браузер в вашей локальной сети и введите IP-адрес ESP32-CAM.
Откройте console.log при открытии браузера. Проверьте, правильно ли загружается OpenCV.js. В правом нижнем углу веб-страницы должно отображаться «OpenCV.JS READY».
Затем нажмите кнопку Color Detection в верхней левой колонке окна браузера.
Вы должны увидеть похожее окно без сообщений об ошибках.
После установки правильных настроек для нацеливания на цвет с помощью зонда целевого цвета (как объяснялось ранее), нажмите кнопку Tracking.
В то же время координаты центроида цели должны отображаться на веб-странице, а также в Serial Monitor ESP32-CAM.
Заключение
Ни один из элементов проекта, описанного в этом уроке, не является новым. Веб-сервер камеры ESP32 и OpenCV были подробно и детально описаны в литературе.
Новизна здесь заключается в объединении этих двух технологий через OpenCV.js. Камера ESP32 с её малыми размерами, Wi-Fi, высокими технологиями и низкой стоимостью обещает стать интересным новым фронтенд-средством захвата изображений для веб-серверных приложений OpenCV.
Узнайте больше об ESP32-CAM
Мы надеемся, что вам понравился этот проект. Узнайте больше об ESP32-CAM в наших уроках:
Об авторе Andrew R. Sass
Этот проект/урок был разработан Andrew R. Sass. Мы отредактировали урок, чтобы он соответствовал стилю наших уроков. Помимо некоторых CSS, код является оригинальным, предоставленным Andrew.
Биография автора: Andrew («DOC») R. Sass имеет степени BSEE (MIT), MSEE и PhD EE (PURDUE). Он является инженером-исследователем на пенсии (компоненты интегральных схем), преподавателем на пенсии второй карьеры (AP Physics, Physics, Robotics) и был наставником местной робототехнической команды FIRST.