11 Commits

Author SHA1 Message Date
3eaf6f0303 fix
Signed-off-by: rattatwinko <seppmutterman@gmail.com>
2026-01-20 08:37:48 +01:00
11c5aa9115 Merge branch 'experimenting'
Signed-off-by: rattatwinko <seppmutterman@gmail.com>

# Conflicts:
#	src/main/java/io/swtc/Main.java
2026-01-20 08:28:10 +01:00
55474092e3 some more optimizing!
now runs much better with lower power systems as we refactored code to firstly rule out double invokelaters, and we optimized the capture loop iteslf!

and we introduced a ShowError class which will be refactored and used quite thoroughly!

Signed-off-by: rattatwinko <seppmutterman@gmail.com>
2026-01-20 08:25:26 +01:00
b49cc8b2f0 refactors 2026-01-19 18:33:42 +01:00
565a4f3cf3 fixed some functionallity added back some which was removed cause of refactoring ...
some new refactoring to make the UI code cleaner

Signed-off-by: rattatwinko <seppmutterman@gmail.com>
2026-01-19 13:04:40 +01:00
f6ee3e915e refactored shitty unusable code
Signed-off-by: rattatwinko <seppmutterman@gmail.com>
2026-01-19 12:00:50 +01:00
41fbf62757 just some theme changes, i like this one more 2026-01-18 22:10:37 +01:00
c393e05bb1 testing some new stuff! 2026-01-18 21:00:24 +01:00
e7a3d98dd0 Quality of Life changes to the viewer
changed:
/ SwingIFrame ; Some new methodes for setting bg color, and fullscreen, keybinds are "F11" and "B"
/ WebcamCaptureLoop.java ; Safety is key!

---
rattatwinko
2026-01-14 20:55:45 +01:00
ccc3d264f7 a new windowing system!
add:
+ CameraPanel ; To make life easier for coders
+ SwingIFrame ; Which is now our main UI component
+ VideoRecorder ; A helper Class for SwingIFrame to record cameras!

changed:
/ SwingCCTVManager ; changes for the new UI Component

deprecation:
/-/ AutoGainProcessor ; cause it isnt needed anymore, back then this was needed cause we opened the webcams manually (color wise)

---
rattatwinko
2026-01-13 20:52:32 +01:00
b767ba27b3 JOptionPane for Errors, general refactoring, deprecated class removed (CameraRenderer) moved into Swing now
modified:
/ SwingCCTVManager ; Error Pane, refactoring
/ WebcamCaptureLoop ; cleanup method for closing cameras reliably, and some MessageDialogs for error handling (just fails lol)
/ SwingCameraWindow ; refactor some legacy code into a "modern" lambda function , Message Dialog for error handling , and some g2d stuff (paintComponent)

removed:
- CameraRenderer.java ; Deprecated Component, was used for SWT GL Surfaces. We dont do that now!

---

rattatwinko
2026-01-13 17:33:47 +01:00
34 changed files with 1693 additions and 341 deletions

View File

@@ -122,5 +122,6 @@
<artifactId>jcodec-javase</artifactId>
<version>0.2.5</version>
</dependency>
</dependencies>
</project>

View File

@@ -1,11 +1,22 @@
package io.swtc;
import javax.swing.*;
import io.swtc.proccessing.ui.ShowError;
public class Main {
public static void main(String[] args) {
for (int i = 0; i < args.length; i++) {
System.out.println("Arg " + i + ": " + args[i]);
}
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
ShowError.warning(null,"LaF Warn","LaF");
}
SwingCCTVManager.main(null);
}
}

View File

@@ -31,23 +31,37 @@ public class SwingCCTVManager {
private final JFrame frame;
private final JTable deviceTable;
private final DefaultTableModel tableModel;
private Timer autoRefreshTimer;
private final SwingIFrame IFrame;
public SwingCCTVManager() {
frame = new JFrame("dashboard");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(1000, 600);
this.IFrame = new SwingIFrame();
this.IFrame.show();
String[] columns = {"Status", "Device Name", "Type", "Resolution", "Address"};
tableModel = new DefaultTableModel(columns, 0) {
@Override public boolean isCellEditable(int r, int c) { return false; }
@Override
public boolean isCellEditable(int r, int c) {
return false;
}
};
deviceTable = new JTable(tableModel);
deviceTable.setRowSelectionAllowed(true);
deviceTable.setFocusable(true);
deviceTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
setupTableAppearance();
deviceTable.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
deviceTable.requestFocusInWindow();
if (e.getClickCount() == 2 && deviceTable.getSelectedRow() != -1) {
launchSelected();
}
@@ -75,29 +89,31 @@ public class SwingCCTVManager {
}
private void setupTableAppearance() {
deviceTable.getColumnModel().getColumn(0).setMaxWidth(80); // Status column
deviceTable.getColumnModel().getColumn(0).setMaxWidth(80);
deviceTable.setRowHeight(30);
// Custom Renderer for Status Colors
deviceTable.getColumnModel().getColumn(0).setCellRenderer(new DefaultTableCellRenderer() {
@Override
public Component getTableCellRendererComponent(JTable t, Object v, boolean s, boolean f, int r, int c) {
Component comp = super.getTableCellRendererComponent(t, v, s, f, r, c);
if ("ONLINE".equals(v)) comp.setForeground(new Color(0, 150, 0));
else comp.setForeground(Color.RED);
setHorizontalAlignment(JLabel.CENTER);
return comp;
}
});
deviceTable.getColumnModel().getColumn(0)
.setCellRenderer(new DefaultTableCellRenderer() {
@Override
public Component getTableCellRendererComponent(
JTable t, Object v, boolean s, boolean f, int r, int c) {
Component comp = super.getTableCellRendererComponent(t, v, s, f, r, c);
setHorizontalAlignment(JLabel.CENTER);
comp.setForeground("ONLINE".equals(v)
? new Color(0, 150, 0)
: Color.RED);
return comp;
}
});
}
private void startAutoRefresh() {
autoRefreshTimer = new Timer(5000, e -> refreshTable());
autoRefreshTimer.start();
new Timer(5000, e -> refreshTable()).start();
}
private void refreshTable() {
int selectedRow = deviceTable.getSelectedRow();
int[] selectedRows = deviceTable.getSelectedRows();
tableModel.setRowCount(0);
List<Webcam> webcams = Webcam.getWebcams();
@@ -115,14 +131,18 @@ public class SwingCCTVManager {
});
}
if (selectedRow != -1 && selectedRow < tableModel.getRowCount()) {
deviceTable.setRowSelectionInterval(selectedRow, selectedRow);
for (int r : selectedRows) {
if (r < tableModel.getRowCount()) {
deviceTable.addRowSelectionInterval(r, r);
}
}
}
private void showContextMenu(MouseEvent e) {
int row = deviceTable.rowAtPoint(e.getPoint());
deviceTable.setRowSelectionInterval(row, row);
if (row >= 0) {
deviceTable.setRowSelectionInterval(row, row);
}
JPopupMenu menu = new JPopupMenu();
JMenuItem launch = new JMenuItem("Launch Live Stream");
@@ -142,17 +162,18 @@ public class SwingCCTVManager {
if (row == -1) return;
String name = (String) tableModel.getValueAt(row, 1);
Webcam selected = Webcam.getWebcams().stream()
Webcam.getWebcams().stream()
.filter(w -> w.getName().equals(name))
.findFirst().orElse(null);
if (selected != null) {
new Thread(() -> new SwingCameraWindow(selected).open()).start();
}
.findFirst()
.ifPresent(cam ->
new Thread(() -> IFrame.addCameraInternalFrame(cam)).start()
);
}
private void deleteSelected() {
int row = deviceTable.getSelectedRow();
if (row == -1) return;
String name = (String) tableModel.getValueAt(row, 1);
if (name.toLowerCase().contains("usb")) return;
@@ -164,8 +185,19 @@ public class SwingCCTVManager {
private static void loadSavedCameras() {
for (CameraConfig config : CameraSettings.load()) {
try {
IpCamDeviceRegistry.register(config.getName(), config.getUrl(), IpCamMode.PUSH);
} catch (MalformedURLException e) { e.printStackTrace(); }
IpCamDeviceRegistry.register(
config.getName(),
config.getUrl(),
IpCamMode.PUSH
);
} catch (MalformedURLException e) {
JOptionPane.showMessageDialog(
null,
"Malformed URL\n" + e.getMessage(),
"Malformed URL",
JOptionPane.ERROR_MESSAGE
);
}
}
}
@@ -173,10 +205,16 @@ public class SwingCCTVManager {
JPanel p = new JPanel(new GridLayout(2, 2, 5, 5));
JTextField n = new JTextField();
JTextField u = new JTextField();
p.add(new JLabel("Name:")); p.add(n);
p.add(new JLabel("URL:")); p.add(u);
int result = JOptionPane.showConfirmDialog(frame, p, "Register IP Camera", JOptionPane.OK_CANCEL_OPTION);
p.add(new JLabel("Name:"));
p.add(n);
p.add(new JLabel("URL:"));
p.add(u);
int result = JOptionPane.showConfirmDialog(
frame, p, "Register IP Camera", JOptionPane.OK_CANCEL_OPTION
);
if (result == JOptionPane.OK_OPTION) {
try {
IpCamDeviceRegistry.register(n.getText(), u.getText(), IpCamMode.PUSH);
@@ -196,4 +234,4 @@ public class SwingCCTVManager {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new SwingCCTVManager().open());
}
}
}

View File

@@ -1,82 +0,0 @@
package io.swtc;
import com.github.sarxos.webcam.Webcam;
import io.swtc.proccessing.WebcamCaptureLoop;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
public class SwingCameraWindow {
private final JFrame frame;
private final CameraPanel cameraPanel;
private final WebcamCaptureLoop captureLoop;
public SwingCameraWindow(Webcam webcam) {
this.frame = new JFrame("scctv@" + webcam.getName());
this.frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
this.frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
// clean shit up
frame.dispose();
}
});
this.cameraPanel = new CameraPanel();
this.frame.add(cameraPanel);
this.frame.pack();
this.frame.setSize(640, 480);
this.captureLoop = new WebcamCaptureLoop(webcam, (BufferedImage img) -> {
SwingUtilities.invokeLater(() -> {
cameraPanel.setImage(img);
});
});
this.frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
captureLoop.stop();
}
});
}
public void open() {
frame.setVisible(true);
captureLoop.start();
}
private static class CameraPanel extends JPanel {
private BufferedImage currentImage;
public void setImage(BufferedImage img) {
this.currentImage = img;
this.repaint(); // Triggers paintComponent
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (currentImage != null) {
// Draw the image scaled to the panel size
g.drawImage(currentImage, 0, 0, getWidth(), getHeight(), null);
}
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
// init in edt
Webcam webcam = Webcam.getDefault();
if (webcam != null) {
SwingCameraWindow window = new SwingCameraWindow(webcam);
window.open();
} else {
System.err.println("No webcam found!");
}
});
}
}

