Processing z OpenCV

Poprzednio wspomniałem, że planuję użyć OpenCV.  Dlatego też, tym razem skupię się na obróbce obrazu z kamery właśnie przy użyciu tej biblioteki.

kola-na-scianie-łapa

Brązowe i niebieskie plamy, to uchwyty na ścianie. Kółka z numerami są wyświetlane przez rzutnik. Celem jest wykrycie przysłonięcia, lub konkretniej, wykrycia kolizji  z kołem. Na początek pierwszy etap, czyli przygotowanie obrazu do analizy.

Do detekcji użyję obrazu z kamery skierowanej na ścianę. Przetworzenie różnic między kolejnymi klatkami obrazu da mi informację o tym co się zmieniło. Czyli gdzie następuje ruch. Do wykonania wszystkich potrzebnych operacji, użyję dostępnych funkcji z OpenCV.  Biblblioteka powstała w Rosyjskim oddziale Intela,
a obecnie rozwijania jest przez grupę Itseez. Głównym jej celem  jest obróbka i analiza obrazu w czasie rzeczywistym. Dostępne są porty na najważniejsze języki, czyli idealna do moich zastosowań.

Na początek należy ją zainstalować. Ponownie użyję Contribution Manager.

add-opencv

 

Od tego momentu dostępne są dobrodziejstwa tego kolosa. Pierwszym krokiem w analizie, będzie eliminacja tła. Najprymitywniejsze rozwiązanie to odjęcie od każdej klatki wcześniej zapamiętanego tła. Uzyskana różnica ułatwi wykrycie tych elementów, które pojawiają się na tle. Na razie moim tłem, będzie kawałek ściany. Chciałbym wykryć, że coś się na tym obrazie pojawiło.

frame-bkg

Tło

frame-reka-na-tle

Obrazek z ręką ofiary

frame-substract-bkg

Różnica obu klatek

 

 

 

 

 

W analizie, kolorowy obraz nie będzie za bardzo przydatny, więc zamienię go na binarny. Przekształcenie to otrzymujemy za pomocą funkcji threshold(). Argument definiuje poziom odcięcia. Na poglądowym rysunku zaznaczyłem punkt odcięcia – niebieska linia. Na czerwono to co odrzucamy, na zielono to co zostanie i będzie białe. Jeśli obraz jest ciemny, to na histogramie będzie dużo po lewej stronie, a niewiele po prawej. Tak jest w przypadku moich zdjęć. Dlatego współczynnik odcięcia ustawiłem dość nisko, to znaczy na poziomie 50. Inaczej nie udało by mi się wyciągnąć czegokolwiek z obrazu. Co ciekawe to co udostępnia Processing, jest wyjątkowo ubogie w możliwości. Oryginalne OpenCV jest bardziej rozbudowane.

threshold

 

Wynik działania operacji threshold()

 

frame-sub-threshold

 

Tak będzie wyglądał kod wykonujący usunięcie tła.  Operacja odpowiedzialna za wyznaczenie różnicy to diff(). Wywołanie getSnapshot()zwraca aktualny stan w OpenCV, czyli wynik dotychczasowych operacji. Co prawdę mówiąc jest takie sobie. Trzeba cały czas pamiętać, co tam się ostatnio ustawiło w obiekcie. Dlatego też, dla wygody dodałem funkcję getGrayImage, która tworzy ładuje i przekształca odpowiedni obrazek.

import gab.opencv.*;
import org.opencv.imgproc.Imgproc;

OpenCV opencv;

PImage src, bkg, diffed;

final static int W = 960;
final static int H = 540;


PImage getGrayImage(String fileName)
{
  OpenCV cv = new OpenCV(this, loadImage(fileName));
  cv.gray();
      
  PImage img = cv.getSnapshot();
  img.resize(W/2, H/2);
  
  return img;
}

