На сегодняшний день оптическое распознавание символов является частью решения таких прикладных задач, как распознавание и оцифровка текстов, распознавание документов, распознавание автомобильных номеров, определение номеров банковских карточек, чтение показаний счетчиков учета, определения номеров домов для создания карт (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... "); 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... "); 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! "); } 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('Ошибка подключения к порту СОМ'); }); }
Пример работы устройства показан в коротком видеоролике.
Заключение
Полученные результаты показывают высокую эффективность метода распознавания на устройствах, казалось бы, совершенно для этого не предназначенных. Небольшое усовершенствование метода, связанное с использованием информации из нескольких кадров для дополнительного «всматривания» в интересующие области, позволит поднять качество распознавания до уровня коммерческих продуктов.
Также понятен подход для анализа и распознавания многопризнаковых объектов, таких как строки рукописного текста или иероглифы, однако для этого нужны устройства с большим, чем у нашего 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.
Отражение динамики в модели СКУД