3. 完整数据流向
c++
// ============================================================================
// 完整数据流
// ============================================================================
StreamHandler (HAL)
↓ getFrameData()
捕获线程 (captureLoop)
↓ forcePush()
rawQueue_ (无锁队列)
↓ tryPopBatch()
解算线程 (processLoop)
↓ processFrameWithSDK() ← SDK 解算深度
↓ submitToWriter() ← 提交到 AsyncFileWriter 队列
↓ frameCallback_() ← 回调应用层
AsyncFileWriter 内部队列
↓ IO 线程异步处理
磁盘文件 (raw/depth/ir)
// ============================================================================
// 为什么需要这么多中间步骤?
// ============================================================================
StreamHandler (HAL)
↓ getFrameData()
CopiedFrame // ① 从 HAL 获取的数据格式
↓ 转换
RawFrameData // ② 放入队列前的格式
↓ push
LockFreeQueue<RawFrameData> // ③ 缓冲解耦
↓ pop
RawFrameData // ④ 取出
↓ processFrameWithSDK() // ⑤ SDK 解算
ProcessedFrameData // ⑥ 解算后的格式(含 depth + ir)
↓ submitToWriter()
AsyncFileWriter // ⑦ 异步写文件
磁盘文件
| 步骤 | 数据类型 | 原因 |
|---|---|---|
| CopiedFrame | HAL 原始数据 | 直接从相机获取 |
| RawFrameData | 队列数据格式 | 带帧ID、时间戳,便于管理 |
| 无锁队列 | 缓冲 | 解耦生产和消费速度 |
| ProcessedFrameData | 解算结果 | 包含 depth + ir 两种数据 |
| AsyncFileWriter | 异步写 | 不阻塞解算线程 |
3. 初始化、启动和停止
scss
构造函数
↓ 创建队列、计算帧间隔
initialize()
↓ 1. 创建输出目录(保存原始数据、深度图、IR图)
↓ 2. 初始化深度处理器(厂商SDK,用于计算深度)
↓ 3. 初始化 StreamHandler (EVS HAL), 启动相机流,传入相机ID、格式等
↓ 4. 初始化异步文件写入器
start()
↓ 启动 Pipeline - 创建工作线程
↓ 创建捕获线程 → 循环获取相机帧
↓ 创建处理线程 → 循环解算深度
↓ 创建统计线程 → 循环更新帧率
stop()
↓ 设置停止标志
↓ 等待所有线程结束
↓ 关闭相机、停止写入器
析构函数
↓ 调用 stop()
4. 线程整体架构图
c++
// ============================================================================
// ToFPipelineManager 三线程架构
// ============================================================================
┌─────────────────────────────────────────────────────────────────────────────┐
│ 捕获线程 (captureLoop) │
│ 职责:从 StreamHandler 获取原始帧,放入原始队列 │
│ │
│ while (!shouldStop) { │
│ frame = streamHandler_->getFrameData(); // 阻塞等待 │
│ rawQueue_->forcePush(frame); // 入队 │
│ } │
└─────────────────────────────────────────────────────────────────────────────┘
↓
┌───────────────┐
│ rawQueue_ │
│ (无锁队列) │
└───────┬───────┘
↓
┌─────────────────────────────────────────────────────────────────────────────┐
│ 解算线程 (processLoop) │
│ 职责:从队列取帧,调用SDK解算深度,保存文件,回调应用 │
│ │
│ while (!shouldStop) { │
│ rawQueue_->tryPopBatch(batch); // 批量取帧 │
│ processFrameWithSDK(raw, out); // SDK解算深度 │
│ submitToWriter(out); // 提交到IO线程写文件 │
│ frameCallback_(out); // 回调应用层 │
│ } │
└─────────────────────────────────────────────────────────────────────────────┘
↓
┌───────────────┐
│ AsyncFileWriter │
│ (IO线程) │
└───────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────┐
│ 统计线程 (statsLoop) │
│ 职责:定期计算帧率,更新统计信息,回调统计 │
│ │
│ while (!shouldStop) { │
│ sleep(1秒); │
│ 计算每秒处理帧数 → currentFps │
│ statsCallback_(stats); // 回调统计 │
│ } │
└─────────────────────────────────────────────────────────────────────────────┘
c++
// ============================================================================
// 四线程架构(实际有4个线程)
// ============================================================================
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ captureLoop │ │ processLoop │ │ statsLoop │ │ IO Thread │
│ (捕获线程) │ │ (解算线程) │ │ (统计线程) │ │ (写文件线程) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │ │
↓ ↓ ↓ ↓
获取相机帧 SDK解算深度 计算帧率 真正写磁盘
放入rawQueue 保存文件 回调统计 不阻塞主流程
↓ 回调应用
FrameDataTypes.h │
-RawFrameData ↓
-ProcessedFrameData submitToWriter()
(只是提交任务,不等待)
│
└──────────────────→ IO Thread
| 线程 | 函数 | 创建位置 | 职责 |
|---|---|---|---|
| 捕获线程 | captureLoop() | start() 中创建 | 从 StreamHandler 取帧 |
| 解算线程 | processLoop() | start() 中创建 | SDK 解算、保存文件 |
| 统计线程 | statsLoop() | start() 中创建 | 计算帧率、上报统计 |
| IO 线程 | AsyncFileWriter::ioLoop() | fileWriter_->start() 中创建 | 异步写文件 |
arduino
EVS 采集线程
↓(拷贝后帧)
mFrameDataQueue
↓
Capture 中转线程
↓(封装 RawFrameData)
【 rawQueue_ ------ 无锁队列 】
↓
Process 解算线程(ToF SDK)
↓
ProcessedFrameData(深度图 + IR图)
↓
显示和写入
3.1 捕获线程 (captureLoop)
-
获取拷贝的帧数据(阻塞等待,直到有帧):CopiedFrame copiedFrame = streamHandler_->getFrameData();
-
直接推入解算队列:RawFrameData raw;
-
移动语义,零拷贝:raw.data = std::move(copiedFrame.data);
-
队列满时自动丢帧,防止内存爆炸:
FrameDataTypes.文件里面有两个结构体:
-
RawFrameData 用于存原始数据帧
-
ProcessedFrameData 用于存结算后的数据帧
3.2 为什么需要RawFrameData(rawQueue_)
CopiedFrame → RawFrameData(rawQueue_) → ProcessedFrameData (procQueue_)
为什么需要 RawFrameData结构体 和rawQueue_队列?不直接把获取的 数据帧 copiedFrame进行下一步解算?生产-消费模型
因为:
- 解算( ToF 深度 / 图像处理 )很慢,耗时 10ms~50ms
getFrameData()是阻塞取帧- 如果解算卡住 ,会导致队列塞满、丢帧、甚至影响相机线程
- 车机里相机采集、数据队列、算法解算必须三个完全独立线程
不是不能直接解算,而是直接解算会把相机回调卡崩!必须用「CopiedFrame → RawFrameData → 无锁队列」做解耦!
c++
// ============================================================================
// 问题:为什么不能直接解算?
// ============================================================================
// ❌ 不好的设计:在捕获线程直接解算
void captureLoop() {
while (!shouldStop_) {
CopiedFrame frame = streamHandler_->getFrameData();
// 直接解算(耗时 30-50ms)
depthProcessor_->processFrame(frame.data); // 阻塞!
// 问题:解算期间相机不能继续收帧,会丢帧!
}
}
// ✅ 好的设计:生产者和消费者分离
void captureLoop() { // 生产者:快速收帧
while (!shouldStop_) {
CopiedFrame frame = streamHandler_->getFrameData(); // 阻塞等待
rawQueue_->push(frame); // 快速入队,不处理
// 循环结束,立即可以收下一帧
}
}
void processLoop() { // 消费者:慢速解算
while (!shouldStop_) {
RawFrameData frame = rawQueue_->pop(); // 取帧
depthProcessor_->processFrame(frame.data); // 耗时操作
// 解算慢不影响收帧
}
}
- 核心原因:速度不匹配
| 线程 | 操作 | 耗时 | 频率 |
|---|---|---|---|
| 捕获线程 | getFrameData() + 入队 | ~1ms | 30fps |
| 解算线程 | SDK 解算 + 写文件 | ~50ms | 可能 <30fps |
无锁队列的作用:缓冲 解耦
markdown
捕获线程(快)→ 队列 → 解算线程(慢)
↓ ↓
不用等待 处理不完的帧会积压
队列满时自动丢帧
3.3 正确架构:三层解耦(车机标准设计)
c++
1. EVS 采集线程(HAL 回调)
→ deliverFrame_1_1
→ memcpy 同步拷贝
→ 异步归还 buffer
→ 推入 mFrameDataQueue
2. Capture 线程(你代码里的 captureLoop)
→ 从 mFrameDataQueue 取 CopiedFrame
→ 封装成 RawFrameData
→ 推入 **rawQueue_(无锁队列)**
3. Process 解算线程(processLoop)
→ 从 **rawQueue_(无锁队列)** 批量取帧
→ ToF SDK 深度解算
→ 输出 ProcessedFrameData
三层线程:
1. EVS 采集线程(拷贝 + 归还)
2. Capture 中转线程(封装 + 入队)
3. Process 解算线程(算法 + 输出)
无锁队列:
rawQueue_ 连接 Capture 线程 和 Process 线程,
负责低延迟、线程安全、无锁的数据传递。
为什么要分三层?
- EVS 线程 :只管收帧、拷贝、归还,绝对不做耗时操作
- 转发线程 :做数据格式转换,削峰、填谷、保护相机
- 算法线程 :慢慢解算,卡住也不影响采集
3.2 解算线程 (processLoop)
-
批量取帧减少锁竞争:size_t n = rawQueue_->tryPopBatch(batch, 4);
-
SDK 解算是 CPU 密集型操作,逐帧处理,SDK 解算深度:processFrameWithSDK(batch[i], out);
-
submitToWriter只是提交任务,不等待写入完成。异步写文件(提交到 IO 线程):submitToWriter(out); -
回调应用层,推送到显示队列
3.3 统计线程 (statsLoop)
-
每秒计算一次帧率
-
记录峰值帧率用于性能分析
-
通过回调上报统计信息
3.4 SDK解算包装函数
c++
// ============================================================================
// 直接调用 vs 包装函数
// ============================================================================
// ❌ 不好的设计:直接调用 SDK
void processLoop() {
RawFrameData frame = rawQueue_->pop();
// 直接调用 SDK,错误处理分散
if (!depthProcessor_->processFrame(frame.data)) {
// 处理错误...
}
const char* depth = depthProcessor_->getDepthData();
const char* ir = depthProcessor_->getIRData();
// 转换数据格式...
// 分配内存...
// 复制数据...
// 错误处理...
// 代码很长,难以维护
}
// ✅ 好的设计:包装函数
bool processFrameWithSDK(const RawFrameData& raw, ProcessedFrameData& out) {
// 所有 SDK 相关逻辑集中在这里
// 1. 检查输入
// 2. 调用 SDK
// 3. 获取结果
// 4. 格式转换
// 5. 错误处理
// 6. 返回结果
}
void processLoop() {
RawFrameData frame = rawQueue_->pop();
ProcessedFrameData out;
if (processFrameWithSDK(frame, out)) {
// 成功,使用 out
submitToWriter(out);
frameCallback_(out);
} else {
// 失败,统计错误
stats_.processErrors++;
}
}
| 好处 | 说明 |
|---|---|
| 封装 | SDK 调用细节不暴露 |
| 复用 | 其他地方也可以调用 |
| 测试 | 可以单独测试解算逻辑 |
| 维护 | SDK 升级只改一个地方 |
| 错误处理集中 | 不用到处写错误处理 |
3.5 异步文件读取
为什么不把submitToWriter做成单独的线程,而是放在AsyncFileWriter.cpp中做类内部管理线程
好的设计原则:
- 单一职责:
AsyncFileWriter只负责异步写文件 - 封装性:线程管理封装在类内部
- 可复用:其他地方也可以使用
AsyncFileWriter
异步文件写入器
arduino
// AsyncFileWriter = 异步文件写入器
// 作用:把写磁盘的操作放到独立线程,不阻塞主流程
为什么是异步?
异步就是非阻塞
只是把任务丢进队列,立即返回(~0.001ms) // 调用者线程继续执行,不等待磁盘写入
为什么叫"异步文件写入器"?
| 关键字 | 含义 |
|---|---|
| 异步 | 调用者不等待结果,立即返回 |
| 文件写入 | 把数据写到磁盘 |
| 器 | 封装成一个类/对象 |
AsyncFileWriter = 负责异步写文件的类
各函数解释
| 函数 | 作用 | 一句话说明 |
|---|---|---|
| 构造函数 | 初始化 | 保存输出目录和队列大小 |
| 析构函数 | 清理 | 调用 stop() 停止线程 |
| start() | 启动IO线程 | 创建 writerLoop 线程 |
| stop() | 停止IO线程 | 等待线程结束,刷新剩余任务 |
| submitWrite() | 提交单个写入任务 | 把任务放入队列,立即返回 |
| submitBatch() | 批量提交任务 | 一次提交多个任务,减少锁竞争 |
| waitForCriticalWrites() | 等待关键帧写完 | 阻塞直到所有关键帧写入完成 |
| getQueueSize() | 查询队列大小 | 返回待写入任务数 |
| writerLoop() | IO线程主循环 | 从队列取任务,真正写磁盘 |
| writeFileInternal() | 实际写文件 | 打开文件 + 写入数据 |
| ensureDirectoryExists() | 确保目录存在 | 不存在则创建 |
| getStatistics() | 获取统计信息 | 总帧数、总字节数、平均耗时等 |
核心流程图
scss
┌─────────────────────────────────────────────────────────────────────────────┐
│ AsyncFileWriter 工作流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 解算线程 (调用者) IO线程 (writerLoop) │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ submitWrite() │ │ while(!stop) { │ │
│ │ ↓ │ │ 等待队列 │ │
│ │ 放入队列 │ ─── 任务 ───→ │ ↓ │ │
│ │ notify_one() │ │ 取出任务 │ │
│ │ ↓ │ │ ↓ │ │
│ │ 立即返回 ✅ │ │ writeFile() │ │
│ └─────────────────┘ │ ↓ │ │
│ │ 写入磁盘 💾 │ │
│ └─────────────────┘ │
│ │
│ 关键:解算线程不等待写磁盘,只把任务丢进队列就继续工作 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
scss
┌─────────────────────────────────────────────────────────────────────────────┐
│ AsyncFileWriter 核心流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 调用者线程 IO线程 │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ submitWrite() │ │ writerLoop() │ │
│ │ ↓ │ │ ↓ │ │
│ │ 构造任务结构体 │ │ unique_lock │ │
│ │ ↓ │ │ ↓ │ │
│ │ lock_guard 加锁 │ │ wait(等待唤醒) │ │
│ │ ↓ │ │ ↓ │ │
│ │ 任务入队 push │ │ move 取出任务 │ │
│ │ ↓ │ │ ↓ │ │
│ │ 解锁(自动) │ │ 出队 pop │ │
│ │ ↓ │ │ ↓ │ │
│ │ notify_one() │ ─────── 唤醒 ────→ │ writeFileInternal│ │
│ │ ↓ │ │ ↓ │ │
│ │ 立即返回 ✅ │ │ ofs.write() 写磁盘│ │
│ └─────────────────┘ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
完整流程时序图
scss
┌─────────────────────────────────────────────────────────────────────────────┐
│ 完整调用时序 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 主线程 IO线程 磁盘 │
│ │ │ │ │
│ │ ① new AsyncFileWriter() │ │ │
│ │────────────────────────→│ │ │
│ │ │ │ │
│ │ ② start() │ │ │
│ │────────────────────────→│ │ │
│ │ │ ③ 创建 writerLoop 线程 │ │
│ │ │←─────────────────────────│ │
│ │ │ │ │
│ │ ④ submitWrite("depth") │ │ │
│ │────────────────────────→│ │ │
│ │ [立即返回 ✅] │ ⑤ 任务入队 │ │
│ │ │ notify_one() │ │
│ │ │ │ │
│ │ │ ⑥ 被唤醒,取任务 │ │
│ │ │ │ │
│ │ │ ⑦ writeFileInternal() │ │
│ │ │─────────────────────────→│ │
│ │ │ │ ⑧ 写入文件 │
│ │ │←─────────────────────────│ │
│ │ │ │ │
│ │ ⑨ submitWrite("ir") │ │ │
│ │────────────────────────→│ │ │
│ │ [立即返回 ✅] │ ⑩ 任务入队 │ │
│ │ │ notify_one() │ │
│ │ │ │ │
│ │ ⑪ waitForCriticalWrites │ │ │
│ │────────────────────────→│ │ │
│ │ [阻塞等待] │ ⑫ 处理剩余任务 │ │
│ │ │─────────────────────────→│ │
│ │ │←─────────────────────────│ │
│ │ │ ⑬ 关键帧写完,通知 │ │
│ │←────────────────────────│ │ │
│ │ [被唤醒,继续] │ │ │
│ │ │ │ │
│ │ ⑭ stop() │ │ │
│ │────────────────────────→│ ⑮ 退出循环,flush剩余 │ │
│ │ │─────────────────────────→│ │
│ │ │ ⑯ 线程结束 │ │
│ │←────────────────────────│ │ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
函数调用关系图
scss
┌─────────────────────────────────────────────────────────────────────────────┐
│ 函数调用关系 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 外部调用者 (ToFPipelineManager) │
│ │ │
│ ├──→ new AsyncFileWriter() ──→ 构造函数 │
│ │ └──→ ensureDirectoryExists() │
│ │ │
│ ├──→ start() ──→ 创建 writerThread_ │
│ │ └──→ writerLoop() (独立线程运行) │
│ │ │
│ ├──→ submitWrite() ──→ 任务入队 │
│ │ └──→ queueCV_.notify_one() │
│ │ │ │
│ │ ↓ │
│ │ writerLoop() 被唤醒 │
│ │ │ │
│ │ └──→ writeFileInternal() │
│ │ │ │
│ │ └──→ ensureDirectoryExists() │
│ │ │
│ ├──→ waitForCriticalWrites() ──→ 等待 criticalDoneCV_ │
│ │ │
│ └──→ stop() ──→ shouldStop_ = true │
│ └──→ writerThread_->join() │
│ │ │
│ └──→ writerLoop() 退出 │
│ └──→ 处理剩余任务 │
│ └──→ writeFileInternal() │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
submitWrite → 入队 → 唤醒 → writerLoop → 取出 → writeFileInternal
核心数据流
scss
┌─────────────────────────────────────────────────────────────────────────────┐
│ 数据流 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 解算线程 磁盘 │
│ │ │ │
│ │ ProcessedFrameData │ │
│ │ │ │ │
│ │ ↓ │ │
│ │ submitWrite("depth.raw", data) │ │
│ │ │ │ │
│ │ ↓ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ WriteTask │ │ │
│ │ │ filename: ... │ │ │
│ │ │ data: 指针 │ ──移动→ │ │
│ │ │ frameId: 123 │ │ │
│ │ └─────────────────┘ │ │
│ │ │ │ │
│ │ ↓ │ │
│ │ taskQueue_.push() │ │
│ │ │ │ │
│ │ │ │ │
│ │ IO线程独立运行 │ │
│ │ │ │ │
│ │ ↓ │ │
│ │ writerLoop() 取任务 │ │
│ │ │ │ │
│ │ ↓ │ │
│ │ writeFileInternal() │ │
│ │ │ │ │
│ │ └─────────────────────────→│ depth_000123.raw │
│ │ │ │
└─────────────────────────────────────────────────────────────────────────────┘
lock_guard vs unique_lock 区别
arduino
// ============================================================================
// lock_guard - 简单场景
// ============================================================================
{
std::lock_guard<std::mutex> lock(mutex_); // 构造时加锁
queue_.push(task); // 临界区
} // 析构时自动解锁,不能提前解锁
// ============================================================================
// unique_lock - 需要条件变量时
// ============================================================================
std::unique_lock<std::mutex> lock(mutex_); // 可以延迟加锁
queueCV_.wait(lock, []{ return !queue_.empty(); }); // wait 需要 unique_lock
lock.unlock(); // 可以提前解锁
// 可以多次加锁/解锁
// ============================================================================
// 你的代码中的使用场景
// ============================================================================
// submitWrite() 中用 lock_guard ✅(只做 push,不需要 wait)
std::lock_guard<std::mutex> lock(queueMutex_);
taskQueue_.push(std::move(task));
// writerLoop() 中用 unique_lock ✅(需要 wait)
std::unique_lock<std::mutex> lock(queueMutex_);
queueCV_.wait(lock, [this]() {
return !taskQueue_.empty() || shouldStop_.load();
});
| 特性 | lock_guard | unique_lock |
|---|---|---|
| 灵活性 | 低,只能构造时加锁,析构时解锁 | 高,可以随时加锁/解锁 |
| 条件变量 | ❌ 不能用 wait() | ✅ 可以用 wait() |
| 性能 | 稍快(开销小) | 稍慢(功能多) |
| 移动语义 | ❌ 不支持 | ✅ 支持 |
| 代码简洁 | ✅ 简单 | 稍复杂 |
总结:需要 wait() 就用 unique_lock,否则用 lock_guard。
生产者-消费者模型
c++
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ ToFPipelineManager 生产者-消费者模型 │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 生产者1 │ │ 消费者1 │ │ 生产者2 │ │ 消费者2 │ │
│ │ (HAL回调) │ ──→ │ (捕获线程) │ ──→ │ (解算线程) │ ──→ │ (IO线程) │ │
│ │ │ │ │ │ │ │ │ │
│ │ deliverFrame │ │ getFrameData │ │ processLoop │ │ ioLoop │ │
│ │ _1_1() │ │ │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │ │
│ ↓ ↓ ↓ ↓ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ GPU内存 │ │ mFrameData │ │ rawQueue_ │ │ Writer队列 │ │
│ │ (HAL Buffer)│ │ Queue │ │ (无锁队列) │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ 大小: 60个 │ │ 大小: 2 │ │ 大小: 8 │ │ 大小: 64 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ↑ ↑ ↑ ↑ │
│ │ │ │ │ │
│ ┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐ │
│ │ 数据流向 │ │ 数据流向 │ │ 数据流向 │ │ 数据流向 │ │
│ │ GPU→CPU │ │ Copy→Move │ │ Batch→SDK │ │ Memory→Disk│ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────┘
| 模型组件 | 你的代码实现 |
|---|---|
| 缓冲区 | mFrameDataQueue、rawQueue_(无锁队列) |
| 生产者线程 | HAL回调线程 (deliverFrame_1_1) |
| 消费者线程 | 捕获线程 (captureLoop) |
| 同步机制 | std::mutex + std::condition_variable |
| 阻塞条件(满) | 队列满时 forcePush 丢帧(你的策略是丢新帧) |
| 阻塞条件(空) | getFrameData() 中 wait() 阻塞 |
本地显示线程
1.整体架构图
c++
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ 车机本地显示架构 │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ToFPipelineManager (数据源) │
│ │ │
│ │ ProcessedFrameData │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────────────────────────┐ │
│ │ DisplayController │ │
│ │ • 管理显示线程 │ │
│ │ • 深度图转伪彩色 (LUT) │ │
│ │ • 调用 Renderer 渲染 │ │
│ └─────────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────────────────────────┐ │
│ │ SimpleRenderer │ │
│ │ • EGL/Surface 管理 │ │
│ │ • OpenGL ES 渲染 │ │
│ │ • Shader 编译 │ │
│ └─────────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────────────────────────┐ │
│ │ SurfaceFlinger (Android 系统) │ │
│ │ • 合成多个窗口 │ │
│ │ • 输出到物理显示屏 │ │
│ └─────────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────────────────────────┐ │
│ │ 车机物理显示屏 │ │
│ │ ┌─────────────────────────┐ ┌─────────────────────────┐ │ │
│ │ │ 深度图窗口 │ │ 红外图窗口 │ │ │
│ │ │ (伪彩色显示) │ │ (灰度显示) │ │ │
│ │ └─────────────────────────┘ └─────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────┘
三个文件的功能分工
| 文件 | 职责 | 核心功能 | 一句话功能 |
|---|---|---|---|
| SimpleRenderer.cpp | OpenGL 渲染 | 创建 EGL 环境、编译 Shader、渲染纹理 | 使用 OpenGL ES 把图像画到车机屏幕上 |
| DisplayController.cpp | 显示控制 | 管理显示线程、深度图转伪彩色、调用渲染器 | 管理显示线程,将深度数据转成彩色图像 |
| createNativeWindow.cpp | 窗口创建 | 创建 Surface、管理窗口位置/大小 | 创建 Android 窗口,设置位置和大小 |
SimpleRenderer.cpp - OpenGL 渲染器
核心职责
将深度图/红外图数据渲染到车机屏幕上。
关键函数
c++
// 1. 初始化 EGL 环境和 OpenGL 资源
bool SimpleRenderer::init(int depthWidth, int depthHeight,
int irWidth, int irHeight) {
// 2. 渲染一帧
void SimpleRenderer::renderFrame(EGLSurface surface, GLuint textureId,
const uint8_t* data, int width, int height,
GLenum format) {
着色器原理
scss
// 顶点着色器:处理顶点坐标和纹理坐标
顶点坐标 (-1,-1) 到 (1,1) → 屏幕坐标
纹理坐标 (0,0) 到 (1,1) → 图像像素
// 片段着色器:每个像素的颜色
颜色 = 纹理采样(纹理坐标) // 直接把纹理像素输出到屏幕
DisplayController.cpp - 显示控制器
核心职责
管理显示线程,将深度数据转换为彩色图像。
关键函数
javascript
// 1. 初始化色彩查找表(将深度值映射为彩色)
void DisplayController::initColorLUT() {
// 2. 显示线程主循环
void DisplayController::displayThreadFunc() {
深度图伪彩色映射
scss
距离近 (0mm) → 蓝色
距离中等 (2.5m) → 绿色
距离远 (5m+) → 红色
车机屏幕上:近处物体显示蓝色,远处显示红色,直观易懂
createNativeWindow.cpp - 窗口管理
核心职责
创建 Android Surface 窗口,管理窗口位置和大小。
arduino
// 创建本地窗口EGLNativeWindowType
createNativeWindow(int width, int height, int x, int y) {
完整数据流
c++
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ 完整数据流 │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ToFPipelineManager::processLoop() │
│ │ │
│ │ ProcessedFrameData { │
│ │ depthData: uint16_t[307200] (0-5000mm) │
│ │ irData: uint8_t[307200] (0-255灰度) │
│ │ } │
│ ↓ │
│ DisplayController::getLatestFrame() │
│ │ │
│ ↓ │
│ DisplayController::displayThreadFunc() │
│ │ │
│ │ 深度图处理: │
│ │ uint16_t depth[307200] → 伪彩色 → uint8_t rgb[921600] │
│ │ │
│ │ 红外图处理: │
│ │ uint8_t ir[307200] → 直接使用 │
│ ↓ │
│ SimpleRenderer::showDepth(rgb, 640, 480) │
│ │ │
│ ↓ │
│ SimpleRenderer::renderFrame() │
│ │ │
│ │ ① eglMakeCurrent (绑定 Surface) │
│ │ ② glTexImage2D (上传 RGB 数据到纹理) │
│ │ ③ glDrawArrays (绘制全屏矩形) │
│ │ ④ eglSwapBuffers (显示到屏幕) │
│ ↓ │
│ SurfaceFlinger (Android 系统合成器) │
│ │ │
│ ↓ │
│ 车机显示屏 │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ 深度图 (伪彩色) │ │ 红外图 (灰度) │ │
│ │ 640x480 │ │ 640x480 │ │
│ │ 位置: (0, 0) │ │ 位置: (650, 0) │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────┘