【OpenCV】C++红绿灯轮廓识别+ROS话题实现

2023-11-22 21:59

本文主要是介绍【OpenCV】C++红绿灯轮廓识别+ROS话题实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

前言

一、背景知识

Opencv轮廓检测

ROS相关知识

二、环境依赖

三、具体实现

Step1:初始化ROS,订阅话题

Step2:接收话题,进入回调

1. 帧处理 

2. 膨胀腐蚀处理

Step3:红绿特征处理

1. 提取绘制轮廓

2. 转换矩形、排序

3. 显示检测结果

四、完整代码

五、使用方法

CMakeLists.txt

 package.xml

detect.launch

六、后续改进思路 


前言

根据需求需要使用Opencv实现红绿灯检测的功能,于是在猿力猪大佬的【OpenCV】红绿灯识别 轮廓识别 C++ OpenCV 案例实现 文章的基础上,将Opencv 3中的写法改成了Opencv 4,在具体图片处理的部分也按照我自己的逻辑进行了一些改动,并写成ROS工作空间包含了完整的话题读取,图片处理及检测结果显示。

一、背景知识

Opencv轮廓检测

这个部分主要用到Opencv中的findContours函数,具体介绍可以参考:findContours函数参数详解,关于轮廓检测的基本概念参考上面提到的猿力猪大佬的博文即可。

ROS相关知识

ROS编译方式:[详细教程]使用ros编译运行自己写的程序

ROS多节点运行:ROS中的roslaunch命令和launch文件(ROS入门学习笔记四)

ROS话题订阅:ROS消息发布(publish)与订阅(subscribe)(C++代码详解)

二、环境依赖

  • OpenCV 4
  • cv_bridge

三、具体实现

Step1:初始化ROS,订阅话题

int main(int argc, char **argv)
{ros::init(argc, argv, "tld_cv_node");ros::NodeHandle nh;std::string image_topic;nh.param<std::string>("sub_topic", image_topic, "/src_rgb/compressed");std::cout << "image_topic: " << image_topic << std::endl;ros::Subscriber camera_sub =nh.subscribe(image_topic, 2, camera_callback);ros::spin();ros::waitForShutdown();return 0;
}

Step2:接收话题,进入回调

1. 帧处理 

  • 从图像话题中读取图像并转换为BGR格式,调整亮度,而后转为YCrCb格式,提取ROI,根据红绿阈值拆分红色和绿色分量
cv_bridge::CvImagePtr cv_ptr =cv_bridge::toCvCopy(msg_pic, sensor_msgs::image_encodings::BGR8);if (rotated){cv::flip(cv_ptr->image, src_image, -1);}else{src_image = cv_ptr->image;}// std::cout << "src_image" << src_image.size() << std::endl;// 亮度参数double a = 0.3;double b = (1 - a) * 125;// 统计检测用时clock_t start, end;start = clock();src_image.copyTo(frame);// 调整亮度src_image.convertTo(img, img.type(), a, b);// cv::imshow("img",img);// 使用ROI(感兴趣区域)方式截取图像cv::Rect roi(0, 0, 2048, 768); // 定义roi,图片尺寸2048*1536// std::cout << "img size:" << img.size() << std::endl;cv::Mat roi_image = img(roi);// 转换为YCrCb颜色空间cvtColor(roi_image, imgYCrCb, cv::COLOR_BGR2YCrCb);// cvtColor(img, imgYCrCb, cv::COLOR_BGR2YCrCb);imgRed.create(imgYCrCb.rows, imgYCrCb.cols, CV_8UC1);imgGreen.create(imgYCrCb.rows, imgYCrCb.cols, CV_8UC1);// 分解YCrCb的三个成分std::vector<cv::Mat> planes;split(imgYCrCb, planes);// 遍历以根据Cr分量拆分红色和绿色cv::MatIterator_<uchar> it_Cr = planes[1].begin<uchar>(),it_Cr_end = planes[1].end<uchar>();cv::MatIterator_<uchar> it_Red = imgRed.begin<uchar>();cv::MatIterator_<uchar> it_Green = imgGreen.begin<uchar>();for (; it_Cr != it_Cr_end; ++it_Cr, ++it_Red, ++it_Green){// RED, 145<Cr<470 红色// if (*it_Cr > 145 && *it_Cr < 470)if (*it_Cr > 140 && *it_Cr < 470)*it_Red = 255;else*it_Red = 0;// GREEN 95<Cr<110 绿色if (*it_Cr > 95 && *it_Cr < 110)*it_Green = 255;else*it_Green = 0;// YELLOW 黄色}

