Riconoscere una targa con il modulo ESP32-CAM e Google

Il tema della domotica è in continua evoluzione: vediamo sempre più frequentemente videocitofoni connessi ai quali è possible accedere da remoto, ma anche termostati “intelligenti” comandabili anche all’esterno della propria casa e che riescono a capire le nostre esigenze. L’attuale tendenza è infatti quella di riuscire a sfruttare la tecnologia e la rete per poter avere sempre il controllo della propria abitazione.

Non solo, questa tendenza coinvolge anche diversi luoghi pubblici o lavorativi. Infatti, ultimamente sempre più servizi sono stati automatizzati attraverso questi nuovi strumenti di impronta domotica; basti pensare che in alcuni parcheggi si può pagare attraverso il telepass e quindi senza dover fare un biglietto o pagare presso uno sportello. Andiamo incontro ad un mondo totalmente connesso, in grado di assecondare ogni nostro desiderio; un mondo dove la capacità di analizzare e discriminare immagini e video gioca un ruolo chiave.

In questo post presenteremo uno strumento in grado di azionare un attuatore se verrà riconosciuta e ritenuta valida la targa del veicolo inquadrata con la telecamera.
Le applicazioni sono innumerevoli: dal semplice garage di casa, fino alla gestione di un ingresso aziendale, in modo tale da permettere al dipendente di entrare senza attesa.
Addirittura in una nostra prova, al posto dell’attuatore, è stata inserita la comunicazione diretta con il garage attraverso il protocollo MQTT, particolarmente adatto alla domotica.
Una base che offre diversi spunti per la personalizzazione da parte di chi tra voi se la sentirà.

Per quest’oggetto abbiamo utilizzato niente di più di un ESP32-CAM AI-Thinker, un sensore PIR, due LED (uno per segnalare l’azione corrente ed uno per simulare l’attuatore) ed alcune chiamate REST verso Google per aiutare il nostro hardware, sicuramente poco propenso a fare l’elaborazione delle immagini, nel suo compito.

Principio di funzionamento

Come interagiscono fra loro questi dispositivi?
Il sensore di movimento PIR rileva la presenza di un veicolo o di un ostacolo, a quel punto interviene la camera che scatta la foto della targa e la invia ad un nostro server di appoggio.
Il compito di quest’ultimo è di fare, in prima battuta, il forward della richiesta verso l’API di Google.
Una volta ricevuta in risposta la targa del veicolo (sotto forma di caratteri alfanumerici), il nostro server controlla sul proprio database o file locale se il veicolo è stato riconosciuto o no.
Una procedura abbastanza semplice che, così costruita, crea meno impatto a livello computazionale per la ESP32-CAM, lasciando tutta la post-elaborazione al server ed a Google.

Questo dettaglio è una grande svolta; infatti sfruttando la rete si va a creare un sistema distribuito, che permette di dividere adeguatamente il carico per l’elaborazione e l’attuazione riuscendo a sfruttare nelle terminazioni oggetti computazionalmente meno performanti.
Ma quali sono i passi da compiere per creare questo sistema distribuito?
Ebbene, prima di tutto è necessario abilitare le API di Google per l’elaborazione della nostra immagine.
Successivamente è necessario creare una pagina web sul nostro server di appoggio da poter chiamare con la nostra camera.
Ed infine dovremo caricare il firmware, che vedremo nel dettaglio, sulla nostra ESP32-CAM, così da scattare le foto quando il sensore PIR rileva del movimento.

Le API di Google per elaborare le immagini

Innanzitutto per poter usufruire dei servizi di “big G” è necessario possedere un account Google. Andando sul browser, una volta loggati, inseriamo nella URL questo link allorché ci troveremo davanti una pagina web come quella mostrata in Fig. 1 che è quella della Google API developer.

Fig. 1

Clicchiamo in alto sulla sinistra sulla dicitura “Seleziona un Progetto” e nel popup che comparirà selezioniamo la voce “Nuovo Progetto”.
Seguiamo i passaggi indicati, inserendo un nome e cliccando su “Crea”, per tornare alla schermata iniziale.
Ora, da qui selezioniamo la voce “Libreria” nel menu che troviamo sulla sinistra: verranno mostrati una serie di servizi sottoforma di rettangoli o tab. A noi interessa “Cloud Vision API”; selezionandolo apriremo la scheda di questa API (Fig. 2) nella quale andremo a cliccare sul pulsante “Abilita”.

