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
15 changes: 10 additions & 5 deletions src/main/java/dev/bot/zeno/app/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

Expand Down Expand Up @@ -47,14 +48,16 @@ public static void main(String[] args) {
AtomicReference<DetectionResult> detections =
new AtomicReference<>(new DetectionResult());
BlockingQueue<Event> events = new ArrayBlockingQueue<>(100);
AtomicBoolean yoloEnabled = new AtomicBoolean(cfg.getBool("yolo.enabled", true));
AtomicBoolean faceEnabled = new AtomicBoolean(cfg.getBool("face.enabled", true));

startDetectors(cfg, latestFrame, detections, events);
startDetectors(cfg, latestFrame, detections, events, yoloEnabled, faceEnabled);

FrameBuffer buffer = new FrameBuffer();
AtomicReference<Double> fps = new AtomicReference<>(0.0);
startPreviewPipeline(cfg, opts, latestFrame, detections, buffer, fps);

DebugServer server = new DebugServer(opts, buffer, events, detections, fps::get);
DebugServer server = new DebugServer(opts, buffer, events, detections, fps::get, yoloEnabled, faceEnabled);
server.start();
}

Expand All @@ -74,13 +77,15 @@ private static CameraManager initCamera(Config cfg, AtomicReference<Mat> latestF
private static void startDetectors(Config cfg,
AtomicReference<Mat> latestFrame,
AtomicReference<DetectionResult> detections,
BlockingQueue<Event> events) {
startDaemonThread("yolo-detector", new YoloDetector(cfg, latestFrame, detections, events, null));
BlockingQueue<Event> events,
AtomicBoolean yoloEnabled,
AtomicBoolean faceEnabled) {
startDaemonThread("yolo-detector", new YoloDetector(cfg, latestFrame, detections, events, null, yoloEnabled));

ArcFaceRecognizer recognizer = cfg.getBool("arcface.enabled", true)
? new ArcFaceRecognizer(cfg) : null;
startDaemonThread("face-detector",
new FaceDetector(cfg, latestFrame, detections, events, recognizer));
new FaceDetector(cfg, latestFrame, detections, events, recognizer, faceEnabled));
}

/**
Expand Down
42 changes: 39 additions & 3 deletions src/main/java/dev/bot/zeno/debug/DebugServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.io.OutputStream;
import java.lang.management.ManagementFactory;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

Expand All @@ -35,18 +36,24 @@ public final class DebugServer {
private final BlockingQueue<Event> events;
private final AtomicReference<DetectionResult> detections;
private final Supplier<Double> fpsSupplier;
private final AtomicBoolean yoloEnabled;
private final AtomicBoolean faceEnabled;
private final PrometheusMeterRegistry registry;

public DebugServer(DebugConfig cfg,
FrameBuffer buffer,
BlockingQueue<Event> events,
AtomicReference<DetectionResult> detections,
Supplier<Double> fpsSupplier) {
BlockingQueue<Event> events,
AtomicReference<DetectionResult> detections,
Supplier<Double> fpsSupplier,
AtomicBoolean yoloEnabled,
AtomicBoolean faceEnabled) {
this.cfg = cfg;
this.buffer = buffer;
this.events = events;
this.detections = detections;
this.fpsSupplier = fpsSupplier;
this.yoloEnabled = yoloEnabled;
this.faceEnabled = faceEnabled;
this.registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
Metrics.addRegistry(registry);
}
Expand All @@ -69,6 +76,35 @@ public void start() {

app.get("/metrics", ctx -> ctx.result(registry.scrape()));

app.post("/yolo/{state}", ctx -> {
// Endpoint para ativar/desativar a detecção YOLO em tempo real.
String state = ctx.pathParam("state");
boolean enable = "on".equalsIgnoreCase(state);
// Valida o valor recebido para evitar estados inesperados.
if (!enable && !"off".equalsIgnoreCase(state)) {
ctx.status(400).result("state must be 'on' or 'off'");
return;
}
yoloEnabled.set(enable);
DetectionResult dr = detections.get();
synchronized (dr.yoloBoxes) { dr.clearYolo(); }
ctx.status(204);
});

app.post("/faces/{state}", ctx -> {
// Endpoint para ativar/desativar a detecção de faces em tempo real.
String state = ctx.pathParam("state");
boolean enable = "on".equalsIgnoreCase(state);
if (!enable && !"off".equalsIgnoreCase(state)) {
ctx.status(400).result("state must be 'on' or 'off'");
return;
}
faceEnabled.set(enable);
DetectionResult dr = detections.get();
synchronized (dr.faceBoxes) { dr.clearFaces(); }
ctx.status(204);
});

app.get("/snapshot.jpg", ctx -> {
byte[] jpeg = encodeJpeg(buffer.get(), cfg.previewWidth, cfg.jpegQuality);
ctx.contentType("image/jpeg");
Expand Down
12 changes: 11 additions & 1 deletion src/main/java/dev/bot/zeno/dnn/FaceDetector.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import dev.bot.zeno.util.QueueUtils;

Expand All @@ -26,19 +27,21 @@ public class FaceDetector implements Runnable {
private final AtomicReference<DetectionResult> latestDetections;
private final BlockingQueue<Event> eventQueue;
private final ArcFaceRecognizer recognizer;
private final AtomicBoolean enabled;
private final Net net;
private final double confTh;
private final int skip;
private int frameCount = 0;
private int nextId = 1;

public FaceDetector(Config cfg, AtomicReference<Mat> latestFrame, AtomicReference<DetectionResult> latestDetections,
BlockingQueue<Event> eq, ArcFaceRecognizer recognizer) {
BlockingQueue<Event> eq, ArcFaceRecognizer recognizer, AtomicBoolean enabled) {
this.cfg = cfg;
this.latestFrame = latestFrame;
this.latestDetections = latestDetections;
this.eventQueue = eq;
this.recognizer = recognizer;
this.enabled = enabled;
this.net = readNetFromCaffe(cfg.get("face.prototxt", "models/deploy.prototxt"),
cfg.get("face.caffemodel", "models/res10_300x300_ssd_iter_140000.caffemodel"));
net.setPreferableBackend(DNN_BACKEND_OPENCV);
Expand All @@ -50,6 +53,13 @@ public FaceDetector(Config cfg, AtomicReference<Mat> latestFrame, AtomicReferenc
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
if (!enabled.get()) {
// Quando desativado, limpamos resultados e dormimos um pouco
DetectionResult res = latestDetections.get();
synchronized (res.faceBoxes) { res.clearFaces(); }
try { Thread.sleep(50); } catch (InterruptedException e) { break; }
continue;
}
Mat src = latestFrame.get();
if (src == null || src.empty()) {
try { Thread.sleep(2); } catch (InterruptedException e) { break; }
Expand Down
12 changes: 11 additions & 1 deletion src/main/java/dev/bot/zeno/dnn/YoloDetector.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static org.bytedeco.opencv.global.opencv_core.CV_32F;
Expand All @@ -28,6 +29,7 @@ public class YoloDetector implements Runnable {
private final AtomicReference<Mat> latestFrame;
private final AtomicReference<DetectionResult> latestDetections;
private final BlockingQueue<Event> eventQueue;
private final AtomicBoolean enabled;

private final List<String> classNames;
private final Net net;
Expand All @@ -52,12 +54,13 @@ public class YoloDetector implements Runnable {

public YoloDetector(Config cfg, AtomicReference<Mat> latestFrame,
AtomicReference<DetectionResult> latestDetections,
BlockingQueue<Event> eq, ObjectMemory memory) {
BlockingQueue<Event> eq, ObjectMemory memory, AtomicBoolean enabled) {
this.cfg = cfg;
this.latestFrame = latestFrame;
this.latestDetections = latestDetections;
this.eventQueue = eq;
this.memory = memory;
this.enabled = enabled;
try {
String namesPath = cfg.get("yolo.names", "src/main/resources/coco.names");
classNames = Files.readAllLines(Paths.get(namesPath));
Expand All @@ -81,6 +84,13 @@ public YoloDetector(Config cfg, AtomicReference<Mat> latestFrame,
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
if (!enabled.get()) {
// Quando desativado, limpamos resultados e aguardamos brevemente
DetectionResult res = latestDetections.get();
synchronized (res.yoloBoxes) { res.clearYolo(); }
try { Thread.sleep(50); } catch (InterruptedException e) { break; }
continue;
}
Mat src = latestFrame.get();
if (src == null || src.empty()) {
try { Thread.sleep(2); } catch (InterruptedException e) { break; }
Expand Down
50 changes: 24 additions & 26 deletions src/main/resources/public/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,6 @@
.kpi { background:#0b1017; border:1px solid #1c2733; border-radius:12px; padding:12px; }
.kpi .label { font-size:12px; color:#93a9c3; }
.kpi .value { font-size:22px; margin-top:4px; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th, td { border-bottom: 1px solid #1f2a36; padding: 6px 8px; text-align: left; }
th { color: #93a9c3; font-weight: 600; }
footer { padding: 10px 16px; color: #6b7f97; font-size: 12px; border-top: 1px solid #1f2a36; }
.row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
.spacer { flex:1; }
Expand All @@ -48,6 +45,8 @@
<button id="snapshot">Snapshot</button>
<label><input id="toggleGrid" type="checkbox"> Grid</label>
<label><input id="toggleOverlay" type="checkbox" checked> Overlay</label>
<button id="toggleYolo">YOLO On</button>
<button id="toggleFaces">Faces On</button>
</div>
</header>

Expand Down Expand Up @@ -79,12 +78,6 @@ <h3>Métricas & Estado</h3>
<div class="kpi"><div class="label">Tracks Ativos</div><div id="kpiTracks" class="value">—</div></div>
<div class="kpi"><div class="label">Bateria (%)</div><div id="kpiBatt" class="value">—</div></div>
</div>
<div style="margin-top:12px;">
<table>
<thead><tr><th>TrackId</th><th>Classe</th><th>Confiança</th><th>BBox (x,y,w,h)</th><th>t</th></tr></thead>
<tbody id="eventsTable"></tbody>
</table>
</div>
</div>
</section>
</main>
Expand Down Expand Up @@ -114,11 +107,14 @@ <h3>Métricas & Estado</h3>
const kpiRam = document.getElementById('kpiRam');
const kpiTracks = document.getElementById('kpiTracks');
const kpiBatt = document.getElementById('kpiBatt');
const eventsTable = document.getElementById('eventsTable');
const toggleYolo = document.getElementById('toggleYolo');
const toggleFaces = document.getElementById('toggleFaces');

let playing = true;
let currentStreamUrl = "";
let eventsAbort = null;
let yoloOn = true;
let facesOn = true;

// If served via file://, default API calls to localhost:8080.
const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:8080' : '';
Expand Down Expand Up @@ -164,6 +160,22 @@ <h3>Métricas & Estado</h3>
gridEl.style.display = toggleGrid.checked ? 'block' : 'none';
};

toggleYolo.onclick = async () => {
yoloOn = !yoloOn;
toggleYolo.textContent = 'YOLO ' + (yoloOn ? 'On' : 'Off');
lastEvents = [];
drawOverlays();
try { await fetch(API_BASE + '/yolo/' + (yoloOn ? 'on' : 'off'), { method: 'POST' }); } catch {}
};

toggleFaces.onclick = async () => {
facesOn = !facesOn;
toggleFaces.textContent = 'Faces ' + (facesOn ? 'On' : 'Off');
lastEvents = [];
drawOverlays();
try { await fetch(API_BASE + '/faces/' + (facesOn ? 'on' : 'off'), { method: 'POST' }); } catch {}
};

function resizeOverlayToImg() {
const rect = videoImg.getBoundingClientRect();
overlay.width = rect.width;
Expand All @@ -174,7 +186,7 @@ <h3>Métricas & Estado</h3>
ro.observe(document.body);

// Render de overlays
let lastEvents = []; // guarda últimos N para tabela
let lastEvents = []; // armazena últimos eventos para desenhar
function drawOverlays() {
if (!toggleOverlay.checked) {
octx.clearRect(0, 0, overlay.width, overlay.height);
Expand Down Expand Up @@ -227,10 +239,9 @@ <h3>Métricas & Estado</h3>
if (!line) continue;
try {
const ev = JSON.parse(line);
// Atualiza tabela (mantém últimos 12)
// Guarda últimos eventos para desenhar overlays
lastEvents.unshift(ev);
if (lastEvents.length > 12) lastEvents.pop();
renderEventsTable();
drawOverlays();
} catch {}
}
Expand All @@ -248,19 +259,6 @@ <h3>Métricas & Estado</h3>
}
}

function renderEventsTable() {
eventsTable.innerHTML = lastEvents.map(ev => {
const bb = ev.bbox ? ev.bbox.join(',') : '-';
return `<tr>
<td>${ev.trackId ?? '-'}</td>
<td>${ev.class ?? '-'}</td>
<td>${ev.conf != null ? ev.conf.toFixed(2) : '-'}</td>
<td>${bb}</td>
<td>${ev.t ?? '-'}</td>
</tr>`;
}).join('');
}

// Atualizar /state periodicamente
async function refreshState() {
try {
Expand Down