View File

@@ -0,0 +1,175 @@
package io.swtc;
import com.github.sarxos.webcam.Webcam;
import io.swtc.proccessing.ui.iframe.*; // Your custom frames
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.HashMap;
import java.util.Map;
public class SwingIFrame {
private final JFrame mainFrame;
private final DesktopPane desktopPane;
private final Map<JInternalFrame, EffectsPanelFrame> cameraToEffects = new HashMap<>();
private boolean fullscreen = false;
private Rectangle windowedBounds;
private boolean blackbg = false;
private final Color bgcolor = Color.decode("#336B6A");
private final Color defDesktopBg = Color.WHITE;
private final JPopupMenu popupMenu = new JPopupMenu();
public SwingIFrame() {
mainFrame = new JFrame("Viewer");
mainFrame.setSize(1280, 720);
mainFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
desktopPane = new DesktopPane(cameraToEffects);
desktopPane.setBackground(defDesktopBg);
mainFrame.add(desktopPane, BorderLayout.CENTER);
setupFullscreenToggle();
setupBlackBg();
initPopupMenu();
desktopPane.addMouseListener(popupListener());
}
public void addCameraInternalFrame(Webcam webcam) {
CameraInternalFrame cameraFrame = new CameraInternalFrame(webcam, this::handleEffectsRequest);
EffectsPanelFrame effectsFrame = new EffectsPanelFrame(
"Effects - " + webcam.getName(),
cameraFrame.getCameraPanel()
);
cameraToEffects.put(cameraFrame, effectsFrame);
int offset = desktopPane.getAllFrames().length * 15;
cameraFrame.setLocation(50 + offset, 50 + offset);
effectsFrame.setLocation(700 + offset, 50 + offset);
effectsFrame.setVisible(false);
cameraFrame.addInternalFrameListener(new javax.swing.event.InternalFrameAdapter() {
@Override
public void internalFrameClosing(javax.swing.event.InternalFrameEvent e) {
EffectsPanelFrame ef = cameraToEffects.remove(cameraFrame);
if (ef != null) ef.dispose();
desktopPane.forgetFrame(cameraFrame);
cameraFrame.dispose();
}
});
desktopPane.add(cameraFrame);
desktopPane.add(effectsFrame);
// Attach popup menu to frames and content
MouseAdapter popup = popupListener();
cameraFrame.addMouseListener(popup);
cameraFrame.getContentPane().addMouseListener(popup);
effectsFrame.addMouseListener(popup);
effectsFrame.getContentPane().addMouseListener(popup);
cameraFrame.setVisible(true);
}
private void handleEffectsRequest(CameraInternalFrame source) {
EffectsPanelFrame effectsFrame = cameraToEffects.get(source);
if (effectsFrame != null) {
effectsFrame.setVisible(true);
try {
effectsFrame.setSelected(true);
effectsFrame.toFront();
} catch (java.beans.PropertyVetoException ex) {
JOptionPane.showMessageDialog(null, "Focus Error: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
}
}
private MouseAdapter popupListener() {
return new MouseAdapter() {
private void showPopup(MouseEvent e) {
if (e.isPopupTrigger()) { // cross-platform trigger
popupMenu.show(e.getComponent(), e.getX(), e.getY());
}
}
@Override public void mousePressed(MouseEvent e) { showPopup(e); }
@Override public void mouseReleased(MouseEvent e) { showPopup(e); }
};
}
private void initPopupMenu() {
popupMenu.removeAll(); // clean slate
JCheckBoxMenuItem fullscreenItem = new JCheckBoxMenuItem("Fullscreen");
fullscreenItem.addActionListener(e -> toggleFullscreen());
popupMenu.addPopupMenuListener(new javax.swing.event.PopupMenuListener() {
@Override
public void popupMenuWillBecomeVisible(javax.swing.event.PopupMenuEvent e) {
fullscreenItem.setState(fullscreen);
}
@Override public void popupMenuWillBecomeInvisible(javax.swing.event.PopupMenuEvent e) {}
@Override public void popupMenuCanceled(javax.swing.event.PopupMenuEvent e) {}
});
popupMenu.add(fullscreenItem);
}
private void setupBlackBg() {
JRootPane root = mainFrame.getRootPane();
root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke("B"), "toggleBlackBg");
root.getActionMap().put("toggleBlackBg", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
toggleBackground();
}
});
}
private void setupFullscreenToggle() {
JRootPane root = mainFrame.getRootPane();
root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke("F11"), "toggleFullscreen");
root.getActionMap().put("toggleFullscreen", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
toggleFullscreen();
}
});
}
private void toggleBackground() {
desktopPane.setBackground(blackbg ? defDesktopBg : bgcolor);
blackbg = !blackbg;
desktopPane.repaint();
}
/** Toggle fullscreen mode */
private void toggleFullscreen() {
if (!fullscreen) {
windowedBounds = mainFrame.getBounds();
mainFrame.dispose();
mainFrame.setUndecorated(true);
mainFrame.setExtendedState(JFrame.MAXIMIZED_BOTH);
mainFrame.setVisible(true);
} else {
mainFrame.dispose();
mainFrame.setUndecorated(false);
mainFrame.setExtendedState(JFrame.NORMAL);
mainFrame.setBounds(windowedBounds);
mainFrame.setVisible(true);
}
fullscreen = !fullscreen;
}
public void show() {
mainFrame.setLocationRelativeTo(null);
mainFrame.setVisible(true);
}
}

View File

