/*
 * @Author: ilikara 3435193369@qq.com
 * @Date: 2025-01-04 06:50:56
 * @LastEditors: ilikara 3435193369@qq.com
 * @LastEditTime: 2025-03-13 08:15:10
 * @FilePath: /2k300_smartcar/src/image_cv.cpp
 * @Description: 视觉巡线管线 —— HSV 二值化 → 洪泛填充 → 逐行边界搜索 → EMA 滤波
 *
 * 处理分辨率: 80×60（line_tracking_width × line_tracking_height）
 * 这是 2K0300 CPU 上的最优平衡点：再大 Otsu 不稳，再小细节丢失
 */
#include "image_cv.h"

// ============================================================
// 全局图像变量
// ============================================================

cv::Mat raw_frame;
cv::Mat binarizedFrame;
cv::Mat morphologyExFrame;
cv::Mat track;

// ============================================================
// 边界线数组 — 左/右边缘和中线的逐行列坐标
//
// 坐标系: row=0 = 图像顶部(远处), row=59 = 底部(近处)
// 列坐标范围: 0 ~ line_tracking_width-1 (0~79)
// 未检测到边界 = -1
// ============================================================
std::vector<int> left_line;
std::vector<int> right_line;
std::vector<int> mid_line;

int line_tracking_width;                 // 处理宽度 = 80
int line_tracking_height;                // 处理高度 = 60

// ============================================================
// image_binerize — HSV 双通道 Otsu 二值化
//
// 输入: BGR 彩色帧 (80×60)
// 输出: 二值图，赛道区域 = 白色(255)，边界/背景 = 黑色(0)
//
// 原理:
//   蓝底赛道 → 蓝色区域饱和度高(S通道高)，色调集中(H通道100~130)
//   灰度路面 → 饱和度低，色调分散
//
//   1. BGR → HSV 色彩空间转换
//   2. H 通道 Otsu 阈值 → 分离蓝色与非蓝色
//   3. S 通道 Otsu 阈值 → 分离高饱和(蓝)与低饱和(灰)
//   4. 两通道做 bitwise_or 取并集 (THRESH_BINARY_INV 确保赛道=255)
//      即只要 H 或 S 任一判定为赛道，就标记为赛道区域
//
//   THRESH_BINARY_INV + THRESH_OTSU:
//     Otsu 自动计算最优阈值 T
//     pixel > T → 0 (黑色), pixel ≤ T → 255 (白色)
//     赛道(蓝色/高饱和)通常偏向一侧，Otsu 将其归到低值区间
//     INV 反转后赛道 = 白色(前景, 255), 背景 = 黑色(0)
// ============================================================
cv::Mat image_binerize(cv::Mat &frame)
{
    cv::Mat output;
    cv::Mat binarizedFrame;
    cv::Mat hsvImage;

    // 1. BGR → HSV：分离色调(H)、饱和度(S)、明度(V) 三个通道
    cv::cvtColor(frame, hsvImage, cv::COLOR_BGR2HSV);

    std::vector<cv::Mat> hsvChannels;
    cv::split(hsvImage, hsvChannels);
    // hsvChannels[0] = H (色调, 0~180)
    // hsvChannels[1] = S (饱和度, 0~255)
    // hsvChannels[2] = V (明度, 0~255)

    // 2. H 通道 Otsu 二值化：区分蓝色与非蓝色
    cv::threshold(hsvChannels[0], binarizedFrame,
                  0, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU);

    // 3. S 通道 Otsu 二值化：区分高饱和(蓝色赛道)与低饱和(灰色路面)
    cv::threshold(hsvChannels[1], output,
                  0, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU);

    // 4. 双通道融合：任一通道认为"非赛道"就排除
    //    bitwise_or: 两幅图同为赛道(255)才保留，否则变为背景(0)
    cv::bitwise_or(output, binarizedFrame, output);

    return output;
}

// ============================================================
// find_road — 形态学去噪 + 洪泛填充提取连通赛道区域
//
// 输入: image_binerize 的输出 (赛道=255, 背景=0)
// 输出: 仅保留与底边中点连通的赛道区域蒙版
//
// 处理步骤:
//   1. 形态学开运算 (MORPH_OPEN): 先腐蚀后膨胀，消除孤立噪点
//      核大小 2×2 十字形 — 极小核，避免吞没细弯
//   2. 在底边中心上方偏移处放置种子点
//   3. 洪泛填充 (floodFill):
//      - 从种子点出发，填充容差范围内的连通区域
//      - 填充值 = 128 (灰色)，区分于原始白色(255)
//      - loDiff/upDiff = 20: 像素值在 [108, 148] 范围内的像素被填充
//      - 邻域 8 连通
//   4. 将蒙版 mask 的 ROI 区域复制到输出图像
//      - 赛道内部 = 128 (非零)，外部 = 0
//
// 为什么要洪泛填充？
//   二值化后可能有多块白色区域（赛道 + 赛道外的反光/杂物）
//   洪泛填充确保只处理"从车底到前方连通的"那块区域
//   排除远处/边缘的伪赛道碎片
// ============================================================
cv::Mat find_road(cv::Mat &frame)
{
    // 1. 形态学开运算去噪
    //    MORPH_CROSS: 十字形结构元素，2×2
    //    MORPH_OPEN: 先腐蚀(去除小白点) 再膨胀(恢复区域尺寸)
    static cv::Mat kernel = cv::getStructuringElement(cv::MORPH_CROSS, cv::Size(2, 2));
    cv::morphologyEx(binarizedFrame, morphologyExFrame, cv::MORPH_OPEN, kernel);

    // 2. 创建洪泛填充蒙版
    //    尺寸比原图各边大 2 像素 (floodFill 要求)
    cv::Mat mask = cv::Mat::zeros(line_tracking_height + 2, line_tracking_width + 2, CV_8UC1);

    // 3. 种子点位置
    //    X = 图像水平中心 (line_tracking_width/2)
    //    Y = 距底部 10 行 (line_tracking_height-10)
    //    假设车在赛道中央附近，底部中心一定是赛道上
    cv::Point seedPoint(line_tracking_width / 2, line_tracking_height - 10);

    // 4. 在种子点位置画一个白色实心圆，确保种子点落在赛道区域内
    //    避免因二值化偶尔在种子位置为黑色导致洪泛失败
    cv::circle(morphologyExFrame, seedPoint, 10, 255, -1);

    // 5. 洪泛填充参数
    cv::Scalar newVal(128);               // 填充值 = 128 (标记为赛道内部)
    cv::Scalar loDiff = cv::Scalar(20);   // 下界: 当前像素值 - 20 = 108
    cv::Scalar upDiff = cv::Scalar(20);   // 上界: 当前像素值 + 20 = 148

    // 6. 执行洪泛填充
    //    从种子点向 8 邻域扩散，填充像素值在 [108, 148] 之间的所有连通像素
    //    填充结果直接写入 morphologyExFrame（in-place）
    //    蒙版 mask 记录被填充的像素位置
    cv::floodFill(morphologyExFrame, mask, seedPoint, newVal,
                  nullptr, loDiff, upDiff, 8);

    // 7. 从蒙版提取赛道区域
    //    mask 的外扩边框(±1) 用于 floodFill 的内部计算，实际区域在(1,1)起
    //    ROI 裁掉边框后即为赛道主体蒙版
    cv::Mat outputImage = cv::Mat::zeros(line_tracking_height, line_tracking_width, CV_8UC1);
    mask(cv::Rect(1, 1, line_tracking_width, line_tracking_height)).copyTo(outputImage);

    return outputImage;
}

// ============================================================
// image_main — 视觉巡线主函数，每帧调用一次
//
// 完整处理流水线：
//   raw_frame (320×240) → resize (80×60) → HSV二值化 → 形态学+洪泛
//   → 逐行最长连续段搜索 → 中线计算 → EMA 自底向上滤波 →
//   输出 left_line[60], right_line[60], mid_line[60]
//
// 注意：坐标原点在左上角
//   - row=0  = 图像顶部（远处）
//   - row=59 = 图像底部（近处，车前方）
//   - 滤波从底部(row=59)往顶部(row=0)递推
// ============================================================
void image_main()
{
    cv::Mat resizedFrame;

    // ── 1. 降采样 ──────────────────────────────────────
    // 320×240 → 80×60，大幅缩减计算量
    cv::resize(raw_frame, resizedFrame,
               cv::Size(line_tracking_width, line_tracking_height));

    // ── 2. HSV 双通道 Otsu 二值化 ─────────────────────
    // 赛道区域 = 白色(255)，背景/边界 = 黑色(0)
    binarizedFrame = image_binerize(resizedFrame);

    // ── 3. 洪泛填充提取赛道主体 ────────────────────────
    // 只保留与车底中点连通的赛道区域，排除伪赛道碎片
    // 输出: track = 赛道内部(128) / 赛道外部(0)
    track = find_road(binarizedFrame);

    // ── 4. 初始化边界线数组 ────────────────────────────
    left_line.clear();
    right_line.clear();
    mid_line.clear();

    left_line.resize(line_tracking_height, -1);
    right_line.resize(line_tracking_height, -1);
    mid_line.resize(line_tracking_height, -1);

    // ── 5. 逐行最长连续段搜索 ──────────────────────────
    //
    // 将 track 的 uchar* 数据解释为二维数组 IMG[row][col]
    // track 中 赛道内部 = 128(非零), 外部 = 0
    // 对每一行: 找到最长的连续非零段
    //   → 段的起点 = left_line[row]
    //   → 段的终点 = right_line[row]
    //   → 无赛道段的行 = -1
    //
    // 这在低分辨率下比八邻域边界跟踪更高效：
    //   80×60 只有 4800 像素，逐行扫描 O(W×H) 已足够快
    uchar(*IMG)[line_tracking_width] =
        reinterpret_cast<uchar(*)[line_tracking_width]>(track.data);

    for (int i = 0; i < line_tracking_height; ++i)  // i = row
    {
        int max_start = -1;       // 最长连续段的起始列
        int max_end = -1;         // 最长连续段的结束列
        int current_start = -1;   // 当前扫描中的段起始列
        int current_length = 0;   // 当前扫描中的段长度
        int max_length = 0;       // 已记录的最长段长度

        for (int j = 0; j < line_tracking_width; ++j)  // j = col
        {
            if (IMG[i][j])  // 非零 = 赛道内部像素
            {
                // 赛道段的第一个像素: 记录起点
                if (current_length == 0)
                {
                    current_start = j;
                    current_length = 1;
                }
                else
                {
                    // 赛道段延续: 长度+1
                    current_length++;
                }

                // 更新最长记录 (≥ 而非 >, 取最后一个最长段，靠右)
                if (current_length >= max_length)
                {
                    max_length = current_length;
                    max_start = current_start;
                    max_end = j;
                }
            }
            else  // 零值 = 非赛道
            {
                // 赛道段结束: 重置
                current_length = 0;
                current_start = -1;
            }
        }

        // 记录该行的左右边界
        if (max_length > 0)
        {
            left_line[i]  = max_start;   // 最长白段的左端点
            right_line[i] = max_end;     // 最长白段的右端点
        }
        else
        {
            // 该行无赛道 → 后续由中线补全逻辑处理
            left_line[i]  = -1;
            right_line[i] = -1;
        }
    }

    // ── 6. 中线计算 + 丢线补全 ─────
    for (int row = line_tracking_height - 1; row >= 10; --row)
    {
        // ── 6a. 丢线补全 ──────────────────────────────
        if (left_line[row] == -1 && right_line[row] == -1)
        {
            // 当前行完全丢线: 用下行(row+1)的中线来虚拟补线
            mid_line[row] = mid_line[row + 1];

            if (mid_line[row] > line_tracking_width / 2)
            {
                // 中线偏右 → 实际赛道在图像右半区
                // 虚拟右边界 = 图像右边缘, 左边界 = 下行中线位置
                right_line[row] = line_tracking_width - 1;
                left_line[row]  = mid_line[row + 1];
            }
            else
            {
                // 中线偏左 → 实际赛道在图像左半区
                // 虚拟左边界 = 图像左边缘, 右边界 = 下行中线位置
                left_line[row]  = 0;
                right_line[row] = mid_line[row + 1];
            }
        }
        else
        {
            // 正常行: 中线 = 左右边界中点
            mid_line[row] = (left_line[row] + right_line[row]) / 2;
        }
    }
}
