树莓派笔记22_小车:小车电机开环运动与opencv摄像头巡线

news2025/1/11 21:05:10

  今日继续学习树莓派4B 4G:(Raspberry Pi,简称RPi或RasPi)

 本人所用树莓派4B 装载的系统与版本如下:

 版本可用命令 (lsb_release -a) 查询:

 Opencv 版本是4.5.1:

 Python 版本3.7.3:

今日尝试搭建一台小车,外观自行设计、树莓派主控、自行挑选模块 ,完成简单的巡线动作,就开环控制了,这篇文章的使用就不闭环了......

巡线的思想就是在视频屏幕最下方1/4的ROI区域灰度识别黑色色块,计算与中点的偏差,与中点偏差的像素过大就原地左右转调整,其余识别到黑线在偏差范围内不大时就正常走直线

自评:原地左右转以及没使用PID闭环使得这个视觉巡线小车看起来不太智能,还可改进>>>

文章提供测试代码讲解,整体代码贴出、测试效果图、完整测试工程下载

目录

小车外观与使用模块:

主要部件:

 部分模块展示:

小车运动控制代码:

Motor_control.py 

程序补足电机对向转动方向:

Opencv识别黑线:

Gray_find_black.py

测试效果截图:

Gray_find_black_roi.py

测试效果截图:

巡线代码:

Line_inspection.py

测试效果视频:

整体工程下载:

网上学习资料网址:


小车外观与使用模块:

搭个小车还是很快很简单的,不到半天就能搭好小车了......

主要部件:

主控:           树莓派4B 4G

电机:           MG310  13线霍尔编码器  减速比 1:20 ( 4个 ) 购于 轮趣科技 淘宝店

电机控制板:幻尔 IIC四路电机扩展板  购于幻尔科技淘宝店

车轮:           购于 金色传说 淘宝店 (2.5mm轴)这个轮子摩擦力可以,价格还便宜!

车架:           使用 购于 星呗机器人淘宝店 的车架,贵~~

屏幕:           7英寸 HDML 显示屏,二手咸鱼购入

摄像头:       普通USB摄像头 (一般使用640*480像素模式),随便来个上网课的摄像头都行!

其余部件:    降压模块、6000mah电池、DC电池开关......

 部分模块展示:

1、幻尔IIC四路电机控制板:

小车运动控制代码:

树莓派通过IIC与电机控制板通信,使电机控制板发PWM信号驱动电机运转:

这里我在Motor_control.py文件中加入了一些获取键盘值、字典的操作来简单控制小车

但在程序上获取键盘值每次都需要按一次回车,比较不方便......

Motor_control.py 

这部分是电机控制代码:包含一些正反转,左右原地旋转的基本逻辑,这是为开环控制写的,因此四个轮子速度设定都一样,

import smbus
import time
import struct

# 设置I2C总线号,通常为1
I2C_BUS = 1

# 设置四路电机驱动模块的I2C地址
MOTOR_ADDR = 0x34

# 寄存器地址
ADC_BAT_ADDR = 0x00
MOTOR_TYPE_ADDR = 0x14  # 编码电机类型设置
MOTOR_ENCODER_POLARITY_ADDR = 0x15  # 设置编码方向极性,
# 如果发现电机转速根本不受控制,要么最快速度转动,要么停止。可以将此地址的值重新设置一下
# 范围0或1,默认0
MOTOR_FIXED_PWM_ADDR = 0x1F  # 固定PWM控制,属于开环控制,范围(-100~100)
MOTOR_FIXED_SPEED_ADDR = 0x33  # 固定转速控制,属于闭环控制,
# 单位:脉冲数每10毫秒,范围(根据具体的编码电机来,受编码线数,电压大小,负载大小等影响,一般在±50左右)

MOTOR_ENCODER_TOTAL_ADDR = 0x3C  # 4个编码电机各自的总脉冲值
# #如果已知电机每转一圈的脉冲数为U,又已知轮子的直径D,那么就可以通过脉冲计数的方式得知每个轮子行进的距离
# #比如读到电机1的脉冲总数为P,那么行进的距离为(P/U) * (3.14159*D)
# #对于不同的电机可以自行测试每圈的脉冲数U,可以手动旋转10圈读出脉冲数,然后取平均值得出