PS:ROI选取这里只是随意截取了图片的上半部分。

2. 膨胀腐蚀处理

  • 膨胀的第三个参数:膨胀操作的内核,我根据实际场景的检测效果进行了修改
// 膨胀和腐蚀dilate(imgRed, imgRed, cv::Mat(8, 8, CV_8UC1), cv::Point(-1, -1));erode(imgRed, imgRed, cv::Mat(1, 1, CV_8UC1), cv::Point(-1, -1));dilate(imgGreen, imgGreen, cv::Mat(12, 12, CV_8UC1), cv::Point(-1, -1));erode(imgGreen, imgGreen, cv::Mat(1, 1, CV_8UC1), cv::Point(-1, -1));

Step3:红绿特征处理

  • 这是我改动最大的一个函数,只保留了原作者提取轮廓转换为矩形的思路。先提取、绘制轮廓、显示检测结果,然后对得到的矩形进行位置排序,再对轮廓依次进行显示。

1. 提取绘制轮廓

// 提取轮廓findContours(tmp_Red, contours_Red, hierarchy_Red, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE);findContours(tmp_Green, contours_Green, hierarchy_Green, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE);// 绘制轮廓drawContours(frame, contours_Red, -1, cv::Scalar(0, 0, 255), cv::FILLED); // Redstd::cout << "提取到的红色轮廓数量:" << contours_Red.size() << std::endl;drawContours(frame, contours_Green, -1, cv::Scalar(0, 255, 0), cv::FILLED); // Greenstd::cout << "提取到的绿色轮廓数量:" << contours_Green.size() << std::endl;// 显示轮廓//  imshow("contours", frame);trackBox_Red = new cv::Rect[contours_Red.size()];trackBox_Green = new cv::Rect[contours_Green.size()];

2. 转换矩形、排序

  • 此处需特别注意trackBox指针的清空
  • 对结构体的排序方式参考了用sort对结构体某个元素排序的方法
// 确定要跟踪的区域for (int i = 0; i < contours_Red.size(); i++){// Yi opencv4 不支持 CvSeqtrackBox_Red[i] = cv::boundingRect(contours_Red[i]);}for (int i = 0; i < contours_Green.size(); i++){// Yi opencv4 不支持 CvSeqtrackBox_Green[i] = cv::boundingRect(contours_Green[i]);}// imshow("contours", tmp);// Rect.tl() 返回矩形左上顶点的坐标for (int i = 0; i < contours_Red.size(); i++){Store_x_color a;a.x = trackBox_Red[i].tl().x;a.y = trackBox_Red[i].tl().y;a.color = 0;x_color.push_back(a);}for (int i = 0; i < contours_Green.size(); i++){Store_x_color a;a.x = trackBox_Green[i].tl().x;a.y = trackBox_Green[i].tl().y;a.color = 1;x_color.push_back(a);}// 清空指针delete[] trackBox_Red;delete[] trackBox_Green;// 对左上顶点横坐标进行排序sort(x_color.begin(), x_color.end(), CompareByX);

3. 显示检测结果

