Wednesday, May 4, 2016

JavaFX and webcams

There is an excellent webcam library available for Java. Unfortunately, when trying to integrate it with a JavaFX application I got into some kind of threading trouble. I was able to escape this by creating my own thread to deal with the webcam.

My demo is available on github as an Eclipse project. You'll need to download the aforementioned library and set up its three JAR files in your build path to make it work. Beyond that, it should be straightforward.

Here are some highlights from the code I wrote.

The ImageThread class handles interactions with the webcam. It requires a function to actually perform the rendering on the interface, but aside from that it takes care of dealing with the webcam. I separated out the rendering function so that anyone can just borrow this class wholesale for whatever application they are writing. Note that it is pretty easy to add additional image processing to the threaded loop.

The implementation is straightforward. When the thread starts, it opens the webcam and begins counting frames and time. Whenever the webcam has a new image available, it grabs the image and calls the renderer with both the image and the current frame rate.

Be sure that your rendering function invokes Platform.runLater() to avoid threading problems!

package edu.hendrix.webcamdemo;

import java.awt.image.BufferedImage;
import java.util.function.BiConsumer;

import com.github.sarxos.webcam.Webcam;

public class ImageThread extends Thread {
  private boolean quit = false;
  private int frames;
  private BiConsumer<Bufferedimage,Double> renderer;
 
  public ImageThread(BiConsumer<Bufferedimage,Double> renderer) {
    this.renderer = renderer;
  }
 
  public void quit() {quit = true;}
 
  @Override
  public void run() {
    Webcam webcam = Webcam.getDefault();
    webcam.open();
    frames = 0;
    long start = System.currentTimeMillis();
    while (!quit) {
      if (webcam.isImageNew()) {
        BufferedImage img = webcam.getImage();
        frames += 1;
        renderer.accept(img, 1000.0*frames / (System.currentTimeMillis() - start));
      }
    }
    webcam.close();
  }
}

The WebcamDemoController class mediates between the GUI and the ImageThread. This gives a nice example as to how to provide a rendering function for the GUI.

package edu.hendrix.webcamdemo;

import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.TextField;

import java.awt.image.BufferedImage;

public class WebcamDemoController implements Quittable {
  @FXML Canvas image;
 
  @FXML TextField frameRate;
 
  ImageThread renderer;
 
  int frames;
 
  public static void render(BufferedImage img, Canvas canv) {
    double cellWidth = canv.getWidth() / img.getWidth();
    double cellHeight = canv.getHeight() / img.getHeight();
    GraphicsContext g = canv.getGraphicsContext2D();
    for (int x = 0; x < img.getWidth(); x++) {
      for (int y = 0; y < img.getHeight(); y++) {
        g.setFill(ColorChannel.buildColorFrom(img.getRGB(x, y)));
        g.fillRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight);
      }
    }
  }
 
  @FXML
  void initialize() {
    renderer = new ImageThread((img, rate) -> Platform.runLater(() -> {
      render(img, image);
      frameRate.setText(String.format("%4.2f", rate));
    }));
    renderer.start();
    frameRate.setEditable(false);
  }

  @Override
  public void quit() {
    renderer.quit();
  }
}

Finally, I also created an Enum to handle the ARGB conversions. Since I'm interested in image processing, it is helpful to be able to directly access the pertinent color channels.

package edu.hendrix.webcamdemo;

import java.util.EnumMap;

import javafx.scene.paint.Color;

public enum ColorChannel {
  ALPHA {
    @Override
    public int shift() {
      return 24;
    }
  }, RED {
    @Override
    public int shift() {
      return 16;
    }
  }, GREEN {
    @Override
    public int shift() {
      return 8;
    }
  }, BLUE {
    @Override
    public int shift() {
      return 0;
    }
  };
 
  abstract public int shift();
 
  public int extractFrom(int pixel) {
    return (pixel >> shift()) & 0xFF;
  }
 
  public static int buildPixelFrom(EnumMap colors) {
    int pixel = 0;
    for (ColorChannel c: values()) {
      pixel += (colors.containsKey(c) ? colors.get(c) : 0) << c.shift();
    }
    return pixel;
  }
 
  public static javafx.scene.paint.Color buildColorFrom(int argb) {
    return Color.rgb(ColorChannel.RED.extractFrom(argb), 
        ColorChannel.GREEN.extractFrom(argb), 
        ColorChannel.BLUE.extractFrom(argb), 
        ColorChannel.ALPHA.extractFrom(argb) / 255.0);
  }
 
  public static EnumMap<ColorChannel,Integer> buildChannelsFrom(int argb) {
    EnumMap<ColorChannel,Integer> channels = new EnumMap<>(ColorChannel.class);
    for (ColorChannel c: values()) {
      channels.put(c, c.extractFrom(argb));
    }
    return channels;
  }
}

No comments:

Post a Comment