Processing Expert Article 2

Computer Vision with OpenCV

Use the OpenCV for Processing library to detect faces, subtract backgrounds, trace contours, and attract a particle swarm to live webcam shapes.

⏱ 45 min OpenCV computer vision webcam particles contours

The OpenCV for Processing library, written by Greg Borenstein, wraps the OpenCV 2.x C++ library in a Processing-friendly Java API. It gives you face detection, background subtraction, contour finding, and optical flow without touching native code directly.

Installation

Open the Contribution Manager (Sketch > Import Library > Manage Libraries), search for OpenCV for Processing, and install. The library installs native binaries for the current platform automatically.

After installation, verify your setup:

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

Capture cam;
OpenCV opencv;

void setup() {
  size(640, 480);
  cam    = new Capture(this, 640, 480);
  opencv = new OpenCV(this, 640, 480);
  cam.start();
}

void draw() {
  if (cam.available()) {
    cam.read();
    opencv.loadImage(cam);
    image(opencv.getOutput(), 0, 0);
  }
}

opencv.getOutput() returns a PImage of whatever the last OpenCV operation produced. opencv.getInput() returns the unmodified source.

Grayscale Conversion

Many CV algorithms require a single-channel image. Call opencv.gray() after loading:

opencv.loadImage(cam);
opencv.gray();
image(opencv.getOutput(), 0, 0);  // grayscale PImage

Chain operations; each modifies the internal buffer in place. Use opencv.getSnapshot() to capture the intermediate state as a PImage before the next operation overwrites it.

Face Detection

OpenCV ships Haar cascade classifiers. opencv.detect() returns an array of Rectangle objects (java.awt.Rectangle):

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

Capture cam;
OpenCV opencv;
Rectangle[] faces;

void setup() {
  size(640, 480);
  cam    = new Capture(this, 640, 480);
  opencv = new OpenCV(this, 640, 480);
  opencv.loadCascade(OpenCV.CASCADE_FRONTALFACE);
  cam.start();
}

void draw() {
  if (cam.available()) cam.read();
  opencv.loadImage(cam);
  faces = opencv.detect();
  image(cam, 0, 0);

  noFill();
  stroke(0, 255, 0);
  strokeWeight(3);
  for (Rectangle r : faces) {
    rect(r.x, r.y, r.width, r.height);
  }
}

Detection is CPU-heavy; limit to every other frame or reduce the image scale with opencv.setGray(opencv.getGray().get(0, 0, width/2, height/2)) for faster throughput.

Background Subtraction

Background subtraction isolates moving objects from a static scene. OpenCV’s MOG2 algorithm maintains a statistical model of the background:

void setup() {
  // ... camera setup as above ...
  opencv.startBackgroundSubtraction(5, 3, 0.5);
  // args: history frames, variance threshold, shadow detection threshold
}

void draw() {
  if (cam.available()) cam.read();
  opencv.loadImage(cam);
  opencv.updateBackground();
  // getOutput() is now the foreground mask (white = motion)
  image(opencv.getOutput(), 0, 0);
}

The foreground mask is a binary image you can feed directly into contour finding.

Contour Finding

opencv.findContours() returns a List<Contour>. Each Contour offers getPoints() (a list of PVector) and area/bounding-box accessors:

import gab.opencv.*;
import processing.video.*;
import java.util.List;

Capture cam;
OpenCV opencv;
List<Contour> contours;

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

void draw() {
  if (cam.available()) cam.read();
  opencv.loadImage(cam);
  opencv.updateBackground();
  opencv.dilate();   // fill gaps
  opencv.erode();    // remove noise
  contours = opencv.findContours(true, true);
  // args: findHoles, approximateSingleContour

  background(0);
  noFill();
  stroke(255, 100, 0);
  strokeWeight(1.5);

  for (Contour c : contours) {
    if (c.area() < 800) continue;   // skip tiny blobs
    c.draw();                        // draws directly to sketch
  }
}

Optical Flow

The opencv.calculateOpticalFlow() method uses Lucas-Kanade sparse optical flow to track feature points. Call it after opencv.loadImage():

opencv.loadImage(cam);
opencv.calculateOpticalFlow();

