news 2026/10/28 18:24:25

YOLOv7与四种多目标跟踪算法离线对比:从数据转换到评测避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
YOLOv7与四种多目标跟踪算法离线对比:从数据转换到评测避坑

简介:面向计算机视觉研究与工程开发者的目标检测与多目标跟踪算法对比平台,以YOLOv7为检测基座,集成SORT、DeepSORT、ByteTrack、Bot-SORT四种跟踪器,并在VisDrone2019数据集上提供离线测试环境,适合需要评估算法在实际空视角视频中表现的开发者、学生与算法选型人员。压缩包共393个文件,约230MB,主体为134个Python脚本与204个编译后的pyc文件,覆盖检测、跟踪、评估与可视化流程;另含配置文件、模型权重pth、YOLO格式的yaml与txt、UI界面及说明文档,可支撑从环境部署到结果分析的多环节复现。目前已有107人学习浏览。另附演示视频、类别映射json及实验文档,便于快速理解不同跟踪算法在VisDrone场景下的鲁棒性差异,为算法调优、论文实验或项目选型提供可直接运行的对照基准。

1. 基于YOLOv7目标检测与多目标跟踪算法比较系统:它到底解决了什么问题

基于YOLOv7目标检测与多目标跟踪算法比较系统,听名字像个大工程,实际拆开看,最值得复用的是它的离线测试机制。它做的事情很具体:把VisDrone2019数据集里的俯拍视频逐帧喂给YOLOv7,检测框再交给SORT、DeepSORT、ByteTrack、Bot-SORT这四种跟踪算法,最后统一落盘成同一套结果格式,用MOTA、IDSW、FPS这些指标打分,给出一张能直接对比的表格。

反直觉的一点是:这类系统的瓶颈基本不在算法本身,而在那些看起来不起眼的“后勤工作”上。同一个跟踪器,置信度阈值设0.1和设0.5,效果差距比换一种跟踪算法还大;类别编号映射错一行,mAP腰斩了你还找不到原因。离线测试的真正价值,是把所有可变参数先锁死在配置里,只让跟踪策略变化,这样才能回答“我该选谁”。

这套方案适合做交通流量统计、无人机巡检、智慧城市项目的人,也适合读完目标检测论文想补一组跟踪对比实验的研究生。下面按我自己做这套系统的顺序来拆:数据转换、模型部署、跟踪器封装、离线跑批、指标计算,最后讲几个血泪踩坑记录。

2. 先把数据和检测链路准备齐:VisDrone2019转换、YOLOv7部署、四种跟踪器的差异

2.1 VisDrone2019目标检测数据集:把官方标注转成YOLOv7能吃的txt

VisDrone2019这个目标检测数据集最大的特点是“俯拍”,无人机高度几十米到几百米,画面里行人只有几十个像素,车辆、自行车、三轮车密集排列,互相遮挡非常严重。官方一共给了十个常见类别:pedestrian、people、bicycle、car、van、truck、tricycle、awning-tricycle、bus、motor,另外还有一个ignore标记用来排除低质量目标。

真正折腾人的是标注格式。VisDrone官方不是PASCAL VOC那种XML,也不是直接给YOLO的txt,而是每帧一个逗号分隔文本,每行十个字段:

frame_index, target_id, bbox_left, bbox_top, bbox_width, bbox_height, score, object_category, truncation, occlusion

frame_index是帧号,target_id是真值轨迹ID,score在真值里一般叫1,碰到0或负值通常代表被忽略的低质量框。多目标跟踪评测需要保留target_id,但谱面检测格式用不上,而且官方标注里还有不少噪声行,直接训练会让模型去学一些根本不存在的“目标”。

我一般会写一个独立的转换脚本,把VisDrone的官方标注统一转成YOLO训练用的归一化txt,同时过滤掉ignore类:

import os # 从 VisDrone 官方类别编号(1~10) 映射到 YOLO 训练编号(0~9) # 常见做法:pedestrian=0, people=1, bicycle=2, car=3, # van=4, truck=5, tricycle=6, awning-tricycle=7, # bus=8, motor=9 CATEGORY_MAP = {1:0, 2:1, 3:2, 4:3, 5:4, 6:5, 7:6, 8:7, 9:8, 10:9} IGNORE_CATEGORY = {11, 12} # 官方标注里的 others 和 ignore def visdrone_to_yolo(txt_path, img_w, img_h, out_path): rows = [] with open(txt_path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue parts = line.split(",") if len(parts) < 8: continue x, y, w, h = int(parts[2]), int(parts[3]), int(parts[4]), int(parts[5]) score = float(parts[6]) cat = int(parts[7]) if cat in IGNORE_CATEGORY or score <= 0: continue if cat not in CATEGORY_MAP: continue # 像素坐标 -> 归一化中心坐标,注意去掉负值和不合理尺寸 if w <= 0 or h <= 0: continue cx = (x + w / 2) / img_w cy = (y + h / 2) / img_h nw = w / img_w nh = h / img_h rows.append(f"{CATEGORY_MAP[cat]} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}") with open(out_path, "w", encoding="utf-8") as f: f.write("\n".join(rows))

这里有个容易翻车的地方:VisDrone官方把car排在第4类,很多资料里却习惯把car放在YOLO的0号位,如果训练和推理用的映射表不一致,模型输出的class_id全部错位。实测里最常见的现象是:检测框位置很准,但car变成了pedestrian,跟踪评测的ID Switch直接崩掉。解决的办法很笨但有效:全项目只维护一份CATEGORY_MAP,训练、推理、结果可视化都从同一个模块导入,不要在每个脚本里手写字典。

转换完后按train/val目录整理,每张jpg配一个同名txt,VisDrone官方还提供了按帧划分的标注,做离线测试平台可以直接取val里的图像序列来跑跟踪评测。

2.2 YOLOv7部署与推理:离线比较首先要稳定检测结果

YOLOv7的原理核心是扩展ELAN结构加重参数化卷积,论文里报的推理速度很有吸引力,但部署时不能只看论文数字。这里踩得最多的坑是:训练完忘了切eval()模式,重参数化结构的BN层还在训练状态,推理速度慢一倍以上,结果还飘。

做离线测试平台,我不建议直接跑detect.py去输出图片,因为下一步要把检测结果喂进跟踪器,最好自己控制前处理、推理、后处理这一段流程。常见做法是封装一个统一的推理函数:

def yolo_infer(model, frame_bgr, conf=0.25, iou=0.45, img_size=640): # 输入 BGR 帧,输出 [x1, y1, x2, y2, score, class_id] 的 numpy 数组 # letterbox 保持长宽比,避免缩放后小目标被压变形 img, ratio, (pad_l, pad_t) = letterbox(frame_bgr, new_shape=img_size) img_t = torch.from_numpy(img[:, :, ::-1].copy()).permute(2, 0, 1).float() / 255.0 img_t = img_t.unsqueeze(0).to(device) with torch.no_grad(): preds = model(img_t) # NMS 用的是 iou 参数,后面跟踪器用的也是 iou,但含义完全不同 dets = non_max_suppression(preds, conf_thres=conf, iou_thres=iou)[0] if dets is None: return np.empty((0, 6)) # 把坐标从 letterbox 图像映射回原图 dets = scale_boxes(img.shape[:2], dets, frame_bgr.shape[:2]).cpu().numpy() return dets

这段代码里conf和iou是两个最容易混淆的参数。conf是检测置信度阈值,决定哪些检测框能进入跟踪器;iou是NMS的IOU阈值,只控制同一个目标附近重叠框的合并。很多人在调跟踪效果时去改iou,其实应该先回去改conf。

VisDrone这类俯拍场景还有一个现实问题:视频原分辨率可能是1920x1080,但如果推理输入缩到640,行人目标可能只剩下十几个像素,检测框飘得很厉害。跟踪算法对此完全无能为力,它们只是关联检测框,不负责“创造”检测框。所以离线测试平台的第一步不是调跟踪器,而是把检测输入分辨率固定住,我一般先在1280上跑一版,如果显存不够就退回960,但四组跟踪实验必须用同一个分辨率,否则FPS和MOTA都没有可比性。

