MTCNN/LPRNet车牌识别细节_lprnet mtcnn-程序员宅基地

技术标签: 算法  python  机器学习  计算机视觉  深度学习  Pytorch 目标检测  


前面一篇文章介绍了利用PyTorch实现的MTCNN/LPRNe车牌识别的理论框架,但是光有理论还不行,这篇文章主要是对里面的一些具体细节进行阐述。

车牌识别整体流程:

  1. 获取图片
  2. PNet网络处理
  3. ONet网络处理
  4. STN网络处理
  5. LPRNet网络识别
  6. 解码网络输出结果

来个流程图就是:

接下来就详细的把这几个步骤的情况说明一下:

1.获取车牌图片

目前已知的公开的数据集,最大的就是CCPD数据集了。

CCPD(中国城市停车数据集,ECCV)和PDRC(车牌检测与识别挑战)。这是一个用于车牌识别的大型国内的数据集,由中科大的科研人员构建出来的。发表在ECCV2018论文Towards End-to-End License Plate Detection and Recognition: A Large Dataset and Baseline

https://github.com/detectRecog/CCPD

该数据集在合肥市的停车场采集得来的,采集时间早上7:30到晚上10:00.涉及多种复杂环境。一共包含超多25万张图片,每种图片大小720x1160x3。一共包含9项。每项占比如下:

CCPD- 数量/k 描述
Base 200 正常车牌
FN 20 距离摄像头相当的远或者相当近
DB 20 光线暗或者比较亮
Rotate 10 水平倾斜20-25°,垂直倾斜-10-10°
Tilt 10 水平倾斜15-45°,垂直倾斜15-45°
Weather 10 在雨天,雪天,或者雾天
Blur(已删除) 5 由于相机抖动造成的模糊(这个后面被删了)
Challenge 10 其他的比较有挑战性的车牌
NP 5 没有车牌的新车

数据标注:

文件名就是数据标注。eg:

025-95_113-154&383_386&473-386&473_177&454_154&383_363&402-0_0_22_27_27_33_16-37-15.jpg

由分隔符-分为几个部分:

  1. 025为车牌占全图面积比,

  2. 95_113 对应两个角度, 水平倾斜度和垂直倾斜度,水平95°, 竖直113°

  3. 154&383_386&473对应边界框坐标:左上(154, 383), 右下(386, 473)

  4. 386&473_177&454_154&383_363&402对应四个角点右下、左下、左上、右上坐标

  5. 0_0_22_27_27_33_16为车牌号码 映射关系如下: 第一个为省份0 对应省份字典皖, 后面的为字母和文字, 查看ads字典.如0为A, 22为Y…

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’]
  1. 37亮度

  2. 15模糊度

所以根据文件名即可获得所有标注信息。

2.MTCNN数据处理

数据处理并不简单,主要包含三个部分:

  1. 对数据处理输入进PNet网络
  2. 对数据处理输入ONet网络

2.1 PNet网络数据预处理

返回最终结果:三种图片路径(positive,negative,part)+ 类别(positive=1,negative=0,part=-1)+ 回归框偏移值。

具体(以一张图片为例):

  1. 获取一张图片(不限大小)
  2. 获取图片已经标记好的boxes坐标(左上,右下)
  3. 生成大量候选框
  4. 计算候选框与实际框的IOU值:
    · 大于0.65,positive=1
    · 大于0.3,negative=0
    · 0.3~0.65,part=-1
  5. 计算回归框偏移量
  6. 分别保存裁剪后的图片
  7. 统一整合成最终的输出结果
    在这里插入图片描述

以下是MTCNN当中PNet人脸数据生成参考代码,仅供阅读:

# coding:utf-8
import os
import cv2
import numpy as np
import numpy.random as npr
def IoU(box, boxes):
    """Compute IoU between detect box and gt boxes
    Parameters:
    ----------
    box: numpy array , shape (4, ): x1, y1, x2, y2
        predicted boxes
    boxes: numpy array, shape (n, 4): x1, x2, y1, y2
        input ground truth boxes

    Returns:
    -------
    ovr: numpy.array, shape (n, )
        IoU
    """
    # 函数的传入参数为box(随机裁剪后的框)和boxes(实际人脸框)
    box_area = (box[2] - box[0] + 1) * (box[3] - box[1] + 1)
    # 计算随机裁剪后的框的面积,因为传入的box是以x1, y1, x2, y2这样的数组形式,所以分别对应着左上角的顶点坐标和右下角的顶点坐标,根据这两个坐
    # 标点就可以确定出了一个裁剪框,然后横纵坐标的差值的乘积就是随机裁剪框的面积,
    area = (boxes[:, 1] - boxes[:, 0] + 1) * (boxes[:, 3] - boxes[:, 2] + 1)
    # 同上,得出的是实际的人脸框的面积,但是这里要注意一点,因为一张图片的人脸是一个或者多个,所以说实际的boxes是个n行4列的数组,n>=1,n表示实
    # 际人脸的个数。故这里用到了boxes[:,2]-boxes[:,0]这样的写法,意思是取出所有维数的第3个元素减去对应的第1个元素,然后加上一,这样就把n个人
    # 脸对应的各自的面积存进了area这个数组里面
    xx1 = np.maximum(box[0], boxes[:, 0])  # 将随机裁剪框的x1和各个人脸的x1比较,得到较大的xx1
    yy1 = np.maximum(box[1], boxes[:, 2])  # 将随机裁剪框的y1和各个人脸的y1比较,得到较大的yy1
    xx2 = np.minimum(box[2], boxes[:, 1])  # 将随机裁剪框的x2和各个人脸的x2比较,得到较小的xx2
    yy2 = np.minimum(box[3], boxes[:, 3])  # 将随机裁剪框的y2和各个人脸的y2比较,得到较小的yy2
    # 这样做的目的是得出两个图片交叉重叠区域的矩形的左上角和右下角坐标

    # compute the width and height of the bounding box
    h = np.maximum(0, xx2 - xx1 + 1)

    w = np.maximum(0, yy2 - yy1 + 1)


    inter = w * h  # 求得重叠区域的面积
    ovr = inter / (box_area + area - inter)  # 重叠区域的面积除以真实人脸框的面积与随机裁剪区域面积的和减去重叠区域的面积就是重合率
    return ovr  # 返回重合率



anno_file = "C:/Desktop/train/trainImageList.txt"  # 下载的wider face数据集对应的每张图片的人脸方框数据
im_dir = "C:\\Users\\Desktop\\train"  # 将图片解压到这个文件夹
pos_save_dir = "E:/MTCNN/12/positive"  # 生成的正样本存放路径
part_save_dir = "E:/MTCNN/12/part"  # 生成的无关样本存放路径
neg_save_dir = 'E:/MTCNN/12/negative'  # 生成的负样本存放路径
save_dir = "E:/MTCNN/12"
if not os.path.exists(save_dir):  # 路径的创建
    os.makedirs(save_dir)
if not os.path.exists(pos_save_dir):
    os.makedirs(pos_save_dir)
if not os.path.exists(part_save_dir):
    os.makedirs(part_save_dir)
if not os.path.exists(neg_save_dir):
    os.makedirs(neg_save_dir)

f1 = open(os.path.join(save_dir, 'pos_12.txt'), 'w')  # 对应的样本的文档建立
f2 = open(os.path.join(save_dir, 'neg_12.txt'), 'w')
f3 = open(os.path.join(save_dir, 'part_12.txt'), 'w')
with open(anno_file, 'r') as f:
    annotations = f.readlines()  # 按行读取存放进列表annotations里面
num = len(annotations)  # 里面的每一个元素对应着一张照片的人脸数据,所以这个列表的大小就是数据集的照片数量。
print("%d pics in total" % num)  # 打印出照片的数量
p_idx = 0  # positive
n_idx = 0  # negative
d_idx = 0  # don't care
idx = 0
box_idx = 0
for annotation in annotations:  # for循环读取数据
    print(annotation)
    annotation = annotation.strip().split(' ')  # 去掉每一行数据的首尾空格换行字符,同时以空格为界限,分成一个个的字符
    # image path
    im_path = annotation[0]  # 第0号元素代表的是一个路径
    # print(im_path)
    # boxed change to float type
    bbox = list(map(float, annotation[1:5]))  # 第1号元素开始到第4个元素,每四个元素代表着一个人脸框
    # gt
    print(bbox)
    boxes = np.array(bbox, dtype=np.float32).reshape(-1, 4)  # 将人脸框的坐标进行reshape操作,变成n行4列的array
    # load image

    path = os.path.join(im_dir, im_path )
    path = path.replace('\\', '/')
    print(path)
    img = cv2.imread(os.path.join(im_dir, im_path ))  # 将路径拼接后读取图片
    idx += 1
    # if idx % 100 == 0:
    # print(idx, "images done")

    height, width, channel = img.shape  # 读取图片的宽、高、通道数并记录下来

    neg_num = 0  # 负样本数初始化为0
    # 1---->50
    # keep crop random parts, until have 50 negative examples
    # get 50 negative sample from every image
    while neg_num < 5:  # 负样本数小于50的时候
        # neg_num's size [40,min(width, height) / 2],min_size:40
        # size is a random number between 12 and min(width,height)
        size = npr.randint(12, min(width, height) / 2)  # size是一个随机数
        # top_left coordinate
        nx = npr.randint(0, width - size)  # 左上方的x坐标是一个随机数
        ny = npr.randint(0, height - size)  # 左上方的y坐标是一个随机数
        # random crop
        crop_box = np.array([nx, ny, nx + size, ny + size])  # 随机裁剪的样本
        print(crop_box)
        # calculate iou
        Iou = IoU(crop_box, boxes)  # 引入Iou()函数,含有两个参数,随机裁剪的样本crop_box和实际的人脸框boxes,计
        # 算出Iou()值

        # crop a part from inital image
        cropped_im = img[ny: ny + size, nx: nx + size, :]  # 将这个部分样本裁剪下来
        # resize the cropped image to size 12*12
        resized_im = cv2.resize(cropped_im, (12, 12),  # resize这个样本成12*12
                                interpolation=cv2.INTER_LINEAR)

        if np.max(Iou) < 0.3:  # 当Iou的值小于0.3的时候为负样本
            # Iou with all gts must below 0.3
            save_file = os.path.join(neg_save_dir, "%s.jpg" % n_idx)
            f2.write("E:/MTCNN/12/negative/%s.jpg" % n_idx + ' 0\n')  # 样本的路径保存下来
            cv2.imwrite(save_file, resized_im)  # 图片保存下来
            n_idx += 1
            neg_num += 1

    # for every bounding boxes
    for box in boxes:
        # box (x_left, x_right,y_top , y_bottom)
        x1, x2, y1, y2 = box
        # gt's width
        w = x2 - x1 + 1
        # gt's height
        h = y2 - y1 + 1
        # 获取每一个样本的宽和高

        # in case the ground truth boxes of small faces are not accurate
        # 忽略一些小的人脸和那些左顶点超出了图片的人脸框
        # 防止那些小人脸的坐标不准确
        if max(w, h) < 20 or x1 < 0 or y1 < 0:
            continue

        # 下面仍然是返回5个负样本,但是返回的样本一定是和真实的人脸框有一定的交集,即(0<IoU<0.3),上面返回的50负样本是不一定和真实人脸框有交集
        for i in range(2):
            # size of the image to be cropped
            size = npr.randint(12, min(width, height) / 2)
            # parameter high of randint make sure there will be intersection between bbox and cropped_box
            delta_x = npr.randint(max(-size, -x1), w)  # 求取(-size和-x1的最大值可以保证x1+delta_x一定大于等于0,
            delta_y = npr.randint(max(-size, -y1), h)  # 同上
            # max here not really necessary
            nx1 = int(max(0, x1 + delta_x))  # 得到x1的偏移坐标nx1
            ny1 = int(max(0, y1 + delta_y))  # 得到y1的偏移坐标ny1

            # 如果裁剪框的右下坐标超出了图片的范围就跳过此次循环,进行下一次裁剪,注意这里的width是原始图片的宽度,不是真实人脸框的宽w
            if nx1 + size > width or ny1 + size > height:
                continue
            crop_box = np.array([nx1, ny1, nx1 + size, ny1 + size])  # 获取裁剪后的矩形框
            Iou = IoU(crop_box, boxes)  # 计算IoU值

            cropped_im = img[ny1: ny1 + size, nx1: nx1 + size, :]
            # 图片resize到12*12
            resized_im = cv2.resize(cropped_im, (12, 12), interpolation=cv2.INTER_LINEAR)
            # 将符合条件的样本框保存,完成这部操作之后每张图片都生成了55个负样本
            if np.max(Iou) < 0.3:
                # Iou with all gts must below 0.3
                save_file = os.path.join(neg_save_dir, "%s.jpg" % n_idx)
                f2.write("E:/MTCNN/12/negative/%s.jpg" % n_idx + ' 0\n')
                cv2.imwrite(save_file, resized_im)
                n_idx += 1

        # 生成正样本和无关样本
        for i in range(3):
            # pos and part face size [minsize*0.8,maxsize*1.25]
            # 设置正样本和部分样本的size
            size = npr.randint(int(min(w, h) * 0.8), np.ceil(1.25 * max(w, h)))

            # delta here is the offset of box center
            if w < 5:
                print(w)
                continue

            # x1和y1的偏移量
            delta_x = npr.randint(-w *0.2, w * 0.2)
            delta_y = npr.randint(-h *0.2, h * 0.2)

            # deduct size/2 to make sure that the right bottom corner will be out of
            # nx1是人脸框的中点的x坐标加减0.2倍宽度再减去一半的size和0之间的最大值
            # ny1是人脸框的中点的y坐标加减0.2倍高度再减去一半的size和0之间的最大值
            nx1 = int(max(x1 + w / 2 + delta_x - size / 2, 0))
            ny1 = int(max(y1 + h / 2 + delta_y - size / 2, 0))
            nx2 = nx1 + size  # 获得右下角的nx2坐标
            ny2 = ny1 + size  # 获得右下角的ny2坐标

            # 去掉超出图片的的坐标点
            if nx2 > width or ny2 > height:
                continue
            crop_box = np.array([nx1, ny1, nx2, ny2])
            # yu gt de offset
            # 这是一个bounding box regression操作
            offset_x1 = (x1 - nx1) / float(size)
            offset_y1 = (y1 - ny1) / float(size)
            offset_x2 = (x2 - nx2) / float(size)
            offset_y2 = (y2 - ny2) / float(size)
            # 裁剪图片
            cropped_im = img[ny1: ny2, nx1: nx2, :]
            # resize操作
            resized_im = cv2.resize(cropped_im, (12, 12), interpolation=cv2.INTER_LINEAR)

            box_ = box.reshape(1, -1)  # reshape成行数等于一列数未知的数组
            iou = IoU(crop_box, box_)  # 计算IoU值
            if iou >= 0.65:  # 保存为正样本
                save_file = os.path.join(pos_save_dir, "%s.jpg" % p_idx)
                f1.write("E:/MTCNN/12/positive/%s.jpg" % p_idx + ' 1 %.2f %.2f %.2f %.2f\n' % (
                offset_x1, offset_y1, offset_x2, offset_y2))
                cv2.imwrite(save_file, resized_im)
                p_idx += 1
            elif iou >= 0.4:  # 保存为部分样本
                save_file = os.path.join(part_save_dir, "%s.jpg" % d_idx)
                f3.write("E:/MTCNN/12/part/%s.jpg" % d_idx + ' -1 %.2f %.2f %.2f %.2f\n' % (
                offset_x1, offset_y1, offset_x2, offset_y2))
                cv2.imwrite(save_file, resized_im)
                d_idx += 1
        box_idx += 1
        if idx % 100 == 0:
            print("%s images done, pos: %s part: %s neg: %s" % (idx, p_idx, d_idx, n_idx))
f1.close()
f2.close()
f3.close()

2.2 PNet网络训练

修改后的MTCNN结构如下:

输入是一个12 * 47大小的图片,所以训练前需要把生成的训练数据(通过生成bounding box,然后把该bounding box 剪切成12 * 47大小的图片),转换成12 * 47 * 3的结构。

PNet是一个全卷积网络,所以Input可以是任意大小的图片,用来传入我们要Inference的图片,但是这个时候Pnet的输出的就不是11大小的特征图了,而是一个WH的特征图,每个特征图上的网格对应于我们上面所说的(2个分类信息,4个回归框信息)

具体步骤:

  1. 定义一个Dataset类,含图片信息,标签信息和回归框信息(人脸识别多一个landmark信息)
  2. torch.utils.data.DataLoader读取数据
  3. 定义优化器和损失函数(分类用loss_cls = nn.CrossEntropyLoss()回归用loss_offset = nn.MSELoss()
  4. 将每张图片的预测值与原始值计算损失和准确率就OK了。
  5. 保存训练好的PNet网络。

2.3 ONet网络数据预处理

从上图撇开RNet网络,可以清晰的知道ONet是干什么的。他接收两部分的输入:

  1. 原始图像信息
  2. 经过PNet网络计算的输出bounding boxes信息

原始图像信息我就不说了,和PNet处理差不多(去GitHub查看),只是说明一下如何对PNet网络计算的输出bounding boxes信息进行利用。

回归框的非极大值抑制
由PNet网络计算的输出步骤,可以看到一个原始图片会产生大量的回归框,那么到底要把那个回归框让RNet继续训练呢?这里采用非极大值抑制方法(NMS),该算法的主要思想是:将所有框的得分排序,选中最高分及其对应的框;遍历其余的框,如果和当前最高分框的重叠面积(IOU)大于一定阈值,我们就将框删除;从未处理的框中继续选一个得分最高的,重复上述过程。

2.4 ONet网络训练

ONet网络训练方式类似于PNet,也是分类用loss_cls = nn.CrossEntropyLoss()回归用loss_offset = nn.MSELoss(),训练好模型之后保存ONet网络。

3. LPRNet数据处理和训练

LPRNet网络就很好理解了,说白了就是个分类网络,损失函数就是nn.CTCLoss(blank=len(CHARS)-1, reduction='mean'),至于为什么使用CTCLoss,可以看这篇文章“如何优雅的使用pytorch内置torch.nn.CTCLoss的方法

训练具体思路:

  1. 定义一个Dataset,获取图片信息(根据坐标只截取车牌部分)和标签信息(就是车牌号对应的数字)
  2. 定义LPRNet网络结构并初始化
  3. 定义STN网络结构,至于STN网络,没什么特殊的,直接用就是了。
  4. 定义优化器和损失函数
  5. 训练,计算损失和参数
  6. 保存模型

4. 预测

前面的话算起来差不多有4个网络(我的天,四个网络……)
这四个网络难点就在于PNet和ONet两个过程,后面两个很简单。
梳理一下这四个网络分别具体干了些啥:

4.1 PNet过程

  1. 我们输入一张图,首先形成图像金字塔(factor = 0.707或sqrt(0.5)),假设我们得到n张图像,我们一次将每张图像送入PNet(这里就体现了全卷积网络的优点了:对输入图像的尺寸没有要求)。
  2. PNet网络预测输出是预测回归框的偏移值(pred_offsets),接下来,对上述产生的结果使用NMS算法,算法的本质就是挑选出置信度最大的候选框
  3. NMS算法计算完毕后,返回从输入的bbox中挑选出的目标索引,因此首先根据索引挑选出目标bbox,然后根据目标bbox中指定的像素坐标和坐标位置差,确定车牌的真实坐标。根据上面所说,bbox的前面4项是bbox在原图像中的像素坐标,而最后面四项是候选框区域相对于像素坐标的偏差。因此,将原像素坐标加上偏差值,即可得到候选框的坐标。

4.2 ONet过程

将原始图片的信息和PNet网络预测的框框,输入给ONet网络,进行进一步修正,流程上除了图像金字塔这一部分其他和PNet差不多。

总结:经过PNet和ONet之后就可以得到精确地车牌位置信息

4.3 STN过程

这一步就很简单了,加载STN网络和预训练好的权重就可以直接用了。目的就是,调整图片(图片增强)。

STN = STNet()
STN.to(device)
STN.load_state_dict(torch.load('LPRNet/weights/Final_STN_model.pth', map_location=lambda storage, loc: storage))
STN.eval()

4.4 LPRNet过程

LPRNet过程就是一个分类过程,输入裁剪后的车牌图片就得出车牌信息对应的数值。

模型主干的基本构建块是受SqueezeNet、Fire Blocks和Inception Blocks的启发。输入图像大小设置为94x24像素RGB图像。图像由空间变换层(STN)进行预处理,以获得更好的特性。转换后的RGB图像通过特征提取骨干网络来捕获重要的特征,而不是使用LSTM。中间特征映射通过全局上下文嵌入和连接在一起进行增强。为了将特征映射的深度调整为字符类数,增加了1x1卷积。模型输出和目标字符序列长度不同。这里我们使用18的长度作为输出,对于每个输出字符,有68个不同的类是由字符产生的。

4.5 解码过程

简单点就是上一步LPRNet输出的并不是标准的车牌信息,而是需要我们将LPRNet输出转变为标准的车牌照字母数值。

def decode(preds, CHARS):
    # greedy decode
    pred_labels = list()
    labels = list()
    for i in range(preds.shape[0]):
        pred = preds[i, :, :]
        pred_label = list()
        for j in range(pred.shape[1]):
            pred_label.append(np.argmax(pred[:, j], axis=0))
        no_repeat_blank_label = list()
        pre_c = pred_label[0]
        for c in pred_label: # dropout repeate label and blank label
            if (pre_c == c) or (c == len(CHARS) - 1):
                if c == len(CHARS) - 1:
                    pre_c = c
                continue
            no_repeat_blank_label.append(c)
            pre_c = c
        pred_labels.append(no_repeat_blank_label)
        
    for i, label in enumerate(pred_labels):
        lb = ""
        for i in label:
            lb += CHARS[i]
        labels.append(lb)
    
    return labels, np.array(pred_labels) 

preds = preds.cpu().detach().numpy()  # (1, 68, 18)
labels, pred_labels = decode(preds, CHARS)
print("label is", labels)
print("pred_labels is", pred_labels)

5. 总结

说白了车牌检测也算是一个比较老的项目了,但是呢,从这个MTCNN+STN+LPRNet的项目来说,准确率是挺高的,但是相应的时间成本,运行成本代价也就稍微高了那么一点点(总之不怎么推荐)。以前做车牌检测都是一直用的OpenCV来做,什么二值化啊,腐蚀膨胀啊,边缘检测啊之类的,这里根据我个人的理解总结一下传统方法和深度学习方法的优缺点。

  1. 深度学习相比于传统方法准确率要高
  2. 深度学习方法对数据大小有要求,数据量小了根本训练不出好的网络,而传统方法对每张图处理方法都一样,不怎么受数据量大小的影响
  3. 传统方法对数据的质量要求比较高,例如正矩形要比平行四边形好识别

此外,针对于这种MTCNN来进行目标检测的,时候可以换成Faster RCNN,Mask RCNN或者YOLO V3/V4呢?LPRnet车牌识别能不能换成其他字符识别呢?

其实有时间的话可以去试一试,反正这些网络很多都有现成的框架,最终效果怎么样,还望在座的各位大佬带带我

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_40195360/article/details/105989051

智能推荐

什么是内部类?成员内部类、静态内部类、局部内部类和匿名内部类的区别及作用?_成员内部类和局部内部类的区别-程序员宅基地

文章浏览阅读3.4k次,点赞8次,收藏42次。一、什么是内部类?or 内部类的概念内部类是定义在另一个类中的类;下面类TestB是类TestA的内部类。即内部类对象引用了实例化该内部对象的外围类对象。public class TestA{ class TestB {}}二、 为什么需要内部类?or 内部类有什么作用?1、 内部类方法可以访问该类定义所在的作用域中的数据,包括私有数据。2、内部类可以对同一个包中的其他类隐藏起来。3、 当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷。三、 内部类的分类成员内部_成员内部类和局部内部类的区别

分布式系统_分布式系统运维工具-程序员宅基地

文章浏览阅读118次。分布式系统要求拆分分布式思想的实质搭配要求分布式系统要求按照某些特定的规则将项目进行拆分。如果将一个项目的所有模板功能都写到一起,当某个模块出现问题时将直接导致整个服务器出现问题。拆分按照业务拆分为不同的服务器,有效的降低系统架构的耦合性在业务拆分的基础上可按照代码层级进行拆分(view、controller、service、pojo)分布式思想的实质分布式思想的实质是为了系统的..._分布式系统运维工具

用Exce分析l数据极简入门_exce l趋势分析数据量-程序员宅基地

文章浏览阅读174次。1.数据源准备2.数据处理step1:数据表处理应用函数:①VLOOKUP函数; ② CONCATENATE函数终表:step2:数据透视表统计分析(1) 透视表汇总不同渠道用户数, 金额(2)透视表汇总不同日期购买用户数,金额(3)透视表汇总不同用户购买订单数,金额step3:讲第二步结果可视化, 比如, 柱形图(1)不同渠道用户数, 金额(2)不同日期..._exce l趋势分析数据量

宁盾堡垒机双因素认证方案_horizon宁盾双因素配置-程序员宅基地

文章浏览阅读3.3k次。堡垒机可以为企业实现服务器、网络设备、数据库、安全设备等的集中管控和安全可靠运行,帮助IT运维人员提高工作效率。通俗来说,就是用来控制哪些人可以登录哪些资产(事先防范和事中控制),以及录像记录登录资产后做了什么事情(事后溯源)。由于堡垒机内部保存着企业所有的设备资产和权限关系,是企业内部信息安全的重要一环。但目前出现的以下问题产生了很大安全隐患:密码设置过于简单,容易被暴力破解;为方便记忆,设置统一的密码,一旦单点被破,极易引发全面危机。在单一的静态密码验证机制下,登录密码是堡垒机安全的唯一_horizon宁盾双因素配置

谷歌浏览器安装(Win、Linux、离线安装)_chrome linux debian离线安装依赖-程序员宅基地

文章浏览阅读7.7k次,点赞4次,收藏16次。Chrome作为一款挺不错的浏览器,其有着诸多的优良特性,并且支持跨平台。其支持(Windows、Linux、Mac OS X、BSD、Android),在绝大多数情况下,其的安装都很简单,但有时会由于网络原因,无法安装,所以在这里总结下Chrome的安装。Windows下的安装:在线安装:离线安装:Linux下的安装:在线安装:离线安装:..._chrome linux debian离线安装依赖

烤仔TVの尚书房 | 逃离北上广?不如押宝越南“北上广”-程序员宅基地

文章浏览阅读153次。中国发达城市榜单每天都在刷新,但无非是北上广轮流坐庄。北京拥有最顶尖的文化资源,上海是“摩登”的国际化大都市,广州是活力四射的千年商都。GDP和发展潜力是衡量城市的数字指...

随便推点

java spark的使用和配置_使用java调用spark注册进去的程序-程序员宅基地

文章浏览阅读3.3k次。前言spark在java使用比较少,多是scala的用法,我这里介绍一下我在项目中使用的代码配置详细算法的使用请点击我主页列表查看版本jar版本说明spark3.0.1scala2.12这个版本注意和spark版本对应,只是为了引jar包springboot版本2.3.2.RELEASEmaven<!-- spark --> <dependency> <gro_使用java调用spark注册进去的程序

汽车零部件开发工具巨头V公司全套bootloader中UDS协议栈源代码,自己完成底层外设驱动开发后,集成即可使用_uds协议栈 源代码-程序员宅基地

文章浏览阅读4.8k次。汽车零部件开发工具巨头V公司全套bootloader中UDS协议栈源代码,自己完成底层外设驱动开发后,集成即可使用,代码精简高效,大厂出品有量产保证。:139800617636213023darcy169_uds协议栈 源代码

AUTOSAR基础篇之OS(下)_autosar 定义了 5 种多核支持类型-程序员宅基地

文章浏览阅读4.6k次,点赞20次,收藏148次。AUTOSAR基础篇之OS(下)前言首先,请问大家几个小小的问题,你清楚:你知道多核OS在什么场景下使用吗?多核系统OS又是如何协同启动或者关闭的呢?AUTOSAR OS存在哪些功能安全等方面的要求呢?多核OS之间的启动关闭与单核相比又存在哪些异同呢?。。。。。。今天,我们来一起探索并回答这些问题。为了便于大家理解,以下是本文的主题大纲:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JCXrdI0k-1636287756923)(https://gite_autosar 定义了 5 种多核支持类型

VS报错无法打开自己写的头文件_vs2013打不开自己定义的头文件-程序员宅基地

文章浏览阅读2.2k次,点赞6次,收藏14次。原因:自己写的头文件没有被加入到方案的包含目录中去,无法被检索到,也就无法打开。将自己写的头文件都放入header files。然后在VS界面上,右键方案名,点击属性。将自己头文件夹的目录添加进去。_vs2013打不开自己定义的头文件

【Redis】Redis基础命令集详解_redis命令-程序员宅基地

文章浏览阅读3.3w次,点赞80次,收藏342次。此时,可以将系统中所有用户的 Session 数据全部保存到 Redis 中,用户在提交新的请求后,系统先从Redis 中查找相应的Session 数据,如果存在,则再进行相关操作,否则跳转到登录页面。此时,可以将系统中所有用户的 Session 数据全部保存到 Redis 中,用户在提交新的请求后,系统先从Redis 中查找相应的Session 数据,如果存在,则再进行相关操作,否则跳转到登录页面。当数据量很大时,count 的数量的指定可能会不起作用,Redis 会自动调整每次的遍历数目。_redis命令

URP渲染管线简介-程序员宅基地

文章浏览阅读449次,点赞3次,收藏3次。URP的设计目标是在保持高性能的同时,提供更多的渲染功能和自定义选项。与普通项目相比,会多出Presets文件夹,里面包含着一些设置,包括本色,声音,法线,贴图等设置。全局只有主光源和附加光源,主光源只支持平行光,附加光源数量有限制,主光源和附加光源在一次Pass中可以一起着色。URP:全局只有主光源和附加光源,主光源只支持平行光,附加光源数量有限制,一次Pass可以计算多个光源。可编程渲染管线:渲染策略是可以供程序员定制的,可以定制的有:光照计算和光源,深度测试,摄像机光照烘焙,后期处理策略等等。_urp渲染管线

推荐文章

热门文章

相关标签