Skip to content

Commit

Permalink
Add example for parallel use of Webcam.getImageBytes(), refs #599
Browse files Browse the repository at this point in the history
  • Loading branch information
sarxos committed Dec 13, 2017
1 parent c8484d1 commit 3a238cf
Showing 1 changed file with 347 additions and 0 deletions.
347 changes: 347 additions & 0 deletions webcam-capture/src/example/java/ParallelGetImageBytesExample.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,347 @@
import java.awt.Dimension;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ComponentColorModel;
import java.awt.image.ComponentSampleModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.Exchanger;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;

import javax.swing.JFrame;

import com.github.sarxos.webcam.Webcam;
import com.github.sarxos.webcam.WebcamPanel;
import com.github.sarxos.webcam.WebcamPanel.ImageSupplier;
import com.github.sarxos.webcam.WebcamResolution;


/**
* This example demonstrate how to implement exchange mechanism which will make
* {@link Webcam#getImageBytes()} to run in parallel without causing FPS drop.
*
* @author Bartosz Firyn (sarxos)
*/
public class ParallelGetImageBytesExample {

private static class ByteBufferExchanger extends Exchanger<ByteBuffer> implements AutoCloseable {

private final AsyncWebcamBuffer owner;

public ByteBufferExchanger(final AsyncWebcamBuffer owner) {
this.owner = owner;
}

/**
* Await for new {@link ByteBuffer} to be ready.
*/
public void await() {
awaitAndGet();
}

/**
* Await for new {@link ByteBuffer} to be available and return it.
*
* @return The {@link ByteBuffer}
*/
public ByteBuffer awaitAndGet() {
try {
return exchange(null);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}

/**
* To be used only from {@link AsyncWebcamBuffer}. Please do not invoke this method from the
* other classes.
*
* @param bb the {@link ByteBuffer} to exchange
*/
public void ready(ByteBuffer bb) {
try {
exchange(bb, 0, TimeUnit.SECONDS);
} catch (InterruptedException | TimeoutException e) {
throw new IllegalStateException(e);
}
}

@Override
public void close() {
owner.dispose(this);
}
}

private static class AsyncWebcamBuffer extends Thread {

private final Webcam webcam;
private AtomicReference<ByteBuffer> buffer = new AtomicReference<ByteBuffer>();
private Set<ByteBufferExchanger> exchangers = Collections.synchronizedSet(new LinkedHashSet<ByteBufferExchanger>());
private final int length;

public AsyncWebcamBuffer(Webcam webcam) {
this.webcam = webcam;
this.length = getLength(webcam.getViewSize());
this.setDaemon(true);
this.start();
}

public int getLength(Dimension size) {
return size.width * size.height * 3;
}

public int length() {
return length;
}

public Webcam getWebcam() {
return webcam;
}

@Override
public void run() {
while (webcam.isOpen()) {

// get buffer from webcam (this is direct byte buffer located in off-heap memory)

final ByteBuffer bb = webcam.getImageBytes();
bb.rewind();

buffer.set(bb);

// notify all exchangers

for (ByteBufferExchanger exchanger : exchangers) {
exchanger.ready(bb);
}
}
}

/**
* Be careful when using this reference! It's non synchronized so you have to take special
* care to synchronize and maintain position in buffer to avoid
* {@link BufferUnderflowException}.
*
* @return Non synchronized {@link ByteBuffer}
*/
public ByteBuffer getByteBuffer() {
return buffer.get();
}

/**
* @return New {@link ByteBufferExchanger}
*/
public ByteBufferExchanger exchanger() {
final ByteBufferExchanger exchanger = new ByteBufferExchanger(this);
exchangers.add(exchanger);
return exchanger;
}

public void dispose(ByteBufferExchanger exchanger) {
exchangers.remove(exchanger);
}

/**
* Rewrite {@link ByteBuffer} data to the provided byte[] array.
*
* @param bytes the byte[] array to rewrite {@link ByteBuffer} into
*/
public void read(byte[] bytes) {
final ByteBuffer buffer = getByteBuffer();
// all operations on buffer need to be synchronized
synchronized (buffer) {
buffer.rewind();
buffer.get(bytes);
buffer.rewind();
}
}

/**
* Rewrite {@link ByteBuffer} to newly created byte[] array and return it.
*
* @return Newly created byte[] array with data from {@link ByteBuffer}
*/
public byte[] read() {
final byte[] bytes = new byte[length];
final ByteBuffer buffer = getByteBuffer();
// all operations on buffer need to be synchronized
synchronized (buffer) {
buffer.rewind();
buffer.get(bytes);
buffer.rewind();
}
return bytes;
}

public boolean isReady() {
return buffer.get() != null;
}
}

private static class WebcamPanelImageSupplier implements ImageSupplier {

private final int[] imageOffset = new int[] { 0 };
private final int[] bandOffsets = new int[] { 0, 1, 2 };
private final int[] bits = { 8, 8, 8 };
private final int dataType = DataBuffer.TYPE_BYTE;
private final Dimension size;
private final AsyncWebcamBuffer buffer;
private final ComponentSampleModel sampleModel;
private final ColorSpace colorSpace;
private final ComponentColorModel colorModel;

public WebcamPanelImageSupplier(AsyncWebcamBuffer buffer) {
this.buffer = buffer;
this.size = buffer.getWebcam().getViewSize();
this.sampleModel = new ComponentSampleModel(dataType, size.width, size.height, 3, size.width * 3, bandOffsets);
this.colorSpace = ColorSpace.getInstance(ColorSpace.CS_sRGB);
this.colorModel = new ComponentColorModel(colorSpace, bits, false, false, Transparency.OPAQUE, dataType);
}

@Override
public BufferedImage get() {

while (!buffer.isReady()) {
return null;
}

final byte[] bytes = new byte[size.width * size.height * 3];
final byte[][] data = new byte[][] { bytes };

buffer.read(bytes);

final DataBufferByte dataBuffer = new DataBufferByte(data, bytes.length, imageOffset);
final WritableRaster raster = Raster.createWritableRaster(sampleModel, dataBuffer, null);
final BufferedImage image = new BufferedImage(colorModel, raster, false, null);

return image;
}
}

public static void main(String[] args) throws InterruptedException {

final Dimension size = WebcamResolution.VGA.getSize();

final Webcam webcam = Webcam.getDefault();
webcam.setViewSize(size);
webcam.open();

final AsyncWebcamBuffer buffer = new AsyncWebcamBuffer(webcam);
final ImageSupplier supplier = new WebcamPanelImageSupplier(buffer);

final WebcamPanel panel = new WebcamPanel(webcam, size, true, supplier);
panel.setFPSDisplayed(true);
panel.setDisplayDebugInfo(true);
panel.setImageSizeDisplayed(true);
panel.setMirrored(true);

final JFrame window = new JFrame("Test webcam panel");
window.add(panel);
window.setResizable(true);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.pack();
window.setVisible(true);

// this thread will get underlying ByteBuffer and perform synchronized op to
// get rewrite it into bytes[] array

final Thread t1 = new Thread() {

@Override
public void run() {

// make sure to close exchanger because you will end up with memory leak

try (final ByteBufferExchanger exchanger = buffer.exchanger()) {

while (webcam.isOpen()) {

long t1 = System.currentTimeMillis();
final ByteBuffer bb = exchanger.awaitAndGet();
long t2 = System.currentTimeMillis();

System.out.println(getName() + " : " + 1000 / (t2 - t1 + 1));

final byte[] bytes = new byte[buffer.length];

// make sure to synchronize or you will end up

synchronized (bb) {
bb.rewind();
bb.get(bytes);
bb.rewind();
}

// do processing on bytes[] array
}
}
}
};
t1.start();

// this thread will await for underlying ByteBuffer to be ready and perform
// synchronized op to get rewrite it into new bytes[] array

final Thread t2 = new Thread() {

@Override
public void run() {

try (final ByteBufferExchanger exchanger = buffer.exchanger()) {
while (webcam.isOpen()) {

long t1 = System.currentTimeMillis();
exchanger.await();
long t2 = System.currentTimeMillis();

System.out.println(getName() + " : " + 1000 / (t2 - t1 + 1));

final byte[] bytes = buffer.read();

// do processing on bytes[] array
}
}
}
};
t2.start();

// this thread will await for underlying ByteBuffer to be ready and perform
// synchronized op to get rewrite it into pre-created bytes[] array

final Thread t3 = new Thread() {

@Override
public void run() {

try (final ByteBufferExchanger exchanger = buffer.exchanger()) {

final byte[] bytes = new byte[buffer.length()];

while (webcam.isOpen()) {

long t1 = System.currentTimeMillis();
exchanger.await();
long t2 = System.currentTimeMillis();

System.out.println(getName() + " : " + 1000 / (t2 - t1 + 1));

buffer.read(bytes);

// do processing on bytes[] array
}
}
}
};
t3.start();
}
}

0 comments on commit 3a238cf

Please sign in to comment.