跑道识别

写在前面

这是一坨屎,主要依靠局部大津法二值化与hsv颜色识别来共同筛选跑道。我绝对不会说这是智能车没拿奖剩下的代码

文章结构

  • python - opencv库的安装与使用
  • hsv图像的颜色筛选
  • 大津法的原理,不足与局部大津法
  • 通过直方图识别像素数量
  • 识别,卡死和优化
  • 可能的改良方向:卷积的初识

第一部分:python opencv安装与使用

这里给到基于树莓派os安装的办法

首先安装numpy库,其为opencv的必要依赖

sudo pip3 install numpy

然后安装opencv(如果必要的话,请换源到清华源

sudo apt-get install python3-opencv

然后等个十来分钟去饮一杯茶先

回来在python3下尝试

import cv2

如果不报错,应该就可以了,如果报错,那是真的牛皮

第二部分:hsv图像的颜色筛选

ps:用opencv进行图像处理总是有一种面向过程编程的奇妙感受wwwww

import cv2
import numpy as np
video = cv2.VideoCapture("hell.mp4")#这里是测试的视频
#video = cv2.VideoCapture(0)#这里是调用第一个摄像头
def line_finder(img=0):
    global video
    ret, img = video.read()#读取你的帧
    #对图像做灰度值处理
    gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    gray_img = cv2.GaussianBlur(gray_img, (gs, gs), 1, 2)
    gray_img = cv2.erode(gray_img, (3, 3), iterations=2)
     # hsv 筛选
    color_dist = {'Lower': np.array([30, 0, 42]), 'Upper': np.array([139, 61, 255])}  # 这一坨是hsv 的参数 
    gs_frame = cv2.GaussianBlur(img, (5, 5), 0)  # 高斯滤波
    hsv = cv2.cvtColor(gs_frame, cv2.COLOR_BGR2HSV)  # 颜色空间转换-hsv
    erode_hsv = cv2.erode(hsv, None, iterations=2)  # 腐蚀
    hsv_got = cv2.inRange(erode_hsv, color_dist['Lower'], color_dist['Upper'])
    cv2.namedWindow("gs", 0);#创建窗口,显示hsv图像
    cv2.resizeWindow("gs", 1920, 1080);
    cv2.imshow("gs", hsv_got)
    cv2.waitKey(20) & 0xff
while True:
    line_finder()

这样就可以把特定hsv范围的图像进行筛选,得到筛选完成的图像

那可能存在的问题就是 那我要怎么才找得到hsv的图像范围捏?

非常好问题,我们可以参考这篇文章的代码

防止学长跑路我就copy过来了

import cv2
import numpy as np
import time

# 'camera' or 'picture'
mode = 'camera'

if mode == 'camera':
    cap = cv2.VideoCapture(0)


def update(x):
    global gs, erode, Hmin, Smin, Vmin, Hmax, Smax, Vmax, img, Hmin2, Hmax2, img0, size_min

    if mode == 'camera':
        ret, img0 = cap.read()
    elif mode == 'picture':
        img0 = cv2.imread('test.jpg')
    img = img0.copy()

    gs = cv2.getTrackbarPos('gs', 'image')
    erode = cv2.getTrackbarPos('erode', 'image')
    Hmin = cv2.getTrackbarPos('Hmin1', 'image')
    Smin = cv2.getTrackbarPos('Smin', 'image')
    Vmin = cv2.getTrackbarPos('Vmin', 'image')
    Hmax = cv2.getTrackbarPos('Hmax1', 'image')
    Smax = cv2.getTrackbarPos('Smax', 'image')
    Vmax = cv2.getTrackbarPos('Vmax', 'image')
    Hmin2 = cv2.getTrackbarPos('Hmin2', 'image')
    Hmax2 = cv2.getTrackbarPos('Hmax2', 'image')
    size_min = cv2.getTrackbarPos('size_min', 'image')
    # 滤波二值化
    gs_frame = cv2.GaussianBlur(img, (gs, gs), 1)
    hsv = cv2.cvtColor(gs_frame, cv2.COLOR_BGR2HSV)
    erode_hsv = cv2.erode(hsv, None, iterations=erode)
    inRange_hsv = cv2.inRange(erode_hsv, np.array([Hmin, Smin, Vmin]), np.array([Hmax, Smax, Vmax]))
    inRange_hsv2 = cv2.inRange(erode_hsv, np.array([Hmin2, Smin, Vmin]), np.array([Hmax2, Smax, Vmax]))
    img = inRange_hsv + inRange_hsv2
    # 外接计算
    cnts = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
    target_list = []
    pos = []
    if size_min < 1:
        size_min = 1
    for c in cnts:
        if cv2.contourArea(c) < size_min:
            continue
        else:
            target_list.append(c)
    for cnt in target_list:
        x, y, w, h = cv2.boundingRect(cnt)
        cv2.rectangle(img0, (x, y), (x + w, y + h), (0, 255, 0), 2)
        pos.append([int(x + w / 2), y + h / 2])
    print(pos)


def img_test():
    sleep = 0.1
    gs = 0
    erode = 0
    Hmin1 = 100
    Hmax1 = 125
    Hmin2 = 179
    Hmax2 = 0
    Smin = 130
    Smax = 255
    Vmin = 50
    Vmax = 240
    size_min = 1000

    # 创建窗口
    cv2.namedWindow('image', cv2.WINDOW_NORMAL)
    cv2.createTrackbar('gs', 'image', 0, 8, update)
    cv2.createTrackbar('erode', 'image', 0, 8, update)
    cv2.createTrackbar('Hmin1', 'image', 0, 179, update)
    cv2.createTrackbar('Hmax1', 'image', 0, 179, update)
    cv2.createTrackbar('Hmin2', 'image', 0, 179, update)
    cv2.createTrackbar('Hmax2', 'image', 0, 179, update)
    cv2.createTrackbar('Smin', 'image', 0, 255, update)
    cv2.createTrackbar('Smax', 'image', 0, 255, update)
    cv2.createTrackbar('Vmin', 'image', 0, 255, update)
    cv2.createTrackbar('Vmax', 'image', 0, 255, update)
    cv2.createTrackbar('size_min', 'image', 1, 100000, update)
    # 默认值
    cv2.setTrackbarPos('gs', 'image', gs)
    cv2.setTrackbarPos('erode', 'image', erode)
    cv2.setTrackbarPos('Hmin1', 'image', Hmin1)
    cv2.setTrackbarPos('Hmax1', 'image', Hmax1)
    cv2.setTrackbarPos('Hmin2', 'image', Hmin2)
    cv2.setTrackbarPos('Hmax2', 'image', Hmax2)
    cv2.setTrackbarPos('Smin', 'image', Smin)
    cv2.setTrackbarPos('Smax', 'image', Smax)
    cv2.setTrackbarPos('Vmin', 'image', Vmin)
    cv2.setTrackbarPos('Vmax', 'image', Vmax)
    cv2.setTrackbarPos('size_min', 'image', size_min)
    while (True):
        try:
            update(1)
        except:
            pass
        cv2.imshow('image', img)
        cv2.imshow('image1', img)
        cv2.imshow('image0', img0)
        time.sleep(sleep)
        if cv2.waitKey(1) == 27:
            break
    cv2.destroyAllWindows()


if __name__ == '__main__':
    img_test()

这样就解决了筛选颜色的问题

第三部分:谁能拒绝大津法

第一节:什么是大津法

大津法(OTSU)是一种确定图像二值化分割阈值的算法,由日本学者大津于1979年提出。从大津法的原理上来讲,该方法又称作最大类间方差法,因为按照大津法求得的阈值进行图像二值化分割后,前景与背景图像的类间方差最大。

它被认为是图像分割中阈值选取的最佳算法,计算简单,不受图像亮度和对比度的影响,因此在数字图像处理上得到了广泛的应用。它是按图像的灰度特性,将图像分成背景和前景两部分。因方差是灰度分布均匀性的一种度量,背景和前景之间的类间方差越大,说明构成图像的两部分的差别越大,当部分前景错分为背景或部分背景错分为前景都会导致两部分差别变小。因此,使类间方差最大的分割意味着错分概率最小。

应用:是求图像全局阈值的最佳方法,应用不言而喻,适用于大部分需要求图像全局阈值的场合。

优点:计算简单快速,不受图像亮度和对比度的影响。

缺点:对图像噪声敏感;只能针对单一目标分割;当目标和背景大小比例悬殊、类间方差函数可能呈现双峰或者多峰,这个时候效果不好。

所以我们亟需解决的包括跑道的噪点,如何准确的对跑道进行二值化

第二节:局部大津法

当一幅图像中包含噪声和非均匀光照时,全局阈值分割法不再有效,因此应该采用基于图像局部特性的阈值分割方法(局部均值和局部方差)。OPENCV中提供了两种方法:平均、高斯加权,调用函数:

cv2.adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C, dst=None)

