From 112a9215aa5e78d83949b74470fde9e6b8ae5133 Mon Sep 17 00:00:00 2001 From: sloanqin Date: Wed, 2 Sep 2026 10:29:24 +0800 Subject: [PATCH 01/14] fix: rename pfld_prepross.py to pfld_preprocess.py and update readme download links --- samples/built-in/detection/PFLD/README.md | 2 +- .../PFLD/script/{pfld_prepross.py => pfld_preprocess.py} | 0 samples/built-in/recognition/FaceNet/README.md | 8 ++++++++ 3 files changed, 9 insertions(+), 1 deletion(-) rename samples/built-in/detection/PFLD/script/{pfld_prepross.py => pfld_preprocess.py} (100%) diff --git a/samples/built-in/detection/PFLD/README.md b/samples/built-in/detection/PFLD/README.md index bf2ed44..69486e3 100644 --- a/samples/built-in/detection/PFLD/README.md +++ b/samples/built-in/detection/PFLD/README.md @@ -171,7 +171,7 @@ mkdir -p model ## 安装依赖 ```bash -# 建议使用python3.7 or 3.8 +# 建议使用python3.7 pip3 install -r requirements.txt ``` diff --git a/samples/built-in/detection/PFLD/script/pfld_prepross.py b/samples/built-in/detection/PFLD/script/pfld_preprocess.py similarity index 100% rename from samples/built-in/detection/PFLD/script/pfld_prepross.py rename to samples/built-in/detection/PFLD/script/pfld_preprocess.py diff --git a/samples/built-in/recognition/FaceNet/README.md b/samples/built-in/recognition/FaceNet/README.md index c13f950..77e6128 100644 --- a/samples/built-in/recognition/FaceNet/README.md +++ b/samples/built-in/recognition/FaceNet/README.md @@ -183,6 +183,14 @@ pip3 install -r requirements.txt LFW(Labeled Faces in the Wild)是一个经典的无约束人脸识别基准数据集,包含 5749 个不同身份的 13233 张真实场景人脸图像,涵盖姿态、光照、表情等自然变异,适用于评估人脸识别算法在非受控环境下的泛化性能与匹配精度。 点击 [LFW](http://vis-www.cs.umass.edu/lfw/lfw.tgz) 下载数据集,点击 [pairs.txt](http://vis-www.cs.umass.edu/lfw/pairs.txt) 下载pairs.txt进行精度评估。 + + 若以上地址无法访问,可通过以下备用方式下载: + + ```bash + wget -O lfw.tgz https://ndownloader.figshare.com/files/5976018 + + wget -O pairs.txt https://ndownloader.figshare.com/files/5976006 + ``` 在`modelzoo/datasets`源码目录下创建`LFW`文件夹,文件结构如下: ``` -- Gitee From 0084a4dbe007616b5bd4a9faf8bf3669251ec878 Mon Sep 17 00:00:00 2001 From: sloanqin Date: Wed, 2 Sep 2026 10:47:51 +0800 Subject: [PATCH 02/14] fix: correct yolo11s-pose model filename in README (yolov11s-pose -> yolo11s-pose) --- samples/samples_GPL/built-in/yolo11s-pose/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/samples_GPL/built-in/yolo11s-pose/README.md b/samples/samples_GPL/built-in/yolo11s-pose/README.md index 4723dfc..d171914 100644 --- a/samples/samples_GPL/built-in/yolo11s-pose/README.md +++ b/samples/samples_GPL/built-in/yolo11s-pose/README.md @@ -158,7 +158,7 @@ mkdir -p model 4. 切换到可执行文件main所在的目录,运行可执行文件。 ``` - ./main --acl ../src/acl.json --model ../model/yolov11s-pose.om --input ../data/file_list_1.json + ./main --acl ../src/acl.json --model ../model/yolo11s-pose.om --input ../data/file_list_1.json ``` 备注:若需要在数据集上进行精度评估,需要参考[安装依赖](#section183221994410)、[准备数据集](#section183221994411)和[精度&性能评估](#section741711594518)章节。 @@ -359,7 +359,7 @@ pip3 install -r requirements.txt 4. 验证batch_size的om模型的性能,参考命令如下: ```bash - ./main --acl ../src/acl.json --model ../model/yolov11s-pose.om --input ../data/file_list_1.json + ./main --acl ../src/acl.json --model ../model/yolo11s-pose.om --input ../data/file_list_1.json ``` 参数说明:(此模式下,输入路径为一张图片) @@ -405,7 +405,7 @@ pip3 install -r requirements.txt --preprocess_bin_dir "../data/preprocess/bin" \ --infer_bin_dir "../out/result_pc/bin" \ --file_list_path "../data/preprocess/file_list.txt" \ - --onnx_model_path "../model/yolov11s-pose.onnx" \ + --onnx_model_path "../model/yolo11s-pose.onnx" \ --input_size 640 640 ``` -- Gitee From d0cfbe1fae3ebc8be0fece647869025db6271c12 Mon Sep 17 00:00:00 2001 From: sloanqin Date: Wed, 2 Sep 2026 10:51:11 +0800 Subject: [PATCH 03/14] fix: correct yolo11s-seg model filename in README (yolo11s_seg -> yolo11s-seg) --- samples/samples_GPL/built-in/yolo11s-seg/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/samples_GPL/built-in/yolo11s-seg/README.md b/samples/samples_GPL/built-in/yolo11s-seg/README.md index 5ffab31..e0ca93a 100644 --- a/samples/samples_GPL/built-in/yolo11s-seg/README.md +++ b/samples/samples_GPL/built-in/yolo11s-seg/README.md @@ -164,7 +164,7 @@ mkdir -p model 4. 切换到可执行文件main所在的目录,运行可执行文件。 ``` - ./main --acl ../src/acl.json --model ../model/yolo11s_seg.om --input ../data/file_list_1.json + ./main --acl ../src/acl.json --model ../model/yolo11s-seg.om --input ../data/file_list_1.json ``` 备注:若需要在数据集上进行精度评估,需要参考[安装依赖](#section183221994410)、[准备数据集](#section183221994411)和[精度&性能评估](#section741711594518)章节。 @@ -308,7 +308,7 @@ pip3 install -r requirements.txt ```bash # 注意:实测模型推理速度较快,但是后处理时掩码解码部分较慢(为了评估精度,置信度候选框设置为0.001,保留了几乎所有候选框,导致端到端看起来比较慢); # 在工业界使用时,置信度一般设置较高(如0.25),会过滤大量的无效框,后处理速度会快很多。另外后处理掩码矩阵计算部分,还可以使用NPU加速。 - ./main --acl ../src/acl.json --model ../model/yolo11s_seg.om --input ../data/file_list.json + ./main --acl ../src/acl.json --model ../model/yolo11s-seg.om --input ../data/file_list.json ``` 推理结果会保存在result目录下 @@ -399,7 +399,7 @@ pip3 install -r requirements.txt ```bash cd out # 注意:性能测试不含前后处理逻辑,只有模型推理部分的耗时和FPS。因为工业界使用时前后处理可以通过CPU异步并行来加速或者会包含其他业务逻辑。 - ./main --acl ../src/acl.json --model ../model/yolo11s_seg.om --input ../data/file_list_1.json + ./main --acl ../src/acl.json --model ../model/yolo11s-seg.om --input ../data/file_list_1.json cd .. ``` -- Gitee From a9061bd322291b22aa5b8f3ca77ace1f30506c5f Mon Sep 17 00:00:00 2001 From: sloanqin Date: Wed, 2 Sep 2026 11:06:09 +0800 Subject: [PATCH 04/14] fix: use default testdata image for yolov9s quick start to avoid dataset dependency --- samples/samples_GPL/built-in/yolov9s/data/file_list_1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/samples_GPL/built-in/yolov9s/data/file_list_1.json b/samples/samples_GPL/built-in/yolov9s/data/file_list_1.json index 2c906e4..2cdd4a4 100644 --- a/samples/samples_GPL/built-in/yolov9s/data/file_list_1.json +++ b/samples/samples_GPL/built-in/yolov9s/data/file_list_1.json @@ -2,7 +2,7 @@ "loop": 300, "fileList": [ [ - "../datasets/coco/images/val2017/000000000139.jpg" + "../../../../../datasets/testdata/2.jpg" ] ] } -- Gitee From e6d1fc245d9b2dd014f13fd7e531fc21cc63039d Mon Sep 17 00:00:00 2001 From: sloanqin Date: Wed, 2 Sep 2026 11:12:10 +0800 Subject: [PATCH 05/14] fix: correct yolov8s-seg model filename in README (yolov8s_seg -> yolov8s-seg) --- samples/samples_GPL/built-in/yolov8s-seg/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/samples_GPL/built-in/yolov8s-seg/README.md b/samples/samples_GPL/built-in/yolov8s-seg/README.md index 519ee1d..0c1512e 100644 --- a/samples/samples_GPL/built-in/yolov8s-seg/README.md +++ b/samples/samples_GPL/built-in/yolov8s-seg/README.md @@ -164,7 +164,7 @@ mkdir -p model 4. 切换到可执行文件main所在的目录,运行可执行文件。 ``` - ./main --acl ../src/acl.json --model ../model/yolov8s_seg.om --input ../data/file_list_1.json + ./main --acl ../src/acl.json --model ../model/yolov8s-seg.om --input ../data/file_list_1.json ``` 备注:若需要在数据集上进行精度评估,需要参考[安装依赖](#section183221994410)、[准备数据集](#section183221994411)和[精度&性能评估](#section741711594518)章节。 -- Gitee From 3b43377e1b4cd4edbc5732642a108df0c8664976 Mon Sep 17 00:00:00 2001 From: sloanqin Date: Wed, 2 Sep 2026 11:16:19 +0800 Subject: [PATCH 06/14] fix: correct yolov8s-seg accuracy eval param name in README (nms_threshold -> iou_threshold) --- samples/samples_GPL/built-in/yolov8s-seg/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/samples_GPL/built-in/yolov8s-seg/README.md b/samples/samples_GPL/built-in/yolov8s-seg/README.md index 0c1512e..b5f38b5 100644 --- a/samples/samples_GPL/built-in/yolov8s-seg/README.md +++ b/samples/samples_GPL/built-in/yolov8s-seg/README.md @@ -469,7 +469,7 @@ pip3 install -r requirements.txt --bin_dir "../out/result_pc/bin" \ --img_dir "../../../../../datasets/coco2017/val2017" \ --output_dir "../out/result_pc/json" \ - --nms_threshold 0.7 \ + --iou_threshold 0.7 \ --conf_threshold 0.001 \ --target_size 640 640 cd .. -- Gitee From bb3c4a250dddc35416c27294943c9f200d2861b5 Mon Sep 17 00:00:00 2001 From: sloanqin Date: Wed, 2 Sep 2026 11:18:57 +0800 Subject: [PATCH 07/14] fix: correct yolo11s-seg accuracy eval param name in README (nms_threshold -> iou_threshold) --- samples/samples_GPL/built-in/yolo11s-seg/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/samples_GPL/built-in/yolo11s-seg/README.md b/samples/samples_GPL/built-in/yolo11s-seg/README.md index e0ca93a..3860ead 100644 --- a/samples/samples_GPL/built-in/yolo11s-seg/README.md +++ b/samples/samples_GPL/built-in/yolo11s-seg/README.md @@ -468,7 +468,7 @@ pip3 install -r requirements.txt --bin_dir "../out/result_pc/bin" \ --img_dir "../../../../../datasets/coco2017/val2017" \ --output_dir "../out/result_pc/json" \ - --nms_threshold 0.7 \ + --iou_threshold 0.7 \ --conf_threshold 0.001 \ --target_size 640 640 ``` -- Gitee From e803075bf161f6f47588ad34fd8a49c62ae6ecef Mon Sep 17 00:00:00 2001 From: sloanqin Date: Wed, 2 Sep 2026 14:54:02 +0800 Subject: [PATCH 08/14] perf: reduce memory usage of yolov8s-seg COCO evaluation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 精度比对结果文件较大时进程被 OOM kill(4GB/8GB 虚拟机均出现过), 见 docs/problem.txt 易用性问题(4)。 - 流式合并:逐文件读取单图 JSON 结果并边读边写 merged_coco_results.json, 不再构造全量 detection_results 列表 - 分块评估:按图片分块 loadRes + evaluate,每块结束即释放 coco_dt 与 ious; 各块 evalImgs 按 (category, area, image) 顺序回填全局索引后, 只调用一次 accumulate()/summarize(),保证结果与全量评估一致 - 建立 file_name stem -> image_id 映射,将非数字文件名的查找由 O(N^2) 降为 O(1) COCO val2017 全量 5000 张图实测峰值内存由约 5.8GB 降至约 1GB, 评估指标输出保持不变。 --- .../script/yolov8s_seg_evaluate.py | 419 +++++++++++++++--- 1 file changed, 358 insertions(+), 61 deletions(-) diff --git a/samples/samples_GPL/built-in/yolov8s-seg/script/yolov8s_seg_evaluate.py b/samples/samples_GPL/built-in/yolov8s-seg/script/yolov8s_seg_evaluate.py index 3784905..eae4c89 100644 --- a/samples/samples_GPL/built-in/yolov8s-seg/script/yolov8s_seg_evaluate.py +++ b/samples/samples_GPL/built-in/yolov8s-seg/script/yolov8s_seg_evaluate.py @@ -13,104 +13,401 @@ # limitations under the License. import os +import gc +import copy import json import argparse +from contextlib import redirect_stdout +from io import StringIO + from pycocotools.coco import COCO from pycocotools.cocoeval import COCOeval from tqdm import tqdm +# 仅作为内部实现参数,不改变脚本原有命令行接口。 +# 500 张/块在 COCO val2017 这类规模上通常能显著降低峰值内存。 +EVAL_CHUNK_SIZE = 500 + + +def _build_image_name_map(coco_gt, all_img_ids): + """一次性建立文件名 stem -> image_id 映射,避免逐文件全量扫描 COCO images。""" + stem_to_img_id = {} + for info in coco_gt.loadImgs(all_img_ids): + stem = os.path.splitext(info["file_name"])[0] + # 保持原脚本 matched[0] 的语义:若 stem 重复,保留首次出现的 image_id。 + stem_to_img_id.setdefault(stem, info["id"]) + return stem_to_img_id + + +def _resolve_image_id(img_name, all_img_id_set, stem_to_img_id): + """保持原脚本的 image_id 查找语义,同时将非数字文件名查找从 O(N) 降为 O(1)。""" + if img_name.isdigit(): + numeric_id = int(img_name) + if numeric_id in all_img_id_set: + return numeric_id + return stem_to_img_id.get(img_name) + + +def _stream_merge_results(coco_gt, json_files, args): + """ + 流式生成 merged_coco_results.json。 + + 不再构造全量 detection_results,仅在内存中保留单个 JSON 文件的结果。 + 同时返回后续分块评估所需的小型 manifest: + [(image_id, [json_path1, json_path2, ...]), ...] + 仅包含“解析成功且至少有 1 条检测结果”的图片,与原脚本 + set(res["image_id"] for res in detection_results) 的语义一致。 + + 同一个 image_id 若意外对应多个结果 JSON,会被归到同一个逻辑图片中, + 确保分块时不会把同一图片的 detections 拆到不同 chunk。 + """ + all_img_ids = coco_gt.getImgIds() + all_img_id_set = set(all_img_ids) + stem_to_img_id = _build_image_name_map(coco_gt, all_img_ids) + + temp_json = os.path.join(args.result_dir, "../merged_coco_results.json") + eval_sources = {} + has_detection = False + first_result = True + + with open(temp_json, "w") as merged_file: + merged_file.write("[") + + for json_file in tqdm(json_files, desc="加载结果文件"): + img_name = os.path.splitext(json_file)[0] + img_id = _resolve_image_id( + img_name, + all_img_id_set, + stem_to_img_id, + ) + + if img_id is None: + print(f"警告: 未找到图片 {img_name} 对应的ID,跳过") + continue + + json_path = os.path.join(args.result_dir, json_file) + file_has_detection = False + try: + with open(json_path, "r") as f: + results = json.load(f) + + # 保持单图 JSON 中结果顺序不变,并逐条写入 merged 文件, + # 避免全量 detection_results 常驻内存。 + for res in results: + res["image_id"] = img_id + if not first_result: + merged_file.write(", ") + json.dump(res, merged_file) + first_result = False + file_has_detection = True + + except Exception as e: + print(f"警告: 处理文件 {json_file} 失败: {e},跳过") + + # 原脚本即使单个文件处理中途异常,异常前已 append 的结果仍会保留。 + # 这里同样只要成功处理过至少一条结果,就纳入评估 manifest。 + if file_has_detection: + eval_sources.setdefault(img_id, []).append(json_path) + has_detection = True + + merged_file.write("]") + + print(f"合并的评估文件已保存至: {temp_json}") + eval_manifest = list(eval_sources.items()) + return temp_json, eval_manifest, has_detection + + +def _prepare_global_evaluator(coco_gt, img_ids, iou_type): + """ + 创建用于最终 accumulate/summarize 的 COCOeval 容器。 + + evaluate() 的 evalImgs 展平顺序为: + category -> area range -> image + 因此预分配全局 evalImgs,分块 evaluate 后按全局索引回填。 + """ + coco_eval = COCOeval(coco_gt, None, iouType=iou_type) + + # 模拟 COCOeval.evaluate() 开头对参数做的规范化。 + coco_eval.params.imgIds = sorted(set(img_ids)) + if coco_eval.params.useCats: + coco_eval.params.catIds = sorted(set(coco_eval.params.catIds)) + coco_eval.params.maxDets = sorted(coco_eval.params.maxDets) + + coco_eval._paramsEval = copy.deepcopy(coco_eval.params) + + cat_ids = ( + coco_eval.params.catIds + if coco_eval.params.useCats + else [-1] + ) + num_categories = len(cat_ids) + num_areas = len(coco_eval.params.areaRng) + num_images = len(coco_eval.params.imgIds) + + coco_eval.evalImgs = [None] * ( + num_categories * num_areas * num_images + ) + + index_info = { + "img_pos": { + img_id: idx + for idx, img_id in enumerate(coco_eval.params.imgIds) + }, + "cat_pos": { + cat_id: idx + for idx, cat_id in enumerate(cat_ids) + }, + "area_pos": { + tuple(area_rng): idx + for idx, area_rng in enumerate(coco_eval.params.areaRng) + }, + "num_images": num_images, + "num_areas": num_areas, + } + return coco_eval, index_info + + +def _merge_chunk_eval_imgs(global_eval, global_index, chunk_eval): + """ + 将单个 chunk 的 evalImgs 回填到完整 COCOeval 的全局 K×A×I 顺序。 + + 不能简单 global_eval.evalImgs.extend(chunk_eval.evalImgs),否则多个 + chunk 拼接后会破坏 COCOeval.accumulate() 期待的展平顺序。 + """ + params = chunk_eval._paramsEval + chunk_img_ids = params.imgIds + chunk_cat_ids = params.catIds if params.useCats else [-1] + + local_num_images = len(chunk_img_ids) + local_num_areas = len(params.areaRng) + + global_num_images = global_index["num_images"] + global_num_areas = global_index["num_areas"] + + for local_cat_idx, cat_id in enumerate(chunk_cat_ids): + global_cat_idx = global_index["cat_pos"][cat_id] + + for local_area_idx, area_rng in enumerate(params.areaRng): + global_area_idx = global_index["area_pos"][tuple(area_rng)] + + local_base = ( + local_cat_idx * local_num_areas + local_area_idx + ) * local_num_images + global_base = ( + global_cat_idx * global_num_areas + global_area_idx + ) * global_num_images + + for local_img_idx, img_id in enumerate(chunk_img_ids): + global_img_idx = global_index["img_pos"][img_id] + global_eval.evalImgs[ + global_base + global_img_idx + ] = chunk_eval.evalImgs[ + local_base + local_img_idx + ] + + +def _load_chunk_results(manifest_chunk): + """重新读取一个 chunk 的单图结果,并补齐正确 image_id。""" + chunk_results = [] + chunk_img_ids = [] + + for img_id, json_paths in manifest_chunk: + image_has_results = False + + # 同一 image_id 的多个 JSON 必须在同一个 chunk 内合并, + # 且保持它们在第一遍 os.listdir 遍历中的相对顺序。 + for json_path in json_paths: + try: + with open(json_path, "r") as f: + results = json.load(f) + + if not results: + continue + + for res in results: + res["image_id"] = img_id + chunk_results.append(res) + image_has_results = True + + except Exception as e: + print( + f"警告: 评估时重新读取文件 " + f"{os.path.basename(json_path)} 失败: {e},跳过" + ) + + if image_has_results: + chunk_img_ids.append(img_id) + + return chunk_results, chunk_img_ids + + +def _evaluate_in_chunks(coco_gt, eval_manifest): + """ + 分块执行 bbox + segm 的 evaluate(),最后分别只 accumulate() 一次。 + + 每块只保留: + - 当前 chunk 的 detection Python 对象 + - 当前 chunk 的 coco_dt / IoU + - 全局 evalImgs + + 从而避免 loadRes(完整 merged JSON) 带来的数 GB Python 对象常驻内存。 + """ + evaluated_img_ids = sorted({ + img_id + for img_id, _ in eval_manifest + }) + + bbox_eval, bbox_index = _prepare_global_evaluator( + coco_gt, + evaluated_img_ids, + "bbox", + ) + segm_eval, segm_index = _prepare_global_evaluator( + coco_gt, + evaluated_img_ids, + "segm", + ) + + total_chunks = ( + len(eval_manifest) + EVAL_CHUNK_SIZE - 1 + ) // EVAL_CHUNK_SIZE + + for start in tqdm( + range(0, len(eval_manifest), EVAL_CHUNK_SIZE), + total=total_chunks, + desc="分块COCO评估", + ): + manifest_chunk = eval_manifest[ + start:start + EVAL_CHUNK_SIZE + ] + chunk_results, chunk_img_ids = _load_chunk_results( + manifest_chunk + ) + + if not chunk_results: + continue + + # loadRes 支持直接传 Python list,不需要为每个 chunk 再落临时 JSON。 + coco_dt = coco_gt.loadRes(chunk_results) + + # bbox evaluate + chunk_bbox = COCOeval( + coco_gt, + coco_dt, + iouType="bbox", + ) + chunk_bbox.params.imgIds = chunk_img_ids + + # COCOeval.evaluate() 会打印固定日志;分块执行会重复很多次。 + # 这里静默 chunk 内部日志,最终 accumulate/summarize 输出保持清晰。 + with redirect_stdout(StringIO()): + chunk_bbox.evaluate() + + _merge_chunk_eval_imgs( + bbox_eval, + bbox_index, + chunk_bbox, + ) + + # 尽早释放 bbox chunk 的 IoU 和 evaluator。 + chunk_bbox.ious = {} + chunk_bbox.evalImgs = [] + del chunk_bbox + gc.collect() + + # segm evaluate + chunk_segm = COCOeval( + coco_gt, + coco_dt, + iouType="segm", + ) + chunk_segm.params.imgIds = chunk_img_ids + + with redirect_stdout(StringIO()): + chunk_segm.evaluate() + + _merge_chunk_eval_imgs( + segm_eval, + segm_index, + chunk_segm, + ) + + chunk_segm.ious = {} + chunk_segm.evalImgs = [] + del chunk_segm + + # chunk_results 与 coco_dt 引用同一批 annotation dict, + # 当前块完成后一起释放。 + del coco_dt + del chunk_results + gc.collect() + + return bbox_eval, segm_eval + + def run_evaluation(args): """读取单张图片的JSON结果并执行COCO评估""" # 加载COCO标注 coco_gt = COCO(args.gt_annotations) - all_img_ids = coco_gt.getImgIds() - - # 创建结果列表 - detection_results = [] # 获取所有JSON结果文件 - json_files = [f for f in os.listdir(args.result_dir) if f.endswith('.json')] + json_files = [ + f + for f in os.listdir(args.result_dir) + if f.endswith(".json") + ] if not json_files: print(f"错误: 在 {args.result_dir} 中未找到任何JSON文件") return - # 处理每个JSON结果 - total = len(json_files) - for idx, json_file in enumerate(tqdm(json_files, desc="加载结果文件")): - # 提取图片名称 - img_name = os.path.splitext(json_file)[0] - - # 查找图片ID(确保与标注文件中的ID匹配) - img_ids = coco_gt.getImgIds(imgIds=[int(img_name)] if img_name.isdigit() else []) - if not img_ids: - # 尝试通过文件名查找 - img_infos = coco_gt.loadImgs(all_img_ids) - matched = [info for info in img_infos if os.path.splitext(info['file_name'])[0] == img_name] - if matched: - img_id = matched[0]['id'] - else: - print(f"警告: 未找到图片 {img_name} 对应的ID,跳过") - continue - else: - img_id = img_ids[0] - - # 读取JSON文件内容 - json_path = os.path.join(args.result_dir, json_file) - try: - with open(json_path, 'r') as f: - results = json.load(f) - - # 确保结果中的image_id正确 - for res in results: - res["image_id"] = img_id - detection_results.append(res) - - except Exception as e: - print(f"警告: 处理文件 {json_file} 失败: {e},跳过") - continue - - # 保存合并的JSON结果(用于COCO评估) - temp_json = os.path.join(args.result_dir, "../merged_coco_results.json") - with open(temp_json, "w") as f: - json.dump(detection_results, f) - print(f"合并的评估文件已保存至: {temp_json}") + # 第一遍: + # 1. 流式生成 merged_coco_results.json; + # 2. 记录真正存在检测结果的图片,用于后续分块评估。 + _, eval_manifest, has_detection = _stream_merge_results( + coco_gt, + json_files, + args, + ) - # 执行COCO评估 - if not detection_results: + if not has_detection: print("警告: 没有有效的检测结果,无法进行评估") return - coco_dt = coco_gt.loadRes(temp_json) + # 第二遍: + # 按图片分块读取结果,每块分别执行 bbox / segm evaluate, + # 避免完整 merged JSON 经 loadRes/json.load 后常驻数 GB 内存。 + coco_eval_bbox, coco_eval_segm = _evaluate_in_chunks( + coco_gt, + eval_manifest, + ) # -------------------------- 1. 评估框检测(bbox)-------------------------- print("=" * 50) print("框检测(bbox)评估结果:") - coco_eval_bbox = COCOeval(coco_gt, coco_dt, iouType='bbox') - # 确保评估使用与推理相同的图片集 - coco_eval_bbox.params.imgIds = list(set(res["image_id"] for res in detection_results)) - coco_eval_bbox.evaluate() coco_eval_bbox.accumulate() coco_eval_bbox.summarize() # -------------------------- 2. 评估分割(segm)-------------------------- print("\n" + "=" * 50) print("分割(segm)评估结果:") - coco_eval_segm = COCOeval(coco_gt, coco_dt, iouType='segm') - coco_eval_segm.params.imgIds = list(set(res["image_id"] for res in detection_results)) - coco_eval_segm.evaluate() coco_eval_segm.accumulate() coco_eval_segm.summarize() def main(): # 解析命令行参数 - parser = argparse.ArgumentParser(description='根据单张图片的JSON结果进行COCO评估') - parser.add_argument('--result_dir', default='../out/result/json', - help='存放单张图片JSON结果的目录') - parser.add_argument('--gt_annotations', - default="../../../../../datasets/coco2017/annotations/instances_val2017.json", - help='COCO的ground truth标注文件路径') + parser = argparse.ArgumentParser(description="根据单张图片的JSON结果进行COCO评估") + parser.add_argument( + "--result_dir", + default="../out/result/json", + help="存放单张图片JSON结果的目录", + ) + parser.add_argument( + "--gt_annotations", + default="../../../../../datasets/coco2017/annotations/instances_val2017.json", + help="COCO的ground truth标注文件路径", + ) args = parser.parse_args() -- Gitee From 530f2a71bbf0e6f1af37aba17895108ecec37cff Mon Sep 17 00:00:00 2001 From: sloanqin Date: Wed, 2 Sep 2026 14:59:11 +0800 Subject: [PATCH 09/14] perf: reduce memory usage of yolo11s-seg COCO evaluation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 与 yolov8s-seg 同一问题:精度比对结果文件较大时进程被 OOM kill (4GB/8GB 虚拟机均出现过),见 docs/problem.txt 易用性问题(4)。 - 流式合并:逐文件读取单图 JSON 结果并边读边写 merged_coco_results.json, 不再构造全量 detection_results 列表 - 分块评估:按图片分块 loadRes + evaluate,每块结束即释放 coco_dt 与 ious; 各块 evalImgs 按 (category, area, image) 顺序回填全局索引后, 只调用一次 accumulate()/summarize(),保证结果与全量评估一致 - 建立 file_name stem -> image_id 映射,将非数字文件名的查找由 O(N^2) 降为 O(1) 改动内容与 yolov8s_seg_evaluate.py 一致(两者为同一套通用评估逻辑)。 --- .../script/yolo11s_seg_evaluate.py | 419 +++++++++++++++--- 1 file changed, 358 insertions(+), 61 deletions(-) diff --git a/samples/samples_GPL/built-in/yolo11s-seg/script/yolo11s_seg_evaluate.py b/samples/samples_GPL/built-in/yolo11s-seg/script/yolo11s_seg_evaluate.py index 3784905..eae4c89 100644 --- a/samples/samples_GPL/built-in/yolo11s-seg/script/yolo11s_seg_evaluate.py +++ b/samples/samples_GPL/built-in/yolo11s-seg/script/yolo11s_seg_evaluate.py @@ -13,104 +13,401 @@ # limitations under the License. import os +import gc +import copy import json import argparse +from contextlib import redirect_stdout +from io import StringIO + from pycocotools.coco import COCO from pycocotools.cocoeval import COCOeval from tqdm import tqdm +# 仅作为内部实现参数,不改变脚本原有命令行接口。 +# 500 张/块在 COCO val2017 这类规模上通常能显著降低峰值内存。 +EVAL_CHUNK_SIZE = 500 + + +def _build_image_name_map(coco_gt, all_img_ids): + """一次性建立文件名 stem -> image_id 映射,避免逐文件全量扫描 COCO images。""" + stem_to_img_id = {} + for info in coco_gt.loadImgs(all_img_ids): + stem = os.path.splitext(info["file_name"])[0] + # 保持原脚本 matched[0] 的语义:若 stem 重复,保留首次出现的 image_id。 + stem_to_img_id.setdefault(stem, info["id"]) + return stem_to_img_id + + +def _resolve_image_id(img_name, all_img_id_set, stem_to_img_id): + """保持原脚本的 image_id 查找语义,同时将非数字文件名查找从 O(N) 降为 O(1)。""" + if img_name.isdigit(): + numeric_id = int(img_name) + if numeric_id in all_img_id_set: + return numeric_id + return stem_to_img_id.get(img_name) + + +def _stream_merge_results(coco_gt, json_files, args): + """ + 流式生成 merged_coco_results.json。 + + 不再构造全量 detection_results,仅在内存中保留单个 JSON 文件的结果。 + 同时返回后续分块评估所需的小型 manifest: + [(image_id, [json_path1, json_path2, ...]), ...] + 仅包含“解析成功且至少有 1 条检测结果”的图片,与原脚本 + set(res["image_id"] for res in detection_results) 的语义一致。 + + 同一个 image_id 若意外对应多个结果 JSON,会被归到同一个逻辑图片中, + 确保分块时不会把同一图片的 detections 拆到不同 chunk。 + """ + all_img_ids = coco_gt.getImgIds() + all_img_id_set = set(all_img_ids) + stem_to_img_id = _build_image_name_map(coco_gt, all_img_ids) + + temp_json = os.path.join(args.result_dir, "../merged_coco_results.json") + eval_sources = {} + has_detection = False + first_result = True + + with open(temp_json, "w") as merged_file: + merged_file.write("[") + + for json_file in tqdm(json_files, desc="加载结果文件"): + img_name = os.path.splitext(json_file)[0] + img_id = _resolve_image_id( + img_name, + all_img_id_set, + stem_to_img_id, + ) + + if img_id is None: + print(f"警告: 未找到图片 {img_name} 对应的ID,跳过") + continue + + json_path = os.path.join(args.result_dir, json_file) + file_has_detection = False + try: + with open(json_path, "r") as f: + results = json.load(f) + + # 保持单图 JSON 中结果顺序不变,并逐条写入 merged 文件, + # 避免全量 detection_results 常驻内存。 + for res in results: + res["image_id"] = img_id + if not first_result: + merged_file.write(", ") + json.dump(res, merged_file) + first_result = False + file_has_detection = True + + except Exception as e: + print(f"警告: 处理文件 {json_file} 失败: {e},跳过") + + # 原脚本即使单个文件处理中途异常,异常前已 append 的结果仍会保留。 + # 这里同样只要成功处理过至少一条结果,就纳入评估 manifest。 + if file_has_detection: + eval_sources.setdefault(img_id, []).append(json_path) + has_detection = True + + merged_file.write("]") + + print(f"合并的评估文件已保存至: {temp_json}") + eval_manifest = list(eval_sources.items()) + return temp_json, eval_manifest, has_detection + + +def _prepare_global_evaluator(coco_gt, img_ids, iou_type): + """ + 创建用于最终 accumulate/summarize 的 COCOeval 容器。 + + evaluate() 的 evalImgs 展平顺序为: + category -> area range -> image + 因此预分配全局 evalImgs,分块 evaluate 后按全局索引回填。 + """ + coco_eval = COCOeval(coco_gt, None, iouType=iou_type) + + # 模拟 COCOeval.evaluate() 开头对参数做的规范化。 + coco_eval.params.imgIds = sorted(set(img_ids)) + if coco_eval.params.useCats: + coco_eval.params.catIds = sorted(set(coco_eval.params.catIds)) + coco_eval.params.maxDets = sorted(coco_eval.params.maxDets) + + coco_eval._paramsEval = copy.deepcopy(coco_eval.params) + + cat_ids = ( + coco_eval.params.catIds + if coco_eval.params.useCats + else [-1] + ) + num_categories = len(cat_ids) + num_areas = len(coco_eval.params.areaRng) + num_images = len(coco_eval.params.imgIds) + + coco_eval.evalImgs = [None] * ( + num_categories * num_areas * num_images + ) + + index_info = { + "img_pos": { + img_id: idx + for idx, img_id in enumerate(coco_eval.params.imgIds) + }, + "cat_pos": { + cat_id: idx + for idx, cat_id in enumerate(cat_ids) + }, + "area_pos": { + tuple(area_rng): idx + for idx, area_rng in enumerate(coco_eval.params.areaRng) + }, + "num_images": num_images, + "num_areas": num_areas, + } + return coco_eval, index_info + + +def _merge_chunk_eval_imgs(global_eval, global_index, chunk_eval): + """ + 将单个 chunk 的 evalImgs 回填到完整 COCOeval 的全局 K×A×I 顺序。 + + 不能简单 global_eval.evalImgs.extend(chunk_eval.evalImgs),否则多个 + chunk 拼接后会破坏 COCOeval.accumulate() 期待的展平顺序。 + """ + params = chunk_eval._paramsEval + chunk_img_ids = params.imgIds + chunk_cat_ids = params.catIds if params.useCats else [-1] + + local_num_images = len(chunk_img_ids) + local_num_areas = len(params.areaRng) + + global_num_images = global_index["num_images"] + global_num_areas = global_index["num_areas"] + + for local_cat_idx, cat_id in enumerate(chunk_cat_ids): + global_cat_idx = global_index["cat_pos"][cat_id] + + for local_area_idx, area_rng in enumerate(params.areaRng): + global_area_idx = global_index["area_pos"][tuple(area_rng)] + + local_base = ( + local_cat_idx * local_num_areas + local_area_idx + ) * local_num_images + global_base = ( + global_cat_idx * global_num_areas + global_area_idx + ) * global_num_images + + for local_img_idx, img_id in enumerate(chunk_img_ids): + global_img_idx = global_index["img_pos"][img_id] + global_eval.evalImgs[ + global_base + global_img_idx + ] = chunk_eval.evalImgs[ + local_base + local_img_idx + ] + + +def _load_chunk_results(manifest_chunk): + """重新读取一个 chunk 的单图结果,并补齐正确 image_id。""" + chunk_results = [] + chunk_img_ids = [] + + for img_id, json_paths in manifest_chunk: + image_has_results = False + + # 同一 image_id 的多个 JSON 必须在同一个 chunk 内合并, + # 且保持它们在第一遍 os.listdir 遍历中的相对顺序。 + for json_path in json_paths: + try: + with open(json_path, "r") as f: + results = json.load(f) + + if not results: + continue + + for res in results: + res["image_id"] = img_id + chunk_results.append(res) + image_has_results = True + + except Exception as e: + print( + f"警告: 评估时重新读取文件 " + f"{os.path.basename(json_path)} 失败: {e},跳过" + ) + + if image_has_results: + chunk_img_ids.append(img_id) + + return chunk_results, chunk_img_ids + + +def _evaluate_in_chunks(coco_gt, eval_manifest): + """ + 分块执行 bbox + segm 的 evaluate(),最后分别只 accumulate() 一次。 + + 每块只保留: + - 当前 chunk 的 detection Python 对象 + - 当前 chunk 的 coco_dt / IoU + - 全局 evalImgs + + 从而避免 loadRes(完整 merged JSON) 带来的数 GB Python 对象常驻内存。 + """ + evaluated_img_ids = sorted({ + img_id + for img_id, _ in eval_manifest + }) + + bbox_eval, bbox_index = _prepare_global_evaluator( + coco_gt, + evaluated_img_ids, + "bbox", + ) + segm_eval, segm_index = _prepare_global_evaluator( + coco_gt, + evaluated_img_ids, + "segm", + ) + + total_chunks = ( + len(eval_manifest) + EVAL_CHUNK_SIZE - 1 + ) // EVAL_CHUNK_SIZE + + for start in tqdm( + range(0, len(eval_manifest), EVAL_CHUNK_SIZE), + total=total_chunks, + desc="分块COCO评估", + ): + manifest_chunk = eval_manifest[ + start:start + EVAL_CHUNK_SIZE + ] + chunk_results, chunk_img_ids = _load_chunk_results( + manifest_chunk + ) + + if not chunk_results: + continue + + # loadRes 支持直接传 Python list,不需要为每个 chunk 再落临时 JSON。 + coco_dt = coco_gt.loadRes(chunk_results) + + # bbox evaluate + chunk_bbox = COCOeval( + coco_gt, + coco_dt, + iouType="bbox", + ) + chunk_bbox.params.imgIds = chunk_img_ids + + # COCOeval.evaluate() 会打印固定日志;分块执行会重复很多次。 + # 这里静默 chunk 内部日志,最终 accumulate/summarize 输出保持清晰。 + with redirect_stdout(StringIO()): + chunk_bbox.evaluate() + + _merge_chunk_eval_imgs( + bbox_eval, + bbox_index, + chunk_bbox, + ) + + # 尽早释放 bbox chunk 的 IoU 和 evaluator。 + chunk_bbox.ious = {} + chunk_bbox.evalImgs = [] + del chunk_bbox + gc.collect() + + # segm evaluate + chunk_segm = COCOeval( + coco_gt, + coco_dt, + iouType="segm", + ) + chunk_segm.params.imgIds = chunk_img_ids + + with redirect_stdout(StringIO()): + chunk_segm.evaluate() + + _merge_chunk_eval_imgs( + segm_eval, + segm_index, + chunk_segm, + ) + + chunk_segm.ious = {} + chunk_segm.evalImgs = [] + del chunk_segm + + # chunk_results 与 coco_dt 引用同一批 annotation dict, + # 当前块完成后一起释放。 + del coco_dt + del chunk_results + gc.collect() + + return bbox_eval, segm_eval + + def run_evaluation(args): """读取单张图片的JSON结果并执行COCO评估""" # 加载COCO标注 coco_gt = COCO(args.gt_annotations) - all_img_ids = coco_gt.getImgIds() - - # 创建结果列表 - detection_results = [] # 获取所有JSON结果文件 - json_files = [f for f in os.listdir(args.result_dir) if f.endswith('.json')] + json_files = [ + f + for f in os.listdir(args.result_dir) + if f.endswith(".json") + ] if not json_files: print(f"错误: 在 {args.result_dir} 中未找到任何JSON文件") return - # 处理每个JSON结果 - total = len(json_files) - for idx, json_file in enumerate(tqdm(json_files, desc="加载结果文件")): - # 提取图片名称 - img_name = os.path.splitext(json_file)[0] - - # 查找图片ID(确保与标注文件中的ID匹配) - img_ids = coco_gt.getImgIds(imgIds=[int(img_name)] if img_name.isdigit() else []) - if not img_ids: - # 尝试通过文件名查找 - img_infos = coco_gt.loadImgs(all_img_ids) - matched = [info for info in img_infos if os.path.splitext(info['file_name'])[0] == img_name] - if matched: - img_id = matched[0]['id'] - else: - print(f"警告: 未找到图片 {img_name} 对应的ID,跳过") - continue - else: - img_id = img_ids[0] - - # 读取JSON文件内容 - json_path = os.path.join(args.result_dir, json_file) - try: - with open(json_path, 'r') as f: - results = json.load(f) - - # 确保结果中的image_id正确 - for res in results: - res["image_id"] = img_id - detection_results.append(res) - - except Exception as e: - print(f"警告: 处理文件 {json_file} 失败: {e},跳过") - continue - - # 保存合并的JSON结果(用于COCO评估) - temp_json = os.path.join(args.result_dir, "../merged_coco_results.json") - with open(temp_json, "w") as f: - json.dump(detection_results, f) - print(f"合并的评估文件已保存至: {temp_json}") + # 第一遍: + # 1. 流式生成 merged_coco_results.json; + # 2. 记录真正存在检测结果的图片,用于后续分块评估。 + _, eval_manifest, has_detection = _stream_merge_results( + coco_gt, + json_files, + args, + ) - # 执行COCO评估 - if not detection_results: + if not has_detection: print("警告: 没有有效的检测结果,无法进行评估") return - coco_dt = coco_gt.loadRes(temp_json) + # 第二遍: + # 按图片分块读取结果,每块分别执行 bbox / segm evaluate, + # 避免完整 merged JSON 经 loadRes/json.load 后常驻数 GB 内存。 + coco_eval_bbox, coco_eval_segm = _evaluate_in_chunks( + coco_gt, + eval_manifest, + ) # -------------------------- 1. 评估框检测(bbox)-------------------------- print("=" * 50) print("框检测(bbox)评估结果:") - coco_eval_bbox = COCOeval(coco_gt, coco_dt, iouType='bbox') - # 确保评估使用与推理相同的图片集 - coco_eval_bbox.params.imgIds = list(set(res["image_id"] for res in detection_results)) - coco_eval_bbox.evaluate() coco_eval_bbox.accumulate() coco_eval_bbox.summarize() # -------------------------- 2. 评估分割(segm)-------------------------- print("\n" + "=" * 50) print("分割(segm)评估结果:") - coco_eval_segm = COCOeval(coco_gt, coco_dt, iouType='segm') - coco_eval_segm.params.imgIds = list(set(res["image_id"] for res in detection_results)) - coco_eval_segm.evaluate() coco_eval_segm.accumulate() coco_eval_segm.summarize() def main(): # 解析命令行参数 - parser = argparse.ArgumentParser(description='根据单张图片的JSON结果进行COCO评估') - parser.add_argument('--result_dir', default='../out/result/json', - help='存放单张图片JSON结果的目录') - parser.add_argument('--gt_annotations', - default="../../../../../datasets/coco2017/annotations/instances_val2017.json", - help='COCO的ground truth标注文件路径') + parser = argparse.ArgumentParser(description="根据单张图片的JSON结果进行COCO评估") + parser.add_argument( + "--result_dir", + default="../out/result/json", + help="存放单张图片JSON结果的目录", + ) + parser.add_argument( + "--gt_annotations", + default="../../../../../datasets/coco2017/annotations/instances_val2017.json", + help="COCO的ground truth标注文件路径", + ) args = parser.parse_args() -- Gitee From 8032381cb8aaabd8a08598b1df088c4de37bc775 Mon Sep 17 00:00:00 2001 From: sloanqin Date: Wed, 2 Sep 2026 16:11:57 +0800 Subject: [PATCH 10/14] refactor: decouple seg postprocess result computation from JSON writing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后处理中最终结果的计算与写文件耦合在同一个函数里,不方便使用者 通过内存传递串接模型,见 docs/problem.txt 易用性问题(5)。 - 新增 SegmentationResult 结构体承载单条检测结果 - 将 SaveJsonResults 拆分为 BuildSegmentationResults(计算并返回结果) 与 SaveJsonResults(仅负责序列化写文件) - 调用处先 BuildSegmentationResults 得到 finalResults 再落盘, 后续需要内存串接时可直接复用 finalResults - 移除计算函数中未使用的 scale 参数(仍用于 ScaleBoxes) 输出内容与日志行为保持不变,改动同时应用于 yolov8s-seg 与 yolo11s-seg。 --- .../post_process/yolo11s_seg_postprocess.cpp | 60 ++++++++++++++----- .../post_process/yolov8s_seg_postprocess.cpp | 60 ++++++++++++++----- 2 files changed, 90 insertions(+), 30 deletions(-) diff --git a/samples/samples_GPL/common/infer/post_process/yolo11s_seg_postprocess.cpp b/samples/samples_GPL/common/infer/post_process/yolo11s_seg_postprocess.cpp index 4603419..c4fcd6b 100644 --- a/samples/samples_GPL/common/infer/post_process/yolo11s_seg_postprocess.cpp +++ b/samples/samples_GPL/common/infer/post_process/yolo11s_seg_postprocess.cpp @@ -57,6 +57,17 @@ namespace Yolo11sSegNS { float input_x1 = 0.0f, input_y1 = 0.0f, input_x2 = 0.0f, input_y2 = 0.0f; }; + struct SegmentationResult { + int imageId = 0; + int categoryId = -1; + float x = 0.0f; + float y = 0.0f; + float width = 0.0f; + float height = 0.0f; + float score = 0.0f; + vector> segmentation; + }; + const vector Yolo80ToCoco90 = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 27, 28, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, @@ -243,12 +254,11 @@ namespace Yolo11sSegNS { return validSegments; } - void SaveJsonResults(const vector& results, const string& savePath, const string& fileName, + vector BuildSegmentationResults(const vector& results, const string& fileName, const float* protos, int targetWidth, int targetHeight, - float scale, int padWidth, int padHeight, int originalWidth, int originalHeight) + int padWidth, int padHeight, int originalWidth, int originalHeight) { - json jsonResults = json::array(); - int validCount = 0; + vector finalResults; for (size_t i = 0; i < results.size(); ++i) { const auto& box = results[i]; @@ -293,15 +303,32 @@ namespace Yolo11sSegNS { continue; } - json result; - result["image_id"] = stoi(fileName); - result["category_id"] = box.cocoClassId; - result["bbox"] = { box.x1, box.y1, bboxWidth, bboxHeight }; - result["score"] = box.score; - result["segmentation"] = segmentation; - jsonResults.push_back(result); + SegmentationResult result; + result.imageId = stoi(fileName); + result.categoryId = box.cocoClassId; + result.x = box.x1; + result.y = box.y1; + result.width = bboxWidth; + result.height = bboxHeight; + result.score = box.score; + result.segmentation = segmentation; + finalResults.push_back(result); + } + + return finalResults; + } - validCount++; + void SaveJsonResults(const vector& results, const string& savePath, const string& fileName) + { + json jsonResults = json::array(); + for (const auto& result : results) { + json jsonResult; + jsonResult["image_id"] = result.imageId; + jsonResult["category_id"] = result.categoryId; + jsonResult["bbox"] = { result.x, result.y, result.width, result.height }; + jsonResult["score"] = result.score; + jsonResult["segmentation"] = result.segmentation; + jsonResults.push_back(jsonResult); } string jsonPath = savePath + "/" + fileName + ".json"; @@ -309,7 +336,7 @@ namespace Yolo11sSegNS { if (jsonFile.is_open()) { jsonFile << jsonResults.dump(2); jsonFile.close(); - LOG(INFO) << "Saved " << validCount << " valid results to: " << jsonPath; + LOG(INFO) << "Saved " << results.size() << " valid results to: " << jsonPath; } else { LOG(WARNING) << "Failed to open json file: " << jsonPath; } @@ -474,8 +501,11 @@ namespace Yolo11sSegNS { } if (!saveJson.empty()) { - SaveJsonResults(nmsResult, saveJson, fileName, protosData, targetWidth, targetHeight, - scale, padWidth, padHeight, originalWidth, originalHeight); + // 结果计算与文件保存解耦。若后续需要内存串接,可直接使用 finalResults。 + vector finalResults = BuildSegmentationResults( + nmsResult, fileName, protosData, targetWidth, targetHeight, + padWidth, padHeight, originalWidth, originalHeight); + SaveJsonResults(finalResults, saveJson, fileName); } LOG(INFO) << "====== Postprocessing Completed ======"; diff --git a/samples/samples_GPL/common/infer/post_process/yolov8s_seg_postprocess.cpp b/samples/samples_GPL/common/infer/post_process/yolov8s_seg_postprocess.cpp index 3b5d5c7..8c35bed 100644 --- a/samples/samples_GPL/common/infer/post_process/yolov8s_seg_postprocess.cpp +++ b/samples/samples_GPL/common/infer/post_process/yolov8s_seg_postprocess.cpp @@ -57,6 +57,17 @@ namespace Yolov8sSegNS { float input_x1 = 0.0f, input_y1 = 0.0f, input_x2 = 0.0f, input_y2 = 0.0f; }; + struct SegmentationResult { + int imageId = 0; + int categoryId = -1; + float x = 0.0f; + float y = 0.0f; + float width = 0.0f; + float height = 0.0f; + float score = 0.0f; + vector> segmentation; + }; + const vector Yolo80ToCoco90 = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 27, 28, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, @@ -243,12 +254,11 @@ namespace Yolov8sSegNS { return validSegments; } - void SaveJsonResults(const vector& results, const string& savePath, const string& fileName, + vector BuildSegmentationResults(const vector& results, const string& fileName, const float* protos, int targetWidth, int targetHeight, - float scale, int padWidth, int padHeight, int originalWidth, int originalHeight) + int padWidth, int padHeight, int originalWidth, int originalHeight) { - json jsonResults = json::array(); - int validCount = 0; + vector finalResults; for (size_t i = 0; i < results.size(); ++i) { const auto& box = results[i]; @@ -293,15 +303,32 @@ namespace Yolov8sSegNS { continue; } - json result; - result["image_id"] = stoi(fileName); - result["category_id"] = box.cocoClassId; - result["bbox"] = { box.x1, box.y1, bboxWidth, bboxHeight }; - result["score"] = box.score; - result["segmentation"] = segmentation; - jsonResults.push_back(result); + SegmentationResult result; + result.imageId = stoi(fileName); + result.categoryId = box.cocoClassId; + result.x = box.x1; + result.y = box.y1; + result.width = bboxWidth; + result.height = bboxHeight; + result.score = box.score; + result.segmentation = segmentation; + finalResults.push_back(result); + } + + return finalResults; + } - validCount++; + void SaveJsonResults(const vector& results, const string& savePath, const string& fileName) + { + json jsonResults = json::array(); + for (const auto& result : results) { + json jsonResult; + jsonResult["image_id"] = result.imageId; + jsonResult["category_id"] = result.categoryId; + jsonResult["bbox"] = { result.x, result.y, result.width, result.height }; + jsonResult["score"] = result.score; + jsonResult["segmentation"] = result.segmentation; + jsonResults.push_back(jsonResult); } string jsonPath = savePath + "/" + fileName + ".json"; @@ -309,7 +336,7 @@ namespace Yolov8sSegNS { if (jsonFile.is_open()) { jsonFile << jsonResults.dump(2); jsonFile.close(); - LOG(INFO) << "Saved " << validCount << " valid results to: " << jsonPath; + LOG(INFO) << "Saved " << results.size() << " valid results to: " << jsonPath; } else { LOG(WARNING) << "Failed to open json file: " << jsonPath; } @@ -474,8 +501,11 @@ namespace Yolov8sSegNS { } if (!saveJson.empty()) { - SaveJsonResults(nmsResult, saveJson, fileName, protosData, targetWidth, targetHeight, - scale, padWidth, padHeight, originalWidth, originalHeight); + // 结果计算与文件保存解耦。若后续需要内存串接,可直接使用 finalResults。 + vector finalResults = BuildSegmentationResults( + nmsResult, fileName, protosData, targetWidth, targetHeight, + padWidth, padHeight, originalWidth, originalHeight); + SaveJsonResults(finalResults, saveJson, fileName); } LOG(INFO) << "====== Postprocessing Completed ======"; -- Gitee From 48d301e0f335b76d8be933563cba635a7a0b2e38 Mon Sep 17 00:00:00 2001 From: sloanqin Date: Wed, 2 Sep 2026 16:17:11 +0800 Subject: [PATCH 11/14] docs: align recommended Python version in yolov6s README with requirements.txt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README 建议 Python 3.7.5,而 requirements.txt 声明的是 python3.8 环境, 两者不一致,统一为 Python 3.8。 --- samples/samples_GPL/built-in/yolov6s/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/samples_GPL/built-in/yolov6s/README.md b/samples/samples_GPL/built-in/yolov6s/README.md index fd7c4dd..a07642b 100644 --- a/samples/samples_GPL/built-in/yolov6s/README.md +++ b/samples/samples_GPL/built-in/yolov6s/README.md @@ -163,7 +163,7 @@ mkdir -p model ## 安装依赖 ```bash -# 建议使用 Python 3.7.5 +# 建议使用 Python 3.8 pip3 install -r requirements.txt ``` -- Gitee From feb1a8410afa5fe79d53cb7945d530308c8b5445 Mon Sep 17 00:00:00 2001 From: sloanqin Date: Wed, 2 Sep 2026 16:47:24 +0800 Subject: [PATCH 12/14] fix: skip result files with unresolvable image_id in COCO eval scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原写法: coco_gt.getImgIds(imgIds=[int(img_name)] if img_name.isdigit() else []) 当文件名非纯数字(如含空格的 "000000000139 test")时会传入空列表, 而 pycocotools 的 getImgIds 在 imgIds 与 catIds 均为空时返回全部图片 id (coco.py:187-188),导致该文件被静默误判为 img_ids[0](某张无关图片), 其检测结果作为假阳性注入评估且无任何告警,污染 AP/AR 结果。 - 新增 _build_image_name_map:一次性建立 file_name stem -> image_id 映射 - 新增 _resolve_image_id:查不到时返回 None,由调用方打印告警并跳过 - 顺带将非数字文件名的查找由 O(N) 全量扫描降为 O(1) 涉及脚本:yolo11s-pose、yolov6s、yolov4 对应 docs/problem.txt 易用性问题(3)。 --- .../detection/yolov4/script/yolo4_evaluate.py | 48 ++++++++++++++----- .../script/yolo11s_pose_evaluate.py | 48 ++++++++++++++----- .../yolov6s/script/yolo6s_evaluate.py | 48 ++++++++++++++----- 3 files changed, 105 insertions(+), 39 deletions(-) diff --git a/samples/built-in/detection/yolov4/script/yolo4_evaluate.py b/samples/built-in/detection/yolov4/script/yolo4_evaluate.py index 9884f8b..848fd09 100644 --- a/samples/built-in/detection/yolov4/script/yolo4_evaluate.py +++ b/samples/built-in/detection/yolov4/script/yolo4_evaluate.py @@ -18,11 +18,41 @@ from pycocotools.cocoeval import COCOeval import argparse +def _build_image_name_map(coco_gt, all_img_ids): + """一次性建立 文件名stem -> image_id 映射,避免逐文件全量扫描 COCO images。""" + stem_to_img_id = {} + for info in coco_gt.loadImgs(all_img_ids): + stem = os.path.splitext(info["file_name"])[0] + # 若 stem 重复,保留首次出现的 image_id + stem_to_img_id.setdefault(stem, info["id"]) + return stem_to_img_id + + +def _resolve_image_id(img_name, all_img_id_set, stem_to_img_id): + """ + 解析结果文件对应的 image_id,查不到时返回 None,交由调用方跳过。 + + 注意:不能写成 + coco_gt.getImgIds(imgIds=[int(img_name)] if img_name.isdigit() else []) + 因为 pycocotools 的 getImgIds 在 imgIds 与 catIds 均为空时会返回全部图片 id, + 导致非数字命名的文件被静默误判为 img_ids[0](某张无关图片), + 把假阳性注入评估且不产生任何告警。 + """ + if img_name.isdigit(): + numeric_id = int(img_name) + if numeric_id in all_img_id_set: + return numeric_id + return stem_to_img_id.get(img_name) + + def run_evaluation(args): """读取txt检测结果并执行COCO评估""" # 加载COCO标注 coco_gt = COCO(args.gt_annotations) all_img_ids = coco_gt.getImgIds() + # 预建映射,将每个结果文件的 image_id 查找由 O(N) 降为 O(1) + all_img_id_set = set(all_img_ids) + stem_to_img_id = _build_image_name_map(coco_gt, all_img_ids) # 创建结果列表 detection_results = [] @@ -39,19 +69,11 @@ def run_evaluation(args): # 提取图片名称 img_name = os.path.splitext(txt_file)[0] - # 查找图片ID - img_ids = coco_gt.getImgIds(imgIds=[int(img_name)] if img_name.isdigit() else []) - if not img_ids: - # 尝试通过文件名查找 - img_infos = coco_gt.loadImgs(all_img_ids) - matched = [info for info in img_infos if os.path.splitext(info['file_name'])[0] == img_name] - if matched: - img_id = matched[0]['id'] - else: - print(f"警告: 未找到图片 {img_name} 对应的ID,跳过") - continue - else: - img_id = img_ids[0] + # 查找图片ID;GT 中不存在的图片直接跳过,避免被误判为其他图片 + img_id = _resolve_image_id(img_name, all_img_id_set, stem_to_img_id) + if img_id is None: + print(f"警告: 未找到图片 {img_name} 对应的ID,跳过") + continue # 读取txt文件内容 txt_path = os.path.join(args.result_dir, txt_file) diff --git a/samples/samples_GPL/built-in/yolo11s-pose/script/yolo11s_pose_evaluate.py b/samples/samples_GPL/built-in/yolo11s-pose/script/yolo11s_pose_evaluate.py index fdc1226..27ea8de 100644 --- a/samples/samples_GPL/built-in/yolo11s-pose/script/yolo11s_pose_evaluate.py +++ b/samples/samples_GPL/built-in/yolo11s-pose/script/yolo11s_pose_evaluate.py @@ -19,6 +19,33 @@ from pycocotools.cocoeval import COCOeval import argparse +def _build_image_name_map(coco_gt, all_img_ids): + """一次性建立 文件名stem -> image_id 映射,避免逐文件全量扫描 COCO images。""" + stem_to_img_id = {} + for info in coco_gt.loadImgs(all_img_ids): + stem = os.path.splitext(info["file_name"])[0] + # 若 stem 重复,保留首次出现的 image_id + stem_to_img_id.setdefault(stem, info["id"]) + return stem_to_img_id + + +def _resolve_image_id(img_name, all_img_id_set, stem_to_img_id): + """ + 解析结果文件对应的 image_id,查不到时返回 None,交由调用方跳过。 + + 注意:不能写成 + coco_gt.getImgIds(imgIds=[int(img_name)] if img_name.isdigit() else []) + 因为 pycocotools 的 getImgIds 在 imgIds 与 catIds 均为空时会返回全部图片 id, + 导致非数字命名的文件被静默误判为 img_ids[0](某张无关图片), + 把假阳性注入评估且不产生任何告警。 + """ + if img_name.isdigit(): + numeric_id = int(img_name) + if numeric_id in all_img_id_set: + return numeric_id + return stem_to_img_id.get(img_name) + + def run_evaluation(args): """执行COCO关键点评估""" # 获取所有txt结果文件 @@ -30,6 +57,9 @@ def run_evaluation(args): # 加载COCO标注 coco_gt = COCO(args.gt_annotations) all_img_ids = coco_gt.getImgIds() + # 预建映射,将每个结果文件的 image_id 查找由 O(N) 降为 O(1) + all_img_id_set = set(all_img_ids) + stem_to_img_id = _build_image_name_map(coco_gt, all_img_ids) # 创建结果列表 detection_results = [] @@ -40,19 +70,11 @@ def run_evaluation(args): # 提取图片名称 img_name = os.path.splitext(txt_file)[0] - # 查找图片ID - img_ids = coco_gt.getImgIds(imgIds=[int(img_name)] if img_name.isdigit() else []) - if not img_ids: - # 尝试通过文件名查找 - img_infos = coco_gt.loadImgs(all_img_ids) - matched = [info for info in img_infos if os.path.splitext(info['file_name'])[0] == img_name] - if matched: - img_id = matched[0]['id'] - else: - print(f"警告: 未找到图片 {img_name} 对应的ID,跳过") - continue - else: - img_id = img_ids[0] + # 查找图片ID;GT 中不存在的图片直接跳过,避免被误判为其他图片 + img_id = _resolve_image_id(img_name, all_img_id_set, stem_to_img_id) + if img_id is None: + print(f"警告: 未找到图片 {img_name} 对应的ID,跳过") + continue # 读取txt文件内容 txt_path = os.path.join(args.result_dir, txt_file) diff --git a/samples/samples_GPL/built-in/yolov6s/script/yolo6s_evaluate.py b/samples/samples_GPL/built-in/yolov6s/script/yolo6s_evaluate.py index ec49dcb..5abc326 100644 --- a/samples/samples_GPL/built-in/yolov6s/script/yolo6s_evaluate.py +++ b/samples/samples_GPL/built-in/yolov6s/script/yolo6s_evaluate.py @@ -19,11 +19,41 @@ from pycocotools.cocoeval import COCOeval import argparse +def _build_image_name_map(coco_gt, all_img_ids): + """一次性建立 文件名stem -> image_id 映射,避免逐文件全量扫描 COCO images。""" + stem_to_img_id = {} + for info in coco_gt.loadImgs(all_img_ids): + stem = os.path.splitext(info["file_name"])[0] + # 若 stem 重复,保留首次出现的 image_id + stem_to_img_id.setdefault(stem, info["id"]) + return stem_to_img_id + + +def _resolve_image_id(img_name, all_img_id_set, stem_to_img_id): + """ + 解析结果文件对应的 image_id,查不到时返回 None,交由调用方跳过。 + + 注意:不能写成 + coco_gt.getImgIds(imgIds=[int(img_name)] if img_name.isdigit() else []) + 因为 pycocotools 的 getImgIds 在 imgIds 与 catIds 均为空时会返回全部图片 id, + 导致非数字命名的文件被静默误判为 img_ids[0](某张无关图片), + 把假阳性注入评估且不产生任何告警。 + """ + if img_name.isdigit(): + numeric_id = int(img_name) + if numeric_id in all_img_id_set: + return numeric_id + return stem_to_img_id.get(img_name) + + def run_evaluation(args): """读取txt检测结果并执行COCO评估""" # 加载COCO标注 coco_gt = COCO(args.gt_annotations) all_img_ids = coco_gt.getImgIds() + # 预建映射,将每个结果文件的 image_id 查找由 O(N) 降为 O(1) + all_img_id_set = set(all_img_ids) + stem_to_img_id = _build_image_name_map(coco_gt, all_img_ids) # 创建结果列表 detection_results = [] @@ -40,19 +70,11 @@ def run_evaluation(args): # 提取图片名称 img_name = os.path.splitext(txt_file)[0] - # 查找图片ID - img_ids = coco_gt.getImgIds(imgIds=[int(img_name)] if img_name.isdigit() else []) - if not img_ids: - # 尝试通过文件名查找 - img_infos = coco_gt.loadImgs(all_img_ids) - matched = [info for info in img_infos if os.path.splitext(info['file_name'])[0] == img_name] - if matched: - img_id = matched[0]['id'] - else: - print(f"警告: 未找到图片 {img_name} 对应的ID,跳过") - continue - else: - img_id = img_ids[0] + # 查找图片ID;GT 中不存在的图片直接跳过,避免被误判为其他图片 + img_id = _resolve_image_id(img_name, all_img_id_set, stem_to_img_id) + if img_id is None: + print(f"警告: 未找到图片 {img_name} 对应的ID,跳过") + continue # 读取txt文件内容 txt_path = os.path.join(args.result_dir, txt_file) -- Gitee From 78ac678b4701acab032bfabbf1e626b30846c279 Mon Sep 17 00:00:00 2001 From: sloanqin Date: Wed, 2 Sep 2026 16:49:02 +0800 Subject: [PATCH 13/14] yolo11s-pose: add pycocotools dependency to requirements --- samples/samples_GPL/built-in/yolo11s-pose/requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/samples/samples_GPL/built-in/yolo11s-pose/requirements.txt b/samples/samples_GPL/built-in/yolo11s-pose/requirements.txt index 9036301..0bbbf70 100644 --- a/samples/samples_GPL/built-in/yolo11s-pose/requirements.txt +++ b/samples/samples_GPL/built-in/yolo11s-pose/requirements.txt @@ -13,4 +13,5 @@ psutil py-cpuinfo pandas>=1.1.4 ultralytics-thop>=2.0.0 -ultralytics==8.3.232 \ No newline at end of file +ultralytics==8.3.232 +pycocotools>=2.0.0 \ No newline at end of file -- Gitee From 269fc6aa00454e9d692c044d3dd18c9dc4e7be6d Mon Sep 17 00:00:00 2001 From: sloanqin Date: Wed, 2 Sep 2026 17:01:59 +0800 Subject: [PATCH 14/14] yolov6s: fix model download/export path in README --- samples/samples_GPL/built-in/yolov6s/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/samples/samples_GPL/built-in/yolov6s/README.md b/samples/samples_GPL/built-in/yolov6s/README.md index a07642b..8f4ffe4 100644 --- a/samples/samples_GPL/built-in/yolov6s/README.md +++ b/samples/samples_GPL/built-in/yolov6s/README.md @@ -228,7 +228,10 @@ pip3 install -r requirements.txt 在 https://github.com/meituan/YOLOv6 中找到所需版本下载,也可以使用下述命令下载。 ``` + mkdir -p model + cd model wget https://github.com/meituan/YOLOv6/releases/download/0.4.0/yolov6s.pt + cd .. ``` 3. 导出onnx文件。 @@ -236,17 +239,15 @@ pip3 install -r requirements.txt 使用YOLOv6/deploy/ONNX/export_onnx.py导出onnx模型 ```bash - mkdir -p model cd YOLOv6 python3 ./deploy/ONNX/export_onnx.py \ - --weights yolov6s.pt \ + --weights ../model/yolov6s.pt \ --img 640 \ --batch 1 \ --simplify cd .. - mv YOLOv6/yolov6s.onnx ./model ``` 4. 使用ATC工具将ONNX模型转OM模型。 -- Gitee