Что будет если смешать орехи, Arduino, OpenCV и Delphi. Часть 2

В первой части я пытался отбирать орехи без OpenCV, и был не прав.Программируя на Делфи еще с института, начиная с версии 2, хоть и будучи довольно близко знакомым с другими ЯП, я все же начал искать заголовки именно для Делфи. И нашел.Скомпилировав пример EdgeDetect, и увидев результаты, я осознал, что OpenCV инструмент действительно мощный, простой и быстрый. Спасибо хорошим людям за паскалевые заголовочные файлы к C интерфейсу этой замечательной библиотеки, ведь они дали мне возможность писать в среде привычного для меня RAD. Определившись с ЯП, я начал разрабатывать ПО с нуля, в данной статье описаны мои победы и злоключения, и прошу, не судите больно, это только вторая моя статья на хабре.Первые грабли были связаны с довольно ощутимой утечкой памяти: связанны они были с тем, что после каждого cvFindContours нужно вызывать cvClearMemStorage.Вскоре осознав что при 30 FPS, что выдавал мой Logitech C270, я не смогу детектить орехи в свободном падении я начал искать высокоскоростные камеры. Для опытов была приобретена PS3 Eye Camera, выдававшая заоблачные 187 FPS при 320×240. В результате чего были найдена еще одна «фича» — лимит отрисовки в 65 FPS под Win7. Как оказалось, лимитирует cvWaitKey — тут же был найден выход, а именно: вызывать cvWaitKey не с каждым обработанным фреймом, а с меньшей периодичностью.Показать if gettickcount-rendertickcount >= 33 then begin // 1000 / 33 = ~30 FPS //… rendertickcount:= gettickcount; cc:= cvWaitKey (1); end; Опишу непосредственно сам алгоритм.Для каждого образца из базы сгенерирован «альбом» повернутых образцов с шагом в 10 градусов. Это дает возможность хранить гораздо меньше образцов в базе эталонов и не тратить ресурсы на вращение «на лету». Примитивную коррекцию перспективы же я реализую «на лету» с помощью cvResize.Показать procedure createAlbum (nsIndex: integer); var i: integer; rot_mat: pCvMat; scale: Double; center: TcvPoint2D32f; width, height: integer;

begin

width:= nsamples[nsIndex].nutimgs[0].width; height:= nsamples[nsIndex].nutimgs[0].height;

for i:= 1 to 35 do begin nsamples[nsIndex].nutimgs[i].width:= width; nsamples[nsIndex].nutimgs[i].height:= height; rot_mat:= cvCreateMat (2, 3, CV_32FC1); center.x:= nsamples[nsIndex].nutimgs[0].width div 2; center.y:= nsamples[nsIndex].nutimgs[0].height div 2; scale:= 1; cv2DRotationMatrix (center, i * 10, scale, rot_mat); cvWarpAffine (nsamples[nsIndex].nutimgs[0], nsamples[nsIndex].nutimgs[i], rot_mat, CV_INTER_LINEAR or CV_WARP_FILL_OUTLIERS, cvScalarAll (0)); end;

end; В результате скольжения орехов по желобам, они быстро пачкают эти самые желоба жиром, на который очень богаты. Данный факт мешает более точному нахождению контуров орехов. Я пробовал и простой cvThreshold и cvThreshold с cvCanny поверх — на грязном фоне работало плохо. Плюс мешала тень, которую отбрасывали орехи, когда пролетами на небольшом отдалении от фона. Для решения этой проблемы я придумал свой фильтр. Суть его в том, что он заменяет наиболее «нецветные» пиксели белыми пикселями.Показать procedure removeBack (var img: PIplImage; k: integer); var x, y: integer; hue: byte; framesize: integer; begin cvcvtColor (img, hsv, CV_BGR2HSV); x:= 1; framesize:= img.width * img.height * 3; while x <= framesize do begin hue := hsv.imageData[x];

if hue < k then begin hsv.imageData[x-1] := 255; hsv.imageData[x+1] := 255; hsv.imageData[x] := 0; end;

