Asosiy qismga o'tish

Ko'p kamera: bitta maydon, bir necha ko'z

15-bob 10 kod bloki 7638 so'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.

kod/yuklab_olish.pydavomi
python71 qator
# 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.

kod/15_kop_kamera.pyyangi fayl · 1-qator: from kitob_yordam import fig, show
python34 qator
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))
Chiqish
150 kadr | 3239 odam-kadr | 155 xil odam | kadrda o'rtacha 21.6 odam
kamerakalibrovka fayliko'rgan odam-kadrulushi, %
C1CVLab12 87688.8
C2CVLab22 56679.2
C3CVLab31 90358.8
C4CVLab457717.8
C5IDIAP175823.4
C6IDIAP22 99892.6
C7IDIAP378024.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.

kod/15_kop_kamera.pydavomi
python28 qator
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))
Chiqish
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
kamerabox larfarq, px (median)box balandligiga nisbatan, % (median)
C12 8767.34.3
C22 5666.32.6
C31 90310.83.5
C45777.73.3
C57588.32.7
C62 9982.52
C778016.36.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.

kod/15_kop_kamera.pydavomi
python16 qator
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))
kamerafarq, sm (median)90% i shundan kamkameradan 10 m gacha10 m dan uzoq
C113.919.9714.3
C27.915.56.48.6
C614.927.66.415.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.

kod/15_kop_kamera.pydavomi
python51 qator
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))
kamerako'rinadigan odam-kadrrasmda topildi (IoU >= 0.5), %polda topildi (<= 50 sm), %poldagi nuqtalarprecision, %
C12 87667.257.32 86857.9
C22 56666.653.82 16366.6
C62 99853.151.32 25973.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.

kod/15_kop_kamera.pydavomi
python11 qator
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))
Chiqish
bir odam, ikki kamera: 2527 juft, median 31 sm | turli odamlar: 48608 juft, median 815 sm
chegara, smbir odam: shu masofada, %turli odamlar: shu masofada, %
5080.060.19
7598.220.94
1001002.32
1501004.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.

kod/15_kop_kamera.pydavomi
python49 qator
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.")
kameralareng yuqori recall, %recall, precision >= 90%recall, precision >= 80%recall, precision >= 70%
C151.3025.740.8
C244.5NaN33.542.1
C651.419.74451.4
C1 + C269.432.85462.5
C1 + C67030.84961.9
C2 + C672.830.657.670.5
C1 + C2 + C681.848.867.974.8

Poldagi nuqtalar uchun precision-recall: yupqa — bitta kamera, o'rta — ikkita, qalin — uchala kamera birga.

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:

  1. Qamrov va to'silish. Bir kamerada to'silgan odam boshqasida ko'rinadi.
  2. 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

kod/15_kop_kamera.pydavomi
python30 qator
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.

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.

kod/15_kop_kamera.pydavomi
python53 qator
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))
Chiqish
GT: ketma-ket kadrlar orasida siljish — median 53 sm, 75% i 68 sm gacha, 95% i 92 sm gacha
kameralartezliktopilgan odam-kadr, %ID larbir odamga IDID almashishialmashish / topilgan, %
C1False25.782382.415218.2
C1True25.781861.7789.34
C1 + C2 + C6False68.573773.332814.77
C1 + C2 + C6True68.573212.191988.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.

kod/15_kop_kamera.pydavomi
python24 qator
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))
Chiqish
sinov: C1 dagi 1934 ta detection
to'liq kalibrovka (K, R, t) bilan xato: median 23.9 sm
pol xaritasixato, sm (median)eng yomon 10% tanlovda
4 ta belgidan32.797.4
8 ta belgidan28.429.9
20 ta belgidan28.128.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

  1. birlashtir da chegarani 50 va 100 sm qilib, PR jadvalini qayta hisoblang. 75 sm haqiqatan ham eng yaxshimi?
  2. Kameralarni boshqa tartibda qo'shing (C6, C2, C1). Natija o'zgaradimi? Nega tartibga bog'liq bo'lishi mumkin?
  3. pol_tracker ga 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?
  4. 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.

Telegramda yozish Narxlar