Interaktion II: Kamera

Kamera, Videofilter, Brightness-Tracking, Farbtracking, Positionen speichern (Puffer/Buffer), Daten glätten (smoothing), Tracking & Smoothing, OpenCV: Gesichtserkennung, OpenCV: Motion-Tracking

Kamera

Zugriff auf angeschlossene Kameras über die Processing-Video-Library. Deren Klasse Capture verwaltet Kameras (die Klasse Movie funktioniert ähnlich für Filmdateien).

import processing.video.*;

Capture kamera;

Einen Überblick über alle verfügbaren Kameras liefert:

println(Capture.list());

Erzeugen eines Kameraobjektes mit new. Starten des Zugriffs mit .start():

import processing.video.*;

Capture kamera;

void setup() {
  size(640,480);
  println(Capture.list());
  kamera = new Capture(this, width, height);
  kamera.start();
}

Das Bild der Kamera kann wie ein PImage gezeichnet werden...

void draw() {
  image(kamera,0,0);
}

...damit es aber die Pixel aus der Kamera enthält, muss es damit gefüllt werden (read()). Am besten immer dann, wenn die Kamera bereit ist (void captureEvent(...) {...}).

void captureEvent(Capture welcheKamera) {
  welcheKamera.read();
}

Videofilter

Wie die Pixel eines Bildes, können auch die einzelnen Pixel eines Videobilds abgefragt werden.

kamera.loadPixels();
Da das Bild in einem einzelnen Array gespeichert ist, müssen die 2D-Positionen (x, y oder i, j) in eine einzelne Zahl umgerechnet werden.

int pos = i + j*kamera.width;
color farbe = kamera.pixels[pos];

Pixelization mit einstellbarer Pixelgröße...

import processing.video.*;
import controlP5.*;

Capture kamera;
int abstand = 25;
ControlP5 cp5;

void setup() {
  size(640, 480);
  kamera = new Capture(this, width, height);
  kamera.start();
  cp5 = new ControlP5(this);
  cp5.addSlider("abstand")
    .setPosition(20, 60)
    .setRange(1, 100)
    .setSize(100,15)
    .setColorCaptionLabel(0)
    .getCaptionLabel().align(ControlP5.BOTTOM, ControlP5.TOP_OUTSIDE);
}

void draw() {
  background(255);
  noStroke();
  noSmooth();

  kamera.loadPixels();
  for (int i=0; i<kamera.width; i = i+abstand) {
    for (int j=0; j<kamera.height; j = j+abstand) {
      int pos = i + j*kamera.width;
      color farbe = kamera.pixels[pos];
      pushMatrix();
      fill(farbe);
      translate(i, j);
      rect(0, 0, abstand, abstand);
      popMatrix();
    }
  }
}

void captureEvent(Capture welcheKamera) {
  welcheKamera.read();
}

...oder Rechtecke, deren Größe von der Helligkeit der Pixel abhängt.

void draw() {
  background(255);
  noStroke();
  noSmooth();

  cam.loadPixels();
  for (int i=0; i<cam.width; i = i+abstand) {
    for (int j=0; j<cam.height; j = j+abstand) {
      int pos = (cam.width - i - 1) + j*cam.width;
      color farbe = cam.pixels[pos];
      pushMatrix();
      translate(i, j);
      fill(0,0,255);
      rectMode(CENTER);
      float gr = map(brightness(farbe),0,255,abstand,0);
      rect(0, 0, gr, gr);
      popMatrix();
    }
  }
}

Brightness-Tracking

Ganz ähnlich lassen sich auch interessante Bereiche im Bild bestimmen. Z.B. indem man sich die Position des hellsten Pixels merkt.

Dazu Variablen für Helligkeit in Position...

  color hellsteFarbe = 0;
  int hellstesX = 0;
  int hellstesY = 0;

...und ein Test (if (...)), der überprüft, ob ein Pixel heller als der bisher hellste ist.

if (brightness(farbe) > brightness(hellsteFarbe)) {
  hellsteFarbe = farbe;
  hellstesX = i;
  hellstesY = j;
}

An die hellste Stelle wir ein Kreis gezeichnet:

ellipseMode(CENTER);
fill(hellsteFarbe, 100);
ellipse(hellstesX, hellstesY, 100, 100);

