commit 8c59e57711fe7b602104fafdbb6849287e9cc4cf Author: rattatwinko Date: Sun May 25 14:14:23 2025 +0200 initial commit ; currently it wont build :C diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf3e1b2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store +/.idea/ diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..86d65d3 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("java") +} + +group = "org.jdetect" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + implementation("org.openpnp:opencv:4.5.1-2") + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") +} + +tasks.test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..8706822 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sun May 25 13:46:09 CEST 2025 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..0311d9d --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "MultiJDetect" \ No newline at end of file diff --git a/src/main/java/org/jdetect/CameraPanel.java b/src/main/java/org/jdetect/CameraPanel.java new file mode 100644 index 0000000..e5947b9 --- /dev/null +++ b/src/main/java/org/jdetect/CameraPanel.java @@ -0,0 +1,126 @@ +package org.jdetect; + +import org.opencv.core.*; +import org.opencv.videoio.VideoCapture; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.image.BufferedImage; + +public class CameraPanel extends JPanel { + private final VideoCapture cap; + private final JLabel videoLabel; + private final JLabel statusLabel; + private DetectionThread detectionThread; + private Timer timer; + private final String source; + private int frameCounter = 0; + private static final int DETECTION_INTERVAL = 5; + + public CameraPanel(String source) { + this.source = source; + boolean isStream = source.startsWith("rtsp://") || source.startsWith("https://"); + + setLayout(new BorderLayout()); + setBorder(BorderFactory.createEtchedBorder()); + + // Video display + videoLabel = new JLabel(); + videoLabel.setHorizontalAlignment(SwingConstants.CENTER); + add(videoLabel, BorderLayout.CENTER); + + // Controls panel + JPanel controlsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + + JButton snapshotButton = new JButton("Snapshot"); + snapshotButton.addActionListener(this::takeSnapshot); + controlsPanel.add(snapshotButton); + + JButton stopButton = new JButton("Remove"); + stopButton.addActionListener(e -> stop()); + controlsPanel.add(stopButton); + + add(controlsPanel, BorderLayout.SOUTH); + + // Status label + statusLabel = new JLabel("Initializing..."); + add(statusLabel, BorderLayout.NORTH); + + // Initialize video source + if (isStream) { + cap = new VideoCapture(source); + } else { + try { + int index = Integer.parseInt(source); + cap = new VideoCapture(index); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid camera source: " + source); + } + } + + if (!cap.isOpened()) { + statusLabel.setText("Failed to open: " + source); + return; + } + + timer = new Timer(30, e -> updateFrame()); + timer.start(); + statusLabel.setText("Active: " + source); + } + + private void updateFrame() { + Mat frame = new Mat(); + if (!cap.read(frame)) { + statusLabel.setText("Error reading frame from: " + source); + return; + } + + // Process every Nth frame for detection + if (frameCounter++ % DETECTION_INTERVAL == 0 && ModelLoader.getNet() != null) { + if (detectionThread == null || detectionThread.isDone()) { + Mat frameCopy = frame.clone(); + detectionThread = new DetectionThread( + ModelLoader.getNet(), + ModelLoader.getClasses(), + ModelLoader.getOutputLayers(), + frameCopy, + this::updateWithDetections + ); + detectionThread.execute(); + } + } + + displayFrame(frame); + } + + private void updateWithDetections(Mat frame) { + SwingUtilities.invokeLater(() -> displayFrame(frame)); + } + + private void displayFrame(Mat frame) { + BufferedImage image = ImageUtils.matToBufferedImage(frame); + ImageIcon icon = new ImageIcon(image); + videoLabel.setIcon(icon); + } + + private void takeSnapshot(ActionEvent e) { + Mat frame = new Mat(); + if (cap.read(frame) && !frame.empty()) { + ImageUtils.saveImage(frame); + } + } + + public void stop() { + timer.stop(); + if (cap != null) cap.release(); + if (detectionThread != null) detectionThread.cancel(true); + + Container parent = getParent(); + if (parent != null) { + parent.remove(this); + parent.revalidate(); + parent.repaint(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/jdetect/DetectionThread.java b/src/main/java/org/jdetect/DetectionThread.java new file mode 100644 index 0000000..005564c --- /dev/null +++ b/src/main/java/org/jdetect/DetectionThread.java @@ -0,0 +1,127 @@ +package org.jdetect; + +import org.opencv.core.*; +import org.opencv.dnn.Dnn; +import org.opencv.imgproc.Imgproc; +import org.opencv.dnn.Net; + +import javax.swing.*; +import java.util.ArrayList; +import java.util.List; + +public class DetectionThread extends SwingWorker { + private final Net net; + private final List classes; + private final List outputLayers; + private final Mat frame; + private final DetectionCallback callback; + + public DetectionThread(Net net, List classes, List outputLayers, + Mat frame, DetectionCallback callback) { + this.net = net; + this.classes = classes; + this.outputLayers = outputLayers; + this.frame = frame; + this.callback = callback; + } + + @Override + protected Mat doInBackground() { + if (net == null || classes == null) return frame; + + try { + // Create blob from image (resize to 416x416 for YOLO) + Mat blob = Dnn.blobFromImage(frame, 1.0/255.0, new Size(416, 416), + new Scalar(0), true, false); + + net.setInput(blob); + List outs = new ArrayList<>(); + net.forward(outs, outputLayers); + + // Process detections + processDetections(frame, outs); + return frame; + } catch (Exception e) { + e.printStackTrace(); + return frame; + } + } + + private void processDetections(Mat frame, List outs) { + List boxes = new ArrayList<>(); + List confidences = new ArrayList<>(); + List classIds = new ArrayList<>(); + + for (Mat out : outs) { + float[] data = new float[(int) out.total() * out.channels()]; + out.get(0, 0, data); + + for (int i = 0; i < out.rows(); i++) { + int offset = i * out.cols(); + float confidence = data[offset + 4]; + + if (confidence > 0.5) { // Confidence threshold + int classId = 0; + float maxScore = 0; + for (int j = 5; j < out.cols(); j++) { + if (data[offset + j] > maxScore) { + maxScore = data[offset + j]; + classId = j - 5; + } + } + + if (maxScore > 0.5 && classId < classes.size()) { + float width = data[offset + 2] * frame.cols(); + float height = data[offset + 3] * frame.rows(); + float x = (data[offset] * frame.cols()) - (width / 2); + float y = (data[offset + 1] * frame.rows()) - (height / 2); + + boxes.add(new Rect((int)x, (int)y, (int)width, (int)height)); + confidences.add(confidence); + classIds.add(classId); + } + } + } + } + + // Apply Non-Maximum Suppression + List rect2dBoxes = new ArrayList<>(); + for (Rect box : boxes) { + rect2dBoxes.add(new Rect2d(box.x, box.y, box.width, box.height)); + } + MatOfRect2d boxesMat = new MatOfRect2d(); + boxesMat.fromList(rect2dBoxes); + + MatOfFloat confidencesMat = new MatOfFloat(); + confidencesMat.fromList(confidences); + MatOfInt indices = new MatOfInt(); + Dnn.NMSBoxes(boxesMat, confidencesMat, 0.5f, 0.4f, indices); + + // Draw detections + for (int i : indices.toArray()) { + Rect box = boxes.get(i); + String label = classes.get(classIds.get(i)); + float confidence = confidences.get(i); + + Imgproc.rectangle(frame, box, new Scalar(0, 255, 0), 2); + String text = String.format("%s %.2f", label, confidence); + Imgproc.putText(frame, text, new Point(box.x, box.y - 5), + Imgproc.FONT_HERSHEY_SIMPLEX, 0.5, new Scalar(0, 255, 0), 1); + } + } + + @Override + protected void done() { + try { + if (!isCancelled()) { + callback.onDetectionComplete(get()); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public interface DetectionCallback { + void onDetectionComplete(Mat frame); + } +} diff --git a/src/main/java/org/jdetect/ImageUtils.java b/src/main/java/org/jdetect/ImageUtils.java new file mode 100644 index 0000000..f1f85dd --- /dev/null +++ b/src/main/java/org/jdetect/ImageUtils.java @@ -0,0 +1,68 @@ +package org.jdetect; + +import org.opencv.core.*; +import org.opencv.imgcodecs.Imgcodecs; +import org.opencv.imgproc.Imgproc; + +import javax.swing.*; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class ImageUtils { + public static BufferedImage matToBufferedImage(Mat mat) { + if (mat.channels() == 1) { + return grayMatToBufferedImage(mat); + } else { + return colorMatToBufferedImage(mat); + } + } + + private static BufferedImage grayMatToBufferedImage(Mat mat) { + BufferedImage image = new BufferedImage(mat.cols(), mat.rows(), BufferedImage.TYPE_BYTE_GRAY); + byte[] data = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); + mat.get(0, 0, data); + return image; + } + + private static BufferedImage colorMatToBufferedImage(Mat mat) { + Mat rgb = new Mat(); + Imgproc.cvtColor(mat, rgb, Imgproc.COLOR_BGR2RGB); + + BufferedImage image = new BufferedImage(rgb.cols(), rgb.rows(), BufferedImage.TYPE_3BYTE_BGR); + byte[] data = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); + rgb.get(0, 0, data); + return image; + } + + public static void saveImage(Mat frame) { + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setDialogTitle("Save Snapshot"); + + // Suggest filename with timestamp + String timestamp = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date()); + fileChooser.setSelectedFile(new File("snapshot_" + timestamp + ".jpg")); + + if (fileChooser.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) { + File file = fileChooser.getSelectedFile(); + String filename = file.toString(); + + // Ensure extension + if (!filename.toLowerCase().endsWith(".jpg") && !filename.toLowerCase().endsWith(".png")) { + filename += ".jpg"; + } + + // Convert to RGB before saving + Mat rgb = new Mat(); + Imgproc.cvtColor(frame, rgb, Imgproc.COLOR_BGR2RGB); + + if (Imgcodecs.imwrite(filename, rgb)) { + JOptionPane.showMessageDialog(null, "Image saved successfully!"); + } else { + JOptionPane.showMessageDialog(null, "Failed to save image!", "Error", JOptionPane.ERROR_MESSAGE); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/org/jdetect/MainApp.java b/src/main/java/org/jdetect/MainApp.java new file mode 100644 index 0000000..369dcf5 --- /dev/null +++ b/src/main/java/org/jdetect/MainApp.java @@ -0,0 +1,127 @@ +package org.jdetect; + +import org.opencv.core.Core; +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.io.IOException; + +public class MainApp extends JFrame { + private final JPanel camerasPanel; + + static { + System.loadLibrary(Core.NATIVE_LIBRARY_NAME); + } + + public MainApp() { + setTitle("Multi-Camera Object Detection"); + setDefaultCloseOperation(EXIT_ON_CLOSE); + setSize(1200, 800); + + // Load model + try { + ModelLoader.loadModel(); + } catch (IOException e) { + JOptionPane.showMessageDialog(this, "Failed to load model: " + e.getMessage(), + "Error", JOptionPane.ERROR_MESSAGE); + System.exit(1); + } + + // Main content + camerasPanel = new JPanel(new WrapLayout(FlowLayout.LEFT, 10, 10)); + add(new JScrollPane(camerasPanel), BorderLayout.CENTER); + + // Control panel + JPanel controlPanel = new JPanel(); + + JButton addCameraBtn = new JButton("Add Camera"); + addCameraBtn.addActionListener(this::addCamera); + controlPanel.add(addCameraBtn); + + JButton addStreamBtn = new JButton("Add Stream"); + addStreamBtn.addActionListener(this::addStream); + controlPanel.add(addStreamBtn); + + add(controlPanel, BorderLayout.SOUTH); + + // Add default camera + addCameraPanel("0"); + } + + private void addCamera(ActionEvent e) { + String input = JOptionPane.showInputDialog(this, "Enter camera index (0, 1, etc.):", "0"); + if (input != null && !input.trim().isEmpty()) { + addCameraPanel(input.trim()); + } + } + + private void addStream(ActionEvent e) { + String input = JOptionPane.showInputDialog(this, + "Enter stream URL (rtsp:// or http://):", + "rtsp://example.com/stream"); + if (input != null && !input.trim().isEmpty()) { + addCameraPanel(input.trim()); + } + } + + private void addCameraPanel(String source) { + CameraPanel panel = new CameraPanel(source); + camerasPanel.add(panel); + camerasPanel.revalidate(); + } + + public static void main(String[] args) { + try { + SwingUtilities.invokeAndWait(() -> { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception e) { + e.printStackTrace(); + } + + MainApp app = new MainApp(); + app.setVisible(true); + }); + } catch (Exception ex) { + ex.printStackTrace(); + System.exit(1); + } + } +} + +// Helper class for wrapping camera panels +class WrapLayout extends FlowLayout { + + public WrapLayout(int align, int hgap, int vgap) { + super(align, hgap, vgap); + } + + @Override + public Dimension preferredLayoutSize(Container target) { + synchronized (target.getTreeLock()) { + int targetWidth = target.getSize().width; + if (targetWidth == 0) { + targetWidth = Integer.MAX_VALUE; + } + + int x = 0; + int y = 0; + int rowHeight = 0; + + for (Component comp : target.getComponents()) { + if (comp.isVisible()) { + Dimension pref = comp.getPreferredSize(); + if (x + pref.width > targetWidth) { + y += rowHeight + getVgap(); + x = 0; + rowHeight = 0; + } + x += pref.width + getHgap(); + rowHeight = Math.max(rowHeight, pref.height); + } + } + y += rowHeight; + return new Dimension(targetWidth, y); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/jdetect/ModelLoader.java b/src/main/java/org/jdetect/ModelLoader.java new file mode 100644 index 0000000..c1784ea --- /dev/null +++ b/src/main/java/org/jdetect/ModelLoader.java @@ -0,0 +1,77 @@ +package org.jdetect; + +import org.opencv.core.*; +import org.opencv.dnn.Dnn; +import org.opencv.dnn.Net; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; +import java.nio.file.Files; +import java.nio.file.Paths; + + +public class ModelLoader { + private static Net net; + private static List classes; + private static List outputLayers; + + public static synchronized void loadModel() throws IOException { + if (net != null) return; + + // Load network + InputStream weightsStream = ModelLoader.class.getResourceAsStream("/yolov4-tiny.weights"); + InputStream configStream = ModelLoader.class.getResourceAsStream("/yolov4-tiny.cfg"); + + assert weightsStream != null; + byte[] weights = weightsStream.readAllBytes(); + assert configStream != null; + byte[] config = configStream.readAllBytes(); + + // Create temp files + String weightsPath = "temp_weights.weights"; + String configPath = "temp_config.cfg"; + + Files.write(Paths.get(weightsPath), weights); + Files.write(Paths.get(configPath), config); + + net = Dnn.readNetFromDarknet(configPath, weightsPath); + + // Load classes + classes = new ArrayList<>(); + try (InputStream classesStream = ModelLoader.class.getResourceAsStream("/coco.names")) { + assert classesStream != null; + try (BufferedReader br = new BufferedReader(new InputStreamReader(classesStream))) { + String line; + while ((line = br.readLine()) != null) { + classes.add(line.trim()); + } + } + } + + // Get output layers + List layerNames = net.getLayerNames(); + MatOfInt unconnectedOutLayers = net.getUnconnectedOutLayers(); + int[] indices = unconnectedOutLayers.toArray(); + + outputLayers = new ArrayList<>(); + for (int idx : indices) { + outputLayers.add(layerNames.get(idx - 1)); + } + } + + public static Net getNet() { + return net; + } + + public static List getClasses() { + return classes; + } + + public static List getOutputLayers() { + return outputLayers; + } +} \ No newline at end of file diff --git a/src/main/resources/coco.names b/src/main/resources/coco.names new file mode 100644 index 0000000..ca76c80 --- /dev/null +++ b/src/main/resources/coco.names @@ -0,0 +1,80 @@ +person +bicycle +car +motorbike +aeroplane +bus +train +truck +boat +traffic light +fire hydrant +stop sign +parking meter +bench +bird +cat +dog +horse +sheep +cow +elephant +bear +zebra +giraffe +backpack +umbrella +handbag +tie +suitcase +frisbee +skis +snowboard +sports ball +kite +baseball bat +baseball glove +skateboard +surfboard +tennis racket +bottle +wine glass +cup +fork +knife +spoon +bowl +banana +apple +sandwich +orange +broccoli +carrot +hot dog +pizza +donut +cake +chair +sofa +pottedplant +bed +diningtable +toilet +tvmonitor +laptop +mouse +remote +keyboard +cell phone +microwave +oven +toaster +sink +refrigerator +book +clock +vase +scissors +teddy bear +hair drier +toothbrush diff --git a/src/main/resources/yolov4-tiny.cfg b/src/main/resources/yolov4-tiny.cfg new file mode 100644 index 0000000..d990b51 --- /dev/null +++ b/src/main/resources/yolov4-tiny.cfg @@ -0,0 +1,294 @@ +[net] +# Testing +#batch=1 +#subdivisions=1 +# Training +batch=64 +subdivisions=1 +width=416 +height=416 +channels=3 +momentum=0.9 +decay=0.0005 +angle=0 +saturation = 1.5 +exposure = 1.5 +hue=.1 + +learning_rate=0.00261 +burn_in=1000 + +max_batches = 2000200 +policy=steps +steps=1600000,1800000 +scales=.1,.1 + + +#weights_reject_freq=1001 +#ema_alpha=0.9998 +#equidistant_point=1000 +#num_sigmas_reject_badlabels=3 +#badlabels_rejection_percentage=0.2 + + +[convolutional] +batch_normalize=1 +filters=32 +size=3 +stride=2 +pad=1 +activation=leaky + +[convolutional] +batch_normalize=1 +filters=64 +size=3 +stride=2 +pad=1 +activation=leaky + +[convolutional] +batch_normalize=1 +filters=64 +size=3 +stride=1 +pad=1 +activation=leaky + +[route] +layers=-1 +groups=2 +group_id=1 + +[convolutional] +batch_normalize=1 +filters=32 +size=3 +stride=1 +pad=1 +activation=leaky + +[convolutional] +batch_normalize=1 +filters=32 +size=3 +stride=1 +pad=1 +activation=leaky + +[route] +layers = -1,-2 + +[convolutional] +batch_normalize=1 +filters=64 +size=1 +stride=1 +pad=1 +activation=leaky + +[route] +layers = -6,-1 + +[maxpool] +size=2 +stride=2 + +[convolutional] +batch_normalize=1 +filters=128 +size=3 +stride=1 +pad=1 +activation=leaky + +[route] +layers=-1 +groups=2 +group_id=1 + +[convolutional] +batch_normalize=1 +filters=64 +size=3 +stride=1 +pad=1 +activation=leaky + +[convolutional] +batch_normalize=1 +filters=64 +size=3 +stride=1 +pad=1 +activation=leaky + +[route] +layers = -1,-2 + +[convolutional] +batch_normalize=1 +filters=128 +size=1 +stride=1 +pad=1 +activation=leaky + +[route] +layers = -6,-1 + +[maxpool] +size=2 +stride=2 + +[convolutional] +batch_normalize=1 +filters=256 +size=3 +stride=1 +pad=1 +activation=leaky + +[route] +layers=-1 +groups=2 +group_id=1 + +[convolutional] +batch_normalize=1 +filters=128 +size=3 +stride=1 +pad=1 +activation=leaky + +[convolutional] +batch_normalize=1 +filters=128 +size=3 +stride=1 +pad=1 +activation=leaky + +[route] +layers = -1,-2 + +[convolutional] +batch_normalize=1 +filters=256 +size=1 +stride=1 +pad=1 +activation=leaky + +[route] +layers = -6,-1 + +[maxpool] +size=2 +stride=2 + +[convolutional] +batch_normalize=1 +filters=512 +size=3 +stride=1 +pad=1 +activation=leaky + +################################## + +[convolutional] +batch_normalize=1 +filters=256 +size=1 +stride=1 +pad=1 +activation=leaky + +[convolutional] +batch_normalize=1 +filters=512 +size=3 +stride=1 +pad=1 +activation=leaky + +[convolutional] +size=1 +stride=1 +pad=1 +filters=255 +activation=linear + + + +[yolo] +mask = 3,4,5 +anchors = 10,14, 23,27, 37,58, 81,82, 135,169, 344,319 +classes=80 +num=6 +jitter=.3 +scale_x_y = 1.05 +cls_normalizer=1.0 +iou_normalizer=0.07 +iou_loss=ciou +ignore_thresh = .7 +truth_thresh = 1 +random=0 +resize=1.5 +nms_kind=greedynms +beta_nms=0.6 +#new_coords=1 +#scale_x_y = 2.0 + +[route] +layers = -4 + +[convolutional] +batch_normalize=1 +filters=128 +size=1 +stride=1 +pad=1 +activation=leaky + +[upsample] +stride=2 + +[route] +layers = -1, 23 + +[convolutional] +batch_normalize=1 +filters=256 +size=3 +stride=1 +pad=1 +activation=leaky + +[convolutional] +size=1 +stride=1 +pad=1 +filters=255 +activation=linear + +[yolo] +mask = 1,2,3 +anchors = 10,14, 23,27, 37,58, 81,82, 135,169, 344,319 +classes=80 +num=6 +jitter=.3 +scale_x_y = 1.05 +cls_normalizer=1.0 +iou_normalizer=0.07 +iou_loss=ciou +ignore_thresh = .7 +truth_thresh = 1 +random=0 +resize=1.5 +nms_kind=greedynms +beta_nms=0.6 +#new_coords=1 +#scale_x_y = 2.0 diff --git a/src/main/resources/yolov4-tiny.weights b/src/main/resources/yolov4-tiny.weights new file mode 100644 index 0000000..27edc5d Binary files /dev/null and b/src/main/resources/yolov4-tiny.weights differ