这个函数大致意思就是把图片每个像素点作为中心取N*N的区域,然后计算这个区域的阈值,来决定这个像素点变0还是变255

src:需要进行二值化的一张灰度图像

maxValue:满足条件的像素点需要设置的灰度值。(将要设置的灰度值)

adaptiveMethod:自适应阈值算法。可选ADAPTIVE_THRESH_MEAN_C 或 ADAPTIVE_THRESH_GAUSSIAN_C

thresholdType:opencv提供的二值化方法,只能THRESH_BINARY或者THRESH_BINARY_INV

blockSize:要分成的区域大小,上面的N值,一般取奇数

C:常数,每个区域计算出的阈值的基础上在减去这个常数作为这个区域的最终阈值,可以为负数

dst:输出图像,可以忽略

前两个参数与threshold的src和maxval一样相同

其实很有卷积的味道捏(

这个我们稍后将进行探讨,目前来看代码的实现

加上hsv的筛选,我们把两个图像筛选的内容合二为一

import cv2
import numpy as np
video = cv2.VideoCapture("hell1.mp4")


def line_finder(img=0):
    global video

    ret, img = video.read()
    # img = img[200:900, 100:1820]

    # ray_img = cv2.cvtColor(img, None, cv2.COLOR_BGR2HSV)
    gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    gray_img = cv2.GaussianBlur(gray_img, (gs, gs), 1, 2)
    gray_img = cv2.erode(gray_img, (3, 3), iterations=2)
    # hsv

    min_dff = cv2.adaptiveThreshold(gray_img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 81, -7)
    #注意到了吗,这里就是局部二值化的方法,参数不必照搬,请务必多多摸索

    djf = min_dff

    qwq, djf = cv2.threshold(djf, 254, 255, cv2.THRESH_BINARY)
    # hsv 筛选
    color_dist = {'Lower': np.array([30, 0, 42]), 'Upper': np.array([139, 61, 255])}  # 这一坨是hsv 的参数 希望有苦力来调一下
    gs_frame = cv2.GaussianBlur(img, (5, 5), 0)  # 高斯滤波
    hsv = cv2.cvtColor(gs_frame, cv2.COLOR_BGR2HSV)  # 颜色空间转换-hsv
    erode_hsv = cv2.erode(hsv, None, iterations=2)  # 腐蚀
    hsv_got = cv2.inRange(erode_hsv, color_dist['Lower'], color_dist['Upper'])

    # 两个图层之合成
    hsv_got = ~hsv_got
    djf = ~djf
    finally_got = djf + hsv_got
    finally_got = ~finally_got
    finally_got = cv2.dilate(finally_got, (5, 30), 5)
    line, left_line, right_line = find_value(finally_got)
    cv2.namedWindow("origin_Pic", 0);
    cv2.resizeWindow("origin_Pic", 1920, 1080);
    cv2.imshow("origin_Pic", img)
    cv2.namedWindow("gs", 0);
    cv2.resizeWindow("gs", 1920, 1080);
    cv2.imshow("gs", hsv_got)
    cv2.namedWindow("shit", 0);
    cv2.resizeWindow("shit", 1920, 1080);
    cv2.imshow("shit", finally_got)

    cv2.waitKey(20) & 0xff

这样我们就得到了几乎完美的跑道图像了,下一步就是把跑道从这一堆噪点里面抓取出来

第四部分:通过直方图识别像素数量

为了从跑道的噪点中识别,我想到了这样一个方法

取得某一块图像上的区域,不用太高,但是长度和图像对齐

从区域中心(其实也是图像中心)向左右两侧开始逐行“扫描”,计算每一列白色(也就是识别到的像素)数量,如果超过取得区域高度的某一个比例(比如一半)就认为这里是跑道。

这样一是去除了噪点的干扰,又可以增强程序的鲁棒性,妙啊妙啊

那么又引出下一个问题,如何找到要多少像素才让程序认为这个是跑道呢

所以我们借助直方图的威力

from numba import jit

"""
请把上面的图像传递进函数以绘制直方图
"""

def find_value(img11):
    # 计算灰度直方图
    global left_start, right_start, up_start, down_start
    finally_list = []

    mid_num = int((right_start + left_start) / 2)
    for i in range(left_start, right_start):
        # error_now = i - mid_num
        n = 0
        for j in range(up_start, down_start):
            if img11[j, i] >= 128:
                n = n + 1
            else:
                pass
        finally_list.append(n)
    print(finally_list)

    x = np.array(range(0,len(finally_list)))
    y = np.array(finally_list)
    plt.bar(x,y,0.8)
    plt.show()

在得到直方图之后,观察跑道附近的像素值与噪点像素值,找到一个足以忽略噪点但是又不足以使跑道被忽略的值,你就完成了这一部分的工作。,恭喜!

第五部分:识别,卡死和优化

那么我们把所有东西合在一起,加上绘制识别的线,去掉直方图的绘制,你会发现代码跑起来卡得一坨屎

问题来源于python语言的特殊性,其解释型语言对于反复的循环一直遍历图像效率非常低下,所以我们利用numba库来解决这个问题,numba库中的@jit修饰器可以直接让被修饰的函数得以先编译后运行,从而提高其循环效率

那么看看吧

import cv2
import numpy as np
import time

from numba import jit
#  直方图绘制所需
import matplotlib.pyplot as plt



left_start = 100
right_start = 1819
up_start = 320
down_start = 350
pressed_pix = 8
video = cv2.VideoCapture("test_1.mp4")

@jit
def find_value(img11):
    # 计算灰度直方图
    global left_start, right_start, up_start, down_start
    finally_list = []

    mid_num = int((right_start + left_start) / 2)
    for i in range(left_start, right_start):
        # error_now = i - mid_num
        n = 0
        for j in range(up_start, down_start):
            if img11[j, i] >= 128:
                n = n + 1
            else:
                pass
        finally_list.append(n)
    #print(finally_list)
    '''
    x = np.array(range(0,len(finally_list)))
    y = np.array(finally_list)
    plt.bar(x,y,0.8)
    plt.show()
    '''
    left_line = left_start
    right_line = right_start
    # fina_error = int(fina_error/sigema + mid_num)
    for i in range(mid_num, right_start-left_start):
        if finally_list[i] >= pressed_pix:
            right_line = i + left_start
            #print(i)
            break
        else:
            pass
    for i in range(mid_num, 0, -1):
        if finally_list[i] >= 1+pressed_pix:
            left_line = i + left_start
            #print(i)
            break
        else:
            pass
    line = (left_line + right_line) / 2
    return line, left_line, right_line


# 'camera' or 'picture'
gs = 9
mode = 'picture'


def line_finder(img=0):
    global video
    Hmin = 0
    Smin = 0
    Vmin = 0

    ret, img = video.read()
    # img = img[200:900, 100:1820]

    # ray_img = cv2.cvtColor(img, None, cv2.COLOR_BGR2HSV)
    gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    gray_img = cv2.GaussianBlur(gray_img, (gs, gs), 1, 2)
    gray_img = cv2.erode(gray_img, (3, 3), iterations=2)
    # hsv

    min_dff = cv2.adaptiveThreshold(gray_img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 81, -7)

    djf = min_dff

    qwq, djf = cv2.threshold(djf, 254, 255, cv2.THRESH_BINARY)
    # hsv 筛选
    color_dist = {'Lower': np.array([30, 0, 42]), 'Upper': np.array([139, 61, 255])}  # 这一坨是hsv 的参数 希望有苦力来调一下
    gs_frame = cv2.GaussianBlur(img, (5, 5), 0)  # 高斯滤波
    hsv = cv2.cvtColor(gs_frame, cv2.COLOR_BGR2HSV)  # 颜色空间转换-hsv
    erode_hsv = cv2.erode(hsv, None, iterations=2)  # 腐蚀
    hsv_got = cv2.inRange(erode_hsv, color_dist['Lower'], color_dist['Upper'])

    # 两个图层之合成
    hsv_got = ~hsv_got
    djf = ~djf
    finally_got = djf + hsv_got
    finally_got = ~finally_got
    finally_got = cv2.dilate(finally_got, (5, 30), 5)
    line, left_line, right_line = find_value(finally_got)

    #print(line)
    cv2.rectangle(finally_got, (left_start, up_start), (right_start, down_start), (255, 255, 255), 3)
    cv2.rectangle(finally_got, (960, up_start), (960, down_start), (255, 255, 255), 3)
    cv2.rectangle(img, (left_line, up_start), (right_line, down_start), (255, 255, 255), 3)
    cv2.line(img,(int(line), int(down_start)), (int(line), int(up_start)),(255,255,255), 3)
    #cv2.rectangle(img, (line, up_start), (line, down_start), (255, 255, 255), 3)

    # 别管
    cv2.namedWindow("origin_Pic", 0);
    cv2.resizeWindow("origin_Pic", 1920, 1080);
    cv2.imshow("origin_Pic", img)
    cv2.namedWindow("gs", 0);
    cv2.resizeWindow("gs", 1920, 1080);
    cv2.imshow("gs", hsv_got)
    cv2.namedWindow("shit", 0);
    cv2.resizeWindow("shit", 1920, 1080);
    cv2.imshow("shit", finally_got)
    # inRange_hsv = cv2.inRange(erode_hsv, np.array([Hmin, Smin, Vmin]), np.array([Hmax, Smax, Vmax]))
    # end_time = time.time()

    # delta_time = end_time - star_time
    # fps = (1 / delta_time)
    # print(fps)
    # 下面这个就是画直方图的,
    # calcGrayHist(djf)

    cv2.waitKey(20) & 0xff


def sewer_finder():
    global video
    ret, img = video.read()
    color_dist = {'Lower': np.array([0, 0, 0]), 'Upper': np.array([130, 255, 255])}  # 这一坨是hsv 的参数 请调到蓝色部分
    # img = img[300:900, 100:1820]
    gs_frame = cv2.GaussianBlur(img, (5, 5), 0)  # 高斯滤波
    hsv = cv2.cvtColor(gs_frame, cv2.COLOR_BGR2HSV)  # 颜色空间转换-hsv
    erode_hsv = cv2.erode(hsv, None, iterations=2)  # 腐蚀
    fina_out = cv2.inRange(erode_hsv, color_dist['Lower'], color_dist['Upper'])  # hsv 抠图二值化
    cv2.imshow("gs", fina_out)
    cv2.waitKey(20) & 0xff
    '''
    以上是hsv抠图部分
    以下是识别面积与 位置
    还没写完qwq
    '''


while True:
    # star_time = time.time()
    line_finder()

这样其实还是可以再加以完善,但是因为covid 的原因(其实不只是疫情),我不得不离开学校

所以留下了一些可以改良的点:

第六部分:卷积

你好,请观看视频

如果你恰好没有妙妙工具来观看这个视频

【官方双语】那么……什么是卷积?_哔哩哔哩_bilibili

如果你有头绪用代码实现里面的边缘检测,请给我发邮件,谢谢。

暂时无法播放,可回源网站播放