// 显示结果for (int i = 0; i < x_color.size(); i++){if (0 == x_color[i].color)cv::putText(frame, "Red", cv::Point(x_color[i].x, x_color[i].y - 25), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 0, 255), 2, 8, 0);else if (1 == x_color[i].color)cv::putText(frame, "Green", cv::Point(x_color[i].x, x_color[i].y - 25), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 255, 0), 2, 8, 0);else if (2 == x_color[i].color)cv::putText(frame, "Yellow", cv::Point(x_color[i].x, x_color[i].y - 25), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 255, 255), 2, 8, 0);elsecv::putText(frame, "Lights off", cv::Point(x_color[i].x, x_color[i].y - 25), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(255, 255, 255), 2, 8, 0);}cv::namedWindow("tld_result", 0);cv::resizeWindow("tld_result", 1920, 1080);cv::imshow("tld_result", frame);cv::waitKey(1);

实际检测结果如下图所示: 

  

四、完整代码

/** @CopyRight: All Rights Reserved by Plusgo* @Author: Yi* @E-mail: waterwinsor@gmail.com* @Date: 2023年 05月 06日 星期六 15:44:35* @LastEditTime: 2023年 05月 08日 星期一 10:07:30*/// requirements: opencv 4#include <iostream>
#include <fstream>
#include <time.h>
#include <algorithm>#include <cv_bridge/cv_bridge.h>
#include <image_transport/image_transport.h>
#include <ros/ros.h>
#include <sensor_msgs/Image.h>
#include <sensor_msgs/PointCloud2.h>#include <opencv2/opencv.hpp>
#include "opencv2/imgproc.hpp"
#include <opencv2/imgproc/types_c.h>struct Store_x_color
{int x;     // 存储左上顶点横坐标int y;     // 存储左上顶点纵坐标int color; // 存储当前点颜色
};// Function headers
void processImg(cv::Mat, cv::Mat); // 前红后绿
bool CompareByX(const Store_x_color &, const Store_x_color &);// Global variables
cv::Mat src_image;
bool rotated = true; // rotate 180cv::Mat frame;
cv::Mat img;
cv::Mat imgYCrCb;
cv::Mat imgGreen;
cv::Mat imgRed;
cv::Mat imgYellow;
std::vector<Store_x_color> x_color;void camera_callback(const sensor_msgs::CompressedImageConstPtr &msg_pic)
{try{cv_bridge::CvImagePtr cv_ptr =cv_bridge::toCvCopy(msg_pic, sensor_msgs::image_encodings::BGR8);if (rotated){cv::flip(cv_ptr->image, src_image, -1);}else{src_image = cv_ptr->image;}// std::cout << "src_image" << src_image.size() << std::endl;// 亮度参数double a = 0.3;double b = (1 - a) * 125;// 统计检测用时clock_t start, end;start = clock();src_image.copyTo(frame);// 调整亮度src_image.convertTo(img, img.type(), a, b);// cv::imshow("img",img);// 使用ROI(感兴趣区域)方式截取图像cv::Rect roi(0, 0, 2048, 768); // 定义roi,图片尺寸2048*1536// std::cout << "img size:" << img.size() << std::endl;cv::Mat roi_image = img(roi);// 转换为YCrCb颜色空间cvtColor(roi_image, imgYCrCb, cv::COLOR_BGR2YCrCb);// cvtColor(img, imgYCrCb, cv::COLOR_BGR2YCrCb);imgRed.create(imgYCrCb.rows, imgYCrCb.cols, CV_8UC1);imgGreen.create(imgYCrCb.rows, imgYCrCb.cols, CV_8UC1);// 分解YCrCb的三个成分std::vector<cv::Mat> planes;split(imgYCrCb, planes);// 遍历以根据Cr分量拆分红色和绿色cv::MatIterator_<uchar> it_Cr = planes[1].begin<uchar>(),it_Cr_end = planes[1].end<uchar>();cv::MatIterator_<uchar> it_Red = imgRed.begin<uchar>();cv::MatIterator_<uchar> it_Green = imgGreen.begin<uchar>();for (; it_Cr != it_Cr_end; ++it_Cr, ++it_Red, ++it_Green){// RED, 145<Cr<470 红色// if (*it_Cr > 145 && *it_Cr < 470)if (*it_Cr > 140 && *it_Cr < 470)*it_Red = 255;else*it_Red = 0;// GREEN 95<Cr<110 绿色if (*it_Cr > 95 && *it_Cr < 110)*it_Green = 255;else*it_Green = 0;// YELLOW 黄色}// 膨胀和腐蚀dilate(imgRed, imgRed, cv::Mat(8, 8, CV_8UC1), cv::Point(-1, -1));erode(imgRed, imgRed, cv::Mat(1, 1, CV_8UC1), cv::Point(-1, -1));dilate(imgGreen, imgGreen, cv::Mat(12, 12, CV_8UC1), cv::Point(-1, -1));erode(imgGreen, imgGreen, cv::Mat(1, 1, CV_8UC1), cv::Point(-1, -1));// 检测和显示processImg(imgRed, imgGreen);// 清空x_colorx_color.clear();end = clock();std::cout << "检测时间:" << (double)(end - start) / CLOCKS_PER_SEC << std::endl; // 打印检测时间}catch (cv_bridge::Exception e){ROS_ERROR_STREAM("cant't get image");}
}int main(int argc, char **argv)
{ros::init(argc, argv, "tld_cv_node");ros::NodeHandle nh;std::string image_topic;nh.param<std::string>("sub_topic", image_topic, "/src_rgb/compressed");std::cout << "image_topic: " << image_topic << std::endl;ros::Subscriber camera_sub =nh.subscribe(image_topic, 2, camera_callback);ros::spin();ros::waitForShutdown();return 0;
}void processImg(cv::Mat src_Red, cv::Mat src_Green)
{cv::Mat tmp_Red;cv::Mat tmp_Green;std::vector<std::vector<cv::Point>> contours_Red;std::vector<std::vector<cv::Point>> contours_Green;std::vector<cv::Vec4i> hierarchy_Red;std::vector<cv::Vec4i> hierarchy_Green;cv::Rect *trackBox_Red;cv::Rect *trackBox_Green;src_Red.copyTo(tmp_Red);src_Green.copyTo(tmp_Green);// 提取轮廓findContours(tmp_Red, contours_Red, hierarchy_Red, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE);findContours(tmp_Green, contours_Green, hierarchy_Green, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE);// 绘制轮廓drawContours(frame, contours_Red, -1, cv::Scalar(0, 0, 255), cv::FILLED); // Redstd::cout << "提取到的红色轮廓数量:" << contours_Red.size() << std::endl;drawContours(frame, contours_Green, -1, cv::Scalar(0, 255, 0), cv::FILLED); // Greenstd::cout << "提取到的绿色轮廓数量:" << contours_Green.size() << std::endl;// 显示轮廓//  imshow("contours", frame);trackBox_Red = new cv::Rect[contours_Red.size()];trackBox_Green = new cv::Rect[contours_Green.size()];// 确定要跟踪的区域for (int i = 0; i < contours_Red.size(); i++){// Yi opencv4 不支持 CvSeqtrackBox_Red[i] = cv::boundingRect(contours_Red[i]);}for (int i = 0; i < contours_Green.size(); i++){// Yi opencv4 不支持 CvSeqtrackBox_Green[i] = cv::boundingRect(contours_Green[i]);}// imshow("contours", tmp);// Rect.tl() 返回矩形左上顶点的坐标for (int i = 0; i < contours_Red.size(); i++){Store_x_color a;a.x = trackBox_Red[i].tl().x;a.y = trackBox_Red[i].tl().y;a.color = 0;x_color.push_back(a);}for (int i = 0; i < contours_Green.size(); i++){Store_x_color a;a.x = trackBox_Green[i].tl().x;a.y = trackBox_Green[i].tl().y;a.color = 1;x_color.push_back(a);}// 清空指针delete[] trackBox_Red;delete[] trackBox_Green;// 对左上顶点横坐标进行排序sort(x_color.begin(), x_color.end(), CompareByX);// 显示结果for (int i = 0; i < x_color.size(); i++){if (0 == x_color[i].color)cv::putText(frame, "Red", cv::Point(x_color[i].x, x_color[i].y - 25), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 0, 255), 2, 8, 0);else if (1 == x_color[i].color)cv::putText(frame, "Green", cv::Point(x_color[i].x, x_color[i].y - 25), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 255, 0), 2, 8, 0);else if (2 == x_color[i].color)cv::putText(frame, "Yellow", cv::Point(x_color[i].x, x_color[i].y - 25), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 255, 255), 2, 8, 0);elsecv::putText(frame, "Lights off", cv::Point(x_color[i].x, x_color[i].y - 25), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(255, 255, 255), 2, 8, 0);}cv::namedWindow("tld_result", 0);cv::resizeWindow("tld_result", 1920, 1080);cv::imshow("tld_result", frame);cv::waitKey(1);return;
}bool CompareByX(const Store_x_color &a, const Store_x_color &b)
{return a.x < b.x;
}

五、使用方法

编译所需的CMakeLists.txt、package.xml和运行所需roslaunch文件如下

  • CMakeLists.txt

cmake_minimum_required(VERSION 2.8.3)
project(tld_cv)set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_BUILD_TYPE "Release")  # Debug Release RelWithDebInfoadd_definitions(-O2 -pthread)
add_compile_options(-std=c++11)find_package(OpenCV REQUIRED)
find_package(catkin REQUIRED COMPONENTSroscppstd_msgssensor_msgscv_bridgeimage_transport
)catkin_package(CATKIN_DEPENDSroscppstd_msgssensor_msgscv_bridgeimage_transport
)include_directories(
# include${catkin_INCLUDE_DIRS}${OpenCV_INCLUDE_DIRS}
)add_executable(tld_cv src/main.cpp)
target_link_libraries(tld_cv${catkin_LIBRARIES}${OpenCV_LIBRARIES})
  •  package.xml

