news 2026/10/19 1:24:00

新能源汽车类型识别:VOC数据集解析与YOLO训练实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
新能源汽车类型识别:VOC数据集解析与YOLO训练实战

简介:面向新能源汽车品牌识别与目标检测任务,这份数据集以VOC格式组织标注信息,覆盖特斯拉、北汽新能源、宝马、比亚迪秦/宋/唐、奇瑞、福特、江淮等常见车型,适用于智慧交通、车辆检索、辅助驾驶等视觉场景。压缩包共2000个XML文件,容量254.13MB,每个XML文件记录单张车辆图像的目标类别与边界框坐标,可直接供YOLO、SSD、Faster R-CNN等主流检测网络读取使用。数据源于正常路面采集,涵盖不同光照、角度与背景,有助于增强模型对真实环境的适应能力。标注类别区分度较高,便于评估模型在相近车型上的判别能力。目前已有751人浏览学习,适合需要批量训练数据或进行车型细分类研究的开发者与研究人员,可作为替代或补充自有数据集的便捷选择。

1. 新能源汽车类型识别:5391张VOC标注数据集的落地价值

做新能源汽车类型识别这件事,真正卡人的往往不是模型结构,而是标注数据。停车场道闸、充电站枪位占用判断、园区车辆统计,这些场景里车型一多,模型就分不清特斯拉和比亚迪。这份资源提供了5391张正常采集的车辆图片,标注格式是熟悉的VOC XML,覆盖特斯拉、北汽新能源、宝马、比亚迪秦/宋/唐、奇瑞、易达、福特、江淮等主流新能源车型。数据是真实拍摄的,不是渲染图,标注框也基本贴着车身。适合两类人:一是想快速验证车型识别方案的开发者,二是需要干净训练集来跑YOLO等检测框架的算法工程师。直接拿去做迁移学习,比从零采数据省下至少一周时间。

2. 读透VOC XML与文件命名编码:5391张图的标注里藏了哪些关键字段

VOC格式看起来就是一堆XML,但里面的字段直接决定你后面转YOLO、转COCO时会不会翻车。先把XML结构拆开,再解析文件名里的编码段,最后用一个脚本做全量体检。

2.1 VOC XML里到底存了什么:从size到object的五个核心字段

VOC标注的XML结构不算复杂,但每个字段都有它的用处。以这份数据集的标注文件为例,根节点下一般会挂这几个块:folder、filename、path、source、size、object。其中size和object是训练时真正会读的字段,其余是溯源信息。

字段作用注意点
folder图片所在目录名可能为空,不要作为路径拼接依据
filename图片文件名和实际图片名称必须一致
path图片完整路径跨机器后大概率失效,忽略即可
size / width图片宽度(像素)转YOLO归一化时必须用到
size / height图片高度(像素)同上
object / name类别名,如“特斯拉”“比亚迪秦”类名是否统一直接影响训练
object / bndbox矩形框xmin、ymin、xmax、ymax坐标是绝对像素值,非归一化
object / difficult0或1,难例标记转格式时最好标记出来,不硬删

我一般拿到XML会先写一段解析代码,把所有字段打印出来,确认坐标是整数像素值而不是0到1的浮点数。这一步能避免后面很多麻烦,比如有的标注工具导出的是归一化坐标,直接拿去算目标框就会全部错位。

import xml.etree.ElementTree as ET xml_path = "00397916666666666-91_269-170-441_524-554-524-554_170-548_173-441_524-449-0_0_5_24_31_24_32_33-108-220_jpg.rf.a3075564be3226045683b6922103ff3a.xml" tree = ET.parse(xml_path) root = tree.getroot() filename = root.findtext("filename") print("filename:", filename) size = root.find("size") width = int(size.findtext("width")) height = int(size.findtext("height")) depth = int(size.findtext("depth")) print("image size:", width, height, depth) for obj in root.iter("object"): name = obj.findtext("name") box = obj.find("bndbox") xmin = float(box.findtext("xmin")) ymin = float(box.findtext("ymin")) xmax = float(box.findtext("xmax")) ymax = float(box.findtext("ymax")) print("object:", name, (xmin, ymin, xmax, ymax))

