![](https://habrastorage.org/web/f84/4f9/fc6/f844f9fc61a04c09994017436efeb43f.jpg)
На сегодняшний день оптическое распознавание символов является частью решения таких прикладных задач, как распознавание и оцифровка текстов, распознавание документов, распознавание автомобильных номеров, определение номеров банковских карточек, чтение показаний счетчиков учета, определения номеров домов для создания карт (Google Street View) и т.д.
Распознавание символа означает анализ его изображения с целью получения некоторого набора признаков для сравнения их с признаками класса [ 1 ]. Выбор такого набора и способы его определения отличают разные методы распознавания, но для большинства из них необходима одномоментная информация обо всех пикселях изображения.
Последнее обстоятельство и достаточно большой объем вычислений делают невозможным использования маломощных вычислительных устройств (микроконтроллеров) для оптического распознавания символов. «Да и зачем?» — воскликнет информированный читатель, «мощности вычислительных устройств постоянно растут, а их цена падает!»[2, 3]. Допустим, что ответ будет такой: просто интересно, возможно ли упростить метод распознавания до такой степени, чтобы можно было бы использовать микроконтроллер?
Оказалось можно, более того, оказалось возможным то, что кажется относится к области фантастики, а именно:
- распознавание независимо от шрифта;
- распознавание строки символов без разделения на отдельные символы;
- распознавание «экранированных» символов, например, символ в символе;
- распознавание «разорванных» символов;
- распознавание символов, состоящих из нескольких частей;
- распознавание без изменения признаков при повороте до 15°. Возможность
распознавания повернутых символов на больший угол за счет изменения его признаков; - распознавание символов в видеопотоке с одного кадра;
- распознавание рукописных символов;
- ограниченное количество признаков для описания класса символов, для арабских цифр
и латиницы — один, для кириллицы — максимум два (например, для некоторых
вариантов написания Ж); - простое «ручное» определение признаков для нового класса;
- автоматическое определение признаков для нового класса;
- расширение классов символов простым добавлением в базу его признаков;
И все это на микроконтроллере.
Основная идея метода
Теперь поподробнее о самом методе. Рассмотрим, например, различные начертания символа А:
![](https://habrastorage.org/web/39c/640/93a/39c64093a92745459223e06f8b97e3f7.jpg)
Несмотря на видимые различия, можно выделить общие признаки структурного типа, которые являются необходимыми признаками прописной буквы А (для не разорванных символов), а именно: если рассматриваемый символ является прописной буквой А, то он будет содержать замкнутую область и область разомкнутую вниз.
![](https://habrastorage.org/web/d13/9e6/55e/d139e655ee2e47feb776c4c1925bce24.jpg)
Еще раз подчеркнем, что указанные признаки являются необходимыми, но не достаточными: если мы будем описывать контуры вокруг двух областей указанного типа,
![](https://habrastorage.org/web/b45/6cc/7a9/b456cc7a97d94ceab7d11165caa564ff.jpg)
то это не обязательно будут буквы прописные А, возможны, например Д, Я,R, строчная рукописная А,..:
![](https://habrastorage.org/web/16a/ee1/261/16aee1261df84c75b8e8580ac0ef39a7.jpg)
Однако использование областей в качестве элементов символа позволяет сформировать достаточные признаки, причем, для подавляющего числа алфавитно-цифровых символов можно сформировать единственный достаточный признак! Его очень просто сформировать для каждого класса и, в отличие от структурных признаков, используемых ABBYYTeam при распознавании рукописных символов [ 1 ], его вариативность очень низкая и его возможно формировать автоматически! Другими словами, такие признаки хорошо работают как для печатных, так и для рукописных символов.
Устройство для распознавания
Первая проверка метода была описана в статье [ 4 ]. Метод проверялся на одиночных цифрах, получаемых примитивной камерой от мыши с семисегментного индикатора или напечатанных на бумаге. После первого успеха возникло естественное желание проверить возможность распознавания последовательности символов, а для этого нужно использовать другую камеру. Мы использовали камеру OV7670 (0.3Мп). Остальные главные компоненты схемы остались без изменения — это Arduino и ESP8266, но изменились их функции. Arduino теперь используется для инициализации камеры, в качестве задающего генератора, приема распознанных символов и их отображения на индикаторах. ESP8266 занимается получением изображения с камеры и его распознаванием, кроме того он обеспечивает передачу данных на Ардуино для отображения и передачу распознанной информации через WiFi на внешние устройства, например, смартфон. Используемая электрическая схема устройства приведена на рисунке:
![](https://habrastorage.org/web/06e/787/e1c/06e787e1c7154d27aaba2dcfa81d588c.jpg)
Изображение попадает в устройство через щель в его нижней части и, отражаясь от зеркала, поступает на камеру. Подсветка изображения осуществляется светодиодом через то же зеркало. Механическая схема устройства приведена на рисунке.
![](https://habrastorage.org/web/a5d/547/01a/a5d54701a367414a98ef103f4450a8c6.jpg)
Первый вариант рабочего прототипа:
Второй вариант рабочего прототипа:
Получение изображения на на ESP8266
Настройки камеры при инициализации взяты из [5]. Частота кадров приблизительно 0,4 кадр/с.Так как количество пинов у ESP8266 недостаточно, то обрабатываются только 6 старших бит каждого яркостного байта изображения (камера настроена в режиме YUV). Для получения изображения используется конечный автомат (машина состояний).
![](https://habrastorage.org/web/63a/34c/5d4/63a34c5d41264892b4dfef1dc13961b4.jpg)
Согласно даташит камеры OV7670 [6]
![](https://habrastorage.org/web/0f4/cc0/685/0f4cc068539e455a98c6fd0dc49db42c.jpg)
![](https://habrastorage.org/web/4ff/a10/cfa/4ffa10cfafe64296b90c9e32b26886f2.jpg)
Можно выделить следующие состояния камеры, условия и сигналы во время ее работы:
Имя состояния,номер | Описание состояния | Сигнал к переходу в другое состояние |
camoff,0 | камера не готова к работе |
vzz (vsync=1,href=0,pclk=0) |
frapause, 1 | пауза между кадрами. ожидание начала кадра. |
zzz (vsync=0,href=0,pclk=0) |
framebeg, 2 | чтение кадра. ожидание начала строки в кадре. |
zhz (vsync=0,href=1,pclk=0) |
framebeg, 2 | чтение кадра. ожидание конца кадра после чтения последнего пикселя |
vzz (vsync=1,href=0,pclk=0) |
fbyteread, 3 | яркостный байт прочитан. ожидание начала паузы перед цветоразностным байтом. |
zhz (vsync=0,href=1,pclk=0) |
fpause, 4 | пауза перед цветоразностным байтом. ожидание начала чтения цветоразностного байта. |
zhp (vsync=0,href=1,pclk=1) |
sbyteread, 5 | цветоразностный байт прочитан. ожидание начала паузы перед яркостным байтом. |
zhz (vsync=0,href=1,pclk=0) |
spause, 6 | пауза перед яркостным байтом. ожидание окончания строки. |
zzz (vsync=0,href=0,pclk=0) |
spause, 6 | пауза перед яркостным байтом. ожидание начала чтения яркостного байта. |
zhp (vsync=0,href=1,pclk=1) |
Реализация машины основана на тех же принципах, которые изложены в [7]. Вся машина описывается ее геном — трехмерным вектором, первый компонент которого содержит ключи, а второй — имена новых состояний, третий — имена функций. Ключ содержит информацию от текущем состоянии и сигнале перехода. Для формирования ключа и сигнала используются битовые операции. Детали реализации понятны из кода модуля чтения камеры.
#include "ets_sys.h"
#include "osapi.h"
#include "os_type.h"
#include <gpio.h>
#include "driver/uart_register.h"
#include "user_config.h"
#include "user_interface.h"
#include "driver/uart.h"
#include "readCam.h"
#define DELAY 5000 /* milliseconds */
LOCAL os_timer_t cam_timer;
uint16_t frN;
extern uint8_t pixVal;
uint8_t rN[10];
LOCAL void ICACHE_FLASH_ATTR getNFrame(void *arg){
uint16_t sig, sV,sH,sP;
uint16_t pVal;
uint16_t d7,d6,d5,d4,d3,d2;
stateMashine camSM;
ets_uart_printf("getNFrame...\r\n");
initSMcm(&camSM);
while(frN<20){
system_soft_wdt_feed();
pVal= *GPIO_IN;
sV=((pVal&(1UL<<VSYNC))>>VSYNC);
sH=((pVal&(1UL<<HREF))>>HREF);
sP=((pVal&(1UL<<PCLK))>>PCLK);
sig=4*sV+2*sH+sP*sH;
d7=((pVal&(1UL<<D7))>>D7);
d6=((pVal&(1UL<<D6))>>D6);
d5=((pVal&(1UL<<D5))>>D5);
d4=((pVal&(1UL<<D4))>>D4);
d3=((pVal&(1UL<<D3))>>D3);
d2=((pVal&(1UL<<D2))>>D2);
pixVal=128*d7+64*d6+32*d5+16*d4+8*d3+4*d2;
exCAM(&camSM,sig,&frN,rN);
}
}
uint32 ICACHE_FLASH_ATTR user_rf_cal_sector_set(void)
{
enum flash_size_map size_map = system_get_flash_size_map();
uint32 rf_cal_sec = 0;
switch (size_map) {
case FLASH_SIZE_4M_MAP_256_256:
rf_cal_sec = 128 - 8;
break;
case FLASH_SIZE_8M_MAP_512_512:
rf_cal_sec = 256 - 5;
break;
case FLASH_SIZE_16M_MAP_512_512:
case FLASH_SIZE_16M_MAP_1024_1024:
rf_cal_sec = 512 - 5;
break;
case FLASH_SIZE_32M_MAP_512_512:
case FLASH_SIZE_32M_MAP_1024_1024:
rf_cal_sec = 1024 - 5;
break;
default:
rf_cal_sec = 0;
break;
}
return rf_cal_sec;
}
void ICACHE_FLASH_ATTR user_init(void){
void (*cbGetFrame)(void *arg);
cbGetFrame=(void*)getNFrame;
UARTInit(BIT_RATE_921600);
user_gpio_init();
os_timer_disarm(&cam_timer);
os_timer_setfn(&cam_timer, (os_timer_func_t *)cbGetFrame, NULL);
os_timer_arm(&cam_timer, DELAY, 0);
}
#ifndef INCLUDE_READCAM_H_
#define INCLUDE_READCAM_H_
#define GPIO_IN ((volatile uint32_t*) 0x60000318)
#define WP 320
#define HP 240
#define PIXTYP 0
//image __________________________________________
#define IMAGEY0 60
#define IMAGEH HP/3
//____________________pins_____________________
#define VSYNC 15
#define HREF 13
#define PCLK 3
#define D7 4
#define D6 12
#define D5 0
#define D4 14
#define D3 2
#define D2 5
//*************signals OV7670*****************
#define ZZZ 0
#define VZZ 4
#define ZHZ 2
#define ZHP 3
//*************states OV7670*******************
#define CAMOFF 0
#define FRAPAUSE 1
#define FRAMEBEG 2
#define FBYTEREAD 3
#define FPAUSE 4
#define SBYTEREAD 5
#define SPAUSE 6
#define SSCC 40//max state_signal_condition count
#define STATE_L 5
#define STATE_V 0x1F
#define SIG_L 8
#define SIG_V 0xFF
typedef struct {
uint8 pix[WP] ;
}linePixel;
typedef struct gen{
uint8_t state;
uint8_t sig;
uint8_t stateX;
void *fp;
}gen;
typedef struct stateMashine{
uint8_t count;
uint16_t ssc[SSCC];
uint8_t stateX[SSCC];
void *fPoint[SSCC];
void *fpd;
}stateMashine;
#endif /* INCLUDE_READCAM_H_ */
#include "ets_sys.h"
#include "osapi.h"
#include "os_type.h"
#include <gpio.h>
#include "driver/uart_register.h"
#include "user_config.h"
#include "user_interface.h"
#include "driver/uart.h"
#include "readCam.h"
void sendLine(uint16_t lN);
void ICACHE_FLASH_ATTR sendFramMark(void);
void ICACHE_FLASH_ATTR sendCtr3Byte(uint8_t typ,uint16_t len);
void user_gpio_init(void);
void sendByte(uint8_t bt);
void ICACHE_FLASH_ATTR initSMcm(stateMashine *psm);
void exCAM( stateMashine *psm,uint8_t sig,uint16_t *frameN,uint8_t *rN);
int indexOf(stateMashine *psm,uint16_t ssc);
linePixel lp;
uint8_t pixVal;
void exCAM( stateMashine *psm,uint8_t sig,uint16_t *frameN,uint8_t *rN){
int16_t ind;
uint16_t lN;
uint16_t pN;
static uint8_t state=CAMOFF,stateX=CAMOFF;
static void (*pfun)()=NULL;
uint16_t stateSigCond=0;
stateSigCond|=((state&STATE_V)<<(16-STATE_L))|((sig&SIG_V)<<(16-STATE_L-SIG_L));
ind=indexOf(psm,stateSigCond);
if(ind>-1) stateX=(*psm).stateX[ind];
if(ind>-1) pfun=(*psm).fPoint[ind];
else pfun=(*psm).fpd;
pfun(frameN,&lN,&pN,rN);
state=stateX;
}
void _cm0(){}
void _cm1(uint16_t *fN,uint16_t *lN,uint16_t *pN){//new frame
sendFramMark();
sendCtr3Byte(PIXTYP,0);
(*lN)=0;
}
void _cm2(uint16_t *fN,uint16_t *lN,uint16_t *pN){//frame end
if(*lN==HP-1)(*fN)++;
}
void _cm3(uint16_t *fN,uint16_t *lN,uint16_t *pN){//new line
uint16_t pixN;
(*pN)=0;
// pixN=(*pN);//right image
pixN=WP-1-(*pN);//revers image
(lp).pix[pixN]=pixVal;
(*pN)++;
}
void _cm4(uint16_t *fN,uint16_t *lN,uint16_t *pN){// first byte
uint16_t pixN;
// pixN=(*pN);//right image
pixN=WP-1-(*pN);//reverse image
(lp).pix[pixN]=pixVal;
// if(pixN<WP-1)(*pN)++;//right image
if(pixN)(*pN)++;//reverse image
}
void _cm5(uint16_t *fN,uint16_t *lN,uint16_t *pN,uint8_t *rN){//end line
uint16_t lineN;
lineN=(*lN);
sendLine(lineN);
if((*lN)<HP-1)(*lN)++;
}
void _cm99(){}
int indexOf(stateMashine *psm,uint16_t ssc){
uint8_t i,count;
count=(*psm).count;
for(i=0;i<count;i++){
if((*psm).ssc[i]==ssc) return i;
}
return -1;
}
void ICACHE_FLASH_ATTR initSMcm(stateMashine *psm){
uint8_t i,count;
count=10;
gen gen[]={
{CAMOFF,VZZ,FRAPAUSE,_cm0},//0#1
{FRAPAUSE,ZZZ,FRAMEBEG,_cm1},//1#2
{FRAMEBEG,VZZ,FRAPAUSE,_cm2},//2#1
{FRAMEBEG,ZHZ,FBYTEREAD,_cm3},//2#3
{FBYTEREAD,ZHP,FPAUSE,_cm0},//3#4
{FPAUSE,ZHZ,SBYTEREAD,_cm0},//4#5
{SBYTEREAD,ZHP,SPAUSE,_cm0},//5#6
{SPAUSE,ZHZ,FBYTEREAD,_cm4},//6#3
{SPAUSE,ZZZ,FRAMEBEG,_cm5},//6#2
{FPAUSE,ZZZ,FRAMEBEG,_cm5},//5#2
};
(*psm).count=count;
for(i=0;i<count;i++){
(*psm).ssc[i]=0;
(*psm).ssc[i]|=((gen[i].state&STATE_V)<<(16-STATE_L))|
((gen[i].sig&SIG_V)<<(16-STATE_L-SIG_L));
(*psm).stateX[i]=gen[i].stateX;
(*psm).fPoint[i]=gen[i].fp;
}
(*psm).fpd=_cm99;
}
void sendByte(uint8_t bt){
uint16_t lenBuff;
uint8_t buf[TX_BUFF_SIZE];
while(lenBuff){
lenBuff = (READ_PERI_REG(UART_STATUS(0))>>UART_TXFIFO_CNT_S)
& UART_TXFIFO_CNT;
}
buf[lenBuff] =bt;
uart0_tx_buffer(buf, lenBuff + 1);
}
void sendLine(uint16_t lN){
uint16_t j;
uint8_t sByt;
for(j=0;j<WP;j++){
sByt=(lp).pix[j];
if(lN<IMAGEY0||lN>(IMAGEY0+IMAGEH))sByt=0xFF;
sendByte(sByt);
}
}
void ICACHE_FLASH_ATTR user_gpio_init(void) {
ets_uart_printf("GPIO initialisation...\r\n");
PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO0_U, FUNC_GPIO0);
gpio_output_set(0, 0, 0, BIT0); // Set GPIO0 as input
PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO2_U, FUNC_GPIO2);
gpio_output_set(0, 0, 0, BIT2); // Set GPIO2 as input
PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0RXD_U, FUNC_GPIO3);
gpio_output_set(0, 0, 0, BIT3); // Set GPIO3 as input
PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO4_U, FUNC_GPIO4);
gpio_output_set(0, 0, 0, BIT4); // Set GPIO4 as input
PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO5_U, FUNC_GPIO5);
gpio_output_set(0, 0, 0, BIT5); // Set GPIO5 as input
PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTDI_U, FUNC_GPIO12);
gpio_output_set(0, 0, 0, BIT1); // Set GPIO13 as input
PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTMS_U, FUNC_GPIO14);
gpio_output_set(0, 0, 0, BIT14); // Set GPIO14 as input
PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTCK_U, FUNC_GPIO13); // Set GPIO13 function
gpio_output_set(0, 0, 0, BIT13); // Set GPIO13 as input
PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTDO_U, FUNC_GPIO15);
gpio_output_set(0, 0, 0, BIT15); // Set GPIO15 as input
ets_uart_printf("...init done!\r\n");
}
void ICACHE_FLASH_ATTR sendFramMark(void){
sendByte(42);
sendByte(42);
}
void ICACHE_FLASH_ATTR sendCtr3Byte(uint8_t typ,uint16_t len){
uint8_t lLen,hLen;
sendByte(typ);
lLen=len&0xFF;
hLen=(len&(0xFF<<8))>>8;
sendByte(lLen);
sendByte(hLen);
}
Обработка изображения
Обработка изображения состоит в построчной бинаризации, соединении полученных отрезков, анализе и синтезе полученных фигур. Целью обработки является формирование интегральных признаков, включающих свойства областей, входящих в фигуры. Несмотря на простоту основной идеи ее реализация содержит ряд специфических моментов, которые не могут быть раскрыты в рамках настоящей статьи.
Визуализация процесса распознавания
Для отладки процесса распознавания использовалась визуализация на ПК исходной картинки, бинаризованной и картинки, которую «видит» или «понимает» микроконтроллер. Несмотря на то, что последняя не сильно радует наш глаз, ее деталей достаточно, чтобы распознать символы. На рисунке приведены примеры визуализации.
![](https://habrastorage.org/web/458/709/d8f/458709d8f67c4bb2b2528985e8f7f3c5.jpg)
![](https://habrastorage.org/web/1ff/c29/fbb/1ffc29fbb48647468cdd1f164d56eec1.jpg)
![](https://habrastorage.org/web/d18/c54/973/d18c549736344afcb3b8e26adf686e8b.jpg)
![](https://habrastorage.org/web/ed0/136/824/ed01368241634db08822959917e4fcd0.jpg)
Нужно отметить, что иногда из-за ошибок синхронизации камеры возникают ситуации, когда в исходной картинке появляются линии с цветоразностными, а не яркостными байтами. В этом случае картинка смазывается и распознавание не происходит. Задача обработки таких ситуаций на настоящем этапе не ставилась
![](https://habrastorage.org/web/327/983/807/327983807370452fa02df894249a40b9.jpg)
Также распознавание не происходит при неправильном позиционировании текста:
![](https://habrastorage.org/web/348/d27/caa/348d27caa0f54fb1a7f372d3f01d5f38.jpg)
Для визуализации использовалась небольшая программа, написанная на Java Script с использованием nodeWebkit.
var btn = document.getElementById('com');
var gui = require("nw.gui");
var select_com = document.getElementById('select_com');
var bdr = document.getElementById('bdr');
var canvas = document.getElementById('canvas');
var dev = document.getElementById('dev');
var ctx = canvas.getContext('2d');
var width = 320,
height = 240;
var byteCount = (width * height)/3;
var lastStr=byteCount-width;
var dataArr;
var dataStr;
var indArr = 0;
var dataArrLen = 0;
var byteCounter = 0;
var newStr = 0;
var sendTyp=0;
document.addEventListener('DOMContentLoaded', function() {
btn.addEventListener('click', function() {
connectCom(function(vector) {
drawImg(vector);
});
});
dev.addEventListener('click', function(){
var win = gui.Window.get();
win.showDevTools();
});
});
function drawImg(imgArr) {
var imgData = ctx.createImageData(width, height);
var ind;
for (var i = 0; i < imgArr.length; i++) {
imgData.data[4 * i] = imgArr[i];
imgData.data[4 * i + 1] = imgArr[i];
imgData.data[4 * i + 2] = imgArr[i];
imgData.data[4 * i + 3] = 255;
if(i<byteCount&&i>lastStr){ //red line
imgData.data[4 * i] = 255;
imgData.data[4 * i + 1] = 0;
imgData.data[4 * i + 2] = 0;
imgData.data[4 * i + 3] = 255;
}
if(i<2*byteCount&&i>byteCount+lastStr){ //green line
imgData.data[4 * i] = 0;
imgData.data[4 * i + 1] = 255;
imgData.data[4 * i + 2] = 0;
imgData.data[4 * i + 3] = 255;
}
if(i<3*byteCount&&i>2*byteCount+lastStr){ //blue line
imgData.data[4 * i] = 0;
imgData.data[4 * i + 1] = 0;
imgData.data[4 * i + 2] = 255;
imgData.data[4 * i + 3] = 255;
}
}
ctx.putImageData(imgData, 0, 0);
imgArr.length=0;
}
function connectCom(callback) {
const PIXTYPE=0,BINTYPE=1,FIGTYPE=2;
var imgTyp=PIXTYPE;
var serialport = require('serialport');
var imgArr = [];
var framCount=0,strNum,colNum;
var pix=false;
var comm = 'COM' + select_com.value;
var boudrate = +bdr.value;
var SerialPort = serialport.SerialPort;
var port = new SerialPort(comm, {
baudRate: boudrate,
dataBits: 8,
stopBits: 1,
parity: "none",
bufferSize: 65536,
parser: SerialPort.parsers.byteLength(1)
});
port.on('open', function() {
console.log('Port ' + comm + ' Open');
});
port.on('data', function(data) {
if(imgTyp==PIXTYPE||imgTyp==BINTYPE){
if (data[0] == 42 && newStr == 0) {
newStr = 1;
data[0]=255;
}
if (newStr == 1 && data[0] == 42) {
newStr = 2;
}
if (newStr == 2 && byteCounter <2*byteCount) {
colNum=byteCounter%width;
strNum=(byteCounter-colNum)/width;
if(strNum%2==0){
imgArr[(strNum/2)*width+colNum]=data[0];
}
if(strNum%2==1){
imgArr[((strNum-1)/2)*width+byteCount+colNum]=data[0];
}
byteCounter++;
}
if (newStr == 2 && byteCounter == 2*byteCount) {
newStr = 0;
byteCounter = 0;
framCount++;
console.log('Frame Num ', framCount);
imgTyp=FIGTYPE;
}
}
if(imgTyp==FIGTYPE){
if (data[0] == 42 && newStr == 0) {
newStr = 1;
data[0]=255;
}
if (newStr == 1 && data[0] == 42) {
newStr = 2;
}
if (newStr == 2 && byteCounter < byteCount) {
imgArr[byteCounter+2*byteCount] = data[0];
byteCounter++;
}
if (newStr == 2 && byteCounter == byteCount) {
newStr = 0;
byteCounter = 0;
framCount++;
console.log('Frame Num ', framCount);
imgTyp=PIXTYPE;
callback(imgArr);
}
}
});
port.on('error', function() {
alert('Ошибка подключения к порту СОМ');
});
}
Пример работы устройства показан в коротком видеоролике.
Заключение
Полученные результаты показывают высокую эффективность метода распознавания на устройствах, казалось бы, совершенно для этого не предназначенных. Небольшое усовершенствование метода, связанное с использованием информации из нескольких кадров для дополнительного «всматривания» в интересующие области, позволит поднять качество распознавания до уровня коммерческих продуктов.
Также понятен подход для анализа и распознавания многопризнаковых объектов, таких как строки рукописного текста или иероглифы, однако для этого нужны устройства с большим, чем у нашего esp (512K, объем программы более 250К) объемом памяти.
Спасибо за внимание.
Ссылки:
1. Распознавание текста в ABBYY FineReader (2/2)
2. Omega2: самый маленький в мире микрокомпьютер с Linux и Wi-Fi
3. Orange Pi 2G-IoT — идеальный одноплатник для IoT
4. Распознавание цифр на микроконтроллере
5. Скетч Arduino для работы с камерой OV7670
6. Даташит камеры OV7670
7. Отражение динамики в модели СКУД
Комментарии (26)
da-nie
15.06.2017 12:16+2Вот этот метод:
LRpro
15.06.2017 12:37+1Спасибо за интересную статью. В ней также как и в большинстве работ по распознаванию делается основной акцент на анализ контуров символа, в то время как очень большую информацию несут и области ограниченные этими контурами. Отличие в этом.
lash05
15.06.2017 12:52распознавание независимо от шрифта
— размер шрифта тоже подразумевается, или только начертание?
LRpro
15.06.2017 12:56Да, размер принципиально не влияет на распознавание, нормализация символов не производится. Хорошо определяет символ в символе, например,1..9, А,… в О. Нарисовать для имеющих прототипов варианты в других буквах проблематичны из-за малого размера щели в устройствах.
de1m
15.06.2017 13:29А какое преминение этому устройсву вы планируете? Щель всё таки довольно таки мала, чтобы что-то большое прочитать.
LRpro
15.06.2017 13:39Ну, цель статьи в том, чтобы показать возможности метода. Понятно, что если он работает на микроконтроллере, то будет работать и на мощном устройстве, например, телефоне или микрокомпьютерах. Связка микрокомпьютера с 2Мп камерой позволит решать все задачи, где применяется OCR. На базе же esp вполне реально построить системы для удаленного сбора данных (http://ocr-molecula.com/ru)
ser-mk
15.06.2017 19:20а почему не выложили код алгоритма для распознования на МК?
и что происходит на этой картинке?
LRpro
15.06.2017 19:31На картинке сверху вниз: изображение непосредственно получаемое с камеры в режиме YUV;
изображение, полученное в результате построчной пороговой бинаризации, порог вычисляется для каждой строки;
изображение, полученное путем синтеза сегментированного бинаризованного изображения, т. е. это изображение, которое анализирует («видит») микроконтроллер для распознавания.
Ну а детали алгоритма это наше ноу хау.ser-mk
15.06.2017 23:56А как порог вычисляете для строки по Otsu?
Как сегментацию проводите бинарного изображения?LRpro
16.06.2017 07:09Скорее по Бернсену, но только для каждой строки, потому что единого изображения микроконтроллер не имеет — работает только с одной строкой в потоке. Классической сегментации нет, речь идет о разделении контура символа (некоторой связной области) на элементы и последующим их соединении. Отсюда и получаются те немного странные изображения в третьем ряду картинок из визуализации процесса распознавания.
osigida
15.06.2017 22:48высокая эффективность это сколько?
LRpro
16.06.2017 06:59Если неправильно, то нужно отметить, что при классификации нет элементов вероятностного выбора: при анализе формируется единственный признак, который однозначно определяет класс графемы. Из-за отсутствия пространства событий говорить об эффективности, как мере вероятности некорректно. Классифицировать полученный без помех символ оказалось легко, проблема получить его без помех с одного кадра.
osigida
16.06.2017 10:33Простите, был неточен.
Полученные результаты показывают высокую эффективность метода распознавания на устройствах
Когда говорят об эффективности то скорее всего имеют в виду, сколько распознано правильно, сколько нет, в процентном соотношении. Интересно узнать сколько получилось.LRpro
16.06.2017 11:18Я уже пытался ответить на этот вопрос, наверное не очень понятно излагаю. Когда для объекта, в данном случае, символа строится один признак, неправильное распознавание означает неправильно построенный признак, а это значит признак другого символа. Ну знаете, когда ребенок научился распознавать ту же печатную букву А, а потом ему показывают рукописную а и он не может ее распознать. Для того, чтобы обмануть машину нужно так написать символ, чтобы она неправильно построила признак, например, сделать Д очень похожей на А, получить А и сказать, а вот ошибка. Так как такая задача не ставилась, а, наоборот, символы как видно из видео, писались очень аккуратно, то ошибок нет. Задержки связаны с искажением из-за помех или неправильным моментом попадания в кадр. Частота -то кадров очень низкая — приблизительно 1 кадр за 2,5 сек.
lynxus
19.06.2017 23:12+1*) на примерах виден тёмный ареал по краям и чёрная полоска снизу
чтобы уменьшить ошибку распознавания эти пикселы можно не процессить
например, сделать калибровку по белой поверхности
во время калибровки по краям должны стабильно образовываться чёрные пикселы
первый и последний белый пиксел в строке даст её рабочую область
если большая часть строки чёрная — это целком нерабочая строка
*) если символ коснулся края (или нерабочей области), можно не пытаться его дальше распознавать, чтобы уменьшить ошибку
простая фича: можно выдать сигнал, в какую сторону надо подвинуть камеру, когда текст выходит за края
это поможет при установке, а также очень простой тест, всё ли в порядкеLRpro
19.06.2017 23:13Приблизительно так и делаем: предполагаем, что нет символов выходящих за края, а так же, что в изображении нет длинных черных полос, хотя в общем виде это может быть несправедливо. Касательно фичи — очень интересно, спасибо!
da-nie
Была такая идея в 80-е — метод точек (забыл их название — узловые, что ли), которые получаются при расфокусировке линзы камеры. Эти точки соединяли и после этого анализировали. В принципе, получалось приемлемое качество распознавания.