// Draw flow vectors
PVector[] flow = opencv.getAverageFlow();
// flow is a grid of PVectors — x/y velocity per cell
// (grid size configurable via opencv.setOpticalFlowGridSize(int))

opencv.drawOpticalFlow();  // convenience: draws arrows onto the sketch

Full Example: Contour-Based Particle Attractor

This sketch finds foreground contours, extracts their points, and uses them as attractors for a particle swarm. Particles steer toward the nearest contour vertex each frame.

import gab.opencv.*;
import processing.video.*;
import java.util.List;
import java.util.ArrayList;

Capture cam;
OpenCV opencv;
List<Contour> contours;
ArrayList<Particle> particles;
PVector[] attractors = new PVector[0];

final int NUM_PARTICLES = 800;

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

  particles = new ArrayList<Particle>();
  for (int i = 0; i < NUM_PARTICLES; i++) {
    particles.add(new Particle(random(width), random(height)));
  }
}

void draw() {
  if (cam.available()) {
    cam.read();
    opencv.loadImage(cam);
    opencv.updateBackground();
    opencv.dilate();
    opencv.erode();
    contours = opencv.findContours(true, true);

    // Rebuild attractor list from all large-enough contour points
    ArrayList<PVector> pts = new ArrayList<PVector>();
    for (Contour c : contours) {
      if (c.area() < 1000) continue;
      for (PVector p : c.getPoints()) {
        pts.add(p.copy());
      }
    }
    attractors = pts.toArray(new PVector[0]);
  }

  // Dim previous frame for trail effect
  fill(0, 25);
  noStroke();
  rect(0, 0, width, height);

  // Update and draw particles
  stroke(255, 160, 50, 200);
  strokeWeight(1.2);
  noFill();
  for (Particle p : particles) {
    p.attract(attractors);
    p.update();
    p.draw();
  }

  // Overlay faint live camera
  tint(255, 40);
  image(cam, 0, 0);
  noTint();
}

class Particle {
  PVector pos, vel, acc;
  float maxSpeed = 3.5;
  float maxForce = 0.18;

  Particle(float x, float y) {
    pos = new PVector(x, y);
    vel = PVector.random2D().mult(2);
    acc = new PVector();
  }

  void attract(PVector[] targets) {
    if (targets.length == 0) {
      // Drift to random position when no contours
      PVector wander = PVector.random2D().mult(0.5);
      acc.add(wander);
      return;
    }

    // Find nearest attractor
    PVector nearest = null;
    float minDist   = Float.MAX_VALUE;
    for (PVector t : targets) {
      float d = PVector.dist(pos, t);
      if (d < minDist) { minDist = d; nearest = t; }
    }

    PVector desired = PVector.sub(nearest, pos);
    float d = desired.mag();

    if (d < 5) {
      // Arrived — pick a random nearby attractor next frame
      acc.add(PVector.random2D().mult(maxSpeed));
      return;
    }

    float speed = map(d, 0, 200, 0.5, maxSpeed);
    desired.setMag(speed);
    PVector steer = PVector.sub(desired, vel);
    steer.limit(maxForce);
    acc.add(steer);
  }

  void update() {
    vel.add(acc);
    vel.limit(maxSpeed);
    pos.add(vel);
    acc.mult(0);

    // Wrap around edges
    if (pos.x < 0) pos.x = width;
    if (pos.x > width)  pos.x = 0;
    if (pos.y < 0) pos.y = height;
    if (pos.y > height) pos.y = 0;
  }

  void draw() {
    point(pos.x, pos.y);
  }
}

Performance Tuning

findContours() allocates new ArrayList objects every call. If garbage collection causes frame drops, consider running the OpenCV processing in a background thread and synchronizing the attractor array with synchronized blocks. For 60 fps with 1,000+ particles, run OpenCV at 15 fps (every fourth frame) and interpolate attractor positions between updates.

Background subtraction quality depends heavily on lighting consistency. Prevent flickering fluorescent lights from corrupting the background model by increasing the history parameter above 20 frames, or by calling opencv.startBackgroundSubtraction(30, 16, 0.0) to disable shadow detection.