На сегодняшний день оптическое распознавание символов является частью решения таких прикладных задач, как распознавание и оцифровка текстов, распознавание документов, распознавание автомобильных номеров, определение номеров банковских карточек, чтение показаний счетчиков учета, определения номеров домов для создания карт (Google Street View) и т.д.

Распознавание символа означает анализ его изображения с целью получения некоторого набора признаков для сравнения их с признаками класса [ 1 ]. Выбор такого набора и способы его определения отличают разные методы распознавания, но для большинства из них необходима одномоментная информация обо всех пикселях изображения.

Последнее обстоятельство и достаточно большой объем вычислений делают невозможным использования маломощных вычислительных устройств (микроконтроллеров) для оптического распознавания символов. «Да и зачем?» — воскликнет информированный читатель, «мощности вычислительных устройств постоянно растут, а их цена падает!»[2, 3]. Допустим, что ответ будет такой: просто интересно, возможно ли упростить метод распознавания до такой степени, чтобы можно было бы использовать микроконтроллер?

Оказалось можно, более того, оказалось возможным то, что кажется относится к области фантастики, а именно:

  • распознавание независимо от шрифта;
  • распознавание строки символов без разделения на отдельные символы;
  • распознавание «экранированных» символов, например, символ в символе;
  • распознавание «разорванных» символов;
  • распознавание символов, состоящих из нескольких частей;
  • распознавание без изменения признаков при повороте до 15°. Возможность
    распознавания повернутых символов на больший угол за счет изменения его признаков;
  • распознавание символов в видеопотоке с одного кадра;
  • распознавание рукописных символов;
  • ограниченное количество признаков для описания класса символов, для арабских цифр
    и латиницы — один, для кириллицы — максимум два (например, для некоторых
    вариантов написания Ж);
  • простое «ручное» определение признаков для нового класса;
  • автоматическое определение признаков для нового класса;
  • расширение классов символов простым добавлением в базу его признаков;

И все это на микроконтроллере.

Основная идея метода


Теперь поподробнее о самом методе. Рассмотрим, например, различные начертания символа А:


Несмотря на видимые различия, можно выделить общие признаки структурного типа, которые являются необходимыми признаками прописной буквы А (для не разорванных символов), а именно: если рассматриваемый символ является прописной буквой А, то он будет содержать замкнутую область и область разомкнутую вниз.


Еще раз подчеркнем, что указанные признаки являются необходимыми, но не достаточными: если мы будем описывать контуры вокруг двух областей указанного типа,


то это не обязательно будут буквы прописные А, возможны, например Д, Я,R, строчная рукописная А,..:


Однако использование областей в качестве элементов символа позволяет сформировать достаточные признаки, причем, для подавляющего числа алфавитно-цифровых символов можно сформировать единственный достаточный признак! Его очень просто сформировать для каждого класса и, в отличие от структурных признаков, используемых ABBYYTeam при распознавании рукописных символов [ 1 ], его вариативность очень низкая и его возможно формировать автоматически! Другими словами, такие признаки хорошо работают как для печатных, так и для рукописных символов.

Устройство для распознавания


Первая проверка метода была описана в статье [ 4 ]. Метод проверялся на одиночных цифрах, получаемых примитивной камерой от мыши с семисегментного индикатора или напечатанных на бумаге. После первого успеха возникло естественное желание проверить возможность распознавания последовательности символов, а для этого нужно использовать другую камеру. Мы использовали камеру OV7670 (0.3Мп). Остальные главные компоненты схемы остались без изменения — это Arduino и ESP8266, но изменились их функции. Arduino теперь используется для инициализации камеры, в качестве задающего генератора, приема распознанных символов и их отображения на индикаторах. ESP8266 занимается получением изображения с камеры и его распознаванием, кроме того он обеспечивает передачу данных на Ардуино для отображения и передачу распознанной информации через WiFi на внешние устройства, например, смартфон. Используемая электрическая схема устройства приведена на рисунке:


Изображение попадает в устройство через щель в его нижней части и, отражаясь от зеркала, поступает на камеру. Подсветка изображения осуществляется светодиодом через то же зеркало. Механическая схема устройства приведена на рисунке.


Фото рабочего прототипа

Первый вариант рабочего прототипа:






Второй вариант рабочего прототипа:







Получение изображения на на ESP8266


Настройки камеры при инициализации взяты из [5]. Частота кадров приблизительно 0,4 кадр/с.Так как количество пинов у ESP8266 недостаточно, то обрабатываются только 6 старших бит каждого яркостного байта изображения (камера настроена в режиме YUV). Для получения изображения используется конечный автомат (машина состояний).