<?xml version="1.0"?>
<package format="2"><name>tld_cv</name><version>0.0.0</version><description>The tld_cv package</description><maintainer email="royry@foxmail.com">Ru1yi</maintainer><license>TODO</license><buildtool_depend>catkin</buildtool_depend><build_depend>cv_bridge</build_depend><build_depend>image_transport</build_depend><build_depend>roscpp</build_depend><build_depend>sensor_msgs</build_depend><build_depend>std_msgs</build_depend><build_export_depend>cv_bridge</build_export_depend><build_export_depend>image_transport</build_export_depend><build_export_depend>roscpp</build_export_depend><build_export_depend>sensor_msgs</build_export_depend><build_export_depend>std_msgs</build_export_depend><exec_depend>cv_bridge</exec_depend><exec_depend>image_transport</exec_depend><exec_depend>roscpp</exec_depend><exec_depend>sensor_msgs</exec_depend><exec_depend>std_msgs</exec_depend><!-- The export tag contains other, unspecified, tags --><export><!-- Other tools can request additional information be placed here --></export>
</package>
  • detect.launch

<launch><arg name="sub_image_topic" value="/camera/image_color/compressed"/><param name="sub_topic" value="$(arg sub_image_topic)"/><node pkg="tld_cv" type="tld_cv" name="tld_cv" output="screen" /></launch>