2.3 SORT、DeepSORT、ByteTrack、Bot-SORT四种跟踪算法到底在比什么

四种跟踪器本质都是“检测结果后处理”,输入同一批检测框,输出带稳定ID的轨迹,但关联策略差异很大。离线测试平台要做的就是把这种差异数字化。

跟踪器关联信号遮挡处理典型代价适合场景
SORT卡尔曼滤波运动预测 + IOU匹配弱极低车辆稀疏、运动简单的场景
DeepSORT运动马氏距离 + ReID外观特征,级联匹配中中等行人多、遮挡频繁的场景
ByteTrack高分框和低分框两阶段BYTE关联较强较低目标密度高、漏检多的场景
Bot-SORT运动 + 外观 + 相机运动补偿(CMC)最强高无人机、车载等相机自身运动剧烈的场景

SORT最朴素,只用卡尔曼滤波预测下一帧位置,然后跟检测框做IOU匹配,没匹配上的轨迹直接删。优点是快,缺点是只要遮挡超过几帧,ID基本就丢了,恢复不回来。DeepSORT在SORT基础上加了一个ReID分支,提取目标外观特征做二次匹配,所以对行人这种外观稳定的目标效果明显更好,但代价是要额外跑一个特征提取网络,而且俯拍场景里行人太小,外观特征质量很不稳定。

ByteTrack的设计思路反直觉:它不丢弃低分检测框,先把高分框匹配完,再用低分框去补匹配那些没有配上的轨迹。这在无人机俯拍里特别有用,因为小目标经常被检测器压到置信度0.1附近,丢掉就再也找不回来。但低分框数量一大,匹配矩阵变大,速度就开始下降。

Bot-SORT可以理解成ByteTrack的加强版,额外加了全局相机运动补偿和更强的ReID。无人机飞行时画面整体在动,目标在图像坐标里的位置变化不只是自身运动,还叠加了相机的平移旋转,如果不补偿这部分,卡尔曼预测会整体偏移。Bot-SORT在这个场景里ID Switch通常最低,但计算量也最大,离线测试跑完,对比FPS差距会很明显。

顺带提一句“deepsort改进”,网上一搜全是换backbone、换ReID网络的方案,但我在这个平台里踩过的坑恰恰相反:换了轻量ReID权重后,在外观特征不可靠的俯拍场景里,余弦距离反而成了噪声,DeepSORT退化得跟SORT差不多。改进前先确认场景里目标外观到底可不可分。

2.4 统一封装四个跟踪器:参数名对不齐是比不了的

四种跟踪器各有各的接口。SORT的函数叫update(dets),DeepSORT的接口里要传frame去提取外观特征,ByteTrack需要frame更新缓冲区,Bot-SORT也要frame做相机运动估计。这还不算完,同一个“轨迹存活多少帧没匹配就删掉”的概念,在SORT里叫max_age,在ByteTrack里叫track_buffer,在DeepSORT里叫max_time_lost。

做比较系统必须先做一层适配器,把所有跟踪器包成同一个接口:

class BaseTracker: def update(self, dets, frame, frame_id): # dets: N x 6 的 numpy 数组 # 返回 list[Track],Track 包含 track_id, x1, y1, x2, y2, score, class_id raise NotImplementedError class SORTTracker(BaseTracker): def __init__(self, cfg): self.max_age = cfg.get("max_age", 30) class DeepSORTTracker(BaseTracker): def __init__(self, cfg): self.max_time_lost = cfg.get("max_time_lost", 30) class ByteTrackTracker(BaseTracker): def __init__(self, cfg): self.track_buffer = cfg.get("track_buffer", 30) class BOTSORTTracker(BaseTracker): def __init__(self, cfg): self.track_buffer = cfg.get("track_buffer", 30) TRACKER_REGISTRY = { "sort": SORTTracker, "deepsort": DeepSORTTracker, "bytetrack": ByteTrackTracker, "botsort": BOTSORTTracker, } def make_tracker(name, cfg): return TRACKER_REGISTRY[name](cfg)

