В первой части я пытался отбирать орехи без OpenCV, и был не прав.
Программируя на Делфи еще с института, начиная с версии 2, хоть и будучи довольно близко знакомым с другими ЯП, я все же начал искать заголовки именно для Делфи. И нашел.
Скомпилировав пример EdgeDetect, и увидев результаты, я осознал, что OpenCV инструмент действительно мощный, простой и быстрый. Спасибо хорошим людям за паскалевые заголовочные файлы к C интерфейсу этой замечательной библиотеки, ведь они дали мне возможность писать в среде привычного для меня RAD. Определившись с ЯП, я начал разрабатывать ПО с нуля, в данной статье описаны мои победы и злоключения, и прошу, не судите больно, это только вторая моя статья на хабре.
Первые грабли были связаны с довольно ощутимой утечкой памяти: связанны они были с тем, что после каждого cvFindContours нужно вызывать cvClearMemStorage.
Вскоре осознав что при 30 FPS, что выдавал мой Logitech C270, я не смогу детектить орехи в свободном падении я начал искать высокоскоростные камеры. Для опытов была приобретена PS3 Eye Camera, выдававшая заоблачные 187 FPS при 320x240. В результате чего были найдена еще одна «фича» — лимит отрисовки в 65 FPS под Win7. Как оказалось, лимитирует cvWaitKey — тут же был найден выход, а именно: вызывать cvWaitKey не с каждым обработанным фреймом, а с меньшей периодичностью.
Опишу непосредственно сам алгоритм.
Для каждого образца из базы сгенерирован «альбом» повернутых образцов с шагом в 10 градусов. Это дает возможность хранить гораздо меньше образцов в базе эталонов и не тратить ресурсы на вращение «на лету». Примитивную коррекцию перспективы же я реализую «на лету» с помощью cvResize.
В результате скольжения орехов по желобам, они быстро пачкают эти самые желоба жиром, на который очень богаты. Данный факт мешает более точному нахождению контуров орехов. Я пробовал и простой cvThreshold и cvThreshold с cvCanny поверх — на грязном фоне работало плохо. Плюс мешала тень, которую отбрасывали орехи, когда пролетами на небольшом отдалении от фона. Для решения этой проблемы я придумал свой фильтр. Суть его в том, что он заменяет наиболее «нецветные» пиксели белыми пикселями.
Для скользящих по белому фону орехов находится контур. Из контура делается маска, которая позволяет копировать с прозрачностью каждый орех в массив из PIplImage. Слишком маленькие и очень большие контуры пропускаются.
Кадр поделен на регионы->линии, в реальности это отдельные желобы, по которым скользят орехи. В конце каждой из линий находится исполнительное устройство, являющее собой форсунку, контролирующую подачу воздуха, находящегося под давлением.
В приложении же, каждую линию обслуживает отдельная нить(thread). Внутри нити мы находим ближайший к форсунке орех, и определяем его «сходство» с базой эталонных образцов. Ниже участок кода, считающий «сходство» через cvAbsDiff:
Значение переменной wcount и является коэффициентом схожести орешка с эталоном в «попугаях». При превышении этого значения выше порогового передаем номер линии через ком порт в ардуино. Контроллер открывает форсунку на заданное время, чем «сдувает» орех, в нормальном состоянии форсунки закрыты. Для асинхронной работы исполнительных устройств был написан следующий скетч.
Форсунки являют собой электромагнитный соленоид. Коммутируем данную нагрузку по следующей схеме. Для каждой форсунки нужен отдельный ключ.
По просьбе заказчика я не могу опубликовать изображения готового устройства. Надеюсь следующее видео даст возможность представить конечное устройство.
Старался описать наиболее сложные и интересные моменты, с которыми встретился в результате работы над этим интересным проектом. Не стесняйтесь задавать вопросы, если что-то, по Вашему мнению, обрисовано не достаточно подробно.
Спасибо за внимание.
Программируя на Делфи еще с института, начиная с версии 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.
Комментариев нет:
Отправить комментарий