ReID: ketib qaytgan odamni tanish
Bu bobning kodi: kod/12_reid_galereya.py — kitob papkasidan ishga tushiring: python kod/12_reid_galereya.py
Talabaning savoli
Kitob shu savoldan boshlangan edi: "Odam kadrdan chiqib, keyin qaytib kelsa, uni qanday taniymiz va qayerdan chiqib ketganini qanday bilamiz?" 9–11-boblardagi tracker lar faqat joy va harakatga tayanadi. To'silish qisqa bo'lsa (1 soniyagacha), Kalman va trek xotirasi yordam beradi. Lekin odam uzoqroq yo'qolsa yoki boshqa joydan qaytsa, harakat hech narsa aytmaydi — faqat tashqi ko'rinish qoladi: kiyim rangi, shakli, gavda.
Bu ReID (re-identification, qayta tanish) vazifasi. Har bir odam rasmidan vektor (embedding) hisoblanadi va ikki vektor qanchalik o'xshashligi o'lchanadi. Bu bobda:
- Uch xil vektorni real MOT17 odamlarida o'lchaymiz va qaysi biri odamni yaxshiroq ajratishini aniqlaymiz.
- Amaliy muammo: kamera oq yoki och pushti devor yonida, odam esa oq ko'ylakda. Devor va kiyim aralashib ketmasligi uchun nima qilish kerak — o'lchab ko'ramiz.
- BoT-SORT ning o'z ReID rejimini sinaymiz.
- Talabaning savoliga javob beradigan tizim: "yo'qolganlar galereyasi" — kim qachon, qayerdan chiqib ketdi va kim qaytib keldi.
import time from collections import Counter, defaultdict from pathlib import Path import cv2 import numpy as np import pandas as pd import torch import torchvision import matplotlib.pyplot as plt from ultralytics import YOLO from ultralytics.engine.results import Boxes from ultralytics.trackers import BOTSORT, BYTETracker from ultralytics.trackers.bot_sort import ReID from ultralytics.utils import LOGGER, YAML, IterableSimpleNamespace from ultralytics.utils.checks import check_yaml LOGGER.setLevel("ERROR") MOT = Path("datasets/MOT17/train") V09 = MOT / "MOT17-09-FRCNN" gt = np.loadtxt(V09 / "gt" / "gt.txt", delimiter=",") piyoda = gt[(gt[:, 7] == 1) & (gt[:, 6] == 1)]
Vektor va o'xshashlik
Har bir odam rasmi (crop) bitta vektorga aylantiriladi. Ikki vektor orasidagi kosinus o'xshashlik — ular orasidagi burchak kosinusi: 1 — bir xil yo'nalish, 0 — umuman bog'liq emas:
Yaxshi vektor uchun bir odamning turli paytdagi rasmlari o'xshashligi yuqori, turli odamlarniki esa past bo'lishi kerak. Standart o'lchov — rank-1: har bir rasm uchun eng o'xshash boshqa rasmni topamiz; u o'sha odam bo'lsa — to'g'ri. Bir xil odamning ikki rasmi orasida kamida 60 kadr (2 soniya) bo'lishini talab qilamiz, aks holda deyarli bir xil qo'shni kadrlar vazifani juda oson qilib qo'yadi.
Sinov to'plami: MOT17-09 ning GT box laridan har 15-kadrda, ko'rinishi ≥ 0.8 va balandligi ≥ 80 px bo'lgan, kadr ichida to'liq turgan odamlar. Har biriga segmentatsiya modeli (yolo11n-seg) bilan niqob (mask) ham olamiz — qaysi piksellar odamga tegishli ekanini bildiradi:
seg = YOLO("weights/yolo11n-seg.pt") tanlov = piyoda[(piyoda[:, 8] >= 0.8) & (piyoda[:, 5] >= 80) & (piyoda[:, 0] % 15 == 0)] tanlov = tanlov[(tanlov[:, 2] >= 0) & (tanlov[:, 3] >= 0) & (tanlov[:, 2] + tanlov[:, 4] <= 1920) & (tanlov[:, 3] + tanlov[:, 5] <= 1080)] namuna = [] # (kadr, GT odam, crop BGR, niqob) for k in np.unique(tanlov[:, 0]).astype(int): kadr = cv2.imread(str(V09 / "img1" / f"{k:06d}.jpg")) r = seg.predict(kadr, classes=[0], imgsz=1280, retina_masks=True, device=0, verbose=False)[0] niqoblar, boxlar = r.masks.data.cpu().numpy() > 0.5, r.boxes.xyxy.cpu().numpy() for g in tanlov[tanlov[:, 0] == k]: x, y, w, h = g[2:6].astype(int) iou = torchvision.ops.box_iou(torch.tensor([[x, y, x + w, y + h]], dtype=torch.float64), torch.tensor(boxlar, dtype=torch.float64)).numpy()[0] if iou.max() >= 0.6: # GT box ga mos segmentatsiya topildi namuna.append((k, int(g[1]), kadr[y:y + h, x:x + w].copy(), niqoblar[iou.argmax()][y:y + h, x:x + w])) idlar = np.array([n[1] for n in namuna]) kadrlar = np.array([n[0] for n in namuna]) bir_xil = (idlar[:, None] == idlar[None]) & (np.abs(kadrlar[:, None] - kadrlar[None]) >= 60) turli = idlar[:, None] != idlar[None] print(f"rasmlar: {len(namuna)} | odamlar: {len(np.unique(idlar))} | 'bir xil odam' juftlari: {int(bir_xil.sum() // 2)}") def baho(E): """Vektorlar -> (bir xil odam o'rtacha o'xshashligi, turli odamlar o'rtachasi, rank-1, turli odamlar 99-persentili).""" E = E / np.linalg.norm(E, axis=1, keepdims=True) S = E @ E.T togri = soni = 0 for q in range(len(E)): if bir_xil[q].any(): s = np.where(bir_xil[q] | turli[q], S[q], -2) # o'zi va qo'shni kadrlar hisobga olinmaydi togri += idlar[s.argmax()] == idlar[q] soni += 1 return S[bir_xil].mean(), S[turli].mean(), togri / soni, np.percentile(S[turli], 99)
rasmlar: 146 | odamlar: 23 | 'bir xil odam' juftlari: 322
Uch xil vektor:
- Ultralytics ning standart ReID modeli — BoT-SORT
with_reidyoqilganda ishlatadiganyolo26n-clsklassifikatori, oxiridan oldingi qatlami (trackers/bot_sort.pydagiReIDklassi). - CLIP (OpenAI, ViT-B/32) — millionlab rasm va matn juftida o'qitilgan mashhur model.
- Rang gistogrammasi — noldan: tananing ustki (ko'ylak) va pastki (shim) qismi uchun alohida HSV rang taqsimoti. Hech qanday neyron tarmoq yo'q.
Har birini ikki xil hisoblaymiz: butun box bo'yicha va faqat odam piksellari (niqob) bo'yicha.
from transformers import CLIPModel, CLIPProcessor def hsv_vektor(crop, niqob=None): """Ust kiyim (bo'yning 15–50%) va pastki qism (50–95%) uchun 16x8 lik HSV gistogramma.""" hsv = cv2.cvtColor(crop, cv2.COLOR_BGR2HSV) h = hsv.shape[0] qismlar = [] for a, b in ((0.15, 0.5), (0.5, 0.95)): m = None if niqob is None else niqob[int(h * a):int(h * b)].astype(np.uint8) qismlar.append(cv2.calcHist([hsv[int(h * a):int(h * b)]], [0, 1], m, [16, 8], [0, 180, 0, 256]).ravel()) return np.concatenate(qismlar) def niqobla(crop, niqob, rang=(128, 128, 128)): c = crop.copy() c[~niqob] = rang # odamdan tashqari hamma piksel — bir xil rang return c reid = ReID("weights/yolo26n-cls.pt") clip_p = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32") clip_m = CLIPModel.from_pretrained("openai/clip-vit-base-patch32").to("cuda").eval() def vektorlar(usul, niqobli): rasmlar = [niqobla(c, m) if niqobli else c for _, _, c, m in namuna] t0 = time.perf_counter() if usul == "ultralytics ReID (yolo26n-cls)": E = np.array([f.cpu().numpy() for f in reid.model.predictor(rasmlar)]) elif usul == "CLIP ViT-B/32": with torch.no_grad(): kirish = clip_p(images=[r[:, :, ::-1] for r in rasmlar], return_tensors="pt").to("cuda") E = clip_m.get_image_features(**kirish).cpu().numpy() else: E = np.array([hsv_vektor(c, m if niqobli else None) for _, _, c, m in namuna]) return E, (time.perf_counter() - t0) / len(namuna) * 1000 qatorlar, chegara = [], {} for usul in ("ultralytics ReID (yolo26n-cls)", "CLIP ViT-B/32", "rang gistogrammasi"): for niqobli in (False, True): E, ms = vektorlar(usul, niqobli) bir, tur, r1, p99 = baho(E) chegara[usul, niqobli] = p99 qatorlar.append({"vektor": usul, "niqob": "ha" if niqobli else "yo'q", "o'lcham": E.shape[1], "bir xil odam": bir, "turli odamlar": tur, "rank-1": r1, "ms/rasm": ms}) show(pd.DataFrame(qatorlar).round(3))
| vektor | niqob | o'lcham | bir xil odam | turli odamlar | rank-1 | ms/rasm |
|---|---|---|---|---|---|---|
| ultralytics ReID (yolo26n-cls) | yo'q | 256 | 0.937 | 0.885 | 0.684 | 1.211 |
| ultralytics ReID (yolo26n-cls) | ha | 256 | 0.95 | 0.904 | 0.701 | 1.073 |
| CLIP ViT-B/32 | yo'q | 512 | 0.874 | 0.799 | 0.667 | 2.536 |
| CLIP ViT-B/32 | ha | 512 | 0.916 | 0.852 | 0.701 | 2.566 |
| rang gistogrammasi | yo'q | 256 | 0.655 | 0.35 | 0.684 | 0.116 |
| rang gistogrammasi | ha | 256 | 0.663 | 0.179 | 0.906 | 0.122 |
Kutilmagan natija. Ultralytics ReID modeli va CLIP da "bir xil odam" va "turli odamlar" o'xshashligi deyarli teng (masalan 0.94 va 0.89): ular rasmda odam borligini yaxshi ko'radi, lekin qaysi odam ekanini yomon ajratadi — ular narsalarni tasniflashga o'qitilgan, odamlarni farqlashga emas. Eng oddiy vektor — niqob bo'yicha rang gistogrammasi — rank-1 da eng yaxshi va eng tez chiqdi, turli odamlar o'xshashligi esa ikki barobar past. Ehtiyot bo'ling: bu 23 kishi, 17 soniya, bir xil yorug'lik. Kunlar davomida, boshqa kamerada, kiyim yoki yorug'lik o'zgarsa, rang tez ishonchsizlanadi. Buning uchun odamlar datasetlarida (Market-1501, MSMT17) maxsus o'qitilgan ReID modellari bor (masalan OSNet) — ular bu kitobda o'rnatilmagan, lekin yondashuv o'sha: vektor, kosinus, niqob.
Oq devor va oq ko'ylak
Amaliy savol: kamera ko'pincha devor yoniga o'rnatiladi. Devor oq yoki och pushti, odam esa oq ko'ylak va och rangli shimda. Ikki narsa aralashib ketmaydimi?
Bu savolning ikki qismi bor va ular turlicha buziladi:
- Aniqlash (odam bormi?). YOLO asosan shakl va chegaralarga qaraydi, rangga emas.
- Tanish (bu o'sha odammi?). Box bo'yicha hisoblangan rang vektori devor piksellarini ham o'z ichiga oladi. Oq devor oldida ikki xil odam bir-biriga o'xshab qoladi, bir xil odam esa boshqa devor oldiga o'tganda o'zgarib ketadi.
O'lchaymiz. Har bir odamni niqob bo'yicha kesib olib, fonini oq va och pushti devor rangiga almashtiramiz — xuddi o'sha odamlar devor oldida turgandek:
devorlar = {"asl fon": None, "oq devor": (240, 240, 240), "och pushti devor": (225, 215, 245)} # BGR qatorlar = [] for nom, rang in devorlar.items(): rasmlar = [(c if rang is None else niqobla(c, m, rang), m) for _, _, c, m in namuna] for niqobli in (False, True): bir, tur, r1, _ = baho(np.array([hsv_vektor(c, m if niqobli else None) for c, m in rasmlar])) qatorlar.append({"fon": nom, "vektor": "niqob bo'yicha" if niqobli else "butun box bo'yicha", "bir xil odam": bir, "turli odamlar": tur, "farq": bir - tur, "rank-1": r1}) show(pd.DataFrame(qatorlar).round(3)) for niqobli in (False, True): a = np.array([hsv_vektor(c, m if niqobli else None) for _, _, c, m in namuna]) b = np.array([hsv_vektor(niqobla(c, m, (240, 240, 240)), m if niqobli else None) for _, _, c, m in namuna]) cos = (a * b).sum(1) / (np.linalg.norm(a, axis=1) * np.linalg.norm(b, axis=1)) print(f"bir xil rasm: asl fon va oq devor orasidagi o'xshashlik ({'niqob bilan' if niqobli else 'niqobsiz'}): {cos.mean():.3f}")
bir xil rasm: asl fon va oq devor orasidagi o'xshashlik (niqobsiz): 0.460 bir xil rasm: asl fon va oq devor orasidagi o'xshashlik (niqob bilan): 1.000
| fon | vektor | bir xil odam | turli odamlar | farq | rank-1 |
|---|---|---|---|---|---|
| asl fon | butun box bo'yicha | 0.655 | 0.35 | 0.306 | 0.684 |
| asl fon | niqob bo'yicha | 0.663 | 0.179 | 0.484 | 0.906 |
| oq devor | butun box bo'yicha | 0.945 | 0.872 | 0.073 | 0.786 |
| oq devor | niqob bo'yicha | 0.663 | 0.179 | 0.484 | 0.906 |
| och pushti devor | butun box bo'yicha | 0.936 | 0.85 | 0.086 | 0.778 |
| och pushti devor | niqob bo'yicha | 0.663 | 0.179 | 0.484 | 0.906 |
Detector ga ham xuddi shu odamlarni devor oldida beramiz (odam kattaroq kulrang yoki devor rangli maydon o'rtasiga qo'yiladi):
detector = YOLO("weights/yolo11n.pt") qatorlar = [] for nom, rang in devorlar.items(): ishonch = [] for _, _, c, m in namuna[::2]: H, W = c.shape[:2] fon = np.full((max(2 * H, 320), max(4 * W, 320), 3), rang if rang else (128, 128, 128), np.uint8) y0, x0 = (fon.shape[0] - H) // 2, (fon.shape[1] - W) // 2 fon[y0:y0 + H, x0:x0 + W] = c if rang is None else niqobla(c, m, rang) b = detector.predict(fon, classes=[0], conf=0.01, verbose=False)[0].boxes ishonch.append(float(b.conf.max()) if len(b) else 0.0) qatorlar.append({"fon": nom, "odamlar": len(ishonch), "o'rtacha ishonch": np.mean(ishonch), "topilmadi (< 0.25)": int((np.array(ishonch) < 0.25).sum())}) show(pd.DataFrame(qatorlar).round(3)) _, axes = plt.subplots(1, 4, figsize=(9, 4)) _, _, c, m = namuna[len(namuna) // 2] for ax, (sarlavha, rasm) in zip(axes, [("asl", c), ("oq devor", niqobla(c, m, (240, 240, 240))), ("och pushti devor", niqobla(c, m, (225, 215, 245))), ("niqob", np.dstack([m * 255] * 3).astype(np.uint8))]): ax.imshow(rasm[:, :, ::-1]) ax.set_title(sarlavha, fontsize=10) ax.axis("off") fig("devor.png", "Bitta odam: asl foni, oq va och pushti devor oldida, va uning niqobi. Niqobdagi oq piksellar — vektor faqat shulardan hisoblanadi.")
| fon | odamlar | o'rtacha ishonch | topilmadi (< 0.25) |
|---|---|---|---|
| asl fon | 73 | 0.884 | 0 |
| oq devor | 73 | 0.919 | 0 |
| och pushti devor | 73 | 0.912 | 0 |

Bitta odam: asl foni, oq va och pushti devor oldida, va uning niqobi. Niqobdagi oq piksellar — vektor faqat shulardan hisoblanadi.
Rasmdagi odam pushti ko'ylakda — och pushti devor oldida ko'ylak va devor deyarli bir rang: aynan savoldagi holat. Niqob esa faqat odamning o'zini ajratib turibdi. Natijalar savolga aniq javob beradi:
- Tanishda devor haqiqiy xavf. Butun box bo'yicha hisoblangan vektor devor oldida "bir xil odam" va "turli odamlar" farqini yo'qotadi — hamma bir-biriga o'xshab qoladi. Bir xil odam asl fonidan oq devor oldiga o'tganda esa o'zi bilan o'xshashligi keskin tushadi. Niqob bo'yicha hisoblangan vektor fonga umuman bog'liq emas — uchala fonda natija aynan bir xil.
- Niqob faqat devor uchun emas: oddiy videoda ham rank-1 ni sezilarli oshirdi (yuqoridagi birinchi jadval). Box ichidagi fon har doim "shovqin".
- Aniqlashda bu sinovda muammo ko'rinmadi — ishonch hatto biroz oshdi. Lekin sinov optimistik: kesib yopishtirilgan odamning chegarasi keskin, haqiqiy oq ko'ylak esa oq devor bilan yorug'likda qo'shilib ketadi. Buni faqat mijozning o'z kamerasidan olingan kadrlarda tekshirish mumkin.
Amaliy retsept — maxsus "oq rang" qoidalarisiz, har qanday fon uchun ishlaydigan:
- Tashqi ko'rinishni faqat odam piksellari bo'yicha hisoblang (segmentatsiya niqobi).
- Tashqi ko'rinishni birinchi dalil emas, qo'shimcha dalil sifatida ishlating: avval harakat va joy "mumkin" desin, keyin rang ikki nomzod orasida tanlasin (pastdagi galereyada aynan shunday).
- Detector ni mijozning kadrlarida qo'shimcha o'qiting, qiyin misollar bilan: och kiyim va och devor (7-bob).
- Kamerani odam devor emas, pol fonida ko'rinadigan qilib o'rnating — III qismdagi joylashtirish qoidalari.
BoT-SORT ning ReID rejimi
botsort.yaml da with_reid: True qilsak, BoT-SORT har box uchun ReID vektorini hisoblaydi va ulash narxiga qo'shadi: IoU narxi va yarim vektor masofasidan kichigini oladi, lekin faqat box lar yetarlicha yaqin bo'lsa (proximity_thresh) va o'xshashlik yuqori bo'lsa (appearance_thresh). 11-bobdagi funksiyalar bilan, bir xil box lar ustida:
def yukla(video): d = np.loadtxt(f"out/mot17/{video}_yolo11n_1280.txt", delimiter=",") return {k: np.c_[d[d[:, 0] == k][:, 2:4], d[d[:, 0] == k][:, 2:4] + d[d[:, 0] == k][:, 4:6], d[d[:, 0] == k][:, 6]] for k in range(1, int(d[:, 0].max()) + 1)} def tracker_sozlama(yaml_nomi, **ozgartirish): s = YAML.load(check_yaml(yaml_nomi)) s.update(ozgartirish) return IterableSimpleNamespace(**s) def ishga_tushir(det, sozlama, kadr_yollari): klass = BOTSORT if sozlama.tracker_type == "botsort" else BYTETracker klass.reset_id() tracker = klass(args=sozlama, frame_rate=30) natija, vaqt = [], 0.0 for k in sorted(det): kadr = cv2.imread(str(kadr_yollari[k - 1])) t0 = time.perf_counter() treklar = tracker.update(Boxes(np.c_[det[k], np.zeros(len(det[k]))], (1080, 1920)), kadr) vaqt += time.perf_counter() - t0 natija += [(k, int(t[4]), *t[:4]) for t in treklar] return np.array(natija), vaqt / len(det) * 1000 def iou_matritsa(a, b): return torchvision.ops.box_iou(torch.tensor(np.asarray(a)[:, :4], dtype=torch.float64), torch.tensor(np.asarray(b)[:, :4], dtype=torch.float64)).numpy() def id_tahlili(trek, piyoda): """9-11-boblardagi kabi (CLEAR-MOT qoidasi).""" idlar, oxirgi, almashish, topildi = {}, {}, 0, 0 for k in np.unique(piyoda[:, 0]).astype(int): G, P = piyoda[piyoda[:, 0] == k], trek[trek[:, 0] == k] if not len(P): continue M = iou_matritsa(np.c_[G[:, 2], G[:, 3], G[:, 2] + G[:, 4], G[:, 3] + G[:, 5]], P[:, 2:6]) juftlar, band_g, band_p = [], set(), set() for a in range(len(G)): b = np.nonzero(P[:, 1] == oxirgi.get(int(G[a, 1]), -1))[0] if len(b) and b[0] not in band_p and M[a, b[0]] >= 0.5: juftlar.append((a, b[0])) band_g.add(a) band_p.add(b[0]) for a, b in sorted(zip(*np.nonzero(M >= 0.5)), key=lambda juft: -M[juft]): if a not in band_g and b not in band_p: juftlar.append((a, b)) band_g.add(a) band_p.add(b) for a, b in juftlar: gid, pid = int(G[a, 1]), int(P[b, 1]) topildi += 1 idlar.setdefault(gid, []).append((k, pid)) if gid in oxirgi and oxirgi[gid] != pid: almashish += 1 oxirgi[gid] = pid return idlar, almashish, topildi def jadval_qatori(nom, trek, ms, piyoda): idlar, almashish, topildi = id_tahlili(trek, piyoda) return {"tracker": nom, "ID lar": len(np.unique(trek[:, 1])), "ID almashishi": almashish, "bir odamga ID": np.mean([len({p for _, p in v}) for v in idlar.values()]), "topilgan GT, %": topildi / len(piyoda) * 100, "tracker, ms/kadr": ms} qatorlar = [] for video, gmc in (("MOT17-09", "none"), ("MOT17-10", "sparseOptFlow")): papka = MOT / f"{video}-FRCNN" g = np.loadtxt(papka / "gt" / "gt.txt", delimiter=",") p = g[(g[:, 7] == 1) & (g[:, 6] == 1)] det, yollar = yukla(video), sorted((papka / "img1").iterdir()) for nom, s in (("BoT-SORT", tracker_sozlama("botsort.yaml", gmc_method=gmc)), ("BoT-SORT + ReID", tracker_sozlama("botsort.yaml", gmc_method=gmc, with_reid=True, model="weights/yolo26n-cls.pt"))): qatorlar.append({"video": video, **jadval_qatori(nom, *ishga_tushir(det, s, yollar), p)}) show(pd.DataFrame(qatorlar).round(2))
| video | tracker | ID lar | ID almashishi | bir odamga ID | topilgan GT, % | tracker, ms/kadr |
|---|---|---|---|---|---|---|
| MOT17-09 | BoT-SORT | 110 | 63 | 3.32 | 72.45 | 1.33 |
| MOT17-09 | BoT-SORT + ReID | 137 | 89 | 3.84 | 73.07 | 26.89 |
| MOT17-10 | BoT-SORT | 147 | 108 | 2.61 | 62.08 | 13.46 |
| MOT17-10 | BoT-SORT + ReID | 205 | 154 | 3.26 | 64.32 | 46.58 |
ReID ikkala videoda ham yomonlashtirdi: ID almashishlari va ID lar ko'paydi, tracker esa kadriga 27–47 ms ga sekinlashdi (har box uchun alohida neyron tarmoq). Sababi yuqoridagi jadvalda: bu vektor turli odamlarni yomon ajratadi, BoT-SORT esa IoU narxi va vektor masofasidan kichigini oladi — yomon vektor noto'g'ri, lekin "arzon" juftliklar yaratadi. Standart botsort.yaml da with_reid: False bo'lishi bejiz emas. Xulosa: tashqi ko'rinish faqat yaxshi vektor bilan foyda beradi, yomon vektor bilan esa zarar qiladi.
Yo'qolganlar galereyasi
Endi talabaning savoliga javob beradigan tizimni quramiz. ByteTrack ustiga qo'shimcha qatlam:
- Har bir trek uchun niqob bo'yicha rang vektorlari yig'iladi (faqat ishonchli box lardan, ishonch ≥ 0.5).
- Trek ByteTrack ning o'z xotirasidan (30 kadr) ham uzoqroq ko'rinmasa, u galereyaga o'tadi: o'rtacha vektori, oxirgi kadri va oxirgi joyi bilan. Oxirgi joyidan "qayerdan chiqib ketdi" aniqlanadi: kadr chetida bo'lsa — o'sha chetdan chiqdi, ichida bo'lsa — to'silib qoldi.
- Yangi ID paydo bo'lsa, uning birinchi 5 ta vektori o'rtachasi galereyadagi so'nggi 10 soniyada yo'qolganlar bilan solishtiriladi. Uch shart bajarilsa, eski ID qaytariladi:
- o'xshashlik chegarasi — qo'lda tanlangan son emas: yuqoridagi o'lchovdan, turli odamlar o'xshashligining 99-persentili (ya'ni turli odamlarning atigi 1% i shunchalik o'xshash);
- yetib kelish mumkinmi — yo'qolgan joyidan yangi joyigacha masofa, odam bo'yi birligida, 1.5 bo'y/soniya tezlikdan oshmasin. Piyoda taxminan 0.8 bo'y/soniya yuradi; bo'y birligi esa kameradan uzoq-yaqinligidan qat'iy nazar ishlaydi;
- ikkilanmaslik — eng yaxshi nomzod ikkinchisidan kamida 0.1 ga yuqori bo'lsin.
Avval ByteTrack ni segmentatsiya modeli bilan MOT17-09 da yuritamiz va har bir natija qatoriga niqobli rang vektorini yozamiz:
bt = tracker_sozlama("bytetrack.yaml") BYTETracker.reset_id() tracker = BYTETracker(args=bt, frame_rate=30) oqim = [] # (kadr, id, x1, y1, x2, y2, vektor yoki None) t7_tekshiruv = Counter() # t[7] indeksi haqiqatan shu trek box iga ishora qiladimi for k, yol in enumerate(sorted((V09 / "img1").iterdir()), 1): kadr = cv2.imread(str(yol)) r = seg.predict(kadr, classes=[0], conf=0.1, imgsz=1280, retina_masks=True, device=0, verbose=False)[0] b = r.boxes.cpu().numpy() niqoblar = r.masks.data.cpu().numpy() > 0.5 if r.masks is not None else None treklar = tracker.update(b, kadr) mos = iou_matritsa(treklar[:, :4], b.xyxy) if len(treklar) and len(b) else None for i, t in enumerate(treklar): bosqich = "1-bosqich (ishonch >= 0.25)" if t[5] >= 0.25 else "2-bosqich (0.1-0.25)" t7_tekshiruv[bosqich, bool(mos[i, int(t[7])] >= 0.5)] += 1 x1, y1, x2, y2 = np.clip(t[:4], 0, [1919, 1079, 1919, 1079]).astype(int) j, v = int(mos[i].argmax()), None # trek ning box i — IoU bo'yicha, t[7] bo'yicha emas if niqoblar is not None and mos[i, j] >= 0.7 and b.conf[j] >= 0.5 and x2 - x1 > 10 and y2 - y1 > 30: v = hsv_vektor(kadr[y1:y2, x1:x2], niqoblar[j][y1:y2, x1:x2]) v = v / np.linalg.norm(v) if v.any() else None oqim.append((k, int(t[4]), *t[:4], v)) asos = np.array([o[:6] for o in oqim], float) print("ByteTrack (segmentatsiya modeli box lari):", len(np.unique(asos[:, 1])), "ta ID") for bosqich in ("1-bosqich (ishonch >= 0.25)", "2-bosqich (0.1-0.25)"): print(f"{bosqich}: t[7] to'g'ri box ga ishora qiladi {t7_tekshiruv[bosqich, True]}, noto'g'ri {t7_tekshiruv[bosqich, False]}")
ByteTrack (segmentatsiya modeli box lari): 120 ta ID 1-bosqich (ishonch >= 0.25): t[7] to'g'ri box ga ishora qiladi 5235, noto'g'ri 20 2-bosqich (0.1-0.25): t[7] to'g'ri box ga ishora qiladi 1, noto'g'ri 472
Kodda bitta nozik joy bor. Tracker natijasining 8-ustuni (t[7]) — "shu trek qaysi box ga ulangan" degan raqam. Lekin ultralytics (8.4.17) da u box larning filtrlangan ro'yxatidagi raqam: birinchi bosqich uchun bu ro'yxat asl ro'yxatning boshi bilan deyarli bir xil (box lar ishonch bo'yicha saralangan; bir nechta istisno — trek ning Kalman bilan silliqlangan box i detector box idan uzoqlashgan holatlar), ikkinchi bosqich uchun esa butunlay boshqa ro'yxat. Oxirgi ikki qatorga qarang: ikkinchi bosqichda ulangan trek larda t[7] noto'g'ri box ni ko'rsatadi. Agar niqobni t[7] bo'yicha olsak, bu trek larga boshqa odamning niqobi va rangi yopishib qoladi. Shuning uchun box ni IoU bo'yicha o'zimiz topamiz. Xuddi shu indeks ultralytics ning o'z track.py sida segmentatsiya va poza natijalarini trek larga bog'lash uchun ishlatiladi — segmentatsiya yoki poza modeli bilan tracking qilganda bunga e'tibor bering.
Galereyaning o'zi:
def chiqish_joyi(box, W=1920, H=1080, chet=0.03): x1, y1, x2, y2 = box if x1 < chet * W: return "chap chetdan chiqdi" if x2 > (1 - chet) * W: return "o'ng chetdan chiqdi" if y2 > (1 - chet) * H: return "pastki chetdan chiqdi" return "kadr ichida yo'qoldi (to'silish)" def galereya(oqim, chegara, tezlik=1.5, fps=30, farq=0.1, n_qaror=5, kutish=30, oyna=300): """Yangi ID larni yo'qolganlar bilan solishtirib, mos kelsa eski ID ni qaytaradi. Qaytaradi: yangi trek jadvali, hodisalar.""" kadr_boyicha = defaultdict(list) for o in oqim: kadr_boyicha[o[0]].append(o) trek, asl_id, galer, hodisalar, kutilmoqda = {}, {}, {}, [], set() for k in sorted(kadr_boyicha): for _, tid, x1, y1, x2, y2, v in kadr_boyicha[k]: if tid not in trek: trek[tid] = {"vek": [], "bosh": k, "oxir": k, "box": (x1, y1, x2, y2), "bosh_box": (x1, y1, x2, y2)} kutilmoqda.add(tid) t = trek[tid] t["oxir"], t["box"] = k, (x1, y1, x2, y2) if v is not None: t["vek"].append(v) if tid in kutilmoqda and (len(t["vek"]) >= n_qaror or k - t["bosh"] >= 15): kutilmoqda.discard(tid) asl_id[tid] = tid if t["vek"] and galer: q = np.mean(t["vek"][:n_qaror], 0) q /= np.linalg.norm(q) bx, by, bh = (t["bosh_box"][0] + t["bosh_box"][2]) / 2, t["bosh_box"][3], t["bosh_box"][3] - t["bosh_box"][1] nomzodlar = [] for gid, g in galer.items(): dt = t["bosh"] - g["oxir"] gx, gy, gh = (g["box"][0] + g["box"][2]) / 2, g["box"][3], g["box"][3] - g["box"][1] if 0 < dt <= oyna and np.hypot(bx - gx, by - gy) / max(bh, gh) <= tezlik * dt / fps + 0.5: nomzodlar.append((float(q @ g["vek"]), gid)) # oyoq nuqtalari orasidagi masofa, bo'y birligida nomzodlar.sort(reverse=True) if nomzodlar and nomzodlar[0][0] >= chegara and (len(nomzodlar) == 1 or nomzodlar[0][0] - nomzodlar[1][0] >= farq): s, gid = nomzodlar[0] g = galer.pop(gid) asl_id[tid] = gid hodisalar.append({"qaytgan ID": tid, "tanildi": gid, "yo'qolgan kadr": g["oxir"], "joyi": g["joy"], "qaytgan kadr": t["bosh"], "tanaffus, s": (t["bosh"] - g["oxir"]) / fps, "o'xshashlik": s}) for tid, t in trek.items(): # uzoq ko'rinmaganlar galereyaga if tid not in kutilmoqda and k - t["oxir"] == kutish and t["vek"]: v = np.mean(t["vek"][-30:], 0) galer[asl_id[tid]] = {"vek": v / np.linalg.norm(v), "oxir": t["oxir"], "box": t["box"], "joy": chiqish_joyi(t["box"])} yangi = np.array([(o[0], asl_id.get(o[1], o[1]), *o[2:6]) for o in oqim], float) return yangi, hodisalar tau = chegara["rang gistogrammasi", True] yangi, hodisalar = galereya(oqim, tau) print(f"o'xshashlik chegarasi (turli odamlar, 99-persentil): {tau:.3f} | qaytarilgan ID lar: {len(hodisalar)}") show(pd.DataFrame([jadval_qatori("ByteTrack", asos, 0, piyoda), jadval_qatori("ByteTrack + galereya", yangi, 0, piyoda)]) .drop(columns="tracker, ms/kadr").round(2))
o'xshashlik chegarasi (turli odamlar, 99-persentil): 0.644 | qaytarilgan ID lar: 16
| tracker | ID lar | ID almashishi | bir odamga ID | topilgan GT, % |
|---|---|---|---|---|
| ByteTrack | 120 | 57 | 2.96 | 72.68 |
| ByteTrack + galereya | 104 | 53 | 2.73 | 72.81 |
ID lar kamaydi. Lekin bu yaxshilanishmi? Ikki turli odamni bitta ID ga "yopishtirish" ham ID lar sonini kamaytiradi — va bu eng yomon xato: davomat jurnalida bir odam o'rniga ikkinchisi yozilib qoladi. Shuning uchun har bir qaytarishni GT bo'yicha tekshiramiz: har bir tracker ID si ko'pchilik kadrlarda qaysi haqiqiy odamga to'g'ri kelgan?
ovoz = defaultdict(Counter) for k, tid, x1, y1, x2, y2, _ in oqim: G = piyoda[piyoda[:, 0] == k] m = iou_matritsa([[x1, y1, x2, y2]], np.c_[G[:, 2], G[:, 3], G[:, 2] + G[:, 4], G[:, 3] + G[:, 5]])[0] if len(G) else np.zeros(1) ovoz[tid][int(G[m.argmax(), 1]) if len(G) and m.max() >= 0.5 else -1] += 1 egasi = {t: c.most_common(1)[0][0] for t, c in ovoz.items()} # -1: hisobga olinadigan piyoda emas (aks, turgan odam, soxta box) def tekshir(hodisalar): natija = Counter() for h in hodisalar: a, b = egasi[h["qaytgan ID"]], egasi[h["tanildi"]] natija["to'g'ri" if a == b != -1 else "piyoda emas" if -1 in (a, b) else "boshqa odam"] += 1 return natija qatorlar = [] for nom, kw in (("faqat o'xshashlik", dict(tezlik=1e9, farq=0.0, n_qaror=3)), ("+ yetib kelish + ikkilanmaslik", {})): for c in (tau, 0.4, 0.6): _, h = galereya(oqim, c, **kw) t = tekshir(h) qatorlar.append({"qoidalar": nom, "chegara": c, "qaytarishlar": len(h), "to'g'ri": t["to'g'ri"], "boshqa odamga": t["boshqa odam"], "piyoda emas": t["piyoda emas"]}) show(pd.DataFrame(qatorlar).round(3))
| qoidalar | chegara | qaytarishlar | to'g'ri | boshqa odamga | piyoda emas |
|---|---|---|---|---|---|
| faqat o'xshashlik | 0.644 | 25 | 7 | 6 | 12 |
| faqat o'xshashlik | 0.4 | 40 | 8 | 18 | 14 |
| faqat o'xshashlik | 0.6 | 26 | 7 | 6 | 13 |
| + yetib kelish + ikkilanmaslik | 0.644 | 16 | 4 | 4 | 8 |
| + yetib kelish + ikkilanmaslik | 0.4 | 25 | 5 | 9 | 11 |
| + yetib kelish + ikkilanmaslik | 0.6 | 19 | 4 | 5 | 10 |
Halol natija: bu videoda rang bo'yicha galereya ishonchli emas. Haqiqiy piyodalar orasidagi qaytarishlarning taxminan yarmi to'g'ri, yarmi — boshqa odamga. Yana ko'p qaytarishlar "piyoda emas" trek larga tegishli: MOT17-09 da do'kon oynasidagi akslar va joyida turgan odamlar ham trek hosil qiladi. Yetib kelish va ikkilanmaslik shartlari xato qaytarishlarni kamaytirdi, lekin to'g'rilarini ham — aniqlik (to'g'ri / piyodalar bo'yicha jami) deyarli o'zgarmadi. Sabab ikkita: bu sahnada ko'pchilik qora-kulrang kiyimda, va kadr chetidan endi kirgan odamning birinchi 5 kadri — uning yarim ko'rinishi. Rang vektori odamni emas, kiyim rangini taniydi.
Muhandislik xulosasi: davomat yoki xavfsizlik jurnalida "qaytgan odamni o'tkazib yuborish" — "ikki odamni bitta deb yozish" dan ancha kichik gunoh. Shuning uchun bunday galereya faqat ishonchli vektor bilan (maxsus ReID modeli) va qat'iy chegara bilan ishlatiladi, "aynan shu odam" kafolati esa boshqa manbadan keladi — yuz yoki kirishdagi karta (13-bob).
Va nihoyat — talabaning savoliga javob: jurnal. Kim qachon, qayerdan yo'qoldi va qachon qaytib tanildi:
jurnal = pd.DataFrame(hodisalar) jurnal["GT bo'yicha"] = ["to'g'ri" if egasi[a] == egasi[b] != -1 else "piyoda emas" if -1 in (egasi[a], egasi[b]) else "XATO" for a, b in zip(jurnal["qaytgan ID"], jurnal["tanildi"])] show(jurnal.round(2), 20)
| qaytgan ID | tanildi | yo'qolgan kadr | joyi | qaytgan kadr | tanaffus, s | o'xshashlik | GT bo'yicha |
|---|---|---|---|---|---|---|---|
| 35 | 8 | 20 | kadr ichida yo'qoldi (to'silish) | 39 | 0.63 | 0.86 | piyoda emas |
| 82 | 1 | 117 | kadr ichida yo'qoldi (to'silish) | 152 | 1.17 | 0.79 | to'g'ri |
| 101 | 5 | 144 | kadr ichida yo'qoldi (to'silish) | 172 | 0.93 | 0.9 | to'g'ri |
| 107 | 21 | 104 | kadr ichida yo'qoldi (to'silish) | 175 | 2.37 | 0.74 | to'g'ri |
| 148 | 34 | 102 | chap chetdan chiqdi | 209 | 3.57 | 0.68 | piyoda emas |
| 187 | 8 | 141 | kadr ichida yo'qoldi (to'silish) | 233 | 3.07 | 0.9 | piyoda emas |
| 195 | 74 | 189 | kadr ichida yo'qoldi (to'silish) | 238 | 1.63 | 0.83 | piyoda emas |
| 245 | 26 | 26 | o'ng chetdan chiqdi | 275 | 8.3 | 0.71 | XATO |
| 248 | 111 | 266 | kadr ichida yo'qoldi (to'silish) | 276 | 0.33 | 0.71 | XATO |
| 299 | 52 | 179 | kadr ichida yo'qoldi (to'silish) | 318 | 4.63 | 0.76 | piyoda emas |
| 337 | 150 | 298 | kadr ichida yo'qoldi (to'silish) | 349 | 1.7 | 0.79 | to'g'ri |
| 373 | 26 | 340 | kadr ichida yo'qoldi (to'silish) | 367 | 0.9 | 0.85 | XATO |
| 412 | 156 | 222 | kadr ichida yo'qoldi (to'silish) | 388 | 5.53 | 0.79 | piyoda emas |
| 477 | 74 | 309 | kadr ichida yo'qoldi (to'silish) | 444 | 4.5 | 0.8 | piyoda emas |
| 517 | 6 | 222 | chap chetdan chiqdi | 482 | 8.67 | 0.76 | XATO |
| 507 | 179 | 362 | chap chetdan chiqdi | 473 | 3.7 | 0.9 | piyoda emas |
Jurnal aynan so'ralgan savollarga javob beradi: kim, qachon, qayerdan (chetdan chiqdimi yoki to'silib qoldimi) va qancha vaqtdan keyin qaytdi. Oxirgi ustun esa uni tekshirish qanchalik muhimligini ko'rsatadi: "XATO" qatorlari — jurnalga yozilgan, lekin noto'g'ri tanilgan qaytishlar. Real tizimda bunday ustun bo'lmaydi — shuning uchun ishga tushirishdan oldin mijozning o'z kadrlarida qo'lda belgilangan qism bilan xuddi shunday tekshiruv o'tkaziladi.
Asosiy fikr
- ReID — odam rasmidan vektor va kosinus o'xshashlik. Standart o'lchov — rank-1. Mashhur katta modellar (CLIP, klassifikatorlar) avtomatik ravishda yaxshi odam vektori emas: ular "nima" ni o'rgangan, "kim" ni emas. Bu videoda niqob bo'yicha oddiy rang gistogrammasi ulardan yaxshiroq chiqdi.
- Box ichidagi fon — shovqin. Oq devor va oq ko'ylak kabi holatlarda u tanishni butunlay buzadi. Yechim — vektorni segmentatsiya niqobi bo'yicha hisoblash, maxsus rang qoidalari emas.
- "ID lar kamaydi" — hali yaxshilanish emas: noto'g'ri birlashtirish ham ID larni kamaytiradi. Har bir qaytarishni tekshiring. Bu videoda rang bo'yicha galereya qaytarishlarining yarmi xato chiqdi, BoT-SORT ning standart ReID i esa tracking ni yomonlashtirdi. "Aynan shu odam" degan kafolat uchun yuz yoki kirishdagi karta kerak — 13-bob.
Mashqlar
galereyadatezlikni 0.8 (oddiy yurish) va 3.0 ga o'zgartiring. To'g'ri va noto'g'ri qaytarishlar qanday o'zgaradi?- Rang gistogrammasiga uchinchi qism — bosh (bo'yning 0–15%) — qo'shing. Soch va bosh kiyim yordam beradimi yoki halaqit beradimi? Rank-1 bilan tekshiring.
- Galereyani MOT17-10 da (kamera harakatda, kechqurun) ishga tushiring. Qorong'ida rang vektori qanchalik ishonchli? Nima uchun?
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.