统一封装后,每个跟踪器内部的参数依然独立,但对外暴露的是同一套BaseTracker。后面跑评测时,只需要在配置里改name和对应参数,主循环不用动。这层抽象看起来简单,实际上决定了整个离线测试平台的可持续性——没有它,每加一个跟踪器就是一次爆改主流程。

3. 搭建离线测试平台:输入链路、结果落盘与评测指标怎么取舍

3.1 输入链路设计:读视频、逐帧索引、不依赖摄像头的真实时间戳

离线测试平台的核心是“可复现”,所以输入不能走摄像头。摄像头帧率、曝光、自动增益都是不稳定因素,同一个算法今天跑和明天跑结果不一样,对比就失去了意义。常见做法是直接喂离线视频文件,或者喂VisDrone官方拆好的图像序列目录。

主循环并不复杂:

cap = cv2.VideoCapture(video_path) frame_id = 0 while True: ok, frame = cap.read() if not ok: break dets = yolo_infer(model, frame, conf=cfg["conf"], iou=cfg["iou"]) tracks = tracker.update(dets, frame, frame_id) # 每个检测框输出一行 for t in tracks: writer.write(frame_id, t) frame_id += 1

这里特别注意:进度只用frame_id这个整数计数,不要去依赖cap.get(cv2.CAP_PROP_POS_FRAMES)。有些视频解码器在seek和跳帧时不保证逐帧递增,你以为是第100帧,实际读出来的是第99帧或第101帧,后面跟真值对齐时全部错位。

平台跑完后,我会把视频路径、分辨率、模型权重路径、conf、iou、tracker配置一起写进一个run_config.json。这个文件不做任何计算,但出报告时必须随结果一起归档。多目标跟踪算法的对比最怕“我忘了上次那组结果是用什么参数跑出来的”,没有配置文件,所有数字都失去上下文。

3.2 结果落盘:MOTChallenge格式和配置文件一起保存

跟踪结果要能被同一个评测脚本统一处理,最好用MOTChallenge标准格式,每行十个字段:

frame, id, bb_left, bb_top, bb_width, bb_height, conf, x, y, z

后面三个字段对2D跟踪来说写-1占位就好。落到代码里:

def write_tracks(writer, frame_id, tracks): # MOTChallenge 格式:frame, id, left, top, w, h, conf, -1, -1, -1 for t in tracks: w = t.x2 - t.x1 h = t.y2 - t.y1 writer.write( f"{frame_id},{t.track_id},{t.x1:.2f},{t.y1:.2f}," f"{w:.2f},{h:.2f},{t.score:.4f},-1,-1,-1\n" )

有两个细节值得说。第一,conf这一列建议写检测置信度,而不是跟踪器内部维护的某个轨迹评分,因为跟踪器各自的轨迹分数口径不同,没法横向比。第二,文件名里务必带上跟踪器名和conf值,比如botsort_conf05.txt、bytetrack_conf03.txt。我早期全部叫output.txt,跑完四组直接覆盖,那个下午是我最不想回忆的翻车现场。

3.3 评测指标:MOTA、IDSW、FPS、HOTA,哪些直接决定选型

指标计算推荐直接复用py-motmetrics这类成熟库,不建议自己从头写匈牙利匹配,99%会写错边界情况。用起来大概是:

import motmetrics as mm acc = mm.MOTAccumulator(auto_id=True) for frame_id in range(total_frames): gt = gt_by_frame[frame_id] # [id, x, y, w, h] hy = track_by_frame[frame_id] # [id, x, y, w, h] dists = mm.distances.iou_matrix(gt[:, 1:], hy[:, 1:], max_iou=0.5) acc.update(gt[:, 0].tolist(), hy[:, 0].tolist(), dists) mh = mm.metrics.create() summary = mh.compute(acc, metrics=["num_frames", "mota", "idsw"], names=["result"]) print(summary)

真正需要理解的是每个指标背后的业务含义:

指标计算意义在无人机场景里的价值
MOTA综合漏检、误检、ID切换的准确率判断整体可用性,但容易被高检测率掩盖关联问题
IDSW目标身份切换次数直接反映跟踪器会不会把人认错,是选跟踪器的核心指标
FPS每秒处理帧数离线测试的FPS不等于部署FPS,只能用来做相对对比
HOTA检测与关联的均衡分数论文里更常用,能同时看出检测和跟踪两方面谁拖后腿

MOTA高但IDSW高的情况很常见:检测器很强,框一直在,但跟踪器频繁给同一个目标换ID,统计人流或车流时会把一辆车数成三辆。这时候回头看SORT和Bot-SORT的差异就特别明显,SORT帧率高但IDSW高,Bot-SORT反过来。

还有一点,离线测试平台跑出的FPS只能做同一硬件下的相对比较,不能直接当实时部署的承诺值。跟踪器本身的FPS差距在无人机高分辨率输入下会被放大,我见过有人只报跟踪算法FPS不报检测FPS,这种横向对比不成立。

4. 避坑清单:跑这套YOLOv7+多目标跟踪比较系统最容易翻车的5个地方

4.1 类别映射前后不一致,mAP看起来漂亮但跟踪结果全乱

现象:检测框画出来位置很准,标签却张冠李戴,car被标成pedestrian,bus变成truck,跟踪ID切换率爆炸。

原因:VisDrone官方类别编号是1到10,YOLO训练编号是0到9,两个体系之间没有唯一正确答案。有人训练时从官方编号减1,有人用我之前映射表,还有人把car放第0类。映射表不一致时,模型输出的类别语义就是错的。

解决:确认全项目只存在一份CATEGORY_MAP,训练脚本、推理脚本、评测脚本全部import同一个模块。宁可多写几行代码,也不要在多个脚本里重复粘贴字典。

4.2 换了轻量ReID权重,DeepSORT退化成了带卡尔曼滤波的SORT

现象:把DeepSORT默认的ReID特征提取网络换成一个很轻的MobileNet后,行人目标频繁换ID,MOTA掉到比SORT还低。

原因:轻量ReID网络在这个俯拍场景里学到的外观特征不可区分。无人机视角下行人只有十几个像素,颜色纹理都糊成一团,外观距离反而变成随机噪声,级联匹配里余弦距离越算越乱。

解决:在VisDrone这类密集小目标场景里,先把DeepSORT的外观距离权重调低,比如0.3以下,让运动匹配占主导;如果还是不行,干脆用纯运动关联的SORT或ByteTrack。想改进DeepSORT不应先换网络,应先在你的数据上评估ReID特征的可分性。

4.3 ByteTrack的低分框把离线平台拖成慢动作

现象:开启ByteTrack后FPS从30掉到5,接近不可用。

原因:ByteTrack设计时保留低分检测框做二次匹配,无人机场景里小目标漏检多,低分框数量极多,匹配矩阵被撑大,重复匹配开销跟着翻倍。

解决:分两步。先把track_buffer从默认的几十帧调小,缩减轨迹存活长度;再把进入ByteTrack的低分置信度阈值从0.1提到0.25,这个参数的调整对MOTA影响很大,我一般会做一次0.1到0.4的扫描,选IDSW和MOTA的平衡点。

4.4 FPS对比自相矛盾:时间戳和帧号没有分开统计

现象:同一段视频同一套参数,第一次跑FPS是35,第二次跑成22,反复跑几次都稳定不下来。

原因:如果按视频自带的PTS或系统墙钟时间来计算FPS,解码器跳帧、后台进程调度都会干扰结果。离线测试平台不需要和真实时间对齐,只关心算法处理一帧花了多久。

解决:用time.perf_counter()统计纯推理加跟踪的耗时,frame_id只管帧数,两者分开。最后报告里的FPS取三次运行的中位数,不要取最快一次。

