4G
5G工作原理详解(解释&图解)_5g通信原理-CSDN博客
整体视频流转播链路 & 各模块作用详解
结合你硬件环境、软件版本、实际操作流程,完整梳理链路、组件功能、配置逻辑,全程对应你实操场景:
硬件拓扑:HI3516 摄像机 → IMX6ULL 开发板 → 阿里云 Ubuntu 云服务器 (MediaMTX) → 本地 PC (VLC 播放器)
核心协议:RTSP + RTP + UDP + Docker + GStreamer
imx路由表:
1. 访问摄像机(局域网网段路由),Destination改为192.168.1.0,这个网段的流量都走eth0.
route add -net 192.168.1.0 netmask 255.255.255.0 dev eth0
2. 访问公网/阿里云(默认网段)10.104.25.1是4G网卡ip,是用来上外网的ip。此时Destination为
0.0.0.0,就是说默认流量都要走eth9.
route add default gw 10.104.25.1 dev eth9
一、完整数据流走向(从采集到播放)
HI3516(IPC) RTSP流(192.168.1.100:554/test)
↓
IMX6ULL开发板(GStreamer 拉流+转封包+UDP推流)
↓ UDP 5004端口 RTP-H264裸流
阿里云服务器(Docker+MediaMTX 接收RTP/转RTSP服务)
↓ RTSP 8554端口
本地PC(VLC 网络串流播放 rtsp://8.160.122.24:8554/cam)
二、逐环节拆解:设备、软件、命令、作用、问题复盘
环节 1:源头设备 —— HI3516 高清摄像头
地址与服务内置 RTSP 服务,固定流地址:rtsp://192.168.1.100:554/test
作用:
负责视频采集、H.264 编码、RTSP 对外输出,是整个链路的视频源。
对应链路:提供原始 RTSP 视频流,供下游 IMX6ULL 拉取。
环节 2:中转设备 —— IMX6ULL 嵌入式开发板(核心转发节点)
板子为百问网嵌入式 Linux,无 apt 包管理器、无法安装 gstreamer1.0-rtsp/rtspclientsink,因此放弃 RTSP 转发方案,改用 GStreamer + UDP+RTP 转发。
执行的最终可用 GStreamer 管道命令
gst-launch-1.0 \ rtspsrc location=rtsp://192.168.1.100:554/test latency=0 protocols=tcp ! \ rtph264depay ! h264parse ! \ rtph264pay config-interval=1 pt=96 ! \ udpsink host=8.160.122.24 port=5004 sync=false详细的GStreamer开发教程_gstreamer教程-CSDN博客
逐个元件作用(按管道顺序):
rtspsrc
功能:GStreamer RTSP 拉流源插件
参数说明:
location:指定 HI3516 摄像头 RTSP 地址;
protocols=tcp:强制使用 TCP 传输 RTSP,避免嵌入式网络丢包;
latency=0:关闭缓冲,降低播放延迟。
作用:从 HI3516 拉取完整 RTSP 流,剥离 RTSP 信令,输出内部 RTP 视频包。
rtph264depay
功能:RTP 解封装
作用:把 RTSP 自带的 RTP 包头去掉,提取出原始 H.264 码流。
h264parse
功能:H.264 码流格式化、分片整理
作用:修复码流格式、补充帧头,保证后续重新封装 RTP 时格式合法。
rtph264pay
功能:RTP 重新封装
参数说明:
pt=96:RTP 负载类型 96,对应 H.264 视频(与后端 MediaMTX 的 SDP 配置严格匹配);
config-interval=1:每隔 1 帧输出 SPS/PPS 头部,保证播放器能正常解码。
作用:将裸 H.264 码流重新打包为 标准 RTP 数据包,为 UDP 传输做准备。
udpsink
功能:UDP 网络发送插件
参数说明:
host=8.160.122.24:阿里云服务器公网 IP;
port=5004:指定 UDP 目标端口;
sync=false:关闭音视频同步,纯视频流优化,防止卡顿。
作用:把封装好的 RTP 包通过 UDP 协议 推送到云服务器 5004 端口。
本环节踩坑复盘:
最初想用 rtspclientsink 直接推 RTSP → 报错 no element "rtspclientsink"
原因:嵌入式系统无包管理器,无法安装 RTSP 相关插件,方案作废。
改用 UDP+RTP 中转,规避插件依赖,适配嵌入式板子环境。
环节 3:云端服务 —— 阿里云 Ubuntu 服务器 + Docker + MediaMTX
服务器角色:接收远端 UDP-RTP 流 → 转换为标准 RTSP 服务 → 对外提供播放流。
3.1 前置环境:Docker 部署
Docker = 一个超级轻量的 “独立小虚拟机 / 软件盒子”
你可以把它理解成:
- 一个密封的软件集装箱
- 里面自带运行环境、配置、依赖
- 和你的服务器系统完全隔离
- 点开就能跑,不会污染系统,不会冲突
- 复制到任何电脑都能一模一样运行
用一条命令拿到一个装好MediaMTX的盒子。
先修复系统异常:dpkg --configure -a(解决 apt 被中断问题)
安装 docker.io、配置国内镜像加速器(解决 Docker Hub 拉取超时)
核心作用:使用 Docker 容器隔离 MediaMTX 服务,简化部署、端口映射、配置挂载。
3.2 MediaMTX 流媒体服务(核心转协议组件)
MediaMTX 是轻量 RTSP 流媒体服务器,本次作用:
监听 UDP 5004 端口接收 RTP-H264 流 → 转成 RTSP 流在 8554 端口对外服务。
1)配置文件 mediamtx.yml 详解
yaml logLevel: info logDestinations: [stdout] rtspAddress: :8554 paths: cam: source: udp+rtp://0.0.0.0:5004 rtpSDP: | v=0 o=- 0 0 IN IP4 0.0.0.0 s=H264 c=IN IP4 0.0.0.0 t=0 0 m=video 5004 RTP/AVP 96 a=rtpmap:96 H264/90000 a=fmtp:96 packetization-mode=1
rtspAddress: :8554:新版 MediaMTX 顶层配置,指定 RTSP 服务监听 8554 端口(旧版 rtsp:{port} 写法会解析报错)。
paths.cam:定义流路径 cam,对应播放地址后缀 /cam。
source: udp+rtp://0.0.0.0:5004:监听本机所有网卡的 UDP 5004 端口,接收 RTP 流。
rtpSDP:SDP 会话描述,告诉 MediaMTX 流格式:H.264、RTP 负载 96、采样率 90000,和开发板 rtph264pay 参数一一对应,是解码成功的关键。
2)容器启动命令 & 端口映射
docker run -d \ --name mediamtx \ --restart=always \ -p 8554:8554/tcp \ -p 5004:5004/udp \ -v /root/mediamtx.yml:/mediamtx.yml \ bluenviron/mediamtx:latest
-p 8554:8554/tcp:容器 RTSP 端口映射到宿主机 TCP 8554,供 VLC 拉流;
-p 5004:5004/udp:容器 UDP 端口映射到宿主机 UDP 5004,接收开发板推流;
-v:挂载本地配置文件到容器内,永久生效。
本环节踩坑复盘:
初始写 rtp://0.0.0.0:5004 → 报错 invalid source
原因:MediaMTX 不支持纯 rtp://,必须使用 udp+rtp://。
旧版写法 rtsp:{port:8554} → 报错 cannot unmarshal object into Go value of type bool
原因:v1.12+ 新版配置语法变更,必须使用顶层 rtspAddress。
安全组 / 防火墙:手动放行 TCP 8554、UDP 5004,保证公网 + 内网端口互通。
环节 4:播放端 —— 本地 PC + VLC 播放器
播放地址
rtsp://8.160.122.24:8554/cam
作用
作为 RTSP 客户端,主动连接云服务器 MediaMTX 的 8554 端口;
接收 RTSP 流、解析 RTP 包、解码 H.264 视频,完成画面播放。
链路终点:整套采集 - 转发 - 转协议 - 播放流程闭环。
三、核心协议流转总表
四、整套方案设计思路(适配你硬件限制)
受限于 IMX6ULL 嵌入式系统无包管理器,放弃端到端 RTSP 直推;
采用 RTSP 拉流 → RTP 重封装 → UDP 透传 绕开插件缺失问题;
云端用 MediaMTX 做 UDP-RTP → RTSP 协议转换,对外提供标准公网 RTSP 服务;
Docker 简化服务部署与端口管理,配合阿里云安全组实现跨网远程视频监控。
Wifi
- 应用层:APP 数据
- 网络层(IP):跨网段寻址(公网 / 路由)
- 数据链路层(MAC):局域网寻址、信道管控、组帧、重传、加密
- 物理层(PHY):QAM/OFDM/ 射频 / 电波,把比特变成无线电信号
WiFi基本知识总结 --- 通信框架及基础概念说明-CSDN博客
WiFi工作模式详解:从station到Mesh,全面解析无线网络架构-CSDN博客
Linux系统将WiFi配置为AP模式 --- hostapd 和 udhcpd的使用说明-CSDN博客
80211 wifi帧格式--管理帧、数据帧、控制帧_wifi帧长度-CSDN博客
PC与IMX开发板同连一个热点,私域透传VLC进度条读秒,但是黑屏。WIFI传到云服务器再用PC端的VLC拉流就正常。检查了路由表,硬件连接正常,无线网卡正常向外传输。可能是以下原因。
gst-launch-1.0 \ rtspsrc location=rtsp://192.168.1.100:554/test latency=0 protocols=tcp ! \ rtph264depay ! h264parse ! \ rtph264pay config-interval=1 pt=96 ! \ udpsink host=8.160.122.24 port=5004 sync=false与4G传输时同样的命令。
MPU6050
添加mpu6050调试
设备树添加mpu6050节点,为适配6050,修改clock-frequency从 400000 改成 100000 。
1.在入 Hi3516CV610_SDK_V1.0.1.0的环境中修改设备树、内核图形工具后出现下面找不到dtb文件的问题。
初步解决办法:
修改 /home/ipc/sdk/Hi3516CV610_SDK_V1.0.1.0-BAK/open_source/linux/Makefile 添加下面强制编译 hi3516cv610-demb-flash.dtb 。
$(MAKE) -C $(BUILD_DIR)/$(KERNEL_VER) ARCH=$(ARCH_TYPE) LLVM=$(LLVM) LLVM_IAS=$(LLVM) $(CROSS) hi3516cv610-demb-flash.dtb;5.10 内核必须先载入 hi3516cv610_defconfig 生成.config,才有 dtbs 编译目标,裸源码直接 make dtbs 必然报错
cd ~/sdk/Hi3516CV610_SDK_V1.0.1.0-BAK/open_source/linux/linux-5.10.y # 配置工具链 export PATH=$PATH:/opt/linux/x86-arm/arm-v01c02-linux-musleabi-gcc/bin export CROSS=arm-v01c02-linux-musleabi- # 1、清理旧配置 make ARCH=arm CROSS_COMPILE=$CROSS mrproper # 2、载入海思原厂配置(关键!没有这步无dtbs目标) make ARCH=arm CROSS_COMPILE=$CROSS hi3516cv610_defconfig # 3、再编译dtbs make ARCH=arm CROSS_COMPILE=$CROSS dtbs之后编译内核,进入内核目录
cd ~/sdk/Hi3516CV610_SDK_V1.0.1.0-BAK/open_source/linux/linux-5.10.y加载配置
make ARCH=arm CROSS_COMPILE=arm-v01c02-linux-musleabi- hi3516cv610_defconfig进入BSP目录,执行官方编译命令
cd ~/sdk/Hi3516CV610_SDK_V1.0.1.0-BAK/smp/a7_linux/source/bsp make LIB_TYPE=musl CHIP=hi3516cv610 DEBUG=1 KERNEL_CFG=hi3516cv610_new_defconfig kernel内核镜像在该路径下:/home/ipc/sdk/Hi3516CV610_SDK_V1.0.1.0-BAK/smp/a7_linux/source/bsp/pub
最终找到问题出现的原因:
必须先 source /etc/profile 刷新 PATH,否则系统找不到交叉编译器。执行完之后,当前终端任意手动编译 dtbs/zImage,都不用再手动 export 工具链。想要新开终端自动带工具链,在~/.bashrc末尾添加 source /etc/profile 。
/etc/profile、 ~/.profile、~/.bashrc三者区别:
1. 生效时机完全不同
登录系统 → 读 /etc/profile → 再读 ~/.profile
打开终端 → 只读 ~/.bashrc
2. 适用 shell 不同
/etc/profile、~/.profile:所有 shell 通用(sh、bash、zsh、dash…)
~/.bashrc:只有 bash 会用,别的 shell 不认
3. 放什么内容
环境变量 PATH → 放 ~/.profile
别名 alias、函数、提示符 → 放 ~/.bashrc
系统全局配置 → 放 /etc/profile
2.探查到了地址但是读寄存器数据都显示为0。
原因:6050默认睡眠模式,在下面修改寄存器唤醒设备。
i2c设备探查
i2cdetect -y -r 2-r= 强制使用普通 I2C 读时序,兼容海思
i2c设备身份确认,寄存器读取:
i2cget -y 2 0x68 0x75 //向 I2C 总线 2 号,地址 0x68 的设备,读取 0x75 寄存器的值i2cset -y 2 0x68 0x6B 0x00 //唤醒MPU6050EIS防抖内容学习:
一、DMP固件加载并开启,I2C控制对应寄存器修改配置。
1. 唤醒 MPU6050。
I2C_Write(0x6B,0x01); I2C_Write(0x6B,0x01); //Bit7(DEVICE_RESET)=0:不执行全芯片硬件复位(写1才是复位,复位硬件自动清0) //Bit6(SLEEP)=0:退出休眠,MPU6050正常上电工作(上电默认Bit6=1休眠) //Bit5(CYCLE)=0:关闭低功耗间歇采样模式 //Bit4:保留位固定0 //Bit3(TEMP_DIS)=0:开启片内温度传感器 //Bit2~Bit0(CLKSEL=001):选用X轴陀螺PLL时钟,DMP专用时钟(禁止000内部8M晶振)2. 关闭信号复位。
0x68(SIGNAL_PATH_RESET):写完 DMP 复位1<<2后,需要清空复位标志,I2C_Write(0x68,0x00);
3. 开启 DMP 模式。
- DMP 总使能:
0x6A(USER_CTRL) |= (1<<7); //Bit7=DMP_EN,DMP硬件开关 - FIFO 开启:
0x6A |= (1<<6); //Bit6=FIFO_EN,DMP姿态数据输出到FIFO必备
4.载入官方DMP固件到片内 SRAM
DMP固件寄存器基地址为0x0000(片内RAM的全局地址)
REG 0x6D(文档 REGISTER 49,表格里 4.19:I²C MASTER STATUS 前面)→BANK_SEL 固件存储分区选择寄存器,BANK = 全局地址 >>8(高 8 位)
REG 0x6E(REGISTER50)→MEM_START_ADDR 片内 SRAM 起始地址,页内偏移:全局地址 & 0xFF(低 8 位)
REG 0x6F(REGISTER51)→MEM_R/W 固件数据读写寄存器,单字节读写 DMP 片内 SRAM,写固件 / 读 DMP 配置全靠它
固件单字节写入流程,循环遍历全部DMP固件数组:
uint16_t dmp_addr = 0x0000; //DMP内存起始基地址 for(uint16_t i=0;i<sizeof(dmp_firmware);i++){ uint8_t bank = dmp_addr >> 8; //高8位=BANK uint8_t addr = dmp_addr & 0xFF; //低8位=页内偏移 I2C_Write(0x6D,bank); I2C_Write(0x6E,addr); I2C_Write(0x6F,dmp_firmware[i]); dmp_addr++; }5. 启动 DMP 解算
固件写完后,延时 5~10ms 等待 DMP 加载固件;
读取 0x6F 寄存器校验关键固件字节,判断固件是否烧录成功;
总结:
1.0x6B退出休眠 + 配置陀螺 PLL 时钟(唤醒 + 系统时钟)
2.0x68 Bit2=1DMP 软复位,随后清复位
3.循环批量:0x6D→0x6E→0x6F,从 DMP 基地址 0x0000 写入全部固件
4.0x6A Bit7=1(BIT7开DMP)+BIT6=1(开FIFO)正式启用 DMP
5.依次配置:0x19 采样分频、0x1A DLPF、0x1B 陀螺量程、0x1C 加速度量程
6.0x37配置中断引脚属性 →0x38开启 DMP 数据就绪中断
7.(可选)0x6A Bit5=1 开启 I2C 主机,外接磁力计
8.等待 DMP 运行,单片机循环读取 FIFO 数据,解析姿态角
二、读取DMP输出→ 四元数 q0/q1/q2/q3
开启配置(在前面初始化已实现):
// 开启 FIFO + 开启 DMP(必须在初始化里写) I2C_Write(0x6A, 0xC0); // Bit7=1 DMP_EN + Bit6=1 FIFO_EN // DMP输出到FIFO(必须配置) I2C_Write(0x18, 0x00); // 使能DMP_FIFO代码示例:
// 标准DMP四元数读取(从硬件FIFO读取,最稳定、不丢帧) void MPU_Read_DMP_Quat(float *q0, float *q1, float *q2, float *q3) { u8 buf[16]; // DMP 一包数据固定 16 字节 u16 fifo_count = 0; // FIFO 内剩余字节数 // 1. 读取 FIFO_COUNT 寄存器,获取当前有多少字节数据 fifo_count = I2C_Read(0x72) << 8; // 高8位 fifo_count |= I2C_Read(0x73); // 低8位 // 2. 只有数据 >=16 字节才读取(DMP一包就是16字节) if(fifo_count >= 16) { // 3. 从硬件 FIFO 端口 0x74 读取 16 字节 I2C_ReadBuf(0x74, buf, 16); } else { // 数据不足,直接返回(防止读乱) return; } // 4. 解析 DMP 输出的四元数(官方标准格式:Q30 → 除以 2^30 = 1073741824.0f) // 注意:必须强转 int32_t,否则负数会出错!芯片只能保存整数,要将芯片中保存的整数转化为实际的小数 *q0 = ( (int32_t)(buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) ) / 1073741824.0f; *q1 = ( (int32_t)(buf[4] << 24 | buf[5] << 16 | buf[6] << 8 | buf[7]) ) / 1073741824.0f; *q2 = ( (int32_t)(buf[8] << 24 | buf[9] << 16 | buf[10] << 8 | buf[11]) ) / 1073741824.0f; *q3 = ( (int32_t)(buf[12] << 24 | buf[13] << 16 | buf[14] << 8 | buf[15]) ) / 1073741824.0f; }总结
1. 读取 FIFO_COUNT 寄存器,获取当前有多少字节数据
2. 只有数据 >=16 字节才读取(DMP一包就是16字节)
3. 从硬件 FIFO 端口 0x74 读取 16 字节
4. 解析 DMP 输出的四元数,芯片只能保存整数,要将芯片中保存的整数转化为实际的小数
三、DMP 四元数 → 直接转欧拉角 pitch(上下) /yaw(左右)
代码示例:
void Quat_To_Angle(float q0, float q1, float q2, float q3, float *pitch, float *yaw) { *pitch = asin(-2 * q1 * q3 + 2 * q0 * q2) * 57.3f; *yaw = atan2(2 * (q1*q2 + q0*q3), q0*q0 + q1*q1 - q2*q2 - q3*q3) * 57.3f; }四、得到稳定 pitch /yaw ,获取hi3516硬件时间戳
海思时间戳接口:
1.获取pitch/yaw
void Calc_Stab_Offset(float pitch, float yaw) { static float base_pitch, base_yaw; static int cnt = 0; if(cnt < 30){ // 前1秒校准基准 base_pitch += pitch; base_yaw += yaw; cnt++; if(cnt==30){ base_pitch /=30; base_yaw /=30; } return; } //由四元数计算出的base_*与diff_*会在下一步计算裁剪坐标时使用 float dp = pitch - base_pitch; float dy = yaw - base_yaw; float max = fmax(fabs(dp), fabs(dy)); if(max < 8){ enable_stab = 1; diff_pitch = dp; diff_yaw = dy; } else if(max > 10){ enable_stab = 0; base_pitch = pitch; base_yaw = yaw; } }2.IMU 数据结构与环形缓冲
// IMU 数据帧(带时间戳,用于视频帧同步) typedef struct { HI_U64 pts; // 海思系统时间戳(与视频帧同源) float diff_pitch; // 上下抖动角度 float diff_yaw; // 左右抖动角度 int enable_stab; // 防抖使能标志 } IMU_FRAME_T; // 环形缓冲配置 #define IMU_BUFFER_SIZE 60 static IMU_FRAME_T g_ImuBuffer[IMU_BUFFER_SIZE]; static int g_ImuWriteIndex = 0; static pthread_mutex_t g_ImuLock = PTHREAD_MUTEX_INITIALIZER;3.获取 PTS 并写入缓冲
IMU(惯性测量单元,即mpu6050) PTS(时间戳)
// 获取海思硬件系统时间戳 HI_U64 imu_pts; HI_MPI_SYS_GetSysStamp(&imu_pts); // 加锁写入环形缓冲区 pthread_mutex_lock(&g_ImuLock); g_ImuBuffer[g_ImuWriteIndex].pts = imu_pts; g_ImuBuffer[g_ImuWriteIndex].diff_pitch = diff_pitch; g_ImuBuffer[g_ImuWriteIndex].diff_yaw = diff_yaw; g_ImuBuffer[g_ImuWriteIndex].enable_stab = enable_stab; // 环形缓冲自增 g_ImuWriteIndex = (g_ImuWriteIndex + 1) % IMU_BUFFER_SIZE; pthread_mutex_unlock(&g_ImuLock);总结
1.上电校准基准 base_pitch / base_yaw,均值校准。
2.计算偏移,diff_*
3.三段防抖策略:<8° → 防抖开启;8~10° → 保持;10° → 重置基准,关闭防抖;
4.获取 PTS 并写入缓冲
五、陀螺仪数据与视频帧匹配
根据视频帧 PTS 查找匹配的 IMU 数据
// 输入:视频帧的 PTS // 输出:最匹配的 IMU 数据 int Get_Matched_IMU(HI_U64 frame_pts, IMU_FRAME_T *pOutImu) { HI_U64 min_diff = 0xFFFFFFFFFFFFFFFF; int best_idx = 0; pthread_mutex_lock(&g_ImuLock); // 遍历缓冲区,寻找时间戳最接近的数据 for (int i = 0; i < IMU_BUFFER_SIZE; i++) { if (g_ImuBuffer[i].pts == 0) continue; HI_U64 diff = llabs(g_ImuBuffer[i].pts - frame_pts); if (diff < min_diff) { min_diff = diff; best_idx = i; } } *pOutImu = g_ImuBuffer[best_idx]; pthread_mutex_unlock(&g_ImuLock); return 0; }- IMU 高频采集(1000Hz),每包数据都打上硬件时间戳
PTS - 视频按帧率输出(30fps),每帧自带
u64PTS - 裁剪前根据帧
PTS找到同一时刻的 IMU 抖动数据
六、角度 → 画面偏移像素(角度转像素 + 裁剪坐标计算公式)
镜头向哪边抖,画面裁剪起点就像反方向移动,人眼看到画面保持不动,就是 EIS 电子防抖。
代码示例
//原始采集大图宽高 #define W 1920 #define H 1080 //FOV (Field of View,视场角)= 镜头能拍到的左右 / 上下最大广角角度 #define FOV_H 86 #define FOV_V 57 //传感器整张大图:例如横向总像素 1920px,对应完整 86° 广角,1°对应横向像素=1920÷86≈22.33像素 float pix_h = W / FOV_H; float pix_v = H / FOV_V; /* 1.预留8°余量 2.diff_yaw × pix_h = 镜头晃动带来的画面实际偏移像素,反向防抖逻辑:相机向左晃 → diff_yaw < 0 -diff_yaw × pix_h变成正数,X 坐标变大 → 画面裁剪起点右移,抵消左晃; 3.y = pix_h *8 - diff_yaw * pix_h,相机向上抬 → diff_pitch>0 → Y 坐标变小,画面向上裁; 4.diff_yaw为第四步计算出的偏移 */ int x = pix_h *8 - diff_yaw * pix_h; int y = pix_v *8 - diff_pitch * pix_v; //不能小于0,不能超出16°总冗余(左右各8°) x = x<0 ? 0 : (x>pix_h*16 ? pix_h*16 : x); y = y<0 ? 0 : (y>pix_v*16 ? pix_v*16 : y);为什么EIS 防抖要预留 8° 余量?
pix_h *8/pix_v *8:基准起始坐标
pix_h*8:8° 对应的横向总像素 = 22.33×8≈178.6pxpix_v*8:8° 对应的纵向总像素 = 18.95×8≈151.6px
原理:
原始大图比最终输出 1080P 画面四周各多预留 8° 画面,无抖动时,画面从 X=179,Y=152 位置开始裁剪(中间居中输出),这是防抖基准原点。
总结:
1、FOV 含义
FOV 视场角:镜头能够捕获景物的广角范围,水平 86°、垂直 57° 对应有效输出画面 1920×1080。含义:镜头每旋转 1°,成像在 CMOS 上横向偏移 22.33 像素、纵向偏移 18.95 像素,建立「陀螺仪角度→图像像素」换算桥梁。
2、预留 8° 余量的核心目的
EIS 电子防抖必须在有效画面四周预留 8° 冗余画面:
- 无抖动:裁剪起点
x=pix_h×8,y=pix_v×8,从大图中间裁切出标准 1080P 画面; - 小幅抖动≤8°:依靠挪动裁剪坐标,使用四周冗余画面反向补偿抖动;
- 抖动>8°:超出冗余补偿极限,对应项目逻辑:判定人为大幅度转动、重置基准、关闭防抖。
3、裁剪坐标公式:x = pix_h*8 - diff_yaw*pix_h;y = pix_v*8 - diff_pitch*pix_v
反向补偿核心逻辑:镜头向哪一侧抖动,VPSS 裁剪起点就反向移动
镜头左摆diff_yaw<0→-diff_yaw*pix_h为正 → X 变大,裁剪起点右移,抵消画面左飘;
镜头右摆diff_yaw>0→ X 变小,裁剪起点左移,抵消画面右飘;
镜头上抬diff_pitch>0→ Y 变小,起点上移,抵消画面下坠;
镜头下压diff_pitch<0→ Y 变大,起点下移,抵消画面上飘;
七、VPSS 动态裁剪
1:VPSS 初始化裁剪(上电执行一次)
在VPSS 初始化函数中开启裁剪,设置默认居中窗口:
// 从第五部分直接沿用的全局变量 #define IMG_W 1920 #define IMG_H 1080 #define FOV_H 86.0f #define FOV_V 57.0f #define MAX_DEG 8.0f float pix_h = IMG_W / FOV_H; float pix_v = IMG_H / FOV_V; // 从第五部分防抖计算出来的结果 extern float diff_pitch; extern float diff_yaw; extern int enable_stab;初始化裁剪:
void VPSS_Init_Crop(ot_vpss_grp vpss_grp) { ot_zoom_attr stZoomAttr; memset(&stZoomAttr, 0, sizeof(ot_zoom_attr)); // 1. 开启裁剪 stZoomAttr.enable = TD_TRUE; stZoomAttr.mode = OT_COORD_ABSOLUTE; // 绝对像素坐标 // 2. 默认居中裁剪(无抖动时的基准) stZoomAttr.rect.x = pix_h * MAX_DEG; stZoomAttr.rect.y = pix_v * MAX_DEG; stZoomAttr.rect.width = IMG_W; stZoomAttr.rect.height = IMG_H; // 3. 设置到 VPSS 硬件 ss_mpi_vpss_set_grp_zoom_in_window(vpss_grp, &stZoomAttr); }2.主线程循环更新裁剪(核心防抖)
直接使用第五部分计算出的 x/y,写入 VPSS
void VPSS_Update_Crop(ot_vpss_grp vpss_grp, HI_U64 frame_pts) { int x, y; int max_x, max_y; IMU_FRAME_T imu; // ========================== // ✅ 根据帧 PTS 匹配 IMU 数据 // ========================== Get_Matched_IMU(frame_pts, &imu); if(imu.enable_stab) { x = pix_h * 8 - imu.diff_yaw * pix_h; y = pix_v * 8 - imu.diff_pitch * pix_v; } else { x = pix_h * 8; y = pix_v * 8; } // 边界限幅 max_x = pix_h * 16; max_y = pix_v * 16; x = (x < 0) ? 0 : (x > max_x ? max_x : x); y = (y < 0) ? 0 : (y > max_y ? max_y : y); // 更新 VPSS 裁剪窗口 ot_zoom_attr stZoomAttr; memset(&stZoomAttr, 0, sizeof(ot_zoom_attr)); stZoomAttr.enable = TD_TRUE; stZoomAttr.mode = OT_COORD_ABSOLUTE; stZoomAttr.rect.x = x; stZoomAttr.rect.y = y; stZoomAttr.rect.width = IMG_W; stZoomAttr.rect.height = IMG_H; ss_mpi_vpss_set_grp_zoom_in_window(vpss_grp, &stZoomAttr); }总结:
1.VPSS 裁剪属于 Group 硬件功能,通过ss_mpi_vpss_set_grp_zoom_in_window实现。
2.x/y 坐标完全来自第五部分 DMP 解算,不需要额外计算。
3.每帧更新一次裁剪窗口,实现实时电子防抖。
4.预留 8° 冗余画面,抖动≤8° 有效补偿,>10° 自动重置基准。
八、主线程
1. 从 VPSS 获取一帧视频图像
2. 拿到这一帧的硬件时间戳 u64PTS
3. 根据帧 PTS,从环形缓冲区找到 时间最匹配的 IMU 抖动数据
4. 使用匹配后的 diff_pitch / diff_yaw 计算裁剪坐标 (x, y)
5. 调用 VPSS 接口,设置动态裁剪窗口
6. 送出图像进行编码、推流
7. 释放视频帧
8. 循环执行