#创作灵感#

工程实践

引言

随着AI技术的不断发展,通过神经网络进行视频目标识别的方案也愈加成熟。通过视频检测目标和通过图像检测目标的不同点之一,就是在视频分析中,需要在前后关联的视频帧当中锁定同一个目标,并可以分析该目标的轨迹。而这一特性在车载目标识别中非常重要,可以据此结合卫星定位器记录该目标的经纬度,方便事后返回调查。例如道路监察机构可以在车辆行驶过程中检测道路上的坑洼地带,野外考察人员可以在行车时或者将该系统放置在某地收集特定动物信息等。

硬件拓扑结构

整个系统通过具备ONVIF协议的IP摄像头采集视频、车载目标识别器做现场分析、卫星定位模块提供时空定位。为方便现场调试与运维,设备预留双网口(Eth1 用于连接 IP 摄像机,Eth0 用于与管理电脑短连调试),并采用抗震的 M.2 存储保证在颠簸环境下的写入稳定性。

系统架构与管理

系统运行基于裁剪版 ARM64 Linux,软件栈由高效的golang代码与性能关键的C++模块混合实现,以兼顾开发效率和运行性能。内置轻量级 Web 服务器,支持客户端通过浏览器对设备进行配置(识别目标选择、网络参数设置、备份及固件升级等),并提供Windows端的设备发现软件,方便根据管理电脑IP地址更改设备地址。前端页面代码已上传至CSDN(在我的 CSDN 主页可查看),对嵌入式前端有兴趣的朋友可以下载了解。 系统整体由 任务调度层(Task Layer)、通信服务层(Service Layer)、识别逻辑层(Recognition Layer) 组成:

┌─────────────────────┐  

 │              HTTP 服务 (API)             │  

├─────────────────────┤  

 │           任务调度 / 协程管理           │  

├─────────────────────┤  

 │            视频分析与识别逻辑         │  

├─────────────────────┤  

 │        ARM64 Linux 系统            │  

└─────────────────────┘

在视频接入层,我们支持常见的 ONVIF 流媒体协议以及 H.264 / H.265 的主流封装(≤500 万像素),并以 H.264 格式输出处理结果以兼容通用回放与存储流程。

主程序框架代码如下,其中  IPCamRoutine() 是处理IP摄像头数据的核心协程 :

