Compare commits

..

No commits in common. "master" and "p0-dashboard" have entirely different histories.

61 changed files with 2072 additions and 2756 deletions

1
.gitignore vendored
View File

@ -51,4 +51,3 @@ __pycache__/
*.swo
.DS_Store
/.worktrees/
agent/safesight-edge-agent

View File

@ -1,9 +1,9 @@
# Minimum version based on requirements from PRD (CMake >= 3.20)
cmake_minimum_required(VERSION 3.20)
project(safesight_media
VERSION 1.0.0
DESCRIPTION "SafeSight Media - AI-powered video analysis engine"
project(rk3588_media_server
VERSION 0.1.0
DESCRIPTION "RK3588 media server bootstrap"
LANGUAGES C CXX)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
@ -57,7 +57,7 @@ endif()
set(SRC_FILES
src/main.cpp
src/edge_server_app.cpp
src/media_server_app.cpp
src/graph_manager.cpp
src/plugin_loader.cpp
src/ai_scheduler.cpp
@ -84,28 +84,28 @@ install(TARGETS rk_shared_state
LIBRARY DESTINATION bin
)
add_executable(safesight-edge-server ${SRC_FILES})
set_target_properties(safesight-edge-server PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
target_include_directories(safesight-edge-server
add_executable(media-server ${SRC_FILES})
set_target_properties(media-server PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
target_include_directories(media-server
PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/third_party
)
target_link_libraries(safesight-edge-server PRIVATE project_options Threads::Threads)
target_link_libraries(media-server PRIVATE project_options Threads::Threads)
if(RK3588_ENABLE_FFMPEG)
target_compile_definitions(safesight-edge-server PRIVATE RK3588_ENABLE_FFMPEG)
target_include_directories(safesight-edge-server PRIVATE ${FFMPEG_INCLUDE_DIRS})
target_link_libraries(safesight-edge-server PRIVATE PkgConfig::FFMPEG PkgConfig::SWSCALE)
target_compile_definitions(media-server PRIVATE RK3588_ENABLE_FFMPEG)
target_include_directories(media-server PRIVATE ${FFMPEG_INCLUDE_DIRS})
target_link_libraries(media-server PRIVATE PkgConfig::FFMPEG PkgConfig::SWSCALE)
endif()
if(RK3588_ENABLE_MPP)
target_include_directories(safesight-edge-server SYSTEM PRIVATE
target_include_directories(media-server SYSTEM PRIVATE
${RK_MPP_ROOT}/inc
${RK_RKNN_ROOT}/examples/3rdparty/mpp/include
)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(safesight-edge-server PRIVATE -Wno-pedantic)
target_compile_options(media-server PRIVATE -Wno-pedantic)
endif()
endif()
@ -120,11 +120,11 @@ if(RK3588_ENABLE_RGA)
message(WARNING "RGA enabled but librga not found; disable RK3588_ENABLE_RGA or install librga-dev")
else()
message(STATUS "Found librga: ${RK_RGA_LIB}")
target_compile_definitions(safesight-edge-server PRIVATE RK3588_ENABLE_RGA)
target_include_directories(safesight-edge-server SYSTEM PRIVATE ${RK_RGA_INCLUDE_DIR})
target_link_libraries(safesight-edge-server PRIVATE ${RK_RGA_LIB})
target_compile_definitions(media-server PRIVATE RK3588_ENABLE_RGA)
target_include_directories(media-server SYSTEM PRIVATE ${RK_RGA_INCLUDE_DIR})
target_link_libraries(media-server PRIVATE ${RK_RGA_LIB})
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(safesight-edge-server PRIVATE -Wno-pedantic)
target_compile_options(media-server PRIVATE -Wno-pedantic)
endif()
endif()
endif()
@ -142,21 +142,21 @@ if(RK3588_ENABLE_RKNN)
if(NOT RK_RKNN_LIB)
message(WARNING "RKNN enabled but librknnrt not found; disable RK3588_ENABLE_RKNN or set RKNN_RUNTIME_LIB_DIR")
else()
target_compile_definitions(safesight-edge-server PRIVATE RK3588_ENABLE_RKNN)
target_include_directories(safesight-edge-server PRIVATE ${RKNN_RUNTIME_INCLUDE_DIR})
target_link_libraries(safesight-edge-server PRIVATE ${RK_RKNN_LIB})
target_compile_definitions(media-server PRIVATE RK3588_ENABLE_RKNN)
target_include_directories(media-server PRIVATE ${RKNN_RUNTIME_INCLUDE_DIR})
target_link_libraries(media-server PRIVATE ${RK_RKNN_LIB})
endif()
endif()
if(WIN32)
target_link_libraries(safesight-edge-server PRIVATE ws2_32)
target_link_libraries(media-server PRIVATE ws2_32)
endif()
target_compile_definitions(safesight-edge-server PRIVATE
target_compile_definitions(media-server PRIVATE
RK_PROJECT_VERSION="${PROJECT_VERSION}"
RK_GIT_SHA="${RK_GIT_SHA}"
)
install(TARGETS safesight-edge-server RUNTIME DESTINATION bin)
install(TARGETS media-server RUNTIME DESTINATION bin)
if(BUILD_SAMPLES)
add_subdirectory(samples)
@ -170,4 +170,4 @@ endif()
add_subdirectory(plugins)
include(GNUInstallDirs)
install(FILES configs/sample_cam1.json DESTINATION ${CMAKE_INSTALL_DATADIR}/safesight-edge-server)
install(FILES configs/sample_cam1.json DESTINATION ${CMAKE_INSTALL_DATADIR}/rk3588-media-server)

View File

@ -1,5 +1,5 @@
# SafeSight 智能视觉安全监测平台 PRDv1.2
# RK3588 智能视频分析系统 PRDv1.2
---
@ -40,7 +40,7 @@
### 2.1 系统逻辑架构
```text
[ safesight-media 主进程 ]
[ media-server 主进程 ]
|
+-- Graph Manager (图管理加载配置、构建DAG、热更新/回滚)
|
@ -53,7 +53,7 @@
+-- HTTP Server (REST API + Web 控制台 + 状态查询)
```
- **safesight-media**:主入口,负责初始化配置、图构建、主事件循环。
- **media-server**:主入口,负责初始化配置、图构建、主事件循环。
- **Graph ManagerGraphMgr**
- 对应多个 graph每个 graph 一路/一组业务流程)。
- 实例化节点Node建立边Edge负责热更新时的 diff & 切换。
@ -1199,13 +1199,13 @@ cmake --build build -j$(nproc)
```bash
# 纯转码网关
./build/safesight-media --config configs/sample_cam1.json
./build/media-server --config configs/sample_cam1.json
# 带预处理的 pipeline
./build/safesight-media --config configs/sample_preprocess.json
./build/media-server --config configs/sample_preprocess.json
# 内置 RTSP Server 模式
./build/safesight-media --config configs/sample_cam1_rtsp_server.json
./build/media-server --config configs/sample_cam1_rtsp_server.json
```
### 10.2 交叉编译(可选)
@ -1228,7 +1228,7 @@ cmake -S . -B build-cross \
cmake --build build-cross -j$(nproc)
# 同步到板子
rsync -avz build-cross/ user@board_ip:/opt/safesight-media/
rsync -avz build-cross/ user@board_ip:/opt/media-server/
```
---
@ -1266,40 +1266,3 @@ rsync -avz build-cross/ user@board_ip:/opt/safesight-media/
- [docs/models.md](docs/models.md) - 模型转换与部署指南
- [docs/Agent_API_Extensions.md](docs/Agent_API_Extensions.md) - Agent API 扩展说明
- [docs/API_Device_RemoteMgmt_InterfaceTable.md](docs/API_Device_RemoteMgmt_InterfaceTable.md) - 远程管理接口
---
## Pose Behavior Pipeline
The pose behavior pipeline is optional and does not replace the existing detection pipeline.
Baseline behavior graph:
```text
input_rtsp -> preprocess -> ai_yolo -> tracker -> region_event -> action_recog -> event_fusion
```
Pose-enabled behavior graph:
```text
input_rtsp -> preprocess -> ai_yolo -> tracker -> ai_pose -> pose_assoc -> region_event -> action_recog -> event_fusion
```
Engineering rules:
- `tracker` remains the source of stable `track_id` for the main pipeline.
- `ai_pose` only produces `Frame.pose` and does not replace `Frame.det`.
- `pose_assoc` is the canonical node for assigning `PoseItem.track_id`.
- `action_recog` may use pose-enhanced rules, but it must still run when pose nodes are absent.
Degradation semantics:
- If `ai_pose` is not present, the graph keeps the original bbox-based behavior logic.
- If `ai_pose` is present but inference fails on some frames, `Frame.pose` may be empty and downstream nodes must continue with bbox data.
- If `pose_assoc` is absent, downstream nodes should treat `pose.track_id` as optional and may fall back to bbox matching only as a compatibility path.
Sample configs:
- `configs/sample_region_behavior_intrusion.json`
- Baseline intrusion-only graph without pose nodes. This is the reference for the non-pose deployment path.
- `configs/sample_region_behavior_full.json`
- Full behavior graph with `ai_pose` and `pose_assoc`, intended for pose-aware `fall` and `fight`.

View File

@ -8,19 +8,18 @@
"discovery_port": 35688,
"device_name": "cam1_strict_minio_alarm",
"device_id_path": "/var/lib/safesight-agent/device_id",
"device_id_path": "/var/lib/rk3588-agent/device_id",
"models_dir": "/opt/safesight-edge-server/models",
"models_dir": "/opt/rk3588-media-server/models",
"max_upload_mb": 200,
"config_path": "/opt/safesight-edge-server/etc/safesight-edge-server.json",
"systemctl_service": "safesight-edge-server",
"config_path": "/opt/rk3588-media-server/etc/media-server.json",
"media_server_process": {
"enable": true,
"exec_path": "/opt/safesight-edge-server/bin/systemctl-wrapper.sh",
"work_dir": "/opt/safesight-edge-server",
"configs_dir": "/opt/safesight-edge-server/etc",
"pid_file": "/var/run/safesight-edge-server.pid",
"exec_path": "/opt/rk3588-media-server/bin/systemctl-wrapper.sh",
"work_dir": "/opt/rk3588-media-server",
"configs_dir": "/opt/rk3588-media-server/etc",
"pid_file": "/var/run/rk3588sys-media-server.pid",
"graceful_timeout_ms": 5000
},
"media_server_base_url": "http://127.0.0.1:9000",

View File

@ -16,13 +16,13 @@ import (
"syscall"
"time"
"safesight-agent/internal/config"
"safesight-agent/internal/discovery"
"safesight-agent/internal/httpapi"
"safesight-agent/internal/log"
"safesight-agent/internal/mediaserver"
"safesight-agent/internal/modelstore"
"safesight-agent/internal/sysinfo"
"rk3588sys/agent/internal/config"
"rk3588sys/agent/internal/discovery"
"rk3588sys/agent/internal/httpapi"
"rk3588sys/agent/internal/log"
"rk3588sys/agent/internal/mediaserver"
"rk3588sys/agent/internal/modelstore"
"rk3588sys/agent/internal/sysinfo"
)
var Version = "0.0.0-dev"

View File

@ -1,3 +1,3 @@
module safesight-agent
module rk3588sys/agent
go 1.21

View File

@ -6,7 +6,7 @@ import (
"path/filepath"
"sync"
"safesight-agent/internal/files"
"rk3588sys/agent/internal/files"
)
type Entry struct {

View File

@ -30,7 +30,6 @@ type AgentConfig struct {
MediaServerBaseURL string `json:"media_server_base_url"`
MediaServerTimeout int `json:"media_server_timeout_ms"`
MediaServerRetry RetryConfig `json:"media_server_retry"`
SystemctlService string `json:"systemctl_service"`
}
type MediaServerProcessConfig struct {
@ -54,19 +53,18 @@ func Default() Config {
RequireTokenForRead: false,
DiscoveryEnable: true,
DiscoveryPort: 35688,
DeviceIDPath: "/var/lib/safesight-edge-agent/device_id",
ModelsDir: "/opt/safesight-edge-server/models",
ResourcesDir: "/opt/safesight-edge-server/resources",
DeviceIDPath: "/var/lib/rk3588-agent/device_id",
ModelsDir: "/opt/rk3588sys/models",
ResourcesDir: "/opt/rk3588sys/resources",
MaxUploadMB: 200,
ConfigPath: "/opt/safesight-edge-server/etc/safesight-edge-server.json",
ConfigPath: "/etc/rk3588sys/config.json",
MediaServerProcess: MediaServerProcessConfig{
Enable: false,
PidFile: "/var/run/safesight-edge-server.pid",
PidFile: "/var/run/rk3588sys-media-server.pid",
GracefulTimeoutMS: 5000,
},
MediaServerBaseURL: "http://127.0.0.1:9000",
MediaServerTimeout: 3000,
SystemctlService: "safesight-edge-server",
MediaServerRetry: RetryConfig{
MaxAttempts: 3,
BackoffMS: []int{200, 500},

View File

@ -11,7 +11,7 @@ import (
"strings"
"time"
"safesight-agent/internal/sysinfo"
"rk3588sys/agent/internal/sysinfo"
)
const Magic = "RK3588SYS_DISCOVERY_V1"

View File

@ -15,7 +15,7 @@ import (
"strings"
"time"
"safesight-agent/internal/files"
"rk3588sys/agent/internal/files"
)
type agentBinaryUpdateResult struct {

View File

@ -14,7 +14,7 @@ import (
"strings"
"time"
"safesight-agent/internal/files"
"rk3588sys/agent/internal/files"
)
func (s *Server) alarmsPath() string {

View File

@ -10,8 +10,8 @@ import (
"strings"
"testing"
"safesight-agent/internal/config"
"safesight-agent/internal/mediaserver"
"rk3588sys/agent/internal/config"
"rk3588sys/agent/internal/mediaserver"
)
func TestHandleConfigCandidateApplyPromotesCandidateAndBacksUpCurrent(t *testing.T) {

View File

@ -11,7 +11,7 @@ import (
"strings"
"testing"
"safesight-agent/internal/config"
"rk3588sys/agent/internal/config"
)
func TestHandleConfigCandidateStoresValidatedCandidate(t *testing.T) {

View File

@ -11,8 +11,8 @@ import (
"path/filepath"
"testing"
"safesight-agent/internal/config"
"safesight-agent/internal/procctl"
"rk3588sys/agent/internal/config"
"rk3588sys/agent/internal/procctl"
)
type fakeProcessController struct {

View File

@ -11,10 +11,10 @@ import (
"strings"
"time"
"safesight-agent/internal/assets"
"safesight-agent/internal/audit"
"safesight-agent/internal/metrics"
"safesight-agent/internal/sysinfo"
"rk3588sys/agent/internal/assets"
"rk3588sys/agent/internal/audit"
"rk3588sys/agent/internal/metrics"
"rk3588sys/agent/internal/sysinfo"
)
type configFileStatus struct {

View File

@ -1,16 +1,16 @@
package httpapi
import (
"context"
"bytes"
"encoding/json"
"log"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"safesight-agent/internal/files"
"rk3588sys/agent/internal/files"
)
type faceGalleryInfo struct {
@ -110,8 +110,6 @@ func (s *Server) handleFaceGallery(w http.ResponseWriter, r *http.Request) {
}
s.recordAudit(r, "face_gallery.update", true, "")
writeJSON(w, http.StatusOK, faceGalleryInfo{Ok: true, Path: dst, Exists: true, Size: st.Size(), MtimeMS: st.ModTime().UnixMilli()})
// Auto-reload: notify edge-server to pick up the new gallery
go s.reloadFaceGallery()
return
default:
errorJSON(w, http.StatusMethodNotAllowed, "method not allowed")
@ -119,53 +117,6 @@ func (s *Server) handleFaceGallery(w http.ResponseWriter, r *http.Request) {
}
}
func (s *Server) reloadFaceGallery() {
reloadSeq := time.Now().UnixMilli()
log.Printf("[face_gallery] auto-reload seq=%d", reloadSeq)
reloaded := 0
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
st, b, err := s.ms.GetGraphs(ctx)
if err != nil {
return
}
if st < 200 || st > 299 {
return
}
var graphs []struct {
Name string `json:"name"`
}
if err := json.Unmarshal(b, &graphs); err != nil {
return
}
for _, g := range graphs {
name := strings.TrimSpace(g.Name)
if name == "" {
continue
}
st2, b2, err := s.ms.GetGraph(ctx, name)
if err != nil || st2 < 200 || st2 > 299 {
continue
}
var snap struct {
Nodes []struct {
ID string `json:"id"`
Type string `json:"type"`
} `json:"nodes"`
}
if err := json.Unmarshal(b2, &snap); err != nil {
continue
}
for _, n := range snap.Nodes {
if n.Type != "ai_face_recog" {
continue
}
s.ms.UpdateNodeConfig(ctx, n.ID, name, map[string]any{"gallery": map[string]any{"reload_seq": reloadSeq}})
reloaded++
}
}
}
func (s *Server) handleFaceGalleryReload(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
errorJSON(w, http.StatusMethodNotAllowed, "method not allowed")
@ -175,7 +126,64 @@ func (s *Server) handleFaceGalleryReload(w http.ResponseWriter, r *http.Request)
errorJSON(w, http.StatusUnauthorized, "unauthorized")
return
}
s.reloadFaceGallery()
ctx := r.Context()
st, b, err := s.ms.GetGraphs(ctx)
if err != nil {
errorJSON(w, http.StatusInternalServerError, "internal error: "+err.Error())
return
}
if st < 200 || st > 299 {
errorJSON(w, http.StatusInternalServerError, fmt.Sprintf("internal error: media-server status=%d body=%s", st, strings.TrimSpace(string(bytes.TrimSpace(b)))))
return
}
var graphs []struct {
Name string `json:"name"`
}
if err := json.Unmarshal(b, &graphs); err != nil {
errorJSON(w, http.StatusInternalServerError, "internal error: parse graphs failed: "+err.Error())
return
}
reloadSeq := time.Now().UnixMilli()
reloaded := 0
for _, g := range graphs {
if strings.TrimSpace(g.Name) == "" {
continue
}
st2, b2, err := s.ms.GetGraph(ctx, g.Name)
if err != nil {
errorJSON(w, http.StatusInternalServerError, "internal error: "+err.Error())
return
}
if st2 < 200 || st2 > 299 {
errorJSON(w, http.StatusInternalServerError, fmt.Sprintf("internal error: media-server status=%d body=%s", st2, strings.TrimSpace(string(bytes.TrimSpace(b2)))))
return
}
var snap struct {
Nodes []struct {
ID string `json:"id"`
Type string `json:"type"`
} `json:"nodes"`
}
if err := json.Unmarshal(b2, &snap); err != nil {
errorJSON(w, http.StatusInternalServerError, "internal error: parse graph snapshot failed: "+err.Error())
return
}
for _, n := range snap.Nodes {
if n.Type != "ai_face_recog" {
continue
}
patch := map[string]any{"gallery": map[string]any{"reload_seq": reloadSeq}}
if err := s.ms.UpdateNodeConfig(ctx, n.ID, g.Name, patch); err != nil {
s.recordAudit(r, "face_gallery.reload", false, err.Error())
errorJSON(w, http.StatusInternalServerError, "internal error: update node config failed: "+err.Error())
return
}
reloaded++
}
}
s.recordAudit(r, "face_gallery.reload", true, "")
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "reloaded": reloaded, "reload_seq": reloadSeq})
}

View File

@ -8,7 +8,7 @@ import (
"strings"
"testing"
"safesight-agent/internal/modelstore"
"rk3588sys/agent/internal/modelstore"
)
func TestHandleModelsStatusReturnsInstalledModels(t *testing.T) {

View File

@ -10,7 +10,7 @@ import (
"sort"
"strings"
"safesight-agent/internal/files"
"rk3588sys/agent/internal/files"
)
type installedResource struct {

View File

@ -10,7 +10,6 @@ import (
"mime"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
@ -18,15 +17,15 @@ import (
"sync"
"time"
"safesight-agent/internal/audit"
"safesight-agent/internal/config"
"safesight-agent/internal/files"
"safesight-agent/internal/mediaserver"
"safesight-agent/internal/metrics"
"safesight-agent/internal/modelstore"
"safesight-agent/internal/procctl"
"safesight-agent/internal/sysinfo"
"safesight-agent/internal/tasks"
"rk3588sys/agent/internal/audit"
"rk3588sys/agent/internal/config"
"rk3588sys/agent/internal/files"
"rk3588sys/agent/internal/mediaserver"
"rk3588sys/agent/internal/metrics"
"rk3588sys/agent/internal/modelstore"
"rk3588sys/agent/internal/procctl"
"rk3588sys/agent/internal/sysinfo"
"rk3588sys/agent/internal/tasks"
)
type Server struct {
@ -89,7 +88,7 @@ func New(agentCfg config.AgentConfig, baseDir string, ms *mediaserver.Client, st
pc = procctl.New(agentCfg, baseDir)
} else {
// 使用 systemctl 模式管理 Media Server
pc = procctl.NewSystemCtlController(agentCfg.SystemctlService, agentCfg.ConfigPath)
pc = procctl.NewSystemCtlController("media-server")
}
execPath, _ := os.Executable()
if execPath != "" {
@ -141,15 +140,15 @@ func New(agentCfg config.AgentConfig, baseDir string, ms *mediaserver.Client, st
mux.HandleFunc("/v1/models", s.handleModelsList)
mux.HandleFunc("/v1/models/status", s.handleModelsStatus)
mux.HandleFunc("/v1/models/", s.handleModelUpload)
mux.HandleFunc("/v1/edge-server/reload", s.handleMediaReload)
mux.HandleFunc("/v1/edge-server/rollback", s.handleMediaRollback)
mux.HandleFunc("/v1/edge-server/start", s.handleMediaStart)
mux.HandleFunc("/v1/edge-server/restart", s.handleMediaRestart)
mux.HandleFunc("/v1/edge-server/stop", s.handleMediaStop)
mux.HandleFunc("/v1/edge-server/status", s.handleMediaStatus)
mux.HandleFunc("/v1/edge-server/configs/", s.handleMediaConfigUpload)
mux.HandleFunc("/v1/edge-server/binary", s.handleMediaBinaryUpdate)
mux.HandleFunc("/v1/edge-server/binary/rollback", s.handleMediaBinaryRollback)
mux.HandleFunc("/v1/media-server/reload", s.handleMediaReload)
mux.HandleFunc("/v1/media-server/rollback", s.handleMediaRollback)
mux.HandleFunc("/v1/media-server/start", s.handleMediaStart)
mux.HandleFunc("/v1/media-server/restart", s.handleMediaRestart)
mux.HandleFunc("/v1/media-server/stop", s.handleMediaStop)
mux.HandleFunc("/v1/media-server/status", s.handleMediaStatus)
mux.HandleFunc("/v1/media-server/configs/", s.handleMediaConfigUpload)
mux.HandleFunc("/v1/media-server/binary", s.handleMediaBinaryUpdate)
mux.HandleFunc("/v1/media-server/binary/rollback", s.handleMediaBinaryRollback)
mux.HandleFunc("/v1/agent/binary", s.handleAgentBinaryUpdate)
mux.HandleFunc("/v1/graph-node-types", s.handleGraphNodeTypes)
mux.HandleFunc("/v1/capabilities", s.handleCapabilities)
@ -157,8 +156,6 @@ func New(agentCfg config.AgentConfig, baseDir string, ms *mediaserver.Client, st
mux.HandleFunc("/v1/graphs/", s.handleGraphDetail)
mux.HandleFunc("/v1/logs/recent", s.handleLogsRecent)
mux.HandleFunc("/v1/metrics", s.handleMetrics)
mux.HandleFunc("/v1/time", s.handleTime)
mux.HandleFunc("/v1/time/set", s.handleTimeSet)
mux.HandleFunc("/v1/versions", s.handleVersions)
mux.HandleFunc("/v1/assets", s.handleAssets)
mux.HandleFunc("/v1/tasks/", s.handleTask)
@ -585,7 +582,7 @@ func (s *Server) handleMediaConfigUpload(w http.ResponseWriter, r *http.Request)
return
}
name := strings.TrimPrefix(r.URL.Path, "/v1/edge-server/configs/")
name := strings.TrimPrefix(r.URL.Path, "/v1/media-server/configs/")
name = strings.TrimSpace(name)
finalName, err := normalizeConfigName(name)
if err != nil {
@ -1113,60 +1110,6 @@ func writeRawJSON(w http.ResponseWriter, status int, raw []byte) {
_, _ = w.Write(raw)
}
type timeResponse struct {
UnixMS int64 `json:"unix_ms"`
Formatted string `json:"formatted"`
}
func (s *Server) handleTime(w http.ResponseWriter, r *http.Request) {
if !s.authorize(r, false) {
errorJSON(w, http.StatusUnauthorized, "unauthorized")
return
}
now := time.Now()
writeJSON(w, http.StatusOK, timeResponse{
UnixMS: now.UnixMilli(),
Formatted: now.Format("2006-01-02 15:04:05"),
})
}
func (s *Server) handleTimeSet(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
errorJSON(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
if !s.authorize(r, true) {
errorJSON(w, http.StatusUnauthorized, "unauthorized")
return
}
var req struct {
UnixMS int64 `json:"unix_ms"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.UnixMS <= 0 {
errorJSON(w, http.StatusBadRequest, "invalid unix_ms")
return
}
now := time.Now()
diff := time.Duration(req.UnixMS-now.UnixMilli()) * time.Millisecond
if diff < 0 {
diff = -diff
}
if diff < 2*time.Second && diff > -2*time.Second {
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "skipped": true})
return
}
// Set system time via date command (requires root)
t := time.UnixMilli(req.UnixMS)
dateStr := t.Format("2006-01-02 15:04:05")
if err := exec.Command("date", "-s", dateStr).Run(); err != nil {
errorJSON(w, http.StatusInternalServerError, "failed to set time: "+err.Error())
return
}
// Sync system time to hardware clock for persistence
exec.Command("hwclock", "--systohc").Run()
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "set_to": dateStr})
}
func errorJSON(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]any{"error": msg})
}

