From dac3b3f2b692f37f20e2d2cb8844159c5c9e942b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BA=86=E5=88=9A?= Date: Wed, 18 Dec 2024 17:35:24 +0800 Subject: [PATCH] 20241218 --- contrast/event_test.py | 33 +- contrast/one2n_contrast.py | 679 ++++++------------ contrast/one2n_contrast_old.py | 498 +++++++++++++ contrast/one2one_contrast.py | 176 +---- ...ne2one_onsite.py => onsite_contrast_pr.py} | 4 +- .../utils/__pycache__/event.cpython-39.pyc | Bin 10567 -> 11312 bytes contrast/utils/event.py | 112 ++- 说明文档.txt | 147 ++-- 8 files changed, 862 insertions(+), 787 deletions(-) create mode 100644 contrast/one2n_contrast_old.py rename contrast/{one2one_onsite.py => onsite_contrast_pr.py} (99%) diff --git a/contrast/event_test.py b/contrast/event_test.py index b148825..1c150f8 100644 --- a/contrast/event_test.py +++ b/contrast/event_test.py @@ -7,7 +7,7 @@ Created on Mon Dec 16 18:56:18 2024 import os import cv2 -from utils.event import ShoppingEvent +from utils.event import ShoppingEvent, save_data def main(): evtpaths = r"\\192.168.1.28\share\测试视频数据以及日志\算法全流程测试\202412\images" @@ -29,39 +29,14 @@ def main(): events = list(set(events)) '''定义当前事件存储地址及生成相应文件件''' - resultPath = r"\\192.168.1.28\share\测试视频数据以及日志\算法全流程测试\202412\result" + resultPath = r"\\192.168.1.28\share\测试视频数据以及日志\算法全流程测试\202412\result\single_event" for evtpath in events: - evtname = os.path.basename(evtpath) event = ShoppingEvent(evtpath) + save_data(event, resultPath) - img_cat = event.draw_tracks() - trajpath = os.path.join(resultPath, "trajectory") - if not os.path.exists(trajpath): - os.makedirs(trajpath) - traj_imgpath = os.path.join(trajpath, evtname+".png") - cv2.imwrite(traj_imgpath, img_cat) + print(event.evtname) - - ## 保存序列图像和轨迹子图 - subimgpath = os.path.join(resultPath, f"{evtname}", "subimg") - imgspath = os.path.join(resultPath, f"{evtname}", "imgs") - if not os.path.exists(subimgpath): - os.makedirs(subimgpath) - if not os.path.exists(imgspath): - os.makedirs(imgspath) - - - subimgpairs = event.save_event_subimg(subimgpath) - for subimgName, subimg in subimgpairs: - spath = os.path.join(subimgpath, subimgName) - cv2.imwrite(spath, subimg) - imgpairs = event.plot_save_image(imgspath) - for imgname, img in imgpairs: - spath = os.path.join(imgspath, imgname) - cv2.imwrite(spath, img) - - print(f"{evtname}") diff --git a/contrast/one2n_contrast.py b/contrast/one2n_contrast.py index 7ad3cb3..5c32ea2 100644 --- a/contrast/one2n_contrast.py +++ b/contrast/one2n_contrast.py @@ -1,498 +1,249 @@ # -*- coding: utf-8 -*- """ -Created on Sat Jul ** 14:07:25 2024 - - 现场测试精度、召回率分析程序,是 feat_select.py 的简化版, - 但支持循环计算,并输出总的pr曲线 +Created on Wed Dec 18 11:49:01 2024 @author: ym """ - - - -import os.path -import shutil - +import os +import pickle import numpy as np -import matplotlib.pyplot as plt -import cv2 from pathlib import Path -import sys -sys.path.append(r"D:\DetectTracking") -from tracking.utils.plotting import Annotator, colors -from tracking.utils.read_data import extract_data, read_deletedBarcode_file, read_tracking_output, read_returnGoods_file -from tracking.utils.plotting import draw_tracking_boxes, get_subimgs -from contrast.utils.tools import showHist, show_recall_prec, compute_recall_precision +import matplotlib.pyplot as plt +from scipy.spatial.distance import cdist +from utils.event import ShoppingEvent -# ============================================================================= -# def read_tracking_output(filepath): -# boxes = [] -# feats = [] -# with open(filepath, 'r', encoding='utf-8') as file: -# for line in file: -# line = line.strip() # 去除行尾的换行符和可能的空白字符 -# -# if not line: -# continue -# -# if line.endswith(','): -# line = line[:-1] -# -# data = np.array([float(x) for x in line.split(",")]) -# if data.size == 9: -# boxes.append(data) -# if data.size == 256: -# feats.append(data) -# -# return np.array(boxes), np.array(feats) -# ============================================================================= - -def read_tracking_imgs(imgspath): +def gen_eventdict(sourcePath, stype="data"): + '''stype: str, + 'source': 由 videos 或 images 生成的 pickle 文件 + 'data': 从 data 文件中读取的现场运行数据 ''' - input: - imgspath:该路径中的图像为Yolo算法的输入图像,640x512 - output: - imgs_0:后摄图像,根据 frameId 进行了排序 - imgs_1:前摄图像,根据 frameId 进行了排序 - ''' - imgs_0, frmIDs_0, imgs_1, frmIDs_1 = [], [], [], [] - - for filename in os.listdir(imgspath): - file, ext = os.path.splitext(filename) - flist = file.split('_') - if len(flist)==4 and ext==".jpg": - camID, frmID = flist[0], int(flist[-1]) - imgpath = os.path.join(imgspath, filename) - img = cv2.imread(imgpath) + + k, errEvents = 0, [] + for source_path in sourcePath: + evtpath, bname = os.path.split(source_path) + + # bname = r"20241126-135911-bdf91cf9-3e9a-426d-94e8-ddf92238e175_6923555210479" + source_path = os.path.join(evtpath, bname) + + pickpath = os.path.join(eventDataPath, f"{bname}.pickle") + if os.path.isfile(pickpath): continue + + try: + event = ShoppingEvent(source_path, stype) + # save_data(event, resultPath) - if camID=='0': - imgs_0.append(img) - frmIDs_0.append(frmID) - if camID=='1': - imgs_1.append(img) - frmIDs_1.append(frmID) - - if len(frmIDs_0): - indice = np.argsort(np.array(frmIDs_0)) - imgs_0 = [imgs_0[i] for i in indice ] - if len(frmIDs_1): - indice = np.argsort(np.array(frmIDs_1)) - imgs_1 = [imgs_1[i] for i in indice ] - - return imgs_0, imgs_1 - + with open(pickpath, 'wb') as f: + pickle.dump(event, f) + print(bname) + except Exception as e: + errEvents.append(source_path) + print(e) + # k += 1 + # if k==1: + # break + + errfile = os.path.join(resultPath, 'error_events.txt') + with open(errfile, 'w', encoding='utf-8') as f: + for line in errEvents: + f.write(line + '\n') -# ============================================================================= -# def draw_tracking_boxes(imgs, tracks): -# '''tracks: [x1, y1, x2, y2, track_id, score, cls, frame_index, box_index] -# 0 1 2 3 4 5 6 7 8 -# 关键:imgs中的次序和 track 中的 fid 对应 -# ''' -# subimgs = [] -# for *xyxy, tid, conf, cls, fid, bid in tracks: -# label = f'id:{int(tid)}_{int(cls)}_{conf:.2f}' -# -# annotator = Annotator(imgs[int(fid-1)].copy()) -# if cls==0: -# color = colors(int(cls), True) -# elif tid>0 and cls!=0: -# color = colors(int(tid), True) -# else: -# color = colors(19, True) # 19为调色板的最后一个元素 -# -# pt2 = [p/2 for p in xyxy] -# annotator.box_label(pt2, label, color=color) -# img0 = annotator.result() -# -# subimgs.append(img0) -# -# return subimgs -# ============================================================================= +def read_eventdict(eventDataPath): + evtDict = {} + for filename in os.listdir(eventDataPath): + evtname, ext = os.path.splitext(filename) + if ext != ".pickle": continue -def get_contrast_paths(pair, basepath): - assert(len(pair)==2 or len(pair)==3), "pair: seqdir, delete, barcodes" - - getout_fold = pair[0] # 取出操作对应的文件夹 - relvt_barcode = pair[1] # 取出操作对应放入操作的 Barcode - if len(pair)==3: - error_match = pair[2] # 取出操作错误匹配的 Barcode - else: - error_match = '' - - - getoutpath, inputpath, errorpath = '', '', '' - - day, hms = getout_fold.strip('_').split('-') - - input_folds, times = [], [] - errmatch_folds, errmatch_times = [], [] - for pathname in os.listdir(basepath): - if pathname.endswith('_'): continue - if os.path.isfile(os.path.join(basepath, pathname)):continue - infold = pathname.split('_') - if len(infold)!=2: continue - - day1, hms1 = infold[0].split('-') - - if day1==day and infold[1]==relvt_barcode and int(hms1) simimax - for k in idx: - if similarity[k] > simimax: - idxmax = k - simimax = similarity[k] - if idxmax>-1: - input_event = events[idxmax] + o2n_evt = [evt for name, evt in evtDicts.items() if name.find(nname[:15])==0] + if len(o2n_evt)==1: + o2nevt = o2n_evt[0] else: - input_event = '' + continue + if typee == "11": + boxes1 = event.front_trackingboxes + boxes2 = o2nevt.front_trackingboxes + + feat1 = event.front_trackingfeats + feat2 = o2nevt.front_trackingfeats + if typee == "10": + boxes1 = event.front_trackingboxes + boxes2 = o2nevt.back_trackingboxes + + feat1 = event.front_trackingfeats + feat2 = o2nevt.back_trackingfeats + if typee == "00": + boxes1 = event.back_trackingboxes + boxes2 = o2nevt.back_trackingboxes + + feat1 = event.back_trackingfeats + feat2 = o2nevt.back_trackingfeats + if typee == "01": + boxes1 = event.back_trackingboxes + boxes2 = o2nevt.front_trackingboxes + + feat1 = event.back_trackingfeats + feat2 = o2nevt.front_trackingfeats + + matrix = 1 - cdist(feat1[0], feat2[0], 'cosine') + simi_mean = np.mean(matrix) + simi_max = np.max(matrix) - errpairs.append((seqdir, input_event, events[index])) - err_similarity.append(max(similarity)) - - return corrpairs, errpairs, corr_similarity, err_similarity - - -def test_rpath_deleted(): - '''deletedBarcode.txt 格式的 1:n 数据结果文件, returnGoods.txt格式数据文件不需要调用该函数''' - - del_bfile = r'\\192.168.1.28\share\测试_202406\709\deletedBarcode.txt' - basepath = r'\\192.168.1.28\share\测试_202406\709' - savepath = r'D:\DetectTracking\contrast\result' - saveimgs = True - - - - relative_paths = [] - - '''1. 读取 deletedBarcode 文件 ''' - all_list = read_deletedBarcode_file(del_bfile) - - '''2. 算法性能评估,并输出 (取出,删除, 错误匹配) 对 ''' - corrpairs, errpairs, _, _ = one2n_deleted(all_list) - - '''3. 构造事件组合(取出,放入并删除, 错误匹配) 对应路径 ''' - for errpair in errpairs: - GetoutPath, InputPath, ErrorPath = get_contrast_paths(errpair, basepath) + evt_names.append(nname) + evt_barcodes.append(barcode) + evt_similars.append(simi_mean) + evt_types.append(typee) - pairs = (GetoutPath, InputPath, ErrorPath) - relative_paths.append(pairs) - print(InputPath) - '''3. 获取 (取出,放入并删除, 错误匹配) 对应路径,保存相应轨迹图像''' - if saveimgs: - save_tracking_imgpairs(pairs, savepath) - -def test_rpath_return(): - return_bfile = r'\\192.168.1.28\share\测试_202406\1101\images\returnGoods.txt' - basepath = r'\\192.168.1.28\share\测试_202406\1101\images' - savepath = r'D:\DetectTracking\contrast\result' + if len(evt_names)==len(evt_barcodes) and len(evt_barcodes)==len(evt_similars) \ + and len(evt_similars)==len(evt_types) and len(evt_names)>0: + + maxsim = evt_similars[evt_similars.index(max(evt_similars))] + for i in range(len(evt_names)): + bcd, simi = evt_barcodes[i], evt_similars[i] + + if bcd==event.barcode and simi==maxsim: + tpsimi.append(simi) + tpevents.append(evtname) + elif bcd==event.barcode and simi!=maxsim: + fnsimi.append(simi) + fnevents.append(evtname) + elif bcd!=event.barcode and simi!=maxsim: + tnsimi.append(simi) + tnevents.append(evtname) + elif bcd!=event.barcode and simi==maxsim and event.barcode in evt_barcodes: + fpsimi.append(simi) + fpevents.append(evtname) + else: + errorFile_one2n.append(evtname) + + + + ''' 1:n 数据存储,需根据相似度排序''' + PPrecise, PRecall = [], [] + NPrecise, NRecall = [], [] - all_list = read_returnGoods_file(return_bfile) - corrpairs, errpairs, _, _ = one2n_return(all_list) - for corrpair in corrpairs: - GetoutPath = os.path.join(basepath, corrpair[0]) - InputPath = os.path.join(basepath, corrpair[1]) + Thresh = np.linspace(-0.2, 1, 100) + for th in Thresh: + '''============================= 1:n 计算''' + TP = sum(np.array(tpsimi) >= th) + FP = sum(np.array(fpsimi) >= th) + FN = sum(np.array(fnsimi) < th) + TN = sum(np.array(tnsimi) < th) - pairs = (GetoutPath, InputPath) - save_tracking_imgpairs(pairs, savepath) - - for errpair in errpairs: - GetoutPath = os.path.join(basepath, errpair[0]) - InputPath = os.path.join(basepath, errpair[1]) - ErrorPath = os.path.join(basepath, errpair[2]) + PPrecise.append(TP/(TP+FP+1e-6)) + PRecall.append(TP/(len(tpsimi)+len(fnsimi)+1e-6)) + NPrecise.append(TN/(TN+FN+1e-6)) + NRecall.append(TN/(len(tnsimi)+len(fpsimi)+1e-6)) - pairs = (GetoutPath, InputPath, ErrorPath) - save_tracking_imgpairs(pairs, savepath) - - -def test_one2n(): - ''' - 1:n 性能测试 - 兼容 2 种 txt 文件格式:returnGoods.txt, deletedBarcode.txt - fpath: 文件路径、或文件夹,其中包含多个 txt 文件 - savepath: pr曲线保存路径 - ''' - # fpath = r'\\192.168.1.28\share\测试_202406\deletedBarcode\other' # deletedBarcode.txt - fpath = r'\\192.168.1.28\share\测试_202406\1108_展厅模型v800测试' # returnGoods.txt - savepath = r'\\192.168.1.28\share\测试_202406\deletedBarcode\illustration' - - if os.path.isdir(fpath): - filepaths = [os.path.join(fpath, f) for f in os.listdir(fpath) - if f.find('.txt')>0 - and (f.find('deletedBarcode')>=0 or f.find('returnGoods')>=0)] - elif os.path.isfile(fpath): - filepaths = [fpath] - else: - return + + '''4. ============================= 1:n 曲线,''' + fig, ax = plt.subplots() + ax.plot(Thresh, PPrecise, 'r', label='Precise_Pos: TP/TPFP') + ax.plot(Thresh, PRecall, 'b', label='Recall_Pos: TP/TPFN') + ax.plot(Thresh, NPrecise, 'g', label='Precise_Neg: TN/TNFP') + ax.plot(Thresh, NRecall, 'c', label='Recall_Neg: TN/TNFN') + ax.set_xlim([0, 1]) + ax.set_ylim([0, 1]) + ax.grid(True) + ax.set_title('1:n Precise & Recall') + ax.set_xlabel(f"Event Num: {len(tpsimi)+len(fnsimi)}") + ax.legend() + plt.show() + ## ============================= 1:n 直方图''' + fig, axes = plt.subplots(2, 2) + axes[0, 0].hist(tpsimi, bins=60, edgecolor='black') + axes[0, 0].set_xlim([-0.2, 1]) + axes[0, 0].set_title('TP') + axes[0, 1].hist(fpsimi, bins=60, edgecolor='black') + axes[0, 1].set_xlim([-0.2, 1]) + axes[0, 1].set_title('FP') + axes[1, 0].hist(tnsimi, bins=60, edgecolor='black') + axes[1, 0].set_xlim([-0.2, 1]) + axes[1, 0].set_title('TN') + axes[1, 1].hist(fnsimi, bins=60, edgecolor='black') + axes[1, 1].set_xlim([-0.2, 1]) + axes[1, 1].set_title('FN') + plt.show() + + return fpevents + +def main(): - if not os.path.exists(savepath): - os.mkdir(savepath) - - BarLists, blists = {}, [] - for pth in filepaths: - file = str(Path(pth).stem) - if file.find('deletedBarcode')>=0: - blist = read_deletedBarcode_file(pth) - if file.find('returnGoods')>=0: - blist = read_returnGoods_file(pth) - - BarLists.update({file: blist}) - blists.extend(blist) + '''1. 生成事件字典并保存至 eventDataPath, 只需运行一次 ''' + # gen_eventdict(sourcePath) - if len(blists): BarLists.update({"Total": blists}) - for file, blist in BarLists.items(): - if all(b['filetype']=="deletedBarcode" for b in blist): - _, _, correct_similarity, err_similarity = one2n_deleted(blist) - if all(b['filetype']=="returnGoods" for b in blists): - _, _, correct_similarity, err_similarity = one2n_return(blist) - - recall, prec, ths = compute_recall_precision(err_similarity, correct_similarity) - - plt1 = show_recall_prec(recall, prec, ths) - # plt1.show() - plt1.xlabel(f'threshold, Num: {len(blist)}') - plt1.savefig(os.path.join(savepath, file+'_pr.png')) - # plt1.close() - - plt2 = showHist(err_similarity, correct_similarity) - plt2.show() - plt2.savefig(os.path.join(savepath, file+'_hist.png')) - # plt.close() - - + '''2. 读取时间字典 ''' + evtDicts = read_eventdict(eventDataPath) + + + '''3. 1:n 比对事件评估 ''' + fpevents = one2n_pr(evtDicts) + fpErrFile = str(Path(resultPath).joinpath("one2n_Error.txt")) + with open(fpErrFile, "w") as file: + for item in fpevents: + file.write(item + "\n") + + + + + + + + if __name__ == '__main__': - # test_one2n() - test_rpath_return() # returnGoods.txt - # test_rpath_deleted() # deleteBarcode.txt + + eventSourcePath = [r"\\192.168.1.28\share\测试视频数据以及日志\算法全流程测试\202412\images"] + resultPath = r"\\192.168.1.28\share\测试视频数据以及日志\算法全流程测试\202412\result" + + eventDataPath = os.path.join(resultPath, "evtobjs") + similPath = os.path.join(resultPath, "simidata") + if not os.path.exists(eventDataPath): + os.makedirs(eventDataPath) + if not os.path.exists(similPath): + os.makedirs(similPath) + + main() - # try: - # test_rpath_return() - # test_rpath_deleted() - # except Exception as e: - # print(e) + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/contrast/one2n_contrast_old.py b/contrast/one2n_contrast_old.py new file mode 100644 index 0000000..7ad3cb3 --- /dev/null +++ b/contrast/one2n_contrast_old.py @@ -0,0 +1,498 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat Jul ** 14:07:25 2024 + + 现场测试精度、召回率分析程序,是 feat_select.py 的简化版, + 但支持循环计算,并输出总的pr曲线 + +@author: ym +""" + + + +import os.path +import shutil + +import numpy as np +import matplotlib.pyplot as plt +import cv2 +from pathlib import Path +import sys +sys.path.append(r"D:\DetectTracking") +from tracking.utils.plotting import Annotator, colors +from tracking.utils.read_data import extract_data, read_deletedBarcode_file, read_tracking_output, read_returnGoods_file +from tracking.utils.plotting import draw_tracking_boxes, get_subimgs +from contrast.utils.tools import showHist, show_recall_prec, compute_recall_precision + + +# ============================================================================= +# def read_tracking_output(filepath): +# boxes = [] +# feats = [] +# with open(filepath, 'r', encoding='utf-8') as file: +# for line in file: +# line = line.strip() # 去除行尾的换行符和可能的空白字符 +# +# if not line: +# continue +# +# if line.endswith(','): +# line = line[:-1] +# +# data = np.array([float(x) for x in line.split(",")]) +# if data.size == 9: +# boxes.append(data) +# if data.size == 256: +# feats.append(data) +# +# return np.array(boxes), np.array(feats) +# ============================================================================= + +def read_tracking_imgs(imgspath): + ''' + input: + imgspath:该路径中的图像为Yolo算法的输入图像,640x512 + output: + imgs_0:后摄图像,根据 frameId 进行了排序 + imgs_1:前摄图像,根据 frameId 进行了排序 + ''' + imgs_0, frmIDs_0, imgs_1, frmIDs_1 = [], [], [], [] + + for filename in os.listdir(imgspath): + file, ext = os.path.splitext(filename) + flist = file.split('_') + if len(flist)==4 and ext==".jpg": + camID, frmID = flist[0], int(flist[-1]) + imgpath = os.path.join(imgspath, filename) + img = cv2.imread(imgpath) + + if camID=='0': + imgs_0.append(img) + frmIDs_0.append(frmID) + if camID=='1': + imgs_1.append(img) + frmIDs_1.append(frmID) + + if len(frmIDs_0): + indice = np.argsort(np.array(frmIDs_0)) + imgs_0 = [imgs_0[i] for i in indice ] + if len(frmIDs_1): + indice = np.argsort(np.array(frmIDs_1)) + imgs_1 = [imgs_1[i] for i in indice ] + + return imgs_0, imgs_1 + + +# ============================================================================= +# def draw_tracking_boxes(imgs, tracks): +# '''tracks: [x1, y1, x2, y2, track_id, score, cls, frame_index, box_index] +# 0 1 2 3 4 5 6 7 8 +# 关键:imgs中的次序和 track 中的 fid 对应 +# ''' +# subimgs = [] +# for *xyxy, tid, conf, cls, fid, bid in tracks: +# label = f'id:{int(tid)}_{int(cls)}_{conf:.2f}' +# +# annotator = Annotator(imgs[int(fid-1)].copy()) +# if cls==0: +# color = colors(int(cls), True) +# elif tid>0 and cls!=0: +# color = colors(int(tid), True) +# else: +# color = colors(19, True) # 19为调色板的最后一个元素 +# +# pt2 = [p/2 for p in xyxy] +# annotator.box_label(pt2, label, color=color) +# img0 = annotator.result() +# +# subimgs.append(img0) +# +# return subimgs +# ============================================================================= + +def get_contrast_paths(pair, basepath): + assert(len(pair)==2 or len(pair)==3), "pair: seqdir, delete, barcodes" + + getout_fold = pair[0] # 取出操作对应的文件夹 + relvt_barcode = pair[1] # 取出操作对应放入操作的 Barcode + if len(pair)==3: + error_match = pair[2] # 取出操作错误匹配的 Barcode + else: + error_match = '' + + + getoutpath, inputpath, errorpath = '', '', '' + + day, hms = getout_fold.strip('_').split('-') + + input_folds, times = [], [] + errmatch_folds, errmatch_times = [], [] + for pathname in os.listdir(basepath): + if pathname.endswith('_'): continue + if os.path.isfile(os.path.join(basepath, pathname)):continue + infold = pathname.split('_') + if len(infold)!=2: continue + + day1, hms1 = infold[0].split('-') + + if day1==day and infold[1]==relvt_barcode and int(hms1) simimax + for k in idx: + if similarity[k] > simimax: + idxmax = k + simimax = similarity[k] + if idxmax>-1: + input_event = events[idxmax] + else: + input_event = '' + + errpairs.append((seqdir, input_event, events[index])) + err_similarity.append(max(similarity)) + + return corrpairs, errpairs, corr_similarity, err_similarity + + +def test_rpath_deleted(): + '''deletedBarcode.txt 格式的 1:n 数据结果文件, returnGoods.txt格式数据文件不需要调用该函数''' + + del_bfile = r'\\192.168.1.28\share\测试_202406\709\deletedBarcode.txt' + basepath = r'\\192.168.1.28\share\测试_202406\709' + savepath = r'D:\DetectTracking\contrast\result' + saveimgs = True + + + + relative_paths = [] + + '''1. 读取 deletedBarcode 文件 ''' + all_list = read_deletedBarcode_file(del_bfile) + + '''2. 算法性能评估,并输出 (取出,删除, 错误匹配) 对 ''' + corrpairs, errpairs, _, _ = one2n_deleted(all_list) + + '''3. 构造事件组合(取出,放入并删除, 错误匹配) 对应路径 ''' + for errpair in errpairs: + GetoutPath, InputPath, ErrorPath = get_contrast_paths(errpair, basepath) + + pairs = (GetoutPath, InputPath, ErrorPath) + relative_paths.append(pairs) + + print(InputPath) + '''3. 获取 (取出,放入并删除, 错误匹配) 对应路径,保存相应轨迹图像''' + if saveimgs: + save_tracking_imgpairs(pairs, savepath) + +def test_rpath_return(): + return_bfile = r'\\192.168.1.28\share\测试_202406\1101\images\returnGoods.txt' + basepath = r'\\192.168.1.28\share\测试_202406\1101\images' + savepath = r'D:\DetectTracking\contrast\result' + + all_list = read_returnGoods_file(return_bfile) + corrpairs, errpairs, _, _ = one2n_return(all_list) + for corrpair in corrpairs: + GetoutPath = os.path.join(basepath, corrpair[0]) + InputPath = os.path.join(basepath, corrpair[1]) + + pairs = (GetoutPath, InputPath) + save_tracking_imgpairs(pairs, savepath) + + for errpair in errpairs: + GetoutPath = os.path.join(basepath, errpair[0]) + InputPath = os.path.join(basepath, errpair[1]) + ErrorPath = os.path.join(basepath, errpair[2]) + + pairs = (GetoutPath, InputPath, ErrorPath) + save_tracking_imgpairs(pairs, savepath) + + +def test_one2n(): + ''' + 1:n 性能测试 + 兼容 2 种 txt 文件格式:returnGoods.txt, deletedBarcode.txt + fpath: 文件路径、或文件夹,其中包含多个 txt 文件 + savepath: pr曲线保存路径 + ''' + # fpath = r'\\192.168.1.28\share\测试_202406\deletedBarcode\other' # deletedBarcode.txt + fpath = r'\\192.168.1.28\share\测试_202406\1108_展厅模型v800测试' # returnGoods.txt + savepath = r'\\192.168.1.28\share\测试_202406\deletedBarcode\illustration' + + if os.path.isdir(fpath): + filepaths = [os.path.join(fpath, f) for f in os.listdir(fpath) + if f.find('.txt')>0 + and (f.find('deletedBarcode')>=0 or f.find('returnGoods')>=0)] + elif os.path.isfile(fpath): + filepaths = [fpath] + else: + return + + if not os.path.exists(savepath): + os.mkdir(savepath) + + BarLists, blists = {}, [] + for pth in filepaths: + file = str(Path(pth).stem) + if file.find('deletedBarcode')>=0: + blist = read_deletedBarcode_file(pth) + if file.find('returnGoods')>=0: + blist = read_returnGoods_file(pth) + + BarLists.update({file: blist}) + blists.extend(blist) + + if len(blists): BarLists.update({"Total": blists}) + for file, blist in BarLists.items(): + if all(b['filetype']=="deletedBarcode" for b in blist): + _, _, correct_similarity, err_similarity = one2n_deleted(blist) + if all(b['filetype']=="returnGoods" for b in blists): + _, _, correct_similarity, err_similarity = one2n_return(blist) + + recall, prec, ths = compute_recall_precision(err_similarity, correct_similarity) + + plt1 = show_recall_prec(recall, prec, ths) + # plt1.show() + plt1.xlabel(f'threshold, Num: {len(blist)}') + plt1.savefig(os.path.join(savepath, file+'_pr.png')) + # plt1.close() + + plt2 = showHist(err_similarity, correct_similarity) + plt2.show() + plt2.savefig(os.path.join(savepath, file+'_hist.png')) + # plt.close() + + + +if __name__ == '__main__': + # test_one2n() + test_rpath_return() # returnGoods.txt + # test_rpath_deleted() # deleteBarcode.txt + + + # try: + # test_rpath_return() + # test_rpath_deleted() + # except Exception as e: + # print(e) + + diff --git a/contrast/one2one_contrast.py b/contrast/one2one_contrast.py index f6b8177..78349b1 100644 --- a/contrast/one2one_contrast.py +++ b/contrast/one2one_contrast.py @@ -52,7 +52,7 @@ from tracking.utils.read_data import extract_data, read_tracking_output, read_si from tracking.utils.plotting import Annotator, colors from feat_extract.config import config as conf from feat_extract.inference import FeatsInterface -from utils.event import ShoppingEvent +from utils.event import ShoppingEvent, save_data from genfeats import gen_bcd_features @@ -84,86 +84,6 @@ def ft16_to_uint8(arr_ft16): return arr_uint8, arr_ft16_ -# ============================================================================= -# def plot_save_image(event, savepath): -# cameras = ('front', 'back') -# for camera in cameras: -# if camera == 'front': -# boxes = event.front_trackerboxes -# imgpaths = event.front_imgpaths -# else: -# boxes = event.back_trackerboxes -# imgpaths = event.back_imgpaths -# -# def array2list(bboxes): -# '''[x1, y1, x2, y2, track_id, score, cls, frame_index, box_index]''' -# frame_ids = bboxes[:, 7].astype(int) -# fID = np.unique(bboxes[:, 7].astype(int)) -# fboxes = [] -# for f_id in fID: -# idx = np.where(frame_ids==f_id)[0] -# box = bboxes[idx, :] -# fboxes.append((f_id, box)) -# return fboxes -# -# fboxes = array2list(boxes) -# -# for fid, fbox in fboxes: -# imgpath = imgpaths[int(fid-1)] -# -# image = cv2.imread(imgpath) -# -# annotator = Annotator(image.copy(), line_width=2) -# for i, *xyxy, tid, score, cls, fid, bid in enumerate(fbox): -# label = f'{int(id), int(cls)}' -# if tid >=0 and cls==0: -# color = colors(int(cls), True) -# elif tid >=0 and cls!=0: -# color = colors(int(id), True) -# else: -# color = colors(19, True) # 19为调色板的最后一个元素 -# annotator.box_label(xyxy, label, color=color) -# -# im0 = annotator.result() -# spath = os.path.join(savepath, Path(imgpath).name) -# cv2.imwrite(spath, im0) -# -# -# def save_event_subimg(event, savepath): -# ''' -# 功能: 保存一次购物事件的轨迹子图 -# 9 items: barcode, type, filepath, back_imgpaths, front_imgpaths, -# back_boxes, front_boxes, back_feats, front_feats, -# feats_compose, feats_select -# 子图保存次序:先前摄、后后摄,以 k 为编号,和 "feats_compose" 中次序相同 -# ''' -# cameras = ('front', 'back') -# for camera in cameras: -# if camera == 'front': -# boxes = event.front_boxes -# imgpaths = event.front_imgpaths -# else: -# boxes = event.back_boxes -# imgpaths = event.back_imgpaths -# -# for i, box in enumerate(boxes): -# x1, y1, x2, y2, tid, score, cls, fid, bid = box -# -# imgpath = imgpaths[int(fid-1)] -# image = cv2.imread(imgpath) -# -# subimg = image[int(y1/2):int(y2/2), int(x1/2):int(x2/2), :] -# -# camerType, timeTamp, _, frameID = os.path.basename(imgpath).split('.')[0].split('_') -# subimgName = f"cam{camerType}_{i}_tid{int(tid)}_fid({int(fid)}, {frameID}).png" -# spath = os.path.join(savepath, subimgName) -# -# cv2.imwrite(spath, subimg) -# # basename = os.path.basename(event['filepath']) -# print(f"Image saved: {os.path.basename(event.eventpath)}") -# ============================================================================= - - def data_precision_compare(stdfeat, evtfeat, evtMessage, save=True): evt, stdbcd, label = evtMessage rltdata, rltdata_ft16, rltdata_ft16_ = [], [], [] @@ -289,43 +209,7 @@ def one2one_simi(): with open(evtpath, 'rb') as f: evtdata = pickle.load(f) evtDict[evtname] = evtdata - - - '''======4.1 事件轨迹子图保存 ======================''' - error_event = [] - for evtname, event in evtDict.items(): - pairpath = os.path.join(subimgPath, f"{evtname}") - if not os.path.exists(pairpath): - os.makedirs(pairpath) - try: - subimgpairs = event.save_event_subimg(pairpath) - for subimgName, subimg in subimgpairs: - spath = os.path.join(pairpath, subimgName) - cv2.imwrite(spath, subimg) - - except Exception as e: - error_event.append(evtname) - - img_path = os.path.join(imagePath, f"{evtname}") - if not os.path.exists(img_path): - os.makedirs(img_path) - try: - imgpairs = event.plot_save_image(img_path) - for imgname, img in imgpairs: - spath = os.path.join(img_path, imgname) - cv2.imwrite(spath, img) - except Exception as e: - error_event.append(evtname) - - - - - errfile = os.path.join(subimgPath, f'error_event.txt') - with open(errfile, 'w', encoding='utf-8') as f: - for line in error_event: - f.write(line + '\n') - '''======4.2 barcode 标准图像保存 ==================''' # for stdbcd in barcodes: @@ -440,16 +324,13 @@ def compute_precise_recall(rltdata): rltpath = os.path.join(similPath, 'pr.png') plt.savefig(rltpath) # svg, png, pdf - def gen_eventdict(sourcePath, saveimg=True): - eventList = [] - errEvents = [] - k = 0 + k, errEvents = 0, [] for source_path in sourcePath: evtpath, bname = os.path.split(source_path) - bname = r"20241126-135911-bdf91cf9-3e9a-426d-94e8-ddf92238e175_6923555210479" + # bname = r"20241126-135911-bdf91cf9-3e9a-426d-94e8-ddf92238e175_6923555210479" source_path = os.path.join(evtpath, bname) pickpath = os.path.join(eventDataPath, f"{bname}.pickle") @@ -457,7 +338,8 @@ def gen_eventdict(sourcePath, saveimg=True): try: event = ShoppingEvent(source_path, stype="data") - eventList.append(event) + # save_data(event, resultPath) + with open(pickpath, 'wb') as f: pickle.dump(event, f) print(bname) @@ -465,11 +347,12 @@ def gen_eventdict(sourcePath, saveimg=True): errEvents.append(source_path) print(e) - k += 1 - if k==1: - break + # k += 1 + # if k==1: + # break + - errfile = os.path.join(eventDataPath, f'error_events.txt') + errfile = os.path.join(resultPath, 'error_events.txt') with open(errfile, 'w', encoding='utf-8') as f: for line in errEvents: f.write(line + '\n') @@ -477,6 +360,8 @@ def gen_eventdict(sourcePath, saveimg=True): def test_one2one(): + + '''==== 0. 生成事件列表和对应的 Barcodes列表 ===========''' bcdList, event_spath = [], [] for evtpath in eventSourcePath: for evtname in os.listdir(evtpath): @@ -486,9 +371,9 @@ def test_one2one(): if len(evt)>=2 and evt[-1].isdigit() and len(evt[-1])>=10: bcdList.append(evt[-1]) event_spath.append(os.path.join(evtpath, evtname)) - - bcdSet = set(bcdList) - '''==== 1. 生成标准特征集, 只需运行一次, 在 genfeats.py 中实现 ===========''' + + '''==== 1. 生成标准特征集, 只需运行一次, 在 genfeats.py 中实现 ===========''' + # bcdSet = set(bcdList) # gen_bcd_features(stdSamplePath, stdBarcodePath, stdFeaturePath, bcdSet) print("stdFeats have generated and saved!") @@ -511,40 +396,25 @@ if __name__ == '__main__': (3) stdFeaturePath: 比对标准特征集特征存储地址 (4) eventSourcePath: 事件地址 (5) resultPath: 结果存储地址 - (6) eventDataPath: 用于1:1比对的购物事件特征存储地址、对应子图存储地址 - (7) subimgPath: 1:1比对购物事件轨迹、标准barcode所对应的 subimgs 存储地址 - (8) similPath: 1:1比对结果存储地址(事件级) + (6) eventDataPath: 用于1:1比对的购物事件存储地址,在resultPath下 + (7) similPath: 1:1比对结果存储地址(事件级),在resultPath下 ''' - # stdSamplePath = r"\\192.168.1.28\share\已标注数据备份\对比数据\barcode\barcode_500_1979_已清洗" - # stdBarcodePath = r"\\192.168.1.28\share\测试_202406\contrast\std_barcodes_2192" - # stdFeaturePath = r"\\192.168.1.28\share\测试_202406\contrast\std_features_ft32" - # eventDataPath = r"\\192.168.1.28\share\测试_202406\contrast\events" - # subimgPath = r'\\192.168.1.28\share\测试_202406\contrast\subimgs' - # similPath = r"D:\DetectTracking\contrast\result\pickle" - # eventSourcePath = [r'\\192.168.1.28\share\测试_202406\1101\images'] stdSamplePath = r"\\192.168.1.28\share\数据\已完成数据\展厅数据\v1.0\比对数据\整理\zhantingBase" stdBarcodePath = r"D:\exhibition\dataset\bcdpath" stdFeaturePath = r"D:\exhibition\dataset\feats" - resultPath = r"D:\exhibition\result\events" + # eventSourcePath = [r'D:\exhibition\images\20241202'] # eventSourcePath = [r"\\192.168.1.28\share\测试视频数据以及日志\各模块测试记录\展厅测试\1129_展厅模型v801测试组测试"] - eventSourcePath = [r"\\192.168.1.28\share\测试视频数据以及日志\各模块测试记录\展厅测试\1126_展厅模型v801测试"] + eventSourcePath = [r"\\192.168.1.28\share\测试视频数据以及日志\算法全流程测试\202412\images"] + resultPath = r"\\192.168.1.28\share\测试视频数据以及日志\算法全流程测试\202412\result" - - '''定义当前事件存储地址及生成相应文件件''' - eventDataPath = os.path.join(resultPath, "1126", "evtobjs") - subimgPath = os.path.join(resultPath, "1126", "subimgs") - imagePath = os.path.join(resultPath, "1126", "image") - similPath = os.path.join(resultPath, "1126", "simidata") - + + eventDataPath = os.path.join(resultPath, "evtobjs") + similPath = os.path.join(resultPath, "simidata") if not os.path.exists(eventDataPath): os.makedirs(eventDataPath) - if not os.path.exists(subimgPath): - os.makedirs(subimgPath) - if not os.path.exists(imagePath): - os.makedirs(imagePath) if not os.path.exists(similPath): os.makedirs(similPath) diff --git a/contrast/one2one_onsite.py b/contrast/onsite_contrast_pr.py similarity index 99% rename from contrast/one2one_onsite.py rename to contrast/onsite_contrast_pr.py index 1456c9d..b26005f 100644 --- a/contrast/one2one_onsite.py +++ b/contrast/onsite_contrast_pr.py @@ -104,7 +104,7 @@ def test_compare(): plot_pr_curve(simiList) -def one2one_pr(paths): +def contrast_pr(paths): ''' 1:1 @@ -501,7 +501,7 @@ def one2one_pr(paths): if __name__ == "__main__": evtpaths = r"\\192.168.1.28\share\测试视频数据以及日志\算法全流程测试\202412\images" - one2one_pr(evtpaths) + contrast_pr(evtpaths) diff --git a/contrast/utils/__pycache__/event.cpython-39.pyc b/contrast/utils/__pycache__/event.cpython-39.pyc index 0cbfef108bcd4b2df107441b46fcc9bf09e088e7..3ee59c8b5952d81d8e15bd16a45582e74514854e 100644 GIT binary patch delta 4384 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 W`D-%WO=7%PO*hscd0e$gW&Z{1^jX*d delta 3746 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 bKe?@D=odytZ6!*N>g|nn>OwvJX