func main() {
    // 确保程序运行的唯一性:
    lockFilePath := "/tmp/TrackManager_1.0.2.lock"
    // 打开或创建锁文件
    lockFile, err := os.OpenFile(lockFilePath, os.O_CREATE|os.O_RDWR, 0666)
    if err != nil {
        fmt.Println("Create or own lockfile failed:", err)
        os.Exit(1)
    }
    defer lockFile.Close()

    // 尝试获取文件锁,LOCK_EX 表示排他锁,LOCK_NB 表示非阻塞
    err = syscall.Flock(int(lockFile.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
    if err != nil {
        fmt.Println("Another program is running, exiting...")
        os.Exit(1)
    }

    // 获取文件锁成功,程序继续执行
    fmt.Println("TrackManager is unique, program starting...")
	defer fmt.Println("This is last indicator")

    // 在程序退出前释放文件锁(Flock 默认会在文件关闭时释放锁)
    defer syscall.Flock(int(lockFile.Fd()), syscall.LOCK_UN)

   // 初始化系统配置信息
    err = initConfig()
    if err != nil {
        fmt.Println("Failed to get configuration:", err)
        // os.Exit(1)
    } else {
        restoreConfig()
    }
    
    // 检查存储介质信息
    err = EnsureDirExists(Default_StoragePath)
    if err != nil {
        fmt.Printf("Check Storage Path Error: %v\n", err)
    } else {
        mountDisk, err := getMountDevice(Default_StoragePath)
        if err != nil {
            fmt.Printf("Check Storage Disk Error: %v\n", err)
        } else {
            if mountDisk == "" {
                // 这时就不考虑nvme硬盘,直接尝试SD卡:
                mountDisk, err = tryMountSDCardForStorage()
                if err != nil {
                    fmt.Printf("Try SD Card Error: %v\n", err)
                } else {
                    g_storageDisk = mountDisk
                }
            } else {
                g_storageDisk = mountDisk
            }
        }
    }
    if g_storageDisk != "" {
        fmt.Println("Storage Disk: ", g_storageDisk)
        initTrackIndex(Default_StoragePath)
    }
    
	// 创建一个信号通道,用于接收系统信号
	sigChan := make(chan os.Signal, 1)
	// 捕捉系统信号,SIGINT 和 SIGTERM 都是常用的退出信号
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
	// 创建一个通道用于传递退出通知
	exitChan := make(chan struct{})

        // 通过ONVIF协议搜索并接收IP摄像机视频数据
        go IPCamRoutine(exitChan)

  	// 监听Windows端设备发现软件的命令
	go listenBroadcast()
    // 检查日期,保证存储结构准确
    go dateChecking()
    
    // 接收浏览器端的命令
    go func() {
        defer fmt.Println("Http Server Exit")
        http.HandleFunc("/api/", Interactive)
        log.Fatal(http.ListenAndServe(":8080", nil))
    }()

	// 等待系统信号
	<-sigChan
	fmt.Println("\nTrackManager Get Quit Signal, Do preparing")

	// 通知 Goroutine 退出
    enableCam(false)
    // fmt.Println("do StopTrack")
    C.StopTrack()

	close(exitChan) // 通知所有协程退出
	// 做一些准备工作
	// time.Sleep(1 * time.Second)
    g_wg.Wait()
	fmt.Println("\nTrackManager Exit")
}

调试与工程实践

在调试与开发阶段,我们重点解决了摄像头实时视频流在解码、智能识别与重新编码过程中对NPU/GPU资源的竞争问题。由于嵌入式芯片与内存资源有限,必须合理分配各类计算资源。此外,在车辆行驶过程中,场景变化剧烈,摄像机传回的视频数据量远大于普通静态监控场景,这些因素都直接关系到设备能否稳定运行。

我们发现,GPU在视频解码方面的性能显著优于编码性能,因此将视频编解码任务拆分至不同线程处理,并在其间设置一个先入先出(FIFO)队列作为缓冲。同时,在实时处理条件下,视频数据对解码线程的负载相对较低(编解码速度比约为1:6),因此将模型识别功能整合至解码线程中执行。

另外,由于摄像机的视频采集帧率会随场景变化(如不同光照条件、场景复杂度等)而动态波动,我们在程序中设计了帧率自适应调整机制,以应对不同情况下的处理需求。

以下是帧率计算的相关代码:

#define HIGHEST_DFS 14  // 每秒钟最高检测帧数
#define LOWEST_DFS  6   // 每秒钟最低检测帧数

g_fetched_frame_unit++;    // 编码的帧数统计
if (g_fetched_frame_unit >= 100)
{
       // 计算这一时段的平均帧率
	auto end = std::chrono::high_resolution_clock::now();
	g_fetched_fps = g_fetched_frame_unit * 1000.0 / std::chrono::duration_cast<std::chrono::milliseconds>(end - g_calc_start).count();

	g_WorkFlow.dfs = g_fetched_fps; 
	// 判断是否更改处理帧的间隔
	if (g_WorkFlow.dfs > HIGHEST_DFS) {
		if (g_WorkFlow.dtInterval < 5) {
			g_WorkFlow.dtInterval++;
		}
	}
	else if (g_WorkFlow.dfs < LOWEST_DFS) {
		if (g_WorkFlow.dtInterval > MIN_DTINTV) {
			g_WorkFlow.dtInterval--;
		}
	}

	g_fetched_frame_unit = 0;
	g_calc_start = std::chrono::high_resolution_clock::now();
}

结语与展望

车载目标识别作为移动数据采集的重要一环,其价值远超单点告警——当事件与地理信息结合后,便具备了用于巡检、科研与城市治理的长期价值。我们期待与有兴趣的朋友门展开更多合作,共同把这类解决方案推广到更多应用场景。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