Rabu, 03 Februari 2021

Deteksi dan Membaca Plat Kendaraan menggunakan Python

Sudah lama sekali rasanya sejak terakhir kali saya menulis di blog ini, terakhir kali saya menulis adalah di tahun 2019. Sekarang, rasaya saya pengen nulis lagi seperti dulu, dan pada postingan kali ini saya akan membahas tentang final project ketika saya kuliah yaitu deteksi dan membaca plat nomor kendaraan.


Deteksi dan Membaca Plat Kendaraan menggunakan Python


Pada awalnya proyek ini ditulis menggunakan Matlab, karena dulu ketika belajar mata kuliah computer vision dosen saya menggunakan Matlab. Matlab bagus sekali untuk digunakan penelitian, namun sayangnya kebutuhan di industri lebih memilih menggunakan Python, jadi saya mencoba belajar Python dan mencoba menulis ulang kode programnya ke dalam bahasa Python.


Tidak semua saya tulis ulang sih, beberapa step ada yg saya lewati atau saya ubah, yang terpenting goalnya sama, bisa deteksi dan membaca plat nomor.


Apa saja yang diperlukan?


Beberapa perangkat dan bahan yang saya gunakan untuk membuat proyek ini, diantaranya:
  • Laptop Dell G3, dengan spesifikasi:
    • Intel core i5 8300H
    • Ram 16 GB
    • VGA Nvidia GTX 1050 4GB
    • SSD 256 GB
  • Smartphone Zenfone 3 Max, dengan spesifikasi kamera:
    • Resolusi 13 MP
    • Aperture f/2.2, AF
  • Citra test kendaraan yang diambil sendiri di parkiran kampus, dengan ketentuan:
    • Diambil ketika cahaya cerah/cukup
    • Jarak pengambilan gambar  sekitar 1 meter, plat hampir lurus.
    • Resolusi 2560 x 1920 pixels
  • Citra karakter dari 0-9 A-Z untuk training, saya lupa sumbernya dari mana karena sudah ada di laptop saya dari dulu, kalau saya ingat akan saya cantumkan sumbernya di sini.
    • Setiap karakter berisi 10 citra, berukuran 28 x 28 piksel
  • Untuk menulis kode program saya menggunakan:
    • Python 3.8.6
    • OpenCV 4.5.1
    • NumPy 1.19.4
    • Tensorflow 2.4.0

Metode yang digunakan


Secara garis besar, bagaimana cara saya mendapatkan hasil deteksi dan pembacaan plat nomor adalah sebagai berikut:

Prapengolahan, foto RGB kendaraan dimasukkan ke sistem (OpenCV akan membacanya BGR), di-resize, diubah menjadi citra grayscale, karena cahaya saat pengambilan gambar tidak selalu sama, lakukan normalisasi kondisi cahaya, lalu konversi menjadi citra BW (hitam-putih) dengan menggunakan pengambanagan Otsu.

Deteksi plat, untuk mendeteksi plat nomor, saya menggunakan contours, dari sini kita bisa mendapatkan area berdasarkan nilai piksel yang sama yang saling berhubungan. Untuk mendapatkan area plat nomor, saya memfilter area tersebut dengan membandingkan lebar dan aspect ratio. 

Segmentasi karakter, karakter yang saya ambil di proyek ini adalah bagian atas pada bagian plat nomor, yaitu bagian yang memuat nomor unik setiap kendaraan. Untuk mendapatkan setiap karakternya, saya menggunakan cara yang sama seperti deteksi plat, yaitu dengan contours, hanya saja filter yang digunakan adalah dari tinggi dan lebar area.

Klasifikasi karakter, untuk membaca atau mengklasifikasi karakter saya menggunakan model pada tutorial Tensorflow, penjelasan lengkapnya sudah ada di situs tersebut.

Proyek ini bisa dipisah menjadi dua bagian, yaitu bagian pelatihan/training, dan bagian testing.

Bagian pelatihan digunakan untuk melatih model agar bisa digunakan untuk mengklasifikasi karakter.

Dan bagian testing digunakan untuk deteksi dan membaca plat nomor menggunakan model yang sudah terlatih.

Hasil Per Tahap


Prapengolahan


Mari kita load citra kendaraan dengan perintah:

img = cv.imread(r'E:\PYTHON\Thesis siap upload\test images\AA5627JT.jpg')

Citra yang dimasukkan adalah citra RBG yang memiliki 3 dimensi warna yaitu Blue, Green, dan Red. Namun di OpenCV akan terbaca sebagai GBR.

Karena ukurannya terlalu besar, resize dengan perintah:

img = cv.resize(img, (int(img.shape[1]*.4),int(img.shape[0]*.4)))

img.shape akan menghasilkan nilai dari ukuran/dimensi citra, baris dan kolom, yaitu 1920 x 2560  piksel, img.shape[1] akan mengambil bagian kolom (2560), dan img.shape[0] akan mengambil bagian baris (1920). Nilai tersebut dikalikan dengan .4 atau 0.4 sehingga menghasilkan ukuran lebih kecil (768 x 1024) namun dengan aspect ratio yang sama.

Lalu ubah menjadi grayscale dengan perintah:

im_gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

Deteksi dan Membaca Plat Kendaraan menggunakan Python
Citra Grayscale

Karena intensitas cahaya pada setiap bagian kendaraan berbeda-beda, maka harus dilakukan normalisasi cahaya, proses ini digunakan agar mendapatkan hasil yang optimal ketika dilakukan konversi ke BW.

Bisa dilihat pada gambar di bawah ini jika tidak dilakukan normalisasi 

Deteksi dan Membaca Plat Kendaraan menggunakan Python
Citra BW belum dinormalisasi

Karakter plat nomor akan menyatu satu dengan yang lainnya, dan ini akan menyusahkan ketika proses segmentasi karakter.

Dan mari lihat apabila dilakukan normalisasi.

Deteksi dan Membaca Plat Kendaraan menggunakan Python
Citra BW terlormalisasi

Bisa dilihat bahwa karakter dan garis tepi plat nomor terlihat dengan lebih jelas,

Bagaimana cara normalisasinya? yaitu dengan melakukan operasi opening pada citra grayscale, lalu kurangkan citra grayscale dengan citra hasil opening. Selanjutnya citra bisa di ubah ke BW dengan pengambangan Otsu.

kernel = cv.getStructuringElement(cv.MORPH_ELLIPSE,(20,20))
im_open = cv.morphologyEx(im_gray, cv.MORPH_OPEN, kernel)
im_subs = im_gray - im_open 
(thresh, im_bw) = cv.threshold(im_subs, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)

Saya menggunakan kernel atau structuring element berupa ellipse atau lingkaran dengan diameter 20 piksel. Opening sendiri adalah gabungan dari 2 proses morfologi yaitu erosi lalu dilasi.

Deteksi Plat


Untuk mendeteksi bagian plat nomor, saya menggunakan fungsi contours. Dari sini kita bisa mendapatkan area-area pada citra yang memiliki nilai piksel yang sama dan saling berhubungan. termasuk bagian plat nomornya.

contours, hierarchy = cv.findContours(im_bw, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) # get the contour for every area

Dari sini kita sudah mendapatkan semua area pada citra. Untuk mendapatkan area plat nomor, mari kita filter berdasarkan lebar dan aspect rationya.

index = 0 # counter index of cnt in contours
idxs = [] # index of cnt in contours that contains plate location candidate 
for cnt in contours:
    x,y,w,h = cv.boundingRect(cnt) # get the x, y, width, height values from cnt in contours
    aspect_ratio = w/h # calculate the aspect ratio
    # get the plate location candidate if pixel width more than or equal to 200 pixels and aspect ratio less than or equal to 4
    if w >= 200 and aspect_ratio <= 4 : 
        idxs.append(index) # get the index value of contours that contain plate location candidate
    index += 1

Untuk mendapatkan nilai lebar dan tinggi, kita bisa menggunakan cv.boundingRect, lalu hitung aspect rationya dengan membagi lebar dengan tinggi.

Jika lebarnya lebih dari atau sama dengan 200 piksel dan aspect rationya kurang dari atau sama dengan 4 maka bagian tersebut adalah plat nomornya.

Namun sayang, terkadang terdapat 2 hasil deteksi plat di tempat yang sama. Keduanya adalah bagian plat nomor, hanya saja box-nya ada dua. Maka dari itu, berdasarkan pengamatan, ambil box yang kedua karena ukurannya lebih pas ke bagian plat nomornya.

for a in idxs: #check one plate?
    xp,yp,wp,hp = cv.boundingRect(contours[a])

if len(idxs) == 1: 
    cv.rectangle(img,(xp,yp),(xp+wp,yp+hp),(0,255,0),5)
    im_plate = im_gray[yp:yp+hp, xp:xp+wp] #crop
else:
    print(' more than one plate detected, save the second box')
    xp,yp,wp,hp = cv.boundingRect(contours[idxs[1]])
    cv.rectangle(img,(xp,yp),(xp+wp,yp+hp),(0,255,0),5)
    im_plate = im_gray[yp:yp+hp, xp:xp+wp] #crop

Hasil deteksi plat nomor akan seperti gambar di bawah ini:

Deteksi dan Membaca Plat Kendaraan menggunakan Python
Hasil deteksi plat nomor

Hasil dari deteksi plat nomor adalah citra plat nomor hasil cropping dari citra grayscale


Deteksi dan Membaca Plat Kendaraan menggunakan Python
Hasil Cropping

Kenapa saya crop dari citra grayscale? bukan citra BW? karena hasil karakternya akan lebih bagus meskipun harus melakukan prapengolahan kembali.

Segmentasi Karakter


Sebelum dilakukan segmentasi, ubah terlebih dahulu citra grayscale plat nomor menjadi BW seperti pada proses prapengolahan. lalu hilangkan bagian kecil yang mungkin bisa mengganggu dengan cara opening.

(thresh, im_bw) = cv.threshold(im_plate, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU) # convert from grayscale to black and white
cv.imshow('plat bw', im_bw)
kernel = cv.getStructuringElement(cv.MORPH_CROSS, (3,3)) # create kernel, shape = cross, size 3,3
im_bw = cv.morphologyEx(im_bw, cv.MORPH_OPEN, kernel) # apply morph open
cv.imshow('opening', im_bw)

Hasil sebelum dan sesudah opening menjadi seperti ini:

Deteksi dan Membaca Plat Kendaraan menggunakan Python
Kiri sebelum opening, dan kanan setelah opening

Selanjutnya adalah segmentasi karakter. Caranya sama seperti bagian deteksi plat nomor, yaitu menggunakan fungsi contours, hanya saja yang ini agak ribet. 

Langkah awal, kita buat dulu area contours-nya. lalu filter dengan berdasarkan tinggi dan lebarnya.

Jika ketinggiannya pada rentang 40-60 piksel dan lebarnya lebih dari atau sama dengan 10 piksel maka area tersebut adalah kemungkinan/kandidat karakternya. Ini berarti bisa benar atau bukan sebuah karakter, masih belum pasti.

contours, hierarchy = cv.findContours(im_bw, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) 
idx = 0 # counter index of contour in contours
index = [] # the character candidate will be stored here

for contour in contours:
    x,y,w,h = cv.boundingRect(contour)
    if (h >= 40 and h <= 60) and (w >=10):
        index.append(idx)
    idx += 1

Contoh hasilnya seperti ini:

Deteksi dan Membaca Plat Kendaraan menggunakan Python
Kandidat karakter

Kita tahu bahwa letak karakter akan selalu lurus (mungkin akan ada sedikit miring), berderet. Maka dari itu, untuk mengetahui kandidat tersebut benar sebuah karakter, kita bisa membandingkan letak ketinggian (koordinat sumbu y) kandidat satu dengan kandidat lainnya.  

Maka dari itu bandingkan setiap ketinggian sumbu y setiap kandidat, cari nilai absolut selisih pada setiap saat membandingkan. Jika selisihnya kurang dari 11 piksel, maka beri skor +1.

    score = np.zeros(len(index))
    c = 0
    for a in index:
        x1,y1,w1,h1 = cv.boundingRect(contours[a])
        for b in index:
            if a == b:
                continue
            else:
                x2,y2,w2,h2 = cv.boundingRect(contours[b])
                ydiff = abs(y1 - y2)
                if ydiff < 11:
                    score[c] = score[c] + 1 
        c += 1

Setiap kandidat yang benar sebuah karakter akan memiliki skor yang sama dan memiliki skor paling tinggi. Maka ambil kandidat tersebut berdasarkan syarat tersebut.

    chars = []
    b = 0
    for a in score:
        if a == max(score):
            chars.append(index[b])
        b += 1

Dari sini kita sudah mendapatkan karakter yang sebenarnya. 

Deteksi dan Membaca Plat Kendaraan menggunakan Python
Karakter yang sesungguhnya

Sayangnya kita tidak bisa langsung mengambil karakter tersebut, karena urutannya belum sesuai.

