Tracking: "bu o'sha odammi?"
Bu bobning kodi: kod/09_tracking_nima.py — kitob papkasidan ishga tushiring: python kod/09_tracking_nima.py
II qism: detector xotirasiz
1-bobdagi do'konni eslang: kassa oldida 1 daqiqa turgan bitta xaridor 25 × 60 = 1 500 kadrda topiladi, va box larni sanasak 1 500 ta "odam" chiqadi. Detector har kadrga alohida qaraydi va oldingi kadrni eslamaydi.
Tracking (kuzatish) shu bo'shliqni to'ldiradi: har bir box ga raqam (ID) beradi va o'sha odam keyingi kadrlarda ham o'sha raqam bilan qoladi. Natija — kadrlar bo'ylab cho'zilgan trek: "17-odam: 1-kadrdan 490-kadrgacha, mana bu joylardan o'tdi". Shundan keyingina "nechta odam kirdi", "qancha turdi", "qaysi eshikdan chiqdi" degan savollarga javob berish mumkin.
Tracker ikki ishni qiladi:
- Ulash (association): yangi kadrdagi box lardan qaysi biri qaysi eski trekning davomi?
- Trek boshqaruvi: qachon yangi trek ochiladi, qachon eski trek yopiladi, to'silgan odamni qancha kutish kerak?
Bu bobda muammoni real video da o'lchaymiz va eng oddiy tracker ni yozamiz. U juda yomon ishlaydi — va aynan qayerda yomonligi keyingi boblarning rejasini beradi.
MOT17: kuzatish uchun etalon
Tracker larni solishtirish uchun hamma bir xil videoda, bir xil qo'lda belgilangan javob bilan o'lchaydi. Eng mashhuri — MOT17 (MOTChallenge). U "MOT16: A Benchmark for Multi-Object Tracking" maqolasidagi (Milan, Leal-Taixé, Reid, Roth, Schindler, 2016, arXiv 1603.00831) videolarni aniqroq belgilangan javob bilan beradi. Litsenziyasi CC BY-NC-SA 3.0: o'rganish va tadqiqot uchun bepul, tijorat uchun emas. Unda o'qitilgan modelni sotib bo'lmaydi — IV qismda buni alohida ko'ramiz.
Yuklab olish (~5.9 GB) va faqat kerakli qismni ochish. Arxivda har bir video uch marta takrorlangan (DPM, FRCNN, SDP — uch xil tayyor detector natijasi bilan). Bizga rasmlar va javoblar bir marta kerak, shuning uchun faqat train qismidagi FRCNN papkalarini olamiz (~850 MB):
# MOT17 (9-bob): 5.9 GB arxivdan faqat train qismidagi FRCNN papkalari (~850 MB) if not (root / "MOT17" / "train").exists(): download("https://motchallenge.net/data/MOT17.zip", dir=root, unzip=False) with zipfile.ZipFile(root / "MOT17.zip") as z: kerak = [n for n in z.namelist() if n.startswith("MOT17/train/") and "-FRCNN/" in n] z.extractall(root, kerak) (root / "MOT17.zip").unlink() # 5.9 GB arxiv endi kerak emas
Arxiv katta — internet sekin bo'lsa, uni terminalda uzilsa davom ettiradigan buyruq bilan yuklab, keyin yuqoridagi faylni ishga tushirish ham mumkin (u tayyor arxivni ko'rsa, faqat ochadi va o'chiradi — yuklab olish qatori fayl bor bo'lsa takrorlanmaydi):
curl -C - -o datasets/MOT17.zip https://motchallenge.net/data/MOT17.zip
Yettita video. Har birining seqinfo.ini fayli va gt/gt.txt javob fayli bor. O'zimiz sanaymiz va MOTChallenge saytidagi jadval bilan solishtiramiz:
import configparser import time 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.utils import LOGGER LOGGER.setLevel("ERROR") MOT = Path("datasets/MOT17/train") sayt = {"02": (62, 18581, "qo'zg'almas"), "04": (83, 47557, "qo'zg'almas"), "05": (133, 6917, "harakatda"), "09": (26, 5325, "qo'zg'almas"), "10": (57, 12839, "harakatda"), "11": (75, 9436, "harakatda"), "13": (110, 11642, "harakatda")} # motchallenge.net/data/MOT17 jadvalidan qatorlar = [] for papka in sorted(MOT.iterdir()): ini = configparser.ConfigParser() ini.read(papka / "seqinfo.ini") s = ini["Sequence"] gt = np.loadtxt(papka / "gt" / "gt.txt", delimiter=",") piyoda = gt[(gt[:, 7] == 1) & (gt[:, 6] == 1)] # 7-ustun: sinf (1 = piyoda), 6-ustun: hisobga olinadimi raqam = papka.name[6:8] qatorlar.append({"video": papka.name[:8], "fps": int(s["frameRate"]), "o'lcham": f"{s['imWidth']}x{s['imHeight']}", "kadrlar": int(s["seqLength"]), "treklar": len(np.unique(piyoda[:, 1])), "sayt: treklar": sayt[raqam][0], "box lar": len(piyoda), "sayt: box lar": sayt[raqam][1], "kamera": sayt[raqam][2]}) show(pd.DataFrame(qatorlar))
| video | fps | o'lcham | kadrlar | treklar | sayt: treklar | box lar | sayt: box lar | kamera |
|---|---|---|---|---|---|---|---|---|
| MOT17-02 | 30 | 1920x1080 | 600 | 62 | 62 | 18 581 | 18 581 | qo'zg'almas |
| MOT17-04 | 30 | 1920x1080 | 1 050 | 83 | 83 | 47 557 | 47 557 | qo'zg'almas |
| MOT17-05 | 14 | 640x480 | 837 | 133 | 133 | 6 917 | 6 917 | harakatda |
| MOT17-09 | 30 | 1920x1080 | 525 | 26 | 26 | 5 325 | 5 325 | qo'zg'almas |
| MOT17-10 | 30 | 1920x1080 | 654 | 57 | 57 | 12 839 | 12 839 | harakatda |
| MOT17-11 | 30 | 1920x1080 | 900 | 75 | 75 | 9 436 | 9 436 | harakatda |
| MOT17-13 | 25 | 1920x1080 | 750 | 110 | 110 | 11 642 | 11 642 | harakatda |
Har bir raqam saytdagi bilan bir xil — ma'lumotni to'g'ri o'qiyapmiz. Yettita videoning to'rttasida kamera harakatlanadi (qo'lda yoki mashinada): u holda odam joyida tursa ham, kadrda uning box i siljiydi. Buni 11-bobda ko'ramiz.
gt.txt formati
gt_yol = MOT / "MOT17-09-FRCNN" / "gt" / "gt.txt" print("".join(open(gt_yol).readlines()[:3])) gt = np.loadtxt(gt_yol, delimiter=",") sinf_nomi = {1: "piyoda", 2: "transportdagi odam", 3: "mashina", 4: "velosiped", 5: "mototsikl", 6: "motorsiz transport", 7: "turgan odam", 8: "chalg'ituvchi", 9: "to'suvchi", 10: "yerdagi to'suvchi", 11: "to'liq to'suvchi", 12: "aks"} show(pd.DataFrame([{"sinf": int(k), "nomi": sinf_nomi[int(k)], "qatorlar": int((gt[:, 7] == k).sum()), "hisobga olinadi (6-ustun = 1)": int(((gt[:, 7] == k) & (gt[:, 6] == 1)).sum())} for k in np.unique(gt[:, 7])]))
1,1,260,450,102,262,1,1,1 2,1,262,449,102,263,1,1,1 3,1,264,449,102,263,1,1,1
| sinf | nomi | qatorlar | hisobga olinadi (6-ustun = 1) |
|---|---|---|---|
| 1 | piyoda | 5 325 | 5 325 |
| 7 | turgan odam | 514 | 0 |
| 8 | chalg'ituvchi | 1 575 | 0 |
| 9 | to'suvchi | 1 050 | 0 |
| 12 | aks | 1 947 | 0 |
Har qator — bitta kadrdagi bitta obyekt:
| Ustun | Ma'nosi |
|---|---|
| 1 | kadr raqami (1 dan) |
| 2 | trek ID si |
| 3–6 | box: chap-yuqori x, y, kenglik, balandlik — 1-bobdagi tlwh formati |
| 7 | hisobga olinadimi (1 — ha, 0 — e'tiborsiz) |
| 8 | sinf |
| 9 | ko'rinish ulushi: 1 — to'liq ko'rinadi, 0 — butunlay to'silgan |
Baholashda faqat hisobga olinadigan piyodalar ishlatiladi. Qolganlari (turgan odam, oynadagi aks, to'suvchi ustun) belgilangan, lekin tracker ularni topsa ham, topmasa ham jazolanmaydi. Sinf nomlari MOT16 maqolasidagi jadvaldan.
Butun videodagi yo'llar — har bir odam oyog'ining o'rtasi, kadrdan kadrga:
piyoda = gt[(gt[:, 7] == 1) & (gt[:, 6] == 1)] birinchi = cv2.imread(str(MOT / "MOT17-09-FRCNN" / "img1" / "000001.jpg"))[:, :, ::-1] _, ax = plt.subplots(figsize=(11, 6.2)) ax.imshow(birinchi, alpha=0.6) ranglar = plt.cm.tab20(np.linspace(0, 1, 20)) for j, tid in enumerate(np.unique(piyoda[:, 1])): t = piyoda[piyoda[:, 1] == tid] t = t[np.argsort(t[:, 0])] ax.plot(t[:, 2] + t[:, 4] / 2, t[:, 3] + t[:, 5], color=ranglar[j % 20], lw=2) # oyoq o'rtasi ax.text(t[0, 2] + t[0, 4] / 2, t[0, 3] + t[0, 5], str(int(tid)), color="white", fontsize=8, bbox=dict(fc=ranglar[j % 20], ec="none", pad=1), clip_on=True) ax.set_xlim(0, birinchi.shape[1]) # kadrdan chiqib ketgan oyoqlar chizilmasin ax.set_ylim(birinchi.shape[0], 0) ax.axis("off") fig("mot09_yollar.png", "MOT17-09: 26 ta piyodaning 525 kadrdagi yo'llari, birinchi kadr ustida. Raqam — trek boshlanadigan joy.")

MOT17-09: 26 ta piyodaning 525 kadrdagi yo'llari, birinchi kadr ustida. Raqam — trek boshlanadigan joy.
Nega qiyin: to'silish
Odamlar bir-birini, ustunlarni, mashinalarni to'sadi. MOT17 da to'silgan odam ham belgilangan — box bor, ko'rinish ulushi esa nolga yaqin:
v = piyoda[:, 8] print(f"piyoda box lari: {len(v)}") print(f"yarmidan kami ko'rinadi (ko'rinish < 0.5): {(v < 0.5).mean() * 100:.1f}%") print(f"deyarli ko'rinmaydi (ko'rinish < 0.1): {(v < 0.1).mean() * 100:.1f}%")
piyoda box lari: 5325 yarmidan kami ko'rinadi (ko'rinish < 0.5): 40.8% deyarli ko'rinmaydi (ko'rinish < 0.1): 23.6%
Detector ko'rinmaydigan odamni topa olmaydi. Demak har qanday tracker bu kadrlarda odamni "yo'qotadi" va u qayta paydo bo'lganda tanishi kerak — talabaning birinchi savoli aynan shu edi. Bitta misol: 1-trek, 31–50-kadrlarda butunlay to'silgan, keyin qaytib chiqadi. Uning GT box ini kadrlardan kesib olamiz:
t1 = piyoda[piyoda[:, 1] == 1] kadrlar = [20, 31, 40, 50, 55, 70] _, axes = plt.subplots(1, len(kadrlar), figsize=(12, 3.6)) for ax, k in zip(axes, kadrlar): q = t1[t1[:, 0] == k][0] x, y, w, h = q[2:6].astype(int) rasm = cv2.imread(str(MOT / "MOT17-09-FRCNN" / "img1" / f"{k:06d}.jpg"))[:, :, ::-1] ax.imshow(rasm[max(y, 0):y + h, max(x, 0):x + w]) ax.set_title(f"{k}-kadr\nko'rinish {q[8]:.2f}", fontsize=9) ax.axis("off") fig("toslish.png", "Bitta odamning GT box i: 31–50-kadrlarda box bor, lekin odam to'suvchi orqasida.")

