Ko'p kamera: bitta maydon, bir necha ko'z
Bu bobning kodi: kod/15_kop_kamera.py — kitob papkasidan ishga tushiring: python kod/15_kop_kamera.py
Nega bitta kamera yetmaydi
9–14-boblarda hamma narsa bitta kamerada edi. Uning ikki chegarasi bor: odam boshqa odam orqasida qolsa, kamera uni ko'rmaydi, va kamera faqat o'z burchagini ko'radi. Real ob'ektda — do'kon, zavod, qurilish maydoni — bitta hududni bir necha kamera turli tomondan ko'radi. Bu yangi savol tug'diradi: C1 dagi 5-box va C2 dagi 12-box — bitta odammi?
Rasmlar orasida bevosita solishtirish qiyin: kameralar turli balandlikda, turli burchakda, odamning oldi va orqasi har xil ko'rinadi (12-bobdagi ReID muammolari). Lekin hamma kamera bitta narsani — polni — ko'radi. Har bir box ni pol ustidagi nuqtaga aylantira olsak, "bitta odammi?" savoli "bu ikki nuqta yaqinmi?" savoliga aylanadi. Buning uchun kameraning geometriyasini bilish kerak — kalibrovka.
WILDTRACK
WILDTRACK (Chavdarova va boshqalar, "The WILDTRACK Multi-Camera Person Dataset", arXiv 1707.09299, 2017) — ETH Zurich binosi oldidagi maydonni bir vaqtda suratga olgan 7 ta kamera (GoPro, 1920x1080). 400 ta kadr sekundiga 2 kadr bilan belgilangan: har bir odamning poldagi joyi va 7 kameraning har biridagi box i. Kameralar kalibrovka qilingan — har biri uchun ichki (K) va tashqi (R, t) parametrlar berilgan. Dataset sahifasida litsenziya ko'rsatilmagan, faqat maqolani keltirish so'ralgan: tijoriy loyihada ishlatishdan oldin mualliflardan so'rang.
To'liq arxiv 6.8 GB (7 kamera, 400 kadr, PNG). Bizga uchta kamera va 150 kadr yetadi. HTTP serverlar fayldan faqat kerakli bayt oralig'ini berishi mumkin (Range sarlavhasi). Zip arxivning oxirida mundarija bor — har bir fayl arxivning qaysi baytidan boshlanishi. Shuning uchun mundarijani o'qib, keyin faqat kerakli fayllarni yuklab olamiz — ~1 GB.
Bitta kutilmagan to'siq bor: bu arxivda Python ning zipfile i ba'zi fayllarning joyini 2^32 (4 GB) ga siljigan holda o'qiydi. Masalan, birinchi belgi fayli arxivning 755-baytida turibdi, zipfile esa uni 4 294 968 051-baytdan (755 + 2^32) qidiradi. Bunday xato 4 GB dan katta zip larda uchraydi, agar ularni yaratgan dastur 64-bitli ofsetlarni to'g'ri yozmagan bo'lsa. Shuning uchun ochib_ol har faylni uch nomzod joyda qidiradi va nomi hamda nazorat yig'indisi (CRC) mos kelganini oladi.
# WILDTRACK (15-bob): 7 kameradan uchtasi (C1, C2, C6), dastlabki 150 ta belgilangan kadr, belgilar va kalibrovka. # Arxiv 6.8 GB. HTTP Range so'rovlari bilan faqat kerakli fayllarni o'qiymiz (~1 GB). import io import struct import urllib.request import zlib class HttpFayl(io.RawIOBase): """Serverdagi faylning faqat so'ralgan bayt oralig'ini (HTTP Range) yuklaydi — zipfile uni oddiy fayl deb o'qiydi.""" def __init__(self, url): self.url, self.pos = url, 0 with urllib.request.urlopen(urllib.request.Request(url, method="HEAD")) as r: self.size = int(r.headers["Content-Length"]) def readable(self): return True def seekable(self): return True def tell(self): return self.pos def seek(self, offset, whence=0): self.pos = {0: offset, 1: self.pos + offset, 2: self.size + offset}[whence] return self.pos def readinto(self, b): if self.pos >= self.size: return 0 oxiri = min(self.pos + len(b), self.size) - 1 with urllib.request.urlopen(urllib.request.Request(self.url, headers={"Range": f"bytes={self.pos}-{oxiri}"})) as r: data = r.read() b[:len(data)] = data self.pos += len(data) return len(data) def ochib_ol(fayl, info, maqsad): """Arxiv a'zosini chiqaradi. Bu arxivda ofsetlar 4 GB (2^32) ga siljigan — to'g'risini nom va CRC bo'yicha topamiz.""" for k in range(3): ofset = info.header_offset % 2**32 + k * 2**32 fayl.seek(ofset) sarlavha = fayl.read(30) nom_uz, qosh_uz = struct.unpack("<HH", sarlavha[26:30]) if sarlavha[:4] != b"PK\x03\x04" or fayl.read(nom_uz).decode("utf-8", "replace") != info.filename: continue fayl.seek(ofset + 30 + nom_uz + qosh_uz) data = fayl.read(info.compress_size) if info.compress_type == zipfile.ZIP_DEFLATED: data = zlib.decompress(data, -15) if zlib.crc32(data) != info.CRC: raise ValueError(f"CRC mos emas: {info.filename}") maqsad.parent.mkdir(parents=True, exist_ok=True) maqsad.write_bytes(data) return raise ValueError(f"arxivda topilmadi: {info.filename}") wt = root / "wildtrack" kadrlar = {f"{k * 5:08d}" for k in range(150)} # belgilangan kadrlar: 0, 5, 10, ... if not (wt / "Wildtrack_dataset" / "Image_subsets" / "C6" / "00000745.png").exists(): arxiv = io.BufferedReader(HttpFayl("https://documents.epfl.ch/groups/c/cv/cvlab-unit/www/data/Wildtrack/Wildtrack_dataset_full.zip"), buffer_size=1 << 20) for info in zipfile.ZipFile(arxiv).infolist(): n = info.filename kerak = ("annotations_positions/" in n or "calibrations/" in n or (Path(n).parent.name in ("C1", "C2", "C6") and Path(n).stem in kadrlar)) if kerak and not info.is_dir() and not n.startswith("__MACOSX") and not (wt / n).exists(): ochib_ol(arxiv, info, wt / n)
Bizning ulanishda bu ~28 daqiqa oldi. Belgilar (annotations_positions/*.json) har kadr uchun bitta fayl: har odamning personID si, poldagi joyi positionID va 7 kameradagi box lari (ko'rinmasa −1). positionID — poldagi 480 x 1440 to'rning katak raqami, qadam 2.5 sm, to'rning boshi (−3 m, −9 m) da. Shunday qilib maydon 12 x 36 metr.
import itertools import json import re from pathlib import Path import cv2 import numpy as np import pandas as pd import torch import torchvision import matplotlib.pyplot as plt from scipy.optimize import linear_sum_assignment from ultralytics import YOLO from ultralytics.utils import LOGGER LOGGER.setLevel("ERROR") WT = Path("datasets/wildtrack/Wildtrack_dataset") KAMERA_NOMI = {"C1": "CVLab1", "C2": "CVLab2", "C3": "CVLab3", "C4": "CVLab4", "C5": "IDIAP1", "C6": "IDIAP2", "C7": "IDIAP3"} KAMERALAR = ["C1", "C2", "C6"] KADRLAR = [k * 5 for k in range(150)] # yuklab olingan kadrlar: 0, 5, ..., 745 def pozitsiya(pid): """positionID -> poldagi (X, Y), santimetrda.""" return np.array([-300 + 2.5 * (pid % 480), -900 + 2.5 * (pid // 480)]) gt = [] # (kadr, odam, X, Y, {kamera: box}) for k in KADRLAR: for p in json.load(open(WT / "annotations_positions" / f"{k:08d}.json")): boxlar = {c: (v["xmin"], v["ymin"], v["xmax"], v["ymax"]) for c, v in zip(KAMERA_NOMI, p["views"]) if v["xmin"] >= 0} gt.append((k, p["personID"], *pozitsiya(p["positionID"]), boxlar)) G = {k: np.array([g[1:4] for g in gt if g[0] == k]) for k in KADRLAR} # kadr -> (odam, X, Y) print(f"{len(KADRLAR)} kadr | {len(gt)} odam-kadr | {len({g[1] for g in gt})} xil odam | kadrda o'rtacha {len(gt) / len(KADRLAR):.1f} odam") show(pd.DataFrame([{"kamera": c, "kalibrovka fayli": n, "ko'rgan odam-kadr": sum(c in g[4] for g in gt), "ulushi, %": 100 * sum(c in g[4] for g in gt) / len(gt)} for c, n in KAMERA_NOMI.items()]).round(1))
150 kadr | 3239 odam-kadr | 155 xil odam | kadrda o'rtacha 21.6 odam
| kamera | kalibrovka fayli | ko'rgan odam-kadr | ulushi, % |
|---|---|---|---|
| C1 | CVLab1 | 2 876 | 88.8 |
| C2 | CVLab2 | 2 566 | 79.2 |
| C3 | CVLab3 | 1 903 | 58.8 |
| C4 | CVLab4 | 577 | 17.8 |
| C5 | IDIAP1 | 758 | 23.4 |
| C6 | IDIAP2 | 2 998 | 92.6 |
| C7 | IDIAP3 | 780 | 24.1 |
Uchta kamerani tanlash sababi jadvalda: C1, C2 va C6 maydondagi odamlarning eng katta qismini ko'radi. C4, C5, C7 esa odamlarning to'rtdan biridan kamini ko'radi.
Kamera modeli: poldan rasmga
Kalibrovka qilingan kamera fazodagi har bir nuqta rasmning qaysi pikseliga tushishini aytadi. Bu teshikli kamera (pinhole) modeli:
- X — nuqtaning dunyodagi koordinatasi (X, Y, Z), santimetrda; pol — Z = 0.
- R, t — tashqi parametrlar: dunyodan kameraga burish (3x3) va siljitish. Kameraning o'zi dunyoda −Rᵀt nuqtada turadi.
- K — ichki parametrlar, 3x3 matritsa: birinchi qatorda f_x, 0, c_x; ikkinchisida 0, f_y, c_y; uchinchisida 0, 0, 1. f_x, f_y — piksellardagi fokus masofasi, (c_x, c_y) — optik markaz. s — chuqurlik, u (u, v, 1) ning uchinchi koordinatasi 1 bo'lishi uchun bo'linadi.
WILDTRACK ning intrinsic_zero kalibrovkasi linza buzilishi to'g'rilangan rasmlar uchun, shuning uchun buzilish koeffitsientlari nol. Tekshiramiz: har bir odamning poldagi joyini rasmga proyeksiya qilamiz va GT box ining pastki tomoni o'rtasi (oyoq nuqtasi, 14-bob) bilan solishtiramiz.
def kalibrovka(c): fs = cv2.FileStorage(str(WT / "calibrations" / "intrinsic_zero" / f"intr_{KAMERA_NOMI[c]}.xml"), cv2.FILE_STORAGE_READ) K = fs.getNode("camera_matrix").mat() matn = (WT / "calibrations" / "extrinsic" / f"extr_{KAMERA_NOMI[c]}.xml").read_text() rvec, t = (np.array(re.search(f"<{teg}>(.*?)</{teg}>", matn, re.S).group(1).split(), float) for teg in ("rvec", "tvec")) return K, cv2.Rodrigues(rvec)[0], t # rvec (burish vektori) -> 3x3 matritsa kal = {c: kalibrovka(c) for c in KAMERA_NOMI} K, R, t = kal["C1"] print("C1: K =\n", K.round(1), "\nt =", t.round(1), "sm") print("C1 kamerasining dunyodagi joyi (X, Y, balandlik):", (-R.T @ t).round(0), "sm") def rasmga(c, XYZ): K, R, t = kal[c] P = (R @ np.asarray(XYZ, float).T).T + t # dunyo -> kamera uv = (K @ P.T).T return uv[:, :2] / uv[:, 2:] # s ga bo'lamiz qatorlar = [] for c in KAMERA_NOMI: q = [(g[2], g[3], g[4][c]) for g in gt if c in g[4]] uv = rasmga(c, [(X, Y, 0) for X, Y, _ in q]) oyoq = np.array([((b[0] + b[2]) / 2, b[3]) for *_, b in q]) boy = np.array([b[3] - b[1] for *_, b in q]) xato = np.hypot(*(uv - oyoq).T) qatorlar.append({"kamera": c, "box lar": len(q), "farq, px (median)": np.median(xato), "box balandligiga nisbatan, % (median)": 100 * np.median(xato / boy)}) show(pd.DataFrame(qatorlar).round(1))
C1: K = [[ 1743.4 0 934.5] [ 0 1735.2 444.4] [ 0 0 1]] t = [ -525.9 45.4 986.7] sm C1 kamerasining dunyodagi joyi (X, Y, balandlik): [ 910 -584 289] sm
| kamera | box lar | farq, px (median) | box balandligiga nisbatan, % (median) |
|---|---|---|---|
| C1 | 2 876 | 7.3 | 4.3 |
| C2 | 2 566 | 6.3 | 2.6 |
| C3 | 1 903 | 10.8 | 3.5 |
| C4 | 577 | 7.7 | 3.3 |
| C5 | 758 | 8.3 | 2.7 |
| C6 | 2 998 | 2.5 | 2 |
| C7 | 780 | 16.3 | 6.5 |
Hamma 7 kamerada proyeksiya GT oyoq nuqtasiga box balandligining bir necha foizi ichida tushadi. Kalibrovka fayllari va kameralar tartibi (C1 = CVLab1, ..., C6 = IDIAP2) to'g'ri.
Rasmdan polga: gomografiya
Bizga teskari yo'nalish kerak: box → poldagi nuqta. Umuman olganda bu mumkin emas — bitta piksel kameradan chiqqan butun bir nurga to'g'ri keladi. Lekin nuqta polda yotishini (Z = 0) bilsak, masala yechiladi. Z = 0 bo'lganda R ning uchinchi ustuni yo'qoladi va proyeksiya 3x3 matritsaga aylanadi — gomografiya:
Bu yerda r₁, r₂ — R ning birinchi va ikkinchi ustunlari, ∼ — "uchinchi koordinataga bo'lgandan keyin teng". H — 3x3 matritsa, uning teskarisi rasmdagi har bir pol nuqtasini poldagi koordinataga aylantiradi. Tekshiruv: GT box larning oyoq nuqtalarini polga qaytaramiz va GT joy bilan solishtiramiz.
H = {c: np.linalg.inv(kal[c][0] @ np.c_[kal[c][1][:, :2], kal[c][2]]) for c in KAMERALAR} # rasm -> pol def polga(c, uv): p = H[c] @ np.c_[uv, np.ones(len(uv))].T return (p[:2] / p[2]).T qatorlar = [] for c in KAMERALAR: q = [(g[2], g[3], g[4][c]) for g in gt if c in g[4]] xy = polga(c, np.array([((b[0] + b[2]) / 2, b[3]) for *_, b in q])) haqiqiy = np.array([(X, Y) for X, Y, _ in q]) xato = np.hypot(*(xy - haqiqiy).T) uzoqlik = np.hypot(*(haqiqiy - (-kal[c][1].T @ kal[c][2])[:2]).T) # odamdan kameragacha, polda qatorlar.append({"kamera": c, "farq, sm (median)": np.median(xato), "90% i shundan kam": np.percentile(xato, 90), "kameradan 10 m gacha": np.median(xato[uzoqlik < 1000]), "10 m dan uzoq": np.median(xato[uzoqlik >= 1000])}) show(pd.DataFrame(qatorlar).round(1))
| kamera | farq, sm (median) | 90% i shundan kam | kameradan 10 m gacha | 10 m dan uzoq |
|---|---|---|---|---|
| C1 | 13.9 | 19.9 | 7 | 14.3 |
| C2 | 7.9 | 15.5 | 6.4 | 8.6 |
| C6 | 14.9 | 27.6 | 6.4 | 15.5 |
GT box ning oyoq nuqtasi polga o'rtacha 8–15 sm aniqlikda tushadi. Bu farq kalibrovka xatosi emas, geometriya: box ning pastki chegarasi — oyoqning kameraga eng yaqin nuqtasi, odamning "joyi" esa uning tanasi markazi ostida, bir oz orqaroqda. Uzoqda farq kattaroq: rasmdagi bitta piksel u yerda poldagi kattaroq masofaga to'g'ri keladi. Amaliy ma'nosi: pol xaritasining aniqligi — o'nlab santimetr, metr emas.
Har bir kamerada YOLO11n
Endi GT box lar o'rniga detector box larini olamiz: YOLO11n, 1280 o'lcham, ishonch ≥ 0.25. Har bir box ning oyoq nuqtasi polga o'tkaziladi; maydondan (12 x 36 m) tashqariga tushganlari tashlanadi — u yerdagi odamlar belgilanmagan. Poldagi nuqtani GT bilan solishtirish uchun masofa ishlatamiz: WILDTRACK da odatdagi chegara — 50 sm.
model = YOLO("weights/yolo11n.pt") det = {} for c in KAMERALAR: fayl = Path(f"out/wildtrack/{c}_yolo11n_1280.txt") if not fayl.exists(): fayl.parent.mkdir(parents=True, exist_ok=True) qatorlar = [] for k in KADRLAR: b = model.predict(WT / "Image_subsets" / c / f"{k:08d}.png", classes=[0], conf=0.25, imgsz=1280, device=0, verbose=False)[0].boxes qatorlar += [f"{k},{x[0]:.1f},{x[1]:.1f},{x[2]:.1f},{x[3]:.1f},{x[4]:.4f}" for x in np.c_[b.xyxy.cpu().numpy(), b.conf.cpu().numpy()]] fayl.write_text("\n".join(qatorlar) + "\n") d = np.loadtxt(fayl, delimiter=",") det.update({(c, k): d[d[:, 0] == k][:, 1:] for k in KADRLAR}) # (n, 5): x1 y1 x2 y2 ishonch def maydonda(xy): return (xy[:, 0] >= -300) & (xy[:, 0] < 900) & (xy[:, 1] >= -900) & (xy[:, 1] < 2700) pol = {} for (c, k), d in det.items(): xy = polga(c, np.c_[(d[:, 0] + d[:, 2]) / 2, d[:, 3]]) pol[c, k] = np.c_[xy, d[:, 4], np.arange(len(d))][maydonda(xy)] # X, Y, ishonch, box raqami def iou_matritsa(a, b): return torchvision.ops.box_iou(torch.tensor(np.asarray(a, float)[:, :4]), torch.tensor(np.asarray(b, float)[:, :4])).numpy() def moslash(A, B, r=50): """A, B: poldagi nuqtalar (n, 2). Masofasi <= r sm bo'lgan juftlar, Venger bilan.""" if not len(A) or not len(B): return [] D = np.hypot(A[:, None, 0] - B[None, :, 0], A[:, None, 1] - B[None, :, 1]) q, u = linear_sum_assignment(np.where(D <= r, D, 1e6)) return [(i, j) for i, j in zip(q, u) if D[i, j] <= r] qatorlar = [] for c in KAMERALAR: kor = rasmda = polda = soxta = nuqta = 0 for k in KADRLAR: q = [(g[1], g[4][c]) for g in gt if g[0] == k and c in g[4]] kor += len(q) if q and len(det[c, k]): M = iou_matritsa([b for _, b in q], det[c, k]) i, j = linear_sum_assignment(-M) rasmda += int((M[i, j] >= 0.5).sum()) m = moslash(pol[c, k][:, :2], G[k][:, 1:]) polda += len({int(G[k][j, 0]) for _, j in m} & {pid for pid, _ in q}) soxta += len(pol[c, k]) - len(m) nuqta += len(pol[c, k]) qatorlar.append({"kamera": c, "ko'rinadigan odam-kadr": kor, "rasmda topildi (IoU >= 0.5), %": 100 * rasmda / kor, "polda topildi (<= 50 sm), %": 100 * polda / kor, "poldagi nuqtalar": nuqta, "precision, %": 100 * (1 - soxta / nuqta)}) show(pd.DataFrame(qatorlar).round(1))
| kamera | ko'rinadigan odam-kadr | rasmda topildi (IoU >= 0.5), % | polda topildi (<= 50 sm), % | poldagi nuqtalar | precision, % |
|---|---|---|---|---|---|
| C1 | 2 876 | 67.2 | 57.3 | 2 868 | 57.9 |
| C2 | 2 566 | 66.6 | 53.8 | 2 163 | 66.6 |
| C6 | 2 998 | 53.1 | 51.3 | 2 259 | 73.7 |
Bitta kamera kutilganidan yomon ishlaydi, va ikki sabab bor. Birinchisi — detector: rasmda ko'rinadigan odamlarning faqat 53–67% i topildi. WILDTRACK — zich olomon, odamlar bir-birini to'sadi, uzoqdagilar kichik. Ikkinchisi — oyoq nuqtasi: to'silgan odamning box i oyog'igacha yetmaydi, uning pastki chegarasi tizzada yoki belda bo'ladi va polga o'tkazilganda nuqta kameraga yaqinroqqa tushadi. Shuning uchun "rasmda topildi" ustuni "polda topildi" dan har doim yuqori. Precision past bo'lishining sababi ham shu: noto'g'ri joyga tushgan nuqta ham soxta, ham topilmagan odam hisoblanadi.
Kameralar orasida: bitta odammi?
Endi asosiy savol. C1 dagi nuqta va C2 dagi nuqta bitta odamniki ekanini qanday bilamiz? Ular poldagi bitta joyga tushishi kerak — lekin 20–50 sm xato bilan. Chegara ni ma'lumotdan tanlaymiz: GT ga moslangan nuqtalarni olib, bir xil odamning turli kameradagi nuqtalari orasidagi masofani turli odamlarnikiga solishtiramiz.
bir, turli = [], [] for k in KADRLAR: egasi = {(c, i): int(G[k][j, 0]) for c in KAMERALAR for i, j in moslash(pol[c, k][:, :2], G[k][:, 1:])} for (ca, i), pa in egasi.items(): for (cb, j), pb in egasi.items(): if ca < cb: (bir if pa == pb else turli).append(np.hypot(*(pol[ca, k][i, :2] - pol[cb, k][j, :2]))) bir, turli = np.array(bir), np.array(turli) print(f"bir odam, ikki kamera: {len(bir)} juft, median {np.median(bir):.0f} sm | turli odamlar: {len(turli)} juft, median {np.median(turli):.0f} sm") show(pd.DataFrame([{"chegara, sm": r, "bir odam: shu masofada, %": 100 * (bir <= r).mean(), "turli odamlar: shu masofada, %": 100 * (turli <= r).mean()} for r in (50, 75, 100, 150)]).round(2))
bir odam, ikki kamera: 2527 juft, median 31 sm | turli odamlar: 48608 juft, median 815 sm
| chegara, sm | bir odam: shu masofada, % | turli odamlar: shu masofada, % |
|---|---|---|
| 50 | 80.06 | 0.19 |
| 75 | 98.22 | 0.94 |
| 100 | 100 | 2.32 |
| 150 | 100 | 4.66 |
75 sm da bir xil odam juftlarining deyarli hammasi ulanadi, turli odamlar juftining esa 1% dan kami chegaradan o'tadi. 100 sm da bu ulush ikki barobardan ko'proq oshadi — olomonda odamlar bir-biridan bir metrga yaqin turadi. Chegara: 75 sm.
Uch kamerani birlashtirish
Algoritm oddiy: kameralarni navbat bilan qo'shamiz. C1 ning nuqtalari — boshlang'ich klasterlar. C2 ning nuqtalari klaster markazlariga Venger bilan ulanadi (≤ 75 sm), ulanmaganlari yangi klaster ochadi. Keyin C6. Klasterning joyi — nuqtalarining o'rtachasi, uning bali — kameralardagi ishonchlar yig'indisi: uch kamera ko'rgan odam bitta kamera ko'rgan odamdan yuqori ball oladi.
Baholash uchun endi maydondagi hamma odamni (qaysi kamera ko'rishidan qat'i nazar) GT deb olamiz — mijozga kerak bo'lgan savol shu: "maydondagi odamlarning qanchasini ko'ryapmiz?" Ball chegarasini o'zgartirib, 7-bobdagi kabi precision-recall egri chizig'ini chizamiz.
def birlashtir(k, kameralar, r=75): klaster = [] # har biri: {kamera: pol qatori} for c in kameralar: P = pol[c, k] markaz = np.array([np.mean([p[:2] for p in q.values()], 0) for q in klaster]).reshape(-1, 2) m = moslash(markaz, P[:, :2], r) for i, j in m: klaster[i][c] = P[j] band = {j for _, j in m} klaster += [{c: P[j]} for j in range(len(P)) if j not in band] return klaster def nuqtalar(klaster): """Klaster -> (X, Y, ball): o'rtacha joy va ishonchlar yig'indisi.""" return np.array([[*np.mean([p[:2] for p in q.values()], 0), sum(p[2] for p in q.values())] for q in klaster]).reshape(-1, 3) JAMI = sum(len(G[k]) for k in KADRLAR) # maydondagi hamma odam-kadr def pr_egri(kameralar): ballar, togri = [], [] for k in KADRLAR: P = nuqtalar(birlashtir(k, kameralar)) m = dict(moslash(P[:, :2], G[k][:, 1:])) ballar += list(P[:, 2]) togri += [i in m for i in range(len(P))] tartib = np.argsort(-np.array(ballar)) t = np.array(togri)[tartib] tp, fp = np.cumsum(t), np.cumsum(~t) return tp / JAMI, tp / (tp + fp), np.array(ballar)[tartib] egri, qatorlar = {}, [] for n in (1, 2, 3): for kam in itertools.combinations(KAMERALAR, n): nom = " + ".join(kam) rec, prec, _ = egri[nom] = pr_egri(list(kam)) qatorlar.append({"kameralar": nom, "eng yuqori recall, %": 100 * rec[-1], **{f"recall, precision >= {p:.0%}": 100 * rec[prec >= p].max() if (prec >= p).any() else np.nan for p in (0.9, 0.8, 0.7)}}) show(pd.DataFrame(qatorlar).round(1)) _, ax = plt.subplots(figsize=(8, 5)) for nom, (rec, prec, _) in egri.items(): qalin = nom.count("+") + 1 ax.plot(rec * 100, prec * 100, lw=0.8 * qalin, label=nom, alpha=0.5 + 0.15 * qalin) ax.set_xlabel("recall, % (maydondagi hamma odam)") ax.set_ylabel("precision, %") ax.set_ylim(40, 101) ax.legend(fontsize=8) fig("kop_kamera_pr.png", "Poldagi nuqtalar uchun precision-recall: yupqa — bitta kamera, o'rta — ikkita, qalin — uchala kamera birga.")
| kameralar | eng yuqori recall, % | recall, precision >= 90% | recall, precision >= 80% | recall, precision >= 70% |
|---|---|---|---|---|
| C1 | 51.3 | 0 | 25.7 | 40.8 |
| C2 | 44.5 | NaN | 33.5 | 42.1 |
| C6 | 51.4 | 19.7 | 44 | 51.4 |
| C1 + C2 | 69.4 | 32.8 | 54 | 62.5 |
| C1 + C6 | 70 | 30.8 | 49 | 61.9 |
| C2 + C6 | 72.8 | 30.6 | 57.6 | 70.5 |
| C1 + C2 + C6 | 81.8 | 48.8 | 67.9 | 74.8 |

Poldagi nuqtalar uchun precision-recall: yupqa — bitta kamera, o'rta — ikkita, qalin — uchala kamera birga.
Bu bobning asosiy natijasi shu jadvalda (NaN — bu kameralar bilan bunday precision ga hech bir chegarada erishilmaydi). Bitta kamera 80% precision da maydondagi odamlarning ko'pi bilan yarmidan kamrog'ini topadi, uchala kamera birga — uchdan ikkisidan ko'prog'ini. Ikkita kamera ham yaxshilaydi, lekin uchinchisi yana sezilarli qo'shadi. Sabab ikkita:
- Qamrov va to'silish. Bir kamerada to'silgan odam boshqasida ko'rinadi.
- Tasdiq. Ikki-uch kamera bir joyda odam ko'rsa, bu nuqta yuqori ball oladi; bitta kameraning soxta box i past ballda qoladi. Shuning uchun uch kamerada egri chiziq yuqori precision da ham uzoq cho'ziladi.
Bu "nechta kamera kerak?" savoliga birinchi o'lchangan javob: zich olomonda bitta hududni kamida ikki, yaxshisi uch kamera turli tomondan ko'rishi kerak. Kameralarning joylashuvi va piksel zichligini III qismda hisoblaymiz.
Bitta kadr, uch kamera
k = max(KADRLAR, key=lambda k: sum(len(q) == 3 for q in birlashtir(k, KAMERALAR))) # uch kamera birga ko'rgan odamlar eng ko'p kadr klaster = birlashtir(k, KAMERALAR) ranglar = [r for i, r in enumerate(plt.cm.tab20.colors[0::2] + plt.cm.tab20.colors[1::2]) if i not in (7, 17)] # kulranglarsiz 18 rang rang = {} for q in klaster: if len(q) >= 2: rang[id(q)] = ranglar[len(rang) % len(ranglar)] _, ax = plt.subplot_mosaic([KAMERALAR, ["pol"] * 3], figsize=(15, 8.5), height_ratios=[1, 1.15]) for c in KAMERALAR: ax[c].imshow(cv2.imread(str(WT / "Image_subsets" / c / f"{k:08d}.png"))[:, :, ::-1]) for q in klaster: if c in q: x1, y1, x2, y2 = det[c, k][int(q[c][3]), :4] ax[c].add_patch(plt.Rectangle((x1, y1), x2 - x1, y2 - y1, fill=False, lw=2, color=rang.get(id(q), "lightgray"))) ax[c].set_title(c, fontsize=10) ax[c].axis("off") ax["pol"].add_patch(plt.Rectangle((-900, -300), 3600, 1200, fill=False, color="gray", ls="--")) ax["pol"].scatter(G[k][:, 2], G[k][:, 1], marker="x", color="black", s=40, label="GT: odamning joyi") for q in klaster: X, Y = np.mean([p[:2] for p in q.values()], 0) ax["pol"].scatter(Y, X, s=40 * len(q), color=rang.get(id(q), "lightgray"), edgecolors="black", lw=0.5) for c in KAMERALAR: X, Y, _ = -kal[c][1].T @ kal[c][2] ax["pol"].scatter(Y, X, marker="^", s=150, color="red") ax["pol"].annotate(c, (Y, X), xytext=(8, -4), textcoords="offset points", color="red") ax["pol"].set_aspect("equal") ax["pol"].set_xlabel("Y, sm") ax["pol"].set_ylabel("X, sm") ax["pol"].legend(loc="upper right", fontsize=8) fig("bir_kadr.png", f"{k}-kadr. Yuqorida: har kamerada box lar, bir xil rang — polda bitta klasterga tushgan (bitta odam deb topilgan). Kulrang — faqat bitta kamera ko'rgan. Pastda: maydon (12 x 36 m) tepadan, doira kattaligi — nechta kamera ko'rgani, qizil uchburchak — kameralar.")

730-kadr. Yuqorida: har kamerada box lar, bir xil rang — polda bitta klasterga tushgan (bitta odam deb topilgan). Kulrang — faqat bitta kamera ko'rgan. Pastda: maydon (12 x 36 m) tepadan, doira kattaligi — nechta kamera ko'rgani, qizil uchburchak — kameralar.
Rasmda ko'p kamerali tizimning ishlash usuli ko'rinadi: C1, C2 va C6 dagi uch xil ko'rinishdagi box lar poldagi bitta nuqtaga yig'iladi va bitta odam bo'ladi. Hech bir rasmni boshqasi bilan piksel bo'yicha solishtirmadik — faqat geometriya.
Vaqt bo'yicha: umumiy ID
Oxirgi qadam — kadrlar orasida bog'lash. Endi trek rasmda emas, polda yuradi va bitta ID hamma kamera uchun umumiy: odam C1 dan chiqib C6 da ko'rinsa ham, u poldagi o'sha yo'lda davom etadi. Tracker 10-bobdagi mini-SORT g'oyasi: Venger bilan ulash, xotira, faqat IoU o'rniga poldagi masofa. Belgilar sekundiga 2 kadr, kadrlar orasida 0.5 soniya — odamlar qancha siljishini GT dan o'lchaymiz va ulash chegarasini 1 m qilamiz. Tezlikni hisobga olish (10-bobdagi Kalman g'oyasining eng soddasi) past fps da ayniqsa muhim.
Nuqtalarni 80% precision beradigan ball chegarasi bilan tanlaymiz va bitta kamera (C1) bilan uch kamerani solishtiramiz.
yol = {} for g in gt: yol.setdefault(g[1], {})[g[0]] = np.array(g[2:4]) qadam = [np.hypot(*(v[k + 5] - v[k])) for v in yol.values() for k in v if k + 5 in v] print(f"GT: ketma-ket kadrlar orasida siljish — median {np.median(qadam):.0f} sm, 75% i {np.percentile(qadam, 75):.0f} sm gacha, " f"95% i {np.percentile(qadam, 95):.0f} sm gacha") def pol_tracker(nuqta, r=100, max_yoqolish=4, tezlik=True): treklar, natija, oxirgi_id = [], [], 0 # trek: [id, X, Y, vX, vY, yo'qolgan kadrlar] for k in KADRLAR: P = nuqta[k][:, :2] taxmin = np.array([[t[1] + t[3] * (t[5] + 1), t[2] + t[4] * (t[5] + 1)] if tezlik else t[1:3] for t in treklar]).reshape(-1, 2) m = moslash(taxmin, P, r) for i, j in m: t = treklar[i] if tezlik: v = (P[j] - np.array(t[1:3])) / (t[5] + 1) t[3:5] = list(0.5 * np.array(t[3:5]) + 0.5 * v) # tezlikni silliqlaymiz t[1:3], t[5] = list(P[j]), 0 ulangan_t, ulangan_p = {i for i, _ in m}, {j for _, j in m} for i, t in enumerate(treklar): if i not in ulangan_t: t[5] += 1 treklar = [t for t in treklar if t[5] <= max_yoqolish] for j in range(len(P)): if j not in ulangan_p: oxirgi_id += 1 treklar.append([oxirgi_id, *P[j], 0.0, 0.0, 0]) natija += [(k, t[0], t[1], t[2]) for t in treklar if t[5] == 0] return np.array(natija) def id_tahlili(trek): idlar, oxirgi, almashish, topildi = {}, {}, 0, 0 for k in KADRLAR: T = trek[trek[:, 0] == k] for i, j in moslash(G[k][:, 1:], T[:, 2:4]): gid, tid = int(G[k][i, 0]), int(T[j, 1]) topildi += 1 idlar.setdefault(gid, set()).add(tid) almashish += gid in oxirgi and oxirgi[gid] != tid oxirgi[gid] = tid return {"topilgan odam-kadr, %": 100 * topildi / JAMI, "ID lar": len(np.unique(trek[:, 1])), "bir odamga ID": np.mean([len(v) for v in idlar.values()]), "ID almashishi": almashish, "almashish / topilgan, %": 100 * almashish / topildi} qatorlar = [] for kam in (["C1"], KAMERALAR): rec, prec, ball = egri[" + ".join(kam)] chegara = ball[np.nonzero(prec >= 0.8)[0][-1]] # 80% precision dagi ball nuqta = {k: P[P[:, 2] >= chegara] for k, P in ((k, nuqtalar(birlashtir(k, kam))) for k in KADRLAR)} for tezlik in (False, True): qatorlar.append({"kameralar": " + ".join(kam), "tezlik": tezlik, **id_tahlili(pol_tracker(nuqta, tezlik=tezlik))}) show(pd.DataFrame(qatorlar).round(2))
GT: ketma-ket kadrlar orasida siljish — median 53 sm, 75% i 68 sm gacha, 95% i 92 sm gacha
| kameralar | tezlik | topilgan odam-kadr, % | ID lar | bir odamga ID | ID almashishi | almashish / topilgan, % |
|---|---|---|---|---|---|---|
| C1 | False | 25.78 | 238 | 2.4 | 152 | 18.2 |
| C1 | True | 25.78 | 186 | 1.7 | 78 | 9.34 |
| C1 + C2 + C6 | False | 68.57 | 377 | 3.3 | 328 | 14.77 |
| C1 + C2 + C6 | True | 68.57 | 321 | 2.19 | 198 | 8.91 |
Natija ikki tomonlama, va buni ochiq aytish kerak:
- Uch kamera odamlarni ancha ko'p kadrda topadi — bu yuqoridagi PR jadvalining davomi.
- Lekin ID lar bo'yicha u hali yaxshi emas: har bir odamga o'rtacha ikkitadan ko'p ID beriladi, ID almashishi soni ham bitta kameradagidan ko'p. Topilgan box ga nisbatan almashishlar ulushi esa ikkala holatda yaqin. Sekundiga 2 kadrda olomon ichida yurayotgan odamlarni faqat joy bo'yicha ulash qiyin: kadrlar orasida odam qo'shnisigacha bo'lgan masofaga teng yo'l bosadi.
- Tezlikni hisobga olish ID almashishini sezilarli kamaytiradi — past fps da harakat modeli muhimroq.
Qaysi biri "yaxshiroq"? Bu jadvaldagi sonlar yana bir-biriga qarshi tortadi: ko'proq topish va ko'proq almashish. Bunday savollar uchun tracking ning standart metrikalari bor — keyingi bobda ularni noldan quramiz.
Real tizimda bu tracker ni kuchaytirishning uch yo'li bor: kadrlar tezligini oshirish (asl video sekundiga 60 kadr — belgilar faqat 2 kadrda), tashqi ko'rinishni qo'shish (12-bob: niqoblangan rang gistogrammasi uch kameradan yig'ilsa, ishonchliroq bo'ladi) va detector ni olomon uchun o'qitish (7-bob).
Kalibrovkasiz: poldagi belgilar bilan
WILDTRACK mualliflari kameralarni maxsus usulda kalibrovka qilgan. Real ob'ektda sizda K, R, t bo'lmaydi. Lekin bizga faqat H kerak — poldan rasmga 3x3 matritsa. Uni kalibrovkasiz topish mumkin: polga bir necha belgi qo'yiladi (masalan, lenta bilan), ularning koordinatasi ruletka bilan o'lchanadi va rasmda ular qaysi pikselda ekani belgilanadi. cv2.findHomography shu juftlardan H ni hisoblaydi. Matematik minimum — 4 nuqta. Amalda nechta kerak?
Tajriba: GT dan n ta tasodifiy odamning (oyoq nuqtasi, poldagi joyi) juftini olamiz — bu "belgilar". H ni shulardan topamiz va uni haqiqiy YOLO11n box larida sinaymiz (box GT box ga rasmda IoU ≥ 0.5 bilan mos kelgan detection lar). Har bir n uchun 50 marta tasodifiy tanlaymiz.
juft = [] # (detection oyog'i u, v, haqiqiy X, Y) for k in KADRLAR: q = [(g[2], g[3], g[4]["C1"]) for g in gt if g[0] == k and "C1" in g[4]] d = det["C1", k] if q and len(d): M = iou_matritsa([b for *_, b in q], d) i, j = linear_sum_assignment(-M) juft += [((d[b, 0] + d[b, 2]) / 2, d[b, 3], q[a][0], q[a][1]) for a, b in zip(i, j) if M[a, b] >= 0.5] D = np.array(juft) belgilar = np.array([((g[4]["C1"][0] + g[4]["C1"][2]) / 2, g[4]["C1"][3], g[2], g[3]) for g in gt if "C1" in g[4]]) print("sinov: C1 dagi", len(D), "ta detection") print(f"to'liq kalibrovka (K, R, t) bilan xato: median {np.median(np.hypot(*(polga('C1', D[:, :2]) - D[:, 2:]).T)):.1f} sm") rng = np.random.default_rng(0) qatorlar = [] for n in (4, 8, 20): xato = [] for _ in range(50): i = rng.choice(len(belgilar), n, replace=False) Hn, _ = cv2.findHomography(belgilar[i, :2], belgilar[i, 2:]) # rasm -> pol, eng kichik kvadratlar xato.append(np.median(np.hypot(*(cv2.perspectiveTransform(D[None, :, :2], Hn)[0] - D[:, 2:]).T))) qatorlar.append({"pol xaritasi": f"{n} ta belgidan", "xato, sm (median)": np.median(xato), "eng yomon 10% tanlovda": np.percentile(xato, 90)}) show(pd.DataFrame(qatorlar).round(1))
sinov: C1 dagi 1934 ta detection to'liq kalibrovka (K, R, t) bilan xato: median 23.9 sm
| pol xaritasi | xato, sm (median) | eng yomon 10% tanlovda |
|---|---|---|
| 4 ta belgidan | 32.7 | 97.4 |
| 8 ta belgidan | 28.4 | 29.9 |
| 20 ta belgidan | 28.1 | 28.8 |
To'rtta belgi — xavfli: odatda ishlaydi, lekin belgilar yomon joylashsa (bir-biriga yaqin yoki deyarli bir chiziqda) xato keskin oshadi — eng yomon tanlovlarda bir metrga yaqinlashadi. Sakkizta va undan ko'p belgi bilan xarita to'liq kalibrovkaga yaqinlashadi va barqaror bo'ladi. O'rnatish qoidasi: har bir kamera uchun kamida 8 ta belgi, butun ko'rinadigan pol bo'ylab tarqatilgan — faqat kamera tagida emas. Bir nechta kamera bitta pol xaritasini ishlatishi uchun hamma belgilar bitta koordinata tizimida o'lchanadi.
Asosiy fikr
- Ko'p kamerali tizimning kaliti — umumiy koordinata: pol. Kalibrovka (K, R, t) yoki poldagi belgilardan topilgan gomografiya H har bir box ning oyoq nuqtasini polga o'tkazadi, aniqligi — o'nlab santimetr.
- "Bu ikki box bitta odammi?" savoli poldagi masofaga aylanadi. Chegara ma'lumotdan: WILDTRACK da 75 sm bir xil odamlarni deyarli to'liq ulaydi va turli odamlarni 1% dan kam aralashtiradi.
- Uch kamera bir hududni ko'rsa, bir xil precision da ancha ko'p odam topiladi — eng yaxshi bitta kameradan ham sezilarli ko'p: to'silgan odam boshqa kamerada ko'rinadi, bir necha kamera ko'rgan nuqta ishonchliroq.
- Poldagi trek hamma kamera uchun bitta ID beradi. Past fps va olomonda faqat joy bo'yicha ulash yetarli emas: tezlik, tashqi ko'rinish va yuqoriroq fps kerak.
Mashqlar
birlashtirda chegarani 50 va 100 sm qilib, PR jadvalini qayta hisoblang. 75 sm haqiqatan ham eng yaxshimi?- Kameralarni boshqa tartibda qo'shing (C6, C2, C1). Natija o'zgaradimi? Nega tartibga bog'liq bo'lishi mumkin?
pol_trackerga 12-bobdagi niqoblangan rang gistogrammasini qo'shing: har bir klaster uchun uch kameradagi box lardan gistogramma yig'ing va ulash narxiga qo'shing. ID almashishi kamayadimi?- Qo'shimcha kamerani yuklab oling (C3) va to'rt kamerali PR egri chizig'ini chizing. To'rtinchi kamera uchinchisichalik foyda beradimi?
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.