六、后续改进思路 

改进可有如下几个方向:

  • ROI

根据具体自动驾驶场景,可以通过红绿灯位置、高度、相机安装方式、车辆定位和IMU信息实时计算出一个更为精确的ROI,检测再进行排序即可确定红绿灯的个数和顺序,方便编写后处理中的逻辑。

  • 筛选面积

根据检测后的结果筛选较大的几个轮廓,可以排除掉某些较小物体的误检干扰 

本人接触OpenCV时间尚短、经验尚浅,如果有任何疏漏、错误还请大家指出~

这篇关于【OpenCV】C++红绿灯轮廓识别+ROS话题实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/413208

相关文章

使用Sentinel自定义返回和实现区分来源方式

《使用Sentinel自定义返回和实现区分来源方式》:本文主要介绍使用Sentinel自定义返回和实现区分来源方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Sentinel自定义返回和实现区分来源1. 自定义错误返回2. 实现区分来源总结Sentinel自定

Java实现时间与字符串互相转换详解

《Java实现时间与字符串互相转换详解》这篇文章主要为大家详细介绍了Java中实现时间与字符串互相转换的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、日期格式化为字符串(一)使用预定义格式(二)自定义格式二、字符串解析为日期(一)解析ISO格式字符串(二)解析自定义

opencv图像处理之指纹验证的实现

