Asosiy qismga o'tish

ReID: ketib qaytgan odamni tanish

12-bob 10 kod bloki 6858 so'z

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:

  1. Uch xil vektorni real MOT17 odamlarida o'lchaymiz va qaysi biri odamni yaxshiroq ajratishini aniqlaymiz.
  2. 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.
  3. BoT-SORT ning o'z ReID rejimini sinaymiz.
  4. Talabaning savoliga javob beradigan tizim: "yo'qolganlar galereyasi" — kim qachon, qayerdan chiqib ketdi va kim qaytib keldi.
kod/12_reid_galereya.pyyangi fayl · 1-qator: from kitob_yordam import fig, show
python22 qator
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:

kod/12_reid_galereya.pydavomi
python31 qator
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)
Chiqish
rasmlar: 146 | odamlar: 23 | 'bir xil odam' juftlari: 322

Uch xil vektor:

  • Ultralytics ning standart ReID modeli — BoT-SORT with_reid yoqilganda ishlatadigan yolo26n-cls klassifikatori, oxiridan oldingi qatlami (trackers/bot_sort.py dagi ReID klassi).
  • 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.

kod/12_reid_galereya.pydavomi
python43 qator
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))
vektorniqobo'lchambir xil odamturli odamlarrank-1ms/rasm
ultralytics ReID (yolo26n-cls)yo'q2560.9370.8850.6841.211
ultralytics ReID (yolo26n-cls)ha2560.950.9040.7011.073
CLIP ViT-B/32yo'q5120.8740.7990.6672.536
CLIP ViT-B/32ha5120.9160.8520.7012.566
rang gistogrammasiyo'q2560.6550.350.6840.116
rang gistogrammasiha2560.6630.1790.9060.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:

kod/12_reid_galereya.pydavomi
python15 qator
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}")
Chiqish
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
fonvektorbir xil odamturli odamlarfarqrank-1
asl fonbutun box bo'yicha0.6550.350.3060.684
asl fonniqob bo'yicha0.6630.1790.4840.906
oq devorbutun box bo'yicha0.9450.8720.0730.786
oq devorniqob bo'yicha0.6630.1790.4840.906
och pushti devorbutun box bo'yicha0.9360.850.0860.778
och pushti devorniqob bo'yicha0.6630.1790.4840.906

Detector ga ham xuddi shu odamlarni devor oldida beramiz (odam kattaroq kulrang yoki devor rangli maydon o'rtasiga qo'yiladi):

kod/12_reid_galereya.pydavomi
python23 qator
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.")
fonodamlaro'rtacha ishonchtopilmadi (< 0.25)
asl fon730.8840
oq devor730.9190
och pushti devor730.9120

Bitta odam: asl foni, oq va och pushti devor oldida, va uning niqobi. Niqobdagi oq piksellar — vektor faqat shulardan hisoblanadi.

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:

  1. Tashqi ko'rinishni faqat odam piksellari bo'yicha hisoblang (segmentatsiya niqobi).
  2. 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).
  3. Detector ni mijozning kadrlarida qo'shimcha o'qiting, qiyin misollar bilan: och kiyim va och devor (7-bob).
  4. 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:

kod/12_reid_galereya.pydavomi
python72 qator
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))
videotrackerID larID almashishibir odamga IDtopilgan GT, %tracker, ms/kadr
MOT17-09BoT-SORT110633.3272.451.33
MOT17-09BoT-SORT + ReID137893.8473.0726.89
MOT17-10BoT-SORT1471082.6162.0813.46
MOT17-10BoT-SORT + ReID2051543.2664.3246.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:

  1. Har bir trek uchun niqob bo'yicha rang vektorlari yig'iladi (faqat ishonchli box lardan, ishonch ≥ 0.5).
  2. 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.
  3. 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:
  4. 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);
  5. 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;
  6. 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:

kod/12_reid_galereya.pydavomi
python25 qator
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]}")
Chiqish
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:

kod/12_reid_galereya.pydavomi
python57 qator
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))
Chiqish
o'xshashlik chegarasi (turli odamlar, 99-persentil): 0.644 | qaytarilgan ID lar: 16
trackerID larID almashishibir odamga IDtopilgan GT, %
ByteTrack120572.9672.68
ByteTrack + galereya104532.7372.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?

kod/12_reid_galereya.pydavomi
python22 qator
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))
qoidalarchegaraqaytarishlarto'g'riboshqa odamgapiyoda emas
faqat o'xshashlik0.644257612
faqat o'xshashlik0.44081814
faqat o'xshashlik0.6267613
+ yetib kelish + ikkilanmaslik0.64416448
+ yetib kelish + ikkilanmaslik0.4255911
+ yetib kelish + ikkilanmaslik0.6194510

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:

kod/12_reid_galereya.pydavomi
python4 qator
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 IDtanildiyo'qolgan kadrjoyiqaytgan kadrtanaffus, so'xshashlikGT bo'yicha
35820kadr ichida yo'qoldi (to'silish)390.630.86piyoda emas
821117kadr ichida yo'qoldi (to'silish)1521.170.79to'g'ri
1015144kadr ichida yo'qoldi (to'silish)1720.930.9to'g'ri
10721104kadr ichida yo'qoldi (to'silish)1752.370.74to'g'ri
14834102chap chetdan chiqdi2093.570.68piyoda emas
1878141kadr ichida yo'qoldi (to'silish)2333.070.9piyoda emas
19574189kadr ichida yo'qoldi (to'silish)2381.630.83piyoda emas
2452626o'ng chetdan chiqdi2758.30.71XATO
248111266kadr ichida yo'qoldi (to'silish)2760.330.71XATO
29952179kadr ichida yo'qoldi (to'silish)3184.630.76piyoda emas
337150298kadr ichida yo'qoldi (to'silish)3491.70.79to'g'ri
37326340kadr ichida yo'qoldi (to'silish)3670.90.85XATO
412156222kadr ichida yo'qoldi (to'silish)3885.530.79piyoda emas
47774309kadr ichida yo'qoldi (to'silish)4444.50.8piyoda emas
5176222chap chetdan chiqdi4828.670.76XATO
507179362chap chetdan chiqdi4733.70.9piyoda 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

  1. galereya da tezlik ni 0.8 (oddiy yurish) va 3.0 ga o'zgartiring. To'g'ri va noto'g'ri qaytarishlar qanday o'zgaradi?
  2. Rang gistogrammasiga uchinchi qism — bosh (bo'yning 0–15%) — qo'shing. Soch va bosh kiyim yordam beradimi yoki halaqit beradimi? Rank-1 bilan tekshiring.
  3. 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.

Telegramda yozish Narxlar