Insgesamt:

import processing.video.*;

Capture kamera;

void setup() {
  size(640, 480);
  kamera = new Capture(this, width, height);
  kamera.start();
}

void draw() {
  noStroke();
  image(kamera,0,0);
  
  color hellsteFarbe = 0;
  int hellstesX = 0;
  int hellstesY = 0;

  kamera.loadPixels();
  for (int i=0; i<kamera.width; i++) {
    for (int j=0; j<kamera.height; j++) {
      int pos = i + j*kamera.width;
      color farbe = kamera.pixels[pos];
      if (brightness(farbe) > brightness(hellsteFarbe)) {
        hellsteFarbe = farbe;
        hellstesX = i;
        hellstesY = j;
      }
    }
  }
  
  ellipseMode(CENTER);
  fill(hellsteFarbe, 100);
  ellipse(hellstesX, hellstesY, 100, 100);
}

void captureEvent(Capture welcheKamera) {
  welcheKamera.read();
}

Farbtracking

Farbtracking in RGB funktioniert analog.

Wir legen eine Zielfarbe fest...

import processing.video.*;

Capture kamera;
color zielfarbe;

void setup() {
  size(640, 480);
  kamera = new Capture(this, width, height);
  kamera.start();
  zielfarbe = color(0);
}

...und benutzen die Distanz-Funktion für 3D-Koordinaten, um den Abstand der R-, G- und B-Werte jedes Pixels von der Zielfarbe zu bestimmen:

float aktuelleDistanz = dist(red(farbe),green(farbe),blue(farbe),red(zielfarbe),green(zielfarbe),blue(zielfarbe));
if (aktuelleDistanz < kleinsteDistanz) {
  kleinsteDistanz = aktuelleDistanz;
  farbeX = i;
  farbeY = j;
}

Per Mausklick kann die Zielfarbe ausgewählt werden:

void mousePressed() {
  zielfarbe = get(mouseX, mouseY);
}

Insgesamt:


import processing.video.*;

Capture kamera;
color zielfarbe;

void setup() {
  size(640, 480);
  kamera = new Capture(this, width, height);
  kamera.start();
  zielfarbe = color(0);
}

void draw() {
  noStroke();
  image(kamera,0,0);
  
  float kleinsteDistanz = width;
  int farbeX = 0;
  int farbeY = 0;

  kamera.loadPixels();
  for (int i=0; i<kamera.width; i++) {
    for (int j=0; j<kamera.height; j++) {
      int pos = i + j*kamera.width;
      color farbe = kamera.pixels[pos];
      float aktuelleDistanz = dist(red(farbe),green(farbe),blue(farbe),red(zielfarbe),green(zielfarbe),blue(zielfarbe));
      if (aktuelleDistanz < kleinsteDistanz) {
        kleinsteDistanz = aktuelleDistanz;
        farbeX = i;
        farbeY = j;
      }
    }
  }
  
  ellipseMode(CENTER);
  fill(zielfarbe, 100);
  ellipse(farbeX, farbeY, 100, 100);
}

void captureEvent(Capture welcheKamera) {
  welcheKamera.read();
}

void mousePressed() {
  zielfarbe = get(mouseX, mouseY);
}

Positionen speichern (Puffer/Buffer)

Das Videobild wird in jedem Frame neu gezeichnet. Daher gehen alle Trackingpunkte aus den vergangenen Frames verloren. Wir speichern daher alle bisherigen Punkte in einer Liste. Die Variablen in der Liste sind Punkte/Vektoren mit 2-Koordinaten: PVector.

ArrayList<PVector> punkte;

...und:

punkte = new ArrayList<PVector>();

In jedem Frame werden die aktuellen Tracking-Koordinaten gespeichert.

punkte.add(new PVector(farbeX, farbeY));

Wenn es zu viele Punkte sind, wird der älteste Punkt verworfen:

if (punkte.size() > 1000) {
  punkte.remove(0);
}

Dann werden alle bisherigen Punkte gezeichnet:

for (int i=0; i<punkte.size()-1; i++) {
  PVector punkt = punkte.get(i);
  PVector naechsterPunkt = punkte.get(i+1);
  stroke(zielfarbe);
  line(punkt.x, punkt.y, naechsterPunkt.x, naechsterPunkt.y);
}

