基于Aidlux平台的智慧社区AI实战

       智慧社区作为智慧城市的最小单元,麻雀虽小五脏俱全。一般来说智慧社区主要分成以下三个方面:住房安全管控、社区环境管控以及物业服务管控三个部分。

(1)住房安全管控,主要包括消防安全的监管和入侵安全之类。其中消防安全主要指对可能出现的消防隐患实时检测,并对存在的隐患实时预警,如社区楼道里的烟火检测、电梯里的电动车检测。入侵安全主要为人为的闯入社区和楼栋,对可能的入侵加强检测,如人脸识别、车牌识别、周界入侵,楼宇对讲,入户指纹解锁等。

(2)社区环境管控,主要是对小区内的公共环境实时监测,如智能垃圾分类,高空抛物以及遛狗牵绳等的实时管理,对人车分流的小区,对闯入的车辆实时报警,对楼栋下禁止停放电动车、自行车,以及电动车、自行车等分区管理实时监管,对在公共区域摔倒的老人检测等。

(3)物业服务管控,主要是包括提升物业效率的智能化管控,如智能巡检,实时检查每个服务人员日常工作完成情况;对小区门口物业脱岗、睡觉等情况实时检测等。

以上为智慧社区的主要场景,除此之外智慧社区还有更广义的定义,如社区内解放人的场景,如小区用电的实时监测、异常点的报警以及物业任务的线上监督等,这些均可认为是智慧社区的一部分。

本次实战主要的场景分为社区中的高空抛物目标跟踪以及社区车辆检测+ 车牌识别推理两部分。

一、高空抛物目标跟踪:

高空抛物是智慧社区的重要部分之一,主要为主动识别高空中抛下的物体,一般场景为以监看和事后取证为主。

基于Aidlux平台的智慧社区AI实战_第1张图片

 比如上面的图片,我们在很多小区经常会看到类似的高空抛物相机,以仰视的角度,往住宅楼的角度拍摄,当发生抛物事件的时候,可以实时的监测到,当发生危险事故时,可以实时的去追踪,查看当时高空抛物的视频,追踪到底是从哪家的窗口抛出的。

难点:

高空抛物一般以事件为指标,需要识别出抛出的物体,并完成报警。

(1)抛出的物体相对于整个楼栋的目标太小;

(2)干扰因素较多,如白天的飞鸟、飘落的树叶、夜晚的背景楼栋灯光等;

(3)环境影响如雨天、雾天、逆光等环境对结果影响较大。

算法设计:

高空抛物的场景主要是识别出抛出来的物体,有几种识别方式:

1. 使用传统的动态目标检测,如光流检测和帧差法;

2. 使用目标检测+目标追踪算法,对抛出的物体先做目标检测,并对检测到的物体做追踪;

3. 使用物体追踪+过滤算法;

4.  使用视频分类的算法。

对于第一种方法传统方法的动态目标检测,如光流检测和帧差法,稳定性稍差,优点在于对于数据要求低。

对于第二种方法,使用目标检测检测被抛物体,并通过目标追踪对抛出物体的运动轨迹做追踪,会受到背景的影响很大,因为楼宇间的灯光等,同时使用目标检测+目标追踪的方法,其难点在于小目标的检测,很容易出现漏检。

同时运动的物体很多,如晒得被子等,容易出现误检,同时需要大量的数据。

对于第三种方法,针对第二种方法中的目标检测算法的效果不佳,采用高斯背景建模的方法,过滤背景信息;

再使用目标追踪如kalman滤波,完成运动轨迹的记录,同时针对第二种方法中视频中会出现的树叶、飞鸟以及晒衣服等的摆动等不符合抛物运动的轨迹的误检,通过SOM网络进行聚类,SOM(自组织映射神经网络)会对不同运动的轨迹进行分析。

分析流程:

基于Aidlux平台的智慧社区AI实战_第2张图片

算法实现:

因为涉及到高空抛物数据集的缺乏,所以在上面的四种方法中,主要选择第一种方法。大家在做项目有数据集支撑的情况下,建议选择第三种或者第四种方法。第二种方法目标检测+追踪的方式,对上游任务目标检测的要求较高,实际情况下的小目标容易漏检和误检,不建议使用。

针对于第一种的传统算法中,一般会有帧差法或者光流检测。

但是这都是最初级的方法,因为有许多局限性,比如帧差法对噪声敏感,无法避免对树叶的误检,在摄像头有轻微摇晃的情况下也会有很多误检,也无法适应光线变化等;

光流法也是相同的问题,而且光流法还有另外一个最大的问题是其基于稀疏特征点匹配的算法,因此实际上没有很好的办法将整张图的特征点分为不同的目标——虽然有稠密光流检测算法,但是耗时较长。基于此,我们可以将第三种方法中的“背景建模”加入第一种方法中。

1.去抖动

背景建模的前提是保证摄像机拍摄位置不变,保证背景是基本不发生变化的。

如路口的监控摄像机,只有车流人流等前景部分能发生移动,而马路树木等背景不能发生移动。

 def debouncing(self, image, ratio=0.7, reprojThresh=4.0, showMatches=False):
        image = cv2.resize(image, (int(image.shape[1]/1), int(image.shape[0]/1)))
        start = time.time()
        (kps, features) = self.detectAndDescribe(image)
        print(f"take {time.time() - start} s")
        M = self.matchKeypoints(kps, self.kps, features, self.features, ratio, reprojThresh)

        if M is None:
            return None

        (matches, H, status) = M
        result = cv2.warpPerspective(image, H, (image.shape[1] + image.shape[1], image.shape[0] + image.shape[0]))

        result = result[int(self.edge[1]):int(image.shape[0] - self.edge[1]),
                 int(self.edge[0]):int(image.shape[1] - self.edge[0])]

        cv2.namedWindow("result", cv2.WINDOW_NORMAL)
        cv2.imshow("result", result)

        start_img = self.start_image[int(self.edge[1]):int(image.shape[0] - self.edge[1]),
                    int(self.edge[0]):int(image.shape[1] - self.edge[0])]

        # 获取两张图的差分图
        sub_img = cv2.absdiff(result, start_img)

        cv2.namedWindow("start_img", cv2.WINDOW_NORMAL)
        cv2.imshow("start_img", start_img)
        cv2.namedWindow("sub_img", cv2.WINDOW_NORMAL)
        cv2.imshow("sub_img", sub_img)

        return result

 在去抖动后,将当前图与初始图对比,获得图片的差分图:

sub_img = cv2.absdiff(result, start_img)

2.背景建模

背景建模主要是为了检测运动物体,输出前景图片

在获得图片的差分图后,将差分图放入背景建模中,获取前景运动图。

背景建模在opencv中主要包含knn建模和高斯建模(MOG2)两种方法,这里我们选择的是KNN的方法,在knnDetector.py文件下: 其中history表示影响背景模型的历史帧数,dist2Threshold 表示像素和样本之间平方距离的阈值,当大于阈值的话,则为前景:

class knnDetector:
    def __init__(self, history, dist2Threshold, minArea):
        self.minArea = minArea 
        """
        此算法结合了静态背景图像估计和每个像素的贝叶斯分割。这是 2012 年Andrew_B.Godbehere,Akihiro_Matsukawa 和 Ken_Goldberg 在文章中提出的。它使用前面很少的图像(默认为前 120 帧)进行背景建模。使用了概率前景估计算法(使用贝叶斯估计鉴定前景)。这是一种自适应的估计,新观察到的对象比旧的对象具有更高的权重,从而对光照变化产生适应。一些形态学操作如开运算闭运算等被用来除去不需要的噪音。在前几帧图像中你会得到一个黑色窗口。对结果进行形态学开运算对与去除噪声帮助
        背景重建方法:MOG2 /knn 
        """
        self.detector = cv2.createBackgroundSubtractorKNN(history, dist2Threshold, False) # 背景建模

        """
        # 得到一个结构元素(卷积核)。主要用于后续的腐蚀、膨胀、开、闭等运算。
          因为这些运算都是依赖于卷积核的,不同的卷积核(形状、大小)对图形的腐蚀、膨胀操作效果不一样

        输入参数:
 		a设定卷积核的形状、b设定卷积核的大小、c表示描点的位置,一般 c = 1, 表示描点位于中心。
        返回值:
 		返回指定形状和尺寸的结构元素(一般是返回一个矩形)、也就是腐蚀/膨胀用的核的大小。
        """
        self.kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) # 

    def detectOneFrame(self, frame,index):
        if frame is None:
            return None
        start = time.time()
        mask = self.detector.apply(frame) # 背景重建,提取前景 
        # if index% 10 == 0 :
        #    cv2.imwrite(os.path.join(r"C:\Users\shime\Desktop\highthrow(1)\images", "mask_unprocess_{index}.jpg".format(index=index)), mask)
        stop = time.time()
        print("detect cast {} ms".format(stop - start))
        # cv2.namedWindow("mask_unprocess", cv2.WINDOW_NORMAL)
        # cv2.imshow("mask_unprocess", mask)

        start = time.time()
        mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, self.kernel) # 做开运算 先腐蚀,再膨胀
        # if index% 10 == 0 :
        #    cv2.imwrite(os.path.join(r"C:\Users\shime\Desktop\highthrow(1)\images", "mask_process_open_{index}.jpg".format(index=index)), mask)
        mask = cv2.morphologyEx(mask, cv2.MORPH_DILATE, self.kernel) # 再膨胀
        # if index% 10 == 0 :
        #    cv2.imwrite(os.path.join(r"C:\Users\shime\Desktop\highthrow(1)\images", "mask_process_dilate_{index}.jpg".format(index=index)), mask)
        stop = time.time()
        print("open contours cast {} ms".format(stop - start))

        start = time.time()
        contours, hierarchy = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 基于mask提取轮廓
        stop = time.time()
        print("find contours cast {} ms".format(stop - start))
        i = 0
        bboxs = []
        start = time.time()
        for c in contours:
            i += 1
            if cv2.contourArea(c) < self.minArea: # 过滤
                continue

            bboxs.append(cv2.boundingRect(c)) # 基于轮廓 寻找外接矩形 
        stop = time.time()
        print("select cast {} ms".format(stop - start))

        return mask, bboxs

3. 形态学处理

从上图中可以看到,前景的mask 中存在很多的干扰,如灯光的干扰等,再通过形态学处理将干扰项移除。首先通过开运算将前景中的毛刺过滤掉:

mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, self.kernel) # 做开运算 先腐蚀,再膨胀

再通过膨胀操作,将目标项变大,方便后面的目标追踪:

mask = cv2.morphologyEx(mask, cv2.MORPH_DILATE, self.kernel) # 再膨胀

4.目标检测

在第三步过滤掉干扰过后,找到目标的外接轮廓,同时过滤掉小的斑点干扰后,提取目标的外接矩形:

    contours, hierarchy = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 基于mask提取轮廓
        stop = time.time()
        print("find contours cast {} ms".format(stop - start))
        i = 0
        bboxs = []
        start = time.time()
        for c in contours:
            i += 1
            if cv2.contourArea(c) < self.minArea: # 过滤
                continue

            bboxs.append(cv2.boundingRect(c)) # 基于轮廓 寻找外接矩形 

5. 目标跟踪

SORT是 SIMPLE ONLINE AND REALTIME TRACKING 的简写,并不是什么排序算法。

其核心算法是匈牙利算法+卡尔曼滤波。

SORT算法没有用到特征跟踪,其本质实际上是根据观测的位置预测下一帧出现的位置,而我们预测的高空抛物实际上是有很强的规律的(重物规律强,较轻的物体如塑料袋或者纸板等,不是很规律,但是其速度不快,在每一帧之间基本上都有IOU重叠,因此也不会漏检),所以完全可以用此算法。

class Sort(object):

    #Sort跟踪算法中主要包括max_age,min_hits,iou_threshold等三个参数
    #1.max_age:表示在多少帧中没有检测,trackers就会终止。即最大预测数
    #2.min_hits:代表持续多少帧检测到,生成trackers。即最小更新数
    def __init__(self, max_age=1, min_hits=3, iou_threshold=0.3):

        #第一种情况:max_age = 3, min_hits = 3, iou_threshold = 0.3
        #第二种情况:max_age = 3, min_hits = 1, iou_threshold = 0.3
        #第三种情况:max_age = 3, min_hits = 1, iou_threshold = 0.8
        #第四种情况:max_age = 1, min_hits = 3, iou_threshold = 0.8
        """
        Sets key parameters for SORT
        """
        self.max_age = max_age
        self.min_hits = min_hits
        self.iou_threshold = iou_threshold
        self.trackers = []
        self.frame_count = 0

    def update(self, dets=np.empty((0, 5))):
        """
        Params:
          dets - a numpy array of detections in the format [[x1,y1,x2,y2,score],[x1,y1,x2,y2,score],...]
        Requires: this method must be called once for each frame even with empty detections (use np.empty((0, 5)) for frames without detections).
        Returns the a similar array, where the last column is the object ID.
        NOTE: The number of objects returned may differ from the number of detections provided.
        """
        self.frame_count += 1
        # get predicted locations from existing trackers.
        trks = np.zeros((len(self.trackers), 5))
        to_del = []
        ret = []
        # step1: predict
        for t, trk in enumerate(trks):
            pos = self.trackers[t].predict()[0]
            trk[:] = [pos[0], pos[1], pos[2], pos[3], 0]
            if np.any(np.isnan(pos)):
                to_del.append(t)
        trks = np.ma.compress_rows(np.ma.masked_invalid(trks))
        for t in reversed(to_del):
            self.trackers.pop(t)

        # if detect or track failed
        matched, unmatched_dets, unmatched_trks = associate_detections_to_trackers(dets, trks, self.iou_threshold)

        # update matched trackers with assigned detections
        for m in matched:
            self.trackers[m[1]].update(dets[m[0], :])

        # create and initialise new trackers for unmatched detections
        for i in unmatched_dets:
            trk = KalmanBoxTracker(dets[i, :])
            self.trackers.append(trk)
        i = len(self.trackers)
        for trk in reversed(self.trackers):
            bbox, is_throw = trk.get_state()
            if is_throw and (trk.time_since_update < 1) and (trk.hit_streak >= self.min_hits or self.frame_count <= self.min_hits):
                ret.append(np.concatenate((bbox, [trk.id + 1])).reshape(1, -1))  # +1 as MOT benchmark requires positive
            i -= 1
            # remove dead tracklet
            if (trk.time_since_update > self.max_age):
                self.trackers.pop(i)
        if (len(ret) > 0):
            return np.concatenate(ret)
        return np.empty((0, 5))

