В первой части я пытался отбирать орехи без 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