这段代码用xml.etree.ElementTree解析XML,先取filename确认对应的图片名,再取size下的宽高和通道数,最后遍历所有object节点取出类别名和边界框。注意bndbox里的坐标我用了float()转换,实际VOC标准里是整数,但后续转YOLO归一化时要先转浮点再除以宽高,所以这里直接读成浮点更省事。如果某个XML读出来width是0或者bndbox为空,说明这个标注文件有残缺,直接进清理列表。

2.2 文件名里的“编码夹层”:Roboflow导出命名规律

这份数据集的XML文件名很长,形如3051215277777777776-91_107-131-434_603-543-603-543_147-535_131-434_577-444-19_1_5_30_30_28_28_29-121-336_jpg.rf.哈希.xml。这种命名明显是从在线标注平台导出的,_jpg.rf.后缀就是Roboflow的标志。文件名里嵌入了图片原始尺寸、多边形坐标点、甚至关键点信息,但具体含义会因为导出设置不同而有差异,不建议完全依赖文件名做业务逻辑。

我一般会从文件名里提取两个东西:一是开头那段时间戳或序列号,用来判断哪些图片属于同一批采集;二是中间夹着的坐标段位,用来快速定位异常标注。

import re fname = "3051215277777777776-91_107-131-434_603-543-603-543_147-535_131-434_577-444-19_1_5_30_30_28_28_29-121-336_jpg.rf.a4d8b91759e0382a643e5b872cb10c5e.xml" # 提取开头序列号 m = re.match(r"^(\d+)-\d+_\d+", fname) if m: seq_id = m.group(1) print("sequence / 采集批次:", seq_id) # 提取可能的坐标段(以-分隔后的第2段到第5段) parts = fname.split("-") print("分段数:", len(parts)) for i, p in enumerate(parts[:6]): print(i, p)

这里的正则^(\d+)-\d+_\d+匹配的是文件名开头的连续数字段,比如3051215277777777776,它更像是采集时的帧序号或时间戳。用split("-")把整个文件名拆开可以看到中间夹着不少数字段,其中一部分是边界框坐标。需要注意,不同导出批次的分段规则可能不一样,所以解析脚本要写成容错模式,解析失败只告警不中断。

2.3 全量体检:一个脚本找出缺失图片、越界框、空标注

拿到5391个XML,不要急着转格式。先跑一遍体检脚本,把三类问题拎出来:图片文件缺失、边界框超出图片范围、空标注文件。这三个问题在训练时表现不同,但都会让你白白浪费时间。比如框越界会导致YOLO训练时出现NaN loss,空标注会导致某个batch没有正样本。

import os import glob import xml.etree.ElementTree as ET annot_dir = "Annotations" # XML目录 img_dir = "JPEGImages" # 图片目录 xml_list = glob.glob(os.path.join(annot_dir, "*.xml")) print("total xml:", len(xml_list)) missing_img = [] out_of_range = [] empty_obj = [] for xml_path in xml_list: tree = ET.parse(xml_path) root = tree.getroot() filename = root.findtext("filename") img_path = os.path.join(img_dir, filename) if not os.path.exists(img_path): missing_img.append(xml_path) continue size = root.find("size") w = int(size.findtext("width")) h = int(size.findtext("height")) obj_list = list(root.iter("object")) if len(obj_list) == 0: empty_obj.append(xml_path) continue for obj in obj_list: box = obj.find("bndbox") xmin = float(box.findtext("xmin")) ymin = float(box.findtext("ymin")) xmax = float(box.findtext("xmax")) ymax = float(box.findtext("ymax")) if xmin < 0 or ymin < 0 or xmax > w or ymax > h: out_of_range.append((xml_path, (xmin, ymin, xmax, ymax), (w, h))) print("missing img:", len(missing_img)) print("empty object:", len(empty_obj)) print("out of range:", len(out_of_range))

这段脚本做的事情很简单:遍历所有XML,先检查filename对应的图片是否存在,再检查每个object的框坐标是否在size范围内,最后统计没有object节点的空文件。逻辑上要注意size的宽高来自XML,而实际图片可能因为后期裁剪对不上,如果out_of_range的比例超过1%,说明标注和图片不是同一版本,建议找原始导出参数重新核对。

