車牌辨識的正確率
車牌辨識能否完全正確,決定此 app 可否商業化,很不幸的,無法完全 100% 正確。
車牌辨識的正確率受限於影像大小、位置、角度、拍攝光線亮度。所以停車場為了要提高辨識精準度,一定會在車子的進出入口,在特定距離、特定角度進行拍攝。在這二個限定條件下,就可以提高精準度,但還是會有例外。所以繳費時,車主只能輸入 4 位車牌數字號碼,然後在眾多照片中選擇是那一台車,再進行付費。
如果是路上隨機拍攝的 Video 影片辨識呢? 這就有很多調校的方式,目前還沒有絕對精準的方法。
車牌位置偵測方法
OpenCv-python
網路上關於車牌位置的偵測,許多介紹都是使用 opencv-python 的灰階化(BGR2GRAY)->高斯模糊(cv2.GaussianBlur)->邊緣檢測(cv2.Canny)->輪廓檢測(cv2.findCoutours),最後定出車牌的四方矩型位置。這種方法其實無法商業化,尤其是車牌是白底黑字,然後車身又是白色,就完全無用。
YOLOv8
使用 YOLOv8 訓練車牌辨識是最精準的方式,此方法適用各國的車型及顏色。
安裝套件
要執行 yolo v8 版,需安裝如下套件,請依序執行如下指令
pip install torch==2.0.1+cu118 torchvision==0.15.2+cu118 torchaudio===2.0.2+cu118 -f https://download.pytorch.org/whl/cu118/torch_stable.html pip install ultralytics pytesseract
下載訓練資料
請到 kaggle 下載 car-plate-license 車牌圖片訓練資料,解開後,將 images 及 annotations 目錄移到專案中的 car 目錄下。解開 .zip 檔後,將 “archive” 目錄改為 “car”。
xml2txt
car/annotations 下的資料為 xml 格式,需將 xml 轉成 txt 格式,請新增 xml2txt.py 檔
import os import xml.etree.ElementTree as ET from xml.dom.minidom import parse path = "./car/annotations" classes = {"licence":0} labels_path="./car/labels" if not os.path.exists(labels_path): os.mkdir("./car/labels") #train_path=os.path.join(labels_path,"licence") train_path=labels_path if not os.path.exists(train_path): os.mkdir(train_path) for annotations in os.listdir(path): dom = parse(os.path.join(path,annotations)) root = dom.documentElement filename = ".txt".join(root.getElementsByTagName("filename")[0].childNodes[0].data.split(".png")) image_width = root.getElementsByTagName("width")[0].childNodes[0].data image_height = root.getElementsByTagName("height")[0].childNodes[0].data with open("./car/labels/"+filename,"w") as r: for items in root.getElementsByTagName("object") : name = items.getElementsByTagName("name")[0].childNodes[0].data xmin = items.getElementsByTagName("xmin")[0].childNodes[0].data ymin = items.getElementsByTagName("ymin")[0].childNodes[0].data xmax = items.getElementsByTagName("xmax")[0].childNodes[0].data ymax = items.getElementsByTagName("ymax")[0].childNodes[0].data x_center_norm = ((int(xmin)+int(xmax)) / 2 ) / int(image_width) y_center_norm = ((int(ymin)+int(ymax))/2) / int(image_height) width_norm = ((int(xmax)-int(xmin))/int(image_width)) height_norm = ((int(ymax)-int(ymin))/int(image_height)) r.write(str(classes[name])+" ") r.write(str(x_center_norm)+" ") r.write(str(y_center_norm)+" ") r.write(str(width_norm)+" ") r.write(str(height_norm)+"\n")
執行完畢後,會產生 car/labels 目錄,裏面的檔案就是轉換後的 txt 檔
分割訓練及驗証資料
新增 split.py 檔,將 90% 的圖片列入 train.txt, 10% 為 val.txt
import os import random import shutil data_path='./car' train_path='./train' valid_path='./valid' if os.path.exists(train_path): shutil.rmtree(train_path) if os.path.exists(valid_path): shutil.rmtree(valid_path) os.makedirs(os.path.join(train_path, 'images')) os.makedirs(os.path.join(train_path, 'labels')) os.makedirs(os.path.join(valid_path, 'images')) os.makedirs(os.path.join(valid_path, 'labels')) files=[os.path.splitext(file)[0] for file in os.listdir(os.path.join(data_path, "images"))] random.shuffle(files) mid=int(len(files)*0.8) for file in files[:mid]: source=os.path.join(data_path, "images", f'{file}.png') target=os.path.join(train_path,"images", f'{file}.png') print(source, target) shutil.copy(source, target) source=os.path.join(data_path, "labels", f'{file}.txt') target=os.path.join(train_path,"labels", f'{file}.txt') print(source, target) shutil.copy(source, target) for file in files[mid:]: source=os.path.join(data_path, "images", f'{file}.png') target=os.path.join(valid_path,"images", f'{file}.png') print(source, target) shutil.copy(source, target) source=os.path.join(data_path, "labels", f'{file}.txt') target=os.path.join(valid_path,"labels", f'{file}.txt') print(source, target) shutil.copy(source, target)
執行完後,會在 car 目錄產生 train.txt 及 val.txt 二個檔案
data.yaml 資料集設定
在專案根目錄下新增 data.yaml,加入如下設定
train: e:/python_ai/car/train/images val: e:/python_ai/car/valid/images nc: 1 names: [ 'license']
訓練模型
新增 train.py 檔,代碼如下
import os import shutil import time from ultralytics import YOLO if __name__=='__main__': train_path="./runs/detect/train" if os.path.exists(train_path): shutil.rmtree(train_path) model = YOLO("yolov8l.pt") print("開始訓練 .........") t1=time.time() model.train(data="./data.yaml", epochs=150, imgsz=640) t2=time.time() print(f'訓練花費時間 : {t2-t1}秒')#2131.11984705925秒, epochs : 121 path=model.export() print(f'模型匯出路徑 : {path}')
訓練完成後,會在專案下的 runs/detect/train/weights 產生 best.pt 模型。
無法訓練的人,請由如下網址下載 : car.pt
車牌偵測及辨識
車牌位置偵測及英數字辨識代碼如下
import os
import platform
import pylab as plt
import cv2
import numpy as np
import pytesseract
from PIL import Image, ImageFont, ImageDraw
from ultralytics import YOLO
def text(img, text, xy=(0, 0), color=(0, 0, 0), size=20):
pil = Image.fromarray(img)
s = platform.system()
if s == "Linux":
font =ImageFont.truetype('/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc', size)
elif s == "Darwin":
font = ImageFont.truetype('....', size)
else:
font = ImageFont.truetype('simsun.ttc', size)
ImageDraw.Draw(pil).text(xy, text, font=font, fill=color)
return np.asarray(pil)
model=YOLO('./runs/detect/train/weights/best.pt')
path="./valid/images"
plt.figure(figsize=(12,9))
for i,file in enumerate(os.listdir(path)[0:6]):
full=os.path.join(path, file)
print(full)
img=cv2.imdecode(np.fromfile(full, dtype=np.uint8), cv2.IMREAD_COLOR)
img=img[:,:,::-1].copy()
results=model.predict(img, save=False)
boxes=results[0].boxes.xyxy
for box in boxes:
#box=box.cpu().numpy()
x1 = int(box[0])
y1 = int(box[1])
x2 = int(box[2])
y2 = int(box[3])
cv2.rectangle(img,(x1, y1), (x2, y2), (0,255,0) , 2)
tmp=cv2.cvtColor(img[y1:y2,x1:x2].copy(), cv2.COLOR_RGB2GRAY)
license = pytesseract.image_to_string(tmp, lang='eng', config='--psm 11')
img=text(img, license, (x1, y1-20), (0,255,0),25)
plt.subplot(2,3,i+1)
plt.axis("off")
plt.imshow(img)
plt.savefig("yolov8_car.jpg")
plt.show()