Per Mausklick wird die Zielfarbe bestimmt und die Punkte werden gelöscht.

void mousePressed() {
  zielfarbe = get(mouseX, mouseY);
  punkte.clear();
}

Insgesamt:

import processing.video.*;

Capture kamera;
color zielfarbe;
ArrayList<PVector> punkte;

void setup() {
  size(640, 480);
  kamera = new Capture(this, width, height);
  kamera.start();
  zielfarbe = color(0);
  punkte = new ArrayList<PVector>();
}

void draw() {
  noStroke();
  image(kamera,0,0);
  
  float kleinsteDistanz = width;
  int farbeX = 0;
  int farbeY = 0;

  kamera.loadPixels();
  for (int i=0; i<kamera.width; i++) {
    for (int j=0; j<kamera.height; j++) {
      int pos = i + j*kamera.width;
      color farbe = kamera.pixels[pos];
      float aktuelleDistanz = dist(red(farbe),green(farbe),blue(farbe),red(zielfarbe),green(zielfarbe),blue(zielfarbe));
      if (aktuelleDistanz < kleinsteDistanz) {
        kleinsteDistanz = aktuelleDistanz;
        farbeX = i;
        farbeY = j;
      }
    }
  }
  
  punkte.add(new PVector(farbeX, farbeY));
  if (punkte.size() > 1000) {
    punkte.remove(0);
  }
  
  for (int i=0; i<punkte.size()-1; i++) {
    PVector punkt = punkte.get(i);
    PVector naechsterPunkt = punkte.get(i+1);
    stroke(zielfarbe);
    line(punkt.x, punkt.y, naechsterPunkt.x, naechsterPunkt.y);
  }
  
  ellipseMode(CENTER);
  fill(zielfarbe, 100);
  ellipse(farbeX, farbeY, 100, 100);
}

void captureEvent(Capture welcheKamera) {
  welcheKamera.read();
}

void mousePressed() {
  zielfarbe = get(mouseX, mouseY);
  punkte.clear();
}

Daten glätten (smoothing)

Um zu verhindern, dass die Eingaben zu sehr springen, kann man die Daten glätten. Zum Beispiel die Mausposition. Dazu werden Variablen eingeführt...

float smoothX;
float smoothY;

..., die in jedem Frame in Richtung der Maus verrückt werden - wobei die Annäherung an die Maus vom Abstand abhängt: je weiter entfernt, desto schneller.

smoothX = smoothX + (mouseX - smoothX) * 0.1;
smoothY = smoothY + (mouseY - smoothY) * 0.1;

Die geglätteten Daten können dann statt der Mausposition verwendet werden:

float smoothX;
float smoothY;

void setup() {
  size(600,400);
}

void draw() {
  background(255);
  noStroke();
  fill(0,0,255);
  
  smoothX = smoothX + (mouseX - smoothX) * 0.1;
  smoothY = smoothY + (mouseY - smoothY) * 0.1;
  
  ellipse(smoothX, smoothY, 50, 50);
}

Tracking & Smoothing

Die Positionen aus dem Farbtracking werden genauso geglättet:

smoothX = smoothX + (farbeX - smoothX) * 0.1;
smoothY = smoothY + (farbeY - smoothY) * 0.1;

Also:

import processing.video.*;

Capture kamera;
color zielfarbe;
float smoothX;
float smoothY;

void setup() {
  size(640, 480);
  kamera = new Capture(this, width, height);
  kamera.start();
  zielfarbe = color(0);
}

void draw() {
  noStroke();
  image(kamera,0,0);
  
  float kleinsteDistanz = width;
  int farbeX = 0;
  int farbeY = 0;

  kamera.loadPixels();
  for (int i=0; i<kamera.width; i++) {
    for (int j=0; j<kamera.height; j++) {
      int pos = i + j*kamera.width;
      color farbe = kamera.pixels[pos];
      float aktuelleDistanz = dist(red(farbe),green(farbe),blue(farbe),red(zielfarbe),green(zielfarbe),blue(zielfarbe));
      if (aktuelleDistanz < kleinsteDistanz) {
        kleinsteDistanz = aktuelleDistanz;
        farbeX = i;
        farbeY = j;
      }
    }
  }
  
  smoothX = smoothX + (farbeX - smoothX) * 0.1;
  smoothY = smoothY + (farbeY - smoothY) * 0.1;
  
  ellipseMode(CENTER);
  fill(zielfarbe, 100);
  ellipse(smoothX, smoothY, 100, 100);
  fill(zielfarbe, 200);
  ellipse(farbeX, farbeY, 10, 10);
}

