Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/java/dev/bot/zeno/app/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ private void initCycle() {
/** Exposes REST, WebSocket and metrics endpoints. */
private void initApi() {
// Start debug dashboard server exposing camera stream and PSI events
debugServer = new DebugServer(debugCfg, frameBuffer, debugEvents, detections, fps::get);
debugServer = new DebugServer(debugCfg, cfg, frameBuffer, debugEvents, detections, fps::get);
debugServer.start(bus, layerEvents, wsSessions);
}

Expand Down
23 changes: 7 additions & 16 deletions src/main/java/dev/bot/zeno/debug/DebugServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import dev.bot.zeno.app.EventBus;
import dev.bot.zeno.overlay.DetectionResult;
import dev.bot.zeno.domain.Event;
import dev.bot.zeno.util.Config;
import io.javalin.Javalin;
import io.javalin.plugin.bundled.CorsPluginConfig;
import io.javalin.websocket.WsConfig;
Expand Down Expand Up @@ -43,6 +44,7 @@
*/
public final class DebugServer {
private final DebugConfig cfg;
private final Config appCfg;
private final FrameBuffer buffer;
private final BlockingQueue<Event> events;
private final AtomicReference<DetectionResult> detections;
Expand All @@ -53,11 +55,13 @@ public final class DebugServer {
private final ObjectMapper mapper = new ObjectMapper();

public DebugServer(DebugConfig cfg,
Config appCfg,
FrameBuffer buffer,
BlockingQueue<Event> events,
AtomicReference<DetectionResult> detections,
Supplier<Double> fpsSupplier) {
this.cfg = cfg;
this.appCfg = appCfg;
this.buffer = buffer;
this.events = events;
this.detections = detections;
Expand Down Expand Up @@ -213,22 +217,9 @@ private void broadcastDetections() {
List<Map<String, Object>> objects = new ArrayList<>();
synchronized (dr.yoloBoxes) {
for (DetectionResult.Box b : dr.yoloBoxes) {
Map<String, Object> o = new LinkedHashMap<>();
o.put("trackId", b.id);
o.put("classId", 0); // classId não informado pela pipeline atual
o.put("className", b.label);
o.put("confidence", (double) b.confidence);
o.put("bbox", Map.of(
"x", b.rect.x(),
"y", b.rect.y(),
"w", b.rect.width(),
"h", b.rect.height()
));
o.put("centroid", Map.of(
"x", b.rect.x() + b.rect.width() / 2,
"y", b.rect.y() + b.rect.height() / 2
));
objects.add(o);
// Delegates serialization to the model, which already handles optional
// contour coordinates when present.
objects.add(b.toJson());
}
}

Expand Down
144 changes: 144 additions & 0 deletions src/main/java/dev/bot/zeno/dnn/ContourExtractor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package dev.bot.zeno.dnn;

import org.bytedeco.opencv.opencv_core.*;


import static org.bytedeco.opencv.global.opencv_imgproc.*;
import static org.bytedeco.opencv.global.opencv_core.*;

/**
* Utility responsible for extracting a polygonal contour from a region of
* interest. The implementation relies on simple Canny edge detection followed
* by morphological operations and contour filtering. Instances of this class
* reuse internal {@link Mat} buffers to avoid repeated allocations and keep the
* GC pressure low. It is <b>not</b> thread safe and should be confined to the
* detector thread that owns it.
*/
public final class ContourExtractor {

/** Parameters controlling the contour extraction. */
public static final class Params {
/** Minimum area (in pixels) for a contour to be considered. */
public final double minArea;
/** Minimum solidity ratio (area / convex hull area). */
public final double minSolidity;
/** Epsilon percentage for polygon approximation. */
public final double approxEpsPct;

public Params(double minArea, double minSolidity, double approxEpsPct) {
this.minArea = minArea;
this.minSolidity = minSolidity;
this.approxEpsPct = approxEpsPct;
}
}

private final Mat gray = new Mat();
private final Mat edges = new Mat();
private final Mat morphed = new Mat();
private final Mat hierarchy = new Mat();
private final Mat kernel = getStructuringElement(MORPH_RECT, new Size(3, 3));

/**
* Extracts a polygonal contour from the provided frame and bounding box.
* Coordinates of the resulting polygon are absolute within the frame.
*
* @param frame full color frame
* @param bbox bounding box defining the ROI
* @param p parameters
* @return array of {@link Point} representing the polygon or {@code null}
* when no suitable contour is found
*/
public synchronized Point[] extract(Mat frame, Rect bbox, Params p) {
// Defensive checks
if (frame == null || frame.empty() || bbox == null) {
return null;
}

Rect clipped = new Rect(
Math.max(0, bbox.x()), Math.max(0, bbox.y()),
Math.min(bbox.width(), frame.cols() - bbox.x()),
Math.min(bbox.height(), frame.rows() - bbox.y())
);
if (clipped.width() <= 0 || clipped.height() <= 0) {
return null;
}

try (Mat roi = new Mat(frame, clipped)) {
// 1. grayscale + blur
cvtColor(roi, gray, COLOR_BGR2GRAY);
GaussianBlur(gray, gray, new Size(3,3), 0);

// 2. edge detection
Canny(gray, edges, 50, 150,3,false);

// 3. morphology (dilate -> erode) to close gaps
dilate(edges, morphed, kernel);
erode(morphed, morphed, kernel);

// 4. find contours
MatVector contours = new MatVector();
findContours(morphed, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);

Mat best = null;
double bestArea = 0.0;
for (int i = 0; i < contours.size(); i++) {
Mat c = contours.get(i);
double area = contourArea(c);
if (area < p.minArea) {
c.release();
continue;
}
Mat hull = new Mat();
convexHull(c, hull);
double hullArea = Math.max(contourArea(hull), 1.0);
double solidity = area / hullArea;
if (solidity < p.minSolidity) {
hull.release();
c.release();
continue;
}
if (area > bestArea) {
if (best != null) best.release();
best = c; // transfer ownership
bestArea = area;
} else {
c.release();
}
hull.release();
}

if (best == null) {
contours.deallocate();
return null;
}

// 5. Approximate polygon
Mat approx = new Mat();
double eps = p.approxEpsPct * arcLength(best, true);
approxPolyDP(best, approx, eps, true);

PointVector vec = new PointVector(approx);
int n = (int) vec.size();
if (n <= 2) {
vec.close();
approx.release();
best.release();
contours.deallocate();
return null; // not a polygon
}

Point[] pts = new Point[n];
for (int i = 0; i < n; i++) {
Point p0 = vec.get(i);
pts[i] = new Point(p0.x() + clipped.x(), p0.y() + clipped.y());
}

vec.close();
approx.release();
best.release();
contours.deallocate();
return pts;
}
}
}

53 changes: 45 additions & 8 deletions src/main/java/dev/bot/zeno/dnn/YoloDetector.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ public class YoloDetector implements Runnable {
private int frameCount = 0;
private final ObjectMemory memory;

// Contour extraction configuration
private final boolean contourEnabled;
private final int contourMaxPerFrame;
private final double contourMinArea;
private final double contourMinSolidity;
private final double contourApproxEpsPct;
private final ContourExtractor contourExtractor;

/**
* Conjunto de classes COCO que desejamos desenhar na tela.
* Apenas objetos pertencentes a esta lista serão reportados
Expand Down Expand Up @@ -77,6 +85,13 @@ public YoloDetector(Config cfg, AtomicReference<Mat> latestFrame,
confThreshold = (float) cfg.getDouble("yolo.confThreshold", 0.35);
nmsThreshold = (float) cfg.getDouble("yolo.nmsThreshold", 0.4);
skip = Math.max(0, cfg.getInt("processing.skipFramesYolo", 0));

contourEnabled = cfg.getBool("vision.contour.enabled", false);
contourMaxPerFrame = cfg.getInt("vision.contour.maxPerFrame", 5);
contourMinArea = cfg.getDouble("vision.contour.minArea", 100.0);
contourMinSolidity = cfg.getDouble("vision.contour.minSolidity", 0.8);
contourApproxEpsPct = cfg.getDouble("vision.contour.approxEpsPct", 0.02);
contourExtractor = contourEnabled ? new ContourExtractor() : null;
}

@Override
Expand Down Expand Up @@ -158,6 +173,7 @@ public void run() {
DetectionResult res = latestDetections.get();
List<DetectionResult.Box> newBoxes = new ArrayList<>();
Map<Integer, Rect2d> updatedTracks = new HashMap<>();
int contourCount = 0;
for (int i = 0; i < indices.limit(); i++) {
int idx = indices.get(i);
Rect2d b = boxes.get(idx);
Expand Down Expand Up @@ -198,14 +214,35 @@ public void run() {
}

float conf = confidences.get(idx);
newBoxes.add(new DetectionResult.Box(id, rect, label, conf));
Map<String, Object> payload = Map.of(
"symbol", "object:" + label,
"class", label,
"conf", (double) conf,
"trackId", id,
"bbox", List.of(rect.x(), rect.y(), rect.width(), rect.height())
);

Point[] contour = null;
if (contourEnabled && contourCount < contourMaxPerFrame && contourExtractor != null) {
contour = contourExtractor.extract(frame, rect,
new ContourExtractor.Params(contourMinArea, contourMinSolidity, contourApproxEpsPct));
if (contour != null) {
contourCount++;
}
}

DetectionResult.Box box = new DetectionResult.Box(id, rect, label, conf);
if (contour != null) {
box.setContour(contour);
}
newBoxes.add(box);

Map<String, Object> payload = new LinkedHashMap<>();
payload.put("symbol", "object:" + label);
payload.put("class", label);
payload.put("conf", (double) conf);
payload.put("trackId", id);
payload.put("bbox", List.of(rect.x(), rect.y(), rect.width(), rect.height()));
if (contour != null) {
List<Map<String, Integer>> pts = new ArrayList<>();
for (Point p0 : contour) {
pts.add(Map.of("x", p0.x(), "y", p0.y()));
}
payload.put("contour", pts);
}
Event ev = new Event.Builder()
.type("perception")
.payload(payload)
Expand Down
13 changes: 12 additions & 1 deletion src/main/java/dev/bot/zeno/overlay/DetectionOverlay.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,18 @@ public static void drawAll(Mat img, DetectionResult res, Config cfg) {
for (DetectionResult.Box b : res.yoloBoxes) {
// Seleciona cor específica para cada classe.
Scalar color = YOLO_COLORS.getOrDefault(b.label, new Scalar(0, 128, 255, 0));
rectangle(img, b.rect, color, 2, LINE_8, 0);

Point[] contour = b.getContour();
if (cfg.getBool("vision.contour.enabled", false) && contour != null) {
for (int i = 0; i < contour.length; i++) {
Point p1 = contour[i];
Point p2 = contour[(i + 1) % contour.length];
line(img, p1, p2, color, 2, LINE_8, 0);
}
} else {
rectangle(img, b.rect, color, 2, LINE_8, 0);
}

String text = b.label + " #" + b.id + String.format(" %.2f", b.confidence);
putText(img, text,
new Point(b.rect.x(), Math.max(0, b.rect.y() - 5)),
Expand Down
43 changes: 43 additions & 0 deletions src/main/java/dev/bot/zeno/overlay/DetectionResult.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package dev.bot.zeno.overlay;

import org.bytedeco.opencv.opencv_core.Point;
import org.bytedeco.opencv.opencv_core.Rect;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class DetectionResult {
/**
Expand All @@ -20,13 +24,52 @@ public static class Box {
public final Rect rect;
public final String label;
public final float confidence;
/** Optional polygonal contour with coordinates in the frame space. */
private Point[] contour;

public Box(int id, Rect r, String label, float confidence) {
this.id = id;
this.rect = r;
this.label = label;
this.confidence = confidence;
}

/** Returns the polygonal contour or {@code null} when not available. */
public Point[] getContour() { return contour; }

/** Updates the contour polygon for this box. */
public void setContour(Point[] contour) { this.contour = contour; }

/**
* Serializa esta detecção para uma estrutura de dados simples, adequada
* para conversão em JSON. Coordenadas de contorno são convertidas em
* pares {@code {x,y}} para manter o payload leve e explícito.
*/
public Map<String, Object> toJson() {
Map<String, Object> o = new LinkedHashMap<>();
o.put("trackId", id);
o.put("classId", 0); // classId ainda não é informado
o.put("className", label);
o.put("confidence", (double) confidence);
o.put("bbox", Map.of(
"x", rect.x(),
"y", rect.y(),
"w", rect.width(),
"h", rect.height()
));
o.put("centroid", Map.of(
"x", rect.x() + rect.width() / 2,
"y", rect.y() + rect.height() / 2
));
if (contour != null && contour.length > 0) {
List<Map<String, Integer>> pts = new ArrayList<>(contour.length);
for (Point p : contour) {
pts.add(Map.of("x", p.x(), "y", p.y()));
}
o.put("contour", pts);
}
return o;
}
}

public final List<Box> yoloBoxes = Collections.synchronizedList(new ArrayList<>());
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/config.properties
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ output.drawFaces=true

# === Vision ===
vision.overlay.enabled=false
vision.contour.enabled=false
vision.contour.method=canny
vision.contour.minArea=100.0
vision.contour.minSolidity=0.8
vision.contour.approxEpsPct=0.02
vision.contour.maxPerFrame=5
# === Data ===
data.faceDb=data/face_db.json
# === Object Memory ===
Expand Down
Loading