3. 数据拆分与格式转换:5391张采集图如何变成可训练样本

拿到体检通过的标注集,下一步就是拆分数据集、统计类别分布、转成目标检测框架需要的格式。这一步决定了训练出来的模型是“真认识车”还是“死记硬背”。

3.1 类别清单与样本分布:不要被标题里的12个类别冲昏头

标题里列出了特斯拉、北汽新能源、宝马、世界、比亚迪秦、比亚迪宋、比亚迪唐、奇瑞、易达、福特、江淮汽车、江淮等名称,但实际类别以XML里object/name字段为准。这里有个关键坑:像“江淮汽车”和“江淮”在标题里同时出现,很可能XML里也是两个不同的name值,训练时会被当成两类,把同类车型的样本硬生生拆开。

import glob import collections import xml.etree.ElementTree as ET all_names = collections.Counter() for xml_path in glob.glob("Annotations/*.xml"): tree = ET.parse(xml_path) root = tree.getroot() for obj in root.iter("object"): all_names[obj.findtext("name")] += 1 for name, cnt in all_names.most_common(): print(name, cnt)

统计出来的类别分布会告诉你哪些类别样本多、哪些少。正常来说会呈现头尾分布:头部类别可能上千张,尾部类别可能只有几十张。这份数据如果存在“江淮”和“江淮汽车”两个类名,最好的做法是在训练前做一个class_map映射表,把二者合并成一个类。合并时要注意,class_map要写死在转换脚本里,不要靠肉眼去改XML文件名,否则下次复现就无法对齐。

3.2 按采集批次拆分数据集:防止“背题”式验证

拆分训练集最忌讳直接random_split。因为这份数据的图片是按时间序列采集的,同一个车牌、同一路口的连续帧会出现在相邻的几十张图里。如果用随机拆分,模型在训练时看到的画面和验证集高度相似,验证指标虚高,一到真实停车场就现原形。

import glob import re from sklearn.model_selection import GroupShuffleSplit xml_list = glob.glob("Annotations/*.xml") # 从文件名开头提取采集批次ID,作为分组依据 groups = [] for p in xml_list: fname = os.path.basename(p) m = re.match(r"^(\d+)", fname) groups.append(m.group(1) if m else fname) gss = GroupShuffleSplit(n_splits=1, train_size=0.8, random_state=42) train_idx, val_idx = next(gss.split(xml_list, groups=groups)) train_xml = [xml_list[i] for i in train_idx] val_xml = [xml_list[i] for i in val_idx] print("train:", len(train_xml), "val:", len(val_xml))

GroupShuffleSplit和普通ShuffleSplit的区别在于,它会保证同一组数据要么全在训练集,要么全在验证集。这里的分组依据是文件名开头的数字段,你可以把它理解为“一次连续采集”的批次ID。如果同一批次里的车辆高度相似,这样拆分后验证集就是真正没见过的画面,指标才有参考价值。参数random_state=42是为了固定随机种子,保证多次运行结果一致。

3.3 从VOC转YOLO格式:归一化坐标与类别重映射一锅端

YOLO系列训练时用的是每行一个目标的txt格式,内容为class cx cy w h,全部归一化到0到1之间。转换脚本要做的就是把VOC里的bndbox绝对像素坐标换算成中心点加宽高的归一化值,同时把中文类别名映射成从0开始的整数ID。