Bitta odamning GT box i: 31–50-kadrlarda box bor, lekin odam to'suvchi orqasida.
31–50-kadrlarda uni boshqa o'tkinchi to'sib o'tadi, 55-kadrda u qisman, 70-kadrda to'liq qaytadi. Tracker uchun savol: 70-kadrdagi odam 20-kadrdagi o'sha odammi? Oradagi 20 kadrda detector unga bitta ham box bermagan bo'lishi mumkin.
Detector: MOT17-09 ning hamma kadrlarida
Tracker detector box lari ustida ishlaydi. YOLO11n ni 525 kadrning hammasida faqat person sinfi uchun ishga tushiramiz. Ishonch chegarasini ataylab past (0.1) qo'yamiz: past ishonchli box lar 11-bobda (ByteTrack) kerak bo'ladi. Kadrlar 1920 × 1080, uzoqdagi odamlar kichik — shuning uchun ikki xil imgsz ni solishtiramiz:
kadr_yollari = sorted((MOT / "MOT17-09-FRCNN" / "img1").iterdir()) model = YOLO("weights/yolo11n.pt") det, qatorlar = {}, [] for imgsz in (640, 1280): natijalar = [model.predict(f, classes=[0], conf=0.1, imgsz=imgsz, device=0, verbose=False)[0] for f in kadr_yollari] qatorlar.append({"imgsz": imgsz, "box lar (conf ≥ 0.1)": sum(len(r.boxes) for r in natijalar), "ulardan conf ≥ 0.25": sum(int((r.boxes.conf >= 0.25).sum()) for r in natijalar), **{k: float(np.median([r.speed[k] for r in natijalar])) for k in ("preprocess", "inference", "postprocess")}}) det[imgsz] = {i: np.hstack([r.boxes.xyxy.cpu().numpy(), r.boxes.conf.cpu().numpy()[:, None]]) for i, r in enumerate(natijalar, 1)} # kadr -> (n, 5): x1 y1 x2 y2 ishonch show(pd.DataFrame(qatorlar).round(2)) # keyingi boblar uchun: MOT det formatida (kadr, -1, x, y, w, h, ishonch) saqlaymiz Path("out/mot17").mkdir(parents=True, exist_ok=True) qator = [f"{k},-1,{b[0]:.1f},{b[1]:.1f},{b[2] - b[0]:.1f},{b[3] - b[1]:.1f},{b[4]:.4f}" for k, bb in det[1280].items() for b in bb] Path("out/mot17/MOT17-09_yolo11n_1280.txt").write_text("\n".join(qator) + "\n") print("saqlandi:", len(qator), "qator")
saqlandi: 8301 qator
| imgsz | box lar (conf ≥ 0.1) | ulardan conf ≥ 0.25 | preprocess | inference | postprocess |
|---|---|---|---|---|---|
| 640 | 7 467 | 5 044 | 1.17 | 4.72 | 0.93 |
| 1 280 | 8 301 | 5 522 | 2.48 | 4.84 | 0.93 |
1280 da detector ko'proq odam topadi, GPU dagi tarmoq vaqti esa deyarli o'zgarmaydi (bu kichik model GPU ni to'ldirmaydi — 8-bob). Keyingi boblarda 1280 dagi box larni ishlatamiz.
Birinchi tracker: faqat IoU
Eng oddiy g'oya: odam bir kadrda ko'p siljimaydi. Demak yangi box oldingi kadrdagi qaysi box bilan eng ko'p ustma-ust tushsa (IoU, 1-bob), o'sha trekning davomi. Algoritm:
- Oldingi kadr trek lari va yangi box lar orasida IoU matritsasi.
- Eng katta IoU li juftlikdan boshlab ulaymiz (har biri bir marta), IoU ≥ 0.3 bo'lsa.
- Ulanmagan yangi box — yangi ID. Ulanmagan eski trek — o'chiriladi.
def iou_matritsa(a, b): return torchvision.ops.box_iou(torch.tensor(a[:, :4]), torch.tensor(b[:, :4])).numpy() def oddiy_tracker(det, min_ishonch=0.25, min_iou=0.3): """Faqat oldingi kadr bilan IoU bo'yicha ochko'z ulash. Qaytaradi: (kadr, id, x1, y1, x2, y2) qatorlar va ID lar soni.""" natija, oldingi, oldingi_id, oxirgi_id = [], np.zeros((0, 5)), [], 0 for k in sorted(det): d = det[k][det[k][:, 4] >= min_ishonch] idlar = [-1] * len(d) if len(oldingi) and len(d): M = iou_matritsa(oldingi, d) band_eski, band_yangi = set(), set() for e, y in sorted(zip(*np.nonzero(M >= min_iou)), key=lambda juft: -M[juft]): # katta IoU dan boshlab if e not in band_eski and y not in band_yangi: idlar[y] = oldingi_id[e] band_eski.add(e) band_yangi.add(y) for j in range(len(d)): if idlar[j] == -1: # ulanmadi -> yangi trek oxirgi_id += 1 idlar[j] = oxirgi_id natija += [(k, idlar[j], *d[j, :4]) for j in range(len(d))] oldingi, oldingi_id = d, idlar return np.array(natija), oxirgi_id trek, soni = oddiy_tracker(det[1280]) print(f"tracker ochgan ID lar: {soni} | videodagi haqiqiy piyodalar: {len(np.unique(piyoda[:, 1]))}")
tracker ochgan ID lar: 483 | videodagi haqiqiy piyodalar: 26
Qanchalik yomonligini aniqroq o'lchaymiz. Har kadrda tracker box larini GT bilan IoU ≥ 0.5 bo'yicha juftlaymiz. Keyin har bir haqiqiy odam uchun: unga nechta turli ID berildi va ID necha marta almashdi (ID switch). Rasmiy CLEAR-MOT qoidasidagi kabi, oldingi kadrdagi juftlik hali ham IoU ≥ 0.5 bo'lsa, u saqlanadi — aks holda olomonda ikki box teng ustma-ust tushganda soxta almashishlar sanalib ketadi. Bu 16-bobdagi rasmiy metrikalarning soddalashtirilgan ko'rinishi:
def id_tahlili(trek, piyoda): """Har GT odamga berilgan ID lar, ID almashishlari soni va topilgan GT box lar soni.""" 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)): # 1) oldingi juftlik hali to'g'ri bo'lsa, saqlaymiz 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]): # 2) qolganlari IoU bo'yicha 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 idlar, almashish, topildi = id_tahlili(trek, piyoda) har_biriga = [len({p for _, p in v}) for v in idlar.values()] print(f"GT box lardan topilgani: {topildi} / {len(piyoda)} ({topildi / len(piyoda) * 100:.0f}%)") print(f"ID almashishlari: {almashish}") print(f"bitta haqiqiy odamga berilgan ID lar: o'rtacha {np.mean(har_biriga):.1f}, eng ko'pi {max(har_biriga)}")
GT box lardan topilgani: 3885 / 5325 (73%) ID almashishlari: 118 bitta haqiqiy odamga berilgan ID lar: o'rtacha 5.6, eng ko'pi 18
Har bir haqiqiy odamning vaqt chizig'i: rang — tracker bergan ID. Rang o'zgargan har joy — ID almashishi:
_, ax = plt.subplots(figsize=(11, 6)) for qator, gid in enumerate(sorted(idlar)): k, p = np.array(idlar[gid]).T ax.scatter(k, np.full(len(k), qator), c=plt.cm.tab20(p % 20), s=6, marker="s") ax.set_yticks(range(len(idlar))) ax.set_yticklabels([f"GT {g}" for g in sorted(idlar)], fontsize=7) ax.set_xlabel("kadr") ax.set_xlim(0, 526) fig("oddiy_idlar.png", "Har qator — bitta haqiqiy odam. Rang — oddiy tracker bergan ID. Bo'shliq — odam topilmagan kadrlar.")

