
专业版
微缩电老鼠
1.1k
0
0
0
简介
使用STM32单片机,实现快速循迹效果。包含摄像头循迹,惯性导航等功能。
简介:使用STM32单片机,实现快速循迹效果。包含摄像头循迹,惯性导航等功能。开源协议
:GPL 3.0
(未经作者授权,禁止转载)创建时间:2025-09-14 21:12:12更新时间:2025-12-31 17:28:37
描述
以下为初步设计
一、项目简介
使用STM32单片机,实现快速循迹效果。包含摄像头循迹,惯性导航等功能。
二、硬件设计
主控STM32G473VET6
MT9V034 摄像头
N630电机驱动
IMU660RB陀螺仪
三、项目功能
小车可以实现自主循迹导航,通过直角弯,十字路口,环岛,虚线等复杂元素。 其核心是大津法阈值计算和图像二值化
示例代码
#define IMG_WIDTH 320 // 图像宽度(根据实际摄像头调整)
#define IMG_HEIGHT 240 // 图像高度
#define PIXEL_TOTAL (IMG_WIDTH * IMG_HEIGHT) // 总像素数
#define THRESHOLD_MAX 255 // 灰度阈值上限
#define THRESHOLD_MIN 0 // 灰度阈值下限
#define DEFAULT_OSTU_VAL 128 // OSTU阈值兜底值
#define CLOSE_THRESH_OFFSET 5 // 近景阈值偏移(+)
#define MID_THRESH_OFFSET 10 // 中景阈值偏移(-)
#define FAR_THRESH_OFFSET 20 // 远景阈值偏移(-)
uint8_t gray_img[IMG_HEIGHT][IMG_WIDTH]; // 原始灰度图像缓存
uint8_t binary_img[IMG_HEIGHT][IMG_WIDTH]; // 二值化图像缓存
uint8_t layer_thresh[3]; // 分层阈值:[0]近景 [1]中景 [2]远景
// 1. 大津法(OSTU)计算全局最优阈值
static uint8_t calc_ostu_threshold(const uint8_t *gray_data, uint32_t pixel_num);
// 2. 计算近/中/远景分层阈值
static void calc_layer_threshold(uint8_t global_ostu);
// 3. 分层阈值图像二值化(近/中/远景差异化处理)
static void img_layer_binary(const uint8_t *gray_data, uint8_t *binary_data);
// 4. 虚线补全(修复断裂的线路像素)
static void dashed_line_makeup(uint8_t *binary_data);
void img_preprocess_core(const uint8_t *input_gray, uint8_t *output_binary)
{
// 1. 拷贝输入灰度图到本地缓存
memcpy(gray_img, input_gray, sizeof(gray_img));
// 2. 大津法计算全局最优阈值
uint8_t global_ostu = calc_ostu_threshold((uint8_t*)gray_img, PIXEL_TOTAL);
// 3. 计算近/中/远景分层阈值
calc_layer_threshold(global_ostu);
// 4. 分层阈值二值化
img_layer_binary((uint8_t*)gray_img, (uint8_t*)binary_img);
// 5. 虚线补全(修复断裂线路)
dashed_line_makeup((uint8_t*)binary_img);
// 6. 输出二值化结果
memcpy(output_binary, binary_img, sizeof(binary_img));
}
/**
* @brief 大津法(OSTU)计算全局最优阈值
* 原理:遍历所有灰度级,找到最大化类间方差的阈值
* @param gray_data 灰度图像数据(一维)
* @param pixel_num 总像素数
* @return 最优二值化阈值
*/
static uint8_t calc_ostu_threshold(const uint8_t *gray_data, uint32_t pixel_num)
{
uint32_t hist[256] = {0}; // 灰度直方图(0-255)
uint32_t total_pixel = 0; // 总像素数(校验用)
float sum_gray = 0.0f; // 灰度总和
float sum_background = 0.0f;// 背景灰度和
float max_variance = 0.0f; // 最大类间方差
uint8_t best_thresh = DEFAULT_OSTU_VAL;
// 步骤1:统计灰度直方图
for (uint32_t i = 0; i < pixel_num; i++)
{
uint8_t gray = gray_data[i];
hist[gray]++;
sum_gray += (float)gray * hist[gray]; // 累计灰度总和
total_pixel++;
}
// 步骤2:遍历所有可能的阈值,计算类间方差
for (int thresh = 0; thresh < 256; thresh++)
{
// 背景像素数(灰度≤thresh)、前景像素数(灰度>thresh)
uint32_t cnt_bg = 0, cnt_fg = 0;
for (int g = 0; g <= thresh; g++) cnt_bg += hist[g];
cnt_fg = total_pixel - cnt_bg;
// 跳过无背景/无前景的情况
if (cnt_bg == 0 || cnt_fg == 0) continue;
// 累计背景灰度和
sum_background += (float)thresh * hist[thresh];
// 背景/前景灰度均值
float mean_bg = sum_background / cnt_bg;
float mean_fg = (sum_gray - sum_background) / cnt_fg;
// 计算类间方差(核心公式)
float variance = (float)cnt_bg * (float)cnt_fg * (mean_bg - mean_fg) * (mean_bg - mean_fg);
// 更新最优阈值
if (variance > max_variance)
{
max_variance = variance;
best_thresh = (uint8_t)thresh;
}
}
// 阈值合法性校验
if (best_thresh < THRESHOLD_MIN || best_thresh > THRESHOLD_MAX)
{
best_thresh = DEFAULT_OSTU_VAL;
}
printf("OSTU计算全局最优阈值:%d\n", best_thresh);
return best_thresh;
}
/**
* @brief 计算近/中/远景分层阈值
* 逻辑:基于全局OSTU阈值,按不同偏移量生成分层阈值
* @param global_ostu 全局最优阈值
*/
static void calc_layer_threshold(uint8_t global_ostu)
{
// 近景:全局阈值+小偏移(近距离光照稳定,少加保证细节)
layer_thresh[0] = (global_ostu + CLOSE_THRESH_OFFSET) > THRESHOLD_MAX ?
THRESHOLD_MAX : (global_ostu + CLOSE_THRESH_OFFSET);
// 中景:全局阈值-中等偏移(平衡识别精度)
layer_thresh[1] = (global_ostu - MID_THRESH_OFFSET) < THRESHOLD_MIN ?
THRESHOLD_MIN : (global_ostu - MID_THRESH_OFFSET);
// 远景:全局阈值-大偏移(降低远景噪声,提升识别距离)
layer_thresh[2] = (global_ostu - FAR_THRESH_OFFSET) < THRESHOLD_MIN ?
THRESHOLD_MIN : (global_ostu - FAR_THRESH_OFFSET);
printf("分层阈值:近景=%d | 中景=%d | 远景=%d\n",
layer_thresh[0], layer_thresh[1], layer_thresh[2]);
}
/**
* @brief 分层阈值二值化
* 逻辑:将图像按行划分为近/中/远三层,每层用不同阈值二值化
* 下1/3=近景 | 中1/3=中景 | 上1/3=远景
* @param gray_data 输入灰度图
* @param binary_data 输出二值化图(255=前景/线路,0=背景)
*/
static void img_layer_binary(const uint8_t *gray_data, uint8_t *binary_data)
{
uint32_t pixel_idx = 0;
for (int y = 0; y < IMG_HEIGHT; y++)
{
uint8_t curr_thresh = 0;
// 按行划分场景层
if (y > IMG_HEIGHT * 2 / 3) // 近景(底部1/3)
curr_thresh = layer_thresh[0];
else if (y > IMG_HEIGHT / 3) // 中景(中部1/3)
curr_thresh = layer_thresh[1];
else // 远景(顶部1/3)
curr_thresh = layer_thresh[2];
// 逐像素二值化
for (int x = 0; x < IMG_WIDTH; x++)
{
// 线路(前景):灰度 < 阈值 → 255;背景 → 0
binary_data[pixel_idx] = (gray_data[pixel_idx] < curr_thresh) ? 255 : 0;
pixel_idx++;
}
}
}
/**
* @brief 虚线补全(修复断裂的线路像素)
* 核心逻辑:检测单行内"左右为前景、中间为背景"的断裂像素,填充为前景
* @param binary_data 二值化图像数据
*/
static void dashed_line_makeup(uint8_t *binary_data)
{
for (int y = 0; y < IMG_HEIGHT; y++)
{
for (int x = 1; x < IMG_WIDTH - 1; x++) // 跳过边界像素
{
uint32_t curr_idx = y * IMG_WIDTH + x;
uint32_t left_idx = curr_idx - 1; // 左邻域
uint32_t right_idx = curr_idx + 1; // 右邻域
// 判定条件:当前像素为背景,左右邻域均为前景 → 补全
if (binary_data[curr_idx] == 0 &&
binary_data[left_idx] == 255 &&
binary_data[right_idx] == 255)
{
binary_data[curr_idx] = 255;
}
// 可选扩展:垂直方向补全(检测上下邻域)
// uint32_t up_idx = curr_idx - IMG_WIDTH;
// uint32_t down_idx = curr_idx + IMG_WIDTH;
// if (binary_data[curr_idx] == 0 &&
// binary_data[up_idx] == 255 &&
// binary_data[down_idx] == 255)
// {
// binary_data[curr_idx] = 255;
// }
}
}
printf("虚线补全完成\n");
}
四、可优化的地方
1.PID调试部分可以配合工程内的无限调试器进行仿真以获得更好的的参数
2.电机可以更换性能更加强劲的电机
3.轮胎打滑,可以尝试增加负压风扇
4.车体机构可以优化
设计图
未生成预览图,请在编辑器重新保存一次BOM
暂无BOM
克隆工程添加到专辑
0
0
分享
侵权投诉
工程成员
知识产权声明&复刻说明
本项目为开源硬件项目,其相关的知识产权归创作者所有。创作者在本平台上传该硬件项目仅供平台用户用于学习交流及研究,不包括任何商业性使用,请勿用于商业售卖或其他盈利性的用途;如您认为本项目涉嫌侵犯了您的相关权益,请点击上方“侵权投诉”按钮,我们将按照嘉立创《侵权投诉与申诉规则》进行处理。
请在进行项目复刻时自行验证电路的可行性,并自行辨别该项目是否对您适用。您对复刻项目的任何后果负责,无论何种情况,本平台将不对您在复刻项目时,遇到的任何因开源项目电路设计问题所导致的直接、间接等损害负责。


评论