# 电机类型具体值
MOTOR_TYPE_WITHOUT_ENCODER = 0
MOTOR_TYPE_TT = 1
MOTOR_TYPE_N20 = 2   #MG310与N20相同
MOTOR_TYPE_JGB37_520_12V_110RPM = 3  # 磁环每转是44个脉冲   减速比:90  默认

# 电机类型及编码方向极性
MotorType = MOTOR_TYPE_N20
MotorEncoderPolarity = 1

bus = smbus.SMBus(I2C_BUS)
speed1 = [50, 50, 50, 50]
speed2 = [-50, -50, -50, -50]
speed3 = [0, 0, 0, 0]
speed4 = [-50, 50, 50, -50]
speed5 = [0, 0, 0, 0]

new_speed = 50

pwm1 = [50, 50, 50, 50]
pwm2 = [-100, -100, -100, -100]
pwm3 = [0, 0, 0, 0]


def Motor_Init():  # 电机初始化
    bus.write_byte_data(MOTOR_ADDR, MOTOR_TYPE_ADDR, MotorType)  # 设置电机类型
    time.sleep(0.5)
    bus.write_byte_data(MOTOR_ADDR, MOTOR_ENCODER_POLARITY_ADDR, MotorEncoderPolarity)  # 设置编码极性

# 控制电机向前的代码
def move_forward():
    print("向前移动")
    global new_speed, speed5
    speed5[0] = new_speed*(-1)
    speed5[1] = new_speed
    speed5[2] = new_speed
    speed5[3] = new_speed*(-1)
    bus.write_i2c_block_data(MOTOR_ADDR, MOTOR_FIXED_SPEED_ADDR, speed5)

# 控制电机向后的代码
def move_backward():
    print("向后移动")
    global new_speed, speed5
    speed5[0] = new_speed
    speed5[1] = new_speed*(-1)
    speed5[2] = new_speed*(-1)
    speed5[3] = new_speed
    bus.write_i2c_block_data(MOTOR_ADDR, MOTOR_FIXED_SPEED_ADDR, speed5)

# 控制电机向左的代码
def move_left():
    print("向左移动")
    global new_speed, speed5
    speed5[0] = new_speed
    speed5[1] = new_speed*(-1)
    speed5[2] = new_speed
    speed5[3] = new_speed*(-1)
    bus.write_i2c_block_data(MOTOR_ADDR, MOTOR_FIXED_SPEED_ADDR, speed5)

# 控制电机向右的代码
def move_right():
    print("向右移动")
    global new_speed, speed5
    speed5[0] = new_speed * (-1)
    speed5[1] = new_speed
    speed5[2] = new_speed * (-1)
    speed5[3] = new_speed
    bus.write_i2c_block_data(MOTOR_ADDR, MOTOR_FIXED_SPEED_ADDR, speed5)

# 停止电机的代码
def stop_motor():
    print("停止移动")
    speed = [0, 0, 0, 0]
    bus.write_i2c_block_data(MOTOR_ADDR, MOTOR_FIXED_SPEED_ADDR, speed)

def reduce_speed():
    print("减速")
    global new_speed
    new_speed=new_speed-10

def add_speed():
    print("加速")
    global new_speed
    new_speed=new_speed+10

direction_mapping = {
    "W": move_forward,
    "S": move_backward,
    "A": move_left,
    "D": move_right,
    "X": stop_motor,
    "Q": reduce_speed,
    "E": add_speed
}


def main():
    while True:
        battery = bus.read_i2c_block_data(MOTOR_ADDR, ADC_BAT_ADDR)
        print("V = {0}mV".format(battery[0] + (battery[1] << 8)))
        Encode = struct.unpack('iiii', bytes(bus.read_i2c_block_data(MOTOR_ADDR, MOTOR_ENCODER_TOTAL_ADDR, 16)))
        print("Encode1 = {0}  Encode2 = {1}  Encode3 = {2}  Encode4 = {3}".format(Encode[0], Encode[1], Encode[2],
                                                                                  Encode[3]))
            # PWM控制(注意:PWM控制是一个持续控制的过程,若有延时则会打断电机的运行)
        # bus.write_i2c_block_data(MOTOR_ADDR, MOTOR_FIXED_PWM_ADDR,pwm1)

        Motor_CLR=input("请输入运动方向(W前/S后/A左/D右/X停止): ").upper()  # 转换为大写以忽略大小写差异
        if Motor_CLR in direction_mapping:
            direction_mapping[Motor_CLR]()
        else:
            print("无效输入,请输入正确的方向(W前/S后/A左/D右/X停止)")