void captureEvent(Capture welcheKamera) {
  welcheKamera.read();
}

void mousePressed() {
  zielfarbe = get(mouseX,mouseY);
}

OpenCV: Gesichtserkennung

Einbinden von OpenCV über Sketch/Add Library.../OpenCV for Processing. OpenCV übernimmt komplizierte Bildverarbeitungs. Zum Beispiel Gesichtserkennung.

Dazu wird die Library eingebunden und gestartet. Zusätzlich benutzen wir wieder Capture, um an das Kamerabild zu kommen, sowie eine Liste für alle bisherigen Punkte. OpenCV.CASCADE_FRONTALFACE legt fest, dass Gesichter von vorne erkannt werden sollen.

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

Capture video;
OpenCV opencv;
ArrayList<PVector> punkte;

void setup() {
  size(640, 480);
  video = new Capture(this, 640/2, 480/2);
  opencv = new OpenCV(this, 640/2, 480/2);
  opencv.loadCascade(OpenCV.CASCADE_FRONTALFACE);  

  video.start();
  punkte = new ArrayList<PVector>();
}

In draw() wird die Library angewiesen, Gesichter zu erkennen (opencv.detect()). Das Ergebnis sind ein oder mehrere Rechtecke. scale(2.0) sorgt dafür, dass Videobild und Rechtecke doppelt so groß gezeichnet werden (640x480 statt 320x240). Die Mitte jedes Rechtecks, wird in der Punktliste abgespeichert.

void draw() {
  opencv.loadImage(video);
  
  scale(2.0);

  image(video, 0, 0);

  noFill();
  stroke(0, 255, 0);
  strokeWeight(3);
  Rectangle[] faces = opencv.detect();
  println(faces.length);

  for (int i = 0; i < faces.length; i++) {
    rect(faces[i].x, faces[i].y, faces[i].width, faces[i].height);
    punkte.add(new PVector(faces[i].x+faces[i].width/2, faces[i].y+faces[i].height/2));
    if (punkte.size() > 1000) {
      punkte.remove(0);
    }
  }
  
  for (int i=0; i < punkte.size()-1; i++) {
    PVector punkt = punkte.get(i);
    PVector naechsterPunkt = punkte.get(i+1);
    strokeWeight(1);
    line(punkt.x, punkt.y, naechsterPunkt.x, naechsterPunkt.y);
  }
}

void captureEvent(Capture c) {
  c.read();
}

Statt des Gesichts können auch Augen...

opencv.loadCascade(OpenCV.CASCADE_NOSE);  

...oder die Nase erkannt werden.

opencv.loadCascade(OpenCV.CASCADE_EYE);  

OpenCV: Motion-Tracking

Auch Motion-Tracking ist mit OpenCV kein Problem. Die Funktion opencv.startBackgroundSubtraction(5, 3, 0.5) sorgt dafür, dass OpenCV jeden Frame mit seinen Vorgängern vergleicht, um Veränderung zu erkennen. findContours() liefert dann die Konturen der Veränderung als Liste.

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

Capture video;
OpenCV opencv;

void setup() {
  size(640, 480, P2D);
  video = new Capture(this, 640, 480);
  opencv = new OpenCV(this, 640, 480);
  
  opencv.startBackgroundSubtraction(5, 3, 0.5);
  
  video.start();
}

void draw() {
  image(video, 0, 0);  
  opencv.loadImage(video);
  
  opencv.updateBackground();
  
  opencv.dilate();
  opencv.erode();

  noFill();
  stroke(255, 0, 0);
  strokeWeight(3);
  ArrayList<Contour> contours = opencv.findContours();
  for (int i=0; i<contours.size(); i++) {
    Contour c = contours.get(i);
    c.draw();
  }
}