7. Aidlux平台的android端部署

通过https://blog.csdn.net/qq_42950407/article/details/127559963该博文,将VScode与Aidlux进行SSH远程链接,并将相关代码移植到Aidlux中,运行主程序便可完成android的推理。

本人所做的高空抛物链接:基于Aidlux平台的高空抛物目标跟踪_哔哩哔哩_bilibili

二、车辆检测+车牌识别

车辆数据集的下载:

因为我们需要训练模型,首先要准备数据集,考虑到智慧社区中,在社区内很少出现工程车辆,所以只需要覆盖大部分的蓝牌和绿牌的场景即可。最普遍的开源车牌数据集是中科大的CCPD数据集,官网链接是:GitHub - detectRecog/CCPD: [ECCV 2018] CCPD: a diverse and well-annotated dataset for license plate detection and recognition

中科大车牌数据集有CCPD2019和CCPD2020,其中CCPD2019主要为蓝牌,CCPD2020为绿牌。其中蓝牌是燃油车,绿牌是电动车。

这里我们主要用CCPD2019的蓝牌来作为我们的任务。

下载完成后会得到6个文件夹:

基于Aidlux平台的智慧社区AI实战_第3张图片

打开文件夹,每张图片的标签通过文件名展示。

图片命名:“0019-1_1-340&500_404&526-404&524_340&526_340&502_404&500-0_0_11_26_25_28_17-66-3.jpg”

基于Aidlux平台的智慧社区AI实战_第4张图片

 

解释:

0019:车牌区域占整个画面的比例;

1_1: 车牌水平和垂直角度, 水平1°, 竖直1°

340&500_404&526:标注框左上、右下坐标,左上(154, 383), 右下(386, 473)

404&524_340&526_340&502_404&500:标注框四个角点坐标,顺序为右下、左下、左上、右上

0_0_11_26_25_2_8:车牌号码映射关系如下: 第一个0为省份 对应省份字典provinces中的’皖’,;第二个0是该车所在地的地市一级代码,对应地市一级代码字典alphabets的’A’;后5位为字母和文字, 查看车牌号ads字典,如11为M,26为2,25为1,2为C,8为J 最终车牌号码为皖AM21CJ

省份:[“皖”, “沪”, “津”, “渝”, “冀”, “晋”, “蒙”, “辽”, “吉”, “黑”, “苏”, “浙”, “京”, “闽”, “赣”, “鲁”, “豫”, “鄂”, “湘”, “粤”, “桂”, “琼”, “川”, “贵”, “云”, “藏”, “陕”, “甘”, “青”, “宁”, “新”]

地市:[‘A’, ‘B’, ‘C’, ‘D’, ‘E’, ‘F’, ‘G’, ‘H’, ‘J’, ‘K’, ‘L’, ‘M’, ‘N’, ‘P’, ‘Q’, ‘R’, ‘S’, ‘T’, ‘U’, ‘V’, ‘W’,‘X’, ‘Y’, ‘Z’]

车牌字典:[‘A’, ‘B’, ‘C’, ‘D’, ‘E’, ‘F’, ‘G’, ‘H’, ‘J’, ‘K’, ‘L’, ‘M’, ‘N’, ‘P’, ‘Q’, ‘R’, ‘S’, ‘T’, ‘U’, ‘V’, ‘W’, ‘X’,‘Y’, ‘Z’, ‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’]

后面我们需要先对图片的标签解析,解析完后才能对其进行训练。

车牌识别的方案主要有两种:

一种是粗粒度的:车牌检测+车牌识别

另外一种细粒度的:车牌检测+车牌矫正+车牌识别。

后一种方法相对于前一种方法增加车牌矫正的部分,这部分主要考虑在场景中车牌在区域中出现的角度变化,如果是车牌与相机是相对平行的,则不需要矫正。

如果角度过大,则需矫正,这里面一般车牌的水平度和垂直度超过15°,建议增加矫正环节。

这里考虑到智慧社区的车与相机位置可以相对平行固定,故采用前一种方法,而其他如加油站场景中,摄像头因为要兼顾多种场景,不一定能做到平行,需要对车牌矫正后识别,效果更好。

考虑到数据集的庞大,需要进行简单验证和批量转换

简单验证:

def txt_translate(path, txt_path):
    for filename in os.listdir(path):
        print(filename)
        if not "-" in filename: #对于np等无标签的图片,过滤
            continue
        subname = filename.split("-", 3)[2]  # 第一次分割,以减号'-'做分割,提取车牌两角坐标
        extension = filename.split(".", 1)[1] #判断车牌是否为图片
        if not extension == 'jpg':
            continue
        lt, rb = subname.split("_", 1)  # 第二次分割,以下划线'_'做分割
        lx, ly = lt.split("&", 1) #左上角坐标
        rx, ry = rb.split("&", 1) # 右下角坐标
        width = int(rx) - int(lx) #车牌宽度
        height = int(ry) - int(ly)  # bounding box的宽和高
        cx = float(lx) + width / 2
        cy = float(ly) + height / 2  # bounding box中心点

        img = cv2.imread(os.path.join(path , filename))
        if img is None:  # 自动删除失效图片(下载过程有的图片会存在无法读取的情况)
            os.remove(os.path.join(path, filename))
            continue
        width = width / img.shape[1]
        height = height / img.shape[0]
        cx = cx / img.shape[1]
        cy = cy / img.shape[0]

        txtname = filename.split(".", 1)[0] +".txt"
        txtfile = os.path.join(txt_path, txtname)
        # 默认车牌为1类,标签为0
        with open(txtfile, "w") as f:
            f.write(str(0) + " " + str(cx) + " " + str(cy) + " " + str(width) + " " + str(height))

Yolo转voc xml格式的代码如下:

def xml_translate(image_path, txt_path,xml_path):
    from xml.dom.minidom import Document

    """此函数用于将yolo格式txt标注文件转换为voc格式xml标注文件
    """
    dic = {'0': "plate",  # 创建字典用来对类型进行转,此处的字典要与自己的classes.txt文件中的类对应,且顺序要一致
           }
    files = os.listdir(txt_path)
    for i, name in enumerate(files):
        xmlBuilder = Document()
        annotation = xmlBuilder.createElement("annotation")  # 创建annotation标签
        xmlBuilder.appendChild(annotation)
        txtFile = open( os.path.join(txt_path , name))
        txtList = txtFile.readlines()
        for root, dirs, filename in os.walk(image_path):
            img = cv2.imread(os.path.join(root , filename[i]))
            Pheight, Pwidth, Pdepth = img.shape

        folder = xmlBuilder.createElement("folder")  # folder标签
        foldercontent = xmlBuilder.createTextNode("driving_annotation_dataset")
        folder.appendChild(foldercontent)
        annotation.appendChild(folder)  # folder标签结束

        filename = xmlBuilder.createElement("filename")  # filename标签
        filenamecontent = xmlBuilder.createTextNode(name[0:-4] + ".jpg")
        filename.appendChild(filenamecontent)
        annotation.appendChild(filename)  # filename标签结束

        size = xmlBuilder.createElement("size")  # size标签
        width = xmlBuilder.createElement("width")  # size子标签width
        widthcontent = xmlBuilder.createTextNode(str(Pwidth))
        width.appendChild(widthcontent)
        size.appendChild(width)  # size子标签width结束

        height = xmlBuilder.createElement("height")  # size子标签height
        heightcontent = xmlBuilder.createTextNode(str(Pheight))
        height.appendChild(heightcontent)
        size.appendChild(height)  # size子标签height结束

        depth = xmlBuilder.createElement("depth")  # size子标签depth
        depthcontent = xmlBuilder.createTextNode(str(Pdepth))
        depth.appendChild(depthcontent)
        size.appendChild(depth)  # size子标签depth结束

        annotation.appendChild(size)  # size标签结束

        for j in txtList:
            oneline = j.strip().split(" ")
            object = xmlBuilder.createElement("object")  # object 标签
            picname = xmlBuilder.createElement("name")  # name标签
            namecontent = xmlBuilder.createTextNode(dic[oneline[0]])
            picname.appendChild(namecontent)
            object.appendChild(picname)  # name标签结束

            pose = xmlBuilder.createElement("pose")  # pose标签
            posecontent = xmlBuilder.createTextNode("Unspecified")
            pose.appendChild(posecontent)
            object.appendChild(pose)  # pose标签结束

            truncated = xmlBuilder.createElement("truncated")  # truncated标签
            truncatedContent = xmlBuilder.createTextNode("0")
            truncated.appendChild(truncatedContent)
            object.appendChild(truncated)  # truncated标签结束

            difficult = xmlBuilder.createElement("difficult")  # difficult标签
            difficultcontent = xmlBuilder.createTextNode("0")
            difficult.appendChild(difficultcontent)
            object.appendChild(difficult)  # difficult标签结束

            bndbox = xmlBuilder.createElement("bndbox")  # bndbox标签
            xmin = xmlBuilder.createElement("xmin")  # xmin标签
            mathData = max(int(((float(oneline[1])) * Pwidth + 1) - (float(oneline[3])) * 0.5 * Pwidth), 0)
            xminContent = xmlBuilder.createTextNode(str(mathData))
            xmin.appendChild(xminContent)
            bndbox.appendChild(xmin)  # xmin标签结束

            ymin = xmlBuilder.createElement("ymin")  # ymin标签
            mathData = max(int(((float(oneline[2])) * Pheight + 1) - (float(oneline[4])) * 0.5 * Pheight),0)
            yminContent = xmlBuilder.createTextNode(str(mathData))
            ymin.appendChild(yminContent)
            bndbox.appendChild(ymin)  # ymin标签结束

            xmax = xmlBuilder.createElement("xmax")  # xmax标签
            mathData = min(int(((float(oneline[1])) * Pwidth + 1) + (float(oneline[3])) * 0.5 * Pwidth),Pwidth)
            xmaxContent = xmlBuilder.createTextNode(str(mathData))
            xmax.appendChild(xmaxContent)
            bndbox.appendChild(xmax)  # xmax标签结束

            ymax = xmlBuilder.createElement("ymax")  # ymax标签
            mathData = min(int(((float(oneline[2])) * Pheight + 1) + (float(oneline[4])) * 0.5 * Pheight),Pheight)
            ymaxContent = xmlBuilder.createTextNode(str(mathData))
            ymax.appendChild(ymaxContent)
            bndbox.appendChild(ymax)  # ymax标签结束

            object.appendChild(bndbox)  # bndbox标签结束

            annotation.appendChild(object)  # object标签结束
        xml_save_path = os.path.join(xml_path, name[0:-4] + ".xml")

        f = open(xml_save_path, 'w')
        xmlBuilder.writexml(f, indent='\t', newl='\n', addindent='\t', encoding='utf-8')
        f.close()

车牌识别数据集建立:

import cv2
import os
import numpy as np

# 参考 https://blog.csdn.net/qq_36516958/article/details/114274778
# https://github.com/ultralytics/yolov5/wiki/Train-Custom-Data#2-create-labels
from PIL import Image
# CCPD车牌有重复,应该是不同角"度或者模糊程度
path = "E:/Aidlux3/image_rec"  # 改成自己的车牌路径

images_path = "E:/Aidlux3/images"        #改成自己的车牌路径
save_images_path = "E:/Aidlux3/image_rec"   #改成保存裁剪车牌的路径
if not os.path.exists(save_images_path):
    os.mkdir(save_images_path)

provinces = ["皖", "沪", "津", "渝", "冀", "晋", "蒙", "辽", "吉", "黑", "苏", "浙", "京", "闽", "赣", "鲁", "豫", "鄂", "湘", "粤", "桂", "琼", "川", "贵", "云", "藏", "陕", "甘", "青", "宁", "新", "警", "学", "O"]
alphabets = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
             'X', 'Y', 'Z', 'O']
ads = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
       'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'O']
num = 0
for filename in os.listdir(images_path):
    num += 1
    result = ""
    _, _, box, points, plate, brightness, blurriness = filename.split('-')
    list_plate = plate.split('_')  # 读取车牌
    result += provinces[int(list_plate[0])]
    result += alphabets[int(list_plate[1])]
    result += ads[int(list_plate[2])] + ads[int(list_plate[3])] + ads[int(list_plate[4])] + ads[int(list_plate[5])] + ads[int(list_plate[6])]
    # 新能源车牌的要求,如果不是新能源车牌可以删掉这个if
    # if result[2] != 'D' and result[2] != 'F' \
    #         and result[-1] != 'D' and result[-1] != 'F':
    #     print(filename)
    #     print("Error label, Please check!")
    #     assert 0, "Error label ^~^!!!"
    print(result)
    img_path = os.path.join(images_path, filename)
    img = cv2.imread(img_path)
    assert os.path.exists(img_path), "image file {} dose not exist.".format(img_path)

    box = box.split('_')  # 车牌边界
    box = [list(map(int, i.split('&'))) for i in box]

    xmin = box[0][0]
    xmax = box[1][0]
    ymin = box[0][1]
    ymax = box[1][1]

    img = Image.fromarray(img)
    img = img.crop((xmin, ymin, xmax, ymax))  # 裁剪出车牌位置
    img = img.resize((94, 24), Image.LANCZOS)
    img = np.asarray(img)  # 转成array,变成24*94*3

    cv2.imencode('.jpg', img)[1].tofile("E:/Aidlux3/image_rec/{}.jpg".format(result))
    # 图片中文名会报错
    # cv2.imwrite(r"K:\MyProject\datasets\ccpd\new\ccpd_2020\rec_images\train\{}.jpg".format(result), img)  # 改成自己存放的路径
print("共生成{}张".format(num))

在完成上述数据集后,本人依旧采用YOLOv5进行车辆与车牌数据集的训练。

车辆检测训练完成后会得到best.pt;车牌识别训练完成后会得到lprnet_best.pth的权重文件。

把车牌检测和识别的pt模型训练出来,整个的pipeline打通,但想把模型移植到Android端的话,我们需要将模型转换成Android端适配的模型。一般android移动端需要轻量化模型,轻量化模型如ncnn,tflite, paddlelite等,这里我们选择的tflite模型,不过pytorch直接转tflite的工具不齐全,一般都会转成序列化成onnx,再轻量化模型, 以pytorch->onnx->tflite 方式。

Onnx模型是基于Protobuf二进制格式,初始由微软和Facebook推出,后面得到了各大厂商和框架的支持。所以本节课,我们首先将车牌检测+识别模型导出成onnx模型。

1.车牌检测onnx导出

2.车牌识别模型的onnx导出

3.onnx模型的前向推理

4.车牌检测+识别模型的tflite的轻量化

5.车牌检测+识别的andorid端部署

注意:

  1. 使用netron 校对outputs和inputs的shape。
  2. 将整体代码上传至AIDLUX上实现部署,注意在aidlux上要使用cvs模块
# aidlux相关
from cvs import *
import aidlite_gpu
from utils import *
import time
import cv2
import os 

anchor = [[10, 13, 16, 30, 33, 23], [30, 61, 62, 45, 59, 119], [116, 90, 156, 198, 373, 326]]
#图像路径
source ="/home/code_plate_detection_recognization/demo/images"
det_model_path = "/home/code_plate_detection_recognization/weights/yolov5.tflite"
recog_model_path = "/home/code_plate_detection_recognization/weights/LPRNet_Simplified.tflite"
save_dir = "/home/code_plate_detection_recognization/demo/video_output"
imgsz =640
# AidLite初始化:调用AidLite进行AI模型的加载与推理,需导入aidlite
aidlite = aidlite_gpu.aidlite()
# Aidlite模型路径
# 定义输入输出shape
# 加载Aidlite检测模型:支持tflite, tnn, mnn, ms, nb格式的模型加载
aidlite.set_g_index(0)
in_shape0 = [1 * 3* 640 * 640 * 4]
out_shape0 = [1 * 3*40*40 * 6 * 4,1 * 3*20*20 * 6 * 4,1 * 3*80*80 * 6 * 4]
aidlite.ANNModel(det_model_path, in_shape0, out_shape0, 4, 0)
# 识别模型 
aidlite.set_g_index(1)
inShape1 =[1 * 3 * 24 *94*4]
outShape1= [1 * 68*18*4]
aidlite.ANNModel(recog_model_path,inShape1,outShape1,4,-1)

#视频路径
videopath = "/home/code_plate_detection_recognization/demo/zzu_carband.mp4"
capture = cv2.VideoCapture(videopath)


# frame_id = 0
#车辆检测+车牌识别的视频推理
# while cap.isOpened():
#     image_ori = capture.read()
#     frame_id += 1
#     if frame_id %5 ==0:

#车辆检测+车牌识别的图像推理
for img_name in os.listdir(source):
    print(img_name)
    image_ori = cv2.imread(os.path.join(source, img_name))
    # frame = cv2.imread("/home/code_plate_detection_recognization_1/demo/images/003748802682-91_84-220&469_341&511-328&514_224&510_224&471_328&475-10_2_5_22_31_31_27-103-12.jpg")
    # img = preprocess_img(frame, target_shape=(640, 640), div_num=255, means=None, stds=None)
    img,  scale, left, top = det_preprocess(image_ori, imgsz=640)
    # 数据转换:因为setTensor_Fp32()需要的是float32类型的数据,所以送入的input的数据需为float32,大多数的开发者都会忘记将图像的数据类型转换为float32
    aidlite.set_g_index(0)
    aidlite.setInput_Float32(img, 640, 640)
    # 模型推理API
    aidlite.invoke()
    # 读取返回的结果
    outputs = [0,0,0]
    for i in range(len(anchor)):
        pred = aidlite.getOutput_Float32(i)
    # 数据维度转换
        if pred.shape[0] ==28800:
            pred = pred.reshape(1, 3,40,40, 6)
            outputs[1] = pred           
        if pred.shape[0] ==7200:
            pred = pred.reshape(1, 3,20,20, 6)
            outputs[0] = pred
        if pred.shape[0]==115200:
            pred = pred.reshape(1,3,80,80, 6)
            outputs[2] = pred
    # 模型推理后处理
    boxes, confs, classes = det_poseprocess(outputs, imgsz, scale, left, top,conf_thresh=0.3, iou_thresh =0.5)   
    pred = np.hstack((boxes, confs,classes)).astype(np.float32, copy=False)

    for i, det in enumerate(pred):  # detections per image
        if len(det):
            xyxy,conf, cls= det[:4],det[4],det[5:]
            if xyxy.min()<0:
                continue           
            # filter 
            xyxy = np.reshape(xyxy, (1, 4))
            xyxy_ = np.copy(xyxy).tolist()[0]
            xyxy_ = [int(i) for i in xyxy_]
            if (xyxy_[2] -xyxy_[0])/(xyxy_[3]-xyxy_[1])>6 or (xyxy_[2] -xyxy_[0])<100:
                continue
            # image_crop = np.array(image_ori[xyxy_[1]:xyxy_[3], xyxy_[0]:xyxy_[2]])
            # image_crop = np.asarray(image_crop)
            image_recog = reg_preprocess(xyxy_, image_ori)
            print(image_recog.max(), image_recog.min(),type(image_recog),image_recog.shape)
            # recognization inference
            aidlite.set_g_index(1)
            aidlite.setInput_Float32(image_recog,94,24)
            aidlite.invoke()
            #取得模型的输出数据
            probs = aidlite.getOutput_Float32(0)
            print(probs.shape)
            probs = np.reshape(probs, (1, 68, 18))

            print("------",probs)
            # proprocess
            probs = reg_postprocess(probs)
            # print("pred_str", probs)
            for prob in probs:
                lb = ""
                for i in prob:
                    lb += CHARS[i]
                cls = lb

            # result show 
            

            label = f'names{[str(cls)]} {conf:.2f}'
            print(label)
            # plot_one_box(xyxy, im0, label=label, color=colors[int(cls)], line_thickness=3)
            # plot_one_box_class(xyxy_, image_ori, label=label, predstr=cls,
            #                     line_thickness=3)
            image_ori = plot_one_box_class(xyxy_, image_ori, label=label, predstr=cls,
                                line_thickness=3)
        # Save results (image with detections)
            #img_path = os.path.join(save_dir, img_name)
            # cv2.imwrite(img_path, image_ori)
            cvs.imshow(image_ori)

本人基于Aidlux平台的车辆检测+车牌识别图像推理链接:

基于Aidlux平台的车辆检测+车牌识别的图像推理_哔哩哔哩_bilibili

本人基于Aidlux平台的车辆检测+车牌识别视频推理链接:

基于Aidlux平台的车辆检测+车牌识别推理_哔哩哔哩_bilibili

其余代码请关注Aidlux公众号获取。

心得体会:

本人是在大刀老师以及Aidlux团队的训练营中学习而来,期间大刀老师区别以往的视频课,采用图文描述的方式以一种更加直观的方式展现出整个项目的流程与细节。不管是AI算法小白还是AI算法的老手都在这次训练营受益匪浅。Aidlux工程实践内容全是干货,同时过程也遇见了很多问题,但是大白老师和训练营的其他同学们都很认真为其他学员解决,耐心辅导,对我来言,刚刚接触这一领域,以及Aidlux平台的使用,让我耳目一新。整个流程下,我已经学会了如何在Aidlux进行模型部署,令我也感觉到成就感,在此特别感谢大刀老师和Aidlux团队的贡献,希望他们以后在AI算法开发的道路事业更加顺利。
 

你可能感兴趣的:(人工智能)