# -*- coding: utf-8 -*- """ Created on Fri Aug 30 17:53:03 2024 功能:1:1比对性能测试程序 1. 基于标准特征集所对应的原始图像样本,生成标准特征集并保存。 func: generate_event_and_stdfeatures(): (1) get_std_barcodeDict(stdSamplePath, stdBarcodePath) 提取 stdSamplePath 中样本地址,生成字典{barcode: [imgpath1, imgpath1, ...]} 并存储为 pickle 文件,barcode.pickle''' (2) stdfeat_infer(stdBarcodePath, stdFeaturePath, bcdSet=None) 标准特征提取,并保存至文件夹 stdFeaturePath 中, 也可在运行过程中根据与购物事件集合 barcodes 交集执行 2. 1:1 比对性能测试, func: one2one_eval(resultPath) (1) 求购物事件和标准特征级 Barcode 交集,构造 evtDict、stdDict (2) 构造扫 A 放 A、扫 A 放 B 组合,mergePairs = AA_list + AB_list (3) 循环计算 mergePairs 中元素 "(A, A) 或 (A, B)" 相似度; 对于未保存的轨迹图像或标准 barcode 图像,保存图像 (4) 保存计算结果 3. precise、recall等指标计算 func: compute_precise_recall(pickpath) @author: ym """ import numpy as np import cv2 import os import sys import random import pickle # import torch import time # import json from pathlib import Path from scipy.spatial.distance import cdist import matplotlib.pyplot as plt import shutil from datetime import datetime # from openpyxl import load_workbook, Workbook # from config import config as conf # from model import resnet18 as resnet18 # from feat_inference import inference_image sys.path.append(r"D:\DetectTracking") from tracking.utils.read_data import extract_data, read_tracking_output, read_one2one_simi, read_deletedBarcode_file from config import config as conf from genfeats import model_init, genfeatures, stdfeat_infer IMG_FORMAT = ['.bmp', '.jpg', '.jpeg', '.png'] def int8_to_ft16(arr_uint8, amin, amax): arr_ft16 = (arr_uint8 / 255 * (amax-amin) + amin).astype(np.float16) return arr_ft16 def ft16_to_uint8(arr_ft16): # pickpath = r"\\192.168.1.28\share\测试_202406\contrast\std_features_ft32vsft16\6902265587712_ft16.pickle" # with open(pickpath, 'rb') as f: # edict = pickle.load(f) # arr_ft16 = edict['feats'] amin = np.min(arr_ft16) amax = np.max(arr_ft16) arr_ft255 = (arr_ft16 - amin) * 255 / (amax-amin) arr_uint8 = arr_ft255.astype(np.uint8) arr_ft16_ = int8_to_ft16(arr_uint8, amin, amax) arrDistNorm = np.linalg.norm(arr_ft16_ - arr_ft16) / arr_ft16_.size return arr_uint8, arr_ft16_ def creat_shopping_event(eventPath): '''构造放入商品事件字典,这些事件需满足条件: 1) 前后摄至少有一条轨迹输出 2) 保存有帧图像,以便裁剪出 boxe 子图 ''' '''evtName 为一次购物事件''' evtName = os.path.basename(eventPath) evtList = evtName.split('_') '''================ 0. 检查 evtName 及 eventPath 正确性和有效性 ================''' if evtName.find('2024')<0 and len(evtList[0])!=15: return if not os.path.isdir(eventPath): return if len(evtList)==1 or (len(evtList)==2 and len(evtList[1])==0): barcode = '' else: barcode = evtList[-1] if len(evtList)==3 and evtList[-1]== evtList[-2]: evtType = 'input' else: evtType = 'other' '''================ 1. 构造事件描述字典,暂定 9 items ===============''' event = {} event['barcode'] = barcode event['type'] = evtType event['filepath'] = eventPath event['back_imgpaths'] = [] event['front_imgpaths'] = [] event['back_boxes'] = np.empty((0, 9), dtype=np.float64) event['front_boxes'] = np.empty((0, 9), dtype=np.float64) event['back_feats'] = np.empty((0, 256), dtype=np.float64) event['front_feats'] = np.empty((0, 256), dtype=np.float64) event['feats_compose'] = np.empty((0, 256), dtype=np.float64) event['one2one_simi'] = None event['feats_select'] = np.empty((0, 256), dtype=np.float64) '''================= 2. 读取 data 文件 =============================''' for dataname in os.listdir(eventPath): # filename = '1_track.data' datapath = os.path.join(eventPath, dataname) if not os.path.isfile(datapath): continue CamerType = dataname.split('_')[0] ''' 2.1 读取 0/1_track.data 中数据,暂不考虑''' # if dataname.find("_track.data")>0: # bboxes, ffeats, trackerboxes, tracker_feat_dict, trackingboxes, tracking_feat_dict = extract_data(datapath) ''' 2.2 读取 0/1_tracking_output.data 中数据''' if dataname.find("_tracking_output.data")>0: tracking_output_boxes, tracking_output_feats = read_tracking_output(datapath) if len(tracking_output_boxes) != len(tracking_output_feats): continue if CamerType == '0': event['back_boxes'] = tracking_output_boxes event['back_feats'] = tracking_output_feats elif CamerType == '1': event['front_boxes'] = tracking_output_boxes event['front_feats'] = tracking_output_feats if dataname.find("process.data")==0: simiDict = read_one2one_simi(datapath) event['one2one_simi'] = simiDict if len(event['back_boxes'])==0 or len(event['front_boxes'])==0: return None '''2.3 事件的特征表征方式: 特征选择、特征集成''' bk_feats = event['back_feats'] ft_feats = event['front_feats'] '''2.3.1 特征集成''' feats_compose = np.empty((0, 256), dtype=np.float64) if len(ft_feats): feats_compose = np.concatenate((feats_compose, ft_feats), axis=0) if len(bk_feats): feats_compose = np.concatenate((feats_compose, bk_feats), axis=0) event['feats_compose'] = feats_compose '''2.3.1 特征选择''' if len(ft_feats): event['feats_select'] = ft_feats '''================ 3. 读取图像文件地址,并按照帧ID排序 =============''' frontImgs, frontFid = [], [] backImgs, backFid = [], [] for imgname in os.listdir(eventPath): name, ext = os.path.splitext(imgname) if ext not in IMG_FORMAT or name.find('frameId')<0: continue CamerType = name.split('_')[0] frameId = int(name.split('_')[3]) imgpath = os.path.join(eventPath, imgname) if CamerType == '0': backImgs.append(imgpath) backFid.append(frameId) if CamerType == '1': frontImgs.append(imgpath) frontFid.append(frameId) frontIdx = np.argsort(np.array(frontFid)) backIdx = np.argsort(np.array(backFid)) '''3.1 生成依据帧 ID 排序的前后摄图像地址列表''' frontImgs = [frontImgs[i] for i in frontIdx] backImgs = [backImgs[i] for i in backIdx] '''3.2 将前、后摄图像路径添加至事件字典''' bfid = event['back_boxes'][:, 7].astype(np.int64) ffid = event['front_boxes'][:, 7].astype(np.int64) if len(bfid) and max(bfid) <= len(backImgs): event['back_imgpaths'] = [backImgs[i-1] for i in bfid] if len(ffid) and max(ffid) <= len(frontImgs): event['front_imgpaths'] = [frontImgs[i-1] for i in ffid] '''================ 4. 判断当前事件有效性,并添加至事件列表 ==========''' condt1 = len(event['back_imgpaths'])==0 or len(event['front_imgpaths'])==0 condt2 = len(event['front_feats'])==0 and len(event['back_feats'])==0 if condt1 or condt2: print(f"Event: {evtName}, Error, condt1: {condt1}, condt2: {condt2}") return None return event 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') k = 0 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[i] 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"{k}_cam-{camerType}_tid-{int(tid)}_fid-({int(fid)}, {frameID}).png" spath = os.path.join(savepath, subimgName) cv2.imwrite(spath, subimg) k += 1 # basename = os.path.basename(event['filepath']) print(f"Image saved: {os.path.basename(event['filepath'])}") def one2one_eval(resultPath): # stdBarcode = [p.stem for p in Path(stdFeaturePath).iterdir() if p.is_file() and p.suffix=='.pickle'] stdBarcode = [p.stem for p in Path(stdBarcodePath).iterdir() if p.is_file() and p.suffix=='.pickle'] '''购物事件列表,该列表中的 Barcode 存在于标准的 stdBarcode 内''' evtList = [(p.stem, p.stem.split('_')[-1]) for p in Path(eventFeatPath).iterdir() if p.is_file() and p.suffix=='.pickle' and (len(p.stem.split('_'))==2 or len(p.stem.split('_'))==3) and p.stem.split('_')[-1].isdigit() and p.stem.split('_')[-1] in stdBarcode ] barcodes = set([bcd for _, bcd in evtList]) '''标准特征集图像样本经特征提取并保存,运行一次后无需再运行''' stdfeat_infer(stdBarcodePath, stdFeaturePath, barcodes) '''========= 构建用于比对的标准特征字典 =============''' stdDict = {} for barcode in barcodes: stdpath = os.path.join(stdFeaturePath, barcode+'.pickle') with open(stdpath, 'rb') as f: stddata = pickle.load(f) stdDict[barcode] = stddata '''========= 构建用于比对的操作事件字典 =============''' evtDict = {} for event, barcode in evtList: evtpath = os.path.join(eventFeatPath, event+'.pickle') with open(evtpath, 'rb') as f: evtdata = pickle.load(f) evtDict[event] = evtdata '''===== 构造 3 个事件对: 扫 A 放 A, 扫 A 放 B, 合并 ====================''' AA_list = [(event, barcode, "same") for event, barcode in evtList] AB_list = [] for event, barcode in evtList: dset = list(barcodes.symmetric_difference(set([barcode]))) idx = random.randint(0, len(dset)-1) AB_list.append((event, dset[idx], "diff")) mergePairs = AA_list + AB_list '''读取事件、标准特征文件中数据,以 AA_list 和 AB_list 中关键字为 key 生成字典''' rltdata, rltdata_ft16, rltdata_ft16_ = [], [], [] for evt, stdbcd, label in mergePairs: event = evtDict[evt] ## 判断是否存在轨迹图像文件夹,不存在则创建文件夹并保存轨迹图像 pairpath = os.path.join(subimgPath, f"{evt}") if not os.path.exists(pairpath): os.makedirs(pairpath) save_event_subimg(event, pairpath) ## 判断是否存在 barcode 标准样本集图像文件夹,不存在则创建文件夹并存储 barcode 样本集图像 stdImgpath = stdDict[stdbcd]["imgpaths"] pstdpath = os.path.join(subimgPath, f"{stdbcd}") if not os.path.exists(pstdpath): os.makedirs(pstdpath) ii = 1 for filepath in stdImgpath: stdpath = os.path.join(pstdpath, f"{stdbcd}_{ii}.png") shutil.copy2(filepath, stdpath) ii += 1 ##============================================ float32 stdfeat = stdDict[stdbcd]["feats"] evtfeat = event["feats_compose"] matrix = 1 - cdist(stdfeat, evtfeat, 'cosine') simi_mean = np.mean(matrix) simi_max = np.max(matrix) stdfeatm = np.mean(stdfeat, axis=0, keepdims=True) evtfeatm = np.mean(evtfeat, axis=0, keepdims=True) simi_mfeat = 1- np.maximum(0.0, cdist(stdfeatm, evtfeatm, 'cosine')) rltdata.append((label, stdbcd, evt, simi_mean, simi_max, simi_mfeat[0,0])) ##============================================ float16 stdfeat_ft16 = stdfeat.astype(np.float16) evtfeat_ft16 = evtfeat.astype(np.float16) stdfeat_ft16 /= np.linalg.norm(stdfeat_ft16, axis=1)[:, None] evtfeat_ft16 /= np.linalg.norm(evtfeat_ft16, axis=1)[:, None] matrix_ft16 = 1 - cdist(stdfeat_ft16, evtfeat_ft16, 'cosine') simi_mean_ft16 = np.mean(matrix_ft16) simi_max_ft16 = np.max(matrix_ft16) stdfeatm_ft16 = np.mean(stdfeat_ft16, axis=0, keepdims=True) evtfeatm_ft16 = np.mean(evtfeat_ft16, axis=0, keepdims=True) simi_mfeat_ft16 = 1- np.maximum(0.0, cdist(stdfeatm_ft16, evtfeatm_ft16, 'cosine')) rltdata_ft16.append((label, stdbcd, evt, simi_mean_ft16, simi_max_ft16, simi_mfeat_ft16[0,0])) '''****************** uint8 is ok!!!!!! ******************''' ##============================================ uint8 # stdfeat_uint8, stdfeat_ft16_ = ft16_to_uint8(stdfeat_ft16) # evtfeat_uint8, evtfeat_ft16_ = ft16_to_uint8(evtfeat_ft16) stdfeat_uint8 = (stdfeat_ft16*128).astype(np.int8) evtfeat_uint8 = (evtfeat_ft16*128).astype(np.int8) stdfeat_ft16_ = stdfeat_uint8.astype(np.float16)/128 evtfeat_ft16_ = evtfeat_uint8.astype(np.float16)/128 absdiff = np.linalg.norm(stdfeat_ft16_ - stdfeat) / stdfeat.size matrix_ft16_ = 1 - cdist(stdfeat_ft16_, evtfeat_ft16_, 'cosine') simi_mean_ft16_ = np.mean(matrix_ft16_) simi_max_ft16_ = np.max(matrix_ft16_) stdfeatm_ft16_ = np.mean(stdfeat_ft16_, axis=0, keepdims=True) evtfeatm_ft16_ = np.mean(evtfeat_ft16_, axis=0, keepdims=True) simi_mfeat_ft16_ = 1- np.maximum(0.0, cdist(stdfeatm_ft16_, evtfeatm_ft16_, 'cosine')) rltdata_ft16_.append((label, stdbcd, evt, simi_mean_ft16_, simi_max_ft16_, simi_mfeat_ft16_[0,0])) tm = datetime.fromtimestamp(time.time()).strftime('%Y%m%d_%H%M%S') ##================================================ save as float32, rppath = os.path.join(resultPath, f'{tm}.pickle') with open(rppath, 'wb') as f: pickle.dump(rltdata, f) rtpath = os.path.join(resultPath, f'{tm}.txt') with open(rtpath, 'w', encoding='utf-8') as f: for result in rltdata: part = [f"{x:.3f}" if isinstance(x, float) else str(x) for x in result] line = ', '.join(part) f.write(line + '\n') ##================================================ save as float16, rppath_ft16 = os.path.join(resultPath, f'{tm}_ft16.pickle') with open(rppath_ft16, 'wb') as f: pickle.dump(rltdata_ft16, f) rtpath_ft16 = os.path.join(resultPath, f'{tm}_ft16.txt') with open(rtpath_ft16, 'w', encoding='utf-8') as f: for result in rltdata_ft16: part = [f"{x:.3f}" if isinstance(x, float) else str(x) for x in result] line = ', '.join(part) f.write(line + '\n') ##================================================ save as uint8, rppath_uint8 = os.path.join(resultPath, f'{tm}_uint8.pickle') with open(rppath_uint8, 'wb') as f: pickle.dump(rltdata_ft16_, f) rtpath_uint8 = os.path.join(resultPath, f'{tm}_uint8.txt') with open(rtpath_uint8, 'w', encoding='utf-8') as f: for result in rltdata_ft16_: part = [f"{x:.3f}" if isinstance(x, float) else str(x) for x in result] line = ', '.join(part) f.write(line + '\n') print("func: one2one_eval(), have finished!") def compute_precise_recall(pickpath): pickfile = os.path.basename(pickpath) file, ext = os.path.splitext(pickfile) if ext != '.pickle': return if file.find('ft16') < 0: return with open(pickpath, 'rb') as f: results = pickle.load(f) Same, Cross = [], [] for label, stdbcd, evt, simi_mean, simi_max, simi_mft in results: if label == "same": Same.append(simi_mean) if label == "diff": Cross.append(simi_mean) Same = np.array(Same) Cross = np.array(Cross) TPFN = len(Same) TNFP = len(Cross) # fig, axs = plt.subplots(2, 1) # axs[0].hist(Same, bins=60, edgecolor='black') # axs[0].set_xlim([-0.2, 1]) # axs[0].set_title(f'Same Barcode, Num: {TPFN}') # axs[1].hist(Cross, bins=60, edgecolor='black') # axs[1].set_xlim([-0.2, 1]) # axs[1].set_title(f'Cross Barcode, Num: {TNFP}') # plt.savefig(f'./result/{file}_hist.png') # svg, png, pdf Recall_Pos, Recall_Neg = [], [] Precision_Pos, Precision_Neg = [], [] Correct = [] Thresh = np.linspace(-0.2, 1, 100) for th in Thresh: TP = np.sum(Same > th) FN = TPFN - TP TN = np.sum(Cross < th) FP = TNFP - TN Recall_Pos.append(TP/TPFN) Recall_Neg.append(TN/TNFP) Precision_Pos.append(TP/(TP+FP+1e-6)) Precision_Neg.append(TN/(TN+FN+1e-6)) Correct.append((TN+TP)/(TPFN+TNFP)) fig, ax = plt.subplots() ax.plot(Thresh, Correct, 'r', label='Correct: (TN+TP)/(TPFN+TNFP)') ax.plot(Thresh, Recall_Pos, 'b', label='Recall_Pos: TP/TPFN') ax.plot(Thresh, Recall_Neg, 'g', label='Recall_Neg: TN/TNFP') ax.plot(Thresh, Precision_Pos, 'c', label='Precision_Pos: TP/(TP+FP)') ax.plot(Thresh, Precision_Neg, 'm', label='Precision_Neg: TN/(TN+FN)') ax.set_xlim([0, 1]) ax.set_ylim([0, 1]) ax.grid(True) ax.set_title('PrecisePos & PreciseNeg') ax.set_xlabel(f"Same Num: {TPFN}, Cross Num: {TNFP}") ax.legend() plt.show() plt.savefig(f'./result/{file}_pr.png') # svg, png, pdf def gen_eventdict(eventDatePath, saveimg=True): eventList = [] # k = 0 for datePath in eventDatePath: for eventName in os.listdir(datePath): pickpath = os.path.join(eventFeatPath, f"{eventName}.pickle") if os.path.isfile(pickpath): continue eventPath = os.path.join(datePath, eventName) eventDict = creat_shopping_event(eventPath) if eventDict: eventList.append(eventDict) with open(pickpath, 'wb') as f: pickle.dump(eventDict, f) print(f"Event: {eventName}, have saved!") # k += 1 # if k==1: # break ## 保存轨迹中 boxes 子图 if not saveimg: return for event in eventList: basename = os.path.basename(event['filepath']) savepath = os.path.join(subimgPath, basename) if not os.path.exists(savepath): os.makedirs(savepath) save_event_subimg(event, savepath) def test_one2one(): eventDatePath = [r'\\192.168.1.28\share\测试_202406\1101\images', # r'\\192.168.1.28\share\测试_202406\0910\images', # r'\\192.168.1.28\share\测试_202406\0723\0723_1', # r'\\192.168.1.28\share\测试_202406\0723\0723_2', # r'\\192.168.1.28\share\测试_202406\0723\0723_3', # r'\\192.168.1.28\share\测试_202406\0722\0722_01', # r'\\192.168.1.28\share\测试_202406\0722\0722_02' # r'\\192.168.1.28\share\测试_202406\0719\719_3', # r'\\192.168.1.28\share\测试_202406\0716\0716_1', # r'\\192.168.1.28\share\测试_202406\0716\0716_2', # r'\\192.168.1.28\share\测试_202406\0716\0716_3', # r'\\192.168.1.28\share\测试_202406\0712\0712_1', # 无帧图像 # r'\\192.168.1.28\share\测试_202406\0712\0712_2', # 无帧图像 ] bcdList = [] for evtpath in eventDatePath: for evtname in os.listdir(evtpath): evt = evtname.split('_') if len(evt)>=2 and evt[-1].isdigit() and len(evt[-1])>=10: bcdList.append(evt[-1]) bcdSet = set(bcdList) model = model_init(conf) '''==== 1. 生成标准特征集, 只需运行一次 ===============''' genfeatures(model, stdSamplePath, stdBarcodePath, stdFeaturePath, bcdSet) print("stdFeats have generated and saved!") '''==== 2. 生成事件字典, 只需运行一次 ===============''' gen_eventdict(eventDatePath) print("eventList have generated and saved!") '''==== 3. 1:1性能评估 ===============''' one2one_eval(resultPath) for filename in os.listdir(resultPath): if filename.find('.pickle') < 0: continue if filename.find('0911') < 0: continue pickpath = os.path.join(resultPath, filename) compute_precise_recall(pickpath) if __name__ == '__main__': ''' 共6个地址: (1) stdSamplePath: 用于生成比对标准特征集的原始图像地址 (2) stdBarcodePath: 比对标准特征集原始图像地址的pickle文件存储,{barcode: [imgpath1, imgpath1, ...]} (3) stdFeaturePath: 比对标准特征集特征存储地址 (4) eventFeatPath: 用于1:1比对的购物事件特征存储地址、对应子图存储地址 (5) subimgPath: 1:1比对购物事件轨迹、标准barcode所对应的 subimgs 存储地址 (6) resultPath: 1:1比对结果存储地址 ''' 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" eventFeatPath = r"\\192.168.1.28\share\测试_202406\contrast\events" subimgPath = r'\\192.168.1.28\share\测试_202406\contrast\subimgs' resultPath = r"D:\DetectTracking\contrast\result\pickle" if not os.path.exists(resultPath): os.makedirs(resultPath) test_one2one()