/*
 * 斑马线实时检测 — 龙芯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 &amplitude, 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 &amp_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;
}