Согласно даташит камеры OV7670 [6]



Можно выделить следующие состояния камеры, условия и сигналы во время ее работы:
Имя состояния,номер Описание состояния Сигнал к переходу
в другое состояние
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]. Вся машина описывается ее геном — трехмерным вектором, первый компонент которого содержит ключи, а второй — имена новых состояний, третий — имена функций. Ключ содержит информацию от текущем состоянии и сигнале перехода. Для формирования ключа и сигнала используются битовые операции. Детали реализации понятны из кода модуля чтения камеры.

user_main.c
#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);
}


readCam.h

#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_ */



readCam.c
#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);
}


Обработка изображения


Обработка изображения состоит в построчной бинаризации, соединении полученных отрезков, анализе и синтезе полученных фигур. Целью обработки является формирование интегральных признаков, включающих свойства областей, входящих в фигуры. Несмотря на простоту основной идеи ее реализация содержит ряд специфических моментов, которые не могут быть раскрыты в рамках настоящей статьи.

Визуализация процесса распознавания


Для отладки процесса распознавания использовалась визуализация на ПК исходной картинки, бинаризованной и картинки, которую «видит» или «понимает» микроконтроллер. Несмотря на то, что последняя не сильно радует наш глаз, ее деталей достаточно, чтобы распознать символы. На рисунке приведены примеры визуализации.





Нужно отметить, что иногда из-за ошибок синхронизации камеры возникают ситуации, когда в исходной картинке появляются линии с цветоразностными, а не яркостными байтами. В этом случае картинка смазывается и распознавание не происходит. Задача обработки таких ситуаций на настоящем этапе не ставилась



Также распознавание не происходит при неправильном позиционировании текста:



Для визуализации использовалась небольшая программа, написанная на Java Script с использованием nodeWebkit.

app.js
*для работы с COM портом необходимо собрать модуль nodeJS «serialport» под 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('Ошибка подключения к порту СОМ');
    });

}


Пример работы устройства показан в коротком видеоролике.

Видео с прототипом №1



Видео с прототипом №2


Заключение


Полученные результаты показывают высокую эффективность метода распознавания на устройствах, казалось бы, совершенно для этого не предназначенных. Небольшое усовершенствование метода, связанное с использованием информации из нескольких кадров для дополнительного «всматривания» в интересующие области, позволит поднять качество распознавания до уровня коммерческих продуктов.