inc (x,3); end; cvcvtColor (hsv, img, CV_HSV2BGR); end; Для скользящих по белому фону орехов находится контур. Из контура делается маска, которая позволяет копировать с прозрачностью каждый орех в массив из PIplImage. Слишком маленькие и очень большие контуры пропускаются.Показать frame:= cvQueryFrame (capture); cvCopy (frame, oframe); cvCvtColor (frame, gframe, CV_BGR2GRAY); cvThreshold (gframe, gframe, LowThreshVal, HighThreshVal, CV_THRESH_BINARY_INV); cvFindContours (gframe, storage, @contours, SizeOf (TCvContour), CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE, cvPoint (0, 0)); b:= contours; NutIndex:= 0; while b <> nil do begin asize:= cvContourArea (b, CV_WHOLE_SEQ);

if ((asize > tbminObjSize) and (asize < tbmaxObjSize)) then begin

_rect:= cvBoundingRect (b); cvZero (mask); cvDrawContours (mask, b, CV_RGB (255, 0, 255), CV_RGB (255, 255, 0), -1, CV_FILLED, 1, cvPoint (0, 0));

snuts[nutIndex].snut.width:= _rect.width; snuts[nutIndex].snut.height:= _rect.height;

cvSetImageROI (oframe, _rect); cvSetImageROI (mask, _rect);

cvZero (snuts[nutIndex].snut); cvCopy (oframe, snuts[nutIndex].snut, mask);

cvResetImageROI (oframe); cvResetImageROI (mask);

snuts[NutIndex].rect:= _rect;

inc (NutIndex); end; b:= b.h_next; end; Кадр поделен на регионы→линии, в реальности это отдельные желобы, по которым скользят орехи. В конце каждой из линий находится исполнительное устройство, являющее собой форсунку, контролирующую подачу воздуха, находящегося под давлением.В приложении же, каждую линию обслуживает отдельная нить (thread). Внутри нити мы находим ближайший к форсунке орех, и определяем его «сходство» с базой эталонных образцов. Ниже участок кода, считающий «сходство» через cvAbsDiff: Показать cvAbsDiff (tnut, nsamples[tp1].nutimgs[angle], matchres); cvCvtColor (matchres, gmatchres, CV_BGR2GRAY); cvThreshold (gmatchres, gmatchres, tbminTreshM, 255, 0); wcount:= cvCountNonZero (gmatchres); Значение переменной wcount и является коэффициентом схожести орешка с эталоном в «попугаях». При превышении этого значения выше порогового передаем номер линии через ком порт в ардуино. Контроллер открывает форсунку на заданное время, чем «сдувает» орех, в нормальном состоянии форсунки закрыты. Для асинхронной работы исполнительных устройств был написан следующий скетч.Показать int timeout = 75; int comm; unsigned long timeStamps[8]; int ePins[] = {2, 3, 4, 5, 6, 7, 8, 9};

void setup () { for (int i=0; i <= 8; i++){ pinMode(ePins[i], OUTPUT); } Serial.begin(9600); while (!Serial) { ; // wait for serial port to connect. Needed for Leonardo only }

} void loop () { if (Serial.available () > 0) { comm = Serial.read (); if (comm >= 0 && comm <= 7) { digitalWrite(ePins[comm], HIGH); timeStamps[comm] = millis(); }

if (comm == 66) { Serial.write (103); // for device autodetection, 103 means version 1.03 } } for (int i=0; i <= 7; i++){ if (millis() - timeStamps[i] >= timeout) { digitalWrite (ePins[i], LOW); } } } Форсунки являют собой электромагнитный соленоид. Коммутируем данную нагрузку по следующей схеме. Для каждой форсунки нужен отдельный ключ.Показать 3e6b409ed726049d142a3de6d2aa07af.png По просьбе заказчика я не могу опубликовать изображения готового устройства. Надеюсь следующее видео даст возможность представить конечное устройство.[embedded content]Старался описать наиболее сложные и интересные моменты, с которыми встретился в результате работы над этим интересным проектом. Не стесняйтесь задавать вопросы, если что-то, по Вашему мнению, обрисовано не достаточно подробно.Спасибо за внимание.

© Habrahabr.ru