Skip to content
Merged
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ LightNVR provides a lightweight yet powerful solution for recording and managing

#### 🎯 Smart Detection & Recording
- **Detection Zones**: Visual polygon-based zone editor for targeted object detection - define multiple zones per camera with custom class filters and confidence thresholds
- **light-object-detect Integration**: Seamless integration with [light-object-detect](https://github.com/opensensor/light-object-detect) API for ONNX/TFLite-based object detection with zone filtering
- **light-object-detect Integration**: Seamless integration with [light-object-detect](https://github.com/opensensor/light-object-detect) API for ONNX/TFLite-based object detection with zone filtering; [DOODS2](https://github.com/snowzach/doods2) servers are supported through a selectable request format
- **ONVIF Motion Events**: Automated recording triggered by ONVIF motion detection events
- **Object Detection**: Optional SOD integration for motion and object detection (supports both RealNet and CNN models)

Expand Down Expand Up @@ -295,6 +295,7 @@ Powerful object detection using modern ONNX and TFLite models with zone-aware fi
- Enable **Detection Based Recording**
- Set **API Detection URL** to `http://localhost:9001/api/v1/detect`
- Choose detection backend: `onnx` (recommended), `tflite`, or `opencv`
- Running [DOODS2](https://github.com/snowzach/doods2) instead? Set **API Request Format** to DOODS2 in **Settings → Detection** and point the URL at its `/detect` endpoint (see [API Detection Settings](docs/CONFIGURATION.md#api-detection-settings))
- Configure **Detection Zones** to define areas of interest

See [Zone Configuration Guide](docs/ZONE_CONFIGURATION.md) for detailed zone setup instructions.
Expand Down
2 changes: 2 additions & 0 deletions config/lightnvr.ini
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ path = /var/lib/lightnvr/data/models
[api_detection]
url = http://localhost:9001/api/v1/detect
backend = onnx ; Detection backend: onnx (YOLOv8 - best accuracy), tflite, or opencv
format = light-object-detect ; Request format: light-object-detect (multipart upload) or doods2 (JSON body)
detector_name = default ; DOODS2 only: detector_name sent with each request
confidence_threshold = 0.35 ; Lower threshold to catch distant vehicles
filter_classes = car,motorcycle,truck,bus,bicycle ; Vehicle classes only

Expand Down
77 changes: 65 additions & 12 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,11 @@ max_streams = 32
path = /var/lib/lightnvr/data/models

[api_detection]
url = http://localhost:9001/api/v1/detect
backend = onnx ; Detection backend: onnx (YOLOv8), tflite, or opencv
confidence_threshold = 0.35
filter_classes = car,motorcycle,truck,bus,bicycle ; Comma-separated class filter
url = http://localhost:8000/api/v1/detect
format = light-object-detect ; Request format: light-object-detect or doods2
backend = onnx ; light-object-detect backend: onnx (YOLOv8), tflite, or opencv
detector_name = default ; doods2 detector_name
detection_threshold = 50

[memory]
buffer_size = 1024 ; Buffer size in KB
Expand Down Expand Up @@ -364,16 +365,68 @@ path = /var/lib/lightnvr/data/models

```ini
[api_detection]
url = http://localhost:9001/api/v1/detect
backend = onnx
confidence_threshold = 0.35
filter_classes = car,motorcycle,truck,bus,bicycle
url = http://localhost:8000/api/v1/detect
format = light-object-detect ; or doods2
backend = onnx ; light-object-detect only
detector_name = default ; doods2 only
detection_threshold = 50 ; Default confidence threshold (0-100%)
```

- `url`: URL of the external detection API
- `backend`: Detection backend to use: `onnx` (YOLOv8 - best accuracy), `tflite`, or `opencv`
- `confidence_threshold`: Minimum confidence threshold for detections (0.0-1.0)
- `filter_classes`: Comma-separated list of object classes to detect (empty = all classes)
- `url`: URL of the external detection API. A stream's **Custom API Endpoint**
overrides it for that stream. Query parameters in either URL are passed
through to the server.
- `format`: Wire format used when posting snapshots: `light-object-detect`
(default) or `doods2`. Both are described below.
- `backend`: `light-object-detect` only. Inference backend requested from the
server: `onnx` (YOLOv8 - best accuracy), `tflite`, or `opencv`.
- `detector_name`: `doods2` only. The `detector_name` sent with each request;
it must match one of the detectors the server lists at `GET /detectors`.
- `detection_threshold`: Default confidence threshold (0-100%) for streams that
do not set their own.

A stream whose only detection engine is an `api` engine may override `format`,
`backend` and `detector_name` in that engine's `config` object via
`PUT /api/streams/{name}/detection-engines`; see
[Detection Engines](DETECTION_ENGINES.md).

#### Wire formats

LightNVR grabs one JPEG per detection interval and posts it to the API. It
never asks the detector to open the camera stream itself, so streaming request
modes offered by some servers are not used.

**light-object-detect** (default)

Request: `POST <url>?backend=<backend>&confidence_threshold=<0-1>&return_image=false`
as `multipart/form-data` with a single `file` part containing the JPEG.

Response:

```json
{"detections": [
{"label": "person", "confidence": 0.91,
"x_min": 0.10, "y_min": 0.20, "x_max": 0.40, "y_max": 0.80}
]}
```

Coordinates are normalized 0-1 and `confidence` is 0-1. The box may instead be
nested under a `bounding_box` object. Optional `track_id` (number) and
`zone_id` (string) are stored when present. Any server that implements this
contract works with the default format.

**doods2**

Request: `POST <url>` with `Content-Type: application/json`:

```json
{"id": "<stream name>", "detector_name": "<detector_name>",
"detect": {"*": <threshold x 100>}, "data": "<base64 JPEG>"}
```

Response: the standard [DOODS2](https://github.com/snowzach/doods2) detect
response. `top`/`left`/`bottom`/`right` are normalized 0-1 and `confidence` is
0-100; LightNVR converts both to its internal 0-1 scale. A non-empty `error`
field fails the request.

### Memory Optimization

Expand Down
21 changes: 21 additions & 0 deletions docs/DETECTION_ENGINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,27 @@ rows including the compatibility engine. Relative object model paths resolve
under the configured models directory. `config` must be a bounded JSON object
and must not contain credentials.

For `api` engines, `model_path` is the detection endpoint URL and `config` may
carry request options that override the global `[api_detection]` settings:
`format` (`light-object-detect` or `doods2`), `backend` (light-object-detect)
and `detector_name` (doods2). See the wire formats in
[CONFIGURATION.md](CONFIGURATION.md#wire-formats).

```json
{
"key": "doods",
"type": "api",
"model_path": "http://doods:8080/detect",
"enabled": true,
"threshold": 0.4,
"interval_seconds": 2,
"config": {"format": "doods2", "detector_name": "tensorflow"}
}
```

The override takes effect when this is the stream's only engine (leave the
stream's legacy detection model empty); see the runtime boundaries below.

## Runtime boundaries

- Motion and local object engines share decoded frames and run at their own
Expand Down
32 changes: 32 additions & 0 deletions include/core/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@

#include "telemetry/system_health_policy.h"

// Wire formats supported by the external detection API client (api_detection.c).
#define API_DETECTION_FORMAT_MAX 32
#define API_DETECTION_BACKEND_MAX 32
#define API_DETECTION_DETECTOR_NAME_MAX 64
#define API_DETECTION_FORMAT_NAME_LIGHT_OBJECT_DETECT "light-object-detect"
#define API_DETECTION_FORMAT_NAME_DOODS2 "doods2"

typedef enum {
API_DETECTION_FORMAT_LIGHT_OBJECT_DETECT = 0, // multipart "file" upload + query params
API_DETECTION_FORMAT_DOODS2 = 1, // JSON body with base64 "data"
} api_detection_format_t;


// Maximum length for path strings
#define MAX_PATH_LENGTH 512
// Maximum length for stream names
Expand Down Expand Up @@ -227,6 +240,8 @@ typedef struct {
// API detection settings
char api_detection_url[MAX_URL_LENGTH]; // URL for the detection API
char api_detection_backend[32]; // Backend to use: onnx, tflite, opencv (default: onnx)
char api_detection_format[API_DETECTION_FORMAT_MAX]; // Wire format: light-object-detect (default) or doods2
char api_detection_detector_name[API_DETECTION_DETECTOR_NAME_MAX]; // DOODS2 detector_name (default: "default")

// Global detection defaults (used when per-stream settings are not specified)
int default_detection_threshold; // Default confidence threshold for detection (0-100)
Expand Down Expand Up @@ -473,4 +488,21 @@ static inline int configured_stream_slots(void) {
return slots;
}

/**
* Canonical name for an API detection wire format.
*/
const char *api_detection_format_name(api_detection_format_t format);

/**
* Parse a format name (case-insensitive; accepts "light-object-detect"/"lod"
* and "doods2"/"doods"). Returns false for unknown names, leaving *out untouched.
*/
bool api_detection_format_parse(const char *name, api_detection_format_t *out);

/**
* Store the canonical name of a parsed format in config->api_detection_format.
* Returns false (and leaves the config untouched) for unknown names.
*/
bool config_set_api_detection_format(config_t *config, const char *name);

#endif /* LIGHTNVR_CONFIG_H */
27 changes: 27 additions & 0 deletions include/utils/base64.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#ifndef LIGHTNVR_UTILS_BASE64_H
#define LIGHTNVR_UTILS_BASE64_H

#include <stddef.h>

/**
* Bytes required to hold the standard (RFC 4648, padded) base64 encoding of
* input_len bytes, including the terminating NUL. Returns 0 on overflow.
*/
size_t base64_encoded_size(size_t input_len);

/**
* Encode input into dst as NUL-terminated base64.
*
* @param out_len Optional; receives the encoded length (without the NUL).
* @return 0 on success, -1 when dst is too small or the arguments are invalid.
*/
int base64_encode(const unsigned char *input, size_t input_len,
char *dst, size_t dst_size, size_t *out_len);

/**
* Convenience wrapper that returns a malloc'd NUL-terminated encoding.
* The caller frees the result. Returns NULL on failure.
*/
char *base64_encode_alloc(const unsigned char *input, size_t input_len);

#endif /* LIGHTNVR_UTILS_BASE64_H */
124 changes: 78 additions & 46 deletions include/video/api_detection.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,80 +2,112 @@
#define LIGHTNVR_API_DETECTION_H

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <time.h>

#include "core/config.h"
#include "video/detection_result.h"

// Model type for API-based detection
#define MODEL_TYPE_API "api"

/**
* Initialize the API detection system
*
* @return 0 on success, non-zero on failure
* Per-request options for the HTTP detection API.
*
* LightNVR speaks two wire formats:
*
* - light-object-detect (default): multipart/form-data upload of the JPEG in a
* "file" part, with backend/confidence_threshold/return_image query
* parameters appended to the URL. The reply is {"detections":[{label,
* confidence 0-1, x_min/y_min/x_max/y_max normalized 0-1 (flat or under
* "bounding_box")}]}.
*
* - doods2: JSON body {"id","detector_name","detect":{"*":percent},"data":
* base64 JPEG} posted verbatim to the URL. The reply is {"detections":[{
* top/left/bottom/right normalized 0-1, label, confidence 0-100}],"error"}.
*
* The global defaults come from [api_detection] in lightnvr.ini and may be
* overridden per stream through the detection engine "config" JSON object
* ({"format","backend","detector_name"}).
*/
int init_api_detection_system(void);
typedef struct {
api_detection_format_t format;
char backend[API_DETECTION_BACKEND_MAX]; // light-object-detect: inference backend query param
char detector_name[API_DETECTION_DETECTOR_NAME_MAX]; // doods2: "detector_name" request field
} api_detection_options_t;

/** Populate options from the global configuration. */
void api_detection_options_from_config(api_detection_options_t *options);

/**
* Shutdown the API detection system
* Overlay per-engine overrides from a detection engine config JSON object.
* Unknown keys are ignored. Returns 0 on success (including an empty or
* absent config) and -1 when the JSON is malformed or names an unknown
* format; options are left in a consistent state either way.
*/
void shutdown_api_detection_system(void);
int api_detection_options_apply_json(api_detection_options_t *options,
const char *config_json);

/**
* Detect objects using the API
*
* Uses the provided decoded frame when available. If no decoded frame is
* supplied and a stream name is available, the implementation may fetch a
* JPEG snapshot from go2rtc instead.
*
* @param api_url The URL of the detection API
* @param frame_data The frame data to detect objects in
* @param width The width of the frame
* @param height The height of the frame
* @param channels The number of channels in the frame
* @param result Pointer to a detection_result_t structure to store the results
* @param stream_name The name of the stream (for database storage)
* @param threshold Confidence threshold for detection (0.0-1.0, use negative for default)
* @param recording_id Recording ID to link detections to (0 for no link)
* @param frame_timestamp Wall-clock time when the frame entered the detection
* pipeline. Used as the DB/MQTT event timestamp so it
* reflects frame time rather than inference completion.
* Pass 0 to fall back to time(NULL) inside the DB layer.
* @return 0 on success, non-zero on failure
* Build the request URL for the selected format. light-object-detect appends
* its query parameters; doods2 uses the base URL verbatim. NULL options mean
* the global configuration. Returns 0 on success, -1 on error.
*/
int api_detection_build_request_url(char *buffer, size_t buffer_size,
const char *base_url, float threshold,
const api_detection_options_t *options);

/**
* Build the DOODS2 JSON request body for a JPEG snapshot. request_id is sent
* back by DOODS2 in the response "id" field (stream name when available).
* Returns a malloc'd string the caller frees, or NULL on failure.
*/
char *api_detection_build_doods2_body(const unsigned char *jpeg_data, size_t jpeg_size,
float threshold, const char *request_id,
const api_detection_options_t *options);

/**
* Parse a detection API response into result (reset first). Both box shapes
* are accepted regardless of format; the confidence scale follows the format
* (0-1 for light-object-detect, 0-100 for doods2). Returns 0 on success and
* -1 when the document is not a detection response or reports an error.
*/
int api_detection_parse_response(const char *json,
const api_detection_options_t *options,
detection_result_t *result);

int init_api_detection_system(void);

void shutdown_api_detection_system(void);

int detect_objects_api(const char *api_url, const unsigned char *frame_data,
int width, int height, int channels, detection_result_t *result,
const char *stream_name, float threshold, uint64_t recording_id,
time_t frame_timestamp);

/**
* @brief Determine whether API detection should fetch a go2rtc snapshot.
*
* This prevents fallback callers from re-entering the go2rtc snapshot path
* after they already decoded a local frame for the same detection attempt.
*/
/** Same as detect_objects_api with explicit request options (NULL = global config). */
int detect_objects_api_with_options(const char *api_url, const unsigned char *frame_data,
int width, int height, int channels,
detection_result_t *result, const char *stream_name,
float threshold, uint64_t recording_id,
time_t frame_timestamp,
const api_detection_options_t *options);

bool api_detection_should_use_go2rtc_snapshot(const unsigned char *frame_data,
int width,
int height,
int channels,
const char *stream_name);

/**
* Detect objects using the API with go2rtc snapshot only (no frame data required)
*
* This function fetches a snapshot directly from go2rtc and sends it to the detection API.
* It does NOT require decoded frame data, which saves significant memory by avoiding
* the need to decode video segments.
*
* @param api_url The URL of the detection API
* @param stream_name The name of the stream (required for go2rtc snapshot)
* @param result Pointer to a detection_result_t structure to store the results
* @param threshold Confidence threshold for detection (0.0-1.0, use negative for default)
* @param recording_id Recording ID to link detections to (0 for no link)
* @return 0 on success, -1 on general failure, -2 if go2rtc snapshot failed (caller should fall back)
*/
int detect_objects_api_snapshot(const char *api_url, const char *stream_name,
detection_result_t *result, float threshold,
uint64_t recording_id, time_t frame_timestamp);

/** Same as detect_objects_api_snapshot with explicit request options (NULL = global config). */
int detect_objects_api_snapshot_with_options(const char *api_url, const char *stream_name,
detection_result_t *result, float threshold,
uint64_t recording_id, time_t frame_timestamp,
const api_detection_options_t *options);

#endif /* LIGHTNVR_API_DETECTION_H */
Loading
Loading