@@ -5,7 +5,6 @@ public class CameraConfig {
public String url;
// Default constructor for Jackson
public CameraConfig() {}
public CameraConfig(String name, String url) {
this.name = name;

View File

@@ -1,6 +1,22 @@
package io.swtc.proccessing;
public class AutoGainProcessor {
/**
* Gray World Algorithm.
*
* <p>
* This class is an implementation of the Gray World algorithm, an automatic
* white balancing method.
* </p>
*
* <p>
* See:
* <a href="https://acorn.stanford.edu/psych221/projects/2000/trek/GWimages.html">
* Stanford explanation
* </a>
* </p>
*/
public class AWBProccessor {
public float[] calculateAutoGains(int[] pixels) {
long rSum = 0, gSum = 0, bSum = 0;

View File

@@ -0,0 +1,69 @@
package io.swtc.proccessing;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
/**
* Enhanced CameraPanel with support for custom image processors
*/
public class CameraPanel extends JPanel {
private volatile BufferedImage sourceImage;
private volatile BufferedImage processedImage;
private Function<BufferedImage, BufferedImage> imageProcessor;
private final AtomicBoolean repaintScheduled = new AtomicBoolean(false);
public CameraPanel() {
setBackground(Color.BLACK);
setPreferredSize(new Dimension(640, 480));
}
public void setImage(BufferedImage img) {
sourceImage = img;
if (repaintScheduled.compareAndSet(false, true)) {
SwingUtilities.invokeLater(() -> {
repaintScheduled.set(false);
repaint();
});
}
}
public void setImageProcessor(Function<BufferedImage, BufferedImage> processor) {
this.imageProcessor = processor;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
BufferedImage src = sourceImage;
if (src == null) return;
BufferedImage img = src;
if (imageProcessor != null) {
// we only apply the proccessor if it is present
// that is due too CPU Usage ; This project was built for a DUAL Core CPU, not a quad or even 6 Core CPU
img = imageProcessor.apply(src);
}
processedImage = img;
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(
RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR
);
g2d.drawImage(img, 0, 0, getWidth(), getHeight(), null);
}
public BufferedImage getCurrentProcessedImage() {
return processedImage;
}
}

View File

@@ -1,96 +0,0 @@
package io.swtc.proccessing;
import org.eclipse.swt.opengl.GLCanvas;
import org.eclipse.swt.widgets.Composite;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GL11;
import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class CameraRenderer {
private final GLCanvas canvas;
private int textureId = -1;
private ByteBuffer pixelBuffer;
private final AutoGainProcessor gainProcessor;
public CameraRenderer(Composite parent, org.eclipse.swt.opengl.GLData data) {
this.canvas = new GLCanvas(parent, 0, data);
this.gainProcessor = new AutoGainProcessor();
// Initialize OpenGL context immediately
this.canvas.setCurrent();
GL.createCapabilities();
initGL();
}
public GLCanvas getCanvas() {
return canvas;
}
private void initGL() {
GL11.glEnable(GL11.GL_TEXTURE_2D);
textureId = GL11.glGenTextures();
}
public void render(BufferedImage img) {
if (canvas.isDisposed()) return;
canvas.setCurrent();
int width = img.getWidth();
int height = img.getHeight();
int[] rgbArray = new int[width * height];
// this is hellishly unefficcient but who cares.
img.getRGB(0, 0, width, height, rgbArray, 0, width);
float[] gains = gainProcessor.calculateAutoGains(rgbArray);
int bufferSize = width * height * 3;
if (pixelBuffer == null || pixelBuffer.capacity() < bufferSize) {
pixelBuffer = ByteBuffer.allocateDirect(bufferSize);
pixelBuffer.order(ByteOrder.nativeOrder());
}
pixelBuffer.clear();
for (int pixel : rgbArray) {
int r = (pixel >> 16) & 0xFF;
int g = (pixel >> 8) & 0xFF;
int b = pixel & 0xFF;
r = Math.min(255, (int)(r * gains[0]));
g = Math.min(255, (int)(g * gains[1]));
b = Math.min(255, (int)(b * gains[2]));
pixelBuffer.put((byte) r);
pixelBuffer.put((byte) g);
pixelBuffer.put((byte) b);
}
pixelBuffer.flip();
// this is just gl stuff
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);
GL11.glViewport(0, 0, canvas.getClientArea().width, canvas.getClientArea().height);
GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureId);
GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGB, width, height,
0, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, pixelBuffer);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
GL11.glColor3f(1.0f, 1.0f, 1.0f);
GL11.glBegin(GL11.GL_QUADS);
GL11.glTexCoord2f(0, 0); GL11.glVertex2f(-1, 1);
GL11.glTexCoord2f(1, 0); GL11.glVertex2f(1, 1);
GL11.glTexCoord2f(1, 1); GL11.glVertex2f(1, -1);
GL11.glTexCoord2f(0, 1); GL11.glVertex2f(-1, -1);
GL11.glEnd();
canvas.swapBuffers();
}
}

View File

@@ -0,0 +1,62 @@
package io.swtc.proccessing;
import java.util.stream.IntStream;
public class ColorProccessor {
public void process(int[] pixels, EffectState state, float[] awbGains) {
float[] effectiveGains = (state.awbEnabled() && awbGains != null) ? awbGains : new float[]{1f, 1f, 1f};
// Parallel processing for O(N) pixel operations
IntStream.range(0, pixels.length).parallel().forEach(i -> {
int rgb = pixels[i];
int r = (rgb >> 16) & 0xFF;
int g = (rgb >> 8) & 0xFF;
int b = rgb & 0xFF;
if (state.awbEnabled()) {
float s = state.awbStrength() / 100f;
r = Math.min(255, (int)(r * (1 + (effectiveGains[0] - 1) * s)));
g = Math.min(255, (int)(g * (1 + (effectiveGains[1] - 1) * s)));
b = Math.min(255, (int)(b * (1 + (effectiveGains[2] - 1) * s)));
}
// 2. Temperature & Tint
if (state.temperature() != 0) {
float factor = state.temperature() / 100f;
r = ImageUtils.clamp(r + (int)(factor * 30));
b = ImageUtils.clamp(b - (int)(factor * 30));
}
if (state.tint() != 0) {
g = ImageUtils.clamp(g + (int)((state.tint() / 100f) * 20));
}
// 3. Saturation
if (state.saturation() != 100) {
float factor = state.saturation() / 100f;
float gray = (r + g + b) / 3f;
r = ImageUtils.clamp((int)(gray + (r - gray) * factor));
g = ImageUtils.clamp((int)(gray + (g - gray) * factor));
b = ImageUtils.clamp((int)(gray + (b - gray) * factor));
}
// 4. Shadows & Highlights
if (state.shadows() != 0 || state.highlights() != 0) {
float lum = (r + g + b) / 765f; // 765 = 3 * 255
if (lum < 0.5f && state.shadows() != 0) {
int adj = (int)((state.shadows() / 100f) * (1 - lum * 2) * 50);
r = ImageUtils.clamp(r + adj);
g = ImageUtils.clamp(g + adj);
b = ImageUtils.clamp(b + adj);
} else if (lum > 0.5f && state.highlights() != 0) {
int adj = (int)((state.highlights() / 100f) * (lum * 2 - 1) * 50);
r = ImageUtils.clamp(r + adj);
g = ImageUtils.clamp(g + adj);
b = ImageUtils.clamp(b + adj);
}
}
pixels[i] = (r << 16) | (g << 8) | b;
});
}
}

View File

@@ -0,0 +1,51 @@
package io.swtc.proccessing;
import java.util.stream.IntStream;
public class DenoiseProccessor {
public int[] process(int[] srcPixels, int width, int height, float strength) {
if (strength <= 0) return srcPixels;
int[] dstPixels = new int[srcPixels.length];
int[] tempPixels = new int[srcPixels.length];
int radius = (int) (strength / 100f * 2) + 1;
// Pass 1: Horizontal
IntStream.range(0, height).parallel().forEach(y ->
blurLine(srcPixels, tempPixels, width, height, y, radius, true)
);
// Pass 2: Vertical
IntStream.range(0, width).parallel().forEach(x ->
blurLine(tempPixels, dstPixels, width, height, x, radius, false)
);
return dstPixels;
}
private void blurLine(int[] src, int[] dest, int w, int h, int lineIndex, int radius, boolean horizontal) {
int length = horizontal ? w : h;
int limit = length - 1;
for (int i = 0; i < length; i++) {
long rSum = 0, gSum = 0, bSum = 0;
int count = 0;
int start = Math.max(0, i - radius);
int end = Math.min(limit, i + radius);
for (int k = start; k <= end; k++) {
int idx = horizontal ? (lineIndex * w + k) : (k * w + lineIndex);
int rgb = src[idx];
rSum += (rgb >> 16) & 0xFF;
gSum += (rgb >> 8) & 0xFF;
bSum += rgb & 0xFF;
count++;
}
int targetIdx = horizontal ? (lineIndex * w + i) : (i * w + lineIndex);
dest[targetIdx] = ((int)(rSum/count) << 16) | ((int)(gSum/count) << 8) | (int)(bSum/count);
}
}
}

View File

@@ -0,0 +1,6 @@
package io.swtc.proccessing;
public record EffectState(boolean awbEnabled, int awbStrength, boolean dnrEnabled, int dnrSpatial, int dnrTemporal,
int temperature, int tint, int saturation, int shadows, int highlights, int sharpness,
boolean edgeEnhance) {
}

View File

@@ -0,0 +1,135 @@
package io.swtc.proccessing;
import io.swtc.proccessing.ui.UIFactory;
import io.swtc.proccessing.ui.sections.*;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
/**
* This is basically the UI Commander, this orchestrates the whole ui package into one useful
* feature
*
* Without this the code will look like ass
* */
public class FilterPanel extends JPanel {
private final CameraPanel cameraPanel;
private final AWBProccessor awbProcessor;
private final PresetLibrary presetLibrary;
private WhiteBalanceSection awbSection;
private DNRSection dnrSection;
private ColorSection colorSection;
private DetailSection detailSection;
private float[] currentGains = {1f, 1f, 1f};
public FilterPanel(CameraPanel cameraPanel) {
this.cameraPanel = cameraPanel;
this.awbProcessor = new AWBProccessor();
this.presetLibrary = new PresetLibrary();
initializePanel();
buildUI();
initGlobalListeners();
}
private void initializePanel() {
setLayout(new BorderLayout());
setBorder(new EmptyBorder(10, 10, 10, 10));
}
private void buildUI() {
PresetSection presetSection = new PresetSection(presetLibrary, this::applyPresetToUI, this::getCurrentState);
add(presetSection, BorderLayout.NORTH);
JPanel scrollContainer = new JPanel();
scrollContainer.setLayout(new BoxLayout(scrollContainer, BoxLayout.Y_AXIS));
awbSection = new WhiteBalanceSection(this::performOneTimeBalance, this::applyToCamera);
dnrSection = new DNRSection(this::applyToCamera);
colorSection = new ColorSection(this::applyToCamera);
detailSection = new DetailSection(this::applyToCamera);
scrollContainer.add(awbSection);
scrollContainer.add(Box.createRigidArea(new Dimension(0, 10)));
scrollContainer.add(dnrSection);
scrollContainer.add(Box.createRigidArea(new Dimension(0, 10)));
scrollContainer.add(colorSection);
scrollContainer.add(Box.createRigidArea(new Dimension(0, 10)));
scrollContainer.add(detailSection);
add(UIFactory.createTransparentScrollPane(scrollContainer), BorderLayout.CENTER);
JPanel footer = new JPanel(new GridLayout(1, 1, 5, 5));
footer.setBorder(new EmptyBorder(10, 0, 0, 0));
footer.add(UIFactory.createActionButton("Reset All Factory Settings", e -> resetUI()));
add(footer, BorderLayout.SOUTH);
}
public EffectState getCurrentState() {
return new EffectState(
awbSection.isEnabled(), awbSection.getStrength(),
dnrSection.isEnabled(), dnrSection.getSpatial(), dnrSection.getTemporal(),
colorSection.getTemp(), colorSection.getTint(), colorSection.getSaturation(),
colorSection.getShadows(), colorSection.getHighlights(),
detailSection.getSharpness(), detailSection.isEdgeEnhanceEnabled()
);
}
private void applyToCamera() {
if (cameraPanel == null) return;
EffectState state = getCurrentState();
cameraPanel.setImageProcessor(img ->
ImageEffectEngine.applyEffects(img, state, currentGains)
);
}
/**
* call awb stuff
*/
private void performOneTimeBalance() {
java.awt.image.BufferedImage img = cameraPanel.getCurrentProcessedImage();
if (img != null) {
int[] pixels = img.getRGB(0, 0, img.getWidth(), img.getHeight(), null, 0, img.getWidth());
currentGains = awbProcessor.calculateAutoGains(pixels);
applyToCamera();
}
}
/**
* Update for a specified state
* */
private void applyPresetToUI(EffectState s) {
awbSection.setState(s.awbEnabled(), s.awbStrength());
dnrSection.setState(s.dnrEnabled(), s.dnrSpatial(), s.dnrTemporal());
colorSection.setState(s.temperature(), s.tint(), s.saturation(), s.shadows(), s.highlights());
detailSection.setState(s.sharpness(), s.edgeEnhance());
// Reset gains if AWB is disabled in the preset
if (!s.awbEnabled()) currentGains = new float[]{1f, 1f, 1f};
applyToCamera();
}
private void resetUI() {
applyPresetToUI(new EffectState(false, 100, false, 30, 50, 0, 0, 100, 0, 0, 100, false));
}
private void initGlobalListeners() {
java.awt.event.ComponentAdapter repaintListener = new java.awt.event.ComponentAdapter() {
@Override public void componentMoved(java.awt.event.ComponentEvent e) { updateOverlay(); }
@Override public void componentResized(java.awt.event.ComponentEvent e) { updateOverlay(); }
};
this.addComponentListener(repaintListener);
if (cameraPanel != null) cameraPanel.addComponentListener(repaintListener);
}
private void updateOverlay() {
RootPaneContainer root = (RootPaneContainer) SwingUtilities.getWindowAncestor(this);
if (root != null) root.getGlassPane().repaint();
}
}

View File

@@ -0,0 +1,40 @@
package io.swtc.proccessing;
import java.awt.image.BufferedImage;
public class ImageEffectEngine {
private static final ColorProccessor colorProcessor = new ColorProccessor();
private static final DenoiseProccessor denoiseProcessor = new DenoiseProccessor();
private static final SharpnessProccessor sharpnessProcessor = new SharpnessProccessor();
public static BufferedImage applyEffects(BufferedImage img, EffectState state, float[] currentGains) {
if (img == null) return null;
// 1. Extract raw data (High Performance)
int width = img.getWidth();
int height = img.getHeight();
int[] pixels = ImageUtils.getPixels(img);
// NOTE: If we want to avoid modifying the original image's backing array,
// we should clone 'pixels' here. If in-place modification is okay, we proceed.
// int[] workingPixels = pixels.clone();
int[] workingPixels = pixels; // Assuming in-place is fine for performance
// 2. Apply Color Pipeline (In-Place)
colorProcessor.process(workingPixels, state, currentGains);
// 3. Apply Sharpness (Returns new array if applied)
if (state.sharpness() != 100 || state.edgeEnhance()) {
workingPixels = sharpnessProcessor.process(workingPixels, width, height, state.sharpness(), state.edgeEnhance());
}
// 4. Apply Denoise (Returns new array if applied)
if (state.dnrEnabled()) {
workingPixels = denoiseProcessor.process(workingPixels, width, height, state.dnrSpatial());
}
// 5. Reconstruct Image
return ImageUtils.createFromPixels(workingPixels, width, height);
}
}

View File

@@ -0,0 +1,36 @@
package io.swtc.proccessing;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
public class ImageUtils {
public static int[] getPixels(BufferedImage img) {
return ((DataBufferInt) ensureIntRGB(img).getRaster().getDataBuffer()).getData();
}
public static BufferedImage createFromPixels(int[] pixels, int width, int height) {
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
int[] dst = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
System.arraycopy(pixels, 0, dst, 0, pixels.length);
return img;
}
public static BufferedImage ensureIntRGB(BufferedImage img) {
if (img.getType() == BufferedImage.TYPE_INT_RGB) {
return img;
}
BufferedImage newImg = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics g = newImg.getGraphics();
g.drawImage(img, 0, 0, null);
g.dispose();
return newImg;
}
public static int clamp(int val) {
if (val < 0) return 0;
if (val > 255) return 255;
return val;
}
}

View File

@@ -0,0 +1,46 @@
package io.swtc.proccessing;
import java.util.HashMap;
import java.util.Map;
public class PresetLibrary {
private final Map<String, EffectState> presets = new HashMap<>();
public PresetLibrary() {
presets.put("Natural", new EffectState(
false, 100, false, 20, 40, 0, 0, 100, 0, 0, 100, false
));
presets.put("Vivid", new EffectState(
true, 100, false, 15, 30, 10, 5, 130, 0, 0, 120, true
));
presets.put("Portrait", new EffectState(
true, 80, true, 40, 50, -5, 10, 95, 10, -5, 90, false
));
presets.put("Low Light", new EffectState(
true, 100, true, 60, 70, 0, 0, 110, 20, -10, 80, false
));
presets.put("High Contrast", new EffectState(
false, 100, false, 25, 35, 0, 0, 120, -20, 20, 130, true
));
presets.put("Cinematic", new EffectState(
true, 70, true, 30, 45, -15, -5, 90, -10, 5, 110, false
));
}
public void savePreset(String name, EffectState state) {
presets.put(name, state);
}
public EffectState get(String name) {
return presets.get(name);
}
public String[] getPresetNames() {
return presets.keySet().toArray(new String[0]);
}
}

View File

@@ -0,0 +1,70 @@
package io.swtc.proccessing;
import java.util.stream.IntStream;
public class SharpnessProccessor {
public int[] process(int[] srcPixels, int width, int height, float amount, boolean edgeEnhance) {
if (amount == 0 && !edgeEnhance) return srcPixels;
int[] dstPixels = new int[srcPixels.length];
// Normalization setup
float centerWeight = edgeEnhance ? 5f : 9f;
float neighborWeight = -1f;
float strength = (amount / 100f);
// Adjust strength scaling to match your original "amount - 1 / 8f" logic if needed,
// but typically sharpness is 0.0 to 1.0.
// Adapting to your specific previous math:
float weightFactor = (amount / 100f - 1) / 8f;
// Parallel loop skipping borders
IntStream.range(1, height - 1).parallel().forEach(y -> {
int yOffset = y * width;
for (int x = 1; x < width - 1; x++) {
int i = yOffset + x;
float rAcc = 0, gAcc = 0, bAcc = 0;
// Center
int pC = srcPixels[i];
float wC = centerWeight * weightFactor + 1.0f;
rAcc += ((pC >> 16) & 0xFF) * wC;
gAcc += ((pC >> 8) & 0xFF) * wC;
bAcc += (pC & 0xFF) * wC;
// Neighbors (North, South, East, West)
int[] neighbors = {
srcPixels[i - width], srcPixels[i + width],
srcPixels[i - 1], srcPixels[i + 1]
};
float wN = neighborWeight * weightFactor;
for(int p : neighbors) {
rAcc += ((p >> 16) & 0xFF) * wN;
gAcc += ((p >> 8) & 0xFF) * wN;
bAcc += (p & 0xFF) * wN;
}
// Diagonals (only if not edge enhance mode, per your original code)
if (!edgeEnhance) {
int[] diags = {
srcPixels[i - width - 1], srcPixels[i - width + 1],
srcPixels[i + width - 1], srcPixels[i + width + 1]
};
for(int p : diags) {
rAcc += ((p >> 16) & 0xFF) * wN;
gAcc += ((p >> 8) & 0xFF) * wN;
bAcc += (p & 0xFF) * wN;
}
}
dstPixels[i] = (ImageUtils.clamp((int)rAcc) << 16) |
(ImageUtils.clamp((int)gAcc) << 8) |
ImageUtils.clamp((int)bAcc);
}
});
return dstPixels;
}
}

View File

@@ -1,59 +1,122 @@
package io.swtc.proccessing;
import com.github.sarxos.webcam.Webcam;
import com.github.sarxos.webcam.WebcamException;
import com.github.sarxos.webcam.WebcamResolution;
import java.awt.Dimension;
import java.awt.image.BufferedImage;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
import java.util.function.Consumer;
import javax.swing.*;
import io.swtc.proccessing.ui.ShowError;
public class WebcamCaptureLoop {
private final Webcam webcam;
private final Consumer<BufferedImage> onFrameCaptured;
private volatile boolean running = false;
private final AtomicBoolean cleanedUp = new AtomicBoolean(false);
private final int targetframes = 20;
public WebcamCaptureLoop(Webcam webcam, Consumer<BufferedImage> onFrameCaptured) {
this.webcam = webcam;
this.onFrameCaptured = onFrameCaptured;
// this is some of the most stupid shit ive seen in years, this is needed for opening the stream.
// the webcam package may not know the res before its opened and well this is before we open it. that is below in the thread.
// so this freaks out and doesnt want to cooperate.
if (!webcam.getName().toLowerCase().contains("ipcam")) {
Dimension[] sizes = webcam.getViewSizes();
webcam.setViewSize(sizes[sizes.length - 1]);
Dimension vga = WebcamResolution.VGA.getSize();
if (isSizeSupported(webcam, vga)) { // we dont do stupid shit anymore! cause we are smarter than before...
webcam.setViewSize(vga);
} else {
Dimension[] dimensions = webcam.getViewSizes();
webcam.setViewSize(dimensions[dimensions.length - 1]);
}
}
private boolean isSizeSupported(Webcam webcam, Dimension vga) {
for (Dimension d: webcam.getViewSizes()) {
if (
d.width == vga.width && d.height == vga.height
) return true; // this should return 640x480 :)
}
return false;
}
public void start() {
if (running) return;
running = true;
Thread captureThread = new Thread(() -> {
// this is where we open it. if the res isnt known then it fucks up.
webcam.open();
while (running) {
BufferedImage img = webcam.getImage();
if (img != null) {
//System.err.println(img);
onFrameCaptured.accept(img);
}
try {
Thread.sleep(15);
} catch (InterruptedException e) {
break;
}
}
try {
webcam.close();
} catch (IllegalStateException e) {
e.printStackTrace();
if (!webcam.isOpen()) webcam.open(); // open if not
long frameDurationLimitns = 1_000_000_000L / targetframes; // we use NanoSeconds cause its more accurate
while (running) {
long startTime = System.nanoTime();
BufferedImage img = webcam.getImage();
if (img != null)
// there is no need for a invoke later swing wise!
onFrameCaptured.accept(img);
long timespent = System.nanoTime() - startTime;
long sleeptime = frameDurationLimitns - timespent;
if (sleeptime > 0) {
LockSupport.parkNanos(sleeptime);
} else {
// do a yield to prevent ui freezes
Thread.yield();
}
}
} catch (Exception e) {
ShowError.warning(
null,
"Exception" + e,
"Exception"
);
} finally {
cleanup();
}
});
captureThread.setName("cam_cap_thread");
}, "cam_cap_thread");
captureThread.start();
}
/**
* Safely release webcam
*/
private synchronized void cleanup() {
if (!cleanedUp.compareAndSet(false, true)) {
return;
}
boolean success = false;
try {
if (webcam.isOpen())
webcam.close();
success = true;
} catch (WebcamException exception) {
SwingUtilities.invokeLater(() ->
JOptionPane.showMessageDialog(
null,
"Cleanup failed \nWebCamException@"+exception.getMessage(),
"WebCamException",
JOptionPane.WARNING_MESSAGE // changed to warning, its better tbh
));
} finally {
if (!success)
cleanedUp.set(false);
}
}
/**
* Arguments: null (or just none)
* What does this method do? : Sets CameraThreads running flag to false and calls cleanup();
*/
public void stop() {
running = false;
}

View File

@@ -0,0 +1,29 @@
package io.swtc.proccessing.ui;
import javax.swing.*;
import java.awt.*;
public abstract class FilterSection extends JPanel {
public FilterSection(String title) {
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
setBorder(BorderFactory.createTitledBorder(
BorderFactory.createEtchedBorder(), title));
}
/**
* Helper to create and add a slider in one line
*/
protected LabeledSlider addSlider(String label, int min, int max, int val, String unit) {
LabeledSlider ls = new LabeledSlider(label, min, max, val, unit);
add(ls);
return ls;
}
/**
* Helper to add spacing between elements
*/
protected void addPadding(int height) {
add(Box.createRigidArea(new Dimension(0, height)));
}
}

View File

@@ -0,0 +1,45 @@
package io.swtc.proccessing.ui;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
public class LabeledSlider extends JPanel {
private final JSlider slider;
private final JLabel label;
private final String title;
private final String unit;
public LabeledSlider(String title, int min, int max, int value, String unit) {
this.title = title;
this.unit = unit;
setLayout(new BorderLayout());
label = new JLabel(title + ": " + value + unit);
slider = new JSlider(min, max, value);
// Internal listener to update the text label as user drags
slider.addChangeListener(e -> updateLabel());
add(label, BorderLayout.NORTH);
add(slider, BorderLayout.CENTER);
setBorder(new EmptyBorder(5, 0, 5, 0));
}
private void updateLabel() {
label.setText(title + ": " + slider.getValue() + unit);
}
public int getValue() { return slider.getValue(); }
public void setValue(int val) {
slider.setValue(val);
updateLabel();
}
public JSlider getSlider() { return slider; }
public void addChangeListener(javax.swing.event.ChangeListener cl) {
slider.addChangeListener(cl);
}
}

View File

@@ -0,0 +1,38 @@
package io.swtc.proccessing.ui;
import javax.swing.*;
import java.awt.*;
public final class ShowError {
private ShowError() {
// we dont instantiate cause it causes some errors
}
public static void error(Component parent, String title, String message) {
JOptionPane.showMessageDialog(
parent,
message,
title,
JOptionPane.ERROR_MESSAGE
);
}
public static void warning(Component parent, String title, String message) {
JOptionPane.showMessageDialog(
parent,
message,
title,
JOptionPane.WARNING_MESSAGE
);
}
public static void info(Component parent, String title, String message) {
JOptionPane.showMessageDialog(
parent,
message,
title,
JOptionPane.INFORMATION_MESSAGE
);
}
}

View File

@@ -0,0 +1,21 @@
package io.swtc.proccessing.ui;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
public class UIFactory {
public static JButton createActionButton(String text, java.awt.event.ActionListener listener) {
JButton btn = new JButton(text);
btn.addActionListener(listener);
return btn;
}
public static JScrollPane createTransparentScrollPane(Component view) {
JScrollPane scroll = new JScrollPane(view);
scroll.setBorder(null);
scroll.getVerticalScrollBar().setUnitIncrement(16);
return scroll;
}
}

View File

@@ -0,0 +1,132 @@
package io.swtc.proccessing.ui.iframe;
import com.github.sarxos.webcam.Webcam;
import io.swtc.proccessing.WebcamCaptureLoop;
import io.swtc.proccessing.CameraPanel;
import io.swtc.recording.VideoRecorder;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.function.Consumer;
public class CameraInternalFrame extends JInternalFrame {
private final WebcamCaptureLoop captureLoop;
private final CameraPanel cameraPanel;
private final VideoRecorder videoRecorder; // Instance of the recorder
private JButton recordBtn;
public CameraInternalFrame(Webcam webcam, Consumer<CameraInternalFrame> onOpenEffects) {
super(webcam.getName(), true, true, true, true);
this.cameraPanel = new CameraPanel();
this.videoRecorder = new VideoRecorder(); // Initialize recorder
// Initialize capture loop
this.captureLoop = new WebcamCaptureLoop(webcam, img ->
SwingUtilities.invokeLater(() -> cameraPanel.setImage(img))
);
setupUI(onOpenEffects);
captureLoop.start();
}
private void setupUI(Consumer<CameraInternalFrame> onOpenEffects) {
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.addTab("View", cameraPanel);
tabbedPane.addTab("Capture", new RecordingPane(cameraPanel, videoRecorder));
tabbedPane.addTab("Effects", new JPanel());
tabbedPane.addChangeListener(e -> {
if (tabbedPane.getSelectedIndex() == 2) {
tabbedPane.setSelectedIndex(0);
onOpenEffects.accept(this);
}
});
add(tabbedPane);
setSize(600, 500);
}
private JPanel createCapturePanel() {
JPanel panel = new JPanel(new GridLayout(2, 1, 10, 10)); // Changed to Grid for better button layout
panel.setBorder(new EmptyBorder(15, 15, 15, 15));
JButton screenshotBtn = new JButton("Take Screenshot");
screenshotBtn.addActionListener(e -> saveSnapshot());
recordBtn = new JButton("Start Recording");
recordBtn.addActionListener(e -> toggleRecording());
panel.add(screenshotBtn);
panel.add(recordBtn);
// Wrap in a wrapper to prevent buttons from stretching too much
JPanel wrapper = new JPanel(new BorderLayout());
wrapper.add(panel, BorderLayout.NORTH);
return wrapper;
}
private void toggleRecording() {
if (!videoRecorder.isRecording()) {
startVideo();
} else {
stopVideo();
}
}
private void startVideo() {
try {
File file = new File(System.getProperty("user.home"), "vid_" + System.currentTimeMillis() + ".mp4");
videoRecorder.startRecording(cameraPanel, file);
recordBtn.setText("Stop Recording");
recordBtn.setForeground(Color.RED);
} catch (IOException ex) {
showError("Failed to start recording", ex);
}
}
private void stopVideo() {
try {
File savedFile = videoRecorder.stopRecording();
recordBtn.setText("Start Recording");
recordBtn.setForeground(Color.BLACK);
JOptionPane.showMessageDialog(this, "Video saved to: " + savedFile.getAbsolutePath());
} catch (IOException ex) {
showError("Failed to stop recording safely", ex);
}
}
private void saveSnapshot() {
BufferedImage img = cameraPanel.getCurrentProcessedImage();
if (img != null) {
try {
File file = new File(System.getProperty("user.home"), "snap_" + System.currentTimeMillis() + ".png");
ImageIO.write(img, "PNG", file);
} catch (Exception ex) {
showError("Snapshot failed", ex);
}
}
}
private void showError(String title, Exception ex) {
JOptionPane.showMessageDialog(this, title + "\n" + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
public CameraPanel getCameraPanel() { return cameraPanel; }
@Override
public void dispose() {
// Safety check: stop recording if the window is closed
if (videoRecorder.isRecording()) {
try { videoRecorder.stopRecording(); } catch (IOException ignored) {}
}
captureLoop.stop();
super.dispose();
}
}

View File

@@ -0,0 +1,66 @@
package io.swtc.proccessing.ui.iframe;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.CubicCurve2D;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
public class DesktopPane extends JDesktopPane {
private final Map<JInternalFrame, EffectsPanelFrame> connections;
private final Map<JInternalFrame, Color> connectionColors = new HashMap<>();
public DesktopPane(Map<JInternalFrame, EffectsPanelFrame> connections) {
this.connections = connections;
}
private Color getConnectionColor(JInternalFrame frame) {
return connectionColors.computeIfAbsent(frame, k -> {
Random rand = new Random();
return new Color(rand.nextInt(256), rand.nextInt(256), rand.nextInt(256), 200);
});
}
public void forgetFrame(JInternalFrame frame) {
connectionColors.remove(frame);
}
@Override
protected void paintChildren(Graphics g) {
super.paintChildren(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
for (Map.Entry<JInternalFrame, EffectsPanelFrame> entry : connections.entrySet()) {
JInternalFrame camera = entry.getKey();
EffectsPanelFrame effects = entry.getValue();
if (camera.isVisible() && effects.isVisible() && !camera.isIcon() && !effects.isIcon()) {
g2d.setColor(getConnectionColor(camera));
drawBezierConnection(g2d, camera, effects);
}
}
g2d.dispose();
}
private void drawBezierConnection(Graphics2D g2d, JInternalFrame from, JInternalFrame to) {
Rectangle f = from.getBounds();
Rectangle t = to.getBounds();
int x1 = f.x + f.width;
int y1 = f.y + (f.height / 2);
int x2 = t.x;
int y2 = t.y + (t.height / 2);
int ctrlOffset = Math.min(Math.abs(x2 - x1) / 2, 150);
CubicCurve2D curve = new CubicCurve2D.Double(x1, y1, x1 + ctrlOffset, y1, x2 - ctrlOffset, y2, x2, y2);
//g2d.setStroke(new BasicStroke(3f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
//g2d.draw(curve);
// Terminals
//g2d.fillOval(x1 - 5, y1 - 5, 10, 10);
//g2d.fillOval(x2 - 5, y2 - 5, 10, 10);
}
}

View File

@@ -0,0 +1,14 @@
package io.swtc.proccessing.ui.iframe;
import io.swtc.proccessing.CameraPanel;
import io.swtc.proccessing.FilterPanel;
import javax.swing.*;
public class EffectsPanelFrame extends JInternalFrame {
public EffectsPanelFrame(String title, CameraPanel cameraPanel) {
super(title, true, true, true, true);
setDefaultCloseOperation(HIDE_ON_CLOSE);
add(new FilterPanel(cameraPanel));
setSize(350, 600);
}
}

View File

@@ -0,0 +1,126 @@
package io.swtc.proccessing.ui.iframe;
import io.swtc.proccessing.CameraPanel;
import io.swtc.recording.VideoRecorder;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
public class RecordingPane extends JPanel {
private final VideoRecorder videoRecorder;
private final CameraPanel cameraPanel;
private JTextField pathField;
private JButton recordBtn;
private JLabel statusLabel;
private File outputDirectory;
public RecordingPane(CameraPanel cameraPanel, VideoRecorder videoRecorder) {
this.cameraPanel = cameraPanel;
this.videoRecorder = videoRecorder;
this.outputDirectory = new File(System.getProperty("user.home"));
setLayout(new GridBagLayout());
JPanel contentPanel = new JPanel();
contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
contentPanel.setPreferredSize(new Dimension(400, 250));
// Add the functional sections
contentPanel.add(createStoragePanel());
contentPanel.add(Box.createVerticalStrut(15));
contentPanel.add(createActionPanel());
contentPanel.add(Box.createVerticalStrut(15));
contentPanel.add(createStatusPanel());
add(contentPanel);
}
private JPanel createStoragePanel() {
JPanel panel = new JPanel(new BorderLayout(5, 5));
panel.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createEtchedBorder(), "Storage Settings", TitledBorder.LEFT, TitledBorder.TOP));
pathField = new JTextField(outputDirectory.getAbsolutePath());
pathField.setEditable(false);
JButton browseBtn = new JButton("Browse...");
browseBtn.addActionListener(e -> {
JFileChooser chooser = new JFileChooser(outputDirectory);
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
outputDirectory = chooser.getSelectedFile();
pathField.setText(outputDirectory.getAbsolutePath());
}
});
panel.add(pathField, BorderLayout.CENTER);
panel.add(browseBtn, BorderLayout.EAST);
return panel;
}
private JPanel createActionPanel() {
JPanel panel = new JPanel(new GridLayout(1, 2, 10, 10));
recordBtn = new JButton("start recording");
recordBtn.addActionListener(e -> toggleRecording());
JButton snapBtn = new JButton("take snapshot");
snapBtn.addActionListener(e -> takeSnapshot());
panel.add(recordBtn);
panel.add(snapBtn);
return panel;
}
private JPanel createStatusPanel() {
JPanel panel = new JPanel(new FlowLayout(FlowLayout.CENTER));
statusLabel = new JLabel("");
statusLabel.setForeground(Color.DARK_GRAY);
panel.add(statusLabel);
return panel;
}
private void toggleRecording() {
if (!videoRecorder.isRecording()) {
try {
File file = new File(outputDirectory, "vid_" + System.currentTimeMillis() + ".mp4");
videoRecorder.startRecording(cameraPanel, file);
recordBtn.setText("stop recording");
statusLabel.setText("recording");
} catch (IOException ex) {
showError("Start Error", ex);
}
} else {
try {
File saved = videoRecorder.stopRecording();
recordBtn.setText("Start Recording");
statusLabel.setText("Status: Saved " + saved.getName());
} catch (IOException ex) {
showError("Stop Error", ex);
}
}
}
private void takeSnapshot() {
BufferedImage img = cameraPanel.getCurrentProcessedImage();
if (img != null) {
try {
File file = new File(outputDirectory, "snap_" + System.currentTimeMillis() + ".png");
ImageIO.write(img, "PNG", file);
statusLabel.setText("captured");
} catch (IOException ex) {
showError("Snapshot Error", ex);
}
}
}
private void showError(String title, Exception ex) {
JOptionPane.showMessageDialog(this, ex.getMessage(), title, JOptionPane.ERROR_MESSAGE);
}
}

View File

@@ -0,0 +1,37 @@
package io.swtc.proccessing.ui.sections;
import io.swtc.proccessing.ui.FilterSection;
import io.swtc.proccessing.ui.LabeledSlider;
public class ColorSection extends FilterSection {
private final LabeledSlider temp, tint, sat, shadows, highlights;
public ColorSection(Runnable onUpdate) {
super("Color Grading");
temp = addSlider("Temperature", -100, 100, 0, "");
tint = addSlider("Tint", -100, 100, 0, "");
sat = addSlider("Saturation", 0, 200, 100, "%");
shadows = addSlider("Shadows", -100, 100, 0, "");
highlights = addSlider("Highlights", -100, 100, 0, "");
LabeledSlider[] sliders = {temp, tint, sat, shadows, highlights};
for (LabeledSlider s : sliders) {
s.addChangeListener(e -> { if(!s.getSlider().getValueIsAdjusting()) onUpdate.run(); });
}
}
public int getTemp() { return temp.getValue(); }
public int getTint() { return tint.getValue(); }
public int getSaturation() { return sat.getValue(); }
public int getShadows() { return shadows.getValue(); }
public int getHighlights() { return highlights.getValue(); }
public void setState(int t, int ti, int s, int sh, int hi) {
temp.setValue(t);
tint.setValue(ti);
sat.setValue(s);
shadows.setValue(sh);
highlights.setValue(hi);
}
}

View File

@@ -0,0 +1,56 @@
package io.swtc.proccessing.ui.sections;
import io.swtc.proccessing.ui.FilterSection;
import io.swtc.proccessing.ui.LabeledSlider;
import javax.swing.*;
public class DNRSection extends FilterSection {
private final JCheckBox enabled;
private final LabeledSlider spatial;
private final LabeledSlider temporal;
public DNRSection(Runnable onUpdate) {
super("3D Denoise (DNR)");
enabled = new JCheckBox("Enable Temporal Denoise");
spatial = addSlider("Spatial Strength", 0, 100, 30, "%");
temporal = addSlider("Temporal Strength", 0, 100, 50, "%");
enabled.addActionListener(e -> {
updateEnabledStates();
onUpdate.run();
});
spatial.addChangeListener(e -> { if(!spatial.getSlider().getValueIsAdjusting()) onUpdate.run(); });
temporal.addChangeListener(e -> { if(!temporal.getSlider().getValueIsAdjusting()) onUpdate.run(); });
add(enabled, 0);
updateEnabledStates();
}
private void updateEnabledStates() {
boolean active = enabled.isSelected();
spatial.getSlider().setEnabled(active);
temporal.getSlider().setEnabled(active);
}
@Override
public boolean isEnabled() {
return enabled.isSelected();
}
public int getSpatial() {
return enabled.isSelected() ? spatial.getValue() : 0;
}
public int getTemporal() {
return enabled.isSelected() ? temporal.getValue() : 0;
}
public void setState(boolean isEnabled, int s, int t) {
enabled.setSelected(isEnabled);
spatial.setValue(s);
temporal.setValue(t);
updateEnabledStates();
}
}

View File

@@ -0,0 +1,38 @@
package io.swtc.proccessing.ui.sections;
import io.swtc.proccessing.ui.FilterSection;
import io.swtc.proccessing.ui.LabeledSlider;
import javax.swing.*;
public class DetailSection extends FilterSection {
private final LabeledSlider sharpness;
private final JCheckBox edgeEnhance;
public DetailSection(Runnable onUpdate) {
super("Detail & Sharpness");
sharpness = addSlider("Sharpness", 0, 200, 100, "%");
edgeEnhance = new JCheckBox("Edge Enhancement");
sharpness.addChangeListener(e -> {
if (!sharpness.getSlider().getValueIsAdjusting()) onUpdate.run();
});
edgeEnhance.addActionListener(e -> onUpdate.run());
add(edgeEnhance);
}
public int getSharpness() {
return sharpness.getValue();
}
public boolean isEdgeEnhanceEnabled() {
return edgeEnhance != null && edgeEnhance.isSelected();
}
public void setState(int sharpVal, boolean edgeActive) {
sharpness.setValue(sharpVal);
edgeEnhance.setSelected(edgeActive);
}
}

View File

@@ -0,0 +1,39 @@
package io.swtc.proccessing.ui.sections;
import io.swtc.proccessing.EffectState;
import io.swtc.proccessing.PresetLibrary;
import javax.swing.*;
import java.awt.*;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class PresetSection extends JPanel {
private final JComboBox<String> presetCombo;
private final JButton saveBtn;
public PresetSection(PresetLibrary library, Consumer<EffectState> onPresetSelected, Supplier<EffectState> stateSupplier) {
setLayout(new BorderLayout(5, 5));
setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), "Presets"));
presetCombo = new JComboBox<>(new String[]{"Custom", "Natural", "Vivid", "Portrait", "Low Light", "Cinematic"});
saveBtn = new JButton("Save");
presetCombo.addActionListener(e -> {
String selected = (String) presetCombo.getSelectedItem();
EffectState state = library.get(selected);
if (state != null) onPresetSelected.accept(state);
});
saveBtn.addActionListener(e -> {
String name = JOptionPane.showInputDialog(this, "Preset Name:");
if (name != null && !name.trim().isEmpty()) {
library.savePreset(name, stateSupplier.get());
presetCombo.addItem(name);
presetCombo.setSelectedItem(name);
}
});
add(presetCombo, BorderLayout.CENTER);
add(saveBtn, BorderLayout.EAST);
}
}

