欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

OpenCV停車場(chǎng)車位實(shí)時(shí)檢測(cè)項(xiàng)目實(shí)踐

 更新時(shí)間:2022年04月11日 11:09:32   作者:翻滾的小@強(qiáng)  
本文主要介紹了OpenCV停車場(chǎng)車位實(shí)時(shí)檢測(cè)項(xiàng)目實(shí)踐,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

1. 寫在前面

今天整理OpenCV入門的第三個(gè)實(shí)戰(zhàn)小項(xiàng)目,前面的兩篇文章整理了信用卡數(shù)字識(shí)別以及文檔OCR掃描, 大部分用到的是OpenCV里面的基礎(chǔ)圖像預(yù)處理技術(shù),比如輪廓檢測(cè),邊緣檢測(cè),形態(tài)學(xué)操作,透視變換等, 而這篇文章的項(xiàng)目呢,不僅需要一些基礎(chǔ)的圖像預(yù)處理,還需要搭建模型進(jìn)行識(shí)別和預(yù)測(cè),所以通過這個(gè)項(xiàng)目,能把圖像預(yù)處理以及建模型等一整套流程拉起來,并應(yīng)用到實(shí)際的應(yīng)用場(chǎng)景,還是非常有意思的。

停車場(chǎng)車位實(shí)時(shí)檢測(cè)任務(wù),是拿到停車場(chǎng)的一段視頻video,主要完成兩件事情:

  • 檢測(cè)整個(gè)停車場(chǎng)當(dāng)中,當(dāng)前一共有多少輛車,一共有多少個(gè)空余的車位
  • 把空余的停車位標(biāo)識(shí)出來,這樣用戶停車的時(shí)候,就可以直接去空余的停車位處, 為停車節(jié)省了很多時(shí)間

所以這個(gè)項(xiàng)目還是非常有實(shí)踐應(yīng)用價(jià)值的,用了大約一天半的時(shí)間搞定這個(gè)項(xiàng)目,參考的是唐老師的OpenCV入門教程視頻, 不過這里面對(duì)于這個(gè)任務(wù)做的相對(duì)粗糙,我在這個(gè)基礎(chǔ)上基于我的理解進(jìn)行了一些優(yōu)化,主要改動(dòng)如下:

  • 據(jù)處理方面,按列框出停車位之后,我對(duì)每一列框的坐標(biāo)手工進(jìn)行了調(diào)整,確保每個(gè)停車位不遺漏,不多余, 然后是對(duì)每個(gè)停車位的坐標(biāo)位置進(jìn)行了微調(diào),盡量讓其標(biāo)記的準(zhǔn)一些
  • 模型方面,原視頻采用遷移學(xué)習(xí)方式,基于keras對(duì)VGG網(wǎng)絡(luò)進(jìn)行的微調(diào),而我模型這里統(tǒng)一基于pytorch,用的ResNet32預(yù)訓(xùn)練模型進(jìn)行的finetune,驗(yàn)證集正確率能到0.94多,但第一版還是有少量預(yù)測(cè)的不是很準(zhǔn),所以又基于已有的幀圖片做了數(shù)據(jù)增強(qiáng),額外增加了一些數(shù)據(jù),把準(zhǔn)確率提升到0.98左右
  • 項(xiàng)目的整體架構(gòu)全部改變,算是聽懂了上面的思想,然后基于自己的理解進(jìn)行的重構(gòu),好處是后面可以進(jìn)行各種優(yōu)化,按照自己需求做數(shù)據(jù)增強(qiáng),數(shù)據(jù)預(yù)處理以及訓(xùn)練各種高級(jí)模型等。

不過,發(fā)現(xiàn)小resnet就夠強(qiáng)大的了,最終的預(yù)測(cè)效果如下:

在這里插入圖片描述

這是視頻中的某一幀圖像,實(shí)際運(yùn)行的時(shí)候,是讀入視頻,快速分開幀,每一幀做出這樣的預(yù)測(cè)標(biāo)記,然后實(shí)時(shí)顯示。這樣在每個(gè)時(shí)刻,都能動(dòng)態(tài)的知道該停車場(chǎng)有哪些車位空了出來。

下面就對(duì)這個(gè)項(xiàng)目中用到的關(guān)鍵技術(shù)進(jìn)行整理,由于這個(gè)項(xiàng)目稍微大一些,代碼量多,不可能在這里全部展示,但想記錄下對(duì)于這個(gè)項(xiàng)目我的思考過程,以及各種處理的動(dòng)機(jī),以及如何進(jìn)行的處理,我覺得這個(gè)才是對(duì)以后有用的東西。

2. 整體流程梳理

首先,拿到這個(gè)任務(wù)之后, 得大致上梳理下流程,才能確定行動(dòng)方案。 我們開始拿到了這樣的一段視頻,那么為了完成上面停車位檢測(cè)以及識(shí)別的任務(wù),就需要考慮兩步:

  • 我得先把停車場(chǎng)的每個(gè)停車位給提取出來
  • 有了每個(gè)停車位,我訓(xùn)練一個(gè)模型,去預(yù)測(cè)這個(gè)停車位上有沒有車就行啦,把沒有車的標(biāo)識(shí)出來,然后統(tǒng)計(jì)個(gè)數(shù)

其實(shí)宏觀上就這么兩大步。那么后面的問題就是怎么把每個(gè)車位提取出來,又怎么訓(xùn)練模型預(yù)測(cè)呢?

我這里主要分為了兩大步, 數(shù)據(jù)預(yù)處理以及模型的訓(xùn)練及預(yù)測(cè):

數(shù)據(jù)預(yù)處理方面

  • 以視頻中某一幀的圖像為單位,進(jìn)行處理
  • 通過二值化,灰度化,邊緣檢測(cè),特定點(diǎn)標(biāo)定連線等,把圖片中多余的部分去掉,只保留停車場(chǎng)內(nèi)的這部分對(duì)象
  • 霍夫變換的直線檢測(cè),去找圖片中的直線,根據(jù)直線坐標(biāo),先按列為單位,把車位按列框起來, 然后對(duì)框手動(dòng)微調(diào)
  • 在每一列中,鎖定每個(gè)停車位的位置,并對(duì)每個(gè)停車位進(jìn)行標(biāo)號(hào),把這個(gè)保存成字典
  • 有了每個(gè)停車位的位置,就能提取出對(duì)應(yīng)圖片,可以作為后面模型的訓(xùn)練以及驗(yàn)證的數(shù)據(jù)集,不過需要人工手動(dòng)劃分

通過上面步驟,會(huì)積累一些數(shù)據(jù),大約800多張圖片,接下來就可以訓(xùn)練模型,但是由于數(shù)據(jù)量太少,從頭訓(xùn)練模型往往效果不好,所以這里采用遷移學(xué)習(xí)的方式,使用了預(yù)訓(xùn)練的resnet34,用這800多張圖片微調(diào)。

訓(xùn)練好了模型保存,接下來,對(duì)于每一幀圖像,有了停車位位置字典,就能直接提取出每一個(gè)停車位,然后對(duì)于這每個(gè)停車位,模型預(yù)測(cè)有沒有車即可

所以有了這樣的一個(gè)流程,就能再進(jìn)一步分解細(xì)化,就可以大處著眼小處著手啦,下面整理每一步里面的關(guān)鍵細(xì)節(jié)。

3. 數(shù)據(jù)預(yù)處理

3.1 背景過濾

首先,把一幀圖像讀入進(jìn)來,原始圖像如下:

在這里插入圖片描述

先通過二值化的方式過濾掉背景,突出重要信息,然后轉(zhuǎn)成灰度圖。

def select_rgb_white_yellow(image):
    # 過濾背景
    lower = np.uint8([120, 120, 120])
    upper = np.uint8([255, 255, 255])
    # 三個(gè)通道內(nèi),低于lower和高于upper的部分分別變成0, 在lower-upper之間的值變成255, 相當(dāng)于mask,過濾背景
    # 保留了像素值在120-255之間的像素值
    white_mask = cv2.inRange(image, lower, upper)
    masked_img = cv2.bitwise_and(image, image, mask=white_mask)
    return masked_img
masked_img = select_rgb_white_yellow(test_image)

這里看到inRange(),想到了之前用到的二值化的方法threshold, 我在想這倆有啥區(qū)別? 為啥這里不用這個(gè)了? 下面是我經(jīng)過探索得到的幾點(diǎn)使用經(jīng)驗(yàn):

  • cv2.threshold(src, thresh, maxval, type[, dst]):針對(duì)的是單通道圖像(灰度圖), 二值化的標(biāo)準(zhǔn),type=THRESH_BINARY: if x > thresh, x = maxval, else x = 0, 而type=THRESH_BINARY_INV: 和上面的標(biāo)準(zhǔn)反著,目前常用到了這倆個(gè)
  • cv2.inRange(src, lowerb, upperb):可以是單通道圖像,可以是三通道圖像,也可以進(jìn)行二值化,標(biāo)準(zhǔn)是if x >= lower and x <= upper, x = 255, else x = 0

這里做了一個(gè)實(shí)驗(yàn), 事先把圖片轉(zhuǎn)化成灰度圖warped = cv2.cvtColor(test_image, cv2.COLOR_BGR2GRAY),然后下面兩句代碼的執(zhí)行結(jié)果是一樣的:

  • cv2.threshold(warped, 119, 255, cv2.THRESH_BINARY)[1]
  • cv2.inRange(warped, 120, 255)

處理之后的圖片長(zhǎng)這樣:

在這里插入圖片描述

3.2 Canny邊緣檢測(cè)

接下來,采用Canny邊緣檢測(cè)算法,檢測(cè)出邊緣來

low_threshold, high_threshold = 50, 200
edges_img = cv2.Canny(gray_img, low_threshold, high_threshold)

結(jié)果如下:

在這里插入圖片描述

下面嘗試把停車場(chǎng)這塊提取出來, 把其余那些沒用的去掉。

3.3 停車場(chǎng)區(qū)域提取

這里的思路就是,先用6個(gè)標(biāo)定點(diǎn)把停車場(chǎng)的這幾個(gè)角給他定住,這個(gè)標(biāo)定點(diǎn)得需要自己找。 找到之后, 采用OpenCV中的填充函數(shù),就能制作一個(gè)mask矩陣,然后就能把其余部分去掉了。

