带有 OpenCV.js 的 ESP32-CAM Web 服务器:颜色识别和跟踪

释放双眼,带上耳机,听听看~!

本教程介绍了使用 ESP32 摄像头网络服务器环境的 OpenCV.js 和 OpenCV 工具。例如,我们将构建一个简单的 ESP32 摄像头网络服务器,其中包括对移动物体的颜色检测和跟踪。

ESP32-CAM-Web-Server-with-OpenCVjs-Color-Detection-Tracking

本教程绝不是对 OpenCV 可以提供给 ESP32 摄像头网络服务器的所有内容的详尽处理。此介绍将激发更多使用 ESP32 相机的 OpenCV 工作。

这个项目/教程是基于国外作者(Andrew R. Sass)的项目创建的。

介绍

ESP32 可以作为浏览器客户端的服务器,某些型号包括一个摄像头(例如,ESP32-CAM),允许客户端在浏览器中查看静态或视频图片。HTML、JavaScript 和其它浏览器语言可以利用 ESP32 及其相机的广泛功能。

对于那些对 ESP32 相机开发板经验很少或没有经验的人,可以从以下教程开始。

ESP32-CAM视频流和Arduino IDE的人脸识别

OpenCV.js

docs.opencv.org 中所述,OpenCV(开源计算机视觉库:http : //opencv.org)是一个包含数百种计算机视觉算法的开源库。OpenCV.js 使用 Emscripten,一个 JavaScript 的编译器,为一个不断增长的 API 库编译 OpenCV 的函数。

OpenCV-logo

OpenCV.js 在浏览器中运行,它允许仅具有适度 HTML 和 JavaScript 背景的人快速试用 OpenCV 功能。那些有 Esp32 相机应用背景的人已经有了这个背景。

项目概况

我们将在本教程中构建的项目创建一个允许对移动对象进行颜色跟踪的 Web 服务器。在 Web 服务器界面上,您可以使用多种配置来正确选择要跟踪的颜色。然后,浏览器将移动物体实时 x 和 y 坐标发送到 ESP32 板。

ESP32-CAM-Color-Tracking-Project-Overview

这是 Web 服务器的预览。

ESP32-CAM-Color-Tracking-Web-Server-Preview

先决条件

在继续此项目之前,请确保正确安装了ESP32的环境组件。

Arduino IDE

我们将使用 Arduino IDE 对 ESP32 板进行编程。因此,您需要安装 Arduino IDE 以及 ESP32 组件:

一、ESP32开发环境搭建(arduino)

 

获得 ESP32 相机

该项目与任何具有 OV2640 摄像头的 ESP32 摄像头板兼容。有几种 ESP32 相机型号。

确保您知道您正在使用的相机板的引脚分配。对于最流行的开发板的引脚分配,请查看这篇文章:

ESP32-CAM AI-Thinker引脚指南:GPIO使用说明

代码 – 带有 OpenCV.js 的 ESP32-CAM

该计划由两部分组成:

  • 在 ESP32 相机上运行服务器程序
  • 在 Chrome 浏览器上运行客户端程序

该程序分为两个文件: OCV_ColorTrack_P.ino 包含服务器程序和 index_OCV_ColorTrack.h 包含客户端程序的头文件(HTML、CSS 和 JavaScript with 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.
  
  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_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_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 中打开一个新选项卡,如下图所示。

Arduino-IDE-Create-New-Tab

Arduino-IDE-Create-New-Tab

命名为 index_OCV_ColorTrack.h.

Arduino-IDE-name-new-file-tab

将以下内容复制到该文件中:

 

保存文件。

网络凭据

为了使程序正常工作,您需要在以下变量中插入您的网络凭据 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

如果您使用的是不同的相机板,请不要忘记正确分配引脚。

代码的工作原理

继续阅读以了解代码的工作原理,或跳到下一部分。

为了便于阅读和理解程序,说明一已被添加在程序注释。

例如,ESP32-CAM 的接线列在ANN :0注释下方,位于.ino文件。ANN :0可以通过 Arduino IDE 的 Edit/Find 命令找到。

#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 摄像头 setup()  它配置了摄像机和服务器的 IP 地址和密码。

注释 1 (ANN:1)

然而,在这个服务器程序中,允许访问控制的非常重要的指令不是标准的。参见代码:ANN1:

//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()  监视客户端消息,并通过ANN:2中的 ExecuteCommand()进行解码:

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

原始程序使用该函数接收和执行客户端中的滑块,该滑块控制图像特征并由客户端通过“获取”指令传输。

在我们当前的程序中,这个特性(待描述)用于将客户端检测到的颜色目标的“center-of-mass(坐标)”传达给 ESP32 服务器;对机器人应用程序至关重要的功能。

除了提取 x 和 y 坐标并打印出来的变化外,服务器程序没有其它变化。

客户端程序 (OpenCV.js)

除了上面引用的原始客户端程序电子书中的图像特征滑块例程及其通过“获取”到服务器的数据传输和错误例程之外,这里的客户端程序是新的,包含专门用于 OpenCV 应用的代码。 js 传输到浏览器的 ESP32 相机图像(如前所述,“获取”用于将颜色目标数据传输到服务器)。

客户端代码大量输出控制台日志,这允许用户查看代码结果的指令。Chrome控制台日志通过同时按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:准备好了

ANN : READY标记表示 OpenCV.js 已初始化的模块。初始化完成后,可以单击颜色检测按钮。虽然更快的计算机不需要此功能,但为了完整起见,将其包含在内。

ESP32-CAM-Web-Server-OpenCVJS-Ready

注释 4 (ANN:4)

在 Chrome 上运行的客户端程序的屏幕截图显示了由代码的 HTML 部分创建的两列。左列显示以大约 1 fps 传输的相机的原始图像。此图片,ID 为显示图像 是程序中OpenCV代码例程的源镜像。

ANN : 4标志着src及其特性的创建;行、列等。

//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 颜色轨迹栏

源图像下方是原始的三个图像特征滑块(质量、亮度和对比度),还有RGB 颜色轨迹栏。

ESP32-CAM-Color-Tracking-RGB-Trackbars

这些用于设置 CV 应用程序中“已处理”图像中允许的颜色颜色范围的限制。跟踪条的代码可在ANN : 5 , ANN : 6 中找到。

<!-----ANN:5---->
<div class="section">
<h2>RGB Color Trackbars</h2>
<table>
  <tr>
    <td>R min:&#160;&#160;&#160;<span id="RMINdemo"></span></td>
    <td><input type="range" id="rmin" min="0" max="255" value="0" class = "slider"></td>
    <td>R max:&#160;&#160;&#160;<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:&#160;&#160;&#160;<span id="GMINdemo"></span></td>
    <td><input type="range" id="gmin" min="0" max="255" value="0" class = "slider"></td>
    <td>G max:&#160;&#160;&#160;<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:&#160;&#160;&#160;<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.innerHTML = RMAXslider.value;
RMAXslider.oninput = function() {
  RMAXoutput.innerHTML = this.value;
  RMAX = parseInt(RMAXoutput.innerHTML,10);
  console.log("RMAX=" + RMAX);
}
console.log("RMAX=" + RMAX);

var RMINslider = document.getElementById("rmin");
var RMINoutput = document.getElementById("RMINdemo");
RMINoutput.innerHTML = RMINslider.value;
RMINslider.oninput = function(){
RMINoutput.innerHTML = this.value;
  RMIN = parseInt(RMINoutput.innerHTML,10);
  console.log("RMIN=" + RMIN);
}
console.log("RMIN=" + RMIN);

var GMAXslider = document.getElementById("gmax");
var GMAXoutput = document.getElementById("GMAXdemo");
GMAXoutput.innerHTML = GMAXslider.value;
GMAXslider.oninput = function(){
  GMAXoutput.innerHTML = this.value;
  GMAX = parseInt(GMAXoutput.innerHTML,10);
}
console.log("GMAX=" + GMAX);

var GMINslider = document.getElementById("gmin");
var GMINoutput = document.getElementById("GMINdemo");
GMINoutput.innerHTML = GMINslider.value;
GMINslider.oninput = function(){
  GMINoutput.innerHTML = this.value;
  GMIN = parseInt(GMINoutput.innerHTML,10);
}
console.log("GMIN=" + GMIN);

var BMAXslider = document.getElementById("bmax");
var BMAXoutput = document.getElementById("BMAXdemo");
BMAXoutput.innerHTML = BMAXslider.value;
BMAXslider.oninput = function(){
  BMAXoutput.innerHTML = this.value;
  BMAX = parseInt(BMAXoutput.innerHTML,10);
}
console.log("BMAX=" + BMAX);

var BMINslider = document.getElementById("bmin");
var BMINoutput = document.getElementById("BMINdemo");
BMINoutput.innerHTML = BMINslider.value;
BMINslider.oninput = function(){
  BMINoutput.innerHTML = this.value;
  BMIN = parseInt(BMINoutput.innerHTML,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没有显示在程序中,尽管它可能是。但是,它被紧跟在inRange()之后的threshold()函数使用。

Threshold()函数检查复合源图像像素值,并根据源值是小于还是大于阈值将相应的目标值设置为0或255。右上角的图像显示了此二进制图像。

为了完整性,在二值图像中增加了反转功能。当单击网页中的反转按钮时,二值图像被反转(黑色变为白色,白色变为黑色),并且对新图像执行后续处理。按钮是双稳态的,因此第二次按下会使二值图像恢复到原始状态。

目标颜色探针

ESP32-CAM-Color-Tracking-Color-Probe

截图中,红帽是普通房间环境中的目标,普通60W日光灯。灯发出红色、绿色和蓝色。红帽反射红色、绿色和蓝色,但主要是红色。现在将描述检测每种反射颜色的量的方法。此方法允许以最小的努力设置 RGB 轨迹栏。强烈建议使用它。

ESP32-CAM-Web-Server-Color-Tracking-Example-2

该方法涉及使用颜色探针滑块。这两个滑块 X 和 Y 探针用于将一个小的白色圆圈探针放置在右侧列底部图像中的所需位置。测量此探头位置的 RGB 值并用于设置inRange()之前描述的 RGB 最大值和最小值。

请参阅ANN : 9 , 9A,9B,9C以了解与此探针相关联的代码。

当使用 X、Y 探针找到所需目标的最佳值并由轨迹栏设置时,二值图像中的目标为白色,图像的其余部分为黑色,理想情况下,如屏幕截图所示。

这种理想通常只有在可以密切控制照明条件时才能实现。室内,标准房间照明是可以接受的。过滤器可用于获得最佳结果,但此处未使用。

这是另一个例子:

ESP32-CAM-Web-Server-Color-Tracking-Example

追踪

一旦认为二值图像可以接受,就可以单击双稳态的TRACKING按钮。ANN : 10标志着跟踪程序的开始。

//ANN:10
if(b_tracker == true){
try{
 if(b_invert==false){

由于,如上所述,本文不关注 INVERT 功能,只关注 b_invert 等于 false 是有意义的

ANN : 11跟踪的第一步是查找轮廓,这是 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允许程序安全继续,在控制台日志和文本框中发布输出。Conours.sizecontours是findContours的输出,是在二进制图像中找到的白色对象的轮廓数组。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标记使用最大面积 函数并传输质心, x_cm, y_cm 通过获取指令到 ESP32。

//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.innerHTML = Math.round(x_cm);
YCMoutput.innerHTML = 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);

在程序运行期间,可以看到质心坐标打印在串行监视器以及 控制台日志并在浏览器屏幕的文本框中。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 探针数据、质心坐标和捕获输出(如果如上所述生成异常)。

ESP32-CAM-Color-Tracking-X-Y-Coordinates-messages

上传代码

插入您的网络凭据和您正在使用的相机的引脚排列后,您可以上传代码。

在将代码上传到您的开发板之前,在工具菜单中选择以下设置。

ESP32-CAM-Wrover-Upload-Options
  • 开发板:ESP32 Wrover Module
  • 上传模式:“QIO”
  • 分区方案:“Huge App (3Mb No OTA/1MB SPIFFS)”
  • 上传频率:“80 Mhz”
  • 上传速度:“115200”
  • 核心调试级别:“无”

测试程序

上传代码后,以115200的波特率打开串口监视器,按下板载的RST按钮,应该会打印出ESP的IP地址。在这种情况下,IP地址是192.168.1.95.

ESP32-CAM-IP-Address-Serial-Monitor

在本地网络上打开浏览器并键入  ESP32-CAM IP 地址。

当浏览器打开时打开控制台日志。检查 OpenCV.js 是否正确加载。在网页的右下角,它应该显示“OpenCV.JS READY”。

然后左键单击浏览器窗口左上列中的颜色检测按钮。

您应该会看到一个类似的窗口,并且没有错误消息。

ESP32-CAM-Color-Detection-Tracking-Web-Server-Preview

在 Target-color Probe 中设置正确的设置以定位颜色后(如前所述),单击Tracking按钮。

同时,目标的质心坐标应显示在网页上以及 ESP32-CAM 串行监视器上。

Serial-Monitor-Color-Tracking-Results-ESP32-CAM-OpenCV-JS

总结

本教程中描述的项目元素都不是新元素。ESP32 摄像头网络服务器和 OpenCV 都在文章中进行了广泛和详细的描述。

这里的新颖之处在于通过 OpenCV.js 将这两种技术结合起来。ESP32 相机凭借其小尺寸、Wi-Fi、高科技和低成本功能,有望成为 OpenCV Web 服务器应用程序的一种有趣的新型前端图像捕获功能。

 

来自文章原作者英文翻译:Andrew R. Sass。

给TA买糖
共{{data.count}}人
人已赞赏
ESP32-CAM免费项目

使用ESP32-CAM将图像发布到本地或云服务器-PHP(Photo Manager)

2021-5-31 12:58:14

ESP32-CAM

ESP32-CAM使用舵机控制旋转-视频流网络服务器(2 轴)

2021-6-5 23:12:27

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索