понедельник, 6 октября 2014 г.

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

В первой части я пытался отбирать орехи без OpenCV, и был не прав.

Программируя на Делфи еще с института, начиная с версии 2, хоть и будучи довольно близко знакомым с другими ЯП, я все же начал искать заголовки именно для Делфи. И нашел.

Скомпилировав пример EdgeDetect, и увидев результаты, я осознал, что OpenCV инструмент действительно мощный, простой и быстрый. Спасибо хорошим людям за паскалевые заголовочные файлы к C интерфейсу этой замечательной библиотеки, ведь они дали мне возможность писать в среде привычного для меня RAD. Определившись с ЯП, я начал разрабатывать ПО с нуля, в данной статье описаны мои победы и злоключения, и прошу, не судите больно, это только вторая моя статья на хабре.



Первые грабли были связаны с довольно ощутимой утечкой памяти: связанны они были с тем, что после каждого cvFindContours нужно вызывать cvClearMemStorage.

Вскоре осознав что при 30 FPS, что выдавал мой Logitech C270, я не смогу детектить орехи в свободном падении я начал искать высокоскоростные камеры. Для опытов была приобретена PS3 Eye Camera, выдававшая заоблачные 187 FPS при 320x240. В результате чего были найдена еще одна «фича» — лимит отрисовки в 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);
}
}
}







Форсунки являют собой электромагнитный соленоид. Коммутируем данную нагрузку по следующей схеме. Для каждой форсунки нужен отдельный ключ.

Показать




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



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

Спасибо за внимание.

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий