车牌识别-只用opencv的方式

news2024/11/18 23:46:41
项目简述

本文描述如何只使用opencv将车牌中的车牌号提取出来,整个过程大致分为三个过程:车牌定位,车牌号元素分割,模式匹配。

在做完这个实验后,我感触是,只用opencv的方式能使用的场景有限,不如用模型的方式适用的场景广,推荐还是使用模型去做。

下面我分别介绍这三个流程:

本文借鉴了  【OpenCV实战】简洁易懂的车牌号识别Python+OpenCV实现“超详解”(含代码)_车牌识别代码-CSDN博客

车牌定位

先定义一些公用的方法:

import cv2
import numpy as np
from matplotlib import pyplot as plt
import os

# plt显示彩色图片
def plt_show0(img):
#cv2与plt的图像通道不同:cv2为[b,g,r];plt为[r, g, b]
    b,g,r = cv2.split(img)
    img = cv2.merge([r, g, b])
    plt.imshow(img)
    plt.show()


# plt显示灰度图片
def plt_show_gray(img):
    plt.imshow(img, cmap='gray')
    plt.show()

# 图像去噪灰度处理
def gray_guss(image):
    image = cv2.GaussianBlur(image, (3, 3), 0)
    image_shape = image.shape
    if len(image_shape) == 3:
        gray_image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
        return gray_image
    else:
        return image
车牌灰度化

我们输入的图片

img_path = r'D:\tmp\cv\plate_number\car_3.jpg'
car = cv2.imread(img_path)
blur_car = gray_guss(car)
plt_show_gray(blur_car)

灰度化是为了后面做sobel检测,灰度化的图片是:

sobel检测
Sobel_x = cv2.Sobel(blur_car, cv2.CV_16S, 1, 0)

 将数据类型从CV_16S转化到CV_8U, 之所以sobel方法要用16S,是因为sobel在计算过程会产生负值,并且最大值会超过255。

absX = cv2.convertScaleAbs(Sobel_x)

寻找车牌位置

核心思想就是使用车牌的特征来寻找这牌的位置,比如说 车牌是长方形(长>宽),车牌一般不会 在图片的边缘上(拍照的习惯)等等,因此我们也看到的opecv方式的弊端,泛化能力比较差。

首先进行二值化

image = absX
ret, image = cv2.threshold(image, 0, 255, cv2.THRESH_OTSU)

然后我们使用闭操作,目的是让车牌形成一个白色的长方形,方便我们用车牌特征来匹配车牌的位置。

kernelX = cv2.getStructuringElement(cv2.MORPH_RECT, (30, 10))
image = cv2.morphologyEx(image, cv2.MORPH_CLOSE, kernelX,iterations = 1)

有点儿意思了,但是长方形内部还有些空隙,我们继续处理:

kernelX = cv2.getStructuringElement(cv2.MORPH_RECT, (50, 1))
kernelY = cv2.getStructuringElement(cv2.MORPH_RECT, (1, 20))

 我们在x方向上做膨胀操作,目的是让白色填充满车牌

image = cv2.dilate(image, kernelX)

在y方向做腐蚀操作,目的是让车牌和其他白色区域分离开,方便后面的轮廓查找

image = cv2.erode(image, kernelX)

然后来一次中值滤波,去掉白色小块,只保留比较大的区域

image = cv2.medianBlur(image, 21)

 

我们将所有的轮廓画出来,并计算一下面积:

contours, hierarchy = cv2.findContours(image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
car_operator = car.copy()

# 将所有区域取出来
data = {}
for index, item in enumerate(contours):
    rect = cv2.boundingRect(item)
    x = rect[0]
    y = rect[1]
    weight = rect[2]
    height = rect[3]
    # 根据轮廓的形状特点,确定车牌的轮廓位置并截取图像
    # if (weight > (height * 3.5)) and (weight < (height * 4)):
    # image = car[y:y + height, x:x + weight]
    # plt_show0(image)
    area = cv2.contourArea(item)
    data[index] = {'area': area, 'x':x, 'y':y, 'w':weight, 'h':height}
    cv2.putText(car_operator, str(area), (x+10, y+height+30), cv2.FONT_HERSHEY_COMPLEX, 0.8, (0, 0, 255), 3)
    cv2.rectangle(car_operator, (x, y), (x+weight, y+height), (0,255,0), 2)

根据车牌的特征过滤一下轮廓,我这里只用了面积和位置,你可以增加一些其他特点,让代码具有更前的泛化能力。

result_key = -1
for k in data:
    # 车牌在图片的中间位置,而不是边缘位置
    if data[k]['x'] == 0 or data[k]['y'] == 0:
        print(f"{k} is  at the top or the left, pass.")
        continue
   
    #  
    if data[k]['y'] + data[k]['h'] >= car_operator.shape[0]:
        print(f"{k} is  at hte bottom, pass.")
        continue

    if data[k]['x'] + data[k]['w'] >= car_operator.shape[1]:
        print(f"{k} is  at hte right, pass.")
        continue

    # 选取区域面积比较大(这个特点有点局限性,在我这个图片中比较合适)
    if result_key == -1:
           result_key = k
    else:
          if data[k]['area'] > data[result_key]['area']:
              result_key = k
result_key


x,y,h,w = data[result_key]['x'],data[result_key]['y'],data[result_key]['h'], data[result_key]['w']
image =  car[y:y + h, x:x + w]

 字符分割

字符分割的目的是将车牌中的多个字符分割成单个的字符,这样我们就可以对单个字符做模式匹配或者是用模型来做分类,从而识别单个字符是那个数字或者是那个字符,也或者是那个汉字,我们也就达到了车牌识别的目的。

二值化

首先进行高斯模糊屏蔽掉一些细节,然后再进行二值化

gray_image = gray_guss(image)
ret, gray_image = cv2.threshold(gray_image, 0, 255, cv2.THRESH_OTSU)

找出字符的轮廓

我们对车牌进行开操作,目的让字符和边缘尽可能的分开

kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
# gray_image = cv2.dilate(gray_image, kernel)
opening = cv2.morphologyEx(gray_image, cv2.MORPH_OPEN, kernel)

然后进行轮廓查找

img_copy = image.copy()
for item in contours:
    rect = cv2.boundingRect(item)
    x = rect[0]
    y = rect[1]
    w = rect[2]
    h = rect[3]
    cv2.rectangle(img_copy,(x,y),(x+w,y+h),(0,255,0),1)

 这里有一个问题,我们可以看到 苏 被分成两个轮廓了,这样肯定是不能进行匹配的,我试过对车牌进行膨胀操作,但是会导致E和车牌上的螺丝连在一起,最终导致E不能被识别。

这个实验中,我先处理的数字和字母,最后处理的汉字。

把所有的轮廓存储起来:

words = []

#对所有轮廓逐一操作
shape = image.shape
for item in contours:
    word = []
    rect = cv2.boundingRect(item)
    x = rect[0]
    y = rect[1]
    weight = rect[2]
    height = rect[3]
    word.append(x)
    word.append(y)
    word.append(weight)
    word.append(height)
    words.append(word)
# 排序,车牌号有顺序。words是一个嵌套列表
words = sorted(words,key=lambda s:s[0],reverse=False)

先做一个初步过滤,去掉那些在边缘上的轮廓:

filter_words = []
for item in words:
    x = item[0]
    y = item[1]
    w = item [2]
    h = item[3]
    if x == 0:
        print('x==0', item)
        continue
    if y == 0:
        print('y==0', item)
        continue
    if y + h >= shape[0]:
        print('y+h', item)
        continue
    if x + w >= shape[1]:
        print('x+w', item)
        continue

    
    filter_words.append(item)
filter_words

查找数字和字母

然后按照字符的长宽比例筛选出 字母和数字:

word_images = []
i = 0
success_word = []
#word中存放轮廓的起始点和宽高
for word in filter_words:
    # 筛选字符的轮廓
    # 高> 宽*1.5  高< 宽*3.5  高大于25
    print(word)
    if (word[3] > (word[2] * 1.5)) and (word[2] > 20 or (word[2] >= 7 and word[3] > 50)):
        i = i+1
        print(r'---')
        splite_image = gray_image[word[1]:word[1] + word[3], word[0]:word[0] + word[2]]
        word_images.append(splite_image)
        success_word.append(word)
        print(i)
# print(words)

for i,j in enumerate(word_images):  
    plt.subplot(1,7,i+1)
    plt.imshow(word_images[i],cmap='gray')

查找汉字

先切割出汉字

sucess_img = gray_image[:, :x]

然后进行膨胀操作,让苏的两部分连在一起

kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
sucess_img = cv2.dilate(sucess_img, kernel)

然后进行轮廓查找

contours, hierarchy = cv2.findContours(sucess_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
img_copy = image.copy()
for item in contours:
    rect = cv2.boundingRect(item)
    x = rect[0]
    y = rect[1]
    w = rect[2]
    h = rect[3]
    cv2.rectangle(img_copy,(x,y),(x+w,y+h),(0,255,0),1)

然后和上文一样,利用长宽特征过滤出有用的轮廓

s_words = []

#对所有轮廓逐一操作
shape = image.shape
for item in contours:
    word = []
    rect = cv2.boundingRect(item)
    x = rect[0]
    y = rect[1]
    weight = rect[2]
    height = rect[3]
    word.append(x)
    word.append(y)
    word.append(weight)
    word.append(height)
    s_words.append(word)
# 排序,车牌号有顺序。words是一个嵌套列表
s_words = sorted(s_words,key=lambda s:s[0],reverse=False)


filter_words = []
for item in s_words:
    x = item[0]
    y = item[1]
    w = item [2]
    h = item[3]
    if x == 0:
        print('x==0', item)
        continue
    if y == 0:
        print('y==0', item)
        continue
    if y + h >= shape[0]:
        print('y+h', item)
        continue
    if x + w >= shape[1]:
        print('x+w', item)
        continue

    
    filter_words.append(item)


splite_image = gray_image[filter_words[-1][1]:filter_words[-1][1] + filter_words[-1][3], filter_words[-1][0]:filter_words[-1][0] + filter_words[-1][2]]
word_images.insert(0, splite_image)

for i,j in enumerate(word_images):  
    plt.subplot(1,7,i+1)
    plt.imshow(word_images[i],cmap='gray')
plt.show()

 模式匹配

这里需要先获取待匹配的图片,也就是代码中的refer1文件夹,有需要的可以到QQ群中找我要,QQ群地址在文章的最后。

#模版匹配
# 准备模板(template[0-9]为数字模板;)
template = ['0','1','2','3','4','5','6','7','8','9',
            'A','B','C','D','E','F','G','H','J','K','L','M','N','P','Q','R','S','T','U','V','W','X','Y','Z',
            '藏','川','鄂','甘','赣','贵','桂','黑','沪','吉','冀','津','晋','京','辽','鲁','蒙','闽','宁',
            '青','琼','陕','苏','皖','湘','新','渝','豫','粤','云','浙']

# 读取一个文件夹下的所有图片,输入参数是文件名,返回模板文件地址列表
def read_directory(directory_name):
    referImg_list = []
    for filename in os.listdir(directory_name):
        referImg_list.append(directory_name + "/" + filename)
    return referImg_list

# 获得中文模板列表(只匹配车牌的第一个字符)
def get_chinese_words_list():
    chinese_words_list = []
    for i in range(34,64):
        #将模板存放在字典中
        c_word = read_directory('./refer1/'+ template[i])
        chinese_words_list.append(c_word)
    return chinese_words_list
chinese_words_list = get_chinese_words_list()


# 获得英文模板列表(只匹配车牌的第二个字符)
def get_eng_words_list():
    eng_words_list = []
    for i in range(10,34):
        e_word = read_directory('./refer1/'+ template[i])
        eng_words_list.append(e_word)
    return eng_words_list
eng_words_list = get_eng_words_list()


# 获得英文和数字模板列表(匹配车牌后面的字符)
def get_eng_num_words_list():
    eng_num_words_list = []
    for i in range(0,34):
        word = read_directory('./refer1/'+ template[i])
        eng_num_words_list.append(word)
    return eng_num_words_list
eng_num_words_list = get_eng_num_words_list()


# 读取一个模板地址与图片进行匹配,返回得分
def template_score(template,image):
    #将模板进行格式转换
    template_img=cv2.imdecode(np.fromfile(template,dtype=np.uint8),1)
    template_img = cv2.cvtColor(template_img, cv2.COLOR_RGB2GRAY)
    #模板图像阈值化处理——获得黑白图
    ret, template_img = cv2.threshold(template_img, 0, 255, cv2.THRESH_OTSU)
#     height, width = template_img.shape
#     image_ = image.copy()
#     image_ = cv2.resize(image_, (width, height))
    image_ = image.copy()
    #获得待检测图片的尺寸
    height, width = image_.shape
    # 将模板resize至与图像一样大小
    template_img = cv2.resize(template_img, (width, height))
    # 模板匹配,返回匹配得分
    result = cv2.matchTemplate(image_, template_img, cv2.TM_CCOEFF)
    return result[0][0]


# 对分割得到的字符逐一匹配
def template_matching(word_images):
    results = []
    for index,word_image in enumerate(word_images):
        if index==0:
            best_score = []
            for chinese_words in chinese_words_list:
                score = []
                for chinese_word in chinese_words:
                    result = template_score(chinese_word,word_image)
                    score.append(result)
                best_score.append(max(score))
            i = best_score.index(max(best_score))
            # print(template[34+i])
            r = template[34+i]
            results.append(r)
            continue
        if index==1:
            best_score = []
            for eng_word_list in eng_words_list:
                score = []
                for eng_word in eng_word_list:
                    result = template_score(eng_word,word_image)
                    score.append(result)
                best_score.append(max(score))
            i = best_score.index(max(best_score))
            # print(template[10+i])
            r = template[10+i]
            results.append(r)
            continue
        else:
            best_score = []
            for eng_num_word_list in eng_num_words_list:
                score = []
                for eng_num_word in eng_num_word_list:
                    result = template_score(eng_num_word,word_image)
                    score.append(result)
                best_score.append(max(score))
            i = best_score.index(max(best_score))
            # print(template[i])
            r = template[i]
            results.append(r)
            continue
    return results

 输出结果

word_images_ = word_images.copy()
# 调用函数获得结果
%time result = template_matching(word_images_)
print(result)
# "".join(result)函数将列表转换为拼接好的字符串,方便结果显示
print( "".join(result))

总体上这个方式的局限性比较大,距离应用在实际场景还有很大的差距,可以当做opencv的练手来玩。

这个车牌是可以识别出字符和数字部分

 

这个图片,车牌都定位不到,待优化的地方还是有很多的,后面可以用yolo试试

####

祝你好运

# 有问题可以进群聊聊

614809646  qq群->数字人和tts,运维、开发等等

####

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1475889.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2.27数据结构

1.链队 //link_que.c #include "link_que.h"//创建链队 Q_p create_que() {Q_p q (Q_p)malloc(sizeof(Q));if(qNULL){printf("空间申请失败\n");return NULL;}node_p L(node_p)malloc(sizeof(node));if(LNULL){printf("申请空间失败\n");return…

【八股文学习日记】集合概述

【八股文学习日记】集合概述 集合概述 Java 集合&#xff0c; 也叫作容器&#xff0c;主要是由两大接口派生而来&#xff1a;一个是 Collection接口&#xff0c;主要用于存放单一元素&#xff1b;另一个是 Map 接口&#xff0c;主要用于存放键值对。对于Collection 接口&#…

深入理解分库、分表、分库分表

前言 分库分表&#xff0c;是企业里面比较常见的针对高并发、数据量大的场景下的一种技术优化方案&#xff0c;所谓"分库分表"&#xff0c;根本就不是一件事儿&#xff0c;而是三件事儿&#xff0c;他们要解决的问题也都不一样&#xff0c;这三个事儿分别是"只…

kali安装ARL灯塔(docker)

1、root身份进入容器 ┌──(root㉿Kali)-[~/桌面] └─# su root ┌──(root㉿Kali)-[~/桌面] └─# docker 2、先更新再克隆 ┌──(root㉿Kali)-[~/桌面] └─# apt-get update …

【Android安全】Windows 环境下载 AOSP 源码

准备环境 安装 git 安装 Python 硬盘剩余容量最好大于 800G 打开 Git Bash&#xff0c;用 git 克隆源代码仓库 git clone https://android.googlesource.com/platform/manifest.git //没有梯子使用清华源 git clone https://aosp.tuna.tsinghua.edu.cn/platform/manifest.git这…

mac苹果电脑系统最好用的清理软件CleanMyMac2024功能介绍及如何激活解锁许可证

CleanMyMac X的界面设计简洁大气&#xff0c;为用户提供了直观且易于操作的使用体验。 布局清晰&#xff1a;界面布局非常明朗&#xff0c;左侧是功能栏&#xff0c;右侧则是信息界面。这种布局方式使得用户能够迅速找到所需的功能选项&#xff0c;提高了操作效率。色彩搭配&a…

Linux磁盘设备LVM介绍和常用场景说明

Linux常见的物理设备数据备份和负载均衡模式 1. LVM技术说明2. 相关概念3. 常用命令3.1 安装lvm命令3.2 创建分区3.3 格式化成LVM3.4 其他格式化 4. 常用场景4.1 创建LVM并挂载4.2 LVM扩容4.2.1 xfs扩容4.2.2 ext4扩容 4.2 缩减逻辑卷lv4.3 缩减vg&#xff1a;&#xff08;迁移…

LeetCode 刷题 [C++] 第54题.螺旋矩阵

题目描述 给你一个 m 行 n 列的矩阵 matrix &#xff0c;请按照 顺时针螺旋顺序 &#xff0c;返回矩阵中的所有元素。 题目分析 根据题意可知&#xff0c;我们不需要记录已经走过的路径&#xff0c;只需要通过调整矩阵的上下左右边界即可完成任务&#xff1b;首先创建出矩阵…

Unity中URP下实现水体(水面反射)

文章目录 前言一、原理1、法一&#xff1a;使用立方体纹理 CubeMap&#xff0c;作为反射纹理使用2、法二&#xff1a;使用反射探针生成环境反射图&#xff0c;所谓反射的采样纹理 二、实现水面反射1、定义和申明CubeMap2、反射向量需要什么3、计算 N ⃗ \vec{N} N 4、计算 V ⃗…

10_Vue

文章目录 Vue快速入门Vue的指令Vue的插值表达式V指令v-bind&#xff08;单向绑定&#xff09;v-model&#xff08;双向绑定&#xff09;v-on&#xff08;事件监听&#xff09;v-for&#xff08;循环&#xff09;v-text、v-htmlv-show&#xff08;显示/隐藏&#xff09;v-if&…

解压缩软件哪个好用?附详细操作步骤~

在日常生活和工作中&#xff0c;我们经常需要处理各种压缩文件&#xff0c;如ZIP、RAR、嗨格式压缩大师等。而要解压这些文件&#xff0c;就需要借助专门的解压缩软件。然而&#xff0c;在众多的解压缩软件中&#xff0c;究竟哪个更好用呢&#xff1f;本文将带您一起探寻&#…

QT C++实战:实现用户登录页面及多个界面跳转

主要思路 一个登录界面&#xff0c;以管理员Or普通用户登录管理员&#xff1a;一个管理员的操作界面&#xff0c;可以把数据录入到数据库中。有返回登陆按钮&#xff0c;可以选择重新登陆&#xff08;管理员Or普通用户普通用户&#xff1a;一个主界面&#xff0c;负责展示视频…

成人年龄判断(个人学习笔记黑马学习)

结合前面学习的input输入语句&#xff0c;完成如下案例: 1.通过input语句&#xff0c;获取键盘输入&#xff0c;为变量age赋值。(注意转换成数字类型) 2.通过if判断是否是成年人&#xff0c;满足条件则输出提示信息&#xff0c;如下&#xff1a; 欢迎来到黑马儿童游乐场&#x…

模块整理!YOLOv9中的“Silence”、“RepNCSPELAN4”、“ADown”、“CBLinear”创新模块汇总!

代码链接&#xff1a;https://github.com/WongKinYiu/yolov9/tree/main 论文链接&#xff1a;YOLOv9: Learning What You Want to Learn Using Programmable Gradient Information 大量文字及图片来袭&#xff01; 本文整理了YOLOv9中的创新模块&#xff0c;附代码和结构图&a…

Golang使用Swag搭建api文档

1. 简介 Gin是Golang目前最为常用的Web框架之一。 公司项目验收需要API接口设计说明书&#xff08;Golang后端服务基于Gin框架编写&#xff09;&#xff0c;编写任务自然就落到了我们研发人员身上。 项目经理提供了文档模板&#xff0c;让我们参考模板来手动编写&#xff0c;要…

代码随想录算法刷题训练营day27:LeetCode(39)组合总和、LeetCode(40)组合总和 II、LeetCode(131)分割回文串

代码随想录算法刷题训练营day27&#xff1a;LeetCode(39)组合总和、LeetCode(40)组合总和 II、LeetCode(131)分割回文串 LeetCode(39)组合总和 题目 代码 import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List;clas…

每日五道java面试题之spring篇(九)

目录&#xff1a; 第一题. 说一下Spring的事务传播行为第二题. 说一下 spring 的事务隔离&#xff1f;第三题. Spring AOP and AspectJ AOP 有什么区别&#xff1f;AOP 有哪些实现方式&#xff1f;第四题. JDK动态代理和CGLIB动态代理的区别第五题. 解释一下Spring AOP里面的几…

代码随想录刷题训练营day25:LeetCode(216)组合总和III、LeetCode(17)电话号码的字母组合

代码随想录刷题训练营day25&#xff1a;LeetCode(40)组合总和 II、LeetCode(216)组合总和III、LeetCode(17)电话号码的字母组合 LeetCode(40)组合总和 II 题目 代码 import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util…

集合详解-迭代器遍历-增强for-List集合-List五种遍历方式-Set集合-排序规则Comparable-双列集合

Collection集合 数组和集合的区别 相同点 都是容器,可以存储多个数据 不同点 数组的长度是不可变的,集合的长度是可变的 数组可以存基本数据类型和引用数据类型 集合只能存引用数据类型,如果要存基本数据类型,需要存对应的包装类 Collection 集合概述和使用 Collection…

毫米波雷达基本原理

毫米波 (mmWave) 是一类使用短波长电磁波的特殊雷达技术。雷达系统发射的电磁波信号被其发射路径上的物体阻挡继而会发生反射。通过捕捉反射的信号&#xff0c;雷达系统可以确定物体的距离、速度和角度。 毫米波雷达可发射波长为毫米量级的信号。在电磁频谱中&#xff0c;这种波…