用Golang写个工具,把DM365的遮挡教程讲得明明白白(附代码思路)
- PG国际电子
- 2026-08-11 19:31:34
- 49
先别急着拍视频,咱们把DM365这个“小祖宗”摸清楚
最近后台好几个朋友私信问我,说手里的DM365(海思那款老芯片,做车载记录仪或者安防摄像头用的)录制视频时,总想加个动态遮挡效果——比如车牌打码、隐私区域模糊化,网上搜“dm365怎么遮挡视频教程”,出来的全是些操作步骤截图,要么是Windows专用软件,要么是剪辑软件里手动逐帧画蒙版,看得人脑壳疼。
我寻思着,既然咱们是写代码的,能不能用Golang直接调底层库,在视频编码前就把遮挡区域“抹掉”?后来翻遍海思SDK文档,发现DM365的ISP(图像信号处理器)模块其实支持OSD(On-Screen Display)叠加,理论上可以在硬件层面做遮挡,但官方给的demo全是C语言,接口封装得跟俄罗斯套娃似的。
今天咱们不纸上谈兵,我熬夜写了个Golang的封装思路,配上实际能跑的代码片段,带你绕过那些坑,文章可能有点长,但看完你至少能明白:遮挡视频的核心逻辑在哪儿,Golang怎么跟C库打交道,以及为啥我最后推荐“软遮挡”而不是硬折腾。
第一步:搞懂DM365的视频流管线,遮挡到底插在哪一环
DM365的处理流程大概是:CMOS传感器→ISP(调白平衡、降噪)→视频编码器(H.264)→输出文件或网络流。遮挡操作最理想的位置,是在ISP输出之后、编码器之前,这时候图像还是YUV格式的裸数据,逐像素修改成本最低。
但问题来了:海思SDK里的HI_MPI_VPSS_SetChnAttr接口,虽然支持在VPSS(视频预处理子系统)通道里叠加OSD,可它要求区域坐标必须是16像素对齐的,比如你想遮一个352×288的区域,实际硬件会帮你扩展到368×304,边缘会多出几像素的“毛边”。
Golang能干啥? 我们可以用cgo直接调用海思的mpi_vi.h和mpi_vpss.h,但说实话,C结构体转Go struct那部分能让人写到怀疑人生,所以我更推荐软拦截方案——用Golang写个代理服务,把DM365输出的RTSP流拉下来,在内存里做YUV像素覆盖,再重新推流出去。
第二步:Golang拉流+像素级遮挡的伪代码实战(关键部分)
先声明,我这套代码没在真实板子上跑过(毕竟手头没有DM365开发板,穷),但逻辑是通的,如果你手边有设备,替换成实际的IP地址和端口就行。
package main
import (
"fmt"
"image"
"image/color"
"log"
"os"
"github.com/grafov/m3u8"
"github.com/bluenviron/gortsplib/v4"
"github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format"
)
// 遮挡区域结构体,比如要遮住画面左上角 (0,0) 到 (320,240) 那块
type MaskRegion struct {
X, Y, Width, Height int
Color color.RGBA // 遮挡颜色,默认黑色
}
func main() {
// 连接DM365的RTSP流(假设地址是 192.168.1.100:554)
conn, err := gortsplib.Dial("rtsp://192.168.1.100:554/stream1")
if err != nil {
log.Fatalf("连接失败: %v", err)
}
defer conn.Close()
// 解析流描述信息,取到H264格式
desc, err := conn.Describe()
if err != nil {
log.Fatal(err)
}
var h264 *format.H264
for _, media := range desc.Medias {
if media.Type == description.MediaTypeVideo {
h264 = media.Formats[0].(*format.H264)
}
}
// 开始接收RTP包,用回调处理每帧数据
_, err = conn.SetupAndPlay(desc.BaseURL, desc.Medias[0])
if err != nil {
log.Fatal(err)
}
// 核心:RTP包里的H264是压缩码流,不能直接改像素
// 需要先解码成RGB或YUV,遮挡完再编码回H264
// 这里为了演示,只打印出帧数,真实场景需要引入ffmpeg的Golang绑定(如 goav)
for {
pkt, _, err := conn.ReadPacket()
if err != nil {
log.Printf("读取失败: %v", err)
break
}
// pkt为RTP包,payload里面是H264的NAL单元
// 遍历包,如果遇到IDR帧,就做一次遮挡处理(实际应该用解码后的帧)
fmt.Printf("收到RTP包,尺寸: %d bytes\n", len(pkt.Payload))
// 这里省略解码、遮挡、重编码的细节,因为代码量巨大
}
}
看明白了吧?最麻烦的其实是“解码-改像素-重编码”这三步,Golang生态里能直接调ffmpeg的库有goav、gocv,但都不是官方维护,兼容性随缘,所以我又想了个歪招——直接用海思自己的编码器。
第三步:正经姿势——用海思MPP的Golang绑定(cgo真香)
海思MPP(Media Process Platform)有一组C接口,可以往VPSS通道的OSD(字幕叠加)区域写数据,虽然官方文档说OSD是用来显示时间戳或字符的,但你完全可以把一个不透明的色块当成“伪OSD”塞进去。
关键api是HI_MPI_RGN_Create(创建区域)和HI_MPI_RGN_AttachToChn(挂载到通道),Golang里这么写:
/*
#cgo LDFLAGS: -lhi_mpi -lhi_common
#include <stdio.h>
#include <stdlib.h>
#include <hi_mpi.h>
#include <hi_comm_video.h>
*/
import "C"
import (
"fmt"
"unsafe"
)
func CreateMaskOverlay(channelID int, x, y, w, h int) error {
// 初始化区域句柄
var rgnHandle C.HI_HANDLE
var rgnAttr C.RGN_ATTR_S
// 设置区域类型为覆盖(COVER),不是字符OSD
rgnAttr.enType = C.OVERLAY_RGN
rgnAttr.unAttr.stOverlayChn.enPixelFormat = C.HI_PIXEL_FORMAT_RGB_8888
rgnAttr.unAttr.stOverlayChn.stSize.u32Width = C.HI_U32(w)
rgnAttr.unAttr.stOverlayChn.stSize.u32Height = C.HI_U32(h)
rgnAttr.unAttr.stOverlayChn.u32CanvasNum = 1
rgnAttr.unAttr.stOverlayChn.stPos.u32X = C.HI_U32(x)
rgnAttr.unAttr.stOverlayChn.stPos.u32Y = C.HI_U32(y)
rgnAttr.unAttr.stOverlayChn.u32BgColor = 0x000000 // 黑色不透明
ret := C.HI_MPI_RGN_Create(&rgnHandle, &rgnAttr)
if ret != 0 {
return fmt.Errorf("创建ROI区域失败: %d", int(ret))
}
// 挂载到VPSS通道上(通道0一般用于编码)
var chnAttr C.MPP_CHN_S
chnAttr.enModId = C.HI_ID_VPSS
chnAttr.s32DevId = C.HI_S32(0) // VPSS组号
chnAttr.s32ChnId = C.HI_S32(channelID)
ret = C.HI_MPI_RGN_AttachToChn(rgnHandle, &chnAttr)
if ret != 0 {
return fmt.Errorf("挂载区域失败: %d", int(ret))
}
return nil
}
这个方案的优点是遮挡发生在硬件层,完全不消耗CPU,编码出来的视频流里就已经带黑色色块了,缺点也很明显:你得有海思交叉编译工具链,而且cgo在ARM Linux上编译容易炸——比如HI_U32是unsigned int的别名,Go里得手动转成uint32。
第四步:如果你不想碰cgo,用外部进程(ffmpeg)打配合
别在一棵树上吊死,我后来想了个更“Go”的解法:用Golang的os/exec调起板子上的ffmpeg命令,用filter滤镜画遮挡框,DM365虽老,但刷个OpenWrt或Debian,跑个静态编译的ffmpeg还是可以的。
比如你的DM365摄像头输出RTSP流,想局部遮挡车牌:
ffmpeg -i rtsp://localhost:8554/stream \ -vf "drawbox=x=100:y=80:w=320:h=180:color=black:t=fill" \ -c:v libx264 -preset ultrafast -f rtsp rtsp://localhost:5555/output
Golang里封装这个命令:
func StartFFmpegMask(inputURL, outputURL string, boxX, boxY, boxW, boxH int) (*exec.Cmd, error) {
vf := fmt.Sprintf("drawbox=x=%d:y=%d:w=%d:h=%d:color=black:t=fill",
boxX, boxY, boxW, boxH)
cmd := exec.Command("ffmpeg",
"-i", inputURL,
"-vf", vf,
"-c:v", "libx264",
"-preset", "ultrafast",
"-f", "rtsp", outputURL)
err := cmd.Start()
if err != nil {
return nil, err
}
return cmd, nil
}
这方法最省事,而且遮挡框的位置可以动态调整——比如你写个Goroutine,每100毫秒读一次GPS坐标,然后更新vf参数里的x和y,就能实现“跟拍遮挡”,类似人像追踪打码,缺点就是编码延迟高了几毫秒,但一般场景看不出来。
第五步:遮挡区域动态变化?用goroutine轮询坐标
比如你的DM365装在一个旋转云台上,想遮挡画面里某个动态目标(比如行人脸),这时候可以用一个全局变量currentMask存储最新的遮挡框坐标,Goroutine定期从某处(比如人脸识别算法接口)拉数据:
var mu sync.RWMutex
var currentMask = &MaskRegion{X: 0, Y: 0, W: 100, H: 100}
func UpdateMaskFromAPI() {
for {
// 假装从HTTP接口拿到人脸框坐标
resp, err := http.Get("http://localhost:8080/face/info")
if err == nil {
var faceInfo struct {
X, Y, W, H int `json:"x" json:"y" json:"w" json:"h"`
}
json.NewDecoder(resp.Body).Decode(&faceInfo)
mu.Lock()
currentMask = &MaskRegion{
X: faceInfo.X, Y: faceInfo.Y,
Width: faceInfo.W, Height: faceInfo.H,
}
mu.Unlock()
}
time.Sleep(100 * time.Millisecond)
}
}
然后再启动ffmpeg时,用-vf的drawbox表达式支持动态变量——其实ffmpeg的drawbox支持t= fill并且坐标参数可以写表达式,比如x='if(lte(1,1),100,200)',但为了简单,我建议每100毫秒重启一次ffmpeg子进程,虽然粗暴,但胜在稳定。
代价就是偶尔会闪一下黑框,但总比手动逐帧遮罩强多了。
踩坑日记:你需要知道的三个“老狐狸”问题
-
YUV色彩空间对齐:DM365的ISP输出的YUV是4:2:2,而大多数Go图像库处理的是RGBA,转换时注意不要用标准转换公式,否则颜色会偏绿,如果非要软遮挡,建议在YUV平面直接改像素值,把Y分量设为16(黑色对应的亮度值),U/V分量设为128,这样既省CPU又不会变色。
-
海思MPP的内存映射:如果走cgo路线,创建RGN后要调用
HI_MPI_RGN_SetBitMap写入像素数据,但这个函数会阻塞,直到VPSS通道输出完一帧,所以得开个独立Goroutine专门管理遮挡区域更新,不能直接在编码回调里干这事。 -
RTSP源地址的鉴权:DM365有些固件会带onvif协议,用户名密码在URL里拼,用Golang的
gortsplib时,如果没正确设置Auth字段,会一直报401,调试时用wireshark抓包看看有没有WWW-Authenticate头,别死磕代码。
最后的实战案例:车牌动态模糊(用ffmpeg的boxblur)
比drawbox黑块更高级的遮挡,是模糊化,比如只模糊车牌区域,可以用ffmpeg的boxblur滤镜加crop组合:
vf := fmt.Sprintf("crop=w=%d:h=%d:x=%d:y=%d,boxblur=luma_radius=10:luma_power=1,overlay=x=%d:y=%d",
boxW, boxH, boxX, boxY,
boxW, boxH, boxX, boxY)
原理:先裁剪出车牌区域,模糊处理,再覆盖回原图对应位置,Golang里生成这个滤镜字符串,跟在exec.Command里就行,这种方式比硬色块看起来自然得多,适合演示或Vlog类场景。
尾声:别把视频遮挡想成高深技术
其实我写这篇东西的初衷,是看到网上那些教程动不动就让你装Adobe Premiere,或者用达芬奇逐帧跟踪,对于DM365这种嵌入式设备,只要思路对了,用Golang调几个API就能解决,我提供的代码不能直接拿去生产,但至少让你知道该往哪个方向查文档。
如果你真在项目里用到了类似方法,欢迎回来告诉我踩了什么坑——毕竟我这篇文章的代码也就值一杯咖啡的时间写的,真正的调试还得靠你自己跟海思的驱动干架,祝你好运,别被MPP的API文档绕晕。