void setup() 
{
  size(960, 540);

  opencv = new OpenCV(this, W/2, H/2);
  opencv.gray();

  bkg = getGrayImage("frame-bkg.jpg");
  src = getGrayImage("frame-reka-na-tle.jpg");
 
  opencv.loadImage( src );
  opencv.diff( bkg );
  
  diffed = opencv.getSnapshot();

  image(bkg, 0, 0);
  image(diffed, W/2, 0);
  
  opencv.threshold(50);
  image(opencv.getSnapshot(), 0, H/2);
  
  opencv.loadImage( diffed );
  opencv.threshold(60);
  image(opencv.getSnapshot(), W/2, H/2);


  noLoop(); // nie potrzebuję wywoływać draw(), bo już wszystkie operacje zostały wykonane.
}

Wygląda to już całkiem obiecująco, ale dobrze by było jeszcze trochę go przetworzyć. Chciałbym uzyskać bardziej regularny kształt i zlikwidować szum. Tutaj z pomocą przychodzą przekształcenia morfologiczneerozja oraz dylatacja. Zmieniają one obraz i pozwalają przygotować dane do dalszej analizy. Jeśli chodzi o więcej detali, proponuję zaglądnąć w jakieś poważniejsze miejsce, choćby tutaj. Google sypie linkami na prawo i lewo.

Erozja obrazu powoduje zmniejszenie obiektów, a tym samym separację od innych na obrazie. Likwiduje także drobne szpilki, punkty itp. Pojawiają się one w związku z niedokładnością pracy kamery, szumami przetwornika, drganiami i innymi niedokładnościami. W konsekwencji uzyskuję efekt podobny do zmniejszenia ilości detali lub jakby coś zjadło elementy na obrazie.

erode

Erozja

 

Po erozji miło byłoby wypełnić powstałe szczeliny i ostre krawędzie. W tym celu użyję dylatacji. Efekt zbliżony do rosnącego ciasta wokół elementów.

dilate

Dylatacja

Większa ilość wywołań dylatacji generuje nam taki efekt:

more-dilate

Wielokrotne wywołanie dylatacji

 

 

Kod generujący powyższy obrazek:

import gab.opencv.*;

OpenCV opencv;

PImage src, bkg, diffed;

final static int W = 960;
final static int H = 540;


PImage getGrayImage(String fileName)
{
  OpenCV cv = new OpenCV(this, loadImage(fileName));
  cv.gray();
      
  PImage img = cv.getSnapshot();
  img.resize(W/2, H/2);
  
  return img;
}

void setup() 
{
  size(960, 540);

  opencv = new OpenCV(this, W/2, H/2);
  opencv.gray();

  bkg = getGrayImage("c:/workspace/climbing/#media/frame-bkg.jpg");
  src = getGrayImage("c:/workspace/climbing/#media/frame-reka-na-tle.jpg");
 
  opencv.loadImage( src );
  opencv.diff(bkg);
  diffed = opencv.getSnapshot();

  image(bkg, 0, 0);
  image(diffed, W/2, 0);
  
  opencv.threshold(80);
  
  image(opencv.getSnapshot(), 0, H/2);

  opencv.erode();
  opencv.dilate();

  opencv.dilate();
  opencv.dilate();
  opencv.dilate();
  opencv.dilate();
  
  opencv.dilate();
  opencv.erode();
 
  image(opencv.getSnapshot(), W/2, H/2);

  noLoop();
}

Dla eksperymentu dodałem jeszcze rozmycie obrazu oraz podniosłem kontrast. Strasznie ciemne mi to wszystko wyszło. Przy okazji dorzuciłem kolejne kroki przetwarzania.

all-in-one

Tło, ruchomy obraz z kamery, różnica, erozja, dylatacja, wszystkie operacje

W tym przykładzie jako tło używam pierwszej złapanej klatki, zaraz po uruchomieniu programu. Może nieco źle dobrałem otoczenie, i efekt nie jest zbyt spektakularny. Myślę, że choć trochę pokazuje co się dzieje. Na pewno lepiej jest to uruchomić i zobaczyć na żywo. Kod generujący sześć kolejnych kroków wygląda następująco:

import gab.opencv.*;
import processing.video.*;

OpenCV opencv;
Capture video;
PImage src, bkg, thresh, dilated, eroded, diff, both;

final static int W = 960;
final static int H = 540;


void setup() 
{
  size(1440, 540);
  frameRate(30);

  opencv = new OpenCV(this, W, H);

  video = new Capture(this, W, H);
  video.start();
}

void update()
{
  if ( video.available() )
  {
    video.read();
    
    // pierwsza klatka traktowana jest jako tło
    if ( bkg == null )
      bkg = video.copy();
    
    opencv.loadImage(video);
    src = opencv.getSnapshot();
    
    opencv.diff(bkg);
    opencv.blur(10);
  
    diff = opencv.getSnapshot();
    opencv.threshold(50);
    
    thresh = opencv.getSnapshot();
  
    opencv.erode();
    eroded = opencv.getSnapshot();

    opencv.loadImage(thresh);
    opencv.dilate();
    dilated = opencv.getSnapshot();
    
    opencv.loadImage(thresh);
    
    //opening
    opencv.erode();
    opencv.dilate(); 
    
    opencv.dilate(); 
    opencv.dilate(); 
    
    //closing
    opencv.dilate();    
    opencv.erode();

    both = opencv.getSnapshot();
  }
}

void draw() 
{
  update();

  if (src == null)
    return;
    
  image(bkg,       0,   0, W/2, H/2);
  image(src,     W/2,   0, W/2, H/2);
  image(diff,      W,   0, W/2, H/2);
  image(eroded,    0, H/2, W/2, H/2);
  image(dilated, W/2, H/2, W/2, H/2);
  image(both,      W, H/2, W/2, H/2);

  fill(255, 0, 0);
  text("bkg",       0 + 10,   0 + 10);
  text("src",     W/2 + 10,   0 + 10);
  text("diff",      W + 10,   0 + 10);
  text("eroded",    0 + 10, H/2 + 10);
  text("dilated", W/2 + 10, H/2 + 10);
  text("both",      W + 10, H/2 + 10);
}

Więcej o filtracji można przeczytać tutaj:  http://sun.aei.polsl.pl/http://etacar.put.poznan.pl/.

W sumie to nie komentowałem źródeł. Zastanawiam się, czy jak będę wrzucał kolejne pliki na githuba, to nie dodam kilka komentarzy. Może będzie łatwiej później do tego wrócić?

Jak na razie całość wygląda jak przygotowania do laborek z Computer Vision. Wyszedł dość brzydki kod. Efekty są w porządku, ale jakoś nie widzę, że będzie to wyglądało znacząco lepiej.  Niby funkcje są, łatwo się tego używa, ale brakuje większej kontroli nad różnymi operacjami, w tym nad erode i dilate. Mogę oczywiście w pętli odpalać wielokrotnie zadaną operację, ale jest to dalekie od optymalnego rozwiązania. Nie znalazłem także możliwości rozbudowanego parametryzowania przekształceń.  

Dodatkowo, nie odpowiada mi edytor w Processing. Pewnie mógłbym coś podłączyć zewnętrznego, ale nie mam specjalnie ochoty na szukanie rozwiązania. Jestem niestety leniwy i lubię mieć pod ręką wygodny edytor z debuggerem, więc przeskakuję na C++. Aktualnie jestem pod Windows, więc użyje VisualStudio 2013, bo takie mam zainstalowane. Jeśli chodzi o biblioteki, to mam do wyboru, czyste OpenCV, OpenFrameworks albo Cinder. Nie mam bladego pojęcia, co będzie optymalne.

Wybieram Cinder, ma fajną ikonkę – zawsze musi być jakiś powód. 

Z pewnością pierwsze kroki w Processing znacząco przyspieszyły eksperymenty, ale teraz przyszedł czas na poważniejsze rozwiązanie. Następny wpis to przepisanie tego co mam, na Cinder+OpenCV.

Dodaj komentarz