diff --git a/src/main/java/io/swtc/Main.java b/src/main/java/io/swtc/Main.java index a5b5d3e..84009e5 100644 --- a/src/main/java/io/swtc/Main.java +++ b/src/main/java/io/swtc/Main.java @@ -1,3 +1,4 @@ + package io.swtc; import javax.swing.*; @@ -5,25 +6,23 @@ import javax.swing.*; 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"); UIManager.setLookAndFeel( "com.sun.java.swing.plaf.windows.WindowsLookAndFeel" ); } catch (Exception e) { JOptionPane.showMessageDialog( null, - "Method not supported, Application will run anyways but might look different, and feel different!", - "SWTCCTVWarning", + "LaF not available", + "LaF-Warning", JOptionPane.WARNING_MESSAGE ); } - - SwingCCTVManager.main(null); + // For some reason we need to invoke Later for LaF to work! + SwingUtilities.invokeLater(() -> { + 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 0ac69fc..0000000 --- a/src/main/java/io/swtc/SwingCameraWindow.java +++ /dev/null @@ -1,80 +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; - -@Deprecated -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 1b4416b..ba6b382 100644 --- a/src/main/java/io/swtc/SwingIFrame.java +++ b/src/main/java/io/swtc/SwingIFrame.java @@ -3,26 +3,18 @@ package io.swtc; import com.github.sarxos.webcam.Webcam; import io.swtc.proccessing.WebcamCaptureLoop; import io.swtc.proccessing.CameraPanel; -import io.swtc.proccessing.FilterPanel; -import io.swtc.recording.VideoRecorder; import javax.imageio.ImageIO; import javax.swing.*; import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; +import javax.swing.event.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.geom.CubicCurve2D; import java.awt.image.BufferedImage; import java.io.File; -import java.text.SimpleDateFormat; import java.util.*; -/** - * Blender-style UI where the Effects panel is triggered by a Tab click - */ public class SwingIFrame { private final JFrame mainFrame; private final BlenderDesktopPane desktopPane; @@ -66,17 +58,16 @@ public class SwingIFrame { tabbedPane.addTab("View", cameraPanel); tabbedPane.addTab("Capture", createCapturePanel(cameraPanel, webcam)); - // Add a placeholder tab that acts as a button - tabbedPane.addTab("Open Effect Panel", new JPanel()); + tabbedPane.addTab("Effects", new JPanel()); contentPanel.add(tabbedPane, BorderLayout.CENTER); + tabbedPane.setPreferredSize(null); - // Logic to open/show the Effects Frame when the tab is clicked + // where we show the effectpanel (io.swtc.proccessing.FilterPanel.java) tabbedPane.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { - if (tabbedPane.getSelectedIndex() == 2) { // "Effects +" tab index - // Revert selection back to "View" so the tab doesn't stay stuck on the empty panel + if (tabbedPane.getSelectedIndex() == 2) { tabbedPane.setSelectedIndex(0); EffectsPanelFrame effectsFrame = cameraToEffects.get(cameraFrame); @@ -234,6 +225,8 @@ public class SwingIFrame { this.connections = connections; } + + @Override protected void paintChildren(Graphics g) { super.paintChildren(g); @@ -273,16 +266,41 @@ public class SwingIFrame { } static class EffectsPanelFrame extends JInternalFrame { + public EffectsPanelFrame(String title, CameraPanel cameraPanel, JInternalFrame cameraFrame) { super(title, true, true, true, true); - add(new FilterPanel(cameraPanel)); - // Repaint curves when moving or iconifying + // Hide instead of dispose so it can be reopened + setDefaultCloseOperation(JInternalFrame.HIDE_ON_CLOSE); + + // Add your FilterPanel + add(new io.swtc.proccessing.FilterPanel(cameraPanel)); + + // Repaint desktop pane when this frame closes so the Bézier disappears + addInternalFrameListener(new InternalFrameAdapter() { + @Override + public void internalFrameClosing(InternalFrameEvent e) { + if (getDesktopPane() != null) { + getDesktopPane().repaint(); + } + } + }); + + // Repaint curves when moving or resizing addComponentListener(new java.awt.event.ComponentAdapter() { - @Override public void componentMoved(java.awt.event.ComponentEvent e) { - if(getDesktopPane() != null) getDesktopPane().repaint(); + @Override + public void componentMoved(java.awt.event.ComponentEvent e) { + if (getDesktopPane() != null) + getDesktopPane().repaint(); + } + + @Override + public void componentResized(java.awt.event.ComponentEvent e) { + if (getDesktopPane() != null) + getDesktopPane().repaint(); } }); } } + } \ 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 97% rename from src/main/java/io/swtc/proccessing/AutoGainProcessor.java rename to src/main/java/io/swtc/proccessing/AWBProccessor.java index 473ba4c..d563092 100644 --- a/src/main/java/io/swtc/proccessing/AutoGainProcessor.java +++ b/src/main/java/io/swtc/proccessing/AWBProccessor.java @@ -16,7 +16,7 @@ package io.swtc.proccessing; *

