Pull to refresh

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

Reading time4 min
Views14K
В первой части я пытался отбирать орехи без 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
  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);
     }
  }
}


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


А так выглядит собранное устройство:
Показать
image
image

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


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

UPD: Добавил slowmotion видео, 75 FPS -> 1 FPS
Tags:
Hubs:
+14
Comments12

Articles