View File

@@ -0,0 +1,55 @@
package io.swtc.proccessing.ui.sections;
import io.swtc.proccessing.ui.FilterSection;
import io.swtc.proccessing.ui.LabeledSlider;
import javax.swing.*;
public class WhiteBalanceSection extends FilterSection {
private final JCheckBox enabled;
private final LabeledSlider strength;
private final JButton balanceBtn;
public WhiteBalanceSection(Runnable onBalanceNow, Runnable onUpdate) {
super("White Balance");
enabled = new JCheckBox("Enable AWB");
strength = addSlider("Strength", 0, 100, 100, "%");
balanceBtn = new JButton("Balance Now");
enabled.addActionListener(e -> {
updateEnabledStates();
onUpdate.run();
});
strength.addChangeListener(e -> {
if (!strength.getSlider().getValueIsAdjusting()) onUpdate.run();
});
balanceBtn.addActionListener(e -> onBalanceNow.run());
add(enabled, 0);
add(balanceBtn);
updateEnabledStates();
}
private void updateEnabledStates() {
boolean active = enabled.isSelected();
strength.getSlider().setEnabled(active);
balanceBtn.setEnabled(active);
}
@Override
public boolean isEnabled() {
return enabled != null && enabled.isSelected();
}
public int getStrength() {
return enabled.isSelected() ? strength.getValue() : 0;
}
public void setState(boolean isEnabled, int str) {
enabled.setSelected(isEnabled);
strength.setValue(str);
updateEnabledStates();
}
}