4.5 无人机小目标多,YOLOv7默认输入尺寸是第一个瓶颈

现象:检测mAP看着不低,但行人类和motor类的Recall很低,跟踪指标被牵连。

原因:VisDrone俯拍图里很多目标实际不到20x20像素,缩到640输入后只剩几个像素,卷积下采样几次特征就没了。YOLOv7检测不出来,后面再好的跟踪器也接不到框。

解决:把推理输入提高到1280,显存不够就分块半精度;或者先跑一段视频,统计检测框的面积分布,如果大量框面积小于400像素,优先处理检测环节,而不是去调跟踪参数。还有一个笨办法很有效:做一个只有检测、没有跟踪的基线版,把跟踪结果全部退化成无ID的裸框,先看单帧检测质量。

5. 让对比结果更可信:用配置矩阵批量扫描,再验证确定性

比较系统的价值不在“能跑”,在“跑出来的结论敢拿出去”。我的习惯是先把所有跟踪器参数集中到一个yaml文件,用脚本批量扫描conf,然后跑一整轮实验。

import csv import itertools import yaml trackers = ["sort", "deepsort", "bytetrack", "botsort"] conf_values = [0.3, 0.5] # 四组算法必须在同样的检测置信度下对比 for name, conf in itertools.product(trackers, conf_values): cfg = yaml.safe_load(open(f"configs/{name}.yaml", "r")) cfg["conf"] = conf runner.run(video_path, tracker_name=name, cfg=cfg, out_dir=f"results/{name}_conf{conf}")

代码逻辑很简单,核心是把conf作为公共变量单独拎出来。如果不做这层扫描,某个跟踪器可能占了个便宜:它的实现里默认conf很低,检测漏检少,MOTA虚高。只有让四个跟踪器共享同一套检测输入,比较才算公平。

跑完批量实验后,我会再做一次确定性验证:同一段视频、同一份配置连续跑两遍,比对两个结果文件里的轨迹内容。跟踪匹配本身是确定性算法,输入不变输出必须一致;如果发现不一致,就检查代码里有没有随机初始化没有固定seed,或者哪个库内部用了随机采样。

我自己的收尾习惯是:在结果文件旁边补一个report_header.txt,写清四个关键条件——检测输入尺寸、conf、iou、是否使用TTA。没有这四行字,MOTA和IDSW的对比就是无根之木。这行信息我会直接拼进输出文件名,比如visdrone_val_1280_conf050.txt,这样三个月后再回来翻文件,也不需要猜参数。

这个方向值不值得做,我的答案是值得,但重点不是把四种跟踪器凑齐,而是把评测环境控制住。跟踪器选型的结论一旦被参数污染,比不跑还要糟糕。希望这篇笔记能让你少走几段我走过的弯路,帮到你。

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

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

基于PSoC CLB-MACO的LLC软启动谐振电流限制方案

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

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

DeepSeek本地部署实战:Ollama、DeepSeek-R1与显存排错

简介&#xff1a;《详解AI本地部署&#xff1a;DeepSeek模型的安装与应用拓展助力AI新发展》是一份面向AI开发者、初学者及具备基础硬件知识的从业者的PDF文档&#xff0c;聚焦DeepSeek模型本地部署与初步应用拓展&#xff0c;兼顾数据隐私、定制配置和不同算力条件下的落地需求…

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

爱仕特与汉磊共研8英寸SiC MOS,碳化硅产业链迎来关键卡位

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

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

在 .NET 4.0 与 VS2010 的 WinForm 中集成 CefSharp Chromium

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

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

C++实现增量Delaunay三角剖分:数据结构、Lawson翻转与避坑指南

简介&#xff1a;这是一份面向C开发者的Delaunay三角剖分算法学习资源&#xff0c;项目采用增量式构建思路&#xff0c;使用石溪大学顾贤峰教授开发的半边数据结构维护网格拓扑&#xff0c;目标是最大化剖分所得三角形的最小角&#xff0c;适合计算机图形学、几何建模与网格生成…

作者头像 李华