if __name__ == "__main__":
    Motor_Init()
    main()

程序补足电机对向转动方向:

其中我们发现这里电机向前转的代码有些轮子的速度值被乘(-1)了,这是因为左右相邻电机对向安装的结果,使得它们旋转方向相反的,在程序上乘个(-1),使其方向调整过来


# 控制电机向前的代码
def move_forward():
    print("向前移动")
    global new_speed, speed5
    speed5[0] = new_speed*(-1)
    speed5[1] = new_speed
    speed5[2] = new_speed
    speed5[3] = new_speed*(-1)
    bus.write_i2c_block_data(MOTOR_ADDR, MOTOR_FIXED_SPEED_ADDR, speed5)
电机转动方向与传入速度数值正负关系
M1(左前轮)M2(左后轮)M3(右前轮)M4(右后轮)
前进
后退
左转
右转

Opencv识别黑线:

Gray_find_black.py

这里展示一下单独可以运行的Opencv识别黑线代码:

灰度图中识别黑色区域,最长的区域视作黑线,并标注出它的中点坐标:

Gray_find_black.py文件代码如下:

import time
import numpy as np
import cv2

'''
标记颜色块中点程序,这里是黑线

'''

# 打开摄像头,0通常是默认摄像头的索引
cap = cv2.VideoCapture(0)
# 设置目标分辨率
target_resolution = (640, 480)


def find_line_midpoint(frame):
    # 转换到灰度图像
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    # 应用阈值处理来创建掩码,这里假设黑线的灰度值较低
    _, mask = cv2.threshold(gray, 50, 255, cv2.THRESH_BINARY_INV)  # 阈值可以根据实际情况调整
    # 形态学操作,去除噪点
    kernel = np.ones((3, 3), np.uint8)
    mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2)  # 使用闭运算来填充小的孔洞
    # 查找轮廓
    contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    # 假设最长的轮廓是黑线
    max_area = 0
    best_cnt = None
    for cnt in contours:
        area = cv2.contourArea(cnt)
        if area > max_area:
            max_area = area
            best_cnt = cnt
            # 如果找到轮廓,则计算中点
    if best_cnt is not None:
        # 计算轮廓的边界矩形
        x, y, w, h = cv2.boundingRect(best_cnt)
        # 绘制矩形框(注意:这里的y是矩形框在原图上的顶部坐标)
        cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 255), 2)  # 使用蓝色绘制矩形框
        M = cv2.moments(best_cnt)
        if M["m00"] != 0:
            cX = int(M["m10"] / M["m00"])
            cY = int(M["m01"] / M["m00"])
            cv2.circle(frame, (cX, cY), 5, (0, 255, 0), -1)  # 标记中点
            print("({},{})".format(cX, cY))

            # 显示结果
    cv2.imshow('Frame', frame)
    cv2.imshow('Mask', mask)


while True:
    ret, frame = cap.read()
    if not ret:
        break
    find_line_midpoint(frame)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

    # 释放资源和关闭所有窗口
cap.release()
cv2.destroyAllWindows()

测试效果截图:

PC运行测试图:

小车实际运行测试图:

框出整个屏幕识别到的最大黑线区域的中心位置

Gray_find_black_roi.py

这个程序文件就是在之前的文件Gray_find_black.py 上对函数进行了优化

设置Roi 使寻迹识别黑色区域在 整个像素区域的下1/4 处

并添加了与中点进行比较的连线以及输出连线俩端点的差值

顺便添加了一些函数返回值,并在函数开头初始化:

        detect  #detect=0 没检测到黑线 , detect=1检测到黑线
        dx      #黑色块中心与Roi中心的x坐标差值
        dy      #黑色块中心与Roi中心的y坐标差值

这个程序直接复制也能直接运行!

import cv2
import numpy as np