View File

@@ -0,0 +1,114 @@
package io.swtc.recording;
import io.swtc.proccessing.CameraPanel;
import org.jcodec.api.awt.AWTSequenceEncoder;
import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.io.SeekableByteChannel;
import org.jcodec.common.model.Rational;
import javax.swing.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
/*
*
* This is not that interesting but surely improtant for any security based people,
* here we record using jcodec, which is more efficient than me writing my own recorder,
* which i am certainly not doing.
*
* Anyways we dont do shit properly anyway.
*
* */
public class VideoRecorder {
private boolean recording = false;
private Timer captureTimer;
private File outputFile;
private AWTSequenceEncoder encoder;
private SeekableByteChannel channel;
private static final int FPS = 30;
public void startRecording(CameraPanel panel, File output) throws IOException {
this.outputFile = output;
this.recording = true;
channel = NIOUtils.writableFileChannel(String.valueOf(outputFile));
encoder = new AWTSequenceEncoder(channel, Rational.R(FPS, 1));
captureTimer = new Timer(1000 / FPS, e -> {
try {
BufferedImage img = panel.getCurrentProcessedImage();
if (img != null) {
BufferedImage rgbImage = convertToRGB(img);
encoder.encodeImage(rgbImage);
}
} catch (IOException ex) {
JOptionPane.showMessageDialog(
null,
"IOException\n" + ex,
"IOException in Recorder",
JOptionPane.ERROR_MESSAGE
);
try {
stopRecording();
} catch (IOException stopEx) {
JOptionPane.showMessageDialog(
null,
"IOException@stopEx\n" + stopEx,
"IOException in Recorder@stopEx",
JOptionPane.ERROR_MESSAGE
);
}
}
});
captureTimer.start();
}
public File stopRecording() throws IOException {
recording = false;
/* some helper methods, i swear its always the same? */
if (captureTimer != null) {
captureTimer.stop();
}
if (encoder != null) {
encoder.finish();
}
if (channel != null) {
channel.close();
}
return outputFile;
}
public boolean isRecording() {
return recording;
}
// maybe i should move this to a component, some useless conversion right here,
// (performance wise)
// yes, capitain?
private BufferedImage convertToRGB(BufferedImage source) {
if (source.getType() == BufferedImage.TYPE_INT_RGB) {
return source;
}
BufferedImage rgb = new BufferedImage(
source.getWidth(),
source.getHeight(),
BufferedImage.TYPE_INT_RGB
);
for (int y = 0; y < source.getHeight(); y++) {
for (int x = 0; x < source.getWidth(); x++) {
rgb.setRGB(x, y, source.getRGB(x, y) & 0xFFFFFF);
}
}
return rgb;
}
}

