最近对人工智能、计算机视觉等一类的东西很感兴趣,突发奇想想做一个停车场管理系统,从其中就需要车牌识别,于是尝试学习并尝试做一下这个yolo车牌检测识别的项目。
在构建停车场管理系统的过程中,车牌识别是核心功能之一。为了实现这一目标,选择了YOLO(You Only Look Once)作为车牌检测与识别的工具。以下将详细解读项目中的各个模块,包括 main.py
、visualize.py
、add\_missing\_data.py
和 util.py
,并逐步讲解它们的处理过程和背后的知识点。
main.py
作为整个项目的主入口,负责整体流程的控制。该模块的核心功能包括加载YOLO模型、读取输入视频、进行图像处理以及输出检测结果。
在项目开始时,首先导入必要的库和模块,如 OpenCV、YOLO 和自定义的工具模块。接着,使用 YOLO 模型加载函数载入预训练的模型文件。这一步骤至关重要,因为它为后续的车辆和车牌检测提供了必要的网络结构和参数。
from ultralytics import YOLO
import cv2
import util
from sort.sort import \*
from util import get\_car, read\_license\_plate, write\_csv
# 初始化检测结果字典和 SORT 追踪器
results = {}
mot\_tracker = Sort()
# 加载 YOLO 模型
coco\_model = YOLO('yolov8n.pt')
license\_plate\_detector = YOLO('./models/license\_plate\_detector.pt')
接下来,使用 OpenCV 读取输入视频并逐帧进行处理。每帧的处理包括检测车辆和车牌的过程。
# 加载视频
cap = cv2.VideoCapture('./sample.mp4')
# 初始化帧计数器
frame\_nmr = -1
ret = True
while ret:
frame\_nmr += 1
ret, frame = cap.read()
if ret:
results[frame\_nmr] = {}
...
对于每帧图像,首先使用 YOLO 模型检测车辆。检测结果包含每个车辆的边界框信息及其置信度分数。通过过滤车辆类别,只保留主要关心的车辆(如轿车、SUV等)。
# 检测车辆
detections = coco\_model(frame)[0]
detections\_ = []
for detection in detections.boxes.data.tolist():
x1, y1, x2, y2, score, class\_id = detection
if int(class\_id) in vehicles:
detections\_.append([x1, y1, x2, y2, score])
使用 SORT 算法对检测到的车辆进行追踪。每个车辆在视频中分配一个唯一的 ID,以便后续识别和关联车牌信息。
# 追踪车辆
track\_ids = mot\_tracker.update(np.asarray(detections\_))
对于每帧图像,使用车牌检测模型来识别车牌。检测到的每个车牌都被分配到相应的车辆 ID,以确保每个车牌和其对应的车辆保持一致。
# 检测车牌
license\_plates = license\_plate\_detector(frame)[0]
for license\_plate in license\_plates.boxes.data.tolist():
x1, y1, x2, y2, score, class\_id = license\_plate
# 将车牌分配给车辆
xcar1, ycar1, xcar2, ycar2, car\_id = get\_car(license\_plate, track\_ids)
成功分配车牌后,裁剪出车牌区域,并对其进行灰度转换和二值化处理,以便于后续的字符识别。
if car\_id != -1:
# 裁剪车牌图像
license\_plate\_crop = frame[int(y1):int(y2), int(x1): int(x2), :]
# 处理车牌图像
license\_plate\_crop\_gray = cv2.cvtColor(license\_plate\_crop, cv2.COLOR\_BGR2GRAY)
\_, license\_plate\_crop\_thresh = cv2.threshold(license\_plate\_crop\_gray, 64, 255, cv2.THRESH\_BINARY\_INV)
# 读取车牌号码
license\_plate\_text, license\_plate\_text\_score = read\_license\_plate(license\_plate\_crop\_thresh)
if license\_plate\_text is not None:
results[frame\_nmr][car\_id] = {'car': {'bbox': [xcar1, ycar1, xcar2, ycar2]},
'license\_plate': {'bbox': [x1, y1, x2, y2],
'text': license\_plate\_text,
'bbox\_score': score,
'text\_score': license\_plate\_text\_score}}
最后,将处理后的检测结果写入 CSV 文件,以便后续的数据分析和处理。
# 写入结果
write\_csv(results, './test.csv')
main.py
模块主要实现了视频中的车辆和车牌检测功能,并将结果存储为结构化数据。
好的,下面是更加详细和自然的描述,包含对 visualize.py
功能的讲解及其重要性。
visualize.py
:结果可视化模块visualize.py
模块的主要功能是将车牌识别的结果以可视化的形式展示在视频中。这不仅可以帮助开发者直观地理解识别系统的表现,还能用于展示最终的产品效果,增强用户体验。
首先需要导入一些必要的库:
cv2
:用于图像和视频处理。numpy
:用于数值计算和数组操作。pandas
:用于数据处理和分析。import ast
import cv2
import numpy as np
import pandas as pd
draw\_border
函数负责在图像上绘制车辆和车牌的边框。通过这种可视化,可以清晰地看到识别的区域,帮助分析模型的准确性。
def draw\_border(img, top\_left, bottom\_right, color=(0, 255, 0), thickness=10, line\_length\_x=200, line\_length\_y=200):
x1, y1 = top\_left
x2, y2 = bottom\_right
# 绘制左上角边框
cv2.line(img, (x1, y1), (x1, y1 + line\_length\_y), color, thickness) # 左侧
cv2.line(img, (x1, y1), (x1 + line\_length\_x, y1), color, thickness) # 上侧
# 绘制左下角边框
cv2.line(img, (x1, y2), (x1, y2 - line\_length\_y), color, thickness) # 左侧
cv2.line(img, (x1, y2), (x1 + line\_length\_x, y2), color, thickness) # 下侧
# 绘制右上角边框
cv2.line(img, (x2, y1), (x2 - line\_length\_x, y1), color, thickness) # 上侧
cv2.line(img, (x2, y1), (x2, y1 + line\_length\_y), color, thickness) # 右侧
# 绘制右下角边框
cv2.line(img, (x2, y2), (x2, y2 - line\_length\_y), color, thickness) # 右侧
cv2.line(img, (x2, y2), (x2 - line\_length\_x, y2), color, thickness) # 下侧
return img
通过 Pandas 读取 CSV 文件中的识别结果,这些结果是模型处理视频后生成的,包括每个车牌的置信度和位置信息。
results = pd.read\_csv('./test\_interpolated.csv') # 读取包含识别结果的 CSV 文件
使用 OpenCV 加载视频文件。同时设置了输出视频的编码格式和帧率,以确保生成的视频与原视频质量一致。
# 加载视频
video\_path = 'sample.mp4'
cap = cv2.VideoCapture(video\_path) # 创建视频捕获对象
# 指定视频编码和帧率
fourcc = cv2.VideoWriter\_fourcc(\*'mp4v')
fps = cap.get(cv2.CAP\_PROP\_FPS) # 获取原视频的帧率
width = int(cap.get(cv2.CAP\_PROP\_FRAME\_WIDTH)) # 获取视频宽度
height = int(cap.get(cv2.CAP\_PROP\_FRAME\_HEIGHT)) # 获取视频高度
out = cv2.VideoWriter('./out.mp4', fourcc, fps, (width, height)) # 创建输出视频文件
为每个车辆提取最大置信度的车牌信息,并存储对应的车牌图像。这样做的目的是在后续绘制过程中,能够快速访问每辆车的识别结果。
license\_plate = {}
for car\_id in np.unique(results['car\_id']): # 遍历所有唯一的车ID
max\_ = np.amax(results[results['car\_id'] == car\_id]['license\_number\_score']) # 找到最大置信度
license\_plate[car\_id] = {
'license\_crop': None, # 初始化车牌裁剪图像
'license\_plate\_number': results[(results['car\_id'] == car\_id) &
(results['license\_number\_score'] == max\_)]['license\_number'].iloc[0] # 存储车牌号
}
# 设置视频帧的位置
cap.set(cv2.CAP\_PROP\_POS\_FRAMES, results[(results['car\_id'] == car\_id) &
(results['license\_number\_score'] == max\_)]['frame\_nmr'].iloc[0])
ret, frame = cap.read() # 读取该帧
# 获取车牌的边界框坐标
x1, y1, x2, y2 = ast.literal\_eval(results[(results['car\_id'] == car\_id) &
(results['license\_number\_score'] == max\_)]['license\_plate\_bbox'].iloc[0].replace('[ ', '[').replace(' ', ' ').replace(' ', ' ').replace(' ', ','))
# 裁剪车牌图像并调整大小
license\_crop = frame[int(y1):int(y2), int(x1):int(x2), :]
license\_crop = cv2.resize(license\_crop, (int((x2 - x1) \* 400 / (y2 - y1)), 400)) # 调整裁剪图像的大小
license\_plate[car\_id]['license\_crop'] = license\_crop # 保存裁剪的车牌图像
在这一部分,逐帧读取视频,并根据识别结果在每帧上绘制车辆和车牌的边界框。通过这种方式,用户可以直观地看到模型的识别效果,并评估其准确性和可靠性。
frame\_nmr = -1 # 帧计数器
cap.set(cv2.CAP\_PROP\_POS\_FRAMES, 0) # 重置视频到开头
# 读取帧
ret = True
while ret:
ret, frame = cap.read() # 读取一帧
frame\_nmr += 1 # 增加帧计数
if ret:
df\_ = results[results['frame\_nmr'] == frame\_nmr] # 获取当前帧的结果
for row\_indx in range(len(df\_)):
# 绘制车辆边界框
car\_x1, car\_y1, car\_x2, car\_y2 = ast.literal\_eval(df\_.iloc[row\_indx]['car\_bbox'].replace('[ ', '[').replace(' ', ' ').replace(' ', ' ').replace(' ', ','))
draw\_border(frame, (int(car\_x1), int(car\_y1)), (int(car\_x2), int(car\_y2)), (0, 255, 0), 25,
line\_length\_x=200, line\_length\_y=200)
# 绘制车牌边界框
x1, y1, x2, y2 = ast.literal\_eval(df\_.iloc[row\_indx]['license\_plate\_bbox'].replace('[ ', '[').replace(' ', ' ').replace(' ', ' ').replace(' ', ','))
cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 12) # 绘制车牌的矩形框
# 获取并显示车牌裁剪图像
license\_crop = license\_plate[df\_.iloc[row\_indx]['car\_id']]['license\_crop'] # 获取车牌裁剪图像
H, W, \_ = license\_crop.shape # 获取车牌裁剪图像的高度和宽度
try:
# 在车辆上方插入车牌图像
frame[int(car\_y1) - H - 100:int(car\_y1) - 100,
int((car\_x2 + car\_x1 - W) / 2):int((car\_x2 + car\_x1 + W) / 2), :] = license\_crop
# 在车牌上方添加白色背景
frame[int(car\_y1) - H - 400:int(car\_y1) - H - 100,
int((car\_x2 + car\_x1 - W) / 2):int((car\_x2 + car\_x1 + W) / 2), :] = (255, 255, 255)
# 获取文本的尺寸
(text\_width, text\_height), \_ = cv2.getTextSize(
license\_plate[df\_.iloc[row\_indx]['car\_id']]['license\_plate\_number'],
cv2.FONT\_HERSHEY\_SIMPLEX,
4.3,
17)
# 在帧中绘制车牌号码
cv2.putText(frame,
license\_plate[df\_.iloc[row\_indx]['car\_id']]['license\_plate\_number'],
(int((car\_x2 + car\_x1 - text\_width) / 2), int(car\_y1 - H - 250 + (text\_height / 2))),
cv2.FONT\_HERSHEY\_SIMPLEX,
4.3,
(0, 0, 0),
17)
except:
pass # 忽略可能出现的错误
out.write(frame) # 将处理后的帧写入输出视频
frame = cv2.resize(frame, (1280, 720)) # 调整输出帧的尺寸
out.release() # 释放视频写入对象
cap.release() # 释放视频捕获对象
好的,我会将 util.py
的内容进行详细描述,并在代码中加入中文注释,便于理解。以下是修改后的内容:
add\_missing\_data.py
:处理缺失数据在构建车牌识别系统时,确保数据的完整性和质量至关重要。add\_missing\_data.py
模块专注于处理数据中的缺失部分,以保证数据集的连贯性和准确性。为此,采用插值填补的方法,以填补在数据收集中可能遗漏的车牌检测结果。
在实际应用中,数据常常不完整,尤其是在视频监控场景中,某些帧可能缺失了车牌的检测结果。为了保证后续分析和处理的准确性,要对这些缺失数据进行补充。插值填补的方法通过已有数据推测缺失值,维持数据的连续性。
具体实现中,首先从输入的CSV文件中读取车牌检测的数据,提取帧编号、车辆ID及其对应的边界框。利用 numpy
数组,来快速处理和过滤这些数据。针对每个车辆ID,筛选出该车辆在不同帧中的检测结果,检查连续帧之间是否存在缺失。当发现某一帧与上一帧之间存在间隔时,利用插值方法填补缺失的边界框。
import csv
import numpy as np
from scipy.interpolate import interp1d
首先提取输入数据中的帧编号、车辆ID和边界框。
def interpolate\_bounding\_boxes(data):
frame\_numbers = np.array([int(row['frame\_nmr']) for row in data])
car\_ids = np.array([int(float(row['car\_id'])) for row in data])
car\_bboxes = np.array([list(map(float, row['car\_bbox'][1:-1].split())) for row in data])
license\_plate\_bboxes = np.array([list(map(float, row['license\_plate\_bbox'][1:-1].split())) for row in data])
接着,对每个唯一的车辆ID进行处理,筛选该车辆在不同帧中的检测结果,并检测是否存在缺失。
interpolated\_data = []
unique\_car\_ids = np.unique(car\_ids)
for car\_id in unique\_car\_ids:
frame\_numbers\_ = [p['frame\_nmr'] for p in data if int(float(p['car\_id'])) == int(float(car\_id))]
car\_mask = car\_ids == car\_id
car\_frame\_numbers = frame\_numbers[car\_mask]
car\_bboxes\_interpolated = []
license\_plate\_bboxes\_interpolated = []
当检测到某一帧与上一帧之间存在间隔时,使用插值方法填补缺失的边界框。
for i in range(len(car\_bboxes[car\_mask])):
frame\_number = car\_frame\_numbers[i]
car\_bbox = car\_bboxes[car\_mask][i]
license\_plate\_bbox = license\_plate\_bboxes[car\_mask][i]
if i > 0:
prev\_frame\_number = car\_frame\_numbers[i - 1]
prev\_car\_bbox = car\_bboxes\_interpolated[-1]
prev\_license\_plate\_bbox = license\_plate\_bboxes\_interpolated[-1]
if frame\_number - prev\_frame\_number > 1:
frames\_gap = frame\_number - prev\_frame\_number
x = np.array([prev\_frame\_number, frame\_number])
x\_new = np.linspace(prev\_frame\_number, frame\_number, num=frames\_gap, endpoint=False)
interp\_func = interp1d(x, np.vstack((prev\_car\_bbox, car\_bbox)), axis=0, kind='linear')
interpolated\_car\_bboxes = interp\_func(x\_new)
interp\_func = interp1d(x, np.vstack((prev\_license\_plate\_bbox, license\_plate\_bbox)), axis=0, kind='linear')
interpolated\_license\_plate\_bboxes = interp\_func(x\_new)
car\_bboxes\_interpolated.extend(interpolated\_car\_bboxes[1:])
license\_plate\_bboxes\_interpolated.extend(interpolated\_license\_plate\_bboxes[1:])
最后,将插值后的数据构建成新的记录,并准备写入CSV文件。
car\_bboxes\_interpolated.append(car\_bbox)
license\_plate\_bboxes\_interpolated.append(license\_plate\_bbox)
for i in range(len(car\_bboxes\_interpolated)):
frame\_number = first\_frame\_number + i
row = {
'frame\_nmr': str(frame\_number),
'car\_id': str(car\_id),
'car\_bbox': ' '.join(map(str, car\_bboxes\_interpolated[i])),
'license\_plate\_bbox': ' '.join(map(str, license\_plate\_bboxes\_interpolated[i])),
'license\_plate\_bbox\_score': '0',
'license\_number': '0',
'license\_number\_score': '0'
}
if str(frame\_number) in frame\_numbers\_:
original\_row = [p for p in data if int(p['frame\_nmr']) == frame\_number and int(float(p['car\_id'])) == int(float(car\_id))][0]
row['license\_plate\_bbox\_score'] = original\_row.get('license\_plate\_bbox\_score', '0')
row['license\_number'] = original\_row.get('license\_number', '0')
row['license\_number\_score'] = original\_row.get('license\_number\_score', '0')
interpolated\_data.append(row)
return interpolated\_data
with open('test.csv', 'r') as file:
reader = csv.DictReader(file)
data = list(reader)
interpolated\_data = interpolate\_bounding\_boxes(data)
header = ['frame\_nmr', 'car\_id', 'car\_bbox', 'license\_plate\_bbox', 'license\_plate\_bbox\_score', 'license\_number', 'license\_number\_score']
with open('test\_interpolated.csv', 'w', newline='') as file:
writer = csv.DictWriter(file, fieldnames=header)
writer.writeheader()
writer.writerows(interpolated\_data)
这种处理方式确保即使在没有检测到车牌的帧中,也能提供合理的边界框数据,从而提升数据集的完整性和准确性。填补完成后,将补充的数据输出到一个新的CSV文件中,确保数据集的完整性。这样做的意义在于,系统能够在处理过程中自动适应和修复数据的缺失,减少人为干预,提升了自动化处理的效率。
util.py
:实用工具函数模块util.py
模块包含了一些辅助函数,这些函数为整个车牌识别系统的其他模块提供支持,包括模型的加载、车牌的读取和格式化等功能。
首先,使用 easyocr
库初始化了一个OCR读取器,它支持英文字符的识别。以下是代码:
import string
import easyocr
# 初始化OCR读取器,指定语言为英语,未启用GPU
reader = easyocr.Reader(['en'], gpu=False)
这里定义了两个字典,用于在车牌识别过程中进行字符与数字之间的转换。
# 字符到数字的映射
dict\_char\_to\_int = {
'O': '0',
'I': '1',
'J': '3',
'A': '4',
'G': '6',
'S': '5'
}
# 数字到字符的映射
dict\_int\_to\_char = {
'0': 'O',
'1': 'I',
'3': 'J',
'4': 'A',
'6': 'G',
'5': 'S'
}
write\_csv
函数用于将识别结果写入CSV文件。它接受两个参数:结果字典和输出文件路径。
def write\_csv(results, output\_path):
"""
将结果写入CSV文件。
参数:
results (dict): 包含结果的字典。
output\_path (str): 输出CSV文件的路径。
"""
with open(output\_path, 'w') as f:
# 写入CSV文件的表头
f.write('{},{},{},{},{},{},{}\n'.format(
'frame\_nmr', 'car\_id', 'car\_bbox',
'license\_plate\_bbox', 'license\_plate\_bbox\_score', 'license\_number',
'license\_number\_score'
))
# 遍历结果字典并写入每一行
for frame\_nmr in results.keys():
for car\_id in results[frame\_nmr].keys():
print(results[frame\_nmr][car\_id]) # 打印当前车辆的信息
if 'car' in results[frame\_nmr][car\_id].keys() and \
'license\_plate' in results[frame\_nmr][car\_id].keys() and \
'text' in results[frame\_nmr][car\_id]['license\_plate'].keys():
# 写入车牌识别结果到CSV文件
f.write('{},{},{},{},{},{},{}\n'.format(
frame\_nmr,
car\_id,
'[{} {} {} {}]'.format(
results[frame\_nmr][car\_id]['car']['bbox'][0],
results[frame\_nmr][car\_id]['car']['bbox'][1],
results[frame\_nmr][car\_id]['car']['bbox'][2],
results[frame\_nmr][car\_id]['car']['bbox'][3]
),
'[{} {} {} {}]'.format(
results[frame\_nmr][car\_id]['license\_plate']['bbox'][0],
results[frame\_nmr][car\_id]['license\_plate']['bbox'][1],
results[frame\_nmr][car\_id]['license\_plate']['bbox'][2],
results[frame\_nmr][car\_id]['license\_plate']['bbox'][3]
),
results[frame\_nmr][car\_id]['license\_plate']['bbox\_score'],
results[frame\_nmr][car\_id]['license\_plate']['text'],
results[frame\_nmr][car\_id]['license\_plate']['text\_score']
))
f.close() # 关闭文件
license\_complies\_format
函数用于检查车牌文本是否符合规定的格式。该格式要求车牌文本长度为7个字符,并且每个字符的位置都有特定的要求。
def license\_complies\_format(text):
"""
检查车牌文本是否符合规定格式。
参数:
text (str): 车牌文本。
返回:
bool: 如果符合格式返回True,否则返回False。
"""
if len(text) != 7:
return False
# 检查每个字符是否符合要求
if (text[0] in string.ascii\_uppercase or text[0] in dict\_int\_to\_char.keys()) and \
(text[1] in string.ascii\_uppercase or text[1] in dict\_int\_to\_char.keys()) and \
(text[2] in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] or text[2] in dict\_char\_to\_int.keys()) and \
(text[3] in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] or text[3] in dict\_char\_to\_int.keys()) and \
(text[4] in string.ascii\_uppercase or text[4] in dict\_int\_to\_char.keys()) and \
(text[5] in string.ascii\_uppercase or text[5] in dict\_int\_to\_char.keys()) and \
(text[6] in string.ascii\_uppercase or text[6] in dict\_int\_to\_char.keys()):
return True
else:
return False
format\_license
函数用于将车牌文本格式化,使用预定义的映射字典转换字符。
def format\_license(text):
"""
通过使用映射字典格式化车牌文本。
参数:
text (str): 车牌文本。
返回:
str: 格式化后的车牌文本。
"""
license\_plate\_ = ''
mapping = {
0: dict\_int\_to\_char,
1: dict\_int\_to\_char,
4: dict\_int\_to\_char,
5: dict\_int\_to\_char,
6: dict\_int\_to\_char,
2: dict\_char\_to\_int,
3: dict\_char\_to\_int
}
for j in [0, 1, 2, 3, 4, 5, 6]:
if text[j] in mapping[j].keys():
license\_plate\_ += mapping[j][text[j]] # 根据映射字典转换字符
else:
license\_plate\_ += text[j] # 如果字符不在字典中,直接添加
return license\_plate\_
read\_license\_plate
函数从给定的裁剪图像中读取车牌文本,并返回格式化后的文本及其置信度分数。
def read\_license\_plate(license\_plate\_crop):
"""
从给定的裁剪图像中读取车牌文本。
参数:
license\_plate\_crop (PIL.Image.Image): 裁剪后的车牌图像。
返回:
tuple: 包含格式化车牌文本和置信度分数的元组。
"""
detections = reader.readtext(license\_plate\_crop) # 使用OCR读取文本
for detection in detections:
bbox, text, score = detection # 解包检测结果
text = text.upper().replace(' ', '') # 将文本转换为大写并去除空格
if license\_complies\_format(text): # 检查格式是否符合
return format\_license(text), score # 返回格式化后的文本和分数
return None, None # 如果没有符合的文本,返回None
get\_car
函数根据车牌坐标和车辆的追踪ID,返回车辆的坐标和ID。
def get\_car(license\_plate, vehicle\_track\_ids):
"""
根据车牌坐标和车辆追踪ID获取车辆坐标和ID。
参数:
license\_plate (tuple): 包含车牌坐标 (x1, y1, x2, y2, score, class\_id) 的元组。
vehicle\_track\_ids (list): 车辆追踪ID及其对应坐标的列表。
返回:
tuple: 包含车辆坐标 (x1, y1, x2, y2) 和ID的元组。
"""
x1, y1, x2, y2, score, class\_id = license\_plate
foundIt = False
for j in range(len(vehicle\_track\_ids)):
xcar1, ycar1, xcar2, ycar2, car\_id = vehicle\_track\_ids[j]
# 检查车牌坐标是否在车辆坐标内
if x1 > xcar1 and y1 > ycar1 and x2 < xcar2 and y2 < ycar2:
car\_indx = j
foundIt = True
break
if foundIt:
return vehicle\_track\_ids[car\_indx] # 返回找到的车辆坐标和ID
return -1, -1, -1, -1, -1 # 如果没有找到,返回负值
这部分实现了车牌的读取、格式化及输出,确保车牌识别的准确性和结果的有效管理。
至此,代码结束,理解并实现车辆车牌识别代码对我这样的小白还是有些困难,还需多实践,多学习 yolov8 的相关项目及知识,加油加油!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。