Fig. 2

Una volta fatto ciò, abbiamo abilitato l’API corretta per il nostro servizio di riconoscimento oggetti, ora mancano solo alcuni passaggi per poter generare la chiave con la quale potremmo accedere al servizio mediante chiamate REST (REpresentational State Transfer).
Clicchiamo il logo di Google in alto a sinistra per tornare alla schermata iniziale e da li questa volta andremo a premere la voce “Credenziali” sempre all’interno del menu laterale.
Scegliamo la voce “Crea credenziali” e successivamente “Chiave API” ottenendo come risultato qualcosa di simile a quanto mostrato nella Fig. 3.

Fig. 3

Ovviamente per questa chiave possono essere impostati dei limiti di chiamata, ma evitiamo questa gestione e ci limitiamo ad aver ottenuto la nostra chiave.
Infine per poter abilitare totalmente il servizio bisogna inserire un metodo di fatturazione nel caso in cui si sfori il numero massimo di transazioni del piano gratis.
Ci sono da fare alcune precisazioni su questo servizio: infatti Google fissa una quota di 1000 interazioni al mese per questa API, superate le quali si deve sostenere un costo che decresce in base all’uso. A scopo didattico/personale è un limite molto alto, ma se si pensa di adottare questo sistema in un parcheggio o un posto pubblico sarà necessario affrontare una spesa.
Di seguito rechiamoci su “Collega un account di fatturazione” e dopo una prima conferma, seguiamo le istruzioni e completiamo i passaggi.

Per testare la chiave possiamo utilizzare un qualsiasi generatore di chiamate REST.
Un software abbastanza conosciuto è sicuramente Postman, uno strumento utilissimo per testare le API in modo molto semplice e intuitivo senza la necessità di creare a mano le classiche classi di test. Con Postman potremmo effettuare la chiamata e verificare la correttezza delle operazioni svolte senza scrivere alcuna riga di codice.

È sufficiente settare una chiamata come nella Fig. 4, inserendo i seguenti parametri:

Fig. 4

A => POST
B => https://vision.googleapis.com/v1/images:annotate?key=LA CHIAVE GENERATA AL PASSO PRECEDENTE
C => raw
D => JSON
E => il JSON che trovate nel Listato 1, sostituendo al tag IMAGE_BASE64 l’immagine che volete far scansionare a Google nel formato Base64:

{
    “requests”: [
{
    “features”: [
{
    “maxResults”: 50,
    “type”: “OBJECT_LOCALIZATION”
},
{
    “maxResults”: 50,
    “type”: “LOGO_DETECTION”
},
{
    “maxResults”: 50,
    “type”: “LABEL_DETECTION”
},
{
    “maxResults”: 50,
    “type”: “DOCUMENT_TEXT_DETECTION”
}
],
    “image”: {
    “content”: “ IMAGE_BASE64”
},
      “imageContext”: {
      “cropHintsParams”: {
      “aspectRatios”: [
      0.8,
      1,
      1.2
    ]
   }
  }
}

In risposta, se tutto è stato impostato correttamente, riceveremo un JSON contenente un array di oggetti response contenenti vari dettagli, dal modello della macchina alla targa.
Questa API e questi settaggi possono essere usati anche per determinare quali oggetti sono presenti in una scena o per altri scopi sempre legati alla computer vision.

Server Altervista

Questa entità avrà il compito di legare i due attori cardini dell’intero sistema, ovvero Google e il nostro modulo camera. Potremmo definirla come un trasportatore di informazioni, ma in realtà la sua funzione non si ferma qui: infatti oltre a questo, elabora la risposta in ritorno da Google, interpretandola, e controllando se la targa è presente tra quelle autorizzate.
Questa sezione dovrà esporre un pagina web verso l’esterno e per far questo esistono due soluzioni.

Una prima soluzione, mirata ad un utente molto più esperto, può essere quella di crearsi da solo lo spazio di hosting. In questo caso si potrà usare una semplice Raspberry Pi sulla quale possiamo per semplicità montare una distribuzione server e nella quale installeremo un’architettura LAMP.
LAMP è l’acronimo delle componenti software che la compongono, ovvero Linux, Apache, MySql ed infine PHP. Oltre alla precedente configurazione l’utente dovrà munirsi anche di un indirizzo statico al quale associare l’indirizzo locale del suo nuovo RaspServer. Questa associazione solitamente è fornita da un servizio ddns (Dynamic-DNS).

Una seconda via è sicuramente quella di acquistare un servizio già pronto all’uso; a tale riguardo evidenziamo come la Mondadori Media metta a disposizione una piattaforma, a scopo didattico, nella quale è possible confezionare siti o avere servizi di hosting semplicemente registrandosi gratuitamente.
Questa alternativa è molto valida sia per i neofiti, sia per chi, nonostante sia in possesso di ampie competenze, vuole partire subito e evitare ogni problema o “seccatura” di configurazione.

Accedendo dunque alla pagina it.altervista.org e cliccando sul tasto “Crea sito” e seguendo le istruzioni è possibile iscriversi al servizio. In particolare, nella seconda schermata consiglio di cliccare su tasto “Vuoi solo il servizio di hosting?”, per evitare che venga creata la piattaforma per il sito vero e proprio ma venga lasciato tutto lo spazio libero per poter sviluppare.
Una volta scelto il nome e finita la registrazione dovremo solo aspettare la e-mail di conferma, all’interno della quale troveremo Username e Password per un primo accesso.
Terminata questa prima parte di configurazione accediamo al nostro servizio attraverso il login e una volta all’interno clicchiamo su “Gestione file” (Fig. 5).

Fig. 5

Ora possiamo visualizzare, creare ed eliminare i vari file. La parte server espone una pagina web (getPhoto.php), costruita per semplicità in php, ed un file di testo. Quest’ultima è una semplificazione dell’apparato per permettere un facile starter anche a chi magari è meno avvezzo a lavorare con entità come database o server.
Creiamo il file di testo chiamato data.txt nella stessa cartella della pagina, e al suo interno inseriamo le varie targhe:

ED485KC
TR123TP

Vediamo nel dettaglio come si compone questa pagina:

<?php
$uploaddir = “./”;
$uploadfile = $uploaddir . basename( $_FILES[‘photo’][‘name’].” “);
if(move_uploaded_file($_FILES[‘photo’][‘tmp_name’], $uploadfile)){
$data = file_get_contents(“./”.$_FILES[‘photo’][‘name’].” “);

Nella prima parte andremo a ricevere il file proveniente dalla videocamera e tramite un gioco di set e get, salvare e riprendere il file.
Questo ci assicura di avere effettivamente ricevuto correttamente il file immagine.
C’è poi la parte di codice proposta nel Listato 2:

$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => ‘https://vision.googleapis.com/v1/images:annotate?key=GOOGLE_API_KEY’,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => ‘’,
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => ‘POST’,
CURLOPT_POSTFIELDS =>’{“requests”: [
{
   “features”: [
{
   “maxResults”: 50,
   “type”: “OBJECT_LOCALIZATION”
},
{
   “maxResults”: 50,
   “type”: “LOGO_DETECTION”
},
{
   “maxResults”: 50,
   “type”: “LABEL_DETECTION”
},
{
   “maxResults”: 50,
   “type”: “DOCUMENT_TEXT_DETECTION”
}
],
   “image”: {
   “content”: “’.base64_encode($data).’”
},
   “imageContext”: {
   “cropHintsParams”: {
   “aspectRatios”: [
   0.8,
   1,
   1.2
   ]
   }
  }
}
]
}’,
   CURLOPT_HTTPHEADER => array(‘Content-Type: application/json’),
));
   $response = curl_exec($curl);
   curl_close($curl);

Qui andremo ad eseguire la CURL, ovvero l’inoltro del messaggio verso i server di Google.
Come si può notare nel Listato 3, il file immagine è stato convertito in base64 all’interno della richiesta:

$resObj = json_decode($response, true);
if(isset($resObj[“responses”]) && count($resObj[“responses”])>0 && (isset($resObj[“responses”][0]
[“textAnnotations”]) && count($resObj[“responses”][0][“textAnnotations”])>0 && isset($resObj[“responses”]
[0][“textAnnotations”][0][“description”]))
&& (isset($resObj[“responses”][0][“labelAnnotations”]) &&
searchCar($resObj[“responses”][0][“labelAnnotations”])) ){
$tg = searchTG($resObj);
$res = explode(PHP_EOL, $tg);
if($res[0]==””){
$res = $res[1];
}else{
$res = $res[0];
}
$stringLicSea = str_replace(‘ ‘, ‘’, trim(preg_replace(‘/\s\s+/’, ‘ ‘, $res)));

Questo è un sistema di codifica che consente la conversione di dati binari in stringhe di testo ASCII.
Inoltre la chiave non è immagazzinata nel client, ma nel server e questo implica una maggior sicurezza e protezione da eventuali attacchi, per esempio scongiurando completamente la possibilità di completare un attacco man in the middle.
Una volta ricevuta la risposta da Google, andiamo quindi a convertire il testo in un oggetto, perché più facile da maneggiare. Di questo controlliamo che sia realmente una targa ed estrapoliamo il dato pulito, senza spazi o altri caratteri particolari.
Riportiamo qui di seguito le due funzioni di ricerca veicolo e ricerca targa.

$datab = file_get_contents(“./data.txt”);
$datab = explode(PHP_EOL, $datab);
foreach($datab as $item){
if($stringLicSea==$item){
echo “open”;
exit;
}
}
echo “close”;
}else{ echo “close”; }
}else{
echo “close”;
}

Ricerchiamo all’interno del file, creato inizialmente, un record simile a quello della targa fotografata.
In caso positivo rispondiamo open, altrimenti close.
Chiaramente tutte le label posso essere cambiate ma in accordo con il codice del client.

In alternativa a queste ultime righe, se siamo utenti più esperti e abbiamo già avuto a che fare con strutture dati più complesse, possiamo optare per una ricerca tramite query a database.
La piattaforma di Altervista ci può essere d’aiuto anche in questa operazione, offrendo un servizio, anch’esso completamente gratis, di database remoto con un’interfaccia molto semplice con cui operare una chiamata PhpMyAdmin. Nel dettaglio, in questo articolo vi descriveremo l’implementazione di una connessione ad un database mySQL.
Come primo passo riportiamo, nel Listato 4, lo script da utilizzare per la creazione della tabella del database e per l’inserimento in essa di alcuni dati di prova:

CREATE TABLE IF NOT EXISTS `license_plate` (
`Id` int(11) NOT NULL AUTO_INCREMENT,
`LicDesc` varchar(128) NOT NULL,
`Active` int(11) NOT NULL DEFAULT ‘1’,
`Owner` varchar(256) NOT NULL,
PRIMARY KEY (`Id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=3 ;
--
-- Dump dei dati per la tabella `license_plate`
--
INSERT INTO `license_plate` (`Id`, `LicDesc`, `Active`, `Owner`) VALUES
(1, ‘ED485KC’, 1, ‘Daniel’)
Mentre a livello di codice, alla decina di righe presentate precedentemente, sostituiamo le seguenti:
$servername = “SERVER_NAME”;
$username = “DB_USER_NAME”;
$password = “DB_USER_PASSWORD”;
$dbname = “DATABASE_NAME”;
$link = mysqli_connect($servername, $username, $password, $dbname);
if($link === false){
echo “close”;
exit;
}
$sql = “select * from license_plate where LicDesc=’”.$stringLicSea.”’ and Active=’1’”;
if($result = mysqli_query($link, $sql)){
if(mysqli_num_rows($result) > 0){
echo “open”;
/*while($row = mysqli_fetch_array($result)){
$ret->tankDefense=$row[‘tank’];
}*/
mysqli_free_result($result);
exit;
}else{ echo “close”; }
}else{ echo “close”; }
}else{ echo “close”; }
}else{
echo “close”;
}
Riporto infine le funzioni per la ricerca della targa:
function searchTG($resObj){
$qlt_h = 768;
$qlt_w = 1024;
$res = null;
$rect = new Tmp();
foreach($resObj[“responses”][0][“localizedObjectAnnotations”] as $value){
if($value[“name”]==”License plate”){
$res = $value;
break;
}
}
if($res!=null){
foreach($resObj[“responses”][0][“textAnnotations”] as $valuep){
if(($value[‘boundingPoly’][‘normalizedVertices’][0][‘x’]*$qlt_w)<=$valuep[‘boundingPoly’][‘vertices’][
0][‘x’] &&
($value[‘boundingPoly’][‘normalizedVertices’][0][‘y’]*$qlt_h)<=$valuep[‘boundingPoly’]
[‘vertices’][0][‘y’] &&
($value[‘boundingPoly’][‘normalizedVertices’][2][‘x’]*$qlt_w)>=$valuep[‘boundingPoly’]
[‘vertices’][2][‘x’] &&
($value[‘boundingPoly’][‘normalizedVertices’][2][‘y’]*$qlt_h)>=$valuep[‘boundingPoly’]
[‘vertices’][2][‘y’]){
$res = $valuep[“description”];
break;
}
}
return $res;
}
return “”;
}
function searchCar($arrayLabel){
$res = 0;
foreach($arrayLabel as $value){
if($value[“description”]==”Vehicle registration plate”){
$res++;
if($res==1){
break;
}
}
}
if($res==1){
return true;
}
return false;
}
class Tmp{}
?> ?>

Nella prima funzione, quella per la ricerca della targa, ci sono due valori: $alt_h e $alt_w. Questi rappresentano la risoluzione della foto, quindi se viene cambiata la parte client che invia la foto, editando la qualità, va modificata anche in queste variabili.

Hardware per riconoscimento targhe

Come già utilizzato in un precedente articolo la configurazione hardware del client si compone di un modulo ESP32-CAM (Fig. 6).

Fig. 6

Questo hardware è composto da un modulo ESP32, da una telecamera, un alloggio per SD-Card e tutto il necessario per far funzionare questi componenti.
Nel dettaglio possiamo notare che il circuito ESP32CAM si basa sull’ESP32, un microcontrollore programmabile con WiFi e Bluetooth integrati ed un’ulteriore RAM esterna di ben 4MB.
Non solo, il nuovo connettore per la fotocamera può ospitare un modulo OV2640 o OV7670: il primo viene fornito con il modulo stesso e presenta una risoluzione di 2 Megapixel.
Sulla scheda è presente anche un LED, ad alta luminosità, che può essere impiegato come flash o illuminatore della scena da riprendere.

Naturalmente i numerosi GPIO montati sulla scheda ci permettono eventualmente di montarne uno esterno nel caso di scarsa illuminazione.
Per piccole applicazioni questo chip si presenta molto robusto e con una grande potenza di elaborazione, basandosi su due core di elaborazione a 32 bit da 120 MHz.
Quest’ultima caratteristica gli permette di avere un frame-rate abbastanza alto, ovviamente in funzione del formato e delle dimensioni: indicativamente si riesce ad arrivare ad un throughput fino a 8 JPEG in SVGA (800×600) al secondo.

Con questo prototipo scatteremo foto con una risoluzione di 1024×768, ma in caso di problemi di rete o di latenze nella comunicazione la risoluzione può essere tranquillamente scalata.
Inoltre, secondo le prove fatte, sarebbe opportuno installare l’antenna sul ESP32 in modo tale da poter sfruttare una maggiore potenza di rete.
Ovviamente alla telecamera sarà affiancato un sensore di movimento PIR (Fig. 7), per poter rilevare la presenza di un eventuale veicolo.

Fig. 7

Il comportamento del PIR è molto semplice: nella nostra configurazione parte con un segnale basso (≃0) e una volta che rileva l’ostacolo alza il suo segnale (≃1). Grazie a questo sensore riusciamo a minimizzare il numero di foto scattate, come accennato qualche paragrafo indietro e quindi riusciamo ad ottimizzare la concretezza di questo strumento.
Vediamo ora come poter collegare i vari attori che compongono il nostro sistema client in modo efficiente. Come prima cosa colleghiamo la ESP32-CAM ad un Arduino UNO che useremo come programmatore. In questo passaggio può essere utilizzato un convertitore USB/TTL al posto dell’Arduino UNO, rendendo il cablaggio, almeno in fase di programmazione estremamente più facile.
Come primo passaggio identifichiamo i pin di TX e RX sulla nostra ESP32-CAM, chiamati rispettivamente U0TXD e U0RXD e li andremo a collegare con i pin TX e RX dell’Arduino UNO (o ai pin TX e RX del convertitore USB/TTL).

Nel modulo della camera questi pin rappresentano anche i pin di GPIO1 e GPIO3. Collegheremo i pin GND e GPIO0 di quest’ultimo a massa, insieme ai pin GND e RESET di Arduino e i due a 5V fra di loro. Naturalmente per tutti i contatti da porre a massa o al 5V possiamo aiutarci con una breadboard, come pure per le altre connessioni. Riassumendo in forma tabellare quanto espresso precedentemente, si hanno le seguenti corrispondenze tra linee di Arduino e pin della ESP32:

TX Arduino -> UOTXD ESP32
RX Arduino -> UORXD ESP32
GND Arduino -> GND ESP32
RESET Arduino -> GPIO0 ESP32
RESET Arduino -> GND Arduino ed ESP32

che poi significa quanto proposto nella Fig. 8.

Fig. 8

Oltre a queste connessioni, bisogna relizzare quelle dell’alimentazione +5V, ovvero unire tutti i fili positivi del sensore PIR, della ESP32-CAM e della scheda Arduino UNO. I due LED terminano a massa (GND).
Aggiungiamo ora il collegamento con il PIR: oltre all’alimentazione, quindi 3V3 e GND ai rispettivi VCC e GND del sensore, porteremo il cavo per il segnale di attuazione, partendo dal pin GPIO13.
Affianchiamo a quest’ultimo anche un LED, sul pin GPIO15, in modo tale da poter avere un feedback visivo nelle varie fasi di elaborazione.

Esp32->PIR
Esp32->LED
3V3->VCC
GND->GND
GPIO13->OUT

Lo schema di cablaggio del sistema per l’utilizzo a regime è quello proposto nella Fig. 9; in pratica corrisponde al circuito finito e programmato.

Fig. 9

Una volta che abbiamo programmato il nostro modulo ESP32-CAM, dobbiamo assolutamente rimuovere il collegamento GPIO0->GND.
Firmware ESP32-CAM
Prima di procedere alla programmazione dovremmo impostare ArduinoIDE, ovvero il nostro ambiente di sviluppo, per poter scrivere correttamente sul modulo.
Apriamo quindi ArduinoIDE e andiamo nel menu File->Impostazioni e aggiungiamo negli URL aggiuntivi per i gestori delle schede questo link https://dl.espressif.com/dl/package_esp32_index.json e facciamo clic su OK (Fig. 10).

Fig. 10

Se si dispone già di uno o più URL si usa la virgola per concatenarli, proprio come nell’esempio.
Ora scarichiamo correttamente il gestore della scheda: rechiamoci in Strumenti->Scheda: XXXX->Gestore schede… , cerchiamo “esp32” e installiamo l’unico risultato della ricerca. (Fig. 7)
Uscendo selezioniamo nuovamente Strumenti->Scheda: XXXX e noteremo una nuova voce ESP32 Arduino, la quale al suo interno ha diverse altre scelte. Selezioniamo ESP32 Wrover Module, comparirà una nuova riga sempre in quel menu: Partition scheme, all’interno del quale andremo a scegliere Huge APP (Fig. 11).

Fig. 11

Ora abbiamo tutto il necessario per poter caricare il firmware sul dispositivo, colleghiamo quindi l’USB del nostro Arduino UNO e selezioniamo la porta virtuale creata da sistema operativo e clicchiamo Carica.
Vediamo ora come si compone il codice da uploadare sul nostro device, il quale è suddiviso per le macro aree GLOBALI, FUNZIONE DI SETUP e FUNZIONE DI LOOP.
Nella prima parte andremo a dichiarare tutte quelle variabili globali che useremo all’interno del programma:

const char* ssid = “WIFI_SSID”;
const char* password = “WIFI_PASSWORD”;

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include “soc/soc.h”
#include “soc/rtc_cntl_reg.h”
#include “esp_camera.h”

//CAMERA_MODEL_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

int gpioPIR = 13;
int gpioLED = 15;
int gpioATTUATORE = 14;

Nella seconda parte andremo a inizializzare il modulo camera e ci andremo a connettere all’hotspot WiFi per il quale abbiamo configurato le credenziali nel passo precedente (Listato 5):

void setup() {
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
  Serial.begin(115200);
  delay(10);
  pinMode(gpioLED, OUTPUT);
  pinMode(gpioATTUATORE, OUTPUT);
  WiFi.mode(WIFI_STA);
  Serial.println(“”);
  Serial.print(“Connecting to“);
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  long int StartTime = millis();
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    if ((StartTime + 10000) < millis()) break;
  }
  Serial.println(“”);
  Serial.println(“STAIP address: “);
  Serial.println(WiFi.localIP());
  Serial.println(“”);
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println(“Reset”);
    ledcAttachPin(4, 3);
    ledcSetup(3, 5000, 8);
    ledcWrite(3, 10);
    delay(200);
    ledcWrite(3, 0);
    delay(200);
    ledcDetachPin(3);
    delay(1000);
    ESP.restart();
  } else {
    ledcAttachPin(4, 3);
    ledcSetup(3, 5000, 8);
    for (int i = 0; i < 5; i++) { ledcWrite(3, 10); delay(200); ledcWrite(3, 0); delay(200); } ledcDetachPin(3); } 
    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; if (psramFound()) { 
    config.frame_size = FRAMESIZE_VGA; config.jpeg_quality = 10; config.fb_count = 2; } else { config.frame_size = FRAMESIZE_QQVGA; 
    config.jpeg_quality = 12; 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(); } 
    sensor_t * s = esp_camera_sensor_get(); s -> set_framesize(s, FRAMESIZE_XGA);
}

Come si può notare, nel caso in cui non riesca a connettersi al WiFi o non riesca ad inizializzare la camera il dispositivo si riavvierà: per questo è importante accertarsi di aver cablato tutto in maniera corretta e inserito le credenziali giuste, in modo da evitare il verificarsi di un bootloop.
Inoltre come accennato precedentemente, la risoluzione dell’immagine può essere settata dal codice alla riga:

config.jpeg_quality = 10;

Qui si può spaziare da 0 a 63, rammentando che i numeri bassi indicano maggiore risoluzione (e di conseguenza migliore qualità delle immagini) e viceversa.
Infine vi è la porzione responsabile del funzionamento del dispositivo a regime, il cui codice è quello riportato nel Listato 6:

String callServerCheck() {
  const char * myDomain = “SERVER_SITE“;
  String getAll = ””, getBody = “”;
  camera_fb_t * fb = NULL;
  fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println(“Camera capture failed”);
    delay(1000);
    ESP.restart();
    return“ Camera capture failed”;
  }
  WiFiClientSecure client_tcp;
  if (client_tcp.connect(myDomain, 443)) {
    Serial.println(“Connected to“ + String(myDomain));
    String head = “--India\ r\ nContent - Disposition: form - data;
    name = \”chat_id\”;\
    r\ n\ r\ n chat - id - 123456789\
    r\ n--India\ r\ nContent - Disposition: form - data;
    name = \”photo\”;
    filename = \”esp32 - cam.jpg\”\ r\ nContent - Type:
      image / jpeg\ r\ n\ r\ n”;
    String tail = “\r\ n--India--\r\ n”;
    uint16_t imageLen = fb -> len;
    uint16_t extraLen = head.length() + tail.length();
    uint16_t totalLen = imageLen + extraLen;
    client_tcp.println(“POST / getPhoto.php HTTP / 1.1”);
    client_tcp.println(“Host: “+String(myDomain));
    client_tcp.println(“Content - Length: “+String(totalLen));
    client_tcp.println(“Content - Type: multipart / form - data; boundary = India”);
    client_tcp.println();
    client_tcp.print(head);
    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_tcp.write(fbBuf, 1024); fbBuf += 1024; } else if (fbLen % 1024 > 0) {
        size_t remainder = fbLen % 1024;
        client_tcp.write(fbBuf, remainder);
      }
    }
    client_tcp.print(tail);
    esp_camera_fb_return(fb);
    int waitTime = 10000; // timeout 10 seconds
    long startTime = millis();
    boolean state = false;
    while ((startTime + waitTime) > millis()) {
      Serial.print(“.”);
      delay(100);
      while (client_tcp.available()) {
        char c = client_tcp.read();
        if (c == ‘\n’) {
          if (getAll.length() == 0) state = true;
          getAll = “”;
        } else if (c != ‘\r’)
          getAll += String(c);
        if (state == true) getBody += String(c);
        startTime = millis();
      }
      if (getBody.length() > 0) break;
    }
    client_tcp.stop();
    //Serial.println(getBody);
  } else {
    getBody = “Connection to server failed.”;
    Serial.println(“Connection to server failed.”);
  }
  return getBody;
}
void loop() {
  pinMode(gpioPIR, INPUT_PULLUP);
  int v = digitalRead(gpioPIR);
  if (v == 1) {
    digitalWrite(gpioLED, HIGH);
    delay(1000);
    digitalWrite(gpioLED, LOW);
    delay(1000);
    String vari = callServerCheck();
    if (vari.indexOf(“open”) > 0) {
      digitalWrite(gpioATTUATORE, HIGH);
      delay(3000);
      digitalWrite(gpioATTUATORE, LOW);
      digitalWrite(gpioLED, HIGH);
      delay(3000);
      digitalWrite(gpioLED, LOW);
    }
    delay(5000);
  }
}

In questa porzione troviamo due funzioni: la prima, callServerCheck, è responsabile dell’acquisizione dell’immagine e dell’invio di questa verso il server; la seconda parte non fa altro che eseguire ciclicamente un’azione di controllo del sensore radard a infrarossi passivi. In caso di ostacolo oppure di oggetto in movimento che viene rilevato, passa subito il controllo alla prima.

Possibili evoluzioni

Nell’esempio in questione andiamo a pilotare un pin esterno (nel dettaglio GPIO14), alzando il suo stato logico per una decina di secondi, ma nulla ci vieta di aggiungere maggiore complessità, come per esempio andare a pilotare l’apertura di un cancello motorizzato, della serranda o basculante di un garage o attivare un relé intelligente che supporti il protocollo MQTT.

Cerchiamo di comprendere meglio cosa significa e quali possibilità offre questo protocollo: alcuni dispositivi sono comparabili a dei “mini server” che espongono un’interfaccia per poterli usare da remoto senza dover essere fisicamente presenti.
Questi solitamente comunicano attraverso un protocollo MQTT, che si basa su almeno due attori: un broker, responsabile dello smistamento dei vari messaggi ed uno o più publisher o client, i quali si litimitano ad inviare o ricevere messaggi.
In breve accade che il publisher si collega al broker e può cominciare da subito ad inviare messaggi con un determinato TOPIC verso quest’ultimo, il quale rilancia il messaggio verso gli altri client collegati a quel determinato argomento.
MQTT è quindi un protocollo altamente ottimizzato ed usato da molti dispositivi per IoT.
Per quanto riguarda il codice, la modifica è abbastanza semplice perché è sufficiente aggiungere le seguenti righe nelle sezioni già descrite del codice in esecuzione sul dispositivo.
Nelle variabili GLOBALI:

#include <PubSubClient.h>
WiFiClient espClient;
PubSubClient mqttClient(espClient);

Nella funzione di SETUP:

char* server = “SERVER_SITE_MQTT”;
mqttClient.setServer(server, MQTT_PORT);
boolean rc = mqttClient.connect(“myClientID”);

o in caso di connessione protetta:

boolean rc = mqttClient.connect(“myClientID”, “myUseername”, “myPassword “);

Nella funzione di LOOP:

if (vari.indexOf(“open”) > 0){
mqttClient.publish(“TOPIC”, “COMMAND”);

Questa è comunque una delle tante modifiche possibili al codice; nulla vieta di realizzarne altre finalizzate a specifiche applicazioni.

Download

Il codice completo per il riconoscimento targhe con ESP32-CAM e le API di Google è scaricabile dopo aver effettuato il login

Conclusioni

Bene, con questo abbiamo terminato la descrizione del nostro sistema di riconoscimento di targhe che sfrutta l’elaborazione in cloud di Google per fare il “grosso” del lavoro e ottenere le lettere e i numeri identificati nel fotogramma passato dalla ESP32-CAM tramite Internet.

Ricordiamo che il progetto è un’applicazione demo attraverso la quale vi abbiamo spiegato come, sfruttando servizi in Rete e su Cloud di grandi provider, è possibile svolgere operazioni complesse e che se svolte localmente avrebbero richiesto grandi risorse di calcolo, utilizzando hardware semplici capaci, però, di interagire con Internet per inviare richieste ed elaborare le risposte.

La nostra applicazione può essere personalizzata e adattata in base alle proprie esigenze, magari cambiando il sensore e/o l’attuatore per utilizzare come evento trigger una condizione differente dall’avvicinamento di un’automobile e per attivare dispositivi di vario genere, come anche passare il segnale di riconoscimento a una successiva elaborazione.

 

1 Commento

  1. Buongiorno, ho seguito il progetto fin dalla sua uscita ma testando la parte sw ( bypassando la ESP come primo step) legge solo i primi due numeri della targa.. e li dopo innumerevoli tentativi mi sono fermato . Saluti

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.

Menu