View File

@@ -1,4 +1,4 @@
import io.swtc.proccessing.AutoGainProcessor;
import io.swtc.proccessing.AWBProccessor;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -13,13 +13,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
* of displaying stuff.
* */
class AutoGainProcessorTest {
class AWBProccessorTest {
private AutoGainProcessor processor;
private AWBProccessor processor;
@BeforeEach
void setUp() {
processor = new AutoGainProcessor();
processor = new AWBProccessor();
}
@Test

View File

@@ -1,98 +0,0 @@
import io.swtc.networking.CameraConfig;
import io.swtc.networking.CameraSettings;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class CameraSettingsTest {
// Must match the filename used in CameraSettings.java
private final File TEST_FILE = new File("network_cameras.json");
@BeforeEach
@AfterEach
void cleanUp() {
// Ensure we start and end with a clean slate to avoid side effects
if (TEST_FILE.exists()) {
TEST_FILE.delete();
}
}
@Test
void testLoadReturnsEmptyListWhenNoFile() {
// If the file doesn't exist, it should return an empty list (not null)
List<CameraConfig> result = CameraSettings.load();
assertNotNull(result, "Load should never return null");
assertTrue(result.isEmpty(), "Should return empty list if file doesn't exist");
}
@Test
void testSaveAndLoad() {
// 1. Create a config (Using your actual constructor)
CameraConfig config = new CameraConfig("FrontDoor", "http://192.168.1.100/mjpeg");
// 2. Save it
CameraSettings.save(config);
// 3. Verify file creation
assertTrue(TEST_FILE.exists(), "File should be created after save");
// 4. Load it back
List<CameraConfig> loaded = CameraSettings.load();
// 5. Verify contents
assertEquals(1, loaded.size());
assertEquals("FrontDoor", loaded.get(0).getName());
assertEquals("http://192.168.1.100/mjpeg", loaded.get(0).getUrl());
}
@Test
void testSaveMultiple() {
// Save two distinct cameras
CameraSettings.save(new CameraConfig("Cam1", "rtsp://10.0.0.1/stream"));
CameraSettings.save(new CameraConfig("Cam2", "rtsp://10.0.0.2/stream"));
List<CameraConfig> loaded = CameraSettings.load();
assertEquals(2, loaded.size());
assertEquals("Cam1", loaded.get(0).getName());
assertEquals("Cam2", loaded.get(1).getName());
}
@Test
void testDelete() {
// Setup: Save two cameras
CameraSettings.save(new CameraConfig("Garage", "http://1.1.1.1"));
CameraSettings.save(new CameraConfig("Garden", "http://2.2.2.2"));
// Action: Delete "Garage"
CameraSettings.delete("Garage");
// Verify: Only "Garden" remains
List<CameraConfig> result = CameraSettings.load();
assertEquals(1, result.size());
assertEquals("Garden", result.get(0).getName());
}
@Test
void testLoadCorruptFile() throws IOException {
// Manually write broken JSON to the file
try (FileWriter writer = new FileWriter(TEST_FILE)) {
writer.write("{ \"this is broken json\": ... ");
}
// The code catches IOException and returns empty list
List<CameraConfig> result = CameraSettings.load();
assertNotNull(result);
assertTrue(result.isEmpty(), "Should handle corrupt JSON gracefully by returning empty list");
}
}