import os import glob import xml.etree.ElementTree as ET # 类别名 -> 整数ID 的映射表,注意合并同名类别 class_map = { "特斯拉": 0, "北汽新能源": 1, "宝马": 2, "比亚迪秦": 3, "比亚迪宋": 4, "比亚迪唐": 5, "奇瑞": 6, "易达": 7, "福特": 8, "江淮汽车": 9, "江淮": 9, # 关键:把两个名字合并成同一类 } def voc_to_yolo(xml_path, out_dir): tree = ET.parse(xml_path) root = tree.getroot() filename = root.findtext("filename") size = root.find("size") w = int(size.findtext("width")) h = int(size.findtext("height")) base_name = os.path.splitext(filename)[0] out_txt = os.path.join(out_dir, base_name + ".txt") lines = [] for obj in root.iter("object"): name = obj.findtext("name") if name not in class_map: continue box = obj.find("bndbox") xmin = float(box.findtext("xmin")) ymin = float(box.findtext("ymin")) xmax = float(box.findtext("xmax")) ymax = float(box.findtext("ymax")) cx = (xmin + xmax) / 2.0 / w cy = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{class_map[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") with open(out_txt, "w") as f: f.write("\n".join(lines) + "\n") os.makedirs("labels", exist_ok=True) for xml_path in glob.glob("Annotations/*.xml"): voc_to_yolo(xml_path, "labels")

这段转换逻辑要注意两点。第一,归一化坐标换算公式里的中心点cx = (xmin + xmax) / 2 / w,宽高bw = (xmax - xmin) / w,顺序不要写反,很多人把cx cy w h写成xmin ymin xmax ymax,训练时loss直接爆炸。第二,class_map里的江淮汽车和江淮都映射到ID 9,这样两个类名在训练时就是同一类。如果你发现还有其他类名变体,比如“BYD秦”这种别称,也要在这里补齐映射,而不是放任不管。

4. 实战避坑:翻车现场与修正方案

数据集从拿到手到稳定训练,中间至少有几个坑等着你。我把踩过的坑按现象、原因、解决三条写清楚。

4.1 图片方向旋转90度导致标注框整体错位

现象:训练出来的模型对竖图完全失效,检测框往左下方偏,横图却正常。验证集mAP值不低,但一上真实视频就崩。

原因:部分照片是用手机或行车记录仪拍摄的,文件里带有EXIF方向信息。OpenCV用cv2.imread读图时不会自动处理EXIF旋转,图片实际显示方向和XML里标注的坐标对不上。

解决:先写一个预处理把图片按EXIF方向转正再训练。PIL.Image默认处理EXIF,可以先用PIL转一遍再存成无方向信息的图片,也可以在训练pipeline里读图时统一用ImageOps.exif_transpose处理。从这次之后我拿到任何数据集,第一步就是抽10张竖图人工核对方向。

4.2 火焰式loss:坐标越界导致的NaN损失

现象:训练前20轮loss正常,到第30轮突然出现nan,然后模型权重全部失效。检查代码发现是某些框的中心点坐标出现了大于1的值。

原因:体检脚本只过滤了xmax>w或ymax>h的框,但没有处理xmin<0的负坐标。负坐标在归一化后依然是负数,YOLO在计算目标损失时遇到负数坐标就会产生NaN。

解决:转换脚本里加一个clip操作,对所有坐标做min(0, max(w, value))的裁剪。更保险的做法是直接将越界比例超过5%的XML剔除,不是修正而是放弃这个标注,因为这种框说明原始标注本身就不可靠。

4.3 类名不统一:一个车型被拆成两个类

现象:训练完看混淆矩阵,发现有模型把同一辆车的前半段和后半段识别成两个不同类别,而且两个类的置信度都很高。

原因:标注时不同批次用了不同的命名习惯,典型表现就是标题里的“江淮汽车”和“江淮”,还有可能是“比亚迪秦”和“秦”同时存在,统计类别数时容易被忽略。

解决:统计脚本跑完先看all_names的完整列表,把所有近似类名归并成一个。关键是在转换阶段就完成映射,不要等到训练后再去改标签。合并后再统计一遍,确认类别数从“12个”降到你实际想要的类别数。

4.4 细分类别样本严重不平衡

现象:特斯拉、比亚迪秦这类头部类别样本几百上千张,易达、世界、福特这类尾部类别可能只有几十张。模型对头部类别识别很好,对尾部类别几乎全漏。

原因:真实采集场景就是长尾分布,热门车型出现的频率远高于冷门车型。这不算数据缺陷,但如果你要求所有类别都达到同等精度,就需要做处理。

解决:最简单的做法是给尾部类别开额外的数据增强,比如在训练配置里对低样本类别提高mosaic和copy-paste的概率。另一种做法是直接把样本少于50张的类别合并成“其他新能源车”这个大类,保证模型不会把一个冷门车型强行细分。

5. 训练验证与误检回灌:让模型在真实停车场撑住

数据集格式转换完成之后,训练环节同样有讲究。从类别mAP差异看数据短板,把关键参数定明白,再把误检样本回灌进去,模型才是真正为场景服务的。

5.1 从类别mAP差异反推数据质量问题

训练完先别急着看整体mAP,按类别拆开看。如果比亚迪唐这类样本多的车mAP到了90%,而福特这种尾部类别只有60%,说明不是模型能力问题,而是样本数量问题。这时候有两种选择:一是补充尾部类别数据,二是降低对该类别的精度预期。

常见做法是用混淆矩阵辅助判断。如果模型频繁把北汽新能源识别成江淮,去抽原始图片看,大概率会发现这两类车的车头造型确实接近,而且标注时框的紧致程度不一致。有的框贴着车身边缘,有的框则包含了车后方的背景,同一类别里标注风格不统一会直接拉低mAP。

5.2 关键参数怎么定:imgsz、anchor与epoch的配合

对于这种细粒度车型识别任务,默认的imgsz=640并不够用。车身Logo、车灯细节在640分辨率下像素太少,模型只能靠整体轮廓猜品牌。我一般会把输入分辨率提到1280,代价是显存和训练时间翻倍,但对车型识别的提升非常明显。

from ultralytics import YOLO model = YOLO("yolo11s.pt") # 换成本机可用的预训练权重即可 results = model.train( data="new_energy.yaml", epochs=150, imgsz=1280, batch=16, lr0=0.01, patience=20, workers=8, )

这里imgsz=1280意味着输入图片统一缩放到1280分辨率,小目标的特征保留更多。patience=20表示连续20个epoch验证集mAP不提升就早停,防止过拟合到训练集。lr0=0.01是用预训练权重时的常用初始学习率,如果你从随机初始化开始训练,这个值会偏高。

anchor参数在YOLO里一般会自动聚类,但自动聚类只看到标注框的统计形状,看不到车型细分的需求。如果你发现模型对细长车型的车侧视角识别差,可以手动检查一下自动聚类生成的anchor尺寸,把车身宽高比在1:1到1:2之间出现的频次拉出来看,再决定要不要调整anchor数量。

5.3 误检样本回灌:一套可持续的闭环流程

模型第一次跑完验证集,总会有一批False Positive样本,最常见的是把车后广告牌上的车身图案误检成车辆,或者把货车车头识别成某品牌新能源车。这类问题不能靠调阈值解决,要把误检图片正式收编为训练负样本。

步骤分三步走。第一步,把验证集里置信度在0.3以上的误检框截图导出。第二步,对这些图片做一次人工确认,标记哪些是真正的车辆但类别错了,哪些根本不是车。第三步,把“不是车”的图片作为背景帧加入训练集,配置里开background_cls或直接加一个“无目标”的训练样本。

import os import cv2 # 假设误检结果保存为 (image_path, x1, y1, x2, y2, pred_cls, conf) fp_samples = [ ("val/0001.jpg", 120, 300, 480, 720, "比亚迪秦", 0.82), ] out_dir = "hard_negatives" os.makedirs(out_dir, exist_ok=True) for idx, (img_path, x1, y1, x2, y2, cls, conf) in enumerate(fp_samples): img = cv2.imread(img_path) crop = img[y1:y2, x1:x2] cv2.imwrite(os.path.join(out_dir, f"fp_{idx}_{cls}_{conf:.2f}.jpg"), crop)

这段代码的作用是把误检区域裁剪成独立小图,放进hard_negatives目录。下一轮训练时,把这些负样本单独放一个类别,或者用背景类的方式让模型学习“这不是我要的东西”。回灌训练后,误检率通常会明显下降,但要注意别把正样本也误裁剪进去,否则模型会产生新的混淆。从那以后我每次训练完都会把Top误检可视化一遍,确认硬负样本真的有效,再进入下一轮迭代。

6. 进阶技巧:用边界框尺寸分布反推输入分辨率和anchor配置