Index atau urutan area pada sebuah contours, berurutan dari atas ke bawah. Tentu ini menjadi masalah, karena kita membaca teks dari kiri ke kanan. Contohnya yang seharusnya mengurut B3023KEZ malah jadi 3B3ZE20K karena urutannya dari atas ke bawah.

Maka dari itu mari kita urutkan dengan berdasarkan koordinat sumbu x.

    a = 0
    xcoors = []
    for char in chars:
        x, y, w, h = cv.boundingRect(contours[char])
        xcoors.append(x) # get the x value

    xcoors = sorted(xcoors) # sort the x value (small --> large)

    real_chars = []
    for xcoor in xcoors:
        for char in chars:
            x, y, w, h = cv.boundingRect(contours[char])
            if xcoors[xcoors.index(xcoor)] == x:
                real_chars.append(char) # storing sorted character

Nah sampai di sini kita sudah mendapatkan karakternya dan mengurutkannya dari kiri ke kanan. 

Klasifikasi karakter


Untuk mengklasifikasi karakter, buat terlebih dahulu modelnya, lalu lakukan pelatihan dengan menggunakan dataset yang disediakan, untuk langkah-langkahnya saya mengikuti tutorial ini. Pada link tersebut sudah sangat jelas langkah-langkahnya dan penjelasannya.. 

Deteksi dan Membaca Plat Kendaraan menggunakan Python
Hasil Pelatihan Model

Setelah model selesai dilatih, model tersebut disimpan, lalu load untuk mengklasifikasi karakter. Untuk mengklasifikasi, kita bisa menggunakan kode di bawah ini:

    img_height = 40 # image height
    img_width = 40 # image width

    class_names = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']

    model = keras.models.load_model('my_model') # load model

    num_plate = []

    for rc in real_chars:
        x,y,w,h = cv.boundingRect(contours[rc])
        char_crop = cv.cvtColor(im_bw[y:y+h,x:x+w], cv.COLOR_GRAY2BGR) #crop character

        char_crop = cv.resize(char_crop, (img_width, img_height)) #resize to desired size

        img_array = keras.preprocessing.image.img_to_array(char_crop)
        img_array = tf.expand_dims(img_array, 0)

        predictions = model.predict(img_array) # make predictions
        score = tf.nn.softmax(predictions[0]) 

        num_plate.append(class_names[np.argmax(score)])
        print(class_names[np.argmax(score)], end='')

    # Show the result

    plate_number = ''
    for a in num_plate:
        plate_number += a

    cv.putText(img, plate_number,(xp, yp + hp + 50), cv.FONT_ITALIC, 2.0, (0,255,0), 3)
    cv.imshow(plate_number, img)

Dan hasil akhirnya seperti ini:

Deteksi dan Membaca Plat Kendaraan menggunakan Python
Hasil deteksi dan membaca plat nomor kendaraan

Saya sudah mencobanya kepada 62 gambar kendaraan, hasilnya 58 plat terdeteksi dengan baik (akurasi deteksi plat 93%).

Saran dan Masukan


Untuk hasil deteksi plat nomor yang baik tergantung dari proses prapengolahannya, temen-temen bisa bereksperimen di bagian ini. Atau temen-temen bisa mencoba metode lainnya, metode yang pernah saya pakai selain menggunakan contours adalah dengan metode edge detection dan profile projection

Untuk hasil klasifikasi yang lebih baik, temen-temen bisa bereksperimen pada prapengolahan citra plat nomornya agar hasilnya karakternya lebih optimal, sedikit gangguan. Temen-temen juga bisa menambahkan dataset lebih banyak untuk pelatihannya, atau memperbagus model yang dibuat sehingga hasil pelatihan menjadi lebih baik.

Kode Sumber


Proyek ini memang jauh dari kata sempurna, bahkan saya hanya menggunakan algoritma dan metode yang sederhana saja, yang mungkin bisa saja sudah usang, tapi dari sini saya bisa belajar OpenCV, Python, dan sedikit tentang Tensorflow.

Bagi temen-temen yang menginginkan kode sumbernya, temen-temen bisa mendownloadnya di halaman Github saya.


Terimakasih banyak yang sudah membaca.

Orang biasa yang sedang berproses. Senang ngoprek dan menulis sesuatu yang berbau teknologi, desain grafis, dan hal random lainnya.

Give us your opinion

Silakan Berkomentar