Asosiy qismga o'tish

Tracking: "bu o'sha odammi?"

9-bob 11 kod bloki 3751 so'z

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:

  1. Ulash (association): yangi kadrdagi box lardan qaysi biri qaysi eski trekning davomi?
  2. 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):

kod/yuklab_olish.pydavomi
python7 qator
# 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):

Terminal
bash1 qator
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:

kod/09_tracking_nima.pyyangi fayl · 1-qator: from kitob_yordam import fig, show
python31 qator
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))
videofpso'lchamkadrlartreklarsayt: treklarbox larsayt: box larkamera
MOT17-02301920x1080600626218 58118 581qo'zg'almas
MOT17-04301920x10801 050838347 55747 557qo'zg'almas
MOT17-0514640x4808371331336 9176 917harakatda
MOT17-09301920x108052526265 3255 325qo'zg'almas
MOT17-10301920x1080654575712 83912 839harakatda
MOT17-11301920x108090075759 4369 436harakatda
MOT17-13251920x108075011011011 64211 642harakatda

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

kod/09_tracking_nima.pydavomi
python7 qator
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])]))
Chiqish
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
sinfnomiqatorlarhisobga olinadi (6-ustun = 1)
1piyoda5 3255 325
7turgan odam5140
8chalg'ituvchi1 5750
9to'suvchi1 0500
12aks1 9470

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:

kod/09_tracking_nima.pydavomi
python15 qator
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.

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:

kod/09_tracking_nima.pydavomi
python4 qator
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}%")
Chiqish
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:

kod/09_tracking_nima.pydavomi
python11 qator
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.

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:

kod/09_tracking_nima.pydavomi
python17 qator
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")
Chiqish
saqlandi: 8301 qator
imgszbox lar (conf ≥ 0.1)ulardan conf ≥ 0.25preprocessinferencepostprocess
6407 4675 0441.174.720.93
1 2808 3015 5222.484.840.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:

  1. Oldingi kadr trek lari va yangi box lar orasida IoU matritsasi.
  2. Eng katta IoU li juftlikdan boshlab ulaymiz (har biri bir marta), IoU ≥ 0.3 bo'lsa.
  3. Ulanmagan yangi box — yangi ID. Ulanmagan eski trek — o'chiriladi.
kod/09_tracking_nima.pydavomi
python27 qator
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]))}")
Chiqish
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:

kod/09_tracking_nima.pydavomi
python34 qator
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)}")
Chiqish
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:

kod/09_tracking_nima.pydavomi
python9 qator
_, 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.

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.txt da kadr, ID, tlwh box, 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

  1. oddiy_tracker da min_iou ni 0.1 va 0.6 ga o'zgartiring. ID lar soni va ID almashishlari qanday o'zgaradi? Nega ikkala tomonga ham yomonlashishi mumkin?
  2. det[640] bilan tracker ni ishga tushiring. 1280 dagi natija bilan solishtiring: kichik odamlarni topish tracking ga qanday ta'sir qildi?
  3. 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.

Telegramda yozish Narxlar