diff --git a/pom.xml b/pom.xml index 16698d9..5e4fbc7 100644 --- a/pom.xml +++ b/pom.xml @@ -122,5 +122,6 @@ jcodec-javase 0.2.5 + \ No newline at end of file diff --git a/src/main/java/io/swtc/Main.java b/src/main/java/io/swtc/Main.java index c51bd32..80cd92f 100644 --- a/src/main/java/io/swtc/Main.java +++ b/src/main/java/io/swtc/Main.java @@ -1,40 +1,11 @@ package io.swtc; -import javax.swing.*; -import java.io.PrintWriter; -import java.io.StringWriter; - 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( - "com.sun.java.swing.plaf.windows.WindowsLookAndFeel" // actual class - //"com.i.throw.errors" // just for testing the try catch! - ); - } catch (Exception e) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - - String message = - "Exception -> " + e.getMessage() + - "\nStacktrace:" + sw - + "\n\nException Class: " + e.getClass().getName() - ; - - JOptionPane.showMessageDialog( - null, - message, - "Exception", - JOptionPane.ERROR_MESSAGE - ); + for (int i = 0; i < args.length; i++) { + System.out.println("Arg " + i + ": " + args[i]); } - SwingCCTVManager.main(null); } } diff --git a/src/main/java/io/swtc/SwingCameraWindow.java b/src/main/java/io/swtc/SwingCameraWindow.java deleted file mode 100644 index de3af66..0000000 --- a/src/main/java/io/swtc/SwingCameraWindow.java +++ /dev/null @@ -1,79 +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(); - captureLoop.stop(); // be sure to call this! otherwise the camera will stay open! - } - }); - - 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))); - - } - - 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) { - Graphics2D g2 = (Graphics2D) g; - g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g2.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 { - JOptionPane.showMessageDialog( - null, - "No Webcam found!", - "Error", - JOptionPane.WARNING_MESSAGE - ); - } - }); - } -} \ No newline at end of file diff --git a/src/main/java/io/swtc/SwingIFrame.java b/src/main/java/io/swtc/SwingIFrame.java index f2f76b2..ff1cc03 100644 --- a/src/main/java/io/swtc/SwingIFrame.java +++ b/src/main/java/io/swtc/SwingIFrame.java @@ -1,113 +1,141 @@ package io.swtc; 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 io.swtc.proccessing.ui.iframe.*; // Your custom frames import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; import java.awt.*; import java.awt.event.ActionEvent; -import java.awt.image.BufferedImage; -import java.io.File; -import java.text.SimpleDateFormat; -import java.util.Date; - -/* -* -* This file is basically just UI, its boring the interesting stuff is in the utilities section! -* -* */ +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 JDesktopPane desktopPane; + private final DesktopPane desktopPane; + private final Map cameraToEffects = new HashMap<>(); + private boolean fullscreen = false; private Rectangle windowedBounds; private boolean blackbg = false; - private final Color defDesktopBg; - private final Color bgcolor; + 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.DO_NOTHING_ON_CLOSE); + mainFrame = new JFrame("Viewer"); + mainFrame.setSize(1280, 720); + mainFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); - // this is good on the eyes for long periods of times, - // i would have used #4B9EA0, which is also easy on the eyes - bgcolor = Color.decode("#1e1e1e"); - - desktopPane = new JDesktopPane(); - desktopPane.setBackground(Color.WHITE); - defDesktopBg = desktopPane.getBackground(); + desktopPane = new DesktopPane(cameraToEffects); + desktopPane.setBackground(defDesktopBg); mainFrame.add(desktopPane, BorderLayout.CENTER); setupFullscreenToggle(); setupBlackBg(); + initPopupMenu(); + + desktopPane.addMouseListener(popupListener()); } public void addCameraInternalFrame(Webcam webcam) { - JInternalFrame iframe = new JInternalFrame( - webcam.getName(), - true, true, true, true + CameraInternalFrame cameraFrame = new CameraInternalFrame(webcam, this::handleEffectsRequest); + + EffectsPanelFrame effectsFrame = new EffectsPanelFrame( + "Effects - " + webcam.getName(), + cameraFrame.getCameraPanel() ); - CameraPanel cameraPanel = new CameraPanel(); - WebcamCaptureLoop captureLoop = new WebcamCaptureLoop(webcam, (BufferedImage img) -> - SwingUtilities.invokeLater(() -> cameraPanel.setImage(img)) - ); + cameraToEffects.put(cameraFrame, effectsFrame); - JPanel contentPanel = new JPanel(new BorderLayout()); + int offset = desktopPane.getAllFrames().length * 15; + cameraFrame.setLocation(50 + offset, 50 + offset); + effectsFrame.setLocation(700 + offset, 50 + offset); + effectsFrame.setVisible(false); - JTabbedPane tabbedPane = new JTabbedPane(); - tabbedPane.addTab("View", cameraPanel); - tabbedPane.addTab("Capture", createCapturePanel(cameraPanel, webcam)); - tabbedPane.addTab("Effects", createEffectsPanel(cameraPanel)); - - contentPanel.add(tabbedPane, BorderLayout.CENTER); - - iframe.addInternalFrameListener(new javax.swing.event.InternalFrameAdapter() { + cameraFrame.addInternalFrameListener(new javax.swing.event.InternalFrameAdapter() { @Override public void internalFrameClosing(javax.swing.event.InternalFrameEvent e) { - // if we dont call this the camera stays open until the procces dies. - captureLoop.stop(); + EffectsPanelFrame ef = cameraToEffects.remove(cameraFrame); + if (ef != null) ef.dispose(); + desktopPane.forgetFrame(cameraFrame); + cameraFrame.dispose(); } }); - iframe.add(contentPanel); - iframe.setSize(600, 500); + desktopPane.add(cameraFrame); + desktopPane.add(effectsFrame); - int offset = desktopPane.getAllFrames().length * 30; - iframe.setLocation(offset, offset); + // Attach popup menu to frames and content + MouseAdapter popup = popupListener(); + cameraFrame.addMouseListener(popup); + cameraFrame.getContentPane().addMouseListener(popup); + effectsFrame.addMouseListener(popup); + effectsFrame.getContentPane().addMouseListener(popup); - desktopPane.add(iframe); - iframe.setVisible(true); - captureLoop.start(); + 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) { - setbgblack(); + toggleBackground(); } }); } - /* Setup F11 for Fullscreen */ + 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) { @@ -116,284 +144,32 @@ public class SwingIFrame { }); } - private void setbgblack() { - if (!blackbg) { - // easy stuff here, just setting the bg to the private color - desktopPane.setBackground(bgcolor); - updateInternalFrameBg(bgcolor); - } else { - desktopPane.setBackground(defDesktopBg); - updateInternalFrameBg(null); - } + private void toggleBackground() { + desktopPane.setBackground(blackbg ? defDesktopBg : bgcolor); blackbg = !blackbg; + desktopPane.repaint(); } - private void updateInternalFrameBg(Color bg) { - for (JInternalFrame frame : desktopPane.getAllFrames()) { - Container content = frame.getContentPane(); - if (bg != null) { - content.setBackground(bg); - } else content.setBackground(null); // restore default - content.repaint(); - } - } - + /** Toggle fullscreen mode */ private void toggleFullscreen() { if (!fullscreen) { - // We set the window to borderless windowed mode, so it doesnt - // lag like shit when we drag windows around, which is annoying windowedBounds = mainFrame.getBounds(); mainFrame.dispose(); mainFrame.setUndecorated(true); mainFrame.setExtendedState(JFrame.MAXIMIZED_BOTH); mainFrame.setVisible(true); } else { - // do the opposite mainFrame.dispose(); mainFrame.setUndecorated(false); mainFrame.setExtendedState(JFrame.NORMAL); mainFrame.setBounds(windowedBounds); mainFrame.setVisible(true); } - fullscreen = !fullscreen; } - - - private JPanel createCapturePanel(CameraPanel cameraPanel, Webcam webcam) { - JPanel panel = new JPanel(new BorderLayout(10, 10)); - panel.setBorder(new EmptyBorder(15, 15, 15, 15)); - - JPanel mainPanel = new JPanel(); - mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); - - // Screenshot section - JPanel screenshotPanel = new JPanel(); - screenshotPanel.setLayout(new BoxLayout(screenshotPanel, BoxLayout.Y_AXIS)); - screenshotPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createEtchedBorder(), "Screenshot", - TitledBorder.LEFT, TitledBorder.TOP)); - - JPanel screenshotPathPanel = new JPanel(new BorderLayout(5, 5)); - JTextField screenshotPath = new JTextField(System.getProperty("user.home") + File.separator + "screenshots"); - JButton browseSS = new JButton("..."); - browseSS.setPreferredSize(new Dimension(40, 25)); - browseSS.addActionListener(e -> browseDirectory(screenshotPath, panel)); - screenshotPathPanel.add(screenshotPath, BorderLayout.CENTER); - screenshotPathPanel.add(browseSS, BorderLayout.EAST); - - JButton takeScreenshot = new JButton("Take Screenshot (S)"); - takeScreenshot.setAlignmentX(Component.CENTER_ALIGNMENT); - takeScreenshot.addActionListener(e -> saveSnapshot(cameraPanel, webcam, screenshotPath.getText(), panel)); - - screenshotPanel.add(screenshotPathPanel); - screenshotPanel.add(Box.createRigidArea(new Dimension(0, 10))); - screenshotPanel.add(takeScreenshot); - - // Recording section - JPanel recordPanel = new JPanel(); - recordPanel.setLayout(new BoxLayout(recordPanel, BoxLayout.Y_AXIS)); - recordPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createEtchedBorder(), "Recording", - TitledBorder.LEFT, TitledBorder.TOP)); - - JPanel recordPathPanel = new JPanel(new BorderLayout(5, 5)); - JTextField recordPath = new JTextField(System.getProperty("user.home") + File.separator + "recordings"); - JButton browseRec = new JButton("..."); - browseRec.setPreferredSize(new Dimension(40, 25)); - browseRec.addActionListener(e -> browseDirectory(recordPath, panel)); - recordPathPanel.add(recordPath, BorderLayout.CENTER); - recordPathPanel.add(browseRec, BorderLayout.EAST); - - VideoRecorder recorder = new VideoRecorder(); - JButton recordButton = new JButton("Start Recording (R)"); - JLabel recordingStatus = new JLabel("Ready"); - recordingStatus.setAlignmentX(Component.CENTER_ALIGNMENT); - - recordButton.setAlignmentX(Component.CENTER_ALIGNMENT); - recordButton.addActionListener(e -> { - if (!recorder.isRecording()) { - String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); - String filename = "recording_" + webcam.getName().replaceAll("[^a-zA-Z0-9]", "_") - + "_" + timestamp + ".mp4"; - File dir = new File(recordPath.getText()); - dir.mkdirs(); - - try { - recorder.startRecording(cameraPanel, new File(dir, filename)); - recordButton.setText("Stop Recording"); - recordingStatus.setText("Recording..."); - recordingStatus.setForeground(Color.RED); - } catch (Exception ex) { - JOptionPane.showMessageDialog(panel, - "Error starting recording: " + ex.getMessage(), - "Error", JOptionPane.ERROR_MESSAGE); - } - } else { - try { - File saved = recorder.stopRecording(); - recordButton.setText("Start Recording (R)"); - recordingStatus.setText("Saved: " + saved.getName()); - recordingStatus.setForeground(Color.BLACK); - } catch (Exception ex) { - recordButton.setText("Start Recording (R)"); - recordingStatus.setText("Error saving"); - recordingStatus.setForeground(Color.RED); - JOptionPane.showMessageDialog(panel, - "Error saving recording: " + ex.getMessage(), - "Error", JOptionPane.ERROR_MESSAGE); - } - } - }); - - recordPanel.add(recordPathPanel); - recordPanel.add(Box.createRigidArea(new Dimension(0, 10))); - recordPanel.add(recordButton); - recordPanel.add(Box.createRigidArea(new Dimension(0, 5))); - recordPanel.add(recordingStatus); - - mainPanel.add(screenshotPanel); - mainPanel.add(Box.createRigidArea(new Dimension(0, 15))); - mainPanel.add(recordPanel); - mainPanel.add(Box.createVerticalGlue()); - - panel.add(mainPanel, BorderLayout.NORTH); - - return panel; - } - - private JPanel createEffectsPanel(CameraPanel cameraPanel) { - JPanel panel = new JPanel(); - panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); - panel.setBorder(new EmptyBorder(15, 15, 15, 15)); - - JPanel transformPanel = new JPanel(new GridLayout(0, 1, 5, 5)); - transformPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createEtchedBorder(), "Transform", - TitledBorder.LEFT, TitledBorder.TOP)); - - JCheckBox mirrorCheck = new JCheckBox("Mirror Horizontal"); - JCheckBox flipCheck = new JCheckBox("Flip Vertical"); - JCheckBox rotateCheck = new JCheckBox("Rotate 180°"); - - mirrorCheck.addActionListener(e -> cameraPanel.setMirror(mirrorCheck.isSelected())); - flipCheck.addActionListener(e -> cameraPanel.setFlip(flipCheck.isSelected())); - rotateCheck.addActionListener(e -> cameraPanel.setRotate(rotateCheck.isSelected())); - - transformPanel.add(mirrorCheck); - transformPanel.add(flipCheck); - transformPanel.add(rotateCheck); - - JPanel colorPanel = new JPanel(); - colorPanel.setLayout(new BoxLayout(colorPanel, BoxLayout.Y_AXIS)); - colorPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createEtchedBorder(), "Color", - TitledBorder.LEFT, TitledBorder.TOP)); - - JCheckBox grayscaleCheck = new JCheckBox("Grayscale"); - JCheckBox invertCheck = new JCheckBox("Invert Colors"); - - grayscaleCheck.addActionListener(e -> cameraPanel.setGrayscale(grayscaleCheck.isSelected())); - invertCheck.addActionListener(e -> cameraPanel.setInvert(invertCheck.isSelected())); - - colorPanel.add(grayscaleCheck); - colorPanel.add(Box.createRigidArea(new Dimension(0, 5))); - colorPanel.add(invertCheck); - - JPanel brightnessPanel = new JPanel(); - brightnessPanel.setLayout(new BoxLayout(brightnessPanel, BoxLayout.Y_AXIS)); - brightnessPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createEtchedBorder(), "Adjustments", - TitledBorder.LEFT, TitledBorder.TOP)); - - JPanel brightnessSliderPanel = new JPanel(new BorderLayout()); - JLabel brightnessLabel = new JLabel("Brightness: 0"); - JSlider brightnessSlider = new JSlider(-100, 100, 0); - brightnessSlider.addChangeListener(e -> { - int value = brightnessSlider.getValue(); - brightnessLabel.setText("Brightness: " + value); - cameraPanel.setBrightness(value); - }); - brightnessSliderPanel.add(brightnessLabel, BorderLayout.NORTH); - brightnessSliderPanel.add(brightnessSlider, BorderLayout.CENTER); - - JPanel contrastSliderPanel = new JPanel(new BorderLayout()); - JLabel contrastLabel = new JLabel("Contrast: 1.0"); - JSlider contrastSlider = new JSlider(0, 200, 100); - contrastSlider.addChangeListener(e -> { - float value = contrastSlider.getValue() / 100f; - contrastLabel.setText("Contrast: " + String.format("%.1f", value)); - cameraPanel.setContrast(value); - }); - contrastSliderPanel.add(contrastLabel, BorderLayout.NORTH); - contrastSliderPanel.add(contrastSlider, BorderLayout.CENTER); - - brightnessPanel.add(brightnessSliderPanel); - brightnessPanel.add(Box.createRigidArea(new Dimension(0, 10))); - brightnessPanel.add(contrastSliderPanel); - - JButton resetButton = new JButton("Reset All Effects"); - resetButton.setAlignmentX(Component.CENTER_ALIGNMENT); - resetButton.addActionListener(e -> { - mirrorCheck.setSelected(false); - flipCheck.setSelected(false); - rotateCheck.setSelected(false); - grayscaleCheck.setSelected(false); - invertCheck.setSelected(false); - brightnessSlider.setValue(0); - contrastSlider.setValue(100); - cameraPanel.resetEffects(); - }); - - panel.add(transformPanel); - panel.add(Box.createRigidArea(new Dimension(0, 10))); - panel.add(colorPanel); - panel.add(Box.createRigidArea(new Dimension(0, 10))); - panel.add(brightnessPanel); - panel.add(Box.createRigidArea(new Dimension(0, 15))); - panel.add(resetButton); - panel.add(Box.createVerticalGlue()); - - return panel; - } - - private void browseDirectory(JTextField field, Component parent) { - JFileChooser chooser = new JFileChooser(field.getText()); - chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); - if (chooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) { - field.setText(chooser.getSelectedFile().getAbsolutePath()); - } - } - - private void saveSnapshot(CameraPanel cameraPanel, Webcam webcam, String directory, Component parent) { - BufferedImage img = cameraPanel.getCurrentProcessedImage(); - if (img != null) { - try { - File dir = new File(directory); - dir.mkdirs(); - - String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); - String filename = "screenshot_" + webcam.getName().replaceAll("[^a-zA-Z0-9]", "_") - + "_" + timestamp + ".png"; - File file = new File(dir, filename); - ImageIO.write(img, "PNG", file); - } catch (Exception ex) { - JOptionPane.showMessageDialog(parent, - "Error: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); - } - } - } - public void show() { mainFrame.setLocationRelativeTo(null); mainFrame.setVisible(true); } - - public static void main(String[] args) { - SwingUtilities.invokeLater(() -> { - SwingIFrame dashboard = new SwingIFrame(); - dashboard.show(); - }); - } -} \ No newline at end of file +} diff --git a/src/main/java/io/swtc/networking/CameraConfig.java b/src/main/java/io/swtc/networking/CameraConfig.java index 1716f54..d3814a0 100644 --- a/src/main/java/io/swtc/networking/CameraConfig.java +++ b/src/main/java/io/swtc/networking/CameraConfig.java @@ -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; diff --git a/src/main/java/io/swtc/proccessing/AutoGainProcessor.java b/src/main/java/io/swtc/proccessing/AWBProccessor.java similarity index 74% rename from src/main/java/io/swtc/proccessing/AutoGainProcessor.java rename to src/main/java/io/swtc/proccessing/AWBProccessor.java index 7138dcd..d563092 100644 --- a/src/main/java/io/swtc/proccessing/AutoGainProcessor.java +++ b/src/main/java/io/swtc/proccessing/AWBProccessor.java @@ -1,13 +1,22 @@ package io.swtc.proccessing; -/* -* -* Soon to be deprecated! -* -* */ +/** + * Gray World Algorithm. + * + *

+ * This class is an implementation of the Gray World algorithm, an automatic + * white balancing method. + *

+ * + *

+ * See: + * + * Stanford explanation + * + *

+ */ -@Deprecated -public class AutoGainProcessor { +public class AWBProccessor { public float[] calculateAutoGains(int[] pixels) { long rSum = 0, gSum = 0, bSum = 0; diff --git a/src/main/java/io/swtc/proccessing/CameraPanel.java b/src/main/java/io/swtc/proccessing/CameraPanel.java index fda37a7..e27851b 100644 --- a/src/main/java/io/swtc/proccessing/CameraPanel.java +++ b/src/main/java/io/swtc/proccessing/CameraPanel.java @@ -3,202 +3,67 @@ 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; -/* -* -* Now here its getting rather interesting! this class processes some -* important stuff! -* -* */ - +/** + * Enhanced CameraPanel with support for custom image processors + */ public class CameraPanel extends JPanel { - private BufferedImage currentImage; - private boolean mirror = false; - private boolean flip = false; - private boolean rotate = false; - private boolean grayscale = false; - private boolean invert = false; - private int brightness = 0; - private float contrast = 1.0f; + + private volatile BufferedImage sourceImage; + private volatile BufferedImage processedImage; + + private Function imageProcessor; + private final AtomicBoolean repaintScheduled = new AtomicBoolean(false); + + public CameraPanel() { + setBackground(Color.BLACK); + setPreferredSize(new Dimension(640, 480)); + } public void setImage(BufferedImage img) { - this.currentImage = img; - this.repaint(); - } + sourceImage = img; - public BufferedImage getCurrentImage() { - return currentImage; - } - - public BufferedImage getCurrentProcessedImage() { - if (currentImage == null) { - return null; + if (repaintScheduled.compareAndSet(false, true)) { + SwingUtilities.invokeLater(() -> { + repaintScheduled.set(false); + repaint(); + }); } - - BufferedImage processed = currentImage; - - // apply color effects - if (grayscale || invert || brightness != 0 || contrast != 1.0f) { - processed = applyColorEffects(processed); - } - - // apply transform. - if (mirror || flip || rotate) { - processed = applyTransforms(processed); - } - - return processed; } - /* Helper Methods ... its the same boilerplate shit over and over again, im sick of this. */ - public void setMirror(boolean mirror) { - this.mirror = mirror; - this.repaint(); - } - - public void setFlip(boolean flip) { - this.flip = flip; - this.repaint(); - } - - public void setRotate(boolean rotate) { - this.rotate = rotate; - this.repaint(); - } - - public void setGrayscale(boolean grayscale) { - this.grayscale = grayscale; - this.repaint(); - } - - public void setInvert(boolean invert) { - this.invert = invert; - this.repaint(); - } - - public void setBrightness(int brightness) { - this.brightness = brightness; - this.repaint(); - } - - public void setContrast(float contrast) { - this.contrast = contrast; - this.repaint(); - } - - public void resetEffects() { - mirror = flip = rotate = grayscale = invert = false; - brightness = 0; - contrast = 1.0f; - this.repaint(); + public void setImageProcessor(Function processor) { + this.imageProcessor = processor; } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); - if (currentImage != null) { - Graphics2D g2 = (Graphics2D) g; - g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - BufferedImage processedImage = currentImage; + BufferedImage src = sourceImage; + if (src == null) return; - // effects - if (grayscale || invert || brightness != 0 || contrast != 1.0f) { - processedImage = applyColorEffects(processedImage); - } + BufferedImage img = src; - // transforms - int w = getWidth(), h = getHeight(); - - if (rotate) { - g2.translate(w / 2, h / 2); - g2.rotate(Math.PI); - g2.translate(-w / 2, -h / 2); - } - - - // here we have if, cause we need to do the stuff, yk? - if (mirror && flip) { - g2.drawImage(processedImage, w, h, -w, -h, null); - } else if (mirror) { - g2.drawImage(processedImage, w, 0, -w, h, null); - } else if (flip) { - g2.drawImage(processedImage, 0, h, w, -h, null); - } else { - g2.drawImage(processedImage, 0, 0, w, h, null); - } - } - } - - private BufferedImage applyColorEffects(BufferedImage img) { - BufferedImage result = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_RGB); - - for (int y = 0; y < img.getHeight(); y++) { - for (int x = 0; x < img.getWidth(); x++) { - int rgb = img.getRGB(x, y); - int r = (rgb >> 16) & 0xFF; - int g = (rgb >> 8) & 0xFF; - int b = rgb & 0xFF; - - if (grayscale) { - int gray = (r + g + b) / 3; - r = g = b = gray; - } - - // this is fun, this regulates the brightness or the contrast! - // this is some real java, the other stuff is just UI design, which i am bad at, - // but this! This is some real shit - r = (int) ((r - 128) * contrast + 128 + brightness); - g = (int) ((g - 128) * contrast + 128 + brightness); - b = (int) ((b - 128) * contrast + 128 + brightness); - - // invert the colors! - if (invert) { - r = 255 - r; - g = 255 - g; - b = 255 - b; - } - - // clamp so we dont get into weird color grades, or any weird looking spaces - // if we dont do this we cant really do stuff which looks bearable - r = Math.max(0, Math.min(255, r)); - g = Math.max(0, Math.min(255, g)); - b = Math.max(0, Math.min(255, b)); - - result.setRGB(x, y, (r << 16) | (g << 8) | b); - } + 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); } - return result; + processedImage = img; + + Graphics2D g2d = (Graphics2D) g; + g2d.setRenderingHint( + RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR + ); + + g2d.drawImage(img, 0, 0, getWidth(), getHeight(), null); } - private BufferedImage applyTransforms(BufferedImage img) { - int width = img.getWidth(); - int height = img.getHeight(); - BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int sourceX = x; - int sourceY = y; - - if (mirror) { - sourceX = width - 1 - x; - } - if (flip) { - sourceY = height - 1 - y; - } - if (rotate) { - int tempX = width - 1 - sourceX; - int tempY = height - 1 - sourceY; - sourceX = tempX; - sourceY = tempY; - } - - result.setRGB(x, y, img.getRGB(sourceX, sourceY)); - } - } - - return result; + public BufferedImage getCurrentProcessedImage() { + return processedImage; } -} \ No newline at end of file +} diff --git a/src/main/java/io/swtc/proccessing/ColorProccessor.java b/src/main/java/io/swtc/proccessing/ColorProccessor.java new file mode 100644 index 0000000..5025986 --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ColorProccessor.java @@ -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; + }); + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/DenoiseProccessor.java b/src/main/java/io/swtc/proccessing/DenoiseProccessor.java new file mode 100644 index 0000000..27adfa2 --- /dev/null +++ b/src/main/java/io/swtc/proccessing/DenoiseProccessor.java @@ -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); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/EffectState.java b/src/main/java/io/swtc/proccessing/EffectState.java new file mode 100644 index 0000000..c436ef5 --- /dev/null +++ b/src/main/java/io/swtc/proccessing/EffectState.java @@ -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) { +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/FilterPanel.java b/src/main/java/io/swtc/proccessing/FilterPanel.java new file mode 100644 index 0000000..c1417c4 --- /dev/null +++ b/src/main/java/io/swtc/proccessing/FilterPanel.java @@ -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(); + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ImageEffectEngine.java b/src/main/java/io/swtc/proccessing/ImageEffectEngine.java new file mode 100644 index 0000000..1a763ff --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ImageEffectEngine.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ImageUtils.java b/src/main/java/io/swtc/proccessing/ImageUtils.java new file mode 100644 index 0000000..1d8117a --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ImageUtils.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/PresetLibrary.java b/src/main/java/io/swtc/proccessing/PresetLibrary.java new file mode 100644 index 0000000..56f81ca --- /dev/null +++ b/src/main/java/io/swtc/proccessing/PresetLibrary.java @@ -0,0 +1,46 @@ +package io.swtc.proccessing; + +import java.util.HashMap; +import java.util.Map; + +public class PresetLibrary { + private final Map 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]); + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/SharpnessProccessor.java b/src/main/java/io/swtc/proccessing/SharpnessProccessor.java new file mode 100644 index 0000000..4dd36cb --- /dev/null +++ b/src/main/java/io/swtc/proccessing/SharpnessProccessor.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/WebcamCaptureLoop.java b/src/main/java/io/swtc/proccessing/WebcamCaptureLoop.java index 77ba110..10f0470 100644 --- a/src/main/java/io/swtc/proccessing/WebcamCaptureLoop.java +++ b/src/main/java/io/swtc/proccessing/WebcamCaptureLoop.java @@ -2,67 +2,84 @@ 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 onFrameCaptured; private volatile boolean running = false; private final AtomicBoolean cleanedUp = new AtomicBoolean(false); + private final int targetframes = 20; public WebcamCaptureLoop(Webcam webcam, Consumer 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(); + if (!webcam.isOpen()) webcam.open(); // open if not - } catch (IllegalStateException e) { - // show a error message from javax.swing.awt.JOptionPane - JOptionPane.showMessageDialog( + 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, - "IllegalStateException@"+e, - "IllegalStateException", - JOptionPane.ERROR_MESSAGE + "Exception" + e, + "Exception" ); + } finally { + cleanup(); } - }); - captureThread.setName("cam_cap_thread"); + }, "cam_cap_thread"); captureThread.start(); } @@ -102,6 +119,5 @@ public class WebcamCaptureLoop { */ public void stop() { running = false; - cleanup(); } } \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/FilterSection.java b/src/main/java/io/swtc/proccessing/ui/FilterSection.java new file mode 100644 index 0000000..6eb3966 --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/FilterSection.java @@ -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))); + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/LabeledSlider.java b/src/main/java/io/swtc/proccessing/ui/LabeledSlider.java new file mode 100644 index 0000000..58904f9 --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/LabeledSlider.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/ShowError.java b/src/main/java/io/swtc/proccessing/ui/ShowError.java new file mode 100644 index 0000000..5efca5a --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/ShowError.java @@ -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 + ); + } +} diff --git a/src/main/java/io/swtc/proccessing/ui/UIFactory.java b/src/main/java/io/swtc/proccessing/ui/UIFactory.java new file mode 100644 index 0000000..8fa07d6 --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/UIFactory.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/iframe/CameraInternalFrame.java b/src/main/java/io/swtc/proccessing/ui/iframe/CameraInternalFrame.java new file mode 100644 index 0000000..127789a --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/iframe/CameraInternalFrame.java @@ -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 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 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(); + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/iframe/DesktopPane.java b/src/main/java/io/swtc/proccessing/ui/iframe/DesktopPane.java new file mode 100644 index 0000000..9fc933d --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/iframe/DesktopPane.java @@ -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 connections; + private final Map connectionColors = new HashMap<>(); + + public DesktopPane(Map 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 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); + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/iframe/EffectsPanelFrame.java b/src/main/java/io/swtc/proccessing/ui/iframe/EffectsPanelFrame.java new file mode 100644 index 0000000..a6533c1 --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/iframe/EffectsPanelFrame.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/iframe/RecordingPane.java b/src/main/java/io/swtc/proccessing/ui/iframe/RecordingPane.java new file mode 100644 index 0000000..22b7d93 --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/iframe/RecordingPane.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/sections/ColorSection.java b/src/main/java/io/swtc/proccessing/ui/sections/ColorSection.java new file mode 100644 index 0000000..c62e1f9 --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/sections/ColorSection.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/sections/DNRSection.java b/src/main/java/io/swtc/proccessing/ui/sections/DNRSection.java new file mode 100644 index 0000000..08f73c2 --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/sections/DNRSection.java @@ -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(); + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/sections/DetailSection.java b/src/main/java/io/swtc/proccessing/ui/sections/DetailSection.java new file mode 100644 index 0000000..98d3f18 --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/sections/DetailSection.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/sections/PresetSection.java b/src/main/java/io/swtc/proccessing/ui/sections/PresetSection.java new file mode 100644 index 0000000..9f10ccb --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/sections/PresetSection.java @@ -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 presetCombo; + private final JButton saveBtn; + + public PresetSection(PresetLibrary library, Consumer onPresetSelected, Supplier 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); + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/sections/WhiteBalanceSection.java b/src/main/java/io/swtc/proccessing/ui/sections/WhiteBalanceSection.java new file mode 100644 index 0000000..e0e77c8 --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/sections/WhiteBalanceSection.java @@ -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(); + } +} \ No newline at end of file diff --git a/src/test/java/AutoGainProcessorTest.java b/src/test/java/AWBProccessorTest.java similarity index 94% rename from src/test/java/AutoGainProcessorTest.java rename to src/test/java/AWBProccessorTest.java index 3bc4ab5..fc85e0c 100644 --- a/src/test/java/AutoGainProcessorTest.java +++ b/src/test/java/AWBProccessorTest.java @@ -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 diff --git a/src/test/java/CameraSettingsTest.java b/src/test/java/CameraSettingsTest.java deleted file mode 100644 index 0abe0ec..0000000 --- a/src/test/java/CameraSettingsTest.java +++ /dev/null @@ -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 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 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 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 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 result = CameraSettings.load(); - - assertNotNull(result); - assertTrue(result.isEmpty(), "Should handle corrupt JSON gracefully by returning empty list"); - } -} \ No newline at end of file