A lightweight Java Swing component to display multiple OpenCV camera streams in a grid, with optional frame processing and snapshot capture.
- Built with JDK 1.8
- Uses OpenCV Java bindings
- Packaged with Maven
Display multiple camera feeds in a Swing UI.
- ✅ Display N camera devices in a grid layout
- ✅ Optional per-frame processing via
IProcessCapture - ✅ Safe start/stop capture threads
- ✅ Snapshot capture without interfering with the streaming thread (recommended)
- JDK 1.8
- OpenCV Java (example: OpenCV 4.6.0)
- OS-supported camera backend (e.g., DirectShow on Windows)
Note: If you use
System.load(...), the DLL must match your OS architecture (x64 vs x86) and your JDK architecture.
mvn clean packageThis demo uses the safer approach:
DeviceGroup.openAll()starts streamingVideoPanel.saveSnapshot(...)saves images without callingVideoCapture.read()from the UI thread
package io.github.beatum;
import io.github.beatum.video.DeviceGroup;
import io.github.beatum.video.VideoPanel;
import org.opencv.core.Core;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
/**
* Demo Application
*/
public class App {
static {
// Recommended: use loadLibrary if java.library.path is configured
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
// If you prefer a hard-coded DLL path, you can use:
// System.load("D:\Happy\Software\Opencv4.6.0\opencv\build\java\x64\opencv_java460.dll");
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
JFrame window = new JFrame("Demo Application");
window.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
window.setBounds(200, 200, 1024, 768);
window.setLayout(new BorderLayout());
// ---------- Input panel ----------
JPanel inputPanel = new JPanel(new SpringLayout());
JLabel labelOfPath = new JLabel("Path:", JLabel.TRAILING);
JTextField textFieldOfPath = new JTextField(50);
textFieldOfPath.setText("D:\test");
labelOfPath.setLabelFor(textFieldOfPath);
JButton btnStart = new JButton("Start");
inputPanel.add(labelOfPath);
inputPanel.add(textFieldOfPath);
inputPanel.add(btnStart);
SpringUtilities.makeCompactGrid(inputPanel, 1, 3, 6, 6, 6, 6);
// ---------- Device grid ----------
JPanel container = new JPanel(new GridLayout(2, 3, 6, 6));
DeviceGroup deviceGroup = new DeviceGroup(container, 6);
// ---------- Capture button ----------
JButton btnCapture = new JButton("Capture");
btnCapture.setEnabled(false);
btnStart.addActionListener(e -> {
deviceGroup.openAll();
btnStart.setEnabled(false);
btnCapture.setEnabled(true);
});
window.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
deviceGroup.close(); // stop + release
System.out.println("Window closed, devices released.");
}
});
// Capture snapshots in background (avoid freezing UI)
btnCapture.addActionListener(e -> {
String pathText = textFieldOfPath.getText().trim();
if (pathText.isEmpty()) {
JOptionPane.showMessageDialog(window, "Path is empty!", "Error", JOptionPane.ERROR_MESSAGE);
return;
}
Path dir = Path.of(pathText);
btnCapture.setEnabled(false);
SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() {
@Override
protected Void doInBackground() throws Exception {
Files.createDirectories(dir);
List<VideoPanel> viewers = deviceGroup.getViewers();
String timestamp = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
for (int i = 0; i < viewers.size(); i++) {
VideoPanel viewer = viewers.get(i);
Path file = dir.resolve("cam" + i + "_" + timestamp + ".png");
boolean ok = viewer.saveSnapshot(file);
if (!ok) {
System.err.println("Snapshot failed: device " + i);
}
}
return null;
}
@Override
protected void done() {
btnCapture.setEnabled(true);
JOptionPane.showMessageDialog(window, "Capture completed.", "Info", JOptionPane.INFORMATION_MESSAGE);
}
};
worker.execute();
});
// ---------- Layout ----------
window.add(inputPanel, BorderLayout.NORTH);
window.add(container, BorderLayout.CENTER);
window.add(btnCapture, BorderLayout.SOUTH);
window.setVisible(true);
});
}
}Avoid reading frames from VideoCapture on the UI thread when your viewer is already streaming:
vc.read(frame); // Not recommended if VideoPanel already reads in another threadReading from the same VideoCapture in multiple threads can cause race conditions and unstable behavior.
Use the viewer API instead:
viewer.saveSnapshot(file); // SafeYou can add a per-frame filter:
// Example: attach filter to the first viewer (if available)
if (!deviceGroup.getViewers().isEmpty()) {
deviceGroup.getViewers().get(0).setImageProcessingFilter(input -> {
// Do OpenCV processing here (e.g. blur, edges, etc.)
return input; // return processed Mat
});
}Tip: Prefer in-place processing to reduce allocations and improve performance.