def find_line_midpoint_in_roi(frame):
    #随便给这三个用到的需要返回的值赋初始值,防止返回报错!
    detect=3  #detect=0没检测到黑线,detect=1检测到黑线
    dx=3
    dy=3
    # 获取帧的高度和宽度
    height, width = frame.shape[:2]
    # 计算底部四分之一区域的高度
    roi_height = height // 4
    # 定义ROI区域(底部四分之一)
    roi_y = height - roi_height
    roi = frame[roi_y:height, 0:width]
    # 转换ROI到灰度图像
    gray_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
    # 应用阈值处理来创建掩码
    _, mask = cv2.threshold(gray_roi, 50, 255, cv2.THRESH_BINARY_INV)
    # 形态学操作,去除噪点(可选)
    kernel = np.ones((3, 3), np.uint8)
    mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2)

    # 查找轮廓
    contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    # 假设最长的轮廓是黑线
    max_area = 0
    best_cnt = None
    for cnt in contours:
        area = cv2.contourArea(cnt)
        if area > max_area:
            max_area = area
            best_cnt = cnt

            # 如果找到轮廓,则计算中点,并在原图上标记
    # 如果找到轮廓,则计算中点,并使用矩形框在原图上框出识别到的黑色块
    if best_cnt is not None:
        detect=1
        # 注意:轮廓点坐标是相对于mask的,但我们需要将其转换为原图的坐标
        # 假设best_cnt是一个轮廓点集

        # 计算轮廓的边界矩形(这一步是在ROI坐标系中进行的)
        x, y, w, h = cv2.boundingRect(best_cnt)

        # 将边界矩形的坐标从ROI坐标系转换为原图坐标系
        # 注意:这里x坐标(即边界矩形的左边界)不需要修改,因为它已经是相对于原图的左边界
        # 而y坐标(即边界矩形的下边界,因为ROI是从底部开始的)需要转换为原图的坐标
        y_original = height - (y + h)  # 将ROI中的下边界转换为原图的上边界

        # 现在我们可以在原图上绘制矩形了
        cv2.rectangle(frame, (x, y_original), (x + w, y_original + h), (0, 0, 255), 2)  # 使用红色绘制矩形框

        # 接下来计算并绘制轮廓的中心点
        M = cv2.moments(best_cnt)
        cX = int(M["m10"] / M["m00"])  # 轮廓中心的X坐标(相对于ROI左上角),无需修改
        cY_roi = int(M["m01"] / M["m00"])  # 轮廓中心的Y坐标(相对于ROI底部)
        cY = height - cY_roi  # 将Y坐标从ROI底部转换为原图顶部
        cv2.circle(frame, (cX, cY), 5, (0, 255, 0), -1)  # 在原图上绘制圆心

        # 打印中心点坐标(可选)
        print("({},{})".format(cX, cY))

        # 计算 ROI 的中心点
        roi_mid_x = width // 2  # ROI 的 X 中心点(因为 ROI 总是从原图底部开始的全宽)
        roi_mid_y = roi_y + roi_height // 2  # ROI 的 Y 中心点(注意要加上 ROI 的起始 Y 坐标)
        # 绘制 ROI 中心点
        cv2.circle(frame, (roi_mid_x, roi_mid_y), 5, (255, 0, 0), -1)  # 使用蓝色绘制 ROI 中心点
        # 计算并绘制连接线
        cv2.line(frame, (roi_mid_x, roi_mid_y), (cX, cY), (0, 0, 255), 2)  # 使用红色绘制连接线
        # 计算并打印两个点的 X 和 Y 差值
        dx = cX - roi_mid_x
        dy = cY - roi_mid_y
        # 创建包含差值的文本字符串
        text = f"dx={dx}, dy={dy}"
        #text_x 与 text_y 表示文本打印的位置
        text_x = int((roi_mid_x + cX) / 2)  # 线的中点 X 坐标(可能需要根据实际情况调整)
        text_y = int((roi_mid_y + cY) / 2 - 10)  # 线的中点 Y 坐标减去一些空间以容纳文本
        cv2.putText(frame, text, (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1, cv2.LINE_AA)
    elif best_cnt is None: #没找到黑线
        detect=0

        # 返回处理后的帧(可选,也可以直接在原图上操作)
    return frame,mask,dx,dy,detect


# 主循环,用于从摄像头读取帧并处理
cap = cv2.VideoCapture(0)

while True:
    ret, frame = cap.read()
    if not ret:
        break

    #使用下划_忽略接收最后俩个返回的dx,dy的差值
    frame,mask,_,_,_= find_line_midpoint_in_roi(frame)


    # 显示结果
    cv2.imshow('Frame', frame)
    cv2.imshow('Mask',mask)

    # 按 'q' 键退出
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

测试效果截图:

PC端测试效果:

掩膜中白色部分就是识别到的黑色区域,而它只会将最大的黑色区域设置为识别到的黑色块

小车实际测试效果:

蓝色的点是屏幕ROI区域的中点,绿色的点是识别到的色块中点

一会和电机运动结合就是>50 就右转,<-50就左转,

这里巡线就不想使用左右侧 差速的思想了,直接原地停下左右调整就好了,也不会浪费时间的,差速总感觉容易冲出去

巡线代码:

本文写到这里我没有pid 闭环电机,就先这样开环能跑跑吧

这部分代码就是最终能控制电机并巡线的代码了,需要与Motor_control.py放在同一个文件夹

Line_inspection.py

这部分写的比较粗糙了,为了能巡线而巡线写的

速度写的比较低,然后添加了15ms 的time.sleep()结合循环的任务

(貌似这个子函数是死循环,会卡死不进入主程序的while循环......)

实际运行也确实发现没有进入主程序的while循环显示摄像头效果,也许这里使用线程会好些

,在下部分文章实验在加入线程思想来写把......


def task():
    pass

def periodic_task():
    while True:
        task()
        time.sleep(0.015)  # 休眠15ms

import cv2
import Motor_control #导入自定义电机控制模块
import time
import numpy as np

def find_line_midpoint_in_roi(frame):
    #随便给这三个用到的需要返回的值赋初始值,防止返回报错!
    detect=3  #detect=0没检测到黑线,detect=1检测到黑线
    dx=3
    dy=3
    # 获取帧的高度和宽度
    height, width = frame.shape[:2]
    # 计算底部四分之一区域的高度
    roi_height = height // 4
    # 定义ROI区域(底部四分之一)
    roi_y = height - roi_height
    roi = frame[roi_y:height, 0:width]
    # 转换ROI到灰度图像
    gray_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
    # 应用阈值处理来创建掩码
    _, mask = cv2.threshold(gray_roi, 50, 255, cv2.THRESH_BINARY_INV)
    # 形态学操作,去除噪点(可选)
    kernel = np.ones((3, 3), np.uint8)
    mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2)

    # 查找轮廓
    contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    # 假设最长的轮廓是黑线
    max_area = 0
    best_cnt = None
    for cnt in contours:
        area = cv2.contourArea(cnt)
        if area > max_area:
            max_area = area
            best_cnt = cnt

            # 如果找到轮廓,则计算中点,并在原图上标记
    # 如果找到轮廓,则计算中点,并使用矩形框在原图上框出识别到的黑色块
    if best_cnt is not None:
        detect=1
        # 注意:轮廓点坐标是相对于mask的,但我们需要将其转换为原图的坐标
        # 假设best_cnt是一个轮廓点集

        # 计算轮廓的边界矩形(这一步是在ROI坐标系中进行的)
        x, y, w, h = cv2.boundingRect(best_cnt)

        # 将边界矩形的坐标从ROI坐标系转换为原图坐标系
        # 注意:这里x坐标(即边界矩形的左边界)不需要修改,因为它已经是相对于原图的左边界
        # 而y坐标(即边界矩形的下边界,因为ROI是从底部开始的)需要转换为原图的坐标
        y_original = height - (y + h)  # 将ROI中的下边界转换为原图的上边界

        # 现在我们可以在原图上绘制矩形了
        cv2.rectangle(frame, (x, y_original), (x + w, y_original + h), (0, 0, 255), 2)  # 使用红色绘制矩形框

        # 接下来计算并绘制轮廓的中心点
        M = cv2.moments(best_cnt)
        cX = int(M["m10"] / M["m00"])  # 轮廓中心的X坐标(相对于ROI左上角),无需修改
        cY_roi = int(M["m01"] / M["m00"])  # 轮廓中心的Y坐标(相对于ROI底部)
        cY = height - cY_roi  # 将Y坐标从ROI底部转换为原图顶部
        cv2.circle(frame, (cX, cY), 5, (0, 255, 0), -1)  # 在原图上绘制圆心

        # 打印中心点坐标(可选)
        print("({},{})".format(cX, cY))

        # 计算 ROI 的中心点
        roi_mid_x = width // 2  # ROI 的 X 中心点(因为 ROI 总是从原图底部开始的全宽)
        roi_mid_y = roi_y + roi_height // 2  # ROI 的 Y 中心点(注意要加上 ROI 的起始 Y 坐标)
        # 绘制 ROI 中心点
        cv2.circle(frame, (roi_mid_x, roi_mid_y), 5, (255, 0, 0), -1)  # 使用蓝色绘制 ROI 中心点
        # 计算并绘制连接线
        cv2.line(frame, (roi_mid_x, roi_mid_y), (cX, cY), (0, 0, 255), 2)  # 使用红色绘制连接线
        # 计算并打印两个点的 X 和 Y 差值
        dx = cX - roi_mid_x
        dy = cY - roi_mid_y
        # 创建包含差值的文本字符串
        text = f"dx={dx}, dy={dy}"
        #text_x 与 text_y 表示文本打印的位置
        text_x = int((roi_mid_x + cX) / 2)  # 线的中点 X 坐标(可能需要根据实际情况调整)
        text_y = int((roi_mid_y + cY) / 2 - 10)  # 线的中点 Y 坐标减去一些空间以容纳文本
        cv2.putText(frame, text, (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1, cv2.LINE_AA)
    elif best_cnt is None: #没找到黑线
        detect=0

        # 返回处理后的帧(可选,也可以直接在原图上操作)
    return frame,mask,dx,dy,detect


# 用于从摄像头读取帧并处理
cap = cv2.VideoCapture(0)

def task():
    # 这里放置你希望每15ms执行一次的任务
    ret, frame = cap.read()
    # 使用下划_忽略接收最后俩个返回的dx,dy的差值
    frame, mask, dxx, _, detect = find_line_midpoint_in_roi(frame)
    if detect == 0:
        Motor_control.stop_motor()
    else:
        if (dxx > 55):
            Motor_control.move_right()
        elif (dxx < -55):
            Motor_control.move_left()
        else:
            Motor_control.move_forward()

def periodic_task():
    while True:
        task()
        time.sleep(0.015)  # 休眠15ms

def main():
    Motor_control.Motor_Init()  #电机初始化
    Motor_control.new_speed=15 #提醒一下可以这样初始化Motor_control模块中的变量
    periodic_task()
    while True:
        # 这里放置你希望每15ms执行一次的任务
        ret, frame = cap.read()
        # 使用下划_忽略接收最后俩个返回的dx,dy的差值
        frame, mask, _, _,_ = find_line_midpoint_in_roi(frame)
        # 显示结果
        cv2.imshow('Frame', frame)
        cv2.imshow('Mask', mask)
        # 按 'q' 键退出
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break


if __name__ == "__main__":
    main()
    cap.release()
    cv2.destroyAllWindows()

最终测试效果视频:

与预期相同,或者效果有些超预期?

反正能巡线了,因为程序原因,摄像头ROI区域识别的实际情况没有在屏幕上打印出来,下篇文章再优化吧......

小车电机开环运动与opencv摄像头巡线

测试自评改进:

后续会对ROI区域进行一些新增,用于识别弯道等更复杂的情况

或者尝试用画回归线连线的方式预测前方寻迹走向

之后优化目标是移除原地转向的逻辑,改为更丝滑的预测回归线后使用差速的方式巡线,

除非检测到类似于直角弯的情况再使用原地转向的运动代码

电源供电方案也有些许问题:树莓派、7寸屏幕、电机这三大块耗电有些大......

程序中的15ms延迟也有问题,影响了cv的窗口创建显示,影响观察识别效果,后续改成多线程程序试试.....

整体工程下载:

https://download.csdn.net/download/qq_64257614/89587652

网上学习资料网址:

树莓派视觉小车 -- OpenCV巡线(HSL色彩空间、PID)_基于数字图像处理的自动巡线-CSDN博客

opencv——实现智能小车巡线_opencv智能小车-CSDN博客

[openCV]基于拟合中线的智能车巡线方案V2_opencv怎么通过两条车道线拟合出中线-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1959704.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

车车科技合纵连横:股价今年以来跌超八成,公司看好未来市场份额

《港湾商业观察》黄懿 6月27日&#xff0c;Cheche Group Inc. (NASDAQ: CCG&#xff0c;下称“车车科技”)宣布&#xff0c;公司已与北京安鹏保险经纪有限公司&#xff08;“北京安鹏”&#xff09;建立战略合作伙伴关系。其中&#xff0c;北京安鹏是北京汽车集团有限公司&…

vllm部署的一些思考

vllm号称利用ray支持多机多卡的方式,链接如下 Distributed Inference and Serving — vLLMhttps://docs.vllm.ai/en/stable/serving/distributed_serving.html但是这种方式只是把非常大的模型如lamma 3.1 ,这个模型有405b,需要用多机多卡的方式进行分布式。 事实上,生产中…

C语言:扫雷游戏实现

一、扫雷游戏的分析和设计 扫雷游戏想必大家都玩过吧&#xff0c;初级的玩法是在一个9*9的棋盘上找到没有雷的格子&#xff0c;而今天我们就要做的就是9*9扫雷游戏的实现。 1、游戏功能和规则 使用控制台实现经典的扫雷游戏游戏可以通过菜单实现继续玩或者退出游戏扫雷的棋盘…

Git 安装教程

1、登录git 官方网站&#xff1a;https://git-scm.com/ 点击左边的 Downloads 或者 右边标识的下载标志&#xff0c;它根据电脑操作系统自动匹配版本 Downloads for Windows 2、以 windows 为例下载对应版本 网络有时可能不大好&#xff0c;阿里镜像下载超快。 下载好以后&a…

传统物流机械锁控的痛点与难点深度剖析

在当今全球化和高度竞争的商业环境中&#xff0c;物流行业作为经济发展的重要支撑&#xff0c;其高效、安全的运营至关重要。而物流锁控&#xff0c;作为保障货物在运输和存储过程中安全的关键环节&#xff0c;传统机械物流锁控方式却面临着诸多严峻的挑战&#xff0c;这些问题…

Linux系统服务——【web,http协议,apache服务和nginx服务】(sixteen day)

一、web基础以及http协议 1、web基本概念和常识 前端开发一般用uniapp. 1、Web:为用户提供的一种在互联网上浏览信息的服务&#xff0c;Web 服务是动态的、可交互的、跨平台的和图形化的。 2、Web 服务为用户提供各种互联网服务&#xff0c;这些服务包括信息浏览服务&#xf…

关于使用pagehelper分页插件 进行mybatis 一对多嵌套查询时 查询数量太多 无法达到你想展示的效果的问题 --已解决

1.问题 先来描述一下问题 我有一个商品表 里面嵌套 1.1如下是我的数据库 一个商品对应两个口味对应三个商品轮播图&#xff0b;两个商品描述图 那可不是1*2*2*3 12 就是分页里面的pageSize 为12 时才能显示完这个商品的完整数据 这是我的sql语句 resultMap映射 2.正确做…

【香橙派系列教程】(二)刷机和系统启动

&#xff08;二&#xff09;刷机和系统启动 文章目录 &#xff08;二&#xff09;刷机和系统启动1.刷机2.基于 Windows PC 将 Linux 镜像烧写到 TF 卡的方法3.Debian 和 Ubuntu 系统使用说明4.串口登录系统5.修改登陆密码6.修改网络配置7.SSH登录开发板8.修改开发板内核启动日志…

学习008-02-04-05 Make a List View Editable(使列表视图可编辑)

Make a List View Editable&#xff08;使列表视图可编辑&#xff09; This lesson explains how to make a List View editable. 本课介绍如何使列表视图可编辑。 The instructions below show how to create new objects of the DemoTask type directly in the Task List V…

MSPM0G3507之电赛小车

一、前言 本文没什么技术分享&#xff0c;纯聊天。以下内容均为笔者的浅薄理解&#xff0c;有不对的地方还请多多包涵。 二、相关配置 主控单元&#xff1a;MSPM0G3507SPTR&#xff08;48角&#xff09; 编译环境&#xff1a;Keil5.33、5.39&#xff08;推荐&#xff09;都可 …

一分钟小课堂!电脑怎么录屏?4款热门软件实操指南

在这个信息超多的时代里&#xff0c;学会电脑怎么录屏可真是一门挺有用的技能。不管是你想做教学视频、录下游戏里好玩的瞬间&#xff0c;还是展示一下工作上的步骤&#xff0c;掌握录屏的方法都能让你的分享更直观、更有效率。今天&#xff0c;咱们就来聊聊四款很火的录屏大师…

vue3-环境变量-JavaScript-axio-基础使用-lzstring-字符串压缩-python

文章目录 1.Vue3环境变量1.1.简介1.2.全局变量的引用1.3.package.json文件 2.axio2.1.promise2.2.安装2.3.配置2.3.1.全局 axios 默认值2.3.2.响应信息格式 2.4.Axios的拦截器2.4.1.请求拦截器2.4.2.响应拦截器2.4.3.移除拦截器2.4.4.自定义实例添加拦截器 3.lz-string3.1.java…

Tantivy使用Rust 开发的全文搜索引擎库

一、概述 Tantivy是一个全文搜索引擎库&#xff0c;灵感来自Apache Lucene&#xff0c;用Rust编写。 如果你正在寻找Elasticsearch或Apache Solr的替代品&#xff0c;请查看我们基于Tantivy构建的分布式搜索引擎Quiuckwit。 Tantivy更接近Apache Lucene&#xff0c;而不是E…

仅需一分钟,使用极空间部署一个强大的开源问卷考试系统『SurveyKing』

仅需一分钟&#xff0c;使用极空间部署一个强大的开源问卷考试系统『SurveyKing』 哈喽小伙伴们好&#xff0c;我是Stark-C~ 我们生活中估计应该都收到了不少的问卷调查吧&#xff1f;很多商家或者运营商都会通过问卷调查的方式了解客户满意度&#xff0c;或者高市场调研&…

数据中台建设之数据汇聚与数据交换

目录 一、数据汇聚 1.1 概述 1.2 汇聚数据类型 1.2.1 结构化数据 1.2.2 半结构化数据 1.2.3 非结构化数据 1.3 汇聚数据模式 1.3.1 概述 1.3.2 离线 1.3.3 实时 1.4 汇聚数据方法 1.4.1 概述 1.4.2 ETL 1.4.3 ELT 1.5 汇聚数据工具 1.5.1 概述 1.5.2 Flink CDC…

Java人力资源招聘社会校招类型招聘系统PC端

&#x1f50d;【揭秘】人力资源新利器&#xff01;社会校招一站式PC端招聘系统全攻略&#x1f680; &#x1f308; 开篇引言&#xff1a;招聘新纪元&#xff0c;效率为王&#xff01; Hey小伙伴们&#xff0c;你是否还在为繁琐的招聘流程头疼不已&#xff1f;&#x1f92f; 面…

Spark累加器(Accumulator)

1.累加器类型&#xff1a; 数值累加器&#xff1a;用于计算总和、计数等。布尔累加器&#xff1a;用于计算满足特定条件的次数。自定义累加器&#xff1a;允许定义复杂的聚合逻辑和数据结构。集合累加器&#xff1a;用于计算唯一元素的数量&#xff0c;处理去重操作。 在 Spar…

Qt Designer,仿作一个ui界面的练习(四):编写代码

一、新建项目&#xff0c;目录结构如图&#xff1a; PYS下存放脚本&#xff0c;SRC下存放资源文件&#xff0c;UIS下存放组态画面文件。 在每个子目录下都有__init__.py文件&#xff0c;系统会自动将其识别为软件包。 其中一个UIS.__init__.py文件的内容&#xff1a; # impo…

手撕数据结构02--二分搜索(附源码)

一、理论基础 二分搜索&#xff0c;也称折半搜索、对数搜索&#xff0c;是一种在有序数组中查找某一特定元素的搜索算法。 二分搜索是一种高效的查找算法&#xff0c;适用于在已排序的数组中查找特定元素。它的基本思想是通过不断将搜索区间对半分割&#xff0c;从而快速缩小…

ROOM数据快速入门

ROOM数据库快速入门 文章目录 ROOM数据库快速入门第一章 准备工作第01节 引入库第02节 布局文件第03节 activity类第04节 效果图 第二章 数据类第01节 实体类&#xff08;表&#xff09;第02节 数据访问类&#xff08;DAO&#xff09;第03节 数据Service层第04节 RoomDataBase …