*/ -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 f0d5620..6d64310 100644 --- a/src/main/java/io/swtc/proccessing/CameraPanel.java +++ b/src/main/java/io/swtc/proccessing/CameraPanel.java @@ -12,15 +12,6 @@ public class CameraPanel extends JPanel { private BufferedImage currentImage; private BufferedImage processedImage; - // Basic transform effects - 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; - // Custom processor for advanced effects private Function imageProcessor; @@ -71,14 +62,6 @@ public class CameraPanel extends JPanel { int srcX = x; int srcY = y; - if (mirror) srcX = width - 1 - x; - if (flip) srcY = height - 1 - y; - if (rotate) { - int temp = srcX; - srcX = width - 1 - srcY; - srcY = temp; - } - // Ensure coordinates are in bounds srcX = Math.max(0, Math.min(width - 1, srcX)); srcY = Math.max(0, Math.min(height - 1, srcY)); @@ -88,33 +71,6 @@ public class CameraPanel extends JPanel { int g = (rgb >> 8) & 0xFF; int b = rgb & 0xFF; - // Apply grayscale - if (grayscale) { - int gray = (r + g + b) / 3; - r = g = b = gray; - } - - // Apply brightness - if (brightness != 0) { - r = clamp(r + brightness); - g = clamp(g + brightness); - b = clamp(b + brightness); - } - - // Apply contrast - if (contrast != 1.0f) { - r = clamp((int)((r - 128) * contrast + 128)); - g = clamp((int)((g - 128) * contrast + 128)); - b = clamp((int)((b - 128) * contrast + 128)); - } - - // Apply invert - if (invert) { - r = 255 - r; - g = 255 - g; - b = 255 - b; - } - result.setRGB(x, y, (r << 16) | (g << 8) | b); } } @@ -122,35 +78,16 @@ public class CameraPanel extends JPanel { return result; } - private int clamp(int value) { - return Math.max(0, Math.min(255, value)); - } - @Override protected void paintComponent(Graphics g) { super.paintComponent(g); if (processedImage != null) { Graphics2D g2d = (Graphics2D) g; + g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - // Scale image to fit panel while maintaining aspect ratio - int panelWidth = getWidth(); - int panelHeight = getHeight(); - int imgWidth = processedImage.getWidth(); - int imgHeight = processedImage.getHeight(); - - double scaleX = (double) panelWidth / imgWidth; - double scaleY = (double) panelHeight / imgHeight; - double scale = Math.min(scaleX, scaleY); - - int scaledWidth = (int) (imgWidth * scale); - int scaledHeight = (int) (imgHeight * scale); - - int x = (panelWidth - scaledWidth) / 2; - int y = (panelHeight - scaledHeight) / 2; - - g2d.drawImage(processedImage, x, y, scaledWidth, scaledHeight, null); + g2d.drawImage(processedImage, 0, 0, getWidth(), getHeight(), null); } } @@ -158,59 +95,4 @@ public class CameraPanel extends JPanel { return processedImage; } - // Basic effect setters - public void setMirror(boolean mirror) { - this.mirror = mirror; - processImage(); - repaint(); - } - - public void setFlip(boolean flip) { - this.flip = flip; - processImage(); - repaint(); - } - - public void setRotate(boolean rotate) { - this.rotate = rotate; - processImage(); - repaint(); - } - - public void setGrayscale(boolean grayscale) { - this.grayscale = grayscale; - processImage(); - repaint(); - } - - public void setInvert(boolean invert) { - this.invert = invert; - processImage(); - repaint(); - } - - public void setBrightness(int brightness) { - this.brightness = brightness; - processImage(); - repaint(); - } - - public void setContrast(float contrast) { - this.contrast = contrast; - processImage(); - repaint(); - } - - public void resetEffects() { - this.mirror = false; - this.flip = false; - this.rotate = false; - this.grayscale = false; - this.invert = false; - this.brightness = 0; - this.contrast = 1.0f; - this.imageProcessor = null; - processImage(); - repaint(); - } } \ 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/ConnectionOverlay.java b/src/main/java/io/swtc/proccessing/ConnectionOverlay.java deleted file mode 100644 index 9be93fc..0000000 --- a/src/main/java/io/swtc/proccessing/ConnectionOverlay.java +++ /dev/null @@ -1,81 +0,0 @@ -package io.swtc.proccessing; - -import javax.swing.*; -import java.awt.*; -import java.awt.geom.CubicCurve2D; - -/** - * Acts as a transparent layer over the entire window to draw connections. - */ -public class ConnectionOverlay extends JComponent { - private final Component source; - private final Component target; - private final Color connectionColor; - - public ConnectionOverlay(Component source, Component target, Color color) { - this.source = source; - this.target = target; - this.connectionColor = color; - setOpaque(false); // Make sure we can see through it - } - - @Override - protected void paintComponent(Graphics g) { - super.paintComponent(g); - if (!source.isShowing() || !target.isShowing()) return; - - Graphics2D g2d = (Graphics2D) g.create(); - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - // 1. Get absolute positions relative to this Overlay - Point p1 = SwingUtilities.convertPoint(source, 0, 0, this); - Point p2 = SwingUtilities.convertPoint(target, 0, 0, this); - - int x1, y1, x2, y2, ctrl1X, ctrl2X; - - // Calculate vertical centers - y1 = p1.y + (source.getHeight() / 2); - y2 = p2.y + (target.getHeight() / 2); - - // 2. Logic to determine Left/Right orientation - // If source is to the left of target - if (p1.x + source.getWidth() < p2.x) { - x1 = p1.x + source.getWidth(); // Right edge of source - x2 = p2.x; // Left edge of target - } - // If source is to the right of target - else if (p1.x > p2.x + target.getWidth()) { - x1 = p1.x; // Left edge of source - x2 = p2.x + target.getWidth(); // Right edge of target - } - // If they are overlapping horizontally, use centers - else { - x1 = p1.x + (source.getWidth() / 2); - x2 = p2.x + (target.getWidth() / 2); - } - - // 3. Dynamic Curve "Stiffness" - // The horizontal distance between the two points determines how far the curve pulls - int horizontalDist = Math.abs(x1 - x2); - int offset = Math.max(horizontalDist / 2, 20); // Minimum 20px pull for short distances - - if (x1 < x2) { - ctrl1X = x1 + offset; - ctrl2X = x2 - offset; - } else { - ctrl1X = x1 - offset; - ctrl2X = x2 + offset; - } - - // 4. Draw the Curve - g2d.setColor(connectionColor); - g2d.setStroke(new BasicStroke(3f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); - - // We keep the Y coordinates for controls the same as the endpoints - // to create that "horizontal entry/exit" look. - CubicCurve2D curve = new CubicCurve2D.Float(x1, y1, ctrl1X, y1, ctrl2X, y2, x2, y2); - g2d.draw(curve); - - g2d.dispose(); - } -} \ 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 index c0d45ce..c436ef5 100644 --- a/src/main/java/io/swtc/proccessing/EffectState.java +++ b/src/main/java/io/swtc/proccessing/EffectState.java @@ -1,25 +1,6 @@ package io.swtc.proccessing; -public class EffectState { - public final boolean awbEnabled, dnrEnabled, edgeEnhance; - public final int awbStrength, dnrSpatial, dnrTemporal; - public final int temperature, tint, saturation, shadows, highlights, sharpness; - - public 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) { - this.awbEnabled = awbEnabled; - this.awbStrength = awbStrength; - this.dnrEnabled = dnrEnabled; - this.dnrSpatial = dnrSpatial; - this.dnrTemporal = dnrTemporal; - this.temperature = temperature; - this.tint = tint; - this.saturation = saturation; - this.shadows = shadows; - this.highlights = highlights; - this.sharpness = sharpness; - this.edgeEnhance = edgeEnhance; - } +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 index 2258e42..c1417c4 100644 --- a/src/main/java/io/swtc/proccessing/FilterPanel.java +++ b/src/main/java/io/swtc/proccessing/FilterPanel.java @@ -1,273 +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 javax.swing.border.TitledBorder; import java.awt.*; -import java.awt.event.ComponentAdapter; -import java.awt.event.ComponentEvent; -import java.awt.image.BufferedImage; -import java.util.Random; +/** + * 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 AutoGainProcessor autoGainProcessor; + private final AWBProccessor awbProcessor; private final PresetLibrary presetLibrary; - private final Color connectionColor; - private JCheckBox awbEnabled, dnrEnabled, edgeEnhance; - private JSlider awbStrength, dnrStrength, dnrTemporal, sharpness; - private JSlider saturation, temperature, tint, shadows, highlights; - private JComboBox presetCombo; + 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.autoGainProcessor = new AutoGainProcessor(); + this.awbProcessor = new AWBProccessor(); this.presetLibrary = new PresetLibrary(); - // FIX: Randomize color with high saturation so it stands out - Random rand = new Random(); - this.connectionColor = Color.getHSBColor(rand.nextFloat(), 0.9f, 0.9f); - - setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); - setBorder(new EmptyBorder(10, 10, 10, 10)); - + initializePanel(); buildUI(); - initListeners(); // Initialize anti-ghosting listeners + initGlobalListeners(); } + private void initializePanel() { + setLayout(new BorderLayout()); + setBorder(new EmptyBorder(10, 10, 10, 10)); + } private void buildUI() { - add(createPresetPanel()); - add(Box.createRigidArea(new Dimension(0, 10))); + PresetSection presetSection = new PresetSection(presetLibrary, this::applyPresetToUI, this::getCurrentState); + add(presetSection, BorderLayout.NORTH); - JPanel container = new JPanel(); - container.setLayout(new BoxLayout(container, BoxLayout.Y_AXIS)); + JPanel scrollContainer = new JPanel(); + scrollContainer.setLayout(new BoxLayout(scrollContainer, BoxLayout.Y_AXIS)); - container.add(createAWBPanel()); - container.add(Box.createRigidArea(new Dimension(0, 10))); - container.add(createDNRPanel()); - container.add(Box.createRigidArea(new Dimension(0, 10))); - container.add(createColorPanel()); - container.add(Box.createRigidArea(new Dimension(0, 10))); - container.add(createDetailPanel()); + awbSection = new WhiteBalanceSection(this::performOneTimeBalance, this::applyToCamera); + dnrSection = new DNRSection(this::applyToCamera); + colorSection = new ColorSection(this::applyToCamera); + detailSection = new DetailSection(this::applyToCamera); - JScrollPane scroll = new JScrollPane(container); - scroll.setBorder(null); - scroll.getVerticalScrollBar().setUnitIncrement(16); - add(scroll); + 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(Box.createRigidArea(new Dimension(0, 10))); - add(createActionPanel()); + 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( - awbEnabled.isSelected(), awbStrength.getValue(), - dnrEnabled.isSelected(), dnrStrength.getValue(), dnrTemporal.getValue(), - temperature.getValue(), tint.getValue(), saturation.getValue(), - shadows.getValue(), highlights.getValue(), - sharpness.getValue(), edgeEnhance.isSelected() + awbSection.isEnabled(), awbSection.getStrength(), + dnrSection.isEnabled(), dnrSection.getSpatial(), dnrSection.getTemporal(), + colorSection.getTemp(), colorSection.getTint(), colorSection.getSaturation(), + colorSection.getShadows(), colorSection.getHighlights(), + detailSection.getSharpness(), detailSection.isEdgeEnhanceEnabled() ); } - // Inside AdvancedEffectsPanel.java - - private void initListeners() { - ComponentAdapter repaintListener = new ComponentAdapter() { - @Override - public void componentMoved(ComponentEvent e) { - updateOverlay(); - } - @Override - public void componentResized(ComponentEvent e) { - updateOverlay(); - } - }; - - this.addComponentListener(repaintListener); - if (this.cameraPanel != null) { - this.cameraPanel.addComponentListener(repaintListener); - } - } - - private void updateOverlay() { - RootPaneContainer root = (RootPaneContainer) SwingUtilities.getWindowAncestor(this); - if (root != null) { - root.getGlassPane().repaint(); - } - } - private void applyToCamera() { + if (cameraPanel == null) return; + EffectState state = getCurrentState(); cameraPanel.setImageProcessor(img -> ImageEffectEngine.applyEffects(img, state, currentGains) ); } - private JPanel createPresetPanel() { - JPanel panel = new JPanel(new BorderLayout(5, 5)); - panel.setBorder(createTitledBorder("Presets")); - - presetCombo = new JComboBox<>(new String[]{"Custom", "Natural", "Vivid", "Portrait", "Low Light", "Cinematic"}); - JButton saveBtn = new JButton("Save"); - - presetCombo.addActionListener(e -> { - String selected = (String) presetCombo.getSelectedItem(); - EffectState state = presetLibrary.get(selected); - if (state != null) applyPresetToUI(state); - }); - - saveBtn.addActionListener(e -> { - String name = JOptionPane.showInputDialog(this, "Preset Name:"); - if (name != null) { - presetLibrary.savePreset(name, getCurrentState()); - presetCombo.addItem(name); - } - }); - - panel.add(presetCombo, BorderLayout.CENTER); - panel.add(saveBtn, BorderLayout.EAST); - return panel; - } - - private JPanel createDNRPanel() { - JPanel panel = new JPanel(); - panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); - panel.setBorder(createTitledBorder("3D Denoise (DNR)")); - - dnrEnabled = new JCheckBox("Enable Temporal Denoise"); - dnrStrength = addSliderToPanel(panel, "Spatial Strength", 0, 100, 30, "%"); - dnrTemporal = addSliderToPanel(panel, "Temporal Strength", 0, 100, 50, "%"); - - dnrEnabled.addActionListener(e -> { - boolean enabled = dnrEnabled.isSelected(); - dnrStrength.setEnabled(enabled); - dnrTemporal.setEnabled(enabled); - applyToCamera(); - }); - - dnrStrength.setEnabled(false); - dnrTemporal.setEnabled(false); - - panel.add(dnrEnabled, 0); - return panel; - } - - private JPanel createDetailPanel() { - JPanel panel = new JPanel(); - panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); - panel.setBorder(createTitledBorder("Detail & Sharpness")); - - sharpness = addSliderToPanel(panel, "Sharpness", 0, 200, 100, "%"); - edgeEnhance = new JCheckBox("Edge Enhancement"); - - edgeEnhance.addActionListener(e -> applyToCamera()); - - panel.add(edgeEnhance); - return panel; - } - - private JPanel createAWBPanel() { - JPanel panel = new JPanel(); - panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); - panel.setBorder(createTitledBorder("White Balance")); - - awbEnabled = new JCheckBox("Enable AWB"); - JPanel sPanel = createSliderPanel("Strength", 0, 100, 100, "%"); - awbStrength = (JSlider) sPanel.getComponent(1); - JButton balanceBtn = new JButton("Balance Now"); - - awbEnabled.addActionListener(e -> { - boolean active = awbEnabled.isSelected(); - awbStrength.setEnabled(active); - balanceBtn.setEnabled(active); - if (active) performOneTimeBalance(); - else { currentGains = new float[]{1f, 1f, 1f}; applyToCamera(); } - }); - - awbStrength.addChangeListener(e -> { if(!awbStrength.getValueIsAdjusting()) applyToCamera(); }); - balanceBtn.addActionListener(e -> performOneTimeBalance()); - - panel.add(awbEnabled); - panel.add(sPanel); - panel.add(balanceBtn); - return panel; - } - - private JPanel createColorPanel() { - JPanel panel = new JPanel(); - panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); - panel.setBorder(createTitledBorder("Color Grading")); - - temperature = addSliderToPanel(panel, "Temperature", -100, 100, 0, ""); - tint = addSliderToPanel(panel, "Tint", -100, 100, 0, ""); - saturation = addSliderToPanel(panel, "Saturation", 0, 200, 100, "%"); - shadows = addSliderToPanel(panel, "Shadows", -100, 100, 0, ""); - highlights = addSliderToPanel(panel, "Highlights", -100, 100, 0, ""); - - return panel; - } - + /** + * call awb stuff + */ private void performOneTimeBalance() { - BufferedImage img = cameraPanel.getCurrentProcessedImage(); + 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 = autoGainProcessor.calculateAutoGains(pixels); + currentGains = awbProcessor.calculateAutoGains(pixels); applyToCamera(); } } + /** + * Update for a specified state + * */ private void applyPresetToUI(EffectState s) { - awbEnabled.setSelected(s.awbEnabled); - awbStrength.setValue(s.awbStrength); - dnrEnabled.setSelected(s.dnrEnabled); - dnrStrength.setValue(s.dnrSpatial); - temperature.setValue(s.temperature); - tint.setValue(s.tint); - saturation.setValue(s.saturation); - shadows.setValue(s.shadows); - highlights.setValue(s.highlights); - sharpness.setValue(s.sharpness); - edgeEnhance.setSelected(s.edgeEnhance); + 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 JSlider addSliderToPanel(JPanel parent, String label, int min, int max, int val, String unit) { - JPanel p = createSliderPanel(label, min, max, val, unit); - JSlider s = (JSlider) p.getComponent(1); - s.addChangeListener(e -> { if(!s.getValueIsAdjusting()) applyToCamera(); }); - parent.add(p); - parent.add(Box.createRigidArea(new Dimension(0, 5))); - return s; - } - - private JPanel createSliderPanel(String label, int min, int max, int initial, String unit) { - JPanel panel = new JPanel(new BorderLayout()); - JLabel title = new JLabel(label + ": " + initial + unit); - JSlider slider = new JSlider(min, max, initial); - slider.addChangeListener(e -> title.setText(label + ": " + slider.getValue() + unit)); - panel.add(title, BorderLayout.NORTH); - panel.add(slider, BorderLayout.CENTER); - return panel; - } - - private TitledBorder createTitledBorder(String title) { - return BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), title); - } - - private JPanel createActionPanel() { - JPanel panel = new JPanel(new GridLayout(1, 2, 10, 0)); - JButton reset = new JButton("Reset All"); - reset.addActionListener(e -> resetUI()); - panel.add(reset); - return panel; - } - 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 index 65eb22f..1a763ff 100644 --- a/src/main/java/io/swtc/proccessing/ImageEffectEngine.java +++ b/src/main/java/io/swtc/proccessing/ImageEffectEngine.java @@ -4,144 +4,37 @@ 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(); - BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + int[] pixels = ImageUtils.getPixels(img); - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int rgb = img.getRGB(x, y); - int r = (rgb >> 16) & 0xFF; - int g = (rgb >> 8) & 0xFF; - int b = rgb & 0xFF; + // 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 - // 1. AWB - if (state.awbEnabled) { - float s = state.awbStrength / 100f; - r = (int) Math.min(255, r * (1 + (currentGains[0] - 1) * s)); - g = (int) Math.min(255, g * (1 + (currentGains[1] - 1) * s)); - b = (int) Math.min(255, b * (1 + (currentGains[2] - 1) * s)); - } + // 2. Apply Color Pipeline (In-Place) + colorProcessor.process(workingPixels, state, currentGains); - // 2. Temp & Tint - if (state.temperature != 0) { - float factor = state.temperature / 100f; - r = clamp(r + (int)(factor * 30)); - b = clamp(b - (int)(factor * 30)); - } - if (state.tint != 0) { - g = 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 = clamp((int)(gray + (r - gray) * factor)); - g = clamp((int)(gray + (g - gray) * factor)); - b = clamp((int)(gray + (b - gray) * factor)); - } - - // 4. Shadows/Highlights - float lum = (r + g + b) / 3f / 255f; - if (lum < 0.5f && state.shadows != 0) { - int adj = (int)((state.shadows / 100f) * (1 - lum * 2) * 50); - r = clamp(r + adj); g = clamp(g + adj); b = clamp(b + adj); - } else if (lum > 0.5f && state.highlights != 0) { - int adj = (int)((state.highlights / 100f) * (lum * 2 - 1) * 50); - r = clamp(r + adj); g = clamp(g + adj); b = clamp(b + adj); - } - - result.setRGB(x, y, (r << 16) | (g << 8) | b); - } + // 3. Apply Sharpness (Returns new array if applied) + if (state.sharpness() != 100 || state.edgeEnhance()) { + workingPixels = sharpnessProcessor.process(workingPixels, width, height, state.sharpness(), state.edgeEnhance()); } - if (state.sharpness != 100 || state.edgeEnhance) { - result = applySharpness(result, state.sharpness / 100f, state.edgeEnhance); + // 4. Apply Denoise (Returns new array if applied) + if (state.dnrEnabled()) { + workingPixels = denoiseProcessor.process(workingPixels, width, height, state.dnrSpatial()); } - if (state.dnrEnabled) { - result = applyDenoise(result, state.dnrSpatial / 100f); - } - - return result; - } - - private static BufferedImage applySharpness(BufferedImage img, float amount, boolean edgeEnhance) { - if (amount == 1f && !edgeEnhance) return img; - - int width = img.getWidth(); - int height = img.getHeight(); - BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - - float[][] kernel = edgeEnhance ? - new float[][]{{0, -1, 0}, {-1, 5, -1}, {0, -1, 0}} : - new float[][]{{-1, -1, -1}, {-1, 9, -1}, {-1, -1, -1}}; - - for (int y = 1; y < height - 1; y++) { - for (int x = 1; x < width - 1; x++) { - float r = 0, g = 0, b = 0; - - for (int ky = -1; ky <= 1; ky++) { - for (int kx = -1; kx <= 1; kx++) { - int rgb = img.getRGB(x + kx, y + ky); - float weight = kernel[ky + 1][kx + 1] * (amount - 1) / 8f; - if (kx == 0 && ky == 0) weight += 1; - - r += ((rgb >> 16) & 0xFF) * weight; - g += ((rgb >> 8) & 0xFF) * weight; - b += (rgb & 0xFF) * weight; - } - } - - result.setRGB(x, y, (clamp((int)r) << 16) | (clamp((int)g) << 8) | clamp((int)b)); - } - } - - return result; - } - - private static BufferedImage applyDenoise(BufferedImage img, float strength) { - if (strength == 0) return img; - - int width = img.getWidth(); - int height = img.getHeight(); - BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - - int radius = (int)(strength * 2) + 1; - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int rSum = 0, gSum = 0, bSum = 0, count = 0; - - for (int dy = -radius; dy <= radius; dy++) { - for (int dx = -radius; dx <= radius; dx++) { - int nx = Math.min(width - 1, Math.max(0, x + dx)); - int ny = Math.min(height - 1, Math.max(0, y + dy)); - - int rgb = img.getRGB(nx, ny); - rSum += (rgb >> 16) & 0xFF; - gSum += (rgb >> 8) & 0xFF; - bSum += rgb & 0xFF; - count++; - } - } - - int r = rSum / count; - int g = gSum / count; - int b = bSum / count; - - result.setRGB(x, y, (r << 16) | (g << 8) | b); - } - } - - return result; - } - - private static int clamp(int val) { - return Math.max(0, Math.min(255, val)); + // 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/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..fbac654 100644 --- a/src/main/java/io/swtc/proccessing/WebcamCaptureLoop.java +++ b/src/main/java/io/swtc/proccessing/WebcamCaptureLoop.java @@ -34,7 +34,18 @@ public class WebcamCaptureLoop { Thread captureThread = new Thread(() -> { // this is where we open it. if the res isnt known then it fucks up. - webcam.open(); + try { + webcam.open(); + } catch (WebcamException e) { + JOptionPane.showMessageDialog( + null, + "WebcamException" + e.getMessage(), + "WebcamException", + JOptionPane.ERROR_MESSAGE + ); + } finally { + webcam.open(); + } while (running) { BufferedImage img = webcam.getImage(); 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/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/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..ffdc94d --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/sections/DNRSection.java @@ -0,0 +1,48 @@ +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, "%"); + + // Logic: Disable sliders if DNR is off + 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); // Checkbox at top + updateEnabledStates(); + } + + private void updateEnabledStates() { + boolean active = enabled.isSelected(); + spatial.getSlider().setEnabled(active); + temporal.getSlider().setEnabled(active); + } + + public boolean isDnrEnabled() { return enabled.isSelected(); } + public int getSpatial() { return spatial.getValue(); } + public int getTemporal() { return temporal.getValue(); } + + 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..d051b24 --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/sections/DetailSection.java @@ -0,0 +1,33 @@ +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.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..10a329a --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/sections/WhiteBalanceSection.java @@ -0,0 +1,49 @@ +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); + } + + public boolean isEnabled() { return enabled.isSelected(); } + public int getStrength() { return strength.getValue(); } + + 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