Har qator — bitta haqiqiy odam. Rang — oddiy tracker bergan ID. Bo'shliq — odam topilmagan kadrlar.
Real vaqt byudjeti
Kamera sekundiga 30 kadr bersa, bitta kadrga 1000 / 30 ≈ 33 ms bor. Yuqoridagi jadvalga ko'ra 1280 da detector (preprocess + inference + postprocess) shundan kichik qismini oladi. Qolgani tracker, chizish, saqlash va tarmoq uchun. Shuning uchun keyingi boblardagi tracker lar tez bo'lishi shart: har kadrda o'nlab box va o'nlab trek, bir necha millisekund ichida.
Asosiy fikr
- Tracking = har kadrdagi box larni ID bilan ulash + trek larni ochish va yopish. Detector buni bilmaydi.
- MOT17 — tracker larni o'lchash uchun etalon: 7 ta o'qitish videosi,
gt.txtda kadr, ID,tlwhbox, sinf va ko'rinish ulushi. Piyoda box larining 40% dan ortig'i yarmidan kam ko'rinadi. Litsenziyasi CC BY-NC-SA 3.0 — tijorat uchun emas. - Faqat oldingi kadr bilan IoU ga tayangan tracker to'silishdan keyin odamni tanimaydi: 26 ta odamga 483 ta ID, bitta odamga o'rtacha 5–6 ta, ba'zilariga 18 tagacha. Keyingi bob: harakatni bashorat qilish (Kalman filtr), eng yaxshi juftlash (Hungarian) va yo'qolgan trek ni bir muddat kutish.
Mashqlar
oddiy_trackerdamin_iouni 0.1 va 0.6 ga o'zgartiring. ID lar soni va ID almashishlari qanday o'zgaradi? Nega ikkala tomonga ham yomonlashishi mumkin?det[640]bilan tracker ni ishga tushiring. 1280 dagi natija bilan solishtiring: kichik odamlarni topish tracking ga qanday ta'sir qildi?- MOT17-05 videosini oling (14 fps, 640 × 480, kamera harakatda). Oddiy tracker u yerda yaxshiroqmi yoki yomonroq? Nega fps muhim?
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.