<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
今天整理OpenCV入門的第三個實戰小專案,前面的兩篇文章整理了信用卡數位識別以及檔案OCR掃描, 大部分用到的是OpenCV裡面的基礎影象預處理技術,比如輪廓檢測,邊緣檢測,形態學操作,透視變換等, 而這篇文章的專案呢,不僅需要一些基礎的影象預處理,還需要搭建模型進行識別和預測,所以通過這個專案,能把影象預處理以及建模型等一整套流程拉起來,並應用到實際的應用場景,還是非常有意思的。
停車場車位實時檢測任務,是拿到停車場的一段視訊video,主要完成兩件事情:
所以這個專案還是非常有實踐應用價值的,用了大約一天半的時間搞定這個專案,參考的是唐老師的OpenCV入門教學視訊, 不過這裡面對於這個任務做的相對粗糙,我在這個基礎上基於我的理解進行了一些優化,主要改動如下:
不過,發現小resnet就夠強大的了,最終的預測效果如下:
這是視訊中的某一幀影象,實際執行的時候,是讀入視訊,快速分開幀,每一幀做出這樣的預測標記,然後實時顯示。這樣在每個時刻,都能動態的知道該停車場有哪些車位空了出來。
下面就對這個專案中用到的關鍵技術進行整理,由於這個專案稍微大一些,程式碼量多,不可能在這裡全部展示,但想記錄下對於這個專案我的思考過程,以及各種處理的動機,以及如何進行的處理,我覺得這個才是對以後有用的東西。
首先,拿到這個任務之後, 得大致上梳理下流程,才能確定行動方案。 我們開始拿到了這樣的一段視訊,那麼為了完成上面停車位檢測以及識別的任務,就需要考慮兩步:
其實宏觀上就這麼兩大步。那麼後面的問題就是怎麼把每個車位提取出來,又怎麼訓練模型預測呢?
我這裡主要分為了兩大步, 資料預處理以及模型的訓練及預測:
資料預處理方面
通過上面步驟,會積累一些資料,大約800多張圖片,接下來就可以訓練模型,但是由於資料量太少,從頭訓練模型往往效果不好,所以這裡採用遷移學習的方式,使用了預訓練的resnet34,用這800多張圖片微調。
訓練好了模型儲存,接下來,對於每一幀影象,有了停車位位置字典,就能直接提取出每一個停車位,然後對於這每個停車位,模型預測有沒有車即可
所以有了這樣的一個流程,就能再進一步分解細化,就可以大處著眼小處著手啦,下面整理每一步裡面的關鍵細節。
首先,把一幀影象讀入進來,原始影象如下:
先通過二值化的方式過濾掉背景,突出重要資訊,然後轉成灰度圖。
def select_rgb_white_yellow(image): # 過濾背景 lower = np.uint8([120, 120, 120]) upper = np.uint8([255, 255, 255]) # 三個通道內,低於lower和高於upper的部分分別變成0, 在lower-upper之間的值變成255, 相當於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, 我在想這倆有啥區別? 為啥這裡不用這個了? 下面是我經過探索得到的幾點使用經驗:
cv2.threshold(src, thresh, maxval, type[, dst])
:針對的是單通道影象(灰度圖), 二值化的標準,type=THRESH_BINARY: if x > thresh, x = maxval, else x = 0
, 而type=THRESH_BINARY_INV
: 和上面的標準反著,目前常用到了這倆個cv2.inRange(src, lowerb, upperb)
:可以是單通道影象,可以是三通道影象,也可以進行二值化,標準是if x >= lower and x <= upper, x = 255, else x = 0
這裡做了一個實驗, 事先把圖片轉化成灰度圖warped = cv2.cvtColor(test_image, cv2.COLOR_BGR2GRAY)
,然後下面兩句程式碼的執行結果是一樣的:
cv2.threshold(warped, 119, 255, cv2.THRESH_BINARY)[1]
cv2.inRange(warped, 120, 255)
處理之後的圖片長這樣:
接下來,採用Canny邊緣檢測演演算法,檢測出邊緣來
low_threshold, high_threshold = 50, 200 edges_img = cv2.Canny(gray_img, low_threshold, high_threshold)
結果如下:
下面嘗試把停車場這塊提取出來, 把其餘那些沒用的去掉。
這裡的思路就是,先用6個標定點把停車場的這幾個角給他定住,這個標定點得需要自己找。 找到之後, 採用OpenCV中的填充函數,就能製作一個mask矩陣,然後就能把其餘部分去掉了。
def select_region(image): """這裡手動選擇區域""" rows, cols = image.shape[:2] # 下面定義6個標定點, 這個點的順序必須讓它化成一個區域,如果調整,可能會交叉起來,所以不要動 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矩陣, 只保留點內部的區域 mask = np.zeros_like(image) if len(mask.shape) == 2: cv2.fillPoly(mask, vertices, 255) # 點框住的地方填充為白色 #cv_imshow('mask', mask) roi_image = cv2.bitwise_and(image, mask) return roi_image roi_image = select_region(edges_img)
處理的效果如下:
這樣處理好了,我們就需要找到這裡面的直線,然後通過直線去猜測大致的位置。
這裡採用霍夫變換檢測直線, 函數是cv2.HoughLinesP, 該函數能檢測直線的兩個端點(x0,y0, x1, y1)。函數原型:
HoughLinesP(image, rho, theta, threshold[, lines[, minLineLength[, maxLineGap]]]) -> lines
所以,這個函數拿來直接用。
def hough_lines(image): # 輸入的影象需要是邊緣檢測後的結果 # minLineLengh(線的最短長度,比這個短的都被忽略)和MaxLineCap(兩條直線之間的最大間隔,小於此值,認為是一條直線) # rho距離精度,theta角度精度,threshod超過設定閾值才被檢測出線段 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)
竟然檢測到了2338條直線,這裡面肯定有很多不能用的,所以後面處理,需要對直線先進行一波篩選。篩選原則是線不能是斜的,且水平方向不能太長或者是太短。 具體程式碼下面會看到,這裡先展示下過濾之後的效果。
過濾完了,總共628條直線。
下面的程式碼會稍微複雜,所以需要分塊講思路。
首先,我們拿到了停車場的直線以及它的座標位置。 過濾操作已經做好,接下來,就是對每條直線進行排序。 讓這些線,從一列一列的,從上往下依次排列好。
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: # 這裡是過濾直線,必須保證不能是斜的線,且水平方向不能太長或者太短 if abs(y2-y1) <=1 and abs(x2-x1) >=25 and abs(x2-x1) <= 55: stayed_lines.append((x1,y1,x2,y2)) # 對直線按照x1排序, 這樣能讓這些線從上到下排列好, 這個排序是從第一列的第一條橫線,往下走,然後是第二列第一條橫線往下,... list1 = sorted(stayed_lines, key=operator.itemgetter(0, 1))
排列好之後,遍歷所有線, 看看相鄰兩條線之間的距離,如果是一列, 那麼兩條線的x_1應該離得非常近,畢竟是同一列,如果這個值太大了,說明是下一列了。根據這個準則,遍歷完之後,就能把這些線劃分到不同的列裡面。這裡是用了一個字典,鍵表示列,值表示每一列裡面的直線。
程式碼接上:
# 找到多個列,相當於每列是一排車 clusters = collections.defaultdict(list) dIndex = 0 clus_dist = 10 # 每一列之間的那個距離 for i in range(len(list1) - 1): # 看看相鄰兩條線之間的距離,如果是一列的,那麼x1這個距離應該很近,畢竟是同一列上的 # 如果這個值大於10了,說明是下一列的了,此時需要移動dIndex, 這個表示的是第幾列 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
有了每一列裡面的直線,下面就是就是遍歷每一列,先拿到所有直線,然後找到縱座標的最大值和最小值,以及橫座標的最大和最小值,但由於橫座標這裡,首尾列都一排車位,中間排都是兩列,不好直接取到最大最小座標,所以這裡採用了求平均的方式。 這樣遍歷完,針對每一列,就能得到左上角點和右下角點,這是一個矩形框。
程式碼接上:
# 得到每列停車位的矩形框 rects = {} i = 0 for key in clusters: all_list = clusters[key] cleaned = list(set(all_list)) # 有5個停車位至少 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,也是進行了一點微調操作。 這種是根據實際場景來的,不是死的。 效果如下:
這樣就會發現,對於每一列的停車位,有了大致上的矩形框標定,但是這個非常粗糙。 原視訊裡面就基於這個往後面走了。 我這裡對於每一列框進行微調,因為這個框非常重要。不準的話影響後面的具體車位劃分。
def rect_finetune(image, rects, copy_img=True): if copy_img: image_copy = image.copy() # 下面需要對上面的框進行座標微調, 讓框更加準確 # 這個框很重要,影響後面停車位的統計,儘量不能有遺漏 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
微調之後的效果如下:
原則就是不遺漏,不多餘。
這裡就是針對每個矩形框, 對裡面的停車位用直線切割成一個個的,每個停車位用(x1,y1,x2,y2)標識,左上角和右下角的座標。並進行標號,最終形成一個字典,字典的鍵就是位置,值就是序號。當然,這裡的一個細節,依然是中間排是兩排,首尾是一排,這個在具體劃分停車位的時候,一定要注意。
def draw_parking(image, rects, make_copy=True, save=True): gap = 15.5 spot_dict = {} # 一個車位對應一個位置 tot_spots = 0 #微調 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) # 計算數量 除了第一列和最後一列,中間的都是兩列的 if key == 0 or key == len(rects) - 1: tot_spots += num_splits + 1 else: tot_spots += 2 * (num_splits + 1) # 字典對應好 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也是我後來加上去的,也是為了讓每一列儘量把車位劃分的準確些。
從這個效果上來看,基本上就把車位一個個的劃分開了,劃分開之後,會發現,這裡面有些並不是車位, 但依然給框住了。這樣統計個數的時候,以及後面給資訊停車的時候會受到影響,所以我這裡又一一排查,去掉了這些無效的車位。
# 去掉多餘的停車位 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
這樣,還可以把處理好的車位資訊進行視覺化,再進行微調,不過,我這裡由於之前的一些微調操作,感覺效果還可以,就沒有做任何調整啦。
# 把每一個有效停車位標記出來 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)
效果如下:
如果要想讓後面模型對於每個車位預測的更加準確,這裡的劃分一定要儘量的細緻和標準。 否則如果矩形框和真實的車位對應不上,比如矩形框卡在了兩個車位中間這種,這樣劃分出的車位拿給模型看,就很容易判斷出錯。
另外,最終的這個字典很重要,因為這個字典裡面儲存的是各個車位的位置資訊。 有了這個東西,拿到一幀圖片,就可以直接把每個車位標定出來,拿給模型預測。 並且對於同一停車場,這個每個車位是固定的。所以這個也不會變,視訊的所有影象共用。 這樣能保證實時性。
有了各個車位的具體位置資訊,下面直接按照這裡面的左邊把每個車位切割出來,就能得到後面CNN的訓練和驗證的資料集了。
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)
這樣,就把模型的訓練資料集準備好。 在檔案中組織成這個樣子:
每個目錄裡面,就是劃分出來的一張張小的車點陣影象,不過這裡是人為劃分到了有車還是無車裡面。所以後面的模型其實做一個二分類任務,給定這樣一張車位的小影象,預測下是不是空的即可。
下面開始說模型的細節。
由於目前的樣本非常少,不足以訓練一個大模型到收斂,所以這裡採用的遷移學習技術,用的預訓練模型。
模型這裡和視訊中不一樣的是,我統一採用pytorch寫的模型訓練和測試程式碼,原因是最近正在嘗試pytorch復現cv裡面的各個經典網路,這個專案正好讓我拿來練手。另外一個就是感覺keras搭建的靈活度不夠,在資料預處理方面不如torchvision裡面transforms用起來方便。 基於這兩個原因, 我這裡直接用pytorch,採用的resnet34預訓練模型,使用這個的原因是這兩天正好把resnet復現了一遍,稍微熟悉了一點罷了,正好能學以致用,沒有啥偏愛。
由於這裡的程式碼非常多,這裡就不過多羅列了,簡單說下邏輯即可,感興趣的可以看具體專案。
首先是訓練模型。
這個整體邏輯倒是可以看下:
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) # 建立模型 注意這裡沒指定類的個數,預設是1000類 net = resnet34() model_weight_path = 'saved_model_weight/resnet34_pretrain_ori_low_torch_version.pth' # 使用預訓練的引數,然後進行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) # 模型訓練設定 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)
因為我這裡採用了一些函數封裝,所以這個邏輯應該稍微清晰些,首先pytorch模型訓練,要先把資料封裝成dataloader的格式,後面模型訓練的時候,是從這個類裡面讀取資料。關於dataloader與dataset的原理這裡就不過多整理。之前我詳細在pytorch基礎那裡整理過了。
不過這裡的細節,就是data_transform_pretrain, 也就是資料預處理操作。
data_transform_pretrain = { "train": transforms.Compose([ transforms.RandomResizedCrop(224), # 對影象隨機裁剪, 訓練集用,驗證集不用 transforms.RandomHorizontalFlip(), transforms.ToTensor(), # 這裡的中心化處理引數需要官方給定的引數,這裡是ImageNet圖片的各個通道的均值和方差,不能隨意指定了 transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)) ]), "val": transforms.Compose([ # 驗證過程中,這裡也進行了一點點改動 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)) ]) }
這裡由於是採用的官方訓練好的resnet網路,我們這裡中心化要參考官方給定的引數,因為它預訓練是ImageNet這個巨量資料集上訓練的,所以這裡每個通道的均值和方差,我們最好別隨意指定。用人家官方給出的。
有了dataloader,接下來建立模型, 這裡是直接使用的resnet34, 把預訓練的模型引數匯入進來。匯入的時候,會發現我這個引數名字的檔案有個low_torch_version
, 是因為之前匯入的時候出現了報錯:
xxx.pt is a zip archive(did you mean to use torch.jit.load()?)「
這個報錯的原因是,官方預訓練儲存的模型引數使用的pytorch版本是1.6以上,PyTorch的1.6版本將torch.save切換為使用新的基於zipfile的檔案格式。
torch.load
仍然保留以舊格式載入檔案的功能。 如果希望torch.save
使用舊格式,請傳遞kwarg _use_new_zipfile_serialization = False
。
我電腦本子的pytorch版本是1.0,所以匯入1.6以上版本儲存的模型引數,就會報這樣的錯誤。 那麼,我怎麼解決的呢? 那就是從我伺服器上,執行了下面這個程式碼
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)
我伺服器上的pytorch版本是1.10的版本,是能匯入這個引數的,匯入完了重新儲存,指定官方給定的引數即可。
這個問題解決之後,下面就說下預訓練模型了, 匯入引數之後,我們需要修改網路最後的一層,因為resnet本身做的是1000分類,最後一層神經元個數是1000,我們這裡需要做二分類,所以需要改成2。
另外,就是遷移學習的三種方式:
我這裡採用的全部訓練的方式,但是這裡有必要整理下,如果是想只訓練後面幾層,或者前面層和後面層不同學習率訓練的時候,應該怎麼做:
# 建立模型 注意這裡沒指定類的個數,預設是1000類 net = resnet34() model_weight_path = 'saved_model_weight/resnet34_pretrain_ori_low_torch_version.pth' # 使用預訓練的引數,然後進行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) # 模型訓練設定 loss_function = nn.CrossEntropyLoss() # 訓練的時候,也可以凍結掉折積層的引數, 也可以指定不同層的引數使用不同的學習率進行訓練 res_params, conv_params, fc_params = [], [], [] # named_parameters()能返回每一層的名字以及引數,是一個字典 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)
這裡修改優化器的引數即可。
這樣完事之後,呼叫模型訓練的函數,直接進行訓練即可。這個指令碼就是常規操作了,這裡就不貼程式碼了。
有了儲存好的模型, 我們拿來一幀影象,根據停車位字典劃分出一個個的停車位來,然後通過模型預測是不是空的,如果是空的, 在原圖上進行標記出來即可。
所以下面是整個專案的核心預測:
def predict_on_img(img, spot_dict, model, class_indict, make_copy=True, color=[0, 255, 0], alpha=0.5, save=True): # 這個是停車場的全景影象 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) # 顯示結果的 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
模型預測的核心,就是model_infer函數,這個也是模型預測的常規操作,這裡不過多解釋了。
視訊的話,無非就是多幀影象,對於每一幀過一下這個函數,就能進行視訊的實時預測:
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()
這就是整個專案啦。
終於看到了一個小麻雀專案了,雖然可能有些簡單,但是卻能把影象處理加模型訓練預測,這一套機制都給利用起來,對我這樣的初學者還算友好。通過這個專案,在影象預處理方面學習到了二值化中的InRange, 霍夫直線檢測,定點標定技術,mask矩陣進行區域鎖定,以及通過座標進行區域提取等。在模型方面學習到了resnet,複習了pytorch遷移學習。 又認識了幾個新的庫glob, shutil, PIL等。所以,收穫頗多,感覺cv越來越有意思了哈。
這個專案感覺實際場景中挺有意義的,開腦洞幻想下未來如果智慧交通普及了,在智慧停車場的運作下, 通過攝像頭實時檢測停車場車位的空餘狀況並標定好位置,把這個資訊傳到無人車系統,然後無人車根據資訊自動規劃停車路線,直接鎖定車位自動把車停好。避免了停車場的擁擠(可能現在我們停車轉好幾圈找不到一個停車位,還有可能堵死在裡面不好出來)。並且停車場的空餘情況能通過大螢幕一目瞭然,節省了使用者找車位,停車的時間。
好吧, 只是提前開了下腦洞,至於能不能成, 未來會給我們答案
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45