В первой части я пытался отбирать орехи без 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 и является коэффициентом схожести орешка с эталоном в «попугаях». При превышении этого значения выше порогового передаем номер линии через ком порт в ардуино. Контроллер открывает форсунку на заданное время, чем «сдувает» орех, в нормальном состоянии форсунки закрыты. Для асинхронной работы исполнительных устройств был написан следующий скетч.
Форсунки являют собой электромагнитный соленоид. Коммутируем данную нагрузку по следующей схеме. Для каждой форсунки нужен отдельный ключ.
А так выглядит собранное устройство:
По просьбе заказчика я не могу опубликовать изображения готового устройства. Надеюсь следующее видео даст возможность представить конечное устройство.
Старался описать наиболее сложные и интересные моменты, с которыми встретился в результате работы над этим интересным проектом. Не стесняйтесь задавать вопросы, если что-то, по Вашему мнению, обрисовано не достаточно подробно.
Спасибо за внимание.
UPD: Добавил slowmotion видео, 75 FPS -> 1 FPS
Программируя на Делфи еще с института, начиная с версии 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
with nsamples[nsIndex] do begin
width := nutimgs[0].width;
height := nutimgs[0].height;
center.x := width div 2;
center.y := height div 2;
scale := 1;
for i:= 1 to 35 do begin
nutimgs[i].width := width;
nutimgs[i].height := height;
rot_mat := cvCreateMat(2, 3, CV_32FC1);
cv2DRotationMatrix(center, i * 10, scale, rot_mat);
cvWarpAffine(nutimgs[0], nutimgs[i], rot_mat, CV_INTER_LINEAR or CV_WARP_FILL_OUTLIERS, cvScalarAll(0));
cvReleaseMat(rot_mat);
end;
end;
end;
В результате скольжения орехов по желобам, они быстро пачкают эти самые желоба жиром, на который очень богаты. Данный факт мешает более точному нахождению контуров орехов. Я пробовал и простой cvThreshold и cvThreshold с cvCanny поверх — на грязном фоне работало плохо. Плюс мешала тень, которую отбрасывали орехи, когда пролетами на небольшом отдалении от фона. Для решения этой проблемы я придумал свой фильтр. Суть его в том, что он заменяет наиболее «нецветные» пиксели белыми пикселями.
Показать
procedure removeBack(var img: PIplImage; k:integer);
var x, y :integer;
sat: byte;
framesize :integer;
begin
cvcvtColor(img, hsv, CV_BGR2HSV);
x := 1;
framesize := img.width * img.height * 3;
while x <= framesize do begin
sat := hsv.imageData[x];
if sat < 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 <= 7; 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);
}
}
}
Форсунки являют собой электромагнитный соленоид. Коммутируем данную нагрузку по следующей схеме. Для каждой форсунки нужен отдельный ключ.
А так выглядит собранное устройство:
Показать



По просьбе заказчика я не могу опубликовать изображения готового устройства. Надеюсь следующее видео даст возможность представить конечное устройство.
Старался описать наиболее сложные и интересные моменты, с которыми встретился в результате работы над этим интересным проектом. Не стесняйтесь задавать вопросы, если что-то, по Вашему мнению, обрисовано не достаточно подробно.
Спасибо за внимание.
UPD: Добавил slowmotion видео, 75 FPS -> 1 FPS