def select_region(image):
    """這里手動(dòng)選擇區(qū)域"""
    rows, cols = image.shape[:2]
    
    # 下面定義6個(gè)標(biāo)定點(diǎn), 這個(gè)點(diǎn)的順序必須讓它化成一個(gè)區(qū)域,如果調(diào)整,可能會(huì)交叉起來,所以不要?jiǎng)?
    pt_1  = [cols*0.06, rows*0.90]   # 左下
    pt_2 = [cols*0.06, rows*0.70]    # 左上
    pt_3 = [cols*0.32, rows*0.51]    # 中左
    pt_4 = [cols*0.6, rows*0.1]      # 中右
    pt_5 = [cols*0.90, rows*0.1]     # 右上
    pt_6 = [cols*0.90, rows*0.90]    # 右下
    
    vertices = np.array([[pt_1, pt_2, pt_3, pt_4, pt_5, pt_6]], dtype=np.int32)
    point_img = image.copy()
    point_img = cv2.cvtColor(point_img, cv2.COLOR_GRAY2BGR)
    for point in vertices[0]:
        cv2.circle(point_img, (point[0], point[1]), 10, (0, 0, 255), 4)
    # cv_imshow('points_img', point_img)
    
    # 定義mask矩陣, 只保留點(diǎn)內(nèi)部的區(qū)域
    mask = np.zeros_like(image)
    if len(mask.shape) == 2:
        cv2.fillPoly(mask, vertices, 255)   # 點(diǎn)框住的地方填充為白色
        #cv_imshow('mask', mask)
    roi_image = cv2.bitwise_and(image, mask)
    return roi_image

roi_image = select_region(edges_img)

處理的效果如下:

在這里插入圖片描述

這樣處理好了,我們就需要找到這里面的直線,然后通過直線去猜測(cè)大致的位置。

3.4 霍夫變換檢測(cè)直線

這里采用霍夫變換檢測(cè)直線, 函數(shù)是cv2.HoughLinesP, 該函數(shù)能檢測(cè)直線的兩個(gè)端點(diǎn)(x0,y0, x1, y1)。函數(shù)原型:

HoughLinesP(image, rho, theta, threshold[, lines[, minLineLength[, maxLineGap]]]) -> lines

  • image: 邊緣檢測(cè)的輸出圖像,這里要注意必須是邊緣檢測(cè)的輸出圖像
  • rho: 參數(shù)極徑r以像素值為單位的分辨率,一般1
  • threa: 以弧度為單位的分辨率,一般1
  • threshold: 檢測(cè)一條直線所需最少的曲線交點(diǎn)
  • minLineLength: 能形成一條直線的最小長(zhǎng)度,太短,不認(rèn)為是一條直線
  • maxLineGap: 兩條直線直接最大間隔,小于這個(gè)值,認(rèn)為是一條直線

所以,這個(gè)函數(shù)拿來直接用。

def hough_lines(image):
    # 輸入的圖像需要是邊緣檢測(cè)后的結(jié)果
    # minLineLengh(線的最短長(zhǎng)度,比這個(gè)短的都被忽略)和MaxLineCap(兩條直線之間的最大間隔,小于此值,認(rèn)為是一條直線)
    # rho距離精度,theta角度精度,threshod超過設(shè)定閾值才被檢測(cè)出線段
    return cv2.HoughLinesP(image, rho=0.1, theta=np.pi/10, threshold=15, minLineLength=9, maxLineGap=4)

list_of_lines = hough_lines(roi_image)  # (2338, 1, 4)

竟然檢測(cè)到了2338條直線,這里面肯定有很多不能用的,所以后面處理,需要對(duì)直線先進(jìn)行一波篩選。篩選原則是線不能是斜的,且水平方向不能太長(zhǎng)或者是太短。 具體代碼下面會(huì)看到,這里先展示下過濾之后的效果。

在這里插入圖片描述

過濾完了,總共628條直線。

3.5 以列為單位,劃分停車位

下面的代碼會(huì)稍微復(fù)雜,所以需要分塊講思路。

首先,我們拿到了停車場(chǎng)的直線以及它的坐標(biāo)位置。 過濾操作已經(jīng)做好,接下來,就是對(duì)每條直線進(jìn)行排序。 讓這些線,從一列一列的,從上往下依次排列好。

def identity_blocks(image, lines, make_copy=True):
    if make_copy:
        new_image = image.copy()
    
    # 過濾部分直線
    stayed_lines = []
    for line in lines:
        for x1, y1, x2, y2 in line:
            # 這里是過濾直線,必須保證不能是斜的線,且水平方向不能太長(zhǎng)或者太短
            if abs(y2-y1) <=1 and abs(x2-x1) >=25 and abs(x2-x1) <= 55:
                stayed_lines.append((x1,y1,x2,y2))
    
    # 對(duì)直線按照x1排序, 這樣能讓這些線從上到下排列好, 這個(gè)排序是從第一列的第一條橫線,往下走,然后是第二列第一條橫線往下,...
    list1 = sorted(stayed_lines, key=operator.itemgetter(0, 1))

排列好之后,遍歷所有線, 看看相鄰兩條線之間的距離,如果是一列, 那么兩條線的x_1應(yīng)該離得非常近,畢竟是同一列,如果這個(gè)值太大了,說明是下一列了。根據(jù)這個(gè)準(zhǔn)則,遍歷完之后,就能把這些線劃分到不同的列里面。這里是用了一個(gè)字典,鍵表示列,值表示每一列里面的直線。

代碼接上:

	# 找到多個(gè)列,相當(dāng)于每列是一排車
    clusters = collections.defaultdict(list)
    dIndex = 0
    clus_dist = 10   # 每一列之間的那個(gè)距離
    for i in range(len(list1) - 1):
        # 看看相鄰兩條線之間的距離,如果是一列的,那么x1這個(gè)距離應(yīng)該很近,畢竟是同一列上的
        # 如果這個(gè)值大于10了,說明是下一列的了,此時(shí)需要移動(dòng)dIndex, 這個(gè)表示的是第幾列 
        distance = abs(list1[i+1][0] - list1[i][0])
        if distance <= clus_dist:
            clusters[dIndex].append(list1[i])
            clusters[dIndex].append(list1[i+1])
        else:
            dIndex += 1

有了每一列里面的直線,下面就是就是遍歷每一列,先拿到所有直線,然后找到縱坐標(biāo)的最大值和最小值,以及橫坐標(biāo)的最大和最小值,但由于橫坐標(biāo)這里,首尾列都一排車位,中間排都是兩列,不好直接取到最大最小坐標(biāo),所以這里采用了求平均的方式。 這樣遍歷完,針對(duì)每一列,就能得到左上角點(diǎn)和右下角點(diǎn),這是一個(gè)矩形框。

代碼接上:

	# 得到每列停車位的矩形框
    rects = {}
    i = 0
    for key in clusters:
        all_list = clusters[key]
        cleaned = list(set(all_list))
        # 有5個(gè)停車位至少
        if len(cleaned) > 5:
            cleaned = sorted(cleaned, key=lambda tup: tup[1])
            avg_y1 = cleaned[0][1]
            avg_y2 = cleaned[-1][1]
            if abs(avg_y2-avg_y1) < 15:
                continue
            avg_x1 = 0
            avg_x2 = 0
            for tup in cleaned:
                avg_x1 += tup[0]
                avg_x2 += tup[2]
            avg_x1 = avg_x1 / len(cleaned)
            avg_x2 = avg_x2 / len(cleaned)
            
            rects[i] = [avg_x1, avg_y1, avg_x2, avg_y2]
            i += 1
    print('Num Parking Lanes: ', len(rects))

下面,把矩形框畫出來:

	# 把列矩形畫出來
    buff = 7
    for key in rects:
        tup_topLeft = (int(rects[key][0] - buff), int(rects[key][1]))
        tup_botRight = (int(rects[key][2] + buff), int(rects[key][3]))
        cv2.rectangle(new_image, tup_topLeft, tup_botRight, (0, 255, 0), 3)
    return new_image, rects

這里的buff,也是進(jìn)行了一點(diǎn)微調(diào)操作。 這種是根據(jù)實(shí)際場(chǎng)景來的,不是死的。 效果如下:

在這里插入圖片描述

這樣就會(huì)發(fā)現(xiàn),對(duì)于每一列的停車位,有了大致上的矩形框標(biāo)定,但是這個(gè)非常粗糙。 原視頻里面就基于這個(gè)往后面走了。 我這里對(duì)于每一列框進(jìn)行微調(diào),因?yàn)檫@個(gè)框非常重要。不準(zhǔn)的話影響后面的具體車位劃分。

def rect_finetune(image, rects, copy_img=True):
    if copy_img:
        image_copy = image.copy()
    # 下面需要對(duì)上面的框進(jìn)行坐標(biāo)微調(diào), 讓框更加準(zhǔn)確
    # 這個(gè)框很重要,影響后面停車位的統(tǒng)計(jì),盡量不能有遺漏
    for k in rects:
        if k == 0:
            rects[k][1] -= 10
        elif k == 1:
            rects[k][1] -= 10
            rects[k][3] -= 10
        elif k == 2 or k == 3 or k == 5:
            rects[k][1] -= 4
            rects[k][3] += 13
        elif k == 6 or k == 8:
            rects[k][1] -= 18
            rects[k][3] += 12
        elif k == 9:
            rects[k][1] += 10
            rects[k][3] += 10
        elif k == 10:
            rects[k][1] += 45
        elif k == 11:
            rects[k][3] += 45
    
    buff = 8
    for key in rects:
        tup_topLeft = (int(rects[key][0]-buff), int(rects[key][1]))
        tup_botRight = (int(rects[key][2]+buff), int(rects[key][3]))
        cv2.rectangle(image_copy, tup_topLeft, tup_botRight, (0, 255, 0), 3)
    
    return image_copy, rects

微調(diào)之后的效果如下:

在這里插入圖片描述

原則就是不遺漏,不多余。

3.6 鎖定每個(gè)停車位

這里就是針對(duì)每個(gè)矩形框, 對(duì)里面的停車位用直線切割成一個(gè)個(gè)的,每個(gè)停車位用(x1,y1,x2,y2)標(biāo)識(shí),左上角和右下角的坐標(biāo)。并進(jìn)行標(biāo)號(hào),最終形成一個(gè)字典,字典的鍵就是位置,值就是序號(hào)。當(dāng)然,這里的一個(gè)細(xì)節(jié),依然是中間排是兩排,首尾是一排,這個(gè)在具體劃分停車位的時(shí)候,一定要注意。

def draw_parking(image, rects, make_copy=True, save=True):
    gap = 15.5
    spot_dict = {}  # 一個(gè)車位對(duì)應(yīng)一個(gè)位置
    tot_spots = 0
    
    #微調(diào)
    adj_x1 = {0: -8, 1:-15, 2:-15, 3:-15, 4:-15, 5:-15, 6:-15, 7:-15, 8:-10, 9:-10, 10:-10, 11:0}
    adj_x2 = {0: 0, 1: 15, 2:15, 3:15, 4:15, 5:15, 6:15, 7:15, 8:10, 9:10, 10:10, 11:0}
    fine_tune_y = {0: 4, 1: -2, 2: 3, 3: 1, 4: -3, 5: 1, 6: 5, 7: -3, 8: 0, 9: 5, 10: 4, 11: 0}
    
    for key in rects:
        tup = rects[key]
        x1 = int(tup[0] + adj_x1[key])
        x2 = int(tup[2] + adj_x2[key])
        y1 = int(tup[1])
        y2 = int(tup[3])
        cv2.rectangle(new_image, (x1, y1),(x2,y2),(0,255,0),2)
        
        num_splits = int(abs(y2-y1)//gap)
        for i in range(0, num_splits+1):
            y = int(y1+i*gap) + fine_tune_y[key]
            cv2.line(new_image, (x1, y), (x2, y), (255, 0, 0), 2)
        if key > 0 and key < len(rects) - 1:
            # 豎直線
            x = int((x1+x2) / 2)
            cv2.line(new_image, (x, y), (x, y2), (0, 0, 255), 2)
        
        # 計(jì)算數(shù)量   除了第一列和最后一列,中間的都是兩列的
        if key == 0 or key == len(rects) - 1:
            tot_spots += num_splits + 1
        else:
            tot_spots += 2 * (num_splits + 1)
        
        # 字典對(duì)應(yīng)好
        if key == 0 or key == len(rects) - 1:
            for i in range(0, num_splits+1):
                cur_len = len(spot_dict)
                y = int(y1 + i * gap) + fine_tune_y[key]
                spot_dict[(x1, y, x2, y+gap)] = cur_len + 1
        else:
            for i in range(0, num_splits+1):
                cur_len = len(spot_dict)
                y = int(y1 + i * gap) + fine_tune_y[key]
                x = int((x1+x2) / 2)
                spot_dict[(x1, y, x, y+gap)] = cur_len + 1
                spot_dict[(x, y, x2, y+gap)] = cur_len + 2
  
    return new_image, spot_dict

這里的fine_tune_y也是我后來加上去的,也是為了讓每一列盡量把車位劃分的準(zhǔn)確些。

在這里插入圖片描述

從這個(gè)效果上來看,基本上就把車位一個(gè)個(gè)的劃分開了,劃分開之后,會(huì)發(fā)現(xiàn),這里面有些并不是車位, 但依然給框住了。這樣統(tǒng)計(jì)個(gè)數(shù)的時(shí)候,以及后面給信息停車的時(shí)候會(huì)受到影響,所以我這里又一一排查,去掉了這些無效的車位。

# 去掉多余的停車位
invalid_spots = [10, 11, 33, 34, 37, 38, 61, 62, 93, 94, 95, 97, 98, 135, 137, 138, 187, 249, 
           250, 253, 254, 323, 324, 327, 328, 467, 468, 531, 532]
valid_spots_dict = {}
cur_idx = 1
for k, v in spot_dict.items():
    if v in invalid_spots:
        continue
    valid_spots_dict[k] = cur_idx
    cur_idx += 1

這樣,還可以把處理好的車位信息進(jìn)行可視化,再進(jìn)行微調(diào),不過,我這里由于之前的一些微調(diào)操作,感覺效果還可以,就沒有做任何調(diào)整啦。

# 把每一個(gè)有效停車位標(biāo)記出來
tmp_img = test_image.copy()
for k, v in valid_spots_dict.items():
    cv2.rectangle(tmp_img, (int(k[0]), int(k[1])),(int(k[2]),int(k[3])), (0,255,0) , 2)
cv_imshow('valid_pot', tmp_img)

效果如下:

在這里插入圖片描述

如果要想讓后面模型對(duì)于每個(gè)車位預(yù)測(cè)的更加準(zhǔn)確,這里的劃分一定要盡量的細(xì)致和標(biāo)準(zhǔn)。 否則如果矩形框和真實(shí)的車位對(duì)應(yīng)不上,比如矩形框卡在了兩個(gè)車位中間這種,這樣劃分出的車位拿給模型看,就很容易判斷出錯(cuò)。

另外,最終的這個(gè)字典很重要,因?yàn)檫@個(gè)字典里面保存的是各個(gè)車位的位置信息。 有了這個(gè)東西,拿到一幀圖片,就可以直接把每個(gè)車位標(biāo)定出來,拿給模型預(yù)測(cè)。 并且對(duì)于同一停車場(chǎng),這個(gè)每個(gè)車位是固定的。所以這個(gè)也不會(huì)變,視頻的所有圖像共用。 這樣能保證實(shí)時(shí)性。

3.7 為CNN生成預(yù)測(cè)圖片

有了各個(gè)車位的具體位置信息,下面直接按照這里面的左邊把每個(gè)車位切割出來,就能得到后面CNN的訓(xùn)練和驗(yàn)證的數(shù)據(jù)集了。

def save_images_for_cnn(image, spot_dict, folder_name = '../cnn_pred_data'):
    for spot in spot_dict.keys():
        (x1, y1, x2, y2) = spot
        (x1, y1, x2, y2) = (int(x1), int(y1), int(x2), int(y2))
        
        # 裁剪
        spot_img = image[y1:y2, x1:x2]
        spot_img = cv2.resize(spot_img, (0, 0), fx=2.0, fy=2.0)
        spot_id = spot_dict[spot]
        
        filename = 'spot_{}.jpg'.format(str(spot_id))
        
        # print(spot_img.shape, filename, (x1,x2,y1,y2))
        cv2.imwrite(os.path.join(folder_name, filename), spot_img)
  
save_images_for_cnn(test_image, valid_spots_dict)

這樣,就把模型的訓(xùn)練數(shù)據(jù)集準(zhǔn)備好。 在文件中組織成這個(gè)樣子:

在這里插入圖片描述

每個(gè)目錄里面,就是劃分出來的一張張小的車位圖像,不過這里是人為劃分到了有車還是無車?yán)锩?。所以后面的模型其?shí)做一個(gè)二分類任務(wù),給定這樣一張車位的小圖像,預(yù)測(cè)下是不是空的即可。

下面開始說模型的細(xì)節(jié)。

4. 模型的訓(xùn)練和預(yù)測(cè)

由于目前的樣本非常少,不足以訓(xùn)練一個(gè)大模型到收斂,所以這里采用的遷移學(xué)習(xí)技術(shù),用的預(yù)訓(xùn)練模型。

模型這里和視頻中不一樣的是,我統(tǒng)一采用pytorch寫的模型訓(xùn)練和測(cè)試代碼,原因是最近正在嘗試pytorch復(fù)現(xiàn)cv里面的各個(gè)經(jīng)典網(wǎng)絡(luò),這個(gè)項(xiàng)目正好讓我拿來練手。另外一個(gè)就是感覺keras搭建的靈活度不夠,在數(shù)據(jù)預(yù)處理方面不如torchvision里面transforms用起來方便。 基于這兩個(gè)原因, 我這里直接用pytorch,采用的resnet34預(yù)訓(xùn)練模型,使用這個(gè)的原因是這兩天正好把resnet復(fù)現(xiàn)了一遍,稍微熟悉了一點(diǎn)罷了,正好能學(xué)以致用,沒有啥偏愛。

由于這里的代碼非常多,這里就不過多羅列了,簡(jiǎn)單說下邏輯即可,感興趣的可以看具體項(xiàng)目。

首先是訓(xùn)練模型。

4.1 模型訓(xùn)練

這個(gè)整體邏輯倒是可以看下:

def train_model():
    # 獲取dataloader
    data_root = os.getcwd()
    image_path = os.path.join(data_root, "train_data")
    train_data_path = os.path.join(image_path, "train")
    val_data_path = os.path.join(image_path, "test")
    train_loader, validat_loader, train_num, val_num = get_dataloader(train_data_path, val_data_path,
                                                                      data_transform_pretrain, batch_size=8)

    # 創(chuàng)建模型 注意這里沒指定類的個(gè)數(shù),默認(rèn)是1000類
    net = resnet34()
    model_weight_path = 'saved_model_weight/resnet34_pretrain_ori_low_torch_version.pth'

    # 使用預(yù)訓(xùn)練的參數(shù),然后進(jìn)行finetune
    net.load_state_dict(torch.load(model_weight_path, map_location='cpu'))

    # 改變fc layer structure  把fc的輸出維度改為2
    in_channel = net.fc.in_features
    net.fc = nn.Linear(in_channel, 2)
    net.to(device)

    # 模型訓(xùn)練配置
    loss_function = nn.CrossEntropyLoss()
    optimizer = optim.Adam(net.parameters(), lr=0.0001)

    epochs = 30
    save_path = "saved_model_weight/resnet34_pretrain.pth"
    best_acc = 0.
    train_steps = len(train_loader)

    model_train(net, train_loader, validat_loader, epochs, device, optimizer, loss_function, train_steps, val_num,
                save_path, best_acc)

因?yàn)槲疫@里采用了一些函數(shù)封裝,所以這個(gè)邏輯應(yīng)該稍微清晰些,首先pytorch模型訓(xùn)練,要先把數(shù)據(jù)封裝成dataloader的格式,后面模型訓(xùn)練的時(shí)候,是從這個(gè)類里面讀取數(shù)據(jù)。關(guān)于dataloader與dataset的原理這里就不過多整理。之前我詳細(xì)在pytorch基礎(chǔ)那里整理過了。

不過這里的細(xì)節(jié),就是data_transform_pretrain, 也就是數(shù)據(jù)預(yù)處理操作。

data_transform_pretrain = {
        "train": transforms.Compose([
            transforms.RandomResizedCrop(224),  # 對(duì)圖像隨機(jī)裁剪, 訓(xùn)練集用,驗(yàn)證集不用
            transforms.RandomHorizontalFlip(),
            transforms.ToTensor(),
            # 這里的中心化處理參數(shù)需要官方給定的參數(shù),這里是ImageNet圖片的各個(gè)通道的均值和方差,不能隨意指定了
            transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
        ]),
        "val": transforms.Compose([
            # 驗(yàn)證過程中,這里也進(jìn)行了一點(diǎn)點(diǎn)改動(dòng)
            transforms.Resize(256),
            transforms.CenterCrop(224),
            transforms.ToTensor(),
            transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
        ]),
        "test": transforms.Compose([
            transforms.Resize(256),
            transforms.CenterCrop(224),
            transforms.ToTensor(),
            transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
        ])
    }

這里由于是采用的官方訓(xùn)練好的resnet網(wǎng)絡(luò),我們這里中心化要參考官方給定的參數(shù),因?yàn)樗A(yù)訓(xùn)練是ImageNet這個(gè)大數(shù)據(jù)集上訓(xùn)練的,所以這里每個(gè)通道的均值和方差,我們最好別隨意指定。用人家官方給出的。

有了dataloader,接下來創(chuàng)建模型, 這里是直接使用的resnet34, 把預(yù)訓(xùn)練的模型參數(shù)導(dǎo)入進(jìn)來。導(dǎo)入的時(shí)候,會(huì)發(fā)現(xiàn)我這個(gè)參數(shù)名字的文件有個(gè)low_torch_version, 是因?yàn)橹皩?dǎo)入的時(shí)候出現(xiàn)了報(bào)錯(cuò):

xxx.pt is a zip archive(did you mean to use torch.jit.load()?)“

這個(gè)報(bào)錯(cuò)的原因是,官方預(yù)訓(xùn)練保存的模型參數(shù)使用的pytorch版本是1.6以上,PyTorch的1.6版本將torch.save切換為使用新的基于zipfile的文件格式。

torch.load仍然保留以舊格式加載文件的功能。 如果希望torch.save使用舊格式,請(qǐng)傳遞kwarg _use_new_zipfile_serialization = False

我電腦本子的pytorch版本是1.0,所以導(dǎo)入1.6以上版本保存的模型參數(shù),就會(huì)報(bào)這樣的錯(cuò)誤。 那么,我怎么解決的呢? 那就是從我服務(wù)器上,運(yùn)行了下面這個(gè)代碼

model_weight_path = "saved_models/resnet34_pretrain_ori.pth"
    state_dict = torch.load(model_weight_path)
    torch.save(state_dict, 'saved_models/resnet34_pretrain_ori_low_torch_version.pth', _use_new_zipfile_serialization=False)

我服務(wù)器上的pytorch版本是1.10的版本,是能導(dǎo)入這個(gè)參數(shù)的,導(dǎo)入完了重新保存,指定官方給定的參數(shù)即可。

這個(gè)問題解決之后,下面就說下預(yù)訓(xùn)練模型了, 導(dǎo)入?yún)?shù)之后,我們需要修改網(wǎng)絡(luò)最后的一層,因?yàn)閞esnet本身做的是1000分類,最后一層神經(jīng)元個(gè)數(shù)是1000,我們這里需要做二分類,所以需要改成2。

另外,就是遷移學(xué)習(xí)的三種方式:

  • 載入權(quán)重后重新訓(xùn)練所有參數(shù) – 硬件設(shè)施好
  • 載入權(quán)重后只訓(xùn)練最后幾層參數(shù),前面的層進(jìn)行凍結(jié), 或者是前面幾層的學(xué)習(xí)率降低, 后面全連接層的學(xué)習(xí)率變大,即分組調(diào)整學(xué)習(xí)率
  • 載入全中后在原網(wǎng)絡(luò)基礎(chǔ)上再添加一層全連接層, 僅訓(xùn)練最后一個(gè)全連接層

我這里采用的全部訓(xùn)練的方式,但是這里有必要整理下,如果是想只訓(xùn)練后面幾層,或者前面層和后面層不同學(xué)習(xí)率訓(xùn)練的時(shí)候,應(yīng)該怎么做:

# 創(chuàng)建模型 注意這里沒指定類的個(gè)數(shù),默認(rèn)是1000類
net = resnet34()
model_weight_path = 'saved_model_weight/resnet34_pretrain_ori_low_torch_version.pth'

# 使用預(yù)訓(xùn)練的參數(shù),然后進(jìn)行finetune
net.load_state_dict(torch.load(model_weight_path, map_location='cpu'))

# 改變fc layer structure  把fc的輸出維度改為2
in_channel = net.fc.in_features
net.fc = nn.Linear(in_channel, 2)
net.to(device)

# 模型訓(xùn)練配置
loss_function = nn.CrossEntropyLoss()
# 訓(xùn)練的時(shí)候,也可以凍結(jié)掉卷積層的參數(shù), 也可以指定不同層的參數(shù)使用不同的學(xué)習(xí)率進(jìn)行訓(xùn)練
res_params, conv_params, fc_params = [], [], []
# named_parameters()能返回每一層的名字以及參數(shù),是一個(gè)字典
for name, param in net.named_parameters():
     # layer 系列是殘差層
     if ('layer' in name):
           res_params.append(param)
     # 全連接層
     elif ('fc' in name):
           fc_params.append(param)
     else:
           param.requires_grad = False

params = [
     {'params': res_params, 'lr': 0.0001},
     {'params': fc_params, 'lr': 0.0002},
]

optimizer = optim.Adam(params)

這里修改優(yōu)化器的參數(shù)即可。

這樣完事之后,調(diào)用模型訓(xùn)練的函數(shù),直接進(jìn)行訓(xùn)練即可。這個(gè)腳本就是常規(guī)操作了,這里就不貼代碼了。

4.2 模型預(yù)測(cè)

有了保存好的模型, 我們拿來一幀圖像,根據(jù)停車位字典劃分出一個(gè)個(gè)的停車位來,然后通過模型預(yù)測(cè)是不是空的,如果是空的, 在原圖上進(jìn)行標(biāo)記出來即可。

所以下面是整個(gè)項(xiàng)目的核心預(yù)測(cè):

def predict_on_img(img, spot_dict, model, class_indict, make_copy=True, color=[0, 255, 0], alpha=0.5, save=True):
    # 這個(gè)是停車場(chǎng)的全景圖像
    if make_copy:
        new_image = np.copy(img)
        overlay = np.copy(img)

    cnt_empty, all_spots = 0, 0
    for spot in tqdm(spot_dict.keys()):
        all_spots += 1
        (x1, y1, x2, y2) = spot
        (x1, y1, x2, y2) = (int(x1), int(y1), int(x2), int(y2))
        spot_img = img[y1:y2, x1:x2]
        spot_img_pil = Image.fromarray(spot_img)

        label = model_infer(spot_img_pil, model, class_indict)
        if label == 'empty':
            cv2.rectangle(overlay, (int(x1), int(y1)), (int(x2), int(y2)), color, -1)
            cnt_empty += 1

    cv2.addWeighted(overlay, alpha, new_image, 1 - alpha, 0, new_image)

    # 顯示結(jié)果的
    cv2.putText(new_image, "Available: %d spots" % cnt_empty, (30, 95),
                cv2.FONT_HERSHEY_SIMPLEX,
                0.7, (255, 255, 255), 2)
    cv2.putText(new_image, "Total: %d spots" % all_spots, (30, 125),
                cv2.FONT_HERSHEY_SIMPLEX,
                0.7, (255, 255, 255), 2)

    if save:
        filename = 'with_marking_predict.jpg'
        cv2.imwrite(filename, new_image)
    # cv_imshow('new_image', new_image)
    return new_image

模型預(yù)測(cè)的核心,就是model_infer函數(shù),這個(gè)也是模型預(yù)測(cè)的常規(guī)操作,這里不過多解釋了。

視頻的話,無非就是多幀圖像,對(duì)于每一幀過一下這個(gè)函數(shù),就能進(jìn)行視頻的實(shí)時(shí)預(yù)測(cè):

def predict_on_video(video_path, spot_dict, model, class_indict, ret=True):
    cap = cv2.VideoCapture(video_path)
    count = 0
    while ret:
        ret, image = cap.read()
        count += 1

        if count == 5:
            count = 0
            new_image = predict_on_img(image, spot_dict, model, class_indict, save=False)

            cv2.imshow('frame', new_image)
            if cv2.waitKey(10) & 0xFF == ord('q'):
                break

    cv2.destroyAllWindows()
    cap.release()

這就是整個(gè)項(xiàng)目啦。

5. 小結(jié)

終于看到了一個(gè)小麻雀項(xiàng)目了,雖然可能有些簡(jiǎn)單,但是卻能把圖像處理加模型訓(xùn)練預(yù)測(cè),這一套機(jī)制都給利用起來,對(duì)我這樣的初學(xué)者還算友好。通過這個(gè)項(xiàng)目,在圖像預(yù)處理方面學(xué)習(xí)到了二值化中的InRange, 霍夫直線檢測(cè),定點(diǎn)標(biāo)定技術(shù),mask矩陣進(jìn)行區(qū)域鎖定,以及通過坐標(biāo)進(jìn)行區(qū)域提取等。在模型方面學(xué)習(xí)到了resnet,復(fù)習(xí)了pytorch遷移學(xué)習(xí)。 又認(rèn)識(shí)了幾個(gè)新的庫(kù)glob, shutil, PIL等。所以,收獲頗多,感覺cv越來越有意思了哈。

這個(gè)項(xiàng)目感覺實(shí)際場(chǎng)景中挺有意義的,開腦洞幻想下未來如果智慧交通普及了,在智能停車場(chǎng)的運(yùn)作下, 通過攝像頭實(shí)時(shí)檢測(cè)停車場(chǎng)車位的空余狀況并標(biāo)定好位置,把這個(gè)信息傳到無人車系統(tǒng),然后無人車根據(jù)信息自動(dòng)規(guī)劃停車路線,直接鎖定車位自動(dòng)把車停好。避免了停車場(chǎng)的擁擠(可能現(xiàn)在我們停車轉(zhuǎn)好幾圈找不到一個(gè)停車位,還有可能堵死在里面不好出來)。并且停車場(chǎng)的空余情況能通過大屏幕一目了然,節(jié)省了用戶找車位,停車的時(shí)間。

好吧, 只是提前開了下腦洞,至于能不能成, 未來會(huì)給我們答案 ??

本次項(xiàng)目代碼地址https://github.com/zhongqiangwu960812/OpenCVLearning

到此這篇關(guān)于OpenCV停車場(chǎng)車位實(shí)時(shí)檢測(cè)項(xiàng)目實(shí)踐的文章就介紹到這了,更多相關(guān)OpenCV停車場(chǎng)車位實(shí)時(shí)檢測(cè)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論