View File

@ -12,7 +12,7 @@ import (
"sort"
"strings"
"safesight-agent/internal/files"
"rk3588sys/agent/internal/files"
)
type Item struct {

View File

@ -14,8 +14,8 @@ import (
"sync"
"time"
"safesight-agent/internal/config"
"safesight-agent/internal/files"
"rk3588sys/agent/internal/config"
"rk3588sys/agent/internal/files"
)
var ErrNotSupported = errors.New("not supported")

View File

@ -9,7 +9,7 @@ import (
"syscall"
"time"
"safesight-agent/internal/log"
"rk3588sys/agent/internal/log"
)
func isAlive(pid int) (bool, error) {

View File

@ -12,15 +12,14 @@ import (
// SystemCtlController 使用 systemctl 管理 Media Server
type SystemCtlController struct {
serviceName string
configPath string
serviceName string
}
func NewSystemCtlController(serviceName string, configPath string) *SystemCtlController {
return &SystemCtlController{
serviceName: serviceName,
configPath: configPath,
}
// NewSystemCtlController 创建 systemctl 控制器
func NewSystemCtlController(serviceName string) *SystemCtlController {
return &SystemCtlController{
serviceName: serviceName,
}
}
func (s *SystemCtlController) Enabled() bool { return s != nil }
@ -50,42 +49,18 @@ func (s *SystemCtlController) Status() (Status, error) {
return Status{
Running: true,
Pid: pid,
ConfigPath: s.configPath,
ConfigPath: "/opt/rk3588-media-server/etc/media-server.json",
}, nil
}
func (s *SystemCtlController) Version() (string, error) {
// 从 systemctl 获取 ExecStart 路径,再调用 --version
// Output format: { path=/path/to/binary ; argv[]=... }
binOut, err := exec.Command("systemctl", "show", "--property=ExecStart", "--value", s.serviceName).Output()
// 直接执行 media-server --version
cmd := exec.Command("/opt/rk3588-media-server/bin/media-server", "--version")
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("get version failed: %w", err)
}
out := strings.TrimSpace(string(binOut))
// Parse path= from systemctl ExecStart format
const prefix = "path="
idx := strings.Index(out, prefix)
if idx < 0 {
return "", fmt.Errorf("get version failed: cannot parse ExecStart")
}
rest := out[idx+len(prefix):]
end := strings.IndexByte(rest, ' ')
if end < 0 {
end = strings.IndexByte(rest, ';')
}
if end < 0 {
end = len(rest)
}
binPath := rest[:end]
if binPath == "" {
return "", fmt.Errorf("get version failed: empty ExecStart")
}
cmd := exec.Command(binPath, "--version")
out2, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("get version failed: %w", err)
}
return strings.TrimSpace(string(out2)), nil
return strings.TrimSpace(string(out)), nil
}
func (s *SystemCtlController) Start(configName string) (Status, error) {

View File

@ -14,7 +14,7 @@ import (
"strings"
"time"
"safesight-agent/internal/files"
"rk3588sys/agent/internal/files"
)
func Hostname() string {

Binary file not shown.

View File

@ -1,45 +0,0 @@
{
"description": "生产环境宽松阈值——降低人脸误报和劳保鞋告警频率",
"instance_overrides": {
"*": {
"override": {
"nodes": {
"alarm_violation": {
"config": {
"cooldown_ms": 30000,
"min_score": 0.5,
"min_duration_ms": 1500,
"min_hits": 3,
"hit_window_ms": 3000,
"rules": [
{
"name": "non_compliant_workshoe",
"min_score": 0.5,
"cooldown_ms": 60000,
"min_hits": 3,
"min_duration_ms": 1500
}
],
"face_rules": [
{
"name": "unknown_face",
"cooldown_ms": 30000,
"max_known_sim": 0.2,
"min_hits": 4,
"hit_window_ms": 3000
},
{
"name": "known_person",
"cooldown_ms": 30000,
"min_sim": 0.75,
"min_hits": 3,
"hit_window_ms": 2000
}
]
}
}
}
}
}
}
}

View File

@ -60,24 +60,6 @@
"max_age_ms": 1200,
"min_hits": 2
},
{
"id": "pose",
"type": "ai_pose",
"role": "filter",
"enable": true,
"model_path": "./models/yolov8n-pose.rknn",
"model_input_w": 640,
"model_input_h": 640,
"conf_thresh": 0.25,
"nms_thresh": 0.45
},
{
"id": "pose_assoc",
"type": "pose_assoc",
"role": "filter",
"enable": true,
"min_iou": 0.1
},
{
"id": "region_evt",
"type": "region_event",
@ -108,38 +90,16 @@
{
"type": "fall",
"window_ms": 1500,
"activate_duration_ms": 300,
"bbox": {
"enabled": true,
"min_drop_pixels": 120,
"min_aspect_ratio_delta": 0.35
},
"pose": {
"enabled": true,
"min_torso_drop_pixels": 120,
"max_upright_ratio": 0.60
},
"fusion": {
"match_mode": "any"
}
"min_drop_pixels": 120,
"min_aspect_ratio_delta": 0.35,
"activate_duration_ms": 300
},
{
"type": "fight",
"window_ms": 1200,
"activate_duration_ms": 200,
"bbox": {
"enabled": true,
"proximity_pixels": 220,
"min_motion_pixels": 90
},
"pose": {
"enabled": true,
"min_wrist_motion_pixels": 120,
"max_wrist_distance_pixels": 120
},
"fusion": {
"match_mode": "any"
}
"proximity_pixels": 220,
"min_motion_pixels": 90,
"activate_duration_ms": 200
}
]
},
@ -230,9 +190,7 @@
["in", "pre"],
["pre", "person_det"],
["person_det", "trk"],
["trk", "pose"],
["pose", "pose_assoc"],
["pose_assoc", "region_evt"],
["trk", "region_evt"],
["region_evt", "action_evt"],
["action_evt", "fusion"],
["fusion", "osd"],

View File

@ -854,118 +854,5 @@ python tools/render_config.py \
---
**更新日期**2026-03-15
## Pose-Enabled Behavior Configuration
This repository now supports two behavior deployment modes.
### Mode 1: Baseline Behavior Graph
Use this when you only need tracked detections and rule-based region behavior:
```text
input_rtsp -> preprocess -> ai_yolo -> tracker -> region_event -> action_recog -> event_fusion
```
Reference config:
- `configs/sample_region_behavior_intrusion.json`
Characteristics:
- No pose model dependency
- Lowest compute cost
- `fall` and `fight` rely on bbox-only rules
### Mode 2: Pose-Enabled Behavior Graph
Use this when you need pose-aware `fall` and `fight`:
```text
input_rtsp -> preprocess -> ai_yolo -> tracker -> ai_pose -> pose_assoc -> region_event -> action_recog -> event_fusion
```
Reference config:
- `configs/sample_region_behavior_full.json`
Characteristics:
- `ai_pose` writes `Frame.pose`
- `pose_assoc` assigns `PoseItem.track_id`
- `action_recog` can fuse bbox and pose signals per event rule
### Optional Integration Rules
- `ai_pose` is optional. Do not add it to graphs that only need intrusion or climb.
- `pose_assoc` should be placed after `ai_pose` and after tracked detections are already available on the frame.
- `action_recog` must remain runnable when `Frame.pose` is missing or empty.
### Degradation Semantics
- Without `ai_pose`, the graph must still behave correctly with bbox-only behavior logic.
- With `ai_pose` but without `pose_assoc`, `pose.track_id` is not guaranteed and downstream logic should treat it as optional compatibility mode.
- If pose inference returns no items for a frame, the rest of the graph must keep using `Frame.det` and `track_id`.
### Structured `action_recog` Rules
`action_recog` supports a structured event format with `bbox`, `pose`, and `fusion` sections.
Example:
```json
{
"id": "action_evt",
"type": "action_recog",
"events": [
{
"type": "fall",
"window_ms": 1500,
"activate_duration_ms": 300,
"bbox": {
"enabled": true,
"min_drop_pixels": 120,
"min_aspect_ratio_delta": 0.35
},
"pose": {
"enabled": true,
"min_torso_drop_pixels": 120,
"max_upright_ratio": 0.60
},
"fusion": {
"match_mode": "any"
}
},
{
"type": "fight",
"window_ms": 1200,
"activate_duration_ms": 200,
"bbox": {
"enabled": true,
"proximity_pixels": 220,
"min_motion_pixels": 90
},
"pose": {
"enabled": true,
"min_wrist_motion_pixels": 120,
"max_wrist_distance_pixels": 120
},
"fusion": {
"match_mode": "any"
}
}
]
}
```
Rules:
- `bbox.enabled=false` disables bbox signal evaluation for that event.
- `pose.enabled=false` disables pose signal evaluation for that event.
- `fusion.match_mode="any"` means either bbox or pose may activate the event.
- `fusion.match_mode="all"` means bbox and pose must both activate the event.
Backward compatibility:
- Old flat keys such as `min_drop_pixels` and `pose_min_torso_drop_pixels` are still accepted.
- New configs should prefer the structured form above.
**版本**v2.1
**更新日期**2026-04-16

View File

@ -1,134 +1,536 @@
# Edge 设备部署指南
# RK3588 Media Server 部署指南
## 首次部署(从 SD 卡克隆)
## 架构说明
### 克隆前准备
在源设备上清理不需要的文件后关机,取出 SD 卡,使用 `dd` 或烧录工具克隆到新卡。
### 克隆后配置
将新 SD 卡插入新设备,开机,执行以下步骤:
#### 1. 生成新的设备 ID
管理端通过设备 ID 区分不同设备,克隆后必须更换:
```bash
sudo rm -f /etc/machine-id /var/lib/safesight-edge-agent/device_id
sudo systemd-machine-id-setup
```
┌─────────────────────────────────────────────────────────────┐
│ RK3588 Device │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Media Server │ │ RK3588 Agent │ │
│ │ (systemd) │◄────│ (systemd) │ │
│ │ │ │ │ │
│ │ - RTSP输出 │ │ - Web管理界面 │ │
│ │ - HLS输出 │ │ - 服务监控 │ │
│ │ - NPU推理 │ │ - 远程配置 │ │
│ └─────────┬───────────┘ └─────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ 运行时数据目录 │ │
│ │ /var/lib/rk3588-media-server/ │ │
│ │ ├── hls/ - HLS分片输出 (root) │ │
│ │ ├── logs/ - 应用日志 (orangepi) │ │
│ │ ├── alarms/ - 告警截图 (orangepi) │ │
│ │ └── clips/ - 告警视频片段 (orangepi) │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ /opt/rk3588-media-server/ # 程序文件 │
│ /opt/rk3588-agent/ # Agent程序 │
└─────────────────────────────────────────────────────────────┘
```
#### 2. 重新生成 SSH 主机密钥
### 设计特点
- **Media Server**: 独立的 systemd 服务,高可用(崩溃自动重启)
- **Agent**: 独立的 systemd 服务,通过 `systemctl` 监控和控制 Media Server
- **两者可独立重启**: 重启 Agent 不会影响 Media Server
- **运行时数据分离**: 程序文件在 `/opt/`,数据在 `/var/lib/`
- **自动清理**: HLS 分片、日志、告警数据自动轮转清理
---
## 快速开始
### 1. 编译
在 RK3588 设备上编译:
```bash
sudo rm -f /etc/ssh/ssh_host_*_key*
sudo ssh-keygen -A
cd ~/apps/OrangePi3588Media
# 一键编译(推荐)
./scripts/build.sh
# 或使用完整 cmake 命令
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_TESTS=OFF \
-DBUILD_SAMPLES=ON \
-DRK3588_ENABLE_FFMPEG=ON \
-DRK3588_ENABLE_MPP=ON \
-DRK3588_ENABLE_RGA=ON \
-DRK3588_ENABLE_ZLMEDIAKIT=ON \
-DRK3588_ENABLE_RKNN=ON \
-DRK_ZLMK_API_LIB_PATH=$PWD/third_party/rknpu2/examples/3rdparty/zlmediakit/aarch64/libmk_api.so \
-DRK_ZLMEDIAKIT_INCLUDE_DIR=$PWD/third_party/rknpu2/examples/3rdparty/zlmediakit/include
cmake --build build -j$(nproc)
# 仅编译 Media Server
./scripts/build.sh -m
# 仅编译 Agent
./scripts/build.sh -a
# Debug 模式编译
./scripts/build.sh -d
# 清理后重新编译
./scripts/build.sh -c
```
#### 3. 设置主机名
**编译脚本选项:**
| 选项 | 说明 |
|------|------|
| `-h, --help` | 显示帮助 |
| `-c, --clean` | 清理构建目录后编译 |
| `-j N` | 指定并行编译线程数 |
| `-m, --media-only` | 仅编译 Media Server |
| `-a, --agent-only` | 仅编译 Agent |
| `-d, --debug` | Debug 模式编译 |
**环境变量:**
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `BUILD_TYPE` | Release | 编译类型 |
| `BUILD_TESTS` | OFF | 是否编译测试 |
| `BUILD_SAMPLES` | ON | 是否编译示例 |
| `ENABLE_FFMPEG` | ON | 启用 FFmpeg |
| `ENABLE_MPP` | ON | 启用 MPP |
| `ENABLE_RGA` | ON | 启用 RGA |
| `ENABLE_ZLMEDIAKIT` | ON | 启用 ZLMediaKit |
| `ENABLE_RKNN` | ON | 启用 RKNN |
### 2. 部署
```bash
sudo hostnamectl set-hostname <新主机名>
cd ~/apps/OrangePi3588Media
sudo ./scripts/deploy.sh install
```
#### 4. 配置静态 IP生产环境
部署过程会:
1. 安装 Media Server 和 Agent 到 `/opt/`
2. 创建运行时数据目录 `/var/lib/rk3588-media-server/`
3. 配置日志轮转和自动清理
4. 启动 systemd 服务
编辑 netplan 配置:
### 3. 查看状态
```bash
sudo nano /etc/netplan/01-network-manager-all.yaml
sudo ./scripts/deploy.sh status
```
写入:
### 3.1 DDR 固频排障
```yaml
network:
version: 2
ethernets:
enP4p65s0: # 网卡名,用 ip addr 确认
dhcp4: no
addresses:
- 10.0.0.82/24 # 目标 IP
routes:
- to: default
via: 10.0.0.1 # 网关
nameservers:
addresses:
- 10.0.0.1 # DNS
```
当出现 `CPU/NPU` 利用率不高,但 RTSP 输出帧率周期性跌落时,优先检查 DDR devfreq 是否在频繁降频。
应用
项目内置了 DDR 模式切换脚本:
```bash
sudo netplan apply
sudo bash ./scripts/ddr_mode.sh status
sudo bash ./scripts/ddr_mode.sh performance
sudo bash ./scripts/ddr_mode.sh restore
```
#### 5. 重启
也可以从运维入口调用:
```bash
sudo reboot
sudo ./scripts/ops.sh ddr-status
sudo ./scripts/ops.sh ddr-performance
sudo ./scripts/ops.sh ddr-restore
```
### 验证
说明:
- `performance`:保存当前 governor/min/max 后,将 DDR 切到高性能模式
- `restore`:恢复到切换前保存的 governor/min/max
- 默认节点为 `/sys/class/devfreq/dmc`
### 3.2 RTSP 解码推荐
对高位监控、烟雾多、偶发 H.264 参考帧异常的 RTSP 源,优先使用 `FFmpeg CPU decode + MPP encode` 组合。
如果业务目标是“只关心已检测到的鞋是否为黑色劳保鞋”,优先使用当前主线配置:
```bash
# 确认服务状态
systemctl status safesight-edge-server safesight-edge-agent
./build/media-server -c configs/person_shoe_two_stage_workshoe_alarm_v8s_shoe640.json
```
# 确认新 IP
ip addr show | grep inet
如果现场误报仍然偏多,可切换到更严格的对照版:
# 确认新设备 ID
cat /var/lib/safesight-edge-agent/device_id
```bash
./build/media-server -c configs/person_shoe_two_stage_workshoe_alarm_v8s_shoe640_strict.json
```
# 管理端发现新设备后,在设备详情页设置别名
说明:
- 输入端:`use_ffmpeg: true`、`use_mpp: false`
- 输出端:仍然使用 `MPP` 编码
- 鞋检测:使用 `shoe_det_yolov8s_workshoe_640_rk3588.rknn`
- 适用场景:源流在 VLC 中播放正常,但项目内 `ffmpeg demux + mpp decode` 会出现固定画面卡顿
- 当前业务目标:只对“检测到鞋,但颜色不符合黑色劳保鞋要求”的目标告警
现场实施时,优先修改这些参数:
- 鞋漏报多:
- `shoe_det.conf`
- `shoe_assoc.person_shoe_check.min_shoe_score`
- `dynamic_roi.min_person_height`
- 蓝框误报多:
- `shoe_det.conf`
- `shoe_assoc.person_shoe_check.min_shoe_score`
- `dynamic_roi.max_box_area_ratio`
- 黑鞋被误报:
- `shoe_color.color_check.dark_threshold`
- 告警太频繁:
- `alarm.rules[].min_duration_ms`
- `alarm.rules[].cooldown_ms`
- 完整流程 FPS 不够:
- `face_det.infer_fps`
- `face_recog.infer_fps`
- `dynamic_roi.max_rois`
排障建议:
- 若 VLC 直接拉原始 RTSP 源不卡,而项目内画面会在固定位置卡顿,优先切到 `use_ffmpeg: true`、`use_mpp: false`
- 若 FFmpeg 解码版不卡,则说明问题主要在 MPP 解码兼容性,而不是 AI 链路或发布链路
### 4. 卸载
```bash
sudo ./scripts/deploy.sh uninstall
```
---
## 更新部署
## 完整命令参考
### 首次安装(新设备/离线部署)
### 部署脚本命令
在开发机(已安装 safesight-edge 源码和编译环境)上:
| 命令 | 说明 |
|------|------|
| `sudo ./deploy.sh install` | 安装/部署服务 |
| `sudo ./deploy.sh upgrade` | 升级服务(保留配置和数据)|
| `sudo ./deploy.sh status` | 查看详细运行状态 |
| `sudo ./deploy.sh logs` | 查看实时日志 |
| `sudo ./deploy.sh clean-hls [天数]` | 清理 HLS 旧分片 |
| `sudo ./deploy.sh uninstall` | 卸载服务 |
### Media Server 服务管理
```bash
cd ~/apps/safesight-edge
git pull && bash scripts/package-offline.sh
# 启动/停止/重启
sudo systemctl start media-server
sudo systemctl stop media-server
sudo systemctl restart media-server
# 查看状态
sudo systemctl status media-server
# 查看日志 (systemd)
sudo journalctl -u media-server -f
sudo journalctl -u media-server --since "1 hour ago"
# 直接查看日志文件 (无需 sudo)
ls -la /var/lib/rk3588-media-server/logs/
cat /var/lib/rk3588-media-server/logs/media-server.log
```
将生成的包传到目标设备:
### Agent 服务管理
```bash
scp safesight-upgrade-*.tar.gz orangepi@<目标IP>:/tmp/
# 启动/停止/重启(不影响 Media Server
sudo systemctl start rk3588-agent
sudo systemctl stop rk3588-agent
sudo systemctl restart rk3588-agent
# 查看状态
sudo systemctl status rk3588-agent
# 查看日志 (systemd)
sudo journalctl -u rk3588-agent -f
# Agent 日志文件位置
cat /var/lib/rk3588-agent/agent.log
```
在目标设备上:
---
```bash
cd /tmp && tar -xzf safesight-upgrade-*.tar.gz
cd safesight-upgrade-*
sudo AGENT_TOKEN=<管理端token> ./deploy.sh install
## 目录结构
### 程序目录 (/opt/)
```
/opt/rk3588-media-server/
├── bin/
│ ├── media-server # 主程序
│ ├── cleanup-hls.sh # HLS清理脚本
│ └── plugins/ # 插件目录
│ ├── libinput_rtsp.so
│ ├── libai_yolo.so
│ ├── libosd.so
│ ├── libtracker.so
│ └── ...
├── lib/ # 依赖库
├── etc/
│ └── media-server.json # 配置文件
├── models/ # AI模型文件
│ ├── ppe_det_yolov8_ppe11_768_rk3588.rknn
│ ├── object_det_yolov8n_coco_640_rk3588.rknn
│ └── ...
└── web/ # Web静态文件
├── index.html
├── graph.html
├── hls_player.html # HLS播放器
└── hls/ # HLS输出(符号链接)
/opt/rk3588-agent/
├── rk3588-agent # Agent程序
└── agent.config.json # Agent配置
```
> 安装后配置为空模板,通过管理端部署向导下发实际配置。
### 运行时数据目录 (/var/lib/)
### 在线更新(可访问 git
```
/var/lib/rk3588-media-server/
├── hls/ # HLS直播分片 (root:root, 755)
│ ├── cam1/
│ │ ├── index.m3u8
│ │ ├── index0.ts
│ │ └── ...
│ ├── cam2/
│ └── ...
├── logs/ # 应用日志 (orangepi:orangepi, 755)
├── alarms/ # 告警截图 (orangepi:orangepi, 755)
└── clips/ # 告警视频片段 (orangepi:orangepi, 755)
/var/lib/rk3588-agent/
└── device_id # 设备唯一标识 (orangepi:orangepi)
```
**权限说明:**
- `logs/`, `alarms/`, `clips/` 目录归属于 `orangepi:orangepi`,方便默认用户查看和管理
- `hls/` 目录归属于 `root:root`,但权限 755 允许 orangepi 组读取
- Agent 相关目录归属于 `orangepi:orangepi`
---
## 配置修改
编辑配置文件:
```bash
cd ~/apps/safesight-edge
sudo nano /opt/rk3588-media-server/etc/media-server.json
```
修改后重启服务:
```bash
sudo systemctl restart media-server
```
---
## 数据管理
### HLS 分片清理
HLS 分片自动清理策略:
- **默认保留**: 3天
- **自动执行**: 每天凌晨3点
- **手动执行**: `sudo /opt/rk3588-media-server/bin/cleanup-hls.sh [天数]`
### 日志轮转
日志自动轮转策略:
- **保留周期**: 7天
- **压缩**: 启用
- **位置**: `/var/lib/rk3588-media-server/logs/`
- **文件归属**: `orangepi:orangepi`(方便默认用户查看)
### 告警数据清理
自动清理策略(通过 cron
- **告警视频** (clips): 保留30天
- **告警图片** (alarms): 保留90天
查看当前存储使用:
```bash
# 使用部署脚本查看(推荐)
sudo ./scripts/deploy.sh status
# 或直接查看目录大小
du -sh /var/lib/rk3588-media-server/*/
```
---
## 升级
升级步骤(保留配置和数据):
```bash
cd ~/apps/OrangePi3588Media
# 1. 拉取最新代码
git pull
bash scripts/build.sh -m -j4
cd agent && go build -o safesight-edge-agent ./cmd/safesight-agent && cd ..
# 2. 重新编译(清理后编译确保干净)
./scripts/build.sh -c
# 3. 执行升级
sudo ./scripts/deploy.sh upgrade
```
### 旧设备首次升级
---
## Web 管理界面
### Agent 管理界面
```
http://<设备IP>:9100
```
功能:
- 查看 Media Server 运行状态
- 启动/停止/重启 Media Server
- 查看配置信息
### HLS 播放器
```
http://<设备IP>:9000/hls_player.html
```
功能:
- 5 路视频同时播放
- 自适应布局
- 实时状态显示
---
## 故障排查
### 服务无法启动
```bash
cd ~/apps/safesight-edge && git pull
bash scripts/build.sh -m -j4
cd agent && go build -o safesight-edge-agent ./cmd/safesight-agent
sudo ./scripts/upgrade-from-old.sh
# 查看详细错误
sudo journalctl -u media-server -n 50
# 检查配置文件语法
sudo cat /opt/rk3588-media-server/etc/media-server.json | python3 -m json.tool
# 检查目录权限
ls -la /var/lib/rk3588-media-server/
ls -la /opt/rk3588-media-server/
# 修复日志目录权限(如需要)
sudo chown orangepi:orangepi /var/lib/rk3588-media-server/logs
sudo chown orangepi:orangepi /var/lib/rk3588-media-server/alarms
sudo chown orangepi:orangepi /var/lib/rk3588-media-server/clips
```
### NPU 问题
```bash
# 查看 NPU 状态
cat /proc/rknpu/load
cat /proc/rknpu/version
# 查看设备节点
ls -la /dev/rknpu*
# 查看温度
cat /sys/class/thermal/thermal_zone6/temp
```
### HLS 无法播放
```bash
# 检查 HLS 输出目录
ls -la /var/lib/rk3588-media-server/hls/
# 检查分片生成
ls -la /var/lib/rk3588-media-server/hls/cam1/
# 检查服务端口
ss -tlnp | grep 9000
```
### RTSP 无法连接
```bash
# 本地测试
ffplay rtsp://localhost:8555/live/cam1
# 检查端口
ss -tlnp | grep 8555
# 查看编码器状态
grep "RKVENC" /proc/mpp_service/sessions-summary
```
---
## 系统要求
- **硬件**: Orange Pi 5 Plus (RK3588) 或其他 RK3588 设备
- **系统**: Ubuntu 22.04 LTS (ARM64) 或 Debian 11
- **内存**: 建议 4GB 以上
- **存储**: 建议 32GB 以上HLS 分片占用空间)
- **依赖**: RKNN Runtime, MPP, FFmpeg
---
## 性能参考
5 路 720p@30fps 并发处理:
| 资源 | 使用率 | 备注 |
|------|--------|------|
| NPU Core0 | 70-80% | YOLO 推理 |
| CPU | 20-30% | 预处理、编码 |
| 编码器 | 5 路 720p | H264 CBR 2Mbps |
| 温度 | 50-60°C | 正常范围 |
| 内存 | 3-4GB | 包含系统 |
---
## 备份与恢复
### 备份配置和数据
```bash
# 创建备份
BACKUP_DIR="/root/rk3588-backup-$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"
# 备份配置
cp /opt/rk3588-media-server/etc/media-server.json "$BACKUP_DIR/"
# 备份模型
cp -r /opt/rk3588-media-server/models "$BACKUP_DIR/"
# 备份 HLS可选占用空间大
cp -r /var/lib/rk3588-media-server/hls "$BACKUP_DIR/"
echo "备份完成: $BACKUP_DIR"
```
### 恢复
```bash
# 恢复配置
cp "$BACKUP_DIR/media-server.json" /opt/rk3588-media-server/etc/
# 恢复模型
cp -r "$BACKUP_DIR/models"/* /opt/rk3588-media-server/models/
# 重启服务
sudo systemctl restart media-server
```

View File

@ -19,7 +19,7 @@ ffmpeg -f dshow -list_options true -i video="你的摄像头名称"
ffmpeg -f dshow -list_options true -i video="HD Webcam eMeet C960"
- 本地运行RTSP服务器
C:\Software\mediamtx\mediamtx.exe
mediamtx.exe
- 推流到RTSP服务器设置摄像头的分辨率为720P
ffmpeg -f dshow -rtbufsize 100M -video_size 1280x720 -framerate 30 -vcodec mjpeg -i video="4K AutoFocus Webcam" -c:v libx264 -preset ultrafast -pix_fmt yuv420p -f rtsp rtsp://localhost:8554/cam
@ -34,8 +34,6 @@ ffmpeg -stream_loop -1 -re -i "boots.mp4" -c:v libx264 -preset fast -tune zerola
ffmpeg -re -stream_loop -1 -i reg_001_单人_侧面_黑色鞋_1.mp4 -c copy -rtsp_transport tcp -f rtsp rtsp://10.0.0.49:8554/cam
C:\Users\Tellme\Pictures\人脸库> ffmpeg -re -stream_loop -1 -i reg_008_unk_011_多人_正面_黑色鞋_白色鞋_1.mp4 -c copy -rtsp_transport tcp -f rtsp rtsp://10.0.0.49:8554/cam
- 本地验证RTSP拉流正确
ffplay rtsp://localhost:8554/cam
@ -50,11 +48,11 @@ http://10.0.0.50:9000/hls_player.html
rtsp://10.0.0.50:8555/live/cam1
- 编译agent
go build -o safesight-agent_linux_arm64 ./cmd/safesight-agent
go build -o rk3588-agent_linux_arm64 ./cmd/rk3588-agent
- 运行模拟告警服务
C:\Users\Tellme\apps\safesight-edge\python .\mock_alarm_server.py
python .\mock_alarm_server.py
uv run --with flask scripts/mock_alarm_server.py
@ -70,7 +68,7 @@ curl -X POST http://10.0.0.49:8080/api/getToken
go run .\cmd\managerd\main.go .\managerd.json
# 综合监控(运行脚本)
~/apps/safesight-edge/scripts/ops.sh hw
~/apps/OrangePi3588Media/scripts/ops.sh hw
- 标准单路全流程测试:

View File

@ -16,7 +16,6 @@ namespace rk3588 {
struct FaceDetResult;
struct FaceRecogResult;
struct PoseResult;
enum class PixelFormat {
NV12,
@ -83,7 +82,6 @@ struct Frame {
std::shared_ptr<DetectionResult> det;
std::shared_ptr<BehaviorEventResult> behavior_events;
std::shared_ptr<PoseResult> pose;
// Face recognition pipeline meta (kept separate from user_meta to avoid conflicts with publish).
std::shared_ptr<FaceDetResult> face_det;
std::shared_ptr<FaceRecogResult> face_recog;

View File

@ -8,9 +8,9 @@
namespace rk3588 {
class EdgeServerApp {
class MediaServerApp {
public:
explicit EdgeServerApp(std::string config_path);
explicit MediaServerApp(std::string config_path);
int Start();
private:

View File

@ -1,34 +0,0 @@
#pragma once
#include <string>
#include <vector>
#include "frame/rect.h"
namespace rk3588 {
struct PosePoint2f {
float x = 0.0f;
float y = 0.0f;
};
struct PoseKeypoint {
PosePoint2f point{};
float score = 0.0f;
};
struct PoseItem {
Rect bbox{};
float score = 0.0f;
int track_id = -1;
std::vector<PoseKeypoint> keypoints;
};
struct PoseResult {
std::vector<PoseItem> items;
int img_w = 0;
int img_h = 0;
std::string model_name;
};
} // namespace rk3588

View File

@ -205,7 +205,7 @@ if(RK3588_ENABLE_ZLMEDIAKIT AND RK_ZLMK_API_LIB)
add_custom_command(TARGET publish POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${RK_ZLMK_API_LIB}" $<TARGET_FILE_DIR:publish>
)
install(FILES "${RK_ZLMK_API_LIB}" DESTINATION ${CMAKE_INSTALL_LIBDIR}/safesight-media/plugins)
install(FILES "${RK_ZLMK_API_LIB}" DESTINATION ${CMAKE_INSTALL_LIBDIR}/rk3588-media-server/plugins)
endif()
set_target_properties(publish PROPERTIES
OUTPUT_NAME "publish"
@ -500,30 +500,6 @@ set_target_properties(action_recog PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR}
)
# ai_pose plugin (pose inference scaffold)
add_library(ai_pose SHARED
ai_pose/ai_pose_node.cpp
)
target_include_directories(ai_pose PRIVATE ${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/third_party)
target_link_libraries(ai_pose PRIVATE project_options Threads::Threads)
set_target_properties(ai_pose PROPERTIES
OUTPUT_NAME "ai_pose"
LIBRARY_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR}
RUNTIME_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR}
)
# pose_assoc plugin (assign pose.track_id from tracked detections)
add_library(pose_assoc SHARED
pose_assoc/pose_assoc_node.cpp
)
target_include_directories(pose_assoc PRIVATE ${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/third_party)
target_link_libraries(pose_assoc PRIVATE project_options Threads::Threads)
set_target_properties(pose_assoc PROPERTIES
OUTPUT_NAME "pose_assoc"
LIBRARY_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR}
RUNTIME_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR}
)
# event_fusion plugin (behavior event id and lifecycle normalization)
add_library(event_fusion SHARED
event_fusion/event_fusion_node.cpp
@ -588,8 +564,8 @@ if(RK3588_ENABLE_ZLMEDIAKIT AND RK_ZLMK_API_LIB)
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${RK_ZLMK_API_LIB}" $<TARGET_FILE_DIR:zlm_http>
)
install(TARGETS zlm_http
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/safesight-media/plugins
RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}/safesight-media/plugins
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/rk3588-media-server/plugins
RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}/rk3588-media-server/plugins
)
endif()
@ -610,7 +586,7 @@ set_target_properties(ai_shoe_det PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR}
)
install(TARGETS input_rtsp input_file publish preprocess ai_yolo ai_face_det ai_scrfd ai_scrfd_sliding ai_face_recog tracker gate osd alarm logic_gate region_event action_recog ai_pose pose_assoc event_fusion storage ai_scheduler ai_shoe_det
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/safesight-media/plugins
RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}/safesight-media/plugins
install(TARGETS input_rtsp input_file publish preprocess ai_yolo ai_face_det ai_scrfd ai_scrfd_sliding ai_face_recog tracker gate osd alarm logic_gate region_event action_recog event_fusion storage ai_scheduler ai_shoe_det
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/rk3588-media-server/plugins
RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}/rk3588-media-server/plugins
)

View File

@ -4,7 +4,6 @@
#include <cmath>
#include <cstdint>
#include <deque>
#include <limits>
#include <map>
#include <memory>
#include <set>
@ -13,7 +12,6 @@
#include <vector>
#include "behavior/behavior_event.h"
#include "pose/pose_result.h"
#include "utils/logger.h"
namespace rk3588 {
@ -24,135 +22,21 @@ enum class ActionEventKind {
Fight
};
enum class ActionSignalMode {
Any,
All
};
struct FallBboxRule {
bool enabled = true;
float min_drop_pixels = 0.0f;
float min_aspect_ratio_delta = 0.0f;
};
struct FallPoseRule {
bool enabled = false;
float min_torso_drop_pixels = 0.0f;
float max_upright_ratio = 0.0f;
};
struct FightBboxRule {
bool enabled = true;
float proximity_pixels = 0.0f;
float min_motion_pixels = 0.0f;
};
struct FightPoseRule {
bool enabled = false;
float min_wrist_motion_pixels = 0.0f;
float max_wrist_distance_pixels = 0.0f;
};
struct ActionEventRule {
ActionEventKind kind = ActionEventKind::Fall;
uint64_t window_ms = 1000;
uint64_t activate_duration_ms = 0;
ActionSignalMode signal_mode = ActionSignalMode::Any;
FallBboxRule fall_bbox{};
FallPoseRule fall_pose{};
FightBboxRule fight_bbox{};
FightPoseRule fight_pose{};
};
struct PoseTrackSample {
bool matched = false;
float torso_center_y = 0.0f;
float upright_ratio = 0.0f;
float left_wrist_x = 0.0f;
float left_wrist_y = 0.0f;
float right_wrist_x = 0.0f;
float right_wrist_y = 0.0f;
bool has_left_wrist = false;
bool has_right_wrist = false;
float min_drop_pixels = 0.0f;
float min_aspect_ratio_delta = 0.0f;
float proximity_pixels = 0.0f;
float min_motion_pixels = 0.0f;
};
struct TrackSample {
uint64_t pts = 0;
Rect bbox{};
PoseTrackSample pose{};
};
static ActionSignalMode ParseSignalMode(const std::string& value) {
if (value == "all") return ActionSignalMode::All;
return ActionSignalMode::Any;
}
static bool EvaluateSignalMode(ActionSignalMode mode, bool bbox_trigger, bool pose_trigger,
bool bbox_enabled, bool pose_enabled) {
if (bbox_enabled && pose_enabled) {
return mode == ActionSignalMode::All ? (bbox_trigger && pose_trigger) : (bbox_trigger || pose_trigger);
}
if (bbox_enabled) return bbox_trigger;
if (pose_enabled) return pose_trigger;
return false;
}
static bool ParseFallRule(const SimpleJson& ev, ActionEventRule& rule) {
rule.kind = ActionEventKind::Fall;
rule.fall_bbox.min_drop_pixels = ev.ValueOr<float>("min_drop_pixels", 0.0f);
rule.fall_bbox.min_aspect_ratio_delta = ev.ValueOr<float>("min_aspect_ratio_delta", 0.0f);
rule.fall_pose.min_torso_drop_pixels = ev.ValueOr<float>("pose_min_torso_drop_pixels", 0.0f);
rule.fall_pose.max_upright_ratio = ev.ValueOr<float>("pose_max_upright_ratio", 0.0f);
if (const SimpleJson* bbox = ev.Find("bbox"); bbox && bbox->IsObject()) {
rule.fall_bbox.enabled = bbox->ValueOr<bool>("enabled", true);
rule.fall_bbox.min_drop_pixels = bbox->ValueOr<float>("min_drop_pixels", rule.fall_bbox.min_drop_pixels);
rule.fall_bbox.min_aspect_ratio_delta =
bbox->ValueOr<float>("min_aspect_ratio_delta", rule.fall_bbox.min_aspect_ratio_delta);
} else {
rule.fall_bbox.enabled = true;
}
if (const SimpleJson* pose = ev.Find("pose"); pose && pose->IsObject()) {
rule.fall_pose.enabled = pose->ValueOr<bool>("enabled", true);
rule.fall_pose.min_torso_drop_pixels =
pose->ValueOr<float>("min_torso_drop_pixels", rule.fall_pose.min_torso_drop_pixels);
rule.fall_pose.max_upright_ratio =
pose->ValueOr<float>("max_upright_ratio", rule.fall_pose.max_upright_ratio);
} else {
rule.fall_pose.enabled = rule.fall_pose.min_torso_drop_pixels > 0.0f || rule.fall_pose.max_upright_ratio > 0.0f;
}
return true;
}
static bool ParseFightRule(const SimpleJson& ev, ActionEventRule& rule) {
rule.kind = ActionEventKind::Fight;
rule.fight_bbox.proximity_pixels = ev.ValueOr<float>("proximity_pixels", 0.0f);
rule.fight_bbox.min_motion_pixels = ev.ValueOr<float>("min_motion_pixels", 0.0f);
rule.fight_pose.min_wrist_motion_pixels = ev.ValueOr<float>("pose_min_wrist_motion_pixels", 0.0f);
rule.fight_pose.max_wrist_distance_pixels = ev.ValueOr<float>("pose_max_wrist_distance_pixels", 0.0f);
if (const SimpleJson* bbox = ev.Find("bbox"); bbox && bbox->IsObject()) {
rule.fight_bbox.enabled = bbox->ValueOr<bool>("enabled", true);
rule.fight_bbox.proximity_pixels = bbox->ValueOr<float>("proximity_pixels", rule.fight_bbox.proximity_pixels);
rule.fight_bbox.min_motion_pixels = bbox->ValueOr<float>("min_motion_pixels", rule.fight_bbox.min_motion_pixels);
} else {
rule.fight_bbox.enabled = true;
}
if (const SimpleJson* pose = ev.Find("pose"); pose && pose->IsObject()) {
rule.fight_pose.enabled = pose->ValueOr<bool>("enabled", true);
rule.fight_pose.min_wrist_motion_pixels =
pose->ValueOr<float>("min_wrist_motion_pixels", rule.fight_pose.min_wrist_motion_pixels);
rule.fight_pose.max_wrist_distance_pixels =
pose->ValueOr<float>("max_wrist_distance_pixels", rule.fight_pose.max_wrist_distance_pixels);
} else {
rule.fight_pose.enabled =
rule.fight_pose.min_wrist_motion_pixels > 0.0f || rule.fight_pose.max_wrist_distance_pixels > 0.0f;
}
return true;
}
static bool ParseRules(const SimpleJson& config, std::vector<ActionEventRule>& out, std::string& err) {
const SimpleJson* events = config.Find("events");
if (!events || !events->IsArray()) {
@ -171,15 +55,15 @@ static bool ParseRules(const SimpleJson& config, std::vector<ActionEventRule>& o
const std::string type = ev.ValueOr<std::string>("type", "");
rule.window_ms = static_cast<uint64_t>(std::max(0, ev.ValueOr<int>("window_ms", 1000)));
rule.activate_duration_ms = static_cast<uint64_t>(std::max(0, ev.ValueOr<int>("activate_duration_ms", 0)));
rule.signal_mode = ParseSignalMode(ev.ValueOr<std::string>("signal_mode", "any"));
if (const SimpleJson* fusion = ev.Find("fusion"); fusion && fusion->IsObject()) {
rule.signal_mode = ParseSignalMode(fusion->ValueOr<std::string>("match_mode", "any"));
}
if (type == "fall") {
ParseFallRule(ev, rule);
rule.kind = ActionEventKind::Fall;
rule.min_drop_pixels = ev.ValueOr<float>("min_drop_pixels", 0.0f);
rule.min_aspect_ratio_delta = ev.ValueOr<float>("min_aspect_ratio_delta", 0.0f);
} else if (type == "fight") {
ParseFightRule(ev, rule);
rule.kind = ActionEventKind::Fight;
rule.proximity_pixels = ev.ValueOr<float>("proximity_pixels", 0.0f);
rule.min_motion_pixels = ev.ValueOr<float>("min_motion_pixels", 0.0f);
} else {
err = "unsupported event type: " + type;
return false;
@ -214,129 +98,6 @@ static float AspectRatio(const Rect& rect) {
return rect.h > 0.0f ? (rect.w / rect.h) : 0.0f;
}
static float BboxIoU(const Rect& lhs, const Rect& rhs) {
const float x1 = std::max(lhs.x, rhs.x);
const float y1 = std::max(lhs.y, rhs.y);
const float x2 = std::min(lhs.x + lhs.w, rhs.x + rhs.w);
const float y2 = std::min(lhs.y + lhs.h, rhs.y + rhs.h);
const float iw = std::max(0.0f, x2 - x1);
const float ih = std::max(0.0f, y2 - y1);
const float inter = iw * ih;
const float uni = lhs.w * lhs.h + rhs.w * rhs.h - inter;
return uni <= 0.0f ? 0.0f : (inter / uni);
}
static bool IsValidKeypoint(const PoseItem& pose, int index) {
return index >= 0 &&
static_cast<size_t>(index) < pose.keypoints.size() &&
pose.keypoints[static_cast<size_t>(index)].score > 0.0f;
}
static PoseTrackSample BuildPoseTrackSample(const PoseItem& pose) {
PoseTrackSample sample;
sample.matched = true;
const bool has_left_shoulder = IsValidKeypoint(pose, 5);
const bool has_right_shoulder = IsValidKeypoint(pose, 6);
const bool has_left_hip = IsValidKeypoint(pose, 11);
const bool has_right_hip = IsValidKeypoint(pose, 12);
if (has_left_shoulder && has_right_shoulder && has_left_hip && has_right_hip) {
const PosePoint2f& ls = pose.keypoints[5].point;
const PosePoint2f& rs = pose.keypoints[6].point;
const PosePoint2f& lh = pose.keypoints[11].point;
const PosePoint2f& rh = pose.keypoints[12].point;
const float shoulder_center_y = (ls.y + rs.y) * 0.5f;
const float hip_center_y = (lh.y + rh.y) * 0.5f;
sample.torso_center_y = (shoulder_center_y + hip_center_y) * 0.5f;
const float min_x = std::min(std::min(ls.x, rs.x), std::min(lh.x, rh.x));
const float max_x = std::max(std::max(ls.x, rs.x), std::max(lh.x, rh.x));
const float min_y = std::min(std::min(ls.y, rs.y), std::min(lh.y, rh.y));
const float max_y = std::max(std::max(ls.y, rs.y), std::max(lh.y, rh.y));
const float horizontal_span = std::fabs(max_x - min_x);
const float vertical_span = std::fabs(max_y - min_y);
sample.upright_ratio = vertical_span > 1e-3f ? (horizontal_span / vertical_span) : std::numeric_limits<float>::infinity();
}
if (IsValidKeypoint(pose, 9)) {
sample.left_wrist_x = pose.keypoints[9].point.x;
sample.left_wrist_y = pose.keypoints[9].point.y;
sample.has_left_wrist = true;
}
if (IsValidKeypoint(pose, 10)) {
sample.right_wrist_x = pose.keypoints[10].point.x;
sample.right_wrist_y = pose.keypoints[10].point.y;
sample.has_right_wrist = true;
}
return sample;
}
static PoseTrackSample MatchPoseToDetection(const Rect& bbox, const std::shared_ptr<PoseResult>& pose_result) {
if (!pose_result) return {};
float best_iou = 0.0f;
const PoseItem* best_pose = nullptr;
for (const auto& pose : pose_result->items) {
const float iou = BboxIoU(bbox, pose.bbox);
if (iou > best_iou) {
best_iou = iou;
best_pose = &pose;
}
}
if (!best_pose || best_iou <= 0.0f) return {};
return BuildPoseTrackSample(*best_pose);
}
static PoseTrackSample MatchPoseToTrackId(int track_id, const Rect& bbox, const std::shared_ptr<PoseResult>& pose_result) {
if (!pose_result) return {};
const PoseItem* best_pose = nullptr;
float best_iou = 0.0f;
for (const auto& pose : pose_result->items) {
if (pose.track_id != track_id) continue;
const float iou = BboxIoU(bbox, pose.bbox);
if (iou >= best_iou) {
best_iou = iou;
best_pose = &pose;
}
}
if (best_pose) return BuildPoseTrackSample(*best_pose);
return MatchPoseToDetection(bbox, pose_result);
}
static float WristMotion(const PoseTrackSample& newer, const PoseTrackSample& older) {
float motion = 0.0f;
if (newer.has_left_wrist && older.has_left_wrist) {
const float dx = newer.left_wrist_x - older.left_wrist_x;
const float dy = newer.left_wrist_y - older.left_wrist_y;
motion += std::sqrt(dx * dx + dy * dy);
}
if (newer.has_right_wrist && older.has_right_wrist) {
const float dx = newer.right_wrist_x - older.right_wrist_x;
const float dy = newer.right_wrist_y - older.right_wrist_y;
motion += std::sqrt(dx * dx + dy * dy);
}
return motion;
}
static float MinimumWristDistance(const PoseTrackSample& lhs, const PoseTrackSample& rhs) {
float best = std::numeric_limits<float>::infinity();
auto update = [&](bool has_a, float ax, float ay, bool has_b, float bx, float by) {
if (!has_a || !has_b) return;
const float dx = ax - bx;
const float dy = ay - by;
best = std::min(best, std::sqrt(dx * dx + dy * dy));
};
update(lhs.has_left_wrist, lhs.left_wrist_x, lhs.left_wrist_y,
rhs.has_left_wrist, rhs.left_wrist_x, rhs.left_wrist_y);
update(lhs.has_left_wrist, lhs.left_wrist_x, lhs.left_wrist_y,
rhs.has_right_wrist, rhs.right_wrist_x, rhs.right_wrist_y);
update(lhs.has_right_wrist, lhs.right_wrist_x, lhs.right_wrist_y,
rhs.has_left_wrist, rhs.left_wrist_x, rhs.left_wrist_y);
update(lhs.has_right_wrist, lhs.right_wrist_x, lhs.right_wrist_y,
rhs.has_right_wrist, rhs.right_wrist_x, rhs.right_wrist_y);
return std::isfinite(best) ? best : std::numeric_limits<float>::infinity();
}
static BehaviorEventType ToBehaviorEventType(ActionEventKind kind) {
return kind == ActionEventKind::Fall ? BehaviorEventType::Fall : BehaviorEventType::Fight;
}
@ -391,7 +152,7 @@ NodeStatus ActionRecogNode::Process(FramePtr frame) {
if (det.track_id < 0) continue;
current_tracks[det.track_id] = det.bbox;
auto& history = impl_->history[det.track_id];
history.push_back(TrackSample{frame->pts, det.bbox, MatchPoseToTrackId(det.track_id, det.bbox, frame->pose)});
history.push_back(TrackSample{frame->pts, det.bbox});
while (!history.empty() && frame->pts > history.front().pts &&
(frame->pts - history.front().pts) > impl_->rules.front().window_ms) {
history.pop_front();
@ -407,18 +168,8 @@ NodeStatus ActionRecogNode::Process(FramePtr frame) {
const float drop = CenterY(bbox) - CenterY(first.bbox);
const float aspect_delta = AspectRatio(bbox) - AspectRatio(first.bbox);
const uint64_t duration = frame->pts >= first.pts ? (frame->pts - first.pts) : 0;
const bool bbox_enabled = rule.fall_bbox.enabled;
const bool pose_enabled = rule.fall_pose.enabled;
const bool bbox_trigger = drop >= rule.fall_bbox.min_drop_pixels &&
aspect_delta >= rule.fall_bbox.min_aspect_ratio_delta;
bool pose_trigger = false;
if (pose_enabled &&
first.pose.matched && it->second.back().pose.matched) {
const float torso_drop = it->second.back().pose.torso_center_y - first.pose.torso_center_y;
pose_trigger = torso_drop >= rule.fall_pose.min_torso_drop_pixels &&
it->second.back().pose.upright_ratio >= rule.fall_pose.max_upright_ratio;
}
if (!EvaluateSignalMode(rule.signal_mode, bbox_trigger, pose_trigger, bbox_enabled, pose_enabled)) continue;
if (drop < rule.min_drop_pixels) continue;
if (aspect_delta < rule.min_aspect_ratio_delta) continue;
if (duration < rule.activate_duration_ms) continue;
BehaviorEventItem event;
@ -438,7 +189,7 @@ NodeStatus ActionRecogNode::Process(FramePtr frame) {
for (auto left = current_tracks.begin(); left != current_tracks.end(); ++left) {
for (auto right = std::next(left); right != current_tracks.end(); ++right) {
const float proximity = CenterDistance(left->second, right->second);
if (proximity > rule.fight_bbox.proximity_pixels) continue;
if (proximity > rule.proximity_pixels) continue;
const auto it_left = impl_->history.find(left->first);
const auto it_right = impl_->history.find(right->first);
@ -452,22 +203,7 @@ NodeStatus ActionRecogNode::Process(FramePtr frame) {
const uint64_t duration = std::min(duration_left, duration_right);
const float combined_motion = left_motion + right_motion;
const bool bbox_enabled = rule.fight_bbox.enabled;
const bool pose_enabled = rule.fight_pose.enabled;
const bool bbox_trigger = combined_motion >= rule.fight_bbox.min_motion_pixels;
bool pose_trigger = false;
const auto& left_pose_now = it_left->second.back().pose;
const auto& right_pose_now = it_right->second.back().pose;
if (pose_enabled &&
it_left->second.front().pose.matched && it_right->second.front().pose.matched &&
left_pose_now.matched && right_pose_now.matched) {
const float pose_motion = WristMotion(left_pose_now, it_left->second.front().pose) +
WristMotion(right_pose_now, it_right->second.front().pose);
const float wrist_distance = MinimumWristDistance(left_pose_now, right_pose_now);
pose_trigger = pose_motion >= rule.fight_pose.min_wrist_motion_pixels &&
wrist_distance <= rule.fight_pose.max_wrist_distance_pixels;
}
if (!EvaluateSignalMode(rule.signal_mode, bbox_trigger, pose_trigger, bbox_enabled, pose_enabled)) continue;
if (combined_motion < rule.min_motion_pixels) continue;
if (duration < rule.activate_duration_ms) continue;
const auto pair_key = std::make_pair(left->first, right->first);

View File

@ -1,466 +0,0 @@
#include "ai_pose_node.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <limits>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "face/face_detection_utils.h"
#include "hw/i_infer_backend.h"
#include "pose/pose_result.h"
#include "utils/logger.h"
namespace rk3588 {
namespace {
using face_detection::HalfToFloat;
using face_detection::ResizeRgbBilinear;
constexpr int kPoseOutputCount = 4;
constexpr int kPoseLocChannels = 64;
constexpr int kPoseScoreChannel = 64;
constexpr int kDflBins = 16;
constexpr int kMaxPoseDetections = 32;
constexpr uint8_t kLetterboxValue = 114;
struct PoseConfig {
bool enabled = true;
std::string model_path;
int model_input_w = 640;
int model_input_h = 640;
int expected_keypoints = 17;
float conf_thresh = 0.25f;
float nms_thresh = 0.45f;
};
struct LetterboxInfo {
float scale = 1.0f;
float pad_x = 0.0f;
float pad_y = 0.0f;
};
struct PoseCandidate {
Rect bbox{};
float score = 0.0f;
int keypoint_index = -1;
};
bool ParseConfig(const SimpleJson& config, PoseConfig& out, std::string& err) {
out.enabled = config.ValueOr<bool>("enabled", true);
out.model_path = config.ValueOr<std::string>("model_path", "");
out.model_input_w = config.ValueOr<int>("model_input_w", 640);
out.model_input_h = config.ValueOr<int>("model_input_h", 640);
out.expected_keypoints = config.ValueOr<int>("expected_keypoints", 17);
out.conf_thresh = config.ValueOr<float>("conf_thresh", 0.25f);
out.nms_thresh = config.ValueOr<float>("nms_thresh", 0.45f);
if (out.model_input_w <= 0 || out.model_input_h <= 0) {
err = "model_input_w and model_input_h must be positive";
return false;
}
if (out.expected_keypoints <= 0) {
err = "expected_keypoints must be positive";
return false;
}
if (out.conf_thresh <= 0.0f || out.conf_thresh >= 1.0f) {
err = "conf_thresh must be in (0, 1)";
return false;
}
if (out.nms_thresh <= 0.0f || out.nms_thresh >= 1.0f) {
err = "nms_thresh must be in (0, 1)";
return false;
}
if (out.enabled && out.model_path.empty()) {
err = "model_path is required when enabled=true";
return false;
}
return true;
}
void PushToDownstream(const std::vector<std::shared_ptr<SpscQueue<FramePtr>>>& output_queues, const FramePtr& frame) {
for (const auto& q : output_queues) {
if (q) q->Push(frame);
}
}
inline float Sigmoid(float x) {
return 1.0f / (1.0f + std::exp(-x));
}
float Unsigmoid(float y) {
return -std::log((1.0f / y) - 1.0f);
}
float ClampFloat(float v, float lo, float hi) {
return std::max(lo, std::min(v, hi));
}
float CalculateIoU(const Rect& lhs, const Rect& rhs) {
const float x1 = std::max(lhs.x, rhs.x);
const float y1 = std::max(lhs.y, rhs.y);
const float x2 = std::min(lhs.x + lhs.w, rhs.x + rhs.w);
const float y2 = std::min(lhs.y + lhs.h, rhs.y + rhs.h);
const float inter_w = std::max(0.0f, x2 - x1);
const float inter_h = std::max(0.0f, y2 - y1);
const float inter = inter_w * inter_h;
const float union_area = lhs.w * lhs.h + rhs.w * rhs.h - inter;
return union_area <= 0.0f ? 0.0f : (inter / union_area);
}
float ReadTensorValue(const InferOutput& output, size_t index) {
#if defined(RK3588_ENABLE_RKNN)
switch (output.type) {
case RKNN_TENSOR_FLOAT32: {
const auto* p = reinterpret_cast<const float*>(output.data.data());
return p[index];
}
case RKNN_TENSOR_FLOAT16: {
const auto* p = reinterpret_cast<const uint16_t*>(output.data.data());
return HalfToFloat(p[index]);
}
case RKNN_TENSOR_INT8: {
const auto* p = reinterpret_cast<const int8_t*>(output.data.data());
return (static_cast<float>(p[index]) - static_cast<float>(output.zp)) * output.scale;
}
case RKNN_TENSOR_UINT8: {
const auto* p = reinterpret_cast<const uint8_t*>(output.data.data());
return (static_cast<float>(p[index]) - static_cast<float>(output.zp)) * output.scale;
}
default:
break;
}
#endif
const auto* p = reinterpret_cast<const float*>(output.data.data());
return p[index];
}
bool BuildLetterboxedRgbInput(const Frame& frame, int dst_w, int dst_h,
std::vector<uint8_t>& out, LetterboxInfo& letterbox) {
if (frame.width <= 0 || frame.height <= 0 || dst_w <= 0 || dst_h <= 0) return false;
if (frame.format != PixelFormat::RGB && frame.format != PixelFormat::BGR) return false;
const uint8_t* src = frame.planes[0].data ? frame.planes[0].data : frame.data;
if (!src) return false;
const int src_stride = frame.planes[0].stride > 0 ? frame.planes[0].stride : frame.width * 3;
out.assign(static_cast<size_t>(dst_w) * static_cast<size_t>(dst_h) * 3, kLetterboxValue);
const float scale = std::min(static_cast<float>(dst_w) / static_cast<float>(frame.width),
static_cast<float>(dst_h) / static_cast<float>(frame.height));
const int resized_w = std::max(1, static_cast<int>(std::round(frame.width * scale)));
const int resized_h = std::max(1, static_cast<int>(std::round(frame.height * scale)));
const int pad_x = (dst_w - resized_w) / 2;
const int pad_y = (dst_h - resized_h) / 2;
std::vector<uint8_t> resized(static_cast<size_t>(resized_w) * static_cast<size_t>(resized_h) * 3);
const bool swap_rb = (frame.format == PixelFormat::BGR);
ResizeRgbBilinear(src, frame.width, frame.height, src_stride, resized.data(), resized_w, resized_h, swap_rb);
for (int y = 0; y < resized_h; ++y) {
uint8_t* dst_row = out.data() + (static_cast<size_t>(y + pad_y) * static_cast<size_t>(dst_w) + pad_x) * 3;
const uint8_t* src_row = resized.data() + static_cast<size_t>(y) * static_cast<size_t>(resized_w) * 3;
std::memcpy(dst_row, src_row, static_cast<size_t>(resized_w) * 3);
}
letterbox.scale = scale;
letterbox.pad_x = static_cast<float>(pad_x);
letterbox.pad_y = static_cast<float>(pad_y);
return true;
}
float DecodeDfl(const InferOutput& output, int cell_index, int cells_per_head, int channel_group) {
float max_value = -std::numeric_limits<float>::infinity();
float logits[kDflBins]{};
for (int i = 0; i < kDflBins; ++i) {
const size_t offset = static_cast<size_t>((channel_group * kDflBins + i) * cells_per_head + cell_index);
logits[i] = ReadTensorValue(output, offset);
max_value = std::max(max_value, logits[i]);
}
float denom = 0.0f;
float numer = 0.0f;
for (int i = 0; i < kDflBins; ++i) {
const float e = std::exp(logits[i] - max_value);
denom += e;
numer += e * static_cast<float>(i);
}
return denom > 0.0f ? (numer / denom) : 0.0f;
}
std::vector<PoseCandidate> DecodePoseCandidates(const std::vector<InferOutput>& outputs,
const PoseConfig& config) {
std::vector<PoseCandidate> candidates;
if (outputs.size() < kPoseOutputCount) return candidates;
const float score_threshold = Unsigmoid(config.conf_thresh);
int keypoint_base_index = 0;
for (int head = 0; head < 3; ++head) {
const int stride = (head == 0) ? 8 : (head == 1 ? 16 : 32);
const int grid_h = config.model_input_h / stride;
const int grid_w = config.model_input_w / stride;
const int cells = grid_h * grid_w;
if (cells <= 0) continue;
const InferOutput& output = outputs[static_cast<size_t>(head)];
const size_t elem_count = sizeof(float) > 0 ? (output.data.size() / sizeof(float)) : 0;
#if defined(RK3588_ENABLE_RKNN)
size_t bytes_per_elem = 1;
switch (output.type) {
case RKNN_TENSOR_FLOAT32: bytes_per_elem = sizeof(float); break;
case RKNN_TENSOR_FLOAT16: bytes_per_elem = sizeof(uint16_t); break;
default: bytes_per_elem = 1; break;
}
const size_t actual_elem_count = bytes_per_elem > 0 ? (output.data.size() / bytes_per_elem) : 0;
#else
const size_t actual_elem_count = elem_count;
#endif
if (actual_elem_count < static_cast<size_t>(65 * cells)) {
keypoint_base_index += cells;
continue;
}
for (int cell = 0; cell < cells; ++cell) {
const size_t score_offset = static_cast<size_t>(kPoseScoreChannel * cells + cell);
const float score_logit = ReadTensorValue(output, score_offset);
if (score_logit < score_threshold) continue;
const float left = DecodeDfl(output, cell, cells, 0);
const float top = DecodeDfl(output, cell, cells, 1);
const float right = DecodeDfl(output, cell, cells, 2);
const float bottom = DecodeDfl(output, cell, cells, 3);
const int grid_x = cell % grid_w;
const int grid_y = cell / grid_w;
const float center_x = (static_cast<float>(grid_x) + 0.5f) * stride;
const float center_y = (static_cast<float>(grid_y) + 0.5f) * stride;
const float x1 = center_x - left * stride;
const float y1 = center_y - top * stride;
const float x2 = center_x + right * stride;
const float y2 = center_y + bottom * stride;
PoseCandidate candidate;
candidate.bbox.x = x1;
candidate.bbox.y = y1;
candidate.bbox.w = std::max(0.0f, x2 - x1);
candidate.bbox.h = std::max(0.0f, y2 - y1);
candidate.score = Sigmoid(score_logit);
candidate.keypoint_index = keypoint_base_index + cell;
candidates.push_back(candidate);
}
keypoint_base_index += cells;
}
std::sort(candidates.begin(), candidates.end(), [](const PoseCandidate& lhs, const PoseCandidate& rhs) {
return lhs.score > rhs.score;
});
std::vector<PoseCandidate> kept;
kept.reserve(candidates.size());
for (const auto& candidate : candidates) {
bool suppressed = false;
for (const auto& accepted : kept) {
if (CalculateIoU(candidate.bbox, accepted.bbox) > config.nms_thresh) {
suppressed = true;
break;
}
}
if (!suppressed) {
kept.push_back(candidate);
if (static_cast<int>(kept.size()) >= kMaxPoseDetections) break;
}
}
return kept;
}
float MapToSourceCoord(float value, float pad, float scale, int limit) {
if (scale <= 0.0f) return 0.0f;
return ClampFloat((value - pad) / scale, 0.0f, static_cast<float>(std::max(0, limit)));
}
std::shared_ptr<PoseResult> BuildPoseResult(const std::vector<InferOutput>& outputs,
const PoseConfig& config,
int frame_w, int frame_h,
const LetterboxInfo& letterbox) {
if (outputs.size() < kPoseOutputCount) return nullptr;
const InferOutput& keypoint_output = outputs[3];
const int total_points = (config.model_input_w / 8) * (config.model_input_h / 8) +
(config.model_input_w / 16) * (config.model_input_h / 16) +
(config.model_input_w / 32) * (config.model_input_h / 32);
const int values_per_point = config.expected_keypoints * 3;
#if defined(RK3588_ENABLE_RKNN)
size_t kp_bytes_per_elem = 1;
switch (keypoint_output.type) {
case RKNN_TENSOR_FLOAT32: kp_bytes_per_elem = sizeof(float); break;
case RKNN_TENSOR_FLOAT16: kp_bytes_per_elem = sizeof(uint16_t); break;
default: kp_bytes_per_elem = 1; break;
}
#else
const size_t kp_bytes_per_elem = sizeof(float);
#endif
const size_t kp_elem_count = kp_bytes_per_elem > 0 ? (keypoint_output.data.size() / kp_bytes_per_elem) : 0;
if (kp_elem_count < static_cast<size_t>(values_per_point * total_points)) return nullptr;
auto result = std::make_shared<PoseResult>();
result->img_w = frame_w;
result->img_h = frame_h;
result->model_name = "yolov8n-pose";
const std::vector<PoseCandidate> candidates = DecodePoseCandidates(outputs, config);
result->items.reserve(candidates.size());
for (const auto& candidate : candidates) {
if (candidate.keypoint_index < 0 || candidate.keypoint_index >= total_points) continue;
PoseItem item;
item.score = candidate.score;
item.track_id = -1;
item.bbox.x = MapToSourceCoord(candidate.bbox.x, letterbox.pad_x, letterbox.scale, frame_w);
item.bbox.y = MapToSourceCoord(candidate.bbox.y, letterbox.pad_y, letterbox.scale, frame_h);
const float x2 = MapToSourceCoord(candidate.bbox.x + candidate.bbox.w, letterbox.pad_x, letterbox.scale, frame_w);
const float y2 = MapToSourceCoord(candidate.bbox.y + candidate.bbox.h, letterbox.pad_y, letterbox.scale, frame_h);
item.bbox.w = std::max(0.0f, x2 - item.bbox.x);
item.bbox.h = std::max(0.0f, y2 - item.bbox.y);
item.keypoints.reserve(static_cast<size_t>(config.expected_keypoints));
for (int k = 0; k < config.expected_keypoints; ++k) {
const size_t x_offset = static_cast<size_t>(k * 3 * total_points + candidate.keypoint_index);
const size_t y_offset = static_cast<size_t>(k * 3 * total_points + total_points + candidate.keypoint_index);
const size_t s_offset = static_cast<size_t>(k * 3 * total_points + 2 * total_points + candidate.keypoint_index);
PoseKeypoint kp;
kp.point.x = MapToSourceCoord(ReadTensorValue(keypoint_output, x_offset), letterbox.pad_x, letterbox.scale, frame_w);
kp.point.y = MapToSourceCoord(ReadTensorValue(keypoint_output, y_offset), letterbox.pad_y, letterbox.scale, frame_h);
kp.score = ReadTensorValue(keypoint_output, s_offset);
item.keypoints.push_back(kp);
}
result->items.push_back(std::move(item));
}
return result;
}
} // namespace
struct AiPoseNode::Impl {
PoseConfig config;
std::string init_err;
std::shared_ptr<IInferBackend> infer_backend;
ModelHandle model_handle = kInvalidModelHandle;
bool started = false;
std::vector<uint8_t> input_rgb;
};
AiPoseNode::AiPoseNode() : impl_(std::make_unique<Impl>()) {}
AiPoseNode::~AiPoseNode() = default;
std::string AiPoseNode::Id() const {
return id_;
}
std::string AiPoseNode::Type() const {
return "ai_pose";
}
bool AiPoseNode::Init(const SimpleJson& config, const NodeContext& ctx) {
id_ = config.ValueOr<std::string>("id", "ai_pose");
if (!ParseConfig(config, impl_->config, impl_->init_err)) {
LogError("[ai_pose] invalid config: " + impl_->init_err);
return false;
}
impl_->infer_backend = ctx.infer_backend;
output_queues_ = ctx.output_queues;
return true;
}
bool AiPoseNode::Start() {
if (!impl_->config.enabled) {
impl_->started = true;
return true;
}
if (!impl_->infer_backend) {
LogError("[ai_pose] infer_backend is required when enabled=true");
return false;
}
std::string err;
impl_->model_handle = impl_->infer_backend->LoadModel(impl_->config.model_path, err);
if (impl_->model_handle == kInvalidModelHandle) {
LogError("[ai_pose] failed to load model: " + impl_->config.model_path + " err=" + err);
return false;
}
impl_->started = true;
return true;
}
void AiPoseNode::Stop() {
if (impl_->infer_backend && impl_->model_handle != kInvalidModelHandle) {
impl_->infer_backend->UnloadModel(impl_->model_handle);
impl_->model_handle = kInvalidModelHandle;
}
impl_->started = false;
}
NodeStatus AiPoseNode::Process(FramePtr frame) {
if (!frame) return NodeStatus::DROP;
if (!impl_->config.enabled) {
PushToDownstream(output_queues_, frame);
return NodeStatus::OK;
}
if (!impl_->started || !impl_->infer_backend || impl_->model_handle == kInvalidModelHandle) {
LogWarn("[ai_pose] process called before node is started");
PushToDownstream(output_queues_, frame);
return NodeStatus::ERROR;
}
LetterboxInfo letterbox;
if (!BuildLetterboxedRgbInput(*frame, impl_->config.model_input_w, impl_->config.model_input_h,
impl_->input_rgb, letterbox)) {
LogWarn("[ai_pose] only RGB/BGR frames are supported currently");
PushToDownstream(output_queues_, frame);
return NodeStatus::OK;
}
InferInput input;
input.data = impl_->input_rgb.data();
input.size = impl_->input_rgb.size();
input.width = impl_->config.model_input_w;
input.height = impl_->config.model_input_h;
input.is_nhwc = true;
input.dma_fd = -1;
input.dma_offset = 0;
InferResult infer_result = impl_->infer_backend->Infer(impl_->model_handle, input);
if (!infer_result.success) {
LogWarn("[ai_pose] inference failed: " + infer_result.error);
PushToDownstream(output_queues_, frame);
return NodeStatus::OK;
}
frame->pose = BuildPoseResult(infer_result.outputs, impl_->config, frame->width, frame->height, letterbox);
if (!frame->pose) {
frame->pose = std::make_shared<PoseResult>();
frame->pose->img_w = frame->width;
frame->pose->img_h = frame->height;
frame->pose->model_name = "yolov8n-pose";
}
PushToDownstream(output_queues_, frame);
return NodeStatus::OK;
}
#ifndef RK3588_TEST_BUILD
REGISTER_NODE(AiPoseNode, "ai_pose");
#endif
} // namespace rk3588

View File

@ -1,30 +0,0 @@
#pragma once
#include <memory>
#include <string>
#include <vector>
#include "node.h"
namespace rk3588 {
class AiPoseNode final : public INode {
public:
AiPoseNode();
~AiPoseNode() override;
std::string Id() const override;
std::string Type() const override;
bool Init(const SimpleJson& config, const NodeContext& ctx) override;
bool Start() override;
void Stop() override;
NodeStatus Process(FramePtr frame) override;
private:
struct Impl;
std::unique_ptr<Impl> impl_;
std::string id_;
std::vector<std::shared_ptr<SpscQueue<FramePtr>>> output_queues_;
};
} // namespace rk3588

View File

@ -5,7 +5,6 @@
#include <cctype>
#include <cstdlib>
#include <ctime>
#include <iomanip>
#include <sstream>
#include <string>
#include <string_view>
@ -112,7 +111,7 @@ void HttpAction::Execute(AlarmEvent& event, std::shared_ptr<Frame> /*frame*/) {
const auto& det = event.detections[i];
if (i > 0) oss << ",";
oss << "{\"cls_id\":" << det.cls_id
<< ",\"score\":" << std::fixed << std::setprecision(2) << det.score
<< ",\"score\":" << det.score
<< ",\"bbox\":{\"x\":" << det.bbox.x
<< ",\"y\":" << det.bbox.y
<< ",\"w\":" << det.bbox.w

View File

@ -951,11 +951,7 @@ private:
Detection d;
d.cls_id = -1;
// Unknown: show stranger likelihood (1 - best_sim). Known: show match confidence (best_sim).
float rawScore = (rule.kind == FaceRule::Kind::Unknown) ? (1.0f - it.best_sim) : it.best_sim;
if (rawScore < 0.0f) rawScore = 0.0f;
if (rawScore > 0.99f) rawScore = 0.99f;
d.score = rawScore;
d.score = it.best_sim;
d.bbox = it.bbox;
d.track_id = it.person_track_id >= 0 ? it.person_track_id : it.best_person_id;
dets.push_back(std::move(d));

View File

@ -1,139 +0,0 @@
#include "pose_assoc_node.h"
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "pose/pose_result.h"
#include "utils/logger.h"
namespace rk3588 {
namespace {
struct PoseAssocConfig {
float min_iou = 0.1f;
};
struct MatchCandidate {
float iou = 0.0f;
int pose_index = -1;
int det_index = -1;
};
bool ParseConfig(const SimpleJson& config, PoseAssocConfig& out, std::string& err) {
out.min_iou = config.ValueOr<float>("min_iou", 0.1f);
if (out.min_iou < 0.0f || out.min_iou > 1.0f) {
err = "min_iou must be in [0, 1]";
return false;
}
return true;
}
float IoU(const Rect& lhs, const Rect& rhs) {
const float x1 = std::max(lhs.x, rhs.x);
const float y1 = std::max(lhs.y, rhs.y);
const float x2 = std::min(lhs.x + lhs.w, rhs.x + rhs.w);
const float y2 = std::min(lhs.y + lhs.h, rhs.y + rhs.h);
const float iw = std::max(0.0f, x2 - x1);
const float ih = std::max(0.0f, y2 - y1);
const float inter = iw * ih;
const float area_l = std::max(0.0f, lhs.w) * std::max(0.0f, lhs.h);
const float area_r = std::max(0.0f, rhs.w) * std::max(0.0f, rhs.h);
const float uni = area_l + area_r - inter;
return uni <= 0.0f ? 0.0f : (inter / uni);
}
void PushToDownstream(const std::vector<std::shared_ptr<SpscQueue<FramePtr>>>& output_queues, const FramePtr& frame) {
for (const auto& q : output_queues) {
if (q) q->Push(frame);
}
}
} // namespace
struct PoseAssocNode::Impl {
PoseAssocConfig config;
std::string init_err;
};
PoseAssocNode::PoseAssocNode() : impl_(std::make_unique<Impl>()) {}
PoseAssocNode::~PoseAssocNode() = default;
std::string PoseAssocNode::Id() const {
return id_;
}
std::string PoseAssocNode::Type() const {
return "pose_assoc";
}
bool PoseAssocNode::Init(const SimpleJson& config, const NodeContext& ctx) {
id_ = config.ValueOr<std::string>("id", "pose_assoc");
if (!ParseConfig(config, impl_->config, impl_->init_err)) {
LogError("[pose_assoc] invalid config: " + impl_->init_err);
return false;
}
output_queues_ = ctx.output_queues;
return true;
}
bool PoseAssocNode::Start() {
return true;
}
void PoseAssocNode::Stop() {}
NodeStatus PoseAssocNode::Process(FramePtr frame) {
if (!frame) return NodeStatus::DROP;
if (frame->pose && frame->det) {
for (auto& pose : frame->pose->items) {
pose.track_id = -1;
}
std::vector<MatchCandidate> candidates;
candidates.reserve(frame->pose->items.size() * frame->det->items.size());
for (size_t pi = 0; pi < frame->pose->items.size(); ++pi) {
const Rect& pose_bbox = frame->pose->items[pi].bbox;
for (size_t di = 0; di < frame->det->items.size(); ++di) {
const auto& det = frame->det->items[di];
if (det.track_id < 0) continue;
const float iou = IoU(pose_bbox, det.bbox);
if (iou >= impl_->config.min_iou) {
candidates.push_back(MatchCandidate{iou, static_cast<int>(pi), static_cast<int>(di)});
}
}
}
std::sort(candidates.begin(), candidates.end(), [](const MatchCandidate& lhs, const MatchCandidate& rhs) {
if (lhs.iou != rhs.iou) return lhs.iou > rhs.iou;
if (lhs.pose_index != rhs.pose_index) return lhs.pose_index < rhs.pose_index;
return lhs.det_index < rhs.det_index;
});
std::vector<bool> used_pose(frame->pose->items.size(), false);
std::vector<bool> used_det(frame->det->items.size(), false);
for (const auto& candidate : candidates) {
if (used_pose[static_cast<size_t>(candidate.pose_index)] ||
used_det[static_cast<size_t>(candidate.det_index)]) {
continue;
}
frame->pose->items[static_cast<size_t>(candidate.pose_index)].track_id =
frame->det->items[static_cast<size_t>(candidate.det_index)].track_id;
used_pose[static_cast<size_t>(candidate.pose_index)] = true;
used_det[static_cast<size_t>(candidate.det_index)] = true;
}
}
PushToDownstream(output_queues_, frame);
return NodeStatus::OK;
}
#ifndef RK3588_TEST_BUILD
REGISTER_NODE(PoseAssocNode, "pose_assoc");
#endif
} // namespace rk3588

View File

@ -1,30 +0,0 @@
#pragma once
#include <memory>
#include <string>
#include <vector>
#include "node.h"
namespace rk3588 {
class PoseAssocNode final : public INode {
public:
PoseAssocNode();
~PoseAssocNode() override;
std::string Id() const override;
std::string Type() const override;
bool Init(const SimpleJson& config, const NodeContext& ctx) override;
bool Start() override;
void Stop() override;
NodeStatus Process(FramePtr frame) override;
private:
struct Impl;
std::unique_ptr<Impl> impl_;
std::string id_;
std::vector<std::shared_ptr<SpscQueue<FramePtr>>> output_queues_;
};
} // namespace rk3588

Binary file not shown.

View File

@ -165,8 +165,8 @@ if [ $BUILD_MEDIA -eq 1 ]; then
cmake --build "$BUILD_DIR" -j"$JOBS"
echo -e "${GREEN}${NC} Media Server 编译完成"
echo " 输出: $BUILD_DIR/safesight-edge-server"
ls -lh "$BUILD_DIR/safesight-edge-server"
echo " 输出: $BUILD_DIR/media-server"
ls -lh "$BUILD_DIR/media-server"
echo ""
fi
@ -190,11 +190,11 @@ if [ $BUILD_AGENT -eq 1 ]; then
# 编译 Agent
echo -e "${CYAN}[编译 Agent]${NC}"
go build -o safesight-edge-agent ./cmd/safesight-agent
go build -o rk3588-agent_linux_arm64 ./cmd/rk3588-agent
echo -e "${GREEN}${NC} Agent 编译完成"
echo " 输出: $AGENT_DIR/safesight-edge-agent"
ls -lh "$AGENT_DIR/safesight-edge-agent"
echo " 输出: $AGENT_DIR/rk3588-agent_linux_arm64"
ls -lh "$AGENT_DIR/rk3588-agent_linux_arm64"
fi
echo ""
fi

1206
scripts/d8_1.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
#
# 用法: sudo ./deploy.sh [install|upgrade|status|logs|clean-hls|uninstall]
# 首次安装 Agent 时传入后台管理统一主钥匙;后续部署默认沿用设备保存的主钥匙:
# sudo AGENT_TOKEN=<safesightd.json 中的 agent_token> ./deploy.sh install
# sudo AGENT_TOKEN=<managerd.json 中的 agent_token> ./deploy.sh install
set -e
@ -22,13 +22,13 @@ PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
BUILD_DIR="$PROJECT_DIR/build"
# 安装目录
INSTALL_DIR="/opt/safesight-edge-server"
AGENT_INSTALL_DIR="/opt/safesight-edge-agent"
INSTALL_DIR="/opt/rk3588-media-server"
AGENT_INSTALL_DIR="/opt/rk3588-agent"
SERVICE_DIR="/etc/systemd/system"
LOGROTATE_DIR="/etc/logrotate.d"
# 运行时数据目录(持久化数据)
RUNTIME_DIR="/var/lib/safesight-edge-server"
RUNTIME_DIR="/var/lib/rk3588-media-server"
HLS_DIR="$RUNTIME_DIR/hls"
LOGS_DIR="$RUNTIME_DIR/logs"
ALARMS_DIR="$RUNTIME_DIR/alarms"
@ -62,7 +62,7 @@ resolve_agent_token() {
echo -e "${GREEN}${NC} 使用设备已保存的后台管理统一主钥匙"
else
echo -e "${RED}错误: 未提供 AGENT_TOKEN且未找到已保存的主钥匙 $AGENT_TOKEN_FILE${NC}"
echo "首次安装必须使用sudo AGENT_TOKEN=<safesightd.json 中的 agent_token> ./scripts/deploy.sh install"
echo "首次安装必须使用sudo AGENT_TOKEN=<managerd.json 中的 agent_token> ./scripts/deploy.sh install"
echo "部署脚本不会本地生成 token。"
exit 1
fi
@ -125,8 +125,8 @@ install_logrotate() {
echo -e "${CYAN}[配置日志轮转]${NC}"
# Media Server 日志轮转
cat > "$LOGROTATE_DIR/safesight-edge-server" << 'EOF'
/var/lib/safesight-edge-server/logs/*.log {
cat > "$LOGROTATE_DIR/rk3588-media-server" << 'EOF'
/var/lib/rk3588-media-server/logs/*.log {
daily
rotate 7
compress
@ -142,8 +142,8 @@ install_logrotate() {
EOF
# HLS 分片清理保留最近7天
cat > "$LOGROTATE_DIR/safesight-edge-server-hls" << 'EOF'
/var/lib/safesight-edge-server/hls/*/index*.ts {
cat > "$LOGROTATE_DIR/rk3588-hls" << 'EOF'
/var/lib/rk3588-media-server/hls/*/index*.ts {
daily
rotate 1
maxage 7
@ -152,7 +152,7 @@ EOF
nocompress
}
/var/lib/safesight-edge-server/hls/*/index*.m3u8 {
/var/lib/rk3588-media-server/hls/*/index*.m3u8 {
daily
rotate 1
maxage 7
@ -162,8 +162,8 @@ EOF
}
EOF
chmod 644 "$LOGROTATE_DIR/safesight-edge-server"
chmod 644 "$LOGROTATE_DIR/safesight-edge-server-hls"
chmod 644 "$LOGROTATE_DIR/rk3588-media-server"
chmod 644 "$LOGROTATE_DIR/rk3588-hls"
echo -e "${GREEN}${NC} 日志轮转配置完成"
}
@ -177,7 +177,7 @@ install_hls_cleanup() {
# HLS 分片清理脚本 - 保留最近 N 天的分片
KEEP_DAYS=${1:-3}
HLS_DIR="/var/lib/safesight-edge-server/hls"
HLS_DIR="/var/lib/rk3588-media-server/hls"
if [ ! -d "$HLS_DIR" ]; then
echo "HLS 目录不存在: $HLS_DIR"
@ -196,20 +196,20 @@ EOF
chmod +x "$INSTALL_DIR/bin/cleanup-hls.sh"
# 添加到 crontab每天凌晨3点执行
CRON_FILE="/etc/cron.d/safesight-edge-server"
CRON_FILE="/etc/cron.d/rk3588-media-server"
cat > "$CRON_FILE" << 'EOF'
# RK3588 Media Server 定时任务
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# 每天凌晨3点清理HLS旧分片保留3天
0 3 * * * root /opt/safesight-edge-server/bin/cleanup-hls.sh 3 >> /var/lib/safesight-edge-server/logs/cleanup.log 2>&1
0 3 * * * root /opt/rk3588-media-server/bin/cleanup-hls.sh 3 >> /var/lib/rk3588-media-server/logs/cleanup.log 2>&1
# 每周日凌晨4点清理告警视频保留30天
0 4 * * 0 root find /var/lib/safesight-edge-server/clips -name "*.mp4" -type f -mtime +30 -delete 2>/dev/null
0 4 * * 0 root find /var/lib/rk3588-media-server/clips -name "*.mp4" -type f -mtime +30 -delete 2>/dev/null
# 每周日凌晨4点清理告警图片保留90天
30 4 * * 0 root find /var/lib/safesight-edge-server/alarms -name "*.jpg" -type f -mtime +90 -delete 2>/dev/null
30 4 * * 0 root find /var/lib/rk3588-media-server/alarms -name "*.jpg" -type f -mtime +90 -delete 2>/dev/null
EOF
chmod 644 "$CRON_FILE"
@ -223,15 +223,12 @@ EOF
# 安装 Media Server
cmd_install_media_server() {
echo -e "${BLUE}[1/5] 安装 Media Server...${NC}"
# 检查编译产物
SCRIPT_DIR_OFFLINE="$(cd "$(dirname "$0")" && pwd)"
if [ -f "$SCRIPT_DIR_OFFLINE/safesight-edge-server" ]; then
BUILD_DIR="$SCRIPT_DIR_OFFLINE"
PROJECT_DIR="$SCRIPT_DIR_OFFLINE"
echo -e "${GREEN}${NC} 离线包模式"
fi
if [ ! -f "$BUILD_DIR/safesight-edge-server" ]; then
echo -e "${RED}错误: 未找到编译好的 safesight-edge-server${NC}"
if [ ! -f "$BUILD_DIR/media-server" ]; then
echo -e "${RED}错误: 未找到编译好的 media-server${NC}"
echo "请先编译: cd $PROJECT_DIR && cmake --build build"
exit 1
fi
@ -243,13 +240,14 @@ cmd_install_media_server() {
mkdir -p "$INSTALL_DIR/models"
# 复制二进制文件
cp "$BUILD_DIR/safesight-edge-server" "$INSTALL_DIR/bin/"
chmod +x "$INSTALL_DIR/bin/safesight-edge-server"
cp "$BUILD_DIR/media-server" "$INSTALL_DIR/bin/"
chmod +x "$INSTALL_DIR/bin/media-server"
# 复制运维脚本
mkdir -p "$INSTALL_DIR/scripts"
if [ -f "$PROJECT_DIR/scripts/ops.sh" ]; then
cp "$PROJECT_DIR/scripts/ops.sh" "$INSTALL_DIR/scripts/"
chmod +x "$INSTALL_DIR/scripts/ops.sh"
echo -e "${GREEN}${NC} 已复制运维脚本: ops.sh"
fi
if [ -f "$PROJECT_DIR/scripts/monitor_hw.sh" ]; then
@ -284,22 +282,7 @@ cmd_install_media_server() {
cp "$BUILD_DIR/librk_shared_state.so" "$INSTALL_DIR/lib/"
echo -e "${GREEN}${NC} 已复制 librk_shared_state.so"
fi
# 安装系统硬件加速库(检查后按需安装)
if [ -d "$BUILD_DIR/lib_deps" ] && [ "$(ls -A "$BUILD_DIR/lib_deps" 2>/dev/null)" ]; then
for lib in "$BUILD_DIR"/lib_deps/*.so*; do
libname=$(basename "$lib")
target="/usr/lib/$libname"
if [ -f "$target" ]; then
echo "$libname 已存在,跳过"
else
cp "$lib" /usr/lib/ 2>/dev/null || true
echo " ✓ 安装 $libname"
fi
done
ldconfig 2>/dev/null || true
fi
# 复制 web 静态文件
if [ -d "$PROJECT_DIR/web" ]; then
cp -r "$PROJECT_DIR/web"/* "$INSTALL_DIR/web/" 2>/dev/null || true
@ -351,7 +334,7 @@ render_template_config() {
fi
rendered_at="$(date -Iseconds)"
out_file="/tmp/safesight-edge-server-${DEPLOY_CONFIG_ID}-${DEPLOY_CONFIG_VERSION}.json"
out_file="/tmp/rk3588-media-server-${DEPLOY_CONFIG_ID}-${DEPLOY_CONFIG_VERSION}.json"
for overlay in $DEPLOY_CONFIG_OVERLAYS; do
overlay_path="$PROJECT_DIR/$overlay"
@ -378,7 +361,7 @@ render_template_config() {
--rendered-at "$rendered_at" \
--out "$out_file"
cp "$out_file" "$INSTALL_DIR/etc/safesight-edge-server.json"
cp "$out_file" "$INSTALL_DIR/etc/media-server.json"
echo -e "${GREEN}${NC} 已渲染部署配置: $out_file"
return 0
}
@ -400,14 +383,6 @@ select_config() {
CONFIGS+=("$(basename "$f")")
fi
done
if [ ${#CONFIGS[@]} -eq 0 ]; then
echo -e "${YELLOW} 无可用的配置文件,使用空配置(后续通过管理端下发)${NC}"
cat > "$INSTALL_DIR/etc/safesight-edge-server.json" << 'EOF'
{"instances":[],"metadata":{"config_id":"default","config_version":"1.0"},"templates":{}}
EOF
return
fi
echo "可用的配置文件:"
for i in "${!CONFIGS[@]}"; do
@ -424,7 +399,7 @@ EOF
fi
SELECTED_CONFIG="$PROJECT_DIR/configs/${CONFIGS[$((choice-1))]}"
cp "$SELECTED_CONFIG" "$INSTALL_DIR/etc/safesight-edge-server.json"
cp "$SELECTED_CONFIG" "$INSTALL_DIR/etc/media-server.json"
echo -e "${GREEN}${NC} 使用配置: ${CONFIGS[$((choice-1))]}"
}
@ -438,10 +413,10 @@ import json
import os
import re
config_file = "/opt/safesight-edge-server/etc/safesight-edge-server.json"
install_dir = "/opt/safesight-edge-server"
runtime_dir = "/var/lib/safesight-edge-server"
project_dir = os.path.expanduser("~/apps/safesight-edge")
config_file = "/opt/rk3588-media-server/etc/media-server.json"
install_dir = "/opt/rk3588-media-server"
runtime_dir = "/var/lib/rk3588-media-server"
project_dir = os.path.expanduser("~/apps/OrangePi3588Media")
def fix_path(path):
"""修复配置文件中的路径"""
@ -545,32 +520,26 @@ cmd_install_agent() {
echo -e "${BLUE}[2/5] 安装 Agent...${NC}"
resolve_agent_token
mkdir -p "$AGENT_INSTALL_DIR"
mkdir -p "/var/lib/safesight-edge-agent"
# 查找 Agent 编译产物(优先使用预编译的 arm64 二进制)
AGENT_SOURCE=""
# 离线包模式优先:二进制在脚本同目录
if [ -f "$SCRIPT_DIR/safesight-edge-agent" ]; then
AGENT_SOURCE="$SCRIPT_DIR/safesight-edge-agent"
echo -e "${GREEN}${NC} 找到离线包 Agent"
elif [ -f "$PROJECT_DIR/agent/safesight-edge-agent_linux_arm64" ]; then
AGENT_SOURCE="$PROJECT_DIR/agent/safesight-edge-agent_linux_arm64"
echo -e "${GREEN}${NC} 找到预编译 Agent: safesight-edge-agent_linux_arm64"
elif [ -f "$PROJECT_DIR/agent/safesight-edge-agent" ]; then
AGENT_SOURCE="$PROJECT_DIR/agent/safesight-edge-agent"
echo -e "${GREEN}${NC} 找到编译好的 Agent: safesight-edge-agent"
elif [ -f "$PROJECT_DIR/agent/cmd/safesight-edge-agent/safesight-edge-agent" ]; then
AGENT_SOURCE="$PROJECT_DIR/agent/cmd/safesight-edge-agent/safesight-edge-agent"
echo -e "${GREEN}${NC} 找到源码目录 Agent: safesight-edge-agent"
if [ -f "$PROJECT_DIR/agent/rk3588-agent_linux_arm64" ]; then
AGENT_SOURCE="$PROJECT_DIR/agent/rk3588-agent_linux_arm64"
echo -e "${GREEN}${NC} 找到预编译 Agent: rk3588-agent_linux_arm64"
elif [ -f "$PROJECT_DIR/agent/cmd/rk3588-agent/rk3588-agent" ]; then
AGENT_SOURCE="$PROJECT_DIR/agent/cmd/rk3588-agent/rk3588-agent"
echo -e "${GREEN}${NC} 找到源码编译 Agent: rk3588-agent"
fi
if [ -z "$AGENT_SOURCE" ]; then
echo -e "${YELLOW}警告: 未找到编译好的 Agent跳过二进制安装${NC}"
else
cp "$AGENT_SOURCE" "$AGENT_INSTALL_DIR/safesight-edge-agent"
chmod +x "$AGENT_INSTALL_DIR/safesight-edge-agent"
echo -e "${YELLOW}警告: 未找到编译好的 Agent跳过安装${NC}"
return
fi
mkdir -p "$AGENT_INSTALL_DIR"
mkdir -p "/var/lib/rk3588-agent"
cp "$AGENT_SOURCE" "$AGENT_INSTALL_DIR/rk3588-agent"
chmod +x "$AGENT_INSTALL_DIR/rk3588-agent"
printf '%s\n' "$AGENT_TOKEN" > "$AGENT_TOKEN_FILE"
chmod 600 "$AGENT_TOKEN_FILE"
@ -584,11 +553,11 @@ cmd_install_agent() {
"discovery_enable": true,
"discovery_port": 35688,
"device_name": "rk3588_$(hostname)",
"device_id_path": "/var/lib/safesight-edge-agent/device_id",
"device_id_path": "/var/lib/rk3588-agent/device_id",
"models_dir": "$INSTALL_DIR/models",
"resources_dir": "$INSTALL_DIR/resources",
"max_upload_mb": 200,
"config_path": "$INSTALL_DIR/etc/safesight-edge-server.json",
"config_path": "$INSTALL_DIR/etc/media-server.json",
"media_server_process": {
"enable": false,
"configs_dir": "$INSTALL_DIR/etc"
@ -600,7 +569,7 @@ cmd_install_agent() {
}
EOF
chown -R orangepi:orangepi "/var/lib/safesight-edge-agent"
chown -R orangepi:orangepi "/var/lib/rk3588-agent"
echo -e "${GREEN}${NC} Agent 安装完成,已写入后台管理统一主钥匙"
}
@ -610,7 +579,7 @@ cmd_install_services() {
echo -e "${BLUE}[3/5] 安装 Systemd 服务...${NC}"
# Media Server 服务
cat > "$SERVICE_DIR/safesight-edge-server.service" << EOF
cat > "$SERVICE_DIR/media-server.service" << EOF
[Unit]
Description=RK3588 Media Server
After=network.target
@ -621,17 +590,17 @@ Type=simple
User=root
Group=root
WorkingDirectory=$INSTALL_DIR
Environment="LD_LIBRARY_PATH=$INSTALL_DIR/lib:$INSTALL_DIR/bin/plugins:/usr/lib:/usr/local/lib"
Environment="LD_LIBRARY_PATH=$INSTALL_DIR/lib:/usr/local/lib"
Environment="RKNPU_LOG_LEVEL=2"
ExecStartPre=/bin/bash -c 'mkdir -p $HLS_DIR $LOGS_DIR $ALARMS_DIR $CLIPS_DIR && chown orangepi:orangepi $LOGS_DIR $ALARMS_DIR $CLIPS_DIR && chmod 755 $LOGS_DIR $ALARMS_DIR $CLIPS_DIR'
ExecStart=$INSTALL_DIR/bin/safesight-edge-server --config $INSTALL_DIR/etc/safesight-edge-server.json
ExecStart=$INSTALL_DIR/bin/media-server --config $INSTALL_DIR/etc/media-server.json
Restart=always
RestartSec=5
StartLimitInterval=60s
StartLimitBurst=3
StandardOutput=journal
StandardError=journal
SyslogIdentifier=safesight-edge-server
SyslogIdentifier=media-server
# 资源限制
LimitNOFILE=65536
@ -646,23 +615,23 @@ WantedBy=multi-user.target
EOF
# Agent 服务
cat > "$SERVICE_DIR/safesight-edge-agent.service" << EOF
cat > "$SERVICE_DIR/rk3588-agent.service" << EOF
[Unit]
Description=RK3588 Agent Service
After=network.target safesight-edge-server.service
Wants=safesight-edge-server.service
After=network.target media-server.service
Wants=media-server.service
[Service]
Type=simple
User=root
Group=root
WorkingDirectory=$AGENT_INSTALL_DIR
ExecStart=$AGENT_INSTALL_DIR/safesight-edge-agent --config $AGENT_INSTALL_DIR/agent.config.json
ExecStart=$AGENT_INSTALL_DIR/rk3588-agent --config $AGENT_INSTALL_DIR/agent.config.json
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=safesight-edge-agent
SyslogIdentifier=rk3588-agent
[Install]
WantedBy=multi-user.target
@ -701,10 +670,10 @@ cmd_install() {
# 启动服务
echo -e "${BLUE}[启动服务]${NC}"
systemctl enable safesight-edge-server
systemctl start safesight-edge-server
systemctl enable safesight-edge-agent
systemctl start safesight-edge-agent
systemctl enable media-server
systemctl start media-server
systemctl enable rk3588-agent
systemctl start rk3588-agent
echo ""
echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}"
@ -714,11 +683,11 @@ cmd_install() {
echo "目录结构:"
echo " 程序: $INSTALL_DIR/"
echo " 数据: $RUNTIME_DIR/"
echo " 配置: $INSTALL_DIR/etc/safesight-edge-server.json"
echo " 配置: $INSTALL_DIR/etc/media-server.json"
echo ""
echo "服务管理:"
echo " 状态: sudo systemctl status safesight-edge-server"
echo " 日志: sudo journalctl -u safesight-edge-server -f"
echo " 状态: sudo systemctl status media-server"
echo " 日志: sudo journalctl -u media-server -f"
echo " 界面: http://$(hostname -I | awk '{print $1}'):9100"
echo ""
echo "HLS播放:"
@ -727,7 +696,7 @@ cmd_install() {
# 升级
cmd_upgrade() {
echo -e "${YELLOW}========== 升级 Edge Server ==========${NC}"
echo -e "${YELLOW}========== 升级 Media Server ==========${NC}"
if [ "$(id -u)" -ne 0 ]; then
echo -e "${RED}错误: 请使用 sudo 运行${NC}"
@ -735,34 +704,19 @@ cmd_upgrade() {
fi
# 备份配置
BACKUP_DIR="/root/safesight-edge-backup-$(date +%Y%m%d%H%M%S)"
BACKUP_DIR="/root/rk3588-backup-$(date +%Y%m%d%H%M%S)"
mkdir -p "$BACKUP_DIR"
[ -f "$INSTALL_DIR/etc/safesight-edge-server.json" ] && cp "$INSTALL_DIR/etc/safesight-edge-server.json" "$BACKUP_DIR/"
[ -f "$INSTALL_DIR/etc/media-server.json" ] && cp "$INSTALL_DIR/etc/media-server.json" "$BACKUP_DIR/"
echo "配置已备份到: $BACKUP_DIR"
# 停止服务
echo "停止服务..."
systemctl stop safesight-edge-server safesight-edge-agent 2>/dev/null || true
systemctl stop media-server rk3588-agent
# 更新 Edge Server 二进制
if [ -f "$BUILD_DIR/safesight-edge-server" ]; then
cp "$BUILD_DIR/safesight-edge-server" "$INSTALL_DIR/bin/"
chmod +x "$INSTALL_DIR/bin/safesight-edge-server"
echo -e "${GREEN}${NC} 已更新 Edge Server"
else
echo -e "${YELLOW}警告: 未找到编译好的 safesight-edge-server${NC}"
fi
# 重新安装
cmd_install
# 更新 Agent
cmd_install_agent
# 启动服务
systemctl start safesight-edge-server safesight-edge-agent
sleep 2
echo ""
echo -e "${GREEN}升级完成${NC}"
echo " Edge Server: $(systemctl is-active safesight-edge-server)"
echo " Agent: $(systemctl is-active safesight-edge-agent)"
}
# 查看状态
@ -787,9 +741,9 @@ cmd_status() {
# Media Server
echo -e "${CYAN}[Media Server]${NC}"
if systemctl is-active --quiet safesight-edge-server 2>/dev/null; then
if systemctl is-active --quiet media-server 2>/dev/null; then
echo -e " 状态: ${GREEN}● 运行中${NC}"
PID=$(systemctl show --property=MainPID --value safesight-edge-server 2>/dev/null)
PID=$(systemctl show --property=MainPID --value media-server 2>/dev/null)
echo " PID: $PID"
if [ -n "$PID" ] && [ "$PID" != "0" ]; then
CPU_MEM=$(ps -p $PID -o %cpu,%mem --no-headers 2>/dev/null || echo "N/A")
@ -825,7 +779,7 @@ cmd_status() {
# Agent
echo -e "${CYAN}[RK3588 Agent]${NC}"
if systemctl is-active --quiet safesight-edge-agent 2>/dev/null; then
if systemctl is-active --quiet rk3588-agent 2>/dev/null; then
echo -e " 状态: ${GREEN}● 运行中${NC}"
echo " 管理界面: http://$(hostname -I | awk '{print $1}'):9100"
else
@ -842,14 +796,14 @@ cmd_status() {
echo ""
echo "常用命令:"
echo " 查看日志: sudo journalctl -u safesight-edge-server -f"
echo " 查看日志: sudo journalctl -u media-server -f"
echo " HLS清理: sudo $INSTALL_DIR/bin/cleanup-hls.sh"
echo " 实时监控: watch -n 1 cat /proc/rknpu/load"
}
# 查看日志
cmd_logs() {
SERVICE="${2:-safesight-edge-server}"
SERVICE="${2:-media-server}"
echo -e "${BLUE}查看 $SERVICE 日志 (按 Ctrl+C 退出)...${NC}"
journalctl -u "$SERVICE" -f
}
@ -880,21 +834,21 @@ cmd_uninstall() {
[ "$REPLY" != "y" ] && [ "$REPLY" != "Y" ] && echo "已取消" && exit 0
echo "[1/4] 停止服务..."
systemctl stop safesight-edge-server safesight-edge-agent 2>/dev/null || true
systemctl disable safesight-edge-server safesight-edge-agent 2>/dev/null || true
systemctl stop media-server rk3588-agent 2>/dev/null || true
systemctl disable media-server rk3588-agent 2>/dev/null || true
echo "[2/4] 删除服务文件..."
rm -f "$SERVICE_DIR/safesight-edge-server.service"
rm -f "$SERVICE_DIR/safesight-edge-agent.service"
rm -f "$LOGROTATE_DIR/safesight-edge-server"
rm -f "$LOGROTATE_DIR/safesight-edge-server-hls"
rm -f "/etc/cron.d/safesight-edge-server"
rm -f "$SERVICE_DIR/media-server.service"
rm -f "$SERVICE_DIR/rk3588-agent.service"
rm -f "$LOGROTATE_DIR/rk3588-media-server"
rm -f "$LOGROTATE_DIR/rk3588-hls"
rm -f "/etc/cron.d/rk3588-media-server"
systemctl daemon-reload
echo "[3/4] 备份数据..."
BACKUP_DIR="/root/safesight-edge-backup-$(date +%Y%m%d%H%M%S)"
BACKUP_DIR="/root/rk3588-backup-$(date +%Y%m%d%H%M%S)"
mkdir -p "$BACKUP_DIR"
[ -f "$INSTALL_DIR/etc/safesight-edge-server.json" ] && cp "$INSTALL_DIR/etc/safesight-edge-server.json" "$BACKUP_DIR/"
[ -f "$INSTALL_DIR/etc/media-server.json" ] && cp "$INSTALL_DIR/etc/media-server.json" "$BACKUP_DIR/"
[ -d "$HLS_DIR" ] && cp -r "$HLS_DIR" "$BACKUP_DIR/" 2>/dev/null || true
echo "数据已备份到: $BACKUP_DIR"
@ -945,7 +899,7 @@ case "${1:-install}" in
echo " uninstall 卸载服务"
echo ""
echo "示例:"
echo " sudo AGENT_TOKEN=<safesightd.json 中的 agent_token> ./deploy.sh install"
echo " sudo AGENT_TOKEN=<managerd.json 中的 agent_token> ./deploy.sh install"
echo " sudo ./deploy.sh upgrade # 沿用设备保存的后台管理统一主钥匙"
echo " sudo ./deploy.sh status"
echo " sudo ./deploy.sh clean-hls 7 # 保留最近7天"

View File

@ -1,19 +0,0 @@
[Unit]
Description=RK3588 Agent Service
After=network.target safesight-edge-server.service
Wants=safesight-edge-server.service
[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/opt/safesight-edge-agent
ExecStart=/opt/safesight-edge-agent/safesight-edge-agent --config /opt/safesight-edge-agent/agent.config.json
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=safesight-edge-agent
[Install]
WantedBy=multi-user.target

View File

@ -1,32 +0,0 @@
[Unit]
Description=RK3588 Media Server
After=network.target
Wants=network.target
[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/opt/safesight-edge-server
Environment="LD_LIBRARY_PATH=/opt/safesight-edge-server/lib:/opt/safesight-edge-server/bin/plugins:/usr/local/lib"
Environment="RKNPU_LOG_LEVEL=2"
ExecStartPre=/bin/bash -c 'mkdir -p /var/lib/safesight-edge-server/hls /var/lib/safesight-edge-server/logs /var/lib/safesight-edge-server/alarms /var/lib/safesight-edge-server/clips && chown orangepi:orangepi /var/lib/safesight-edge-server/logs /var/lib/safesight-edge-server/alarms /var/lib/safesight-edge-server/clips && chmod 755 /var/lib/safesight-edge-server/logs /var/lib/safesight-edge-server/alarms /var/lib/safesight-edge-server/clips'
ExecStart=/opt/safesight-edge-server/bin/safesight-edge-server --config /opt/safesight-edge-server/etc/media-server.json
Restart=always
RestartSec=5
StartLimitInterval=60s
StartLimitBurst=3
StandardOutput=journal
StandardError=journal
SyslogIdentifier=safesight-edge-server
# 资源限制
LimitNOFILE=65536
LimitNPROC=4096
# 优雅关闭
TimeoutStopSec=30
KillSignal=SIGTERM
[Install]
WantedBy=multi-user.target

View File

@ -1,3 +0,0 @@
@echo off
chcp 65001 >nul
"C:\Users\Tellme\AppData\Local\Microsoft\WinGet\Packages\Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe\ffmpeg-8.0.1-full_build\bin\ffmpeg.exe" -re -stream_loop -1 -i C:\Users\Tellme\Pictures\人脸库\reg_008_unk_011_多人_正面_黑色鞋_白色鞋_1.mp4 -c copy -rtsp_transport tcp -f rtsp rtsp://10.0.0.49:8554/cam

View File

@ -291,5 +291,5 @@ echo " pkg-config --exists libcurl && echo 'libcurl: OK'"
echo " ffmpeg -version | head -1"
echo ""
echo "下一步:"
echo " cd ~/apps/safesight-edge/build"
echo " cd ~/apps/OrangePi3588Media/build"
echo " cmake .. && make -j\$(nproc)"

View File

@ -1,3 +0,0 @@
@echo off
cd /d "C:\Users\Tellme\apps\safesight\safesight-edge\scripts"
uv run --with flask mock_alarm_server.py

View File

@ -1,6 +1,6 @@
#!/bin/bash
# RK3588 Edge Server 运维脚本
# 安装后位于 /opt/safesight-edge-server/script/ops.sh
# RK3588 Media Server 运维脚本
# 安装后位于 /opt/rk3588-media-server/script/ops.sh
#
# 用法: sudo ./ops.sh [status|start|stop|restart|logs|hw|ddr-status|ddr-performance|ddr-ondemand|ddr-restore]
@ -14,11 +14,11 @@ CYAN='\033[0;36m'
NC='\033[0m'
# 服务名称
MEDIA_SERVICE="safesight-edge-server"
AGENT_SERVICE="safesight-edge-agent"
MEDIA_SERVICE="media-server"
AGENT_SERVICE="rk3588-agent"
cmd_status() {
echo -e "${BLUE}========== RK3588 Edge Server 状态 ==========${NC}"
echo -e "${BLUE}========== RK3588 Media Server 状态 ==========${NC}"
echo ""
# 系统信息
@ -36,8 +36,8 @@ cmd_status() {
fi
echo ""
# Edge Server
echo -e "${CYAN}[Edge Server]${NC}"
# Media Server
echo -e "${CYAN}[Media Server]${NC}"
if systemctl is-active --quiet $MEDIA_SERVICE 2>/dev/null; then
echo -e " 状态: ${GREEN}● 运行中${NC}"
PID=$(systemctl show --property=MainPID --value $MEDIA_SERVICE 2>/dev/null)
@ -54,7 +54,7 @@ cmd_status() {
echo ""
# Agent
echo -e "${CYAN}[Edge Agent]${NC}"
echo -e "${CYAN}[RK3588 Agent]${NC}"
if systemctl is-active --quiet $AGENT_SERVICE 2>/dev/null; then
echo -e " 状态: ${GREEN}● 运行中${NC}"
echo " 管理界面: http://$(hostname -I | awk '{print $1}'):9100"
@ -65,8 +65,8 @@ cmd_status() {
# HLS 输出
echo -e "${CYAN}[HLS 输出]${NC}"
if [ -d "/var/lib/safesight-edge-server/hls" ]; then
CHANNELS=$(find "/var/lib/safesight-edge-server/hls" -maxdepth 1 -type d 2>/dev/null | wc -l)
if [ -d "/var/lib/rk3588-media-server/hls" ]; then
CHANNELS=$(find "/var/lib/rk3588-media-server/hls" -maxdepth 1 -type d 2>/dev/null | wc -l)
CHANNELS=$((CHANNELS - 1))
echo " 通道数: $CHANNELS"
fi
@ -74,8 +74,8 @@ cmd_status() {
# 存储使用
echo -e "${CYAN}[存储使用]${NC}"
echo " HLS: $(du -sh /var/lib/safesight-edge-server/hls 2>/dev/null | cut -f1)"
echo " 日志: $(du -sh /var/lib/safesight-edge-server/logs 2>/dev/null | cut -f1)"
echo " HLS: $(du -sh /var/lib/rk3588-media-server/hls 2>/dev/null | cut -f1)"
echo " 日志: $(du -sh /var/lib/rk3588-media-server/logs 2>/dev/null | cut -f1)"
echo ""
echo "命令帮助:"
@ -115,23 +115,23 @@ cmd_logs() {
}
cmd_metrics() {
/opt/safesight-edge-server/scripts/monitor_hw.sh
/opt/rk3588-media-server/scripts/monitor_hw.sh
}
cmd_ddr_status() {
bash /opt/safesight-edge-server/scripts/ddr_mode.sh status
bash /opt/rk3588-media-server/scripts/ddr_mode.sh status
}
cmd_ddr_performance() {
bash /opt/safesight-edge-server/scripts/ddr_mode.sh performance
bash /opt/rk3588-media-server/scripts/ddr_mode.sh performance
}
cmd_ddr_ondemand() {
bash /opt/safesight-edge-server/scripts/ddr_mode.sh ondemand
bash /opt/rk3588-media-server/scripts/ddr_mode.sh ondemand
}
cmd_ddr_restore() {
bash /opt/safesight-edge-server/scripts/ddr_mode.sh restore
bash /opt/rk3588-media-server/scripts/ddr_mode.sh restore
}
# 主入口
@ -167,7 +167,7 @@ case "${1:-status}" in
cmd_ddr_restore
;;
*)
echo "RK3588 Edge Server 运维脚本"
echo "RK3588 Media Server 运维脚本"
echo ""
echo "用法: sudo $(basename $0) [命令]"
echo ""

View File

@ -1,98 +0,0 @@
#!/bin/bash
# 离线升级包构建脚本
# 编译所有组件并打包,可在无网络的生产设备上完整部署
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
PACKAGE_NAME="safesight-upgrade-$(date +%Y%m%d-%H%M%S)"
BUILD_DIR="/tmp/$PACKAGE_NAME"
echo "========== 构建离线升级包 =========="
echo ""
# 编译
echo "[1/3] 编译 Edge Server..."
cd "$PROJECT_DIR"
bash scripts/build.sh -m -j4
echo "[2/3] 编译 Agent..."
cd "$PROJECT_DIR/agent"
go build -o safesight-edge-agent ./cmd/safesight-agent
cd "$PROJECT_DIR"
# 打包
echo "[3/3] 打包..."
mkdir -p "$BUILD_DIR"
# 核心二进制
cp build/safesight-edge-server "$BUILD_DIR/"
cp agent/safesight-edge-agent "$BUILD_DIR/"
# 部署脚本和配置
cp scripts/deploy.sh "$BUILD_DIR/"
cp scripts/upgrade-from-old.sh "$BUILD_DIR/"
cp -r scripts/deploy/ "$BUILD_DIR/deploy/"
# 插件和库
if [ -d build/plugins ]; then
mkdir -p "$BUILD_DIR/plugins"
cp build/plugins/*.so "$BUILD_DIR/plugins/" 2>/dev/null || true
fi
find build/ -maxdepth 1 -name '*.so' -exec cp {} "$BUILD_DIR/" \; 2>/dev/null || true
# web 静态文件
if [ -d web ]; then
cp -r web "$BUILD_DIR/"
fi
# 模型文件
if [ -d models ]; then
mkdir -p "$BUILD_DIR/models"
cp models/*.rknn "$BUILD_DIR/models/" 2>/dev/null || true
fi
# 配置文件
if [ -d configs ]; then
mkdir -p "$BUILD_DIR/configs"
cp -r configs/templates "$BUILD_DIR/configs/" 2>/dev/null || true
cp -r configs/profiles "$BUILD_DIR/configs/" 2>/dev/null || true
cp -r configs/overlays "$BUILD_DIR/configs/" 2>/dev/null || true
fi
# 运维脚本
if [ -f scripts/ops.sh ]; then cp scripts/ops.sh "$BUILD_DIR/"; fi
if [ -f scripts/monitor_hw.sh ]; then cp scripts/monitor_hw.sh "$BUILD_DIR/"; fi
if [ -f scripts/ddr_mode.sh ]; then cp scripts/ddr_mode.sh "$BUILD_DIR/"; fi
if [ -f scripts/install_deps.sh ]; then cp scripts/install_deps.sh "$BUILD_DIR/"; fi
# 系统硬件加速库(从当前设备复制,确保兼容)
mkdir -p "$BUILD_DIR/lib_deps"
for pattern in '/usr/lib/librknnrt.so*' '/usr/lib/aarch64-linux-gnu/librga.so*' '/usr/lib/aarch64-linux-gnu/librockchip_mpp.so*' '/usr/lib/aarch64-linux-gnu/libavcodec.so*' '/usr/lib/aarch64-linux-gnu/libavformat.so*' '/usr/lib/aarch64-linux-gnu/libavutil.so*' '/usr/lib/aarch64-linux-gnu/libswscale.so*' '/usr/lib/aarch64-linux-gnu/libswresample.so*'; do
cp $pattern "$BUILD_DIR/lib_deps/" 2>/dev/null || true
done
echo "库文件: $(ls $BUILD_DIR/lib_deps/ | wc -l)"
# 创建说明文件
cat > "$BUILD_DIR/README.txt" << 'EOF'
离线升级包 - SafeSight Edge Server
====================================
新设备(从新系统安装):
sudo ./deploy.sh install
(需要 AGENT_TOKEN 参数)
已部署过 safesight-edge-server 的设备升级:
sudo ./deploy.sh upgrade
首次从旧版 rk3588-media-server/3588admin 升级:
sudo ./upgrade-from-old.sh
EOF
cd /tmp
tar -czf "$PROJECT_DIR/$PACKAGE_NAME.tar.gz" "$PACKAGE_NAME"
rm -rf "$BUILD_DIR"
echo ""
echo "✓ 打包完成: $PROJECT_DIR/$PACKAGE_NAME.tar.gz"
ls -lh "$PROJECT_DIR/$PACKAGE_NAME.tar.gz"

56
scripts/run_on_board.sh Normal file
View File

@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: run_on_board.sh --host <ip> --user <name> --build-dir <dir>
Copies build artifacts (pass the install directory for best results) to the RK3588 board and runs sanity commands.
EOF
}
HOST=""
USER="root"
BUILD_DIR="build/rk3588"
while [[ $# -gt 0 ]]; do
case "$1" in
--host)
HOST="$2"; shift 2;;
--user)
USER="$2"; shift 2;;
--build-dir)
BUILD_DIR="$2"; shift 2;;
-h|--help)
usage; exit 0;;
*)
echo "Unknown arg: $1" >&2
usage; exit 1;;
esac
done
if [[ -z "$HOST" ]]; then
echo "--host is required" >&2
exit 1
fi
ARTIFACT_DIR="$BUILD_DIR"
if [[ ! -d "$ARTIFACT_DIR" ]]; then
echo "Build directory $ARTIFACT_DIR not found" >&2
exit 1
fi
REMOTE_DIR="/tmp/rk3588_media_server"
scp -r "$ARTIFACT_DIR" "$USER@$HOST:$REMOTE_DIR"
ssh "$USER@$HOST" <<EOF
set -e
cd $REMOTE_DIR
echo "--- Running media-server --version ---"
./media-server --version || true
echo "--- Running mpp_decode_demo (dry-run) ---"
./mpp_decode_demo || true
echo "--- Running rknn_infer_demo (dry-run) ---"
./rknn_infer_demo || true
EOF

View File

@ -1,102 +0,0 @@
#!/bin/bash
# 旧设备升级脚本:从 OrangePi3588Media/rk3588-media-server 升级到 safesight-edge-server
# 使用离线包 + deploy.sh保留所有现有摄像头配置丢弃旧源码
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
echo -e "${YELLOW}========================================${NC}"
echo -e "${YELLOW} 旧设备升级到 safesight-edge-server${NC}"
echo -e "${YELLOW}========================================${NC}"
echo ""
if [ "$(id -u)" -ne 0 ]; then
echo -e "${RED}请使用 sudo 运行${NC}"
exit 1
fi
AGENT_TOKEN="${AGENT_TOKEN:-}"
if [ -z "$AGENT_TOKEN" ]; then
# Try to read from old agent config
if [ -f /opt/rk3588-agent/agent.config.json ]; then
AGENT_TOKEN=$(grep -o '"token":"[^"]*"' /opt/rk3588-agent/agent.config.json | cut -d'"' -f4)
echo -e "${GREEN}${NC} 从旧配置读取 agent token"
fi
fi
if [ -z "$AGENT_TOKEN" ]; then
echo -e "${RED}错误: 无法获取 AGENT_TOKEN${NC}"
echo "请设置: sudo AGENT_TOKEN=<token> $0"
exit 1
fi
# 确认
BACKUP_DIR="/root/safesight-upgrade-backup-$(date +%Y%m%d%H%M%S)"
mkdir -p "$BACKUP_DIR"
echo "此操作将:"
echo " 1. 备份配置到 $BACKUP_DIR"
echo " 2. 停止并移除旧服务"
echo " 3. 部署新版本(保留摄像头配置)"
echo " 4. 移除旧源码"
echo ""
read -p "继续? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "已取消"
exit 0
fi
# ---- 1. 备份配置 ----
echo -e "${GREEN}[1/5] 备份配置${NC}"
[ -f /opt/rk3588-media-server/etc/media-server.json ] && cp /opt/rk3588-media-server/etc/media-server.json "$BACKUP_DIR/" && echo " ✓ media-server 配置"
[ -f /opt/rk3588-agent/agent.config.json ] && cp /opt/rk3588-agent/agent.config.json "$BACKUP_DIR/" && echo " ✓ agent 配置"
# ---- 2. 停止并移除旧服务 ----
echo -e "${GREEN}[2/5] 停止旧服务${NC}"
systemctl stop rk3588-agent media-server 2>/dev/null || true
systemctl disable rk3588-agent media-server 2>/dev/null || true
rm -f /etc/systemd/system/rk3588-agent.service /etc/systemd/system/media-server.service
systemctl daemon-reload
echo " 旧服务已停止"
# ---- 3. 用 deploy.sh 安装新版本 ----
echo -e "${GREEN}[3/5] 安装新版本${NC}"
AGENT_TOKEN="$AGENT_TOKEN" bash "$SCRIPT_DIR/deploy.sh" install
# ---- 4. 保存旧配置参考 ----
echo -e "${GREEN}[4/5] 保存旧配置参考${NC}"
if [ -f "$BACKUP_DIR/media-server.json" ]; then
echo "旧配置中的摄像头 RTSP 地址:"
grep -o '"url":"[^"]*"' "$BACKUP_DIR/media-server.json" | sort -u | while read line; do
echo " $line"
done
echo ""
echo "请使用管理端部署向导,用以上地址重新配置摄像头。"
echo "完整配置已备份到: $BACKUP_DIR/media-server.json"
fi
# ---- 5. 清理旧源码 ----
echo -e "${GREEN}[5/5] 清理旧源码${NC}"
if [ -d ~/apps/OrangePi3588Media ]; then
rm -rf ~/apps/OrangePi3588Media
echo " ✓ 已删除 ~/apps/OrangePi3588Media"
fi
# Remove old installation directories
rm -rf /opt/rk3588-media-server /opt/rk3588-agent 2>/dev/null || true
rm -rf /var/lib/rk3588-media-server /var/lib/rk3588-agent 2>/dev/null || true
echo " ✓ 旧安装目录已清理"
echo ""
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN} 升级完成!${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo " 备份位置: $BACKUP_DIR"
echo " Edge Server: $(systemctl is-active safesight-edge-server)"
echo " Agent: $(systemctl is-active safesight-edge-agent)"
echo ""
echo " 验证: curl http://localhost:9100/v1/info"

View File

@ -2,17 +2,17 @@
#include <string>
#include <string_view>
#include "edge_server_app.h"
#include "media_server_app.h"
#include "version.h"
namespace {
void PrintUsage() {
std::cout << "Usage: safesight-media [--config <path>] [--version]\n";
std::cout << "Usage: media-server [--config <path>] [--version]\n";
}
void PrintVersion() {
std::cout << "SafeSight Media v" << rk3588::ProjectVersion()
std::cout << "RK3588 Media Server v" << rk3588::ProjectVersion()
<< " (git " << rk3588::GitSha() << ")\n";
}
@ -50,6 +50,6 @@ int main(int argc, char** argv) {
}
PrintVersion();
rk3588::EdgeServerApp app(config_path);
rk3588::MediaServerApp app(config_path);
return app.Start();
}

View File

@ -1,4 +1,4 @@
#include "edge_server_app.h"
#include "media_server_app.h"
#include <atomic>
#include <chrono>
@ -125,9 +125,9 @@ void InotifyWatchLoop(const std::string& path, std::atomic<bool>& stop_flag, Gra
if (matched) {
std::string err;
if (!mgr.ReloadFromFile(path, err)) {
LogWarn("[EdgeServerApp] hot reload failed: " + err);
LogWarn("[MediaServerApp] hot reload failed: " + err);
} else {
LogInfo("[EdgeServerApp] hot reload succeeded");
LogInfo("[MediaServerApp] hot reload succeeded");
}
// Debounce burst events.
std::this_thread::sleep_for(std::chrono::milliseconds(300));
@ -159,9 +159,9 @@ void PollWatchLoop(const std::string& path, std::atomic<bool>& stop_flag, GraphM
std::string err;
if (!mgr.ReloadFromFile(path, err)) {
LogWarn("[EdgeServerApp] hot reload failed: " + err);
LogWarn("[MediaServerApp] hot reload failed: " + err);
} else {
LogInfo("[EdgeServerApp] hot reload succeeded");
LogInfo("[MediaServerApp] hot reload succeeded");
}
#else
(void)path;
@ -172,14 +172,14 @@ void PollWatchLoop(const std::string& path, std::atomic<bool>& stop_flag, GraphM
} // namespace
EdgeServerApp::EdgeServerApp(std::string config_path)
MediaServerApp::MediaServerApp(std::string config_path)
: config_path_(std::move(config_path)), graph_manager_(ResolvePluginDir()) {}
int EdgeServerApp::Start() {
int MediaServerApp::Start() {
SimpleJson root_cfg;
std::string err;
if (!GraphManager::LoadConfigFile(config_path_, root_cfg, err)) {
LogError("[EdgeServerApp] Failed to load config: " + err);
LogError("[MediaServerApp] Failed to load config: " + err);
return 1;
}
@ -191,7 +191,7 @@ int EdgeServerApp::Start() {
}
if (!graph_manager_.BuildFromFile(config_path_, err)) {
LogError("[EdgeServerApp] Failed to build graphs: " + err);
LogError("[MediaServerApp] Failed to build graphs: " + err);
return 1;
}
@ -214,7 +214,7 @@ int EdgeServerApp::Start() {
const int sig = sigtimedwait(&set, nullptr, &ts);
if (sig == SIGINT || sig == SIGTERM) {
LogWarn("[EdgeServerApp] Caught signal, stopping...");
LogWarn("[MediaServerApp] Caught signal, stopping...");
graph_manager_.RequestStop();
return;
}
@ -229,7 +229,7 @@ int EdgeServerApp::Start() {
signal_thread_ = std::thread([this]() {
while (!stop_signal_thread_.load(std::memory_order_acquire)) {
if (g_stop_signal) {
LogWarn("[EdgeServerApp] Caught signal, stopping...");
LogWarn("[MediaServerApp] Caught signal, stopping...");
graph_manager_.RequestStop();
return;
}
@ -244,7 +244,7 @@ int EdgeServerApp::Start() {
};
if (!graph_manager_.StartAll()) {
LogError("[EdgeServerApp] Failed to start graphs");
LogError("[MediaServerApp] Failed to start graphs");
stop_signal();
return 1;
}
@ -252,7 +252,7 @@ int EdgeServerApp::Start() {
HttpServer http(graph_manager_, metrics_port, web_root);
(void)http.Start();
LogInfo("[EdgeServerApp] Running. Press Ctrl+C to stop.");
LogInfo("[MediaServerApp] Running. Press Ctrl+C to stop.");
std::atomic<bool> stop_watch{false};
std::thread watcher;
@ -266,7 +266,7 @@ int EdgeServerApp::Start() {
#else
watcher = std::thread([&] { PollWatchLoop(config_path_, stop_watch, graph_manager_); });
#endif
LogInfo("[EdgeServerApp] Hot reload enabled");
LogInfo("[MediaServerApp] Hot reload enabled");
}
graph_manager_.BlockUntilStop();
@ -277,7 +277,7 @@ int EdgeServerApp::Start() {
stop_watch.store(true);
if (watcher.joinable()) watcher.join();
LogInfo("[EdgeServerApp] Shutdown complete.");
LogInfo("[MediaServerApp] Shutdown complete.");
return 0;
}

View File

@ -44,8 +44,6 @@ add_executable(rk3588_gtests
test_person_shoe_roi.cpp
test_region_event.cpp
test_action_recog.cpp
test_ai_pose.cpp
test_pose_assoc.cpp
test_event_fusion.cpp
test_alarm_behavior_events.cpp
test_log_action.cpp
@ -65,8 +63,6 @@ add_executable(rk3588_gtests
${CMAKE_SOURCE_DIR}/src/utils/dma_alloc.cpp
${CMAKE_SOURCE_DIR}/plugins/region_event/region_event_node.cpp
${CMAKE_SOURCE_DIR}/plugins/action_recog/action_recog_node.cpp
${CMAKE_SOURCE_DIR}/plugins/ai_pose/ai_pose_node.cpp
${CMAKE_SOURCE_DIR}/plugins/pose_assoc/pose_assoc_node.cpp
${CMAKE_SOURCE_DIR}/plugins/event_fusion/event_fusion_node.cpp
${CMAKE_SOURCE_DIR}/plugins/logic_gate/color_analyzer.cpp
${CMAKE_SOURCE_DIR}/plugins/alarm/actions/log_action.cpp

View File

@ -5,7 +5,6 @@
#include "frame/frame.h"
#include "node.h"
#include "pose/pose_result.h"
#include "utils/simple_json.h"
#include "../plugins/action_recog/action_recog_node.h"
@ -125,385 +124,5 @@ TEST(ActionRecogTest, EmitsFightWhenTwoTracksStayCloseWithRepeatedMotion) {
EXPECT_EQ(frame2->behavior_events->items[0].track_ids[1], 32);
}
TEST(ActionRecogTest, EmitsFallFromPoseSequenceWhenBboxShapeChangeIsSmall) {
ActionRecogNode node;
const std::string config_text = R"({
"id": "action_evt",
"events": [
{
"type": "fall",
"window_ms": 1500,
"min_drop_pixels": 120,
"min_aspect_ratio_delta": 10.0,
"activate_duration_ms": 0,
"pose_min_torso_drop_pixels": 120,
"pose_max_upright_ratio": 0.6
}
]
})";
SimpleJson config = ParseActionConfig(config_text);
NodeContext ctx;
auto out = std::make_shared<SpscQueue<FramePtr>>(4, QueueDropStrategy::DropOldest);
ctx.output_queues.push_back(out);
ASSERT_TRUE(node.Init(config, ctx));
ASSERT_TRUE(node.Start());
auto frame1 = std::make_shared<Frame>();
frame1->width = 1920;
frame1->height = 1080;
frame1->pts = 1000;
frame1->det = std::make_shared<DetectionResult>();
frame1->det->img_w = 1920;
frame1->det->img_h = 1080;
frame1->det->items.push_back(Detection{0, 0.92f, Rect{800.0f, 220.0f, 180.0f, 300.0f}, 17});
frame1->pose = std::make_shared<PoseResult>();
frame1->pose->img_w = 1920;
frame1->pose->img_h = 1080;
PoseItem pose1;
pose1.bbox = Rect{800.0f, 220.0f, 180.0f, 300.0f};
pose1.score = 0.9f;
pose1.keypoints.resize(17);
pose1.keypoints[5] = PoseKeypoint{PosePoint2f{840.0f, 280.0f}, 0.9f};
pose1.keypoints[6] = PoseKeypoint{PosePoint2f{940.0f, 280.0f}, 0.9f};
pose1.keypoints[11] = PoseKeypoint{PosePoint2f{850.0f, 420.0f}, 0.9f};
pose1.keypoints[12] = PoseKeypoint{PosePoint2f{930.0f, 420.0f}, 0.9f};
frame1->pose->items.push_back(pose1);
auto frame2 = std::make_shared<Frame>();
frame2->width = 1920;
frame2->height = 1080;
frame2->pts = 1600;
frame2->det = std::make_shared<DetectionResult>();
frame2->det->img_w = 1920;
frame2->det->img_h = 1080;
frame2->det->items.push_back(Detection{0, 0.93f, Rect{780.0f, 420.0f, 200.0f, 280.0f}, 17});
frame2->pose = std::make_shared<PoseResult>();
frame2->pose->img_w = 1920;
frame2->pose->img_h = 1080;
PoseItem pose2;
pose2.bbox = Rect{780.0f, 420.0f, 200.0f, 280.0f};
pose2.score = 0.91f;
pose2.keypoints.resize(17);
pose2.keypoints[5] = PoseKeypoint{PosePoint2f{790.0f, 520.0f}, 0.9f};
pose2.keypoints[6] = PoseKeypoint{PosePoint2f{970.0f, 520.0f}, 0.9f};
pose2.keypoints[11] = PoseKeypoint{PosePoint2f{820.0f, 590.0f}, 0.9f};
pose2.keypoints[12] = PoseKeypoint{PosePoint2f{940.0f, 590.0f}, 0.9f};
frame2->pose->items.push_back(pose2);
EXPECT_EQ(static_cast<int>(node.Process(frame1)), static_cast<int>(NodeStatus::OK));
EXPECT_EQ(static_cast<int>(node.Process(frame2)), static_cast<int>(NodeStatus::OK));
ASSERT_NE(frame2->behavior_events, nullptr);
ASSERT_EQ(frame2->behavior_events->items.size(), 1u);
EXPECT_EQ(frame2->behavior_events->items[0].type, BehaviorEventType::Fall);
ASSERT_EQ(frame2->behavior_events->items[0].track_ids.size(), 1u);
EXPECT_EQ(frame2->behavior_events->items[0].track_ids[0], 17);
}
TEST(ActionRecogTest, EmitsFightFromPoseMotionWhenBboxMotionIsSmall) {
ActionRecogNode node;
const std::string config_text = R"({
"id": "action_evt",
"events": [
{
"type": "fight",
"window_ms": 1200,
"proximity_pixels": 220,
"min_motion_pixels": 1000,
"activate_duration_ms": 0,
"pose_min_wrist_motion_pixels": 120,
"pose_max_wrist_distance_pixels": 120
}
]
})";
SimpleJson config = ParseActionConfig(config_text);
NodeContext ctx;
auto out = std::make_shared<SpscQueue<FramePtr>>(4, QueueDropStrategy::DropOldest);
ctx.output_queues.push_back(out);
ASSERT_TRUE(node.Init(config, ctx));
ASSERT_TRUE(node.Start());
auto frame1 = std::make_shared<Frame>();
frame1->width = 1920;
frame1->height = 1080;
frame1->pts = 1000;
frame1->det = std::make_shared<DetectionResult>();
frame1->det->img_w = 1920;
frame1->det->img_h = 1080;
frame1->det->items.push_back(Detection{0, 0.90f, Rect{700.0f, 320.0f, 120.0f, 300.0f}, 31});
frame1->det->items.push_back(Detection{0, 0.89f, Rect{860.0f, 320.0f, 120.0f, 300.0f}, 32});
frame1->pose = std::make_shared<PoseResult>();
frame1->pose->img_w = 1920;
frame1->pose->img_h = 1080;
PoseItem left1;
left1.bbox = Rect{700.0f, 320.0f, 120.0f, 300.0f};
left1.keypoints.resize(17);
left1.keypoints[9] = PoseKeypoint{PosePoint2f{780.0f, 430.0f}, 0.9f};
left1.keypoints[10] = PoseKeypoint{PosePoint2f{790.0f, 450.0f}, 0.9f};
PoseItem right1;
right1.bbox = Rect{860.0f, 320.0f, 120.0f, 300.0f};
right1.keypoints.resize(17);
right1.keypoints[9] = PoseKeypoint{PosePoint2f{900.0f, 430.0f}, 0.9f};
right1.keypoints[10] = PoseKeypoint{PosePoint2f{910.0f, 450.0f}, 0.9f};
frame1->pose->items.push_back(left1);
frame1->pose->items.push_back(right1);
auto frame2 = std::make_shared<Frame>();
frame2->width = 1920;
frame2->height = 1080;
frame2->pts = 1300;
frame2->det = std::make_shared<DetectionResult>();
frame2->det->img_w = 1920;
frame2->det->img_h = 1080;
frame2->det->items.push_back(Detection{0, 0.91f, Rect{710.0f, 325.0f, 120.0f, 300.0f}, 31});
frame2->det->items.push_back(Detection{0, 0.92f, Rect{850.0f, 315.0f, 120.0f, 300.0f}, 32});
frame2->pose = std::make_shared<PoseResult>();
frame2->pose->img_w = 1920;
frame2->pose->img_h = 1080;
PoseItem left2;
left2.bbox = Rect{710.0f, 325.0f, 120.0f, 300.0f};
left2.keypoints.resize(17);
left2.keypoints[9] = PoseKeypoint{PosePoint2f{860.0f, 430.0f}, 0.9f};
left2.keypoints[10] = PoseKeypoint{PosePoint2f{870.0f, 450.0f}, 0.9f};
PoseItem right2;
right2.bbox = Rect{850.0f, 315.0f, 120.0f, 300.0f};
right2.keypoints.resize(17);
right2.keypoints[9] = PoseKeypoint{PosePoint2f{840.0f, 430.0f}, 0.9f};
right2.keypoints[10] = PoseKeypoint{PosePoint2f{830.0f, 450.0f}, 0.9f};
frame2->pose->items.push_back(left2);
frame2->pose->items.push_back(right2);
EXPECT_EQ(static_cast<int>(node.Process(frame1)), static_cast<int>(NodeStatus::OK));
EXPECT_EQ(static_cast<int>(node.Process(frame2)), static_cast<int>(NodeStatus::OK));
ASSERT_NE(frame2->behavior_events, nullptr);
ASSERT_EQ(frame2->behavior_events->items.size(), 1u);
EXPECT_EQ(frame2->behavior_events->items[0].type, BehaviorEventType::Fight);
ASSERT_EQ(frame2->behavior_events->items[0].track_ids.size(), 2u);
EXPECT_EQ(frame2->behavior_events->items[0].track_ids[0], 31);
EXPECT_EQ(frame2->behavior_events->items[0].track_ids[1], 32);
}
TEST(ActionRecogTest, SupportsStructuredFusionConfigForFall) {
ActionRecogNode node;
const std::string config_text = R"({
"id": "action_evt",
"events": [
{
"type": "fall",
"window_ms": 1500,
"activate_duration_ms": 0,
"bbox": {
"enabled": false
},
"pose": {
"enabled": true,
"min_torso_drop_pixels": 120,
"max_upright_ratio": 0.6
},
"fusion": {
"match_mode": "any"
}
}
]
})";
SimpleJson config = ParseActionConfig(config_text);
NodeContext ctx;
ASSERT_TRUE(node.Init(config, ctx));
ASSERT_TRUE(node.Start());
auto frame1 = std::make_shared<Frame>();
frame1->width = 1920;
frame1->height = 1080;
frame1->pts = 1000;
frame1->det = std::make_shared<DetectionResult>();
frame1->det->items.push_back(Detection{0, 0.92f, Rect{800.0f, 220.0f, 180.0f, 300.0f}, 17});
frame1->pose = std::make_shared<PoseResult>();
PoseItem pose1;
pose1.track_id = 17;
pose1.bbox = Rect{800.0f, 220.0f, 180.0f, 300.0f};
pose1.keypoints.resize(17);
pose1.keypoints[5] = PoseKeypoint{PosePoint2f{840.0f, 280.0f}, 0.9f};
pose1.keypoints[6] = PoseKeypoint{PosePoint2f{940.0f, 280.0f}, 0.9f};
pose1.keypoints[11] = PoseKeypoint{PosePoint2f{850.0f, 420.0f}, 0.9f};
pose1.keypoints[12] = PoseKeypoint{PosePoint2f{930.0f, 420.0f}, 0.9f};
frame1->pose->items.push_back(pose1);
auto frame2 = std::make_shared<Frame>();
frame2->width = 1920;
frame2->height = 1080;
frame2->pts = 1600;
frame2->det = std::make_shared<DetectionResult>();
frame2->det->items.push_back(Detection{0, 0.93f, Rect{790.0f, 400.0f, 180.0f, 300.0f}, 17});
frame2->pose = std::make_shared<PoseResult>();
PoseItem pose2;
pose2.track_id = 17;
pose2.bbox = Rect{790.0f, 400.0f, 180.0f, 300.0f};
pose2.keypoints.resize(17);
pose2.keypoints[5] = PoseKeypoint{PosePoint2f{790.0f, 520.0f}, 0.9f};
pose2.keypoints[6] = PoseKeypoint{PosePoint2f{970.0f, 520.0f}, 0.9f};
pose2.keypoints[11] = PoseKeypoint{PosePoint2f{820.0f, 590.0f}, 0.9f};
pose2.keypoints[12] = PoseKeypoint{PosePoint2f{940.0f, 590.0f}, 0.9f};
frame2->pose->items.push_back(pose2);
EXPECT_EQ(static_cast<int>(node.Process(frame1)), static_cast<int>(NodeStatus::OK));
EXPECT_EQ(static_cast<int>(node.Process(frame2)), static_cast<int>(NodeStatus::OK));
ASSERT_NE(frame2->behavior_events, nullptr);
ASSERT_EQ(frame2->behavior_events->items.size(), 1u);
EXPECT_EQ(frame2->behavior_events->items[0].type, BehaviorEventType::Fall);
}
TEST(ActionRecogTest, SuppressesFallFalsePositiveWhenPoseShapeConditionFails) {
ActionRecogNode node;
const std::string config_text = R"({
"id": "action_evt",
"events": [
{
"type": "fall",
"window_ms": 1500,
"activate_duration_ms": 0,
"bbox": {
"enabled": false
},
"pose": {
"enabled": true,
"min_torso_drop_pixels": 120,
"max_upright_ratio": 0.8
},
"fusion": {
"match_mode": "any"
}
}
]
})";
SimpleJson config = ParseActionConfig(config_text);
NodeContext ctx;
ASSERT_TRUE(node.Init(config, ctx));
ASSERT_TRUE(node.Start());
auto frame1 = std::make_shared<Frame>();
frame1->pts = 1000;
frame1->det = std::make_shared<DetectionResult>();
frame1->det->items.push_back(Detection{0, 0.92f, Rect{800.0f, 220.0f, 180.0f, 300.0f}, 17});
frame1->pose = std::make_shared<PoseResult>();
PoseItem pose1;
pose1.track_id = 17;
pose1.bbox = Rect{800.0f, 220.0f, 180.0f, 300.0f};
pose1.keypoints.resize(17);
pose1.keypoints[5] = PoseKeypoint{PosePoint2f{840.0f, 280.0f}, 0.9f};
pose1.keypoints[6] = PoseKeypoint{PosePoint2f{940.0f, 280.0f}, 0.9f};
pose1.keypoints[11] = PoseKeypoint{PosePoint2f{850.0f, 420.0f}, 0.9f};
pose1.keypoints[12] = PoseKeypoint{PosePoint2f{930.0f, 420.0f}, 0.9f};
frame1->pose->items.push_back(pose1);
auto frame2 = std::make_shared<Frame>();
frame2->pts = 1600;
frame2->det = std::make_shared<DetectionResult>();
frame2->det->items.push_back(Detection{0, 0.93f, Rect{790.0f, 400.0f, 180.0f, 300.0f}, 17});
frame2->pose = std::make_shared<PoseResult>();
PoseItem pose2;
pose2.track_id = 17;
pose2.bbox = Rect{790.0f, 400.0f, 180.0f, 300.0f};
pose2.keypoints.resize(17);
pose2.keypoints[5] = PoseKeypoint{PosePoint2f{860.0f, 520.0f}, 0.9f};
pose2.keypoints[6] = PoseKeypoint{PosePoint2f{900.0f, 520.0f}, 0.9f};
pose2.keypoints[11] = PoseKeypoint{PosePoint2f{865.0f, 700.0f}, 0.9f};
pose2.keypoints[12] = PoseKeypoint{PosePoint2f{905.0f, 700.0f}, 0.9f};
frame2->pose->items.push_back(pose2);
EXPECT_EQ(static_cast<int>(node.Process(frame1)), static_cast<int>(NodeStatus::OK));
EXPECT_EQ(static_cast<int>(node.Process(frame2)), static_cast<int>(NodeStatus::OK));
ASSERT_NE(frame2->behavior_events, nullptr);
EXPECT_TRUE(frame2->behavior_events->items.empty());
}
TEST(ActionRecogTest, SuppressesFightFalsePositiveWhenWristsStayFarApart) {
ActionRecogNode node;
const std::string config_text = R"({
"id": "action_evt",
"events": [
{
"type": "fight",
"window_ms": 1200,
"activate_duration_ms": 0,
"bbox": {
"enabled": false,
"proximity_pixels": 220,
"min_motion_pixels": 0
},
"pose": {
"enabled": true,
"min_wrist_motion_pixels": 120,
"max_wrist_distance_pixels": 60
},
"fusion": {
"match_mode": "any"
}
}
]
})";
SimpleJson config = ParseActionConfig(config_text);
NodeContext ctx;
ASSERT_TRUE(node.Init(config, ctx));
ASSERT_TRUE(node.Start());
auto frame1 = std::make_shared<Frame>();
frame1->pts = 1000;
frame1->det = std::make_shared<DetectionResult>();
frame1->det->items.push_back(Detection{0, 0.90f, Rect{700.0f, 320.0f, 120.0f, 300.0f}, 31});
frame1->det->items.push_back(Detection{0, 0.89f, Rect{860.0f, 320.0f, 120.0f, 300.0f}, 32});
frame1->pose = std::make_shared<PoseResult>();
PoseItem left1;
left1.track_id = 31;
left1.bbox = Rect{700.0f, 320.0f, 120.0f, 300.0f};
left1.keypoints.resize(17);
left1.keypoints[9] = PoseKeypoint{PosePoint2f{760.0f, 430.0f}, 0.9f};
left1.keypoints[10] = PoseKeypoint{PosePoint2f{770.0f, 450.0f}, 0.9f};
PoseItem right1;
right1.track_id = 32;
right1.bbox = Rect{860.0f, 320.0f, 120.0f, 300.0f};
right1.keypoints.resize(17);
right1.keypoints[9] = PoseKeypoint{PosePoint2f{960.0f, 430.0f}, 0.9f};
right1.keypoints[10] = PoseKeypoint{PosePoint2f{970.0f, 450.0f}, 0.9f};
frame1->pose->items.push_back(left1);
frame1->pose->items.push_back(right1);
auto frame2 = std::make_shared<Frame>();
frame2->pts = 1300;
frame2->det = std::make_shared<DetectionResult>();
frame2->det->items.push_back(Detection{0, 0.91f, Rect{710.0f, 325.0f, 120.0f, 300.0f}, 31});
frame2->det->items.push_back(Detection{0, 0.92f, Rect{850.0f, 315.0f, 120.0f, 300.0f}, 32});
frame2->pose = std::make_shared<PoseResult>();
PoseItem left2;
left2.track_id = 31;
left2.bbox = Rect{710.0f, 325.0f, 120.0f, 300.0f};
left2.keypoints.resize(17);
left2.keypoints[9] = PoseKeypoint{PosePoint2f{820.0f, 430.0f}, 0.9f};
left2.keypoints[10] = PoseKeypoint{PosePoint2f{830.0f, 450.0f}, 0.9f};
PoseItem right2;
right2.track_id = 32;
right2.bbox = Rect{850.0f, 315.0f, 120.0f, 300.0f};
right2.keypoints.resize(17);
right2.keypoints[9] = PoseKeypoint{PosePoint2f{980.0f, 430.0f}, 0.9f};
right2.keypoints[10] = PoseKeypoint{PosePoint2f{990.0f, 450.0f}, 0.9f};
frame2->pose->items.push_back(left2);
frame2->pose->items.push_back(right2);
EXPECT_EQ(static_cast<int>(node.Process(frame1)), static_cast<int>(NodeStatus::OK));
EXPECT_EQ(static_cast<int>(node.Process(frame2)), static_cast<int>(NodeStatus::OK));
ASSERT_NE(frame2->behavior_events, nullptr);
EXPECT_TRUE(frame2->behavior_events->items.empty());
}
} // namespace
} // namespace rk3588

View File

@ -1,232 +0,0 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <cstring>
#include <memory>
#include <string>
#include <vector>
#include "hw/i_infer_backend.h"
#include "node.h"
#include "pose/pose_result.h"
#include "utils/simple_json.h"
#include "../plugins/ai_pose/ai_pose_node.h"
namespace rk3588 {
namespace {
SimpleJson ParsePoseConfig(const std::string& text) {
SimpleJson config;
std::string err;
const bool ok = ParseSimpleJson(text, config, err);
EXPECT_TRUE(ok);
return config;
}
class FakeInferBackend final : public IInferBackend {
public:
ModelHandle LoadModel(const std::string& /*model_path*/, std::string& /*err*/) override {
return 1;
}
void UnloadModel(ModelHandle /*handle*/) override {}
bool GetModelInfo(ModelHandle /*handle*/, ModelInfo& info) const override {
info.input_width = 640;
info.input_height = 640;
info.input_channels = 3;
info.n_input = 1;
info.n_output = 4;
info.name = "fake_yolov8_pose";
return true;
}
InferResult Infer(ModelHandle /*handle*/, const InferInput& /*input*/) override {
InferResult result;
result.success = true;
result.outputs = outputs_;
return result;
}
AiScheduler::BorrowedInferResult InferBorrowed(ModelHandle /*handle*/, const InferInput& /*input*/) override {
AiScheduler::BorrowedInferResult result;
result.success = false;
result.error = "not used in unit test";
return result;
}
std::vector<InferOutput> outputs_;
};
InferOutput MakeFloatOutput(const std::vector<float>& values) {
InferOutput out;
out.size = values.size() * sizeof(float);
out.data.resize(out.size);
std::memcpy(out.data.data(), values.data(), out.size);
return out;
}
TEST(AiPoseNodeTest, RejectsEnabledConfigWithoutModelPath) {
AiPoseNode node;
SimpleJson config = ParsePoseConfig(R"({
"id": "pose0",
"enabled": true
})");
NodeContext ctx;
EXPECT_FALSE(node.Init(config, ctx));
}
TEST(AiPoseNodeTest, DisabledNodeStartsAndPassesFramesThrough) {
AiPoseNode node;
SimpleJson config = ParsePoseConfig(R"({
"id": "pose0",
"enabled": false
})");
NodeContext ctx;
auto out = std::make_shared<SpscQueue<FramePtr>>(4, QueueDropStrategy::DropOldest);
ctx.output_queues.push_back(out);
ASSERT_TRUE(node.Init(config, ctx));
ASSERT_TRUE(node.Start());
auto frame = std::make_shared<Frame>();
frame->width = 1280;
frame->height = 720;
EXPECT_EQ(static_cast<int>(node.Process(frame)), static_cast<int>(NodeStatus::OK));
FramePtr forwarded;
ASSERT_TRUE(out->TryPop(forwarded));
EXPECT_EQ(forwarded.get(), frame.get());
EXPECT_EQ(frame->pose, nullptr);
}
TEST(AiPoseNodeTest, EnabledNodeRequiresInferBackendAtStart) {
AiPoseNode node;
SimpleJson config = ParsePoseConfig(R"({
"id": "pose0",
"enabled": true,
"model_path": "models/yolov8n-pose.rknn"
})");
NodeContext ctx;
ASSERT_TRUE(node.Init(config, ctx));
EXPECT_FALSE(node.Start());
}
TEST(AiPoseNodeTest, DecodePoseOutputsToOriginalCoordinates) {
const int num_keypoints = 17;
const int total_points = 8400;
std::vector<float> head0(static_cast<size_t>(65 * 80 * 80), -10.0f);
std::vector<float> head1(static_cast<size_t>(65 * 40 * 40), -10.0f);
std::vector<float> head2(static_cast<size_t>(65 * 20 * 20), -10.0f);
std::vector<float> keypoints(static_cast<size_t>(num_keypoints * 3 * total_points), 0.0f);
const int cell_x = 10;
const int cell_y = 15;
const int grid_w = 80;
const int point_index = cell_y * grid_w + cell_x;
auto set_dfl_bin = [&](int channel_group, int bin) {
for (int i = 0; i < 16; ++i) {
head0[static_cast<size_t>((channel_group * 16 + i) * grid_w * 80 + point_index)] = (i == bin) ? 10.0f : -10.0f;
}
};
set_dfl_bin(0, 4);
set_dfl_bin(1, 6);
set_dfl_bin(2, 8);
set_dfl_bin(3, 10);
head0[static_cast<size_t>(64 * grid_w * 80 + point_index)] = 10.0f;
for (int k = 0; k < num_keypoints; ++k) {
keypoints[static_cast<size_t>(k * 3 * total_points + point_index)] = 120.0f + static_cast<float>(k);
keypoints[static_cast<size_t>(k * 3 * total_points + total_points + point_index)] = 140.0f + static_cast<float>(k * 2);
keypoints[static_cast<size_t>(k * 3 * total_points + 2 * total_points + point_index)] = 0.9f;
}
auto backend = std::make_shared<FakeInferBackend>();
backend->outputs_ = {
MakeFloatOutput(head0),
MakeFloatOutput(head1),
MakeFloatOutput(head2),
MakeFloatOutput(keypoints),
};
AiPoseNode node;
SimpleJson config = ParsePoseConfig(R"({
"id": "pose0",
"enabled": true,
"model_path": "models/yolov8n-pose.rknn",
"model_input_w": 640,
"model_input_h": 640,
"conf_thresh": 0.25,
"nms_thresh": 0.45
})");
NodeContext ctx;
ctx.infer_backend = backend;
auto out = std::make_shared<SpscQueue<FramePtr>>(4, QueueDropStrategy::DropOldest);
ctx.output_queues.push_back(out);
ASSERT_TRUE(node.Init(config, ctx));
ASSERT_TRUE(node.Start());
auto rgb = std::make_shared<std::vector<uint8_t>>(static_cast<size_t>(320 * 320 * 3), 127);
auto frame = std::make_shared<Frame>();
frame->width = 320;
frame->height = 320;
frame->format = PixelFormat::RGB;
frame->data = rgb->data();
frame->data_size = rgb->size();
frame->stride = 320 * 3;
frame->plane_count = 1;
frame->planes[0] = {frame->data, frame->stride, static_cast<int>(frame->data_size), 0};
frame->data_owner = rgb;
EXPECT_EQ(static_cast<int>(node.Process(frame)), static_cast<int>(NodeStatus::OK));
ASSERT_NE(frame->pose, nullptr);
ASSERT_EQ(frame->pose->items.size(), 1u);
const PoseItem& item = frame->pose->items[0];
EXPECT_NEAR(item.bbox.x, 26.0f, 1.0f);
EXPECT_NEAR(item.bbox.y, 38.0f, 1.0f);
EXPECT_NEAR(item.bbox.w, 48.0f, 1.0f);
EXPECT_NEAR(item.bbox.h, 64.0f, 1.0f);
ASSERT_EQ(item.keypoints.size(), static_cast<size_t>(num_keypoints));
EXPECT_NEAR(item.keypoints[0].point.x, 60.0f, 1.0f);
EXPECT_NEAR(item.keypoints[0].point.y, 70.0f, 1.0f);
EXPECT_NEAR(item.keypoints[16].point.x, 68.0f, 1.0f);
EXPECT_NEAR(item.keypoints[16].point.y, 86.0f, 1.0f);
FramePtr forwarded;
ASSERT_TRUE(out->TryPop(forwarded));
EXPECT_EQ(forwarded.get(), frame.get());
}
TEST(PoseResultTest, SupportsPerTrackKeypoints) {
PoseResult result;
result.img_w = 1920;
result.img_h = 1080;
result.model_name = "pose_model";
PoseItem item;
item.track_id = 42;
item.score = 0.93f;
item.bbox = Rect{100.0f, 200.0f, 300.0f, 400.0f};
item.keypoints.push_back(PoseKeypoint{PosePoint2f{120.0f, 220.0f}, 0.99f});
item.keypoints.push_back(PoseKeypoint{PosePoint2f{140.0f, 260.0f}, 0.95f});
result.items.push_back(item);
ASSERT_EQ(result.items.size(), 1u);
EXPECT_EQ(result.items[0].track_id, 42);
ASSERT_EQ(result.items[0].keypoints.size(), 2u);
EXPECT_FLOAT_EQ(result.items[0].keypoints[0].point.x, 120.0f);
EXPECT_FLOAT_EQ(result.items[0].keypoints[1].score, 0.95f);
}
} // namespace
} // namespace rk3588

View File

@ -1,117 +0,0 @@
#include <gtest/gtest.h>
#include <memory>
#include <string>
#include "frame/frame.h"
#include "node.h"
#include "pose/pose_result.h"
#include "utils/simple_json.h"
#include "../plugins/pose_assoc/pose_assoc_node.h"
namespace rk3588 {
namespace {
SimpleJson ParsePoseAssocConfig(const std::string& text) {
SimpleJson config;
std::string err;
const bool ok = ParseSimpleJson(text, config, err);
EXPECT_TRUE(ok);
return config;
}
TEST(PoseAssocNodeTest, AssociatesPoseItemsToTrackedDetections) {
PoseAssocNode node;
SimpleJson config = ParsePoseAssocConfig(R"({
"id": "pose_assoc0",
"min_iou": 0.1
})");
NodeContext ctx;
auto out = std::make_shared<SpscQueue<FramePtr>>(4, QueueDropStrategy::DropOldest);
ctx.output_queues.push_back(out);
ASSERT_TRUE(node.Init(config, ctx));
ASSERT_TRUE(node.Start());
auto frame = std::make_shared<Frame>();
frame->det = std::make_shared<DetectionResult>();
frame->det->items.push_back(Detection{0, 0.95f, Rect{100.0f, 100.0f, 120.0f, 240.0f}, 7});
frame->det->items.push_back(Detection{0, 0.94f, Rect{320.0f, 120.0f, 120.0f, 240.0f}, 8});
frame->pose = std::make_shared<PoseResult>();
PoseItem pose0;
pose0.bbox = Rect{110.0f, 110.0f, 110.0f, 220.0f};
PoseItem pose1;
pose1.bbox = Rect{330.0f, 130.0f, 100.0f, 210.0f};
frame->pose->items.push_back(pose0);
frame->pose->items.push_back(pose1);
EXPECT_EQ(static_cast<int>(node.Process(frame)), static_cast<int>(NodeStatus::OK));
ASSERT_EQ(frame->pose->items.size(), 2u);
EXPECT_EQ(frame->pose->items[0].track_id, 7);
EXPECT_EQ(frame->pose->items[1].track_id, 8);
FramePtr forwarded;
ASSERT_TRUE(out->TryPop(forwarded));
EXPECT_EQ(forwarded.get(), frame.get());
}
TEST(PoseAssocNodeTest, UsesOneToOneAssignmentForOverlappingCandidates) {
PoseAssocNode node;
SimpleJson config = ParsePoseAssocConfig(R"({
"id": "pose_assoc0",
"min_iou": 0.05
})");
NodeContext ctx;
ASSERT_TRUE(node.Init(config, ctx));
ASSERT_TRUE(node.Start());
auto frame = std::make_shared<Frame>();
frame->det = std::make_shared<DetectionResult>();
frame->det->items.push_back(Detection{0, 0.95f, Rect{100.0f, 100.0f, 150.0f, 250.0f}, 11});
frame->det->items.push_back(Detection{0, 0.94f, Rect{130.0f, 120.0f, 150.0f, 250.0f}, 12});
frame->pose = std::make_shared<PoseResult>();
PoseItem pose0;
pose0.bbox = Rect{105.0f, 110.0f, 145.0f, 245.0f};
PoseItem pose1;
pose1.bbox = Rect{132.0f, 125.0f, 148.0f, 245.0f};
frame->pose->items.push_back(pose0);
frame->pose->items.push_back(pose1);
EXPECT_EQ(static_cast<int>(node.Process(frame)), static_cast<int>(NodeStatus::OK));
EXPECT_NE(frame->pose->items[0].track_id, frame->pose->items[1].track_id);
EXPECT_TRUE(frame->pose->items[0].track_id == 11 || frame->pose->items[0].track_id == 12);
EXPECT_TRUE(frame->pose->items[1].track_id == 11 || frame->pose->items[1].track_id == 12);
}
TEST(PoseAssocNodeTest, PassesThroughWhenPoseOrDetectionMissing) {
PoseAssocNode node;
SimpleJson config = ParsePoseAssocConfig(R"({
"id": "pose_assoc0"
})");
NodeContext ctx;
ASSERT_TRUE(node.Init(config, ctx));
ASSERT_TRUE(node.Start());
auto no_pose = std::make_shared<Frame>();
no_pose->det = std::make_shared<DetectionResult>();
no_pose->det->items.push_back(Detection{0, 0.9f, Rect{0.0f, 0.0f, 10.0f, 10.0f}, 1});
EXPECT_EQ(static_cast<int>(node.Process(no_pose)), static_cast<int>(NodeStatus::OK));
EXPECT_EQ(no_pose->pose, nullptr);
auto no_det = std::make_shared<Frame>();
no_det->pose = std::make_shared<PoseResult>();
PoseItem pose;
pose.bbox = Rect{0.0f, 0.0f, 10.0f, 10.0f};
no_det->pose->items.push_back(pose);
EXPECT_EQ(static_cast<int>(node.Process(no_det)), static_cast<int>(NodeStatus::OK));
ASSERT_EQ(no_det->pose->items.size(), 1u);
EXPECT_EQ(no_det->pose->items[0].track_id, -1);
}
} // namespace
} // namespace rk3588

View File

@ -1,4 +1,4 @@
#Fri Jul 17 11:04:00 CST 2026
#Mon May 11 15:07:02 CST 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip