730 lines
27 KiB
C++
730 lines
27 KiB
C++
/*
|
||
* 斑马线实时检测 — 龙芯2K0300 小车摄像头版 (含逆透视变换 IPM)
|
||
* =================================================================
|
||
*
|
||
* 【设计目的】
|
||
* 在车辆行驶过程中实时识别前方斑马线,将检测结果写入标志文件
|
||
* 供巡线主控模块 (control.cpp) 读取,做出减速/停车等安全决策。
|
||
*
|
||
* 【算法来源】
|
||
* 移植自 Python 开源项目: https://github.com/TomMao23/ZebraCrossing_Detection
|
||
* 利用斑马线的四个视觉特征做传统图像处理检测:
|
||
* (1) 梯度一致性 — 黑白交替 → 边缘方向高度集中
|
||
* (2) 等间隔 — 线条等距分布 (当前未使用)
|
||
* (3) 多根线 — 一个区域内有大量平行边缘
|
||
* (4) 宽度大 — 斑马线宽度明显大于车道线
|
||
*
|
||
* 【处理流水线 (每帧)】
|
||
*
|
||
* 摄像头 320×240 (MJPG, /dev/video0)
|
||
* ↓
|
||
* ┌── [IPM] 逆透视变换 ──────────────────────────────┐
|
||
* │ 前视图 → 鸟瞰图 (BEV, Bird's Eye View) │
|
||
* │ cv::warpPerspective(frame, bev, iM, 640×640) │
|
||
* │ 将倾斜的透视角度"拉平",使斑马线恢复矩形形状 │
|
||
* │ 矩阵 M 需针对摄像头安装角度/高度单独标定 │
|
||
* │ 标定方法:拍摄地面矩形标定板 → 四点透视变换 │
|
||
* └──────────────────────────────────────────────────┘
|
||
* ↓ if USE_IPM
|
||
* 640×640 BEV 图 (已校正透视畸变)
|
||
* ↓ cv::resize
|
||
* 400×400 正方形 BGR 图
|
||
* ↓ extractChannel(0) → 只取 B 通道 (利用蓝色通道衰减黄色减速带)
|
||
* medianBlur 5×5 → 去砖缝/椒盐噪声, 保留梯度大小
|
||
* ↓
|
||
* MORPH_OPEN 3×3 iter=4 → 多次腐蚀去窄车道线, 斑马线保留
|
||
* ↓
|
||
* MORPH_CLOSE 5×5 iter=3 → 先膨胀填斑马线缺损, 再腐蚀抑制路面箭头碎片
|
||
* ↓
|
||
* Canny(30, 90) → 双阈值边缘检测, 利用连通性合并弱边缘
|
||
* ↓
|
||
* Sobel 3×3 → 计算每个边缘像素的梯度模值 Amplitude 和方向 theta(0~90°)
|
||
* ↓ 弱边缘过滤 (Amplitude < 30 → 置零)
|
||
* 滑动窗口 100×302, 步长 50, 水平范围固定 [39, 341]
|
||
* ↓
|
||
* 每个窗口内:
|
||
* - 统计有效梯度点 (Amplitude > 0, theta ∈ [0, 70°)) 的方向直方图
|
||
* - 14 个 bin, 每 bin 5° (0°~5°, 5°~10°, ..., 65°~70°)
|
||
* - 取峰值 bin 的点数
|
||
* - 峰值 > 1500 → 判为斑马线
|
||
* ↓
|
||
* 所有阳性窗口的纵向跨度 = 最终斑马线位置 (紫色框)
|
||
* ↓
|
||
* LCD 叠加显示 + 写入 ./zebra_detected 文件
|
||
*
|
||
* 【为何需要逆透视变换 (IPM)】
|
||
* 前视摄像头拍到的斑马线是透视畸变的 (近大远小、倾斜变形)
|
||
* → 斑马线的平行特征被破坏 → 梯度一致性减弱 → 检测效果下降
|
||
* IPM 将图像变换为鸟瞰视角 → 斑马线恢复矩形/平行特征
|
||
* → Sobel 梯度方向集中在同一角度 → 直方图峰值更高 → 检测更准
|
||
*
|
||
* 【注意】IPM 矩阵必须针对实车摄像头标定!
|
||
* 当前矩阵是 Python 项目原始标定值,在 2K0300 小车上可能不适用。
|
||
* 建议先关闭 IPM 测试 (USE_IPM=0),确认检测逻辑正确后,
|
||
* 再用标定板重新标定并开启 IPM。
|
||
*
|
||
* 【为何方向限制在 0~70°】
|
||
* 排除水平停止线干扰:
|
||
* 停止线 = 横线 → Sobel dx 大 / dy → 0 → 方向接近 0°
|
||
* 斑马线 = 纵线/斜线 → 方向偏大 (30°~90°)
|
||
* 实际上 bin 0 也在统计范围但峰值不高, 主要靠阈值过滤
|
||
*
|
||
* 【为何窗口固定水平范围 [39, 341]】
|
||
* 排除道路两侧的行人道/建筑/绿化等干扰
|
||
* 400 像素宽, 窗口 302 宽, 居中左偏 39px, 右侧留 59px
|
||
*
|
||
* 【为何单独用蓝色通道而非灰度图】
|
||
* BGR 三通道中:
|
||
* B 通道 = 蓝色分量
|
||
* 黄色减速带 ≈ R+G (无蓝色) → B 通道响应 ≈ 0 → 完全消除减速带边缘
|
||
* 白色斑马线 ≈ R+G+B 均等 → B 通道正常响应 → 斑马线边缘保留
|
||
* 如果用灰度 (0.299R + 0.587G + 0.114B), 黄色减速带会产生强边缘
|
||
*
|
||
* 【为何用中值滤波而非高斯/均值滤波】
|
||
* 中值滤波最大程度保留原始梯度大小 (不模糊边缘), 同时完美消灭人行道砖缝纹理
|
||
* 高斯/均值会平滑边缘 → 梯度幅度衰减 → 影响判定阈值
|
||
*
|
||
* 【为何先开运算再闭运算】
|
||
* 开运算 (先 Erosion 后 Dilation):
|
||
* iter=4 次连续腐蚀 → 窄车道线被彻底腐蚀消失
|
||
* 后续膨胀 → 恢复剩余斑马线的尺寸
|
||
* 等效: 保留 "宽度 > 某阈值" 的线条 (斑马线),去除 "窄的" 线条 (车道线)
|
||
* 闭运算 (先 Dilation 后 Erosion):
|
||
* 先膨胀 → 填补斑马线内部的微小缺损/裂缝
|
||
* 后腐蚀 → 恢复原始尺寸,同时消除箭头碎片的虚假边缘
|
||
*
|
||
* 【为何用 Canny 而非直接对 Sobel 结果判定】
|
||
* (1) Canny 双阈值 + 连通性分析 → 孤立噪点被抑制, 只有真正连续的边缘通过
|
||
* (2) Canny 使一条边缘只保留最细的 1px 脊线 → 斑马线 "多根线" 特征更突出
|
||
* (直接 Sobel 一根粗边缘产生 N 个梯度点 → 降低了线数的区分度)
|
||
* (3) Canny 两个阈值让调参范围更大, 比单梯度阈值更鲁棒
|
||
*
|
||
* 【2K0300 算力评估 (400×400 分辨率)】
|
||
* IPM warpPerspective: O(640×640×常数) ≈ 2.5M
|
||
* 蓝色通道提取: O(160K) ≈ 忽略
|
||
* medianBlur 5×5: O(160K × 25) ≈ 4M
|
||
* morphOpen ×4: O(160K × 9 × 8) ≈ 11.5M
|
||
* morphClose ×3: O(160K × 25 × 6) ≈ 24M
|
||
* Canny: O(160K × 常数) ≈ 几M
|
||
* Sobel ×2: O(160K × 9 × 2) ≈ 2.9M
|
||
* 梯度逐像素: O(160K × 10) ≈ 1.6M
|
||
* 滑动窗口 (7窗): O(7 × 30200) ≈ 0.2M
|
||
* ---------------------------------------------------
|
||
* 总计约 50~55M 操作/帧, 2K0300 0.43 GOPS → 理论 7~10 FPS
|
||
* 优化: 关闭 IPM 可节省 ~2.5M/帧; 增大 WIN_STEP 减少窗口数
|
||
*
|
||
* 用法:
|
||
* ./zebra_demo 带 LCD 显示 + 检测 (含 IPM)
|
||
* ./zebra_demo --no-display 无头模式 (只写状态文件, 省算力)
|
||
* ./zebra_demo --no-ipm 关闭逆透视变换 (使用前视图)
|
||
*
|
||
* 跨模块通信:
|
||
* 写入 ./zebra_detected: "1" = 检测到斑马线, "0" = 未检测到
|
||
* 控制模块 (control.cpp) 可用 readFlag("./zebra_detected") 读取
|
||
* 仅在状态变化时写入, 减少文件 I/O
|
||
*
|
||
* 退出: 按 Ctrl+C (SIGINT) 或 kill (SIGTERM) 优雅清理资源
|
||
*/
|
||
|
||
#include <opencv2/opencv.hpp>
|
||
#include <iostream>
|
||
#include <vector>
|
||
#include <cmath>
|
||
#include <fstream>
|
||
#include <csignal>
|
||
#include <atomic>
|
||
|
||
// Linux 特定: 帧缓冲屏 + mmap + ioctl
|
||
#include <fcntl.h>
|
||
#include <unistd.h>
|
||
#include <sys/mman.h>
|
||
#include <sys/ioctl.h>
|
||
#include <linux/fb.h>
|
||
|
||
// ============================================================
|
||
// 可调参数 — 与 Python 版算法完全对齐
|
||
// ============================================================
|
||
|
||
// --- 逆透视变换 (IPM) ---
|
||
// 是否启用 IPM: 0=关闭(使用前视图), 1=开启(使用鸟瞰图)
|
||
// 启用前必须用标定板重新标定矩阵 M, 否则效果可能比关闭更差!
|
||
#define USE_IPM 1
|
||
|
||
// IPM 输出鸟瞰图尺寸 (与 Python 版 gd.py 一致)
|
||
const int IPM_SIZE = 640;
|
||
|
||
// --- 滑动窗口 ---
|
||
const int WIN_H = 100;
|
||
const int WIN_W = 302;
|
||
const int WIN_STEP = 50;
|
||
const int WIN_COL_L = 39;
|
||
const int WIN_COL_R = 341;
|
||
|
||
// --- 处理分辨率 ---
|
||
const int PROC_SIZE = 400;
|
||
|
||
// --- 梯度过滤 ---
|
||
const float GRAD_THRESHOLD = 30.0f;
|
||
|
||
// --- 方向直方图 ---
|
||
const int HIST_BIN_SIZE = 5;
|
||
const int HIST_ANGLE_MAX = 70;
|
||
const int HIST_BIN_COUNT = 14;
|
||
|
||
// --- 判定阈值 ---
|
||
const int ZEBRA_THRESHOLD = 1500;
|
||
|
||
// --- 摄像头 ---
|
||
const int CAM_WIDTH = 320;
|
||
const int CAM_HEIGHT = 240;
|
||
|
||
// ============================================================
|
||
// IPM 逆透视变换矩阵
|
||
//
|
||
// Python 源码中的变换逻辑:
|
||
// M = 原图 → BEV 的 3×3 透视变换矩阵
|
||
// iM = M.inv() = BEV → 原图的逆矩阵
|
||
//
|
||
// xy = 640×640 BEV 坐标网格 (px, py)
|
||
// ixy = perspectiveTransform(xy, iM) → 每个 BEV 像素在原图中的坐标
|
||
// remap(frame, mapx, mapy) → 采样原图得到 BEV 图像
|
||
//
|
||
// C++ 等价实现:
|
||
// iM = M.inv()
|
||
// warpPerspective(frame, bev, iM, Size(640,640), INTER_LINEAR)
|
||
//
|
||
// warpPerspective 的矩阵含义是 dst→src 映射, 恰好就是 iM
|
||
//
|
||
// 【标定方法】
|
||
// 1. 在小车前方地面放置一个已知尺寸的矩形标定板 (如 A4 纸或棋盘格)
|
||
// 2. 拍摄一张包含整个矩形的前视图
|
||
// 3. 获取矩形的 4 个角点在原图中的像素坐标 pts_src
|
||
// 4. 在 BEV 中定义矩形应有的 4 个角点坐标 pts_dst
|
||
// 5. M = cv::getPerspectiveTransform(pts_src, pts_dst)
|
||
// 6. 将 M 填入下方矩阵中
|
||
//
|
||
// 当前矩阵是 Python 项目的原始标定值,需替换为实车标定值!
|
||
// ============================================================
|
||
|
||
// Python 原始矩阵 M (前视图 → BEV), 来源: gd.py:255-257
|
||
// 元素按 (row, col) = (0,0) (0,1) (0,2)
|
||
// (1,0) (1,1) (1,2)
|
||
// (2,0) (2,1) (2,2)
|
||
static const double g_M_data[3][3] = {
|
||
{-1.86073726e-01, -5.02678929e-01, 4.72322899e+02},
|
||
{-1.39150388e-02, -1.50260445e+00, 1.00507430e+03},
|
||
{-1.77785988e-05, -1.65517173e-03, 1.00000000e+00}
|
||
};
|
||
|
||
/**
|
||
* 计算逆矩阵 (BEV → 前视图), 用于 warpPerspective
|
||
* 在启动时调用一次, 结果全局缓存
|
||
*/
|
||
static cv::Mat build_ipm_inverse_matrix()
|
||
{
|
||
cv::Mat M(3, 3, CV_64F);
|
||
for (int r = 0; r < 3; ++r)
|
||
for (int c = 0; c < 3; ++c)
|
||
M.at<double>(r, c) = g_M_data[r][c];
|
||
|
||
// iM = M.inv(): BEV 坐标 → 原始图像坐标
|
||
// warpPerspective 接受 dst→src 映射, 即 iM
|
||
cv::Mat iM = M.inv();
|
||
return iM;
|
||
}
|
||
|
||
// 全局变量: 在 main() 中计算一次, 之后每帧复用
|
||
static cv::Mat g_iM;
|
||
|
||
// ============================================================
|
||
// 全局状态 — 优雅退出机制
|
||
// ============================================================
|
||
|
||
static std::atomic<bool> g_running{true};
|
||
|
||
static void signal_handler(int)
|
||
{
|
||
g_running.store(false);
|
||
}
|
||
|
||
// ============================================================
|
||
// apply_ipm — 逆透视变换: 前视图 → 鸟瞰图 (BEV)
|
||
//
|
||
// 输入: 原始摄像头帧 (320×240 或任意分辨率)
|
||
// 预计算的逆矩阵 g_iM (BEV→原图)
|
||
// 输出: 640×640 鸟瞰图 (消除透视畸变, 斑马线恢复矩形)
|
||
//
|
||
// 等价于 Python 版:
|
||
// remap(frame, mapx, mapy, INTER_LINEAR) // 预计算映射表
|
||
//
|
||
// 为什么选 640×640 输出?
|
||
// 1. BEV 鸟瞰需要比原图更大的空间来容纳"拉平"后的路面
|
||
// 2. 640 是 2 的幂次方的近似值, 对 resize 和后续操作友好
|
||
// 3. Python 版使用此尺寸, 算法参数基于此调优
|
||
//
|
||
// 如果不开启 IPM, 此函数会被跳过, 直接用前视图进入处理管线
|
||
// ============================================================
|
||
static cv::Mat apply_ipm(const cv::Mat &frame)
|
||
{
|
||
cv::Mat bev;
|
||
cv::warpPerspective(frame, bev, g_iM,
|
||
cv::Size(IPM_SIZE, IPM_SIZE),
|
||
cv::INTER_LINEAR);
|
||
// warpPerspective 使用 INTER_LINEAR 双线性插值,
|
||
// 比 INTER_NEAREST 最近邻更平滑, 避免产生锯齿状伪边缘
|
||
return bev;
|
||
}
|
||
|
||
// ============================================================
|
||
// preprocess — 图像预处理 (蓝色通道 + 滤波 + 形态学)
|
||
//
|
||
// 输入: BGR 彩色图 (400×400)
|
||
// 输出: 预处理后的单通道图 (实际是蓝色通道处理结果)
|
||
//
|
||
// 五步处理:
|
||
// [1] extractChannel(bgr, blue, 0)
|
||
// 取出 B 通道 — 关键设计!
|
||
// 黄色减速带在 B 通道中亮度极低 (黄色≈R+G, 无B)
|
||
// 白色斑马线在 B 通道中亮度正常 (白色≈R=G=B)
|
||
// → B 通道天然过滤减速带, 保留斑马线
|
||
//
|
||
// [2] medianBlur(blue, 5)
|
||
// 中值滤波核大小=5
|
||
// 替代像素值为邻域中值 → 椒盐噪声/砖缝等独立噪点直接抹除
|
||
// 与均值滤波不同: 中值保持边缘处的实际梯度值不衰减
|
||
//
|
||
// [3] morphologyEx(MORPH_OPEN, 3×3, iter=4)
|
||
// 开运算 = Erode ×4 → Dilate ×4
|
||
// 3×3 矩形核较小, 但 4 次迭代累积效果强:
|
||
// - 窄车道线 (宽度 < 8px) → 在连续腐蚀中被完全消除
|
||
// - 宽斑马线 (宽度 >> 8px) → 腐蚀后仍有残留 → 膨胀恢复
|
||
// 本质: 保留"粗线条", 删除"细线条"
|
||
//
|
||
// [4] morphologyEx(MORPH_CLOSE, 5×5, iter=3)
|
||
// 闭运算 = Dilate ×3 → Erode ×3
|
||
// 5×5 较大核, 3 次迭代:
|
||
// - 先膨胀填平斑马线内部的小缺口/裂缝 (缺损斑马线恢复)
|
||
// - 后腐蚀恢复原始尺寸, 同时消除路面箭头碎片
|
||
// 注意: 如果闭运算过强, 会把相邻的细斑马线粘成一片 → iter=3 是经验平衡点
|
||
// ============================================================
|
||
static cv::Mat preprocess(const cv::Mat &bgr)
|
||
{
|
||
cv::Mat blue;
|
||
|
||
// [1] 提取 B 通道 (蓝底色 + 减弱黄减速带)
|
||
cv::extractChannel(bgr, blue, 0);
|
||
|
||
// [2] 中值滤波 5×5 → 去砖缝纹理/椒盐噪声/虚假小梯度点
|
||
cv::medianBlur(blue, blue, 5);
|
||
|
||
// [3] 开运算: 3×3 矩形核, 4 次迭代 → 去除细窄车道线
|
||
cv::Mat kernel_open = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
|
||
cv::morphologyEx(blue, blue, cv::MORPH_OPEN, kernel_open, cv::Point(-1, -1), 4);
|
||
|
||
// [4] 闭运算: 5×5 矩形核, 3 次迭代 → 填补缺损 + 抑制箭头碎片
|
||
cv::Mat kernel_close = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5, 5));
|
||
cv::morphologyEx(blue, blue, cv::MORPH_CLOSE, kernel_close, cv::Point(-1, -1), 3);
|
||
|
||
return blue;
|
||
}
|
||
|
||
// ============================================================
|
||
// compute_gradient — Sobel 边缘梯度模值和方向
|
||
//
|
||
// 输入: Canny 边缘检测结果 (单通道, 像素值 0 或 255)
|
||
// 输出: Amplitude — 梯度模值 (浮点, 弱边缘 < 30 置零)
|
||
// theta — 梯度方向 0~90° (浮点)
|
||
//
|
||
// 方向定义:
|
||
// 对每个像素 (r, c) 计算 Sobel 梯度:
|
||
// gx = ∂I/∂c → 水平方向的变化率
|
||
// gy = ∂I/∂r → 垂直方向的变化率
|
||
//
|
||
// Amplitude = sqrt(gx² + gy²) → 边缘强度
|
||
// theta = atan2(|gy|, |gx|) → 边缘方向 (0~90°)
|
||
//
|
||
// 其中 atan2 取 abs 意味着不区分 "从左到右" 和 "从右到左" 的边缘,
|
||
// 也不区分 "白→黑" 和 "黑→白" 的方向, 统一归到 0~90°。
|
||
// 这对斑马线检测是合适的 — 我们只关心边缘的方向一致性, 不关心极性。
|
||
//
|
||
// 方向参考:
|
||
// 0° = 纯水平边缘 (dy→0, dx 大)
|
||
// 45° = 135° 角斜线
|
||
// 90° = 纯垂直边缘 (dx→0, dy 大)
|
||
// 斑马线纵线 ≈ 70~90°, 斜线 ≈ 30~60°
|
||
//
|
||
// 弱边缘过滤:
|
||
// Canny 之后仍有少量噪声梯度 (<30)
|
||
// 直接置零 → 后续直方图统计不参与
|
||
// ============================================================
|
||
static void compute_gradient(const cv::Mat &canny,
|
||
cv::Mat &litude, cv::Mat &theta)
|
||
{
|
||
cv::Mat dx, dy;
|
||
|
||
// Sobel 算子 3×3:
|
||
// dx = [-1 0 1; -2 0 2; -1 0 1] (x方向导数)
|
||
// dy = [-1 -2 -1; 0 0 0; 1 2 1] (y方向导数)
|
||
// CV_32F = 32 位浮点, 保留梯度方向亚度数精度
|
||
cv::Sobel(canny, dx, CV_32F, 1, 0, 3);
|
||
cv::Sobel(canny, dy, CV_32F, 0, 1, 3);
|
||
|
||
// 初始化输出矩阵为全零
|
||
amplitude = cv::Mat::zeros(canny.size(), CV_32F);
|
||
theta = cv::Mat::zeros(canny.size(), CV_32F);
|
||
|
||
// 逐像素计算模值和方向
|
||
// 逐像素循环对于 400×400 = 160K 像素来说不是瓶颈,
|
||
// 瓶颈在 medianBlur 和 morphologyEx 的核运算上
|
||
for (int r = 0; r < canny.rows; ++r)
|
||
{
|
||
for (int c = 0; c < canny.cols; ++c)
|
||
{
|
||
float gx = dx.at<float>(r, c);
|
||
float gy = dy.at<float>(r, c);
|
||
|
||
float amp = std::sqrt(gx * gx + gy * gy);
|
||
float ang = std::atan2(std::abs(gy), std::abs(gx) + 1e-10f)
|
||
* 180.0f / static_cast<float>(CV_PI);
|
||
|
||
// 弱边缘置零 (不参与后续方向统计)
|
||
amplitude.at<float>(r, c) = (amp >= GRAD_THRESHOLD) ? amp : 0.0f;
|
||
theta.at<float>(r, c) = ang;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// is_zebra_window — 对单个滑动窗口做斑马线分类
|
||
//
|
||
// 输入: 窗口内的梯度模值图 (amp_win) 和方向图 (ang_win)
|
||
// 输出: true = 斑马线, false = 背景
|
||
//
|
||
// 判定逻辑:
|
||
// [1] 扫描窗口内每个像素
|
||
// - 跳过 amp ≤ 0 的像素 (弱边缘/噪声/平滑区域)
|
||
// - 跳过 ang ≥ 70° 的像素 (方向在统计范围外)
|
||
//
|
||
// [2] 统计方向直方图 hist[0..13]
|
||
// hist[k] = 方向在 [k*5, k*5+5) 的有效梯度点数量
|
||
// e.g. hist[0] = 方向 0~4.999° 的点数
|
||
//
|
||
// [3] 取峰值 bin 的点数 = peak
|
||
// peak 反映了窗口内 "某个特定方向上" 有多少条平行的边缘线
|
||
//
|
||
// [4] peak > ZEBRA_THRESHOLD (1500) → 斑马线
|
||
// 斑马线 = 同一方向上大量平行边缘 → peak 极高
|
||
// 普通路面 = 边缘方向杂乱 → 峰值被分散到不同 bin → 各 bin 点数低
|
||
//
|
||
// 在 IPM 开启时, 鸟瞰图将斑马线恢复为垂直矩形
|
||
// → 所有边缘方向集中在 90° 附近 → hist[13] 极高 → 检测更灵敏
|
||
// ============================================================
|
||
static bool is_zebra_window(const cv::Mat &_win, const cv::Mat &ang_win)
|
||
{
|
||
int hist[HIST_BIN_COUNT] = {0};
|
||
|
||
for (int r = 0; r < amp_win.rows; ++r)
|
||
{
|
||
for (int c = 0; c < amp_win.cols; ++c)
|
||
{
|
||
float amp = amp_win.at<float>(r, c);
|
||
if (amp <= 0.0f)
|
||
continue;
|
||
|
||
float ang = ang_win.at<float>(r, c);
|
||
if (ang < 0.0f || ang >= HIST_ANGLE_MAX)
|
||
continue;
|
||
|
||
int bin = static_cast<int>(ang) / HIST_BIN_SIZE;
|
||
if (bin >= 0 && bin < HIST_BIN_COUNT)
|
||
hist[bin]++;
|
||
}
|
||
}
|
||
|
||
int peak = 0;
|
||
for (int i = 0; i < HIST_BIN_COUNT; ++i)
|
||
if (hist[i] > peak)
|
||
peak = hist[i];
|
||
|
||
return peak > ZEBRA_THRESHOLD;
|
||
}
|
||
|
||
// ============================================================
|
||
// 帧缓冲显示 — 将 OpenCV BGR Mat 转为 RGB565 格式直写 /dev/fb0
|
||
//
|
||
// 硬件: SPI LCD 通过 /dev/fb0 (RGB565 格式)
|
||
//
|
||
// RGB565 编码:
|
||
// 16 位: [R4 R3 R2 R1 R0] [G5 G4 G3] [G2 G1 G0] [B4 B3 B2 B1 B0]
|
||
// R 取 高 5 位 (>>3, &0xF8)
|
||
// G 取 高 6 位 (>>2, &0xFC)
|
||
// B 取 高 5 位 (>>3)
|
||
// ============================================================
|
||
|
||
static uint16_t convertRGBToRGB565(uint8_t r, uint8_t g, uint8_t b)
|
||
{
|
||
return ((r & 0xF8) << 8) |
|
||
((g & 0xFC) << 3) |
|
||
(b >> 3);
|
||
}
|
||
|
||
static void display_on_fb(const cv::Mat &bgr, int /* fb_fd */,
|
||
uint16_t *fb_buf, int scr_w, int scr_h)
|
||
{
|
||
cv::Mat display;
|
||
cv::resize(bgr, display, cv::Size(scr_w, scr_h));
|
||
|
||
for (int y = 0; y < scr_h; ++y)
|
||
{
|
||
for (int x = 0; x < scr_w; ++x)
|
||
{
|
||
cv::Vec3b pixel = display.at<cv::Vec3b>(y, x);
|
||
fb_buf[y * scr_w + x] = convertRGBToRGB565(
|
||
pixel[2], pixel[1], pixel[0]);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// main — 初始化 + 主检测循环 + 清理
|
||
// ============================================================
|
||
int main(int argc, char *argv[])
|
||
{
|
||
// ---- 命令行参数解析 ----
|
||
bool no_display = false;
|
||
bool use_ipm = (USE_IPM != 0); // 默认跟随编译宏
|
||
|
||
for (int i = 1; i < argc; ++i)
|
||
{
|
||
if (std::strcmp(argv[i], "--no-display") == 0)
|
||
no_display = true;
|
||
else if (std::strcmp(argv[i], "--no-ipm") == 0)
|
||
use_ipm = false;
|
||
}
|
||
|
||
// ---- 注册信号处理 ----
|
||
std::signal(SIGINT, signal_handler);
|
||
std::signal(SIGTERM, signal_handler);
|
||
|
||
// ---- 预计算 IPM 逆矩阵 (启用时) ----
|
||
if (use_ipm)
|
||
{
|
||
g_iM = build_ipm_inverse_matrix();
|
||
printf("IPM 逆透视变换: 已启用 (%dx%d BEV)\n", IPM_SIZE, IPM_SIZE);
|
||
printf(" 注意: 矩阵未针对实车标定, 效果可能不佳\n");
|
||
}
|
||
else
|
||
{
|
||
printf("IPM 逆透视变换: 已关闭 (使用前视图)\n");
|
||
}
|
||
|
||
// ========================================================
|
||
// 第 1 步: 打开 USB 摄像头
|
||
// ========================================================
|
||
cv::VideoCapture cap;
|
||
|
||
cap.open(0, cv::CAP_V4L2);
|
||
if (!cap.isOpened())
|
||
{
|
||
cap.open(0);
|
||
if (!cap.isOpened())
|
||
{
|
||
std::cerr << "无法打开摄像头 /dev/video0" << std::endl;
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
cap.set(cv::CAP_PROP_FRAME_WIDTH, CAM_WIDTH);
|
||
cap.set(cv::CAP_PROP_FRAME_HEIGHT, CAM_HEIGHT);
|
||
cap.set(cv::CAP_PROP_FOURCC,
|
||
cv::VideoWriter::fourcc('M', 'J', 'P', 'G'));
|
||
|
||
printf("摄像头已打开 %dx%d\n",
|
||
(int)cap.get(cv::CAP_PROP_FRAME_WIDTH),
|
||
(int)cap.get(cv::CAP_PROP_FRAME_HEIGHT));
|
||
|
||
// ========================================================
|
||
// 第 2 步: 打开帧缓冲 /dev/fb0 → mmap 映射到用户空间
|
||
// ========================================================
|
||
int fb_fd = -1;
|
||
uint16_t *fb_buf = nullptr;
|
||
int scr_w = 0, scr_h = 0;
|
||
|
||
if (!no_display)
|
||
{
|
||
fb_fd = open("/dev/fb0", O_RDWR);
|
||
if (fb_fd < 0)
|
||
{
|
||
std::cerr << "警告: 无法打开 /dev/fb0,切换为无头模式" << std::endl;
|
||
no_display = true;
|
||
}
|
||
else
|
||
{
|
||
struct fb_var_screeninfo vinfo;
|
||
if (ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo) < 0)
|
||
{
|
||
std::cerr << "无法获取屏幕信息" << std::endl;
|
||
close(fb_fd);
|
||
no_display = true;
|
||
}
|
||
else
|
||
{
|
||
scr_w = vinfo.xres;
|
||
scr_h = vinfo.yres;
|
||
size_t fb_size = vinfo.yres_virtual * vinfo.xres_virtual
|
||
* vinfo.bits_per_pixel / 8;
|
||
|
||
fb_buf = (uint16_t *)mmap(nullptr, fb_size,
|
||
PROT_READ | PROT_WRITE,
|
||
MAP_SHARED, fb_fd, 0);
|
||
if (fb_buf == MAP_FAILED)
|
||
{
|
||
std::cerr << "无法映射帧缓冲区" << std::endl;
|
||
close(fb_fd);
|
||
no_display = true;
|
||
}
|
||
else
|
||
{
|
||
printf("屏幕分辨率: %dx%d\n", scr_w, scr_h);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========================================================
|
||
// 第 3 步: 主循环
|
||
// ========================================================
|
||
cv::Mat frame, bev, proc, gray, canny, amplitude, theta, result;
|
||
int frame_count = 0;
|
||
int zebra_frames = 0;
|
||
bool was_zebra = false;
|
||
|
||
printf("\n开始斑马线检测 (按 Ctrl+C 退出)...\n");
|
||
|
||
while (g_running.load())
|
||
{
|
||
// ---- 3a. 摄像头采集一帧 ----
|
||
if (!cap.read(frame) || frame.empty())
|
||
{
|
||
usleep(5000);
|
||
continue;
|
||
}
|
||
|
||
// ---- 3b. [IPM] 逆透视变换: 前视图 → 鸟瞰图 ----
|
||
// 将倾斜透视拉平为俯瞰视角, 让斑马线恢复矩形平行特征
|
||
// 未启用 IPM 时直接使用原始帧
|
||
if (use_ipm)
|
||
{
|
||
bev = apply_ipm(frame);
|
||
}
|
||
else
|
||
{
|
||
bev = frame; // 使用前视图
|
||
}
|
||
|
||
// ---- 3c. 尺寸归一化: BEV/原图 → 400×400 ----
|
||
cv::resize(bev, proc, cv::Size(PROC_SIZE, PROC_SIZE));
|
||
|
||
// ---- 3d. 预处理: 蓝色通道 + 中值滤波 + 形态学 ----
|
||
gray = preprocess(proc);
|
||
|
||
// ---- 3e. Canny 边缘检测 (双阈值 30/90) ----
|
||
cv::Canny(gray, canny, 30, 90, 3);
|
||
|
||
// ---- 3f. 梯度模值与方向 ----
|
||
compute_gradient(canny, amplitude, theta);
|
||
|
||
// ---- 3g. 滑动窗口逐窗检测 ----
|
||
int img_h = proc.rows;
|
||
bool detected = false;
|
||
int z_ymin = img_h;
|
||
int z_ymax = 0;
|
||
|
||
for (int top = 0; top <= img_h - WIN_H; top += WIN_STEP)
|
||
{
|
||
cv::Rect roi(WIN_COL_L, top, WIN_W, WIN_H);
|
||
|
||
if (is_zebra_window(amplitude(roi), theta(roi)))
|
||
{
|
||
detected = true;
|
||
if (top < z_ymin) z_ymin = top;
|
||
if (top + WIN_H > z_ymax) z_ymax = top + WIN_H;
|
||
}
|
||
}
|
||
|
||
// ---- 3h. 写入状态文件 (仅在状态变化时写) ----
|
||
if (detected != was_zebra)
|
||
{
|
||
std::ofstream ofs("./zebra_detected", std::ios::trunc);
|
||
ofs << (detected ? "1" : "0");
|
||
was_zebra = detected;
|
||
}
|
||
|
||
// ---- 3i. 绘制调试叠加图 ----
|
||
result = proc.clone();
|
||
|
||
for (int top = 0; top <= img_h - WIN_H; top += WIN_STEP)
|
||
{
|
||
cv::line(result, cv::Point(WIN_COL_L, top),
|
||
cv::Point(WIN_COL_R, top),
|
||
cv::Scalar(128, 128, 128), 1);
|
||
}
|
||
|
||
if (detected)
|
||
{
|
||
cv::rectangle(result,
|
||
cv::Rect(WIN_COL_L, z_ymin, WIN_W, z_ymax - z_ymin),
|
||
cv::Scalar(255, 0, 255), 3);
|
||
}
|
||
|
||
if (detected)
|
||
{
|
||
cv::putText(result, "ZEBRA CROSSING!",
|
||
cv::Point(10, 30),
|
||
cv::FONT_HERSHEY_SIMPLEX, 0.8,
|
||
cv::Scalar(0, 0, 255), 2);
|
||
}
|
||
|
||
// ---- 3j. LCD 显示 ----
|
||
if (!no_display && fb_buf)
|
||
display_on_fb(result, fb_fd, fb_buf, scr_w, scr_h);
|
||
|
||
// ---- 3k. 统计 ----
|
||
frame_count++;
|
||
if (detected) zebra_frames++;
|
||
|
||
if (frame_count % 30 == 0)
|
||
{
|
||
printf("帧 %d | 斑马线: %s | 累计命中: %d/%d | IPM: %s\n",
|
||
frame_count,
|
||
detected ? "YES" : "NO ",
|
||
zebra_frames, frame_count,
|
||
use_ipm ? "ON" : "OFF");
|
||
}
|
||
}
|
||
|
||
// ========================================================
|
||
// 第 4 步: 清理
|
||
// ========================================================
|
||
cap.release();
|
||
|
||
if (!no_display && fb_buf && fb_buf != MAP_FAILED)
|
||
{
|
||
struct fb_var_screeninfo vinfo;
|
||
if (ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo) == 0)
|
||
{
|
||
size_t fb_size = vinfo.yres_virtual * vinfo.xres_virtual
|
||
* vinfo.bits_per_pixel / 8;
|
||
munmap(fb_buf, fb_size);
|
||
}
|
||
close(fb_fd);
|
||
}
|
||
|
||
printf("\n退出。共处理 %d 帧,命中 %d 帧 (%.1f%%), IPM: %s\n",
|
||
frame_count, zebra_frames,
|
||
frame_count > 0 ? (100.0 * zebra_frames / frame_count) : 0.0,
|
||
use_ipm ? "ON" : "OFF");
|
||
|
||
return 0;
|
||
}
|