《opencv图像处理之指纹验证的实现》本文主要介绍了opencv图像处理之指纹验证的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录一、简介二、具体案例实现1. 图像显示函数2. 指纹验证函数3. 主函数4、运行结果三、总结一、

Springboot处理跨域的实现方式(附Demo)

《Springboot处理跨域的实现方式(附Demo)》:本文主要介绍Springboot处理跨域的实现方式(附Demo),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不... 目录Springboot处理跨域的方式1. 基本知识2. @CrossOrigin3. 全局跨域设置4.

Spring Boot 3.4.3 基于 Spring WebFlux 实现 SSE 功能(代码示例)

《SpringBoot3.4.3基于SpringWebFlux实现SSE功能(代码示例)》SpringBoot3.4.3结合SpringWebFlux实现SSE功能,为实时数据推送提供... 目录1. SSE 简介1.1 什么是 SSE?1.2 SSE 的优点1.3 适用场景2. Spring WebFlu

基于SpringBoot实现文件秒传功能

《基于SpringBoot实现文件秒传功能》在开发Web应用时,文件上传是一个常见需求,然而,当用户需要上传大文件或相同文件多次时,会造成带宽浪费和服务器存储冗余,此时可以使用文件秒传技术通过识别重复... 目录前言文件秒传原理代码实现1. 创建项目基础结构2. 创建上传存储代码3. 创建Result类4.

SpringBoot日志配置SLF4J和Logback的方法实现

《SpringBoot日志配置SLF4J和Logback的方法实现》日志记录是不可或缺的一部分,本文主要介绍了SpringBoot日志配置SLF4J和Logback的方法实现,文中通过示例代码介绍的非... 目录一、前言二、案例一:初识日志三、案例二:使用Lombok输出日志四、案例三:配置Logback一

Python如何使用__slots__实现节省内存和性能优化

《Python如何使用__slots__实现节省内存和性能优化》你有想过,一个小小的__slots__能让你的Python类内存消耗直接减半吗,没错,今天咱们要聊的就是这个让人眼前一亮的技巧,感兴趣的... 目录背景:内存吃得满满的类__slots__:你的内存管理小助手举个大概的例子:看看效果如何?1.

Python+PyQt5实现多屏幕协同播放功能

《Python+PyQt5实现多屏幕协同播放功能》在现代会议展示、数字广告、展览展示等场景中,多屏幕协同播放已成为刚需,下面我们就来看看如何利用Python和PyQt5开发一套功能强大的跨屏播控系统吧... 目录一、项目概述:突破传统播放限制二、核心技术解析2.1 多屏管理机制2.2 播放引擎设计2.3 专

Python实现无痛修改第三方库源码的方法详解

《Python实现无痛修改第三方库源码的方法详解》很多时候,我们下载的第三方库是不会有需求不满足的情况,但也有极少的情况,第三方库没有兼顾到需求,本文将介绍几个修改源码的操作,大家可以根据需求进行选择... 目录需求不符合模拟示例 1. 修改源文件2. 继承修改3. 猴子补丁4. 追踪局部变量需求不符合很