模型训练的效果除了看loss曲线,还能通过标注框的统计特征提前预判。我以前拿到数据集就直接开训,结果模型对小尺寸车辆识别极差,后来才发现数据里大量车身只占图片的十分之一面积。用一段脚本统计所有标注框的宽高比和面积占比,可以在训练前就把问题拦下来。

import glob import xml.etree.ElementTree as ET import numpy as np ratios = [] areas = [] for xml_path in glob.glob("Annotations/*.xml"): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") w = int(size.findtext("width")) h = int(size.findtext("height")) img_area = w * h for obj in root.iter("object"): box = obj.find("bndbox") bw = float(box.findtext("xmax")) - float(box.findtext("xmin")) bh = float(box.findtext("ymax")) - float(box.findtext("ymin")) if bh == 0: continue ratios.append(bw / bh) areas.append(bw * bh / img_area) ratios = np.array(ratios) areas = np.array(areas) print("wh ratio percentiles:", np.percentile(ratios, [10, 25, 50, 75, 90])) print("area percentiles:", np.percentile(areas, [10, 25, 50, 75, 90]))

输出结果可以直接指导两个参数。第一,如果面积占比的90%分位数小于0.3,说明大部分车辆在图中都很小,imgsz要用1280甚至1536,而不是640。第二,如果宽高比的中位数明显大于2,说明有大量侧面拍摄的细长车身,anchor设计时要多给水平长条形预设框,同时训练集里要保证侧面视角的样本占比足够。

我自己的习惯是,每拿到一批新的标注数据,先跑这个脚本,再决定训练配置,而不是直接套默认参数。有一次我拿到一批框面积占比普遍不到0.2的数据,硬用640分辨率训了50轮,mAP卡在70%上不去,换成1280之后直接跳到88%。这个工具虽然简单,却是避免在无效配置上浪费算力的最直接手段。希望这篇拆解能帮你在做新能源车型识别时少走弯路,拿到VOC标注后十分钟内完成体检、拆分和格式转换,顺利进入模型迭代环节。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 1:23:14

嵌入式面试必备:UART、I2C、SPI串行通信协议深度解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/19 1:22:39

airllm:单卡低显存运行70B大模型的分层加载方案

1. 当显存成为瓶颈&#xff1a;airllm 要解决的真实问题跑过大模型推理的人大概都经历过这种绝望&#xff1a;手里只有一张消费级显卡&#xff0c;显存 8GB 或者 12GB&#xff0c;想加载一个 70B 参数的模型&#xff0c;光是权重文件就超过 140GB&#xff0c;连门都摸不到。传统…

作者头像 李华
网站建设 2026/10/19 1:22:01

Vue权限控制实战:路由、菜单与按钮级三层方案

简介&#xff1a;这份资源面向Vue前端开发者与后台管理系统搭建者&#xff0c;聚焦权限控制这一核心场景&#xff0c;讲解如何让不同角色用户仅访问被授权的功能。内容围绕两种限制方式展开&#xff1a;一是路由不可见&#xff0c;借助Vue Router的meta字段存储权限列表&#x…

作者头像 李华
网站建设 2026/10/19 1:21:53

UART、I2C、SPI三大串行协议区别与面试高频考点全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/19 1:21:11

.NET网上商城源码实战:从架构拆解到高并发秒杀扩展

简介&#xff1a;基于微软.NET框架的网上商城完整源码&#xff0c;是一份适合兼具ASP.NET Web开发基础的学习者与电商系统二次开发人员研读的工程实践。项目以VB.NET为后台语言&#xff0c;围绕商品展示、在线购物、订单处理等业务组织代码&#xff0c;并带有文章管理、折扣推荐…

作者头像 李华
网站建设 2026/10/19 1:18:28

Snap.Hutao 免费原神工具箱:5 步装完并登录

Snap.Hutao 免费原神工具箱&#xff1a;5 步装完并登录 【免费下载链接】Snap.Hutao 实用的开源多功能原神工具箱 &#x1f9f0; / Multifunctional Open-Source Genshin Impact Toolkit &#x1f9f0; 项目地址: https://gitcode.com/GitHub_Trending/sn/Snap.Hutao 这…

作者头像 李华