Compare commits
No commits in common. "master" and "p0-dashboard" have entirely different histories.
master
...
p0-dashboa
1
.gitignore
vendored
1
.gitignore
vendored
@ -51,4 +51,3 @@ __pycache__/
|
||||
*.swo
|
||||
.DS_Store
|
||||
/.worktrees/
|
||||
agent/safesight-edge-agent
|
||||
|
||||
@ -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)
|
||||
|
||||
51
Readme.md
51
Readme.md
@ -1,5 +1,5 @@
|
||||
|
||||
# SafeSight 智能视觉安全监测平台 PRD(v1.2)
|
||||
# RK3588 智能视频分析系统 PRD(v1.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 Manager(GraphMgr)**:
|
||||
- 对应多个 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`.
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"
|
||||
@ -1,3 +1,3 @@
|
||||
module safesight-agent
|
||||
module rk3588sys/agent
|
||||
|
||||
go 1.21
|
||||
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"safesight-agent/internal/files"
|
||||
"rk3588sys/agent/internal/files"
|
||||
)
|
||||
|
||||
type Entry struct {
|
||||
|
||||
@ -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},
|
||||
|
||||
@ -11,7 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"safesight-agent/internal/sysinfo"
|
||||
"rk3588sys/agent/internal/sysinfo"
|
||||
)
|
||||
|
||||
const Magic = "RK3588SYS_DISCOVERY_V1"
|
||||
|
||||
@ -15,7 +15,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"safesight-agent/internal/files"
|
||||
"rk3588sys/agent/internal/files"
|
||||
)
|
||||
|
||||
type agentBinaryUpdateResult struct {
|
||||
|
||||
@ -14,7 +14,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"safesight-agent/internal/files"
|
||||
"rk3588sys/agent/internal/files"
|
||||
)
|
||||
|
||||
func (s *Server) alarmsPath() string {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -11,7 +11,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"safesight-agent/internal/config"
|
||||
"rk3588sys/agent/internal/config"
|
||||
)
|
||||
|
||||
func TestHandleConfigCandidateStoresValidatedCandidate(t *testing.T) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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})
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"safesight-agent/internal/modelstore"
|
||||
"rk3588sys/agent/internal/modelstore"
|
||||
)
|
||||
|
||||
func TestHandleModelsStatusReturnsInstalledModels(t *testing.T) {
|
||||
|
||||
@ -10,7 +10,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"safesight-agent/internal/files"
|
||||
"rk3588sys/agent/internal/files"
|
||||
)
|
||||
|
||||
type installedResource struct {
|
||||
|
||||
@ -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})
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"safesight-agent/internal/files"
|
||||
"rk3588sys/agent/internal/files"
|
||||
)
|
||||
|
||||
type Item struct {
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -9,7 +9,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"safesight-agent/internal/log"
|
||||
"rk3588sys/agent/internal/log"
|
||||
)
|
||||
|
||||
func isAlive(pid int) (bool, error) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -14,7 +14,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"safesight-agent/internal/files"
|
||||
"rk3588sys/agent/internal/files"
|
||||
)
|
||||
|
||||
func Hostname() string {
|
||||
|
||||
Binary file not shown.
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"],
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
- 标准单路全流程测试:
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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:
|
||||
@ -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
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
BIN
scripts/__pycache__/onnx2rknn_prenorm.cpython-311.pyc
Normal file
BIN
scripts/__pycache__/onnx2rknn_prenorm.cpython-311.pyc
Normal file
Binary file not shown.
@ -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
1206
scripts/d8_1.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -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天"
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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)"
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
cd /d "C:\Users\Tellme\apps\safesight\safesight-edge\scripts"
|
||||
uv run --with flask mock_alarm_server.py
|
||||
@ -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 ""
|
||||
|
||||
@ -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
56
scripts/run_on_board.sh
Normal 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
|
||||
@ -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"
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user