Также понятен подход для анализа и распознавания многопризнаковых объектов, таких как строки рукописного текста или иероглифы, однако для этого нужны устройства с большим, чем у нашего 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)


  1. da-nie
    15.06.2017 12:11

    Была такая идея в 80-е — метод точек (забыл их название — узловые, что ли), которые получаются при расфокусировке линзы камеры. Эти точки соединяли и после этого анализировали. В принципе, получалось приемлемое качество распознавания.


  1. da-nie
    15.06.2017 12:16
    +2

    Вот этот метод:





    1. LRpro
      15.06.2017 12:37
      +1

      Спасибо за интересную статью. В ней также как и в большинстве работ по распознаванию делается основной акцент на анализ контуров символа, в то время как очень большую информацию несут и области ограниченные этими контурами. Отличие в этом.


  1. lash05
    15.06.2017 12:26

    можно выделить общие признаки структурного типа
    — если не секрет, используете ли тут специализированные алгебраические преобразования надо бинарными матрицами?


    1. LRpro
      15.06.2017 12:32

      Нет, в этом нет необходимости


  1. lash05
    15.06.2017 12:52

    распознавание независимо от шрифта
    — размер шрифта тоже подразумевается, или только начертание?


  1. LRpro
    15.06.2017 12:56

    Да, размер принципиально не влияет на распознавание, нормализация символов не производится. Хорошо определяет символ в символе, например,1..9, А,… в О. Нарисовать для имеющих прототипов варианты в других буквах проблематичны из-за малого размера щели в устройствах.


  1. de1m
    15.06.2017 13:29

    А какое преминение этому устройсву вы планируете? Щель всё таки довольно таки мала, чтобы что-то большое прочитать.


  1. LRpro
    15.06.2017 13:39

    Ну, цель статьи в том, чтобы показать возможности метода. Понятно, что если он работает на микроконтроллере, то будет работать и на мощном устройстве, например, телефоне или микрокомпьютерах. Связка микрокомпьютера с 2Мп камерой позволит решать все задачи, где применяется OCR. На базе же esp вполне реально построить системы для удаленного сбора данных (http://ocr-molecula.com/ru)


  1. ser-mk
    15.06.2017 19:20

    а почему не выложили код алгоритма для распознования на МК?

    и что происходит на этой картинке?
    image


  1. LRpro
    15.06.2017 19:31

    На картинке сверху вниз: изображение непосредственно получаемое с камеры в режиме YUV;
    изображение, полученное в результате построчной пороговой бинаризации, порог вычисляется для каждой строки;
    изображение, полученное путем синтеза сегментированного бинаризованного изображения, т. е. это изображение, которое анализирует («видит») микроконтроллер для распознавания.
    Ну а детали алгоритма это наше ноу хау.


    1. ser-mk
      15.06.2017 23:56

      А как порог вычисляете для строки по Otsu?
      Как сегментацию проводите бинарного изображения?


      1. LRpro
        16.06.2017 07:09

        Скорее по Бернсену, но только для каждой строки, потому что единого изображения микроконтроллер не имеет — работает только с одной строкой в потоке. Классической сегментации нет, речь идет о разделении контура символа (некоторой связной области) на элементы и последующим их соединении. Отсюда и получаются те немного странные изображения в третьем ряду картинок из визуализации процесса распознавания.


        1. ser-mk
          16.06.2017 14:42

          а сегментацию делаете сразу на бинаризированном изображении? или еще нужно избавляться от шумов?


          1. LRpro
            16.06.2017 15:38

            Сразу после построения бинаризованного изображения


            1. ser-mk
              16.06.2017 16:29

              А как проводится сегментация? почему в неё не попадают зашумленные линии?


              1. LRpro
                16.06.2017 17:53

                То что шум отсеивается вы видите по различиям между вторым и третьим рядом на иллюстрациях. Детали относятся к тонкостям алгоритма.


  1. osigida
    15.06.2017 22:48

    высокая эффективность это сколько?


    1. LRpro
      15.06.2017 22:54

      Если правильно понял вопрос, то до $50


    1. LRpro
      16.06.2017 06:59

      Если неправильно, то нужно отметить, что при классификации нет элементов вероятностного выбора: при анализе формируется единственный признак, который однозначно определяет класс графемы. Из-за отсутствия пространства событий говорить об эффективности, как мере вероятности некорректно. Классифицировать полученный без помех символ оказалось легко, проблема получить его без помех с одного кадра.


      1. osigida
        16.06.2017 10:33

        Простите, был неточен.

        Полученные результаты показывают высокую эффективность метода распознавания на устройствах


        Когда говорят об эффективности то скорее всего имеют в виду, сколько распознано правильно, сколько нет, в процентном соотношении. Интересно узнать сколько получилось.


        1. LRpro
          16.06.2017 11:18

          Я уже пытался ответить на этот вопрос, наверное не очень понятно излагаю. Когда для объекта, в данном случае, символа строится один признак, неправильное распознавание означает неправильно построенный признак, а это значит признак другого символа. Ну знаете, когда ребенок научился распознавать ту же печатную букву А, а потом ему показывают рукописную а и он не может ее распознать. Для того, чтобы обмануть машину нужно так написать символ, чтобы она неправильно построила признак, например, сделать Д очень похожей на А, получить А и сказать, а вот ошибка. Так как такая задача не ставилась, а, наоборот, символы как видно из видео, писались очень аккуратно, то ошибок нет. Задержки связаны с искажением из-за помех или неправильным моментом попадания в кадр. Частота -то кадров очень низкая — приблизительно 1 кадр за 2,5 сек.


  1. merl1n
    15.06.2017 23:08

    Результат впечатляет. Не думал, что на OV7670 и Arduino можно сделать такое.


  1. LRpro
    19.06.2017 23:12

    +


  1. lynxus
    19.06.2017 23:12
    +1

    *) на примерах виден тёмный ареал по краям и чёрная полоска снизу
    чтобы уменьшить ошибку распознавания эти пикселы можно не процессить

    например, сделать калибровку по белой поверхности
    во время калибровки по краям должны стабильно образовываться чёрные пикселы
    первый и последний белый пиксел в строке даст её рабочую область
    если большая часть строки чёрная — это целком нерабочая строка

    *) если символ коснулся края (или нерабочей области), можно не пытаться его дальше распознавать, чтобы уменьшить ошибку
    простая фича: можно выдать сигнал, в какую сторону надо подвинуть камеру, когда текст выходит за края
    это поможет при установке, а также очень простой тест, всё ли в порядке


    1. LRpro
      19.06.2017 23:13

      Приблизительно так и делаем: предполагаем, что нет символов выходящих за края, а так же, что в изображении нет длинных черных полос, хотя в общем виде это может быть несправедливо. Касательно фичи — очень интересно, спасибо!