Asosiy qismga o'tish

Apparat: bitta kompyuter nechta kamerani ko'taradi

18-bob 9 kod bloki 5209 so'z

Bu bobning kodi: kod/18_apparat.py — kitob papkasidan ishga tushiring: python kod/18_apparat.py

Savol: qancha kompyuter kerak

Mijoz 40 ta kamera bilan keladi. Bitta kompyuter yetadimi, ikkitami? Qanday GPU? Bu savolga "YOLO 1.5 ms da ishlaydi" degan javob yetmaydi: har bir kadr avval dekodlanadi (siqilgan videodan rasmga), keyin modelga tayyorlanadi, model ishlaydi, natija tozalanadi va tracker ga beriladi. Qaysi bosqich birinchi bo'lib to'lsa, tizim o'sha yerda to'xtaydi. Bu bobda har bir bosqichni alohida o'lchaymiz, keyin hammasini birga — N ta kamerada.

Hamma o'lchovlar bitta kompyuterda: NVIDIA RTX 5070 Ti (16 GB), WSL da 8 ta protsessor ipi. Kompyuterda boshqa dasturlar ham ishlab turgan — real serverdagi kabi.

Test videolari

Kamera ikki oqim beradi: asosiy (masalan, 1080p, 4 Mbit/s — yozib olish uchun) va ikkinchi (sub-stream, masalan, 640x360, 0.5 Mbit/s — ko'rish uchun). MOT17-09 dan shunday videolar tayyorlaymiz: 30 fps (dekodlash tezligi uchun) va 15 fps (real vaqt sinovi uchun).

kod/18_apparat.pyyangi fayl · 1-qator: from kitob_yordam import fig, show
python35 qator
import json
import re
import subprocess
import sys
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 scipy.optimize import linear_sum_assignment
from ultralytics import YOLO
from ultralytics.utils import LOGGER

LOGGER.setLevel("ERROR")
MANBA = "datasets/MOT17/train/MOT17-09-FRCNN/img1/%06d.jpg"
OUT = Path("out/apparat")
OUT.mkdir(parents=True, exist_ok=True)
KLIPLAR = {"1080p_h264_30fps": ("1920:1080", 30, ["-c:v", "libx264", "-b:v", "4000k", "-maxrate", "4000k", "-bufsize", "8000k"]),
           "1080p_h265_30fps": ("1920:1080", 30, ["-c:v", "libx265", "-b:v", "2000k", "-maxrate", "2000k", "-bufsize", "4000k",
                                                  "-x265-params", "log-level=error"]),
           "360p_h264_30fps": ("640:360", 30, ["-c:v", "libx264", "-b:v", "512k", "-maxrate", "512k", "-bufsize", "1024k"]),
           "1080p_h264_15fps": ("1920:1080", 15, ["-c:v", "libx264", "-b:v", "4000k", "-maxrate", "4000k", "-bufsize", "8000k"]),
           "360p_h264_15fps": ("640:360", 15, ["-c:v", "libx264", "-b:v", "512k", "-maxrate", "512k", "-bufsize", "1024k"])}
klip = {}
for nom, (olcham, fps, kod) in KLIPLAR.items():
    klip[nom] = OUT / f"MOT17-09_{nom}.mp4"
    if not klip[nom].exists():
        filtr = f"select=not(mod(n\\,2)),setpts=N/15/TB,scale={olcham}" if fps == 15 else f"scale={olcham}"
        subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-framerate", "30", "-i", MANBA, "-vf", filtr, "-r", str(fps), *kod,
                        "-g", str(2 * fps), "-pix_fmt", "yuv420p", str(klip[nom])], check=True)
show(pd.DataFrame([{"klip": n, "fayl, KB": f.stat().st_size // 1024} for n, f in klip.items()]))
klipfayl, KB
1080p_h264_30fps8 552
1080p_h265_30fps4 244
360p_h264_30fps1 078
1080p_h264_15fps8 498
360p_h264_15fps1 083

1-bosqich: dekodlash

ffmpeg videoni uch usulda dekodlaydi: protsessorda bitta ip bilan, protsessorning hamma iplari bilan va GPU dagi alohida video blokda — NVDEC (NVIDIA ning apparat dekoderi, h264_cuvid, hevc_cuvid). -f null natijani hech qayerga yozmaydi — sof dekodlash tezligi. -benchmark qancha protsessor vaqti ketganini ham aytadi: "CPU yadro" — shu vaqt ichida o'rtacha nechta yadro band bo'lgani.

kod/18_apparat.pydavomi
python15 qator
def ffmpeg_dekod(fayl, dekoder=None, iplar=None, takror=4):
    buyruq = ["ffmpeg", "-hide_banner", "-benchmark", *(["-threads", str(iplar)] if iplar else []), *(["-c:v", dekoder] if dekoder else []),
              "-stream_loop", str(takror - 1), "-i", str(fayl), "-f", "null", "-"]
    r = subprocess.run(buyruq, capture_output=True, text=True, check=True)
    kadr = int(re.findall(r"frame=\s*(\d+)", r.stderr)[-1])
    cpu_vaqt, soat = map(float, re.search(r"utime=([\d.]+)s.*rtime=([\d.]+)s", r.stderr).groups())
    return kadr / soat, cpu_vaqt / soat

qatorlar = []
for nom in ("1080p_h264_30fps", "1080p_h265_30fps", "360p_h264_30fps"):
    kodek = "hevc" if "h265" in nom else "h264"
    for usul, dekoder, iplar in (("CPU, 1 ip", None, 1), ("CPU, hamma ip", None, None), ("GPU, NVDEC", f"{kodek}_cuvid", None)):
        kadr_s, yadro = ffmpeg_dekod(klip[nom], dekoder, iplar)
        qatorlar.append({"video": nom, "usul": usul, "kadr/s": kadr_s, "CPU yadro": yadro, "1 yadroga kadr/s": kadr_s / yadro})
show(pd.DataFrame(qatorlar).round(2))
videousulkadr/sCPU yadro1 yadroga kadr/s
1080p_h264_30fpsCPU, 1 ip342.470.94363.76
1080p_h264_30fpsCPU, hamma ip1 460.364.61316.46
1080p_h264_30fpsGPU, NVDEC843.030.322 654.87
1080p_h265_30fpsCPU, 1 ip295.480.96309.14
1080p_h265_30fpsCPU, hamma ip1 070.343.66292.68
1080p_h265_30fpsGPU, NVDEC975.380.342 841.68
360p_h264_30fpsCPU, 1 ip2 755.910.863 215.93
360p_h264_30fpsCPU, hamma ip8 203.123.082 664.97
360p_h264_30fpsGPU, NVDEC1 433.450.226 402.44

Bitta protsessor yadrosi 1080p H.264 ni sekundiga bir necha yuz kadr dekodlaydi — 15 fps li kameradan yigirmatacha. NVDEC tezligi bitta oqimda protsessorning hamma iplaridan past, lekin u deyarli protsessor vaqti olmaydi — boshqa ishlarga yadrolar bo'shaydi. 360p ikkinchi oqimni dekodlash 1080p dan 8 barobarga yaqin arzon.

Lekin dekodlangan kadr hali Python ga yetib kelmadi. Uni uch xil yo'l bilan olamiz: OpenCV (cv2.VideoCapture, protsessorda), NVDEC dan to'liq 1080p kadr (quvur orqali), va NVDEC ning o'zida 640x360 ga kichraytirilgan kadr.

kod/18_apparat.pydavomi
python18 qator
def pythonga(fayl, usul, takror=4):
    t0, n = time.perf_counter(), 0
    if usul == "OpenCV (CPU)":
        for _ in range(takror):
            cap = cv2.VideoCapture(str(fayl))
            while cap.read()[0]:
                n += 1
        return n / (time.perf_counter() - t0)
    w, h, qoshimcha = (1920, 1080, []) if usul == "NVDEC, 1080p" else (640, 360, ["-resize", "640x360"])
    p = subprocess.Popen(["ffmpeg", "-loglevel", "error", "-c:v", "h264_cuvid", *qoshimcha, "-stream_loop", str(takror - 1), "-i", str(fayl),
                          "-f", "rawvideo", "-pix_fmt", "bgr24", "-"], stdout=subprocess.PIPE, bufsize=w * h * 3 * 4)
    while len(p.stdout.read(w * h * 3)) == w * h * 3:                      # har kadr: w*h*3 bayt BGR
        n += 1
    p.wait()
    return n / (time.perf_counter() - t0)

show(pd.DataFrame([{"Python ga kadr olish": u, "kadr/s": pythonga(klip["1080p_h264_30fps"], u)}
                   for u in ("OpenCV (CPU)", "NVDEC, 1080p", "NVDEC, 640x360 ga kichraytirib")]).round(0))
Python ga kadr olishkadr/s
OpenCV (CPU)939
NVDEC, 1080p157
NVDEC, 640x360 ga kichraytirib521

Kutilmagan natija: NVDEC dan to'liq 1080p kadrni Python ga olish OpenCV dan ancha sekin. Sabab — quvur: har bir 1080p kadr 6 MB, uni GPU dan protsessor xotirasiga ko'chirish va rang formatini (NV12 → BGR) protsessorda o'zgartirish dekodlashning o'zidan qimmat. Kadrni NVDEC ning o'zida 640x360 ga kichraytirsak (model baribir 640 da ishlaydi — 17-bob), ko'chiriladigan ma'lumot 9 marta kamayadi va tezlik bir necha barobar oshadi.

2-bosqich: model

YOLO11n ni turli rejimlarda o'lchaymiz, har safar 1080p kadrlar bilan va ultralytics ning to'liq predict i bilan (tayyorlash, model, natijani tozalash — hammasi). 8-bobda TensorRT eksportini ko'rgan edik; endi uni kamera kadrining shakliga (16:9 → 384x640) va bir nechta kadrni birdan (batch, 16 tagacha) qabul qiladigan qilib eksport qilamiz.

kod/18_apparat.pydavomi
python28 qator
dvigatel = Path("weights/yolo11n_384x640_b16.engine")
if not dvigatel.exists():
    Path(YOLO("weights/yolo11n.pt").export(format="engine", imgsz=(384, 640), half=True, dynamic=True, batch=16, device=0)).rename(dvigatel)
    Path("weights/yolo11n.onnx").unlink(missing_ok=True)                  # eksportning oraliq fayli
kadrlar = [cv2.imread(str(p)) for p in sorted(Path("datasets/MOT17/train/MOT17-09-FRCNN/img1").iterdir())[:64]]

def ms_kadr(model, batch, **kw):
    for _ in range(3):
        model.predict(kadrlar[:batch], verbose=False, **kw)              # qizdirish
    n, t0 = 0, time.perf_counter()
    while time.perf_counter() - t0 < 4:
        for i in range(0, 64, batch):
            model.predict(kadrlar[i:i + batch], verbose=False, **kw)
            n += batch
    return (time.perf_counter() - t0) / n * 1000

pt, trt = YOLO("weights/yolo11n.pt"), YOLO(str(dvigatel), task="detect")
pt_cpu = YOLO("weights/yolo11n.pt")                                       # alohida nusxa: model birinchi chaqiruvdagi qurilmada qoladi
sinovlar = [("PyTorch FP16, GPU", pt, 640, b, dict(half=True, device=0)) for b in (1, 8)] + \
           [("PyTorch FP16, GPU", pt, 1280, b, dict(half=True, device=0)) for b in (1, 8)] + \
           [("TensorRT FP16, GPU", trt, (384, 640), b, dict(device=0)) for b in (1, 8, 16)] + \
           [("PyTorch, faqat CPU", pt_cpu, 640, 1, dict(device="cpu"))]
qatorlar = []
for nom, model, imgsz, batch, kw in sinovlar:
    ms = ms_kadr(model, batch, imgsz=imgsz, classes=[0], **kw)
    qatorlar.append({"rejim": nom, "imgsz": str(imgsz), "batch": batch, "ms/kadr": ms, "kadr/s": 1000 / ms})
model_jadval = pd.DataFrame(qatorlar)
show(model_jadval.round(2))
rejimimgszbatchms/kadrkadr/s
PyTorch FP16, GPU64016.01166.41
PyTorch FP16, GPU64081.99503.2
PyTorch FP16, GPU128017.53132.77
PyTorch FP16, GPU128084.93202.9
TensorRT FP16, GPU(384, 640)13.08324.61
TensorRT FP16, GPU(384, 640)81.46684.11
TensorRT FP16, GPU(384, 640)161.38723.28
PyTorch, faqat CPU640115.7463.53
  • Batch eng katta yutuq: GPU bitta kadrda deyarli bo'sh turadi, 8–16 kadrni birdan olsa, har kadr bir necha barobar arzonlashadi. Ko'p kamerali tizimda batch tabiiy: har bir aylanishda hamma kameralarning oxirgi kadrlari birga yuboriladi.
  • TensorRT PyTorch dan tez, ayniqsa batch 1 da.
  • imgsz 1280 batch bilan 640 dan taxminan 2.5 barobar qimmat (17-bobda "to'rt barobar piksel" degan edik — piksel to'rt barobar, vaqt esa kamroq oshadi, chunki kichik modelda vaqtning bir qismi tayyorlash va natijani tozalashga ketadi).
  • Faqat CPU: bir kadrga ~16 millisekund — 8 ipda bir necha kamera. GPU siz tizim mumkin, lekin kichik (pastda o'lchaymiz).

Ikkinchi oqim yetadimi

imgsz=640 da model 1080p kadrni baribir 640x360 ga kichraytiradi. Unda kameraning 360p ikkinchi oqimini to'g'ridan-to'g'ri olsak-chi — dekodlash 1080p dan ancha arzon? Farq faqat siqish sifatida: ikkinchi oqim 0.5 Mbit/s bilan keladi. 17-bobdagi usul bilan tekshiramiz.

kod/18_apparat.pydavomi
python23 qator
gt = np.loadtxt("datasets/MOT17/train/MOT17-09-FRCNN/gt/gt.txt", delimiter=",")
gt = gt[gt[:, 7] == 1]
yaxshi = (gt[:, 8] >= 0.8) & (gt[:, 2] >= 0) & (gt[:, 3] >= 0) & (gt[:, 2] + gt[:, 4] <= 1920) & (gt[:, 3] + gt[:, 5] <= 1080)

def recall(fayl):
    cap, k, tp, jami = cv2.VideoCapture(str(fayl)), 0, 0, 0
    while True:
        ok, rasm = cap.read()
        if not ok:
            return 100 * tp / jami
        k += 1
        s = 1920 / rasm.shape[1]                                          # 360p box larini 1080p koordinataga qaytaramiz
        D = pt.predict(rasm, imgsz=640, conf=0.25, classes=[0], device=0, verbose=False)[0].boxes.xyxy.cpu().numpy() * s
        m = gt[:, 0] == k
        G = np.c_[gt[m, 2:4], gt[m, 2:4] + gt[m, 4:6]]
        t = np.zeros(len(G), bool)
        if len(G) and len(D):
            M = torchvision.ops.box_iou(torch.tensor(G), torch.tensor(D, dtype=torch.float64)).numpy()
            r, c = linear_sum_assignment(-M)
            t[r[M[r, c] >= 0.5]] = True
        tp, jami = tp + t[yaxshi[m]].sum(), jami + yaxshi[m].sum()

show(pd.DataFrame([{"oqim (imgsz 640)": n, "recall, %": recall(klip[n])} for n in ("1080p_h264_30fps", "360p_h264_30fps")]).round(1))
oqim (imgsz 640)recall, %
1080p_h264_30fps96.2
360p_h264_30fps93.4

imgsz=640 bilan ikkinchi oqim biroz past natija beradi (96.2% o'rniga 93.4%) — past bitreyt ning narxi. Evaziga dekodlash 8 barobarga yaqin arzon. Ko'p kamerali tizimda bu yaxshi murosa; aniqlik muhim hududlar uchun asosiy oqimni qoldiring. Asosiy oqim baribir yozib olish va yaqinlashtirilgan qirqimlar (yuz, kaska — 17-bob) uchun kerak.

Hammasi birga: N ta kamera

Endi haqiqiy sinov. Har bir soxta kamera — alohida ip, u videoni real vaqtda, sekundiga 15 kadr bilan beradi va faqat oxirgi kadrni saqlaydi. Asosiy sikl hamma kameralardan yangi kadrlarni yig'adi, 16 talik batch larda TensorRT ga beradi va har bir kameraning o'z ByteTrack iga uzatadi. Tizim ulgurmasa, kamera kadrni yangisi bilan almashtiradi — bu "tashlangan kadr". Bu kod keyingi bobdagi real oqimning soddalashtirilgan ko'rinishi; uni alohida faylga yozamiz, chunki bir nechta nusxasini parallel ishga tushiramiz.

kod/18_kamera_ishchisi.pyyangi fayl
python115 qator
# Bitta ishchi jarayon: N ta soxta kamera (real vaqtda 15 kadr/s), YOLO11n va har kameraga ByteTrack.
# Natija — bitta JSON qator. Misol: python kod/18_kamera_ishchisi.py --usul sub --kameralar 32
import argparse
import json
import os
import subprocess
import threading
import time

import numpy as np
import cv2
import psutil
from ultralytics import YOLO
from ultralytics.trackers import BYTETracker
from ultralytics.utils import LOGGER, YAML, IterableSimpleNamespace
from ultralytics.utils.checks import check_yaml

LOGGER.setLevel("ERROR")
FPS = 15
KLIP = {"cpu": "out/apparat/MOT17-09_1080p_h264_15fps.mp4",              # asosiy oqim, OpenCV dekodlaydi
        "gpu": "out/apparat/MOT17-09_1080p_h264_15fps.mp4",              # asosiy oqim, NVDEC dekodlaydi va 640x360 ga kichraytiradi
        "sub": "out/apparat/MOT17-09_360p_h264_15fps.mp4"}               # ikkinchi oqim, OpenCV dekodlaydi


class Kamera(threading.Thread):
    """Real vaqtda kadr beradigan soxta kamera. Faqat oxirgi kadr saqlanadi: olinmay almashtirilgani — tashlangan kadr."""
    def __init__(self, usul):
        super().__init__(daemon=True)
        self.usul, self.kadr, self.berildi, self.olindi, self.ish = usul, None, 0, 0, True
        self.qulf = threading.Lock()

    def run(self):
        if self.usul == "gpu":
            p = subprocess.Popen(["ffmpeg", "-loglevel", "error", "-re", "-c:v", "h264_cuvid", "-resize", "640x360", "-stream_loop", "-1",
                                  "-i", KLIP["gpu"], "-f", "rawvideo", "-pix_fmt", "bgr24", "-"], stdout=subprocess.PIPE)
            while self.ish:                                              # -re: ffmpeg o'zi real vaqtda beradi
                b = p.stdout.read(640 * 360 * 3)
                if len(b) < 640 * 360 * 3:
                    break
                self.qoy(np.frombuffer(b, np.uint8).reshape(360, 640, 3))
            p.kill()
            return
        cap, keyingi = cv2.VideoCapture(KLIP[self.usul]), time.perf_counter()
        while self.ish:
            ok, rasm = cap.read()
            if not ok:                                                   # video tugadi — boshidan
                cap = cv2.VideoCapture(KLIP[self.usul])
                continue
            keyingi += 1 / FPS
            time.sleep(max(0.0, keyingi - time.perf_counter()))
            self.qoy(rasm)

    def qoy(self, rasm):
        with self.qulf:
            self.kadr, self.berildi = rasm, self.berildi + 1

    def ol(self):
        with self.qulf:
            rasm, self.kadr = self.kadr, None
        self.olindi += rasm is not None
        return rasm


def main():
    p = argparse.ArgumentParser()
    p.add_argument("--usul", choices=list(KLIP), default="sub")
    p.add_argument("--kameralar", type=int, default=16)
    p.add_argument("--soniya", type=float, default=20)
    p.add_argument("--model", default="weights/yolo11n_384x640_b16.engine")
    p.add_argument("--qurilma", default="0")
    a = p.parse_args()

    model = YOLO(a.model, task="detect")
    qurilma = "cpu" if a.qurilma == "cpu" else int(a.qurilma)
    for b in range(1, 17):                                               # har batch o'lchami uchun qizdiramiz
        model.predict([np.zeros((360, 640, 3), np.uint8)] * b, imgsz=(384, 640), device=qurilma, verbose=False)
    bt = IterableSimpleNamespace(**YAML.load(check_yaml("bytetrack.yaml")))
    kameralar = [Kamera(a.usul) for _ in range(a.kameralar)]
    trackerlar = [BYTETracker(bt, frame_rate=FPS) for _ in kameralar]
    for k in kameralar:
        k.start()
    time.sleep(2)
    for k in kameralar:
        k.berildi = k.olindi = 0
    smi = subprocess.Popen(["nvidia-smi", "--query-gpu=utilization.gpu,utilization.decoder,memory.used",
                            "--format=csv,noheader,nounits", "-lms", "500"], stdout=subprocess.PIPE, text=True)
    psutil.cpu_percent(None)
    t0 = time.perf_counter()
    while time.perf_counter() - t0 < a.soniya:
        tayyor = [(i, r) for i, k in enumerate(kameralar) if (r := k.ol()) is not None]
        if not tayyor:
            time.sleep(0.001)
            continue
        for j in range(0, len(tayyor), 16):                              # dvigatel 16 tagacha batch qabul qiladi
            qism = tayyor[j:j + 16]
            natija = model.predict([r for _, r in qism], imgsz=(384, 640), device=qurilma, verbose=False, classes=[0])
            for (i, rasm), r in zip(qism, natija):
                trackerlar[i].update(r.boxes.cpu().numpy(), rasm)
    vaqt = time.perf_counter() - t0
    cpu = psutil.cpu_percent(None)
    smi.terminate()
    gpu = np.array([[float(x) for x in q.split(",")] for q in smi.stdout.read().split("\n") if q.strip()])
    for k in kameralar:
        k.ish = False
    for k in kameralar:
        k.join(timeout=2)                                                # ffmpeg jarayonlari ham yopiladi
    berildi = sum(k.berildi for k in kameralar) / vaqt / len(kameralar)
    ishlandi = sum(k.olindi for k in kameralar) / vaqt / len(kameralar)
    print(json.dumps({"berildi": berildi, "ishlandi": ishlandi, "tashlandi": 100 * (1 - ishlandi / berildi), "cpu": cpu,
                      "gpu": gpu[:, 0].mean(), "nvdec": gpu[:, 1].mean(), "gpu_xotira": gpu[:, 2].max(),
                      "ram": psutil.Process(os.getpid()).memory_info().rss / 2**20}))


if __name__ == "__main__":
    main()

Uch xil kirish yo'lini 1 dan 64 tagacha kamera bilan sinaymiz. Har bir sinov 20 soniya.

kod/18_apparat.pydavomi
python32 qator
def gpu_xotira():
    return float(subprocess.run(["nvidia-smi", "--query-gpu=memory.used", "--format=csv,noheader,nounits"],
                                capture_output=True, text=True).stdout)

def ishchilar(usul, kameralar, nusxa=1, **kw):
    """nusxa ta ishchi jarayonni parallel ishga tushiradi, har biri `kameralar` ta kamera bilan."""
    buyruq = [sys.executable, "kod/18_kamera_ishchisi.py", "--usul", usul, "--kameralar", str(kameralar),
              *[x for k, v in kw.items() for x in (f"--{k}", str(v))]]
    jarayonlar = [subprocess.Popen(buyruq, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True) for _ in range(nusxa)]
    return [json.loads(p.communicate()[0].strip().splitlines()[-1]) for p in jarayonlar]

asos = gpu_xotira()
print(f"sinovdan oldin GPU xotirasi band: {asos:.0f} MB (16 GB dan)")
NOMI = {"cpu": "1080p, OpenCV (CPU)", "gpu": "1080p, NVDEC + kichraytirish", "sub": "360p ikkinchi oqim, OpenCV"}
qatorlar = []
for usul in NOMI:
    for n in (1, 16, 32, 48, 64):
        r = ishchilar(usul, n)[0]
        qatorlar.append({"kirish": NOMI[usul], "kameralar": n, "kameraga ishlangan kadr/s": r["ishlandi"], "tashlandi, %": r["tashlandi"],
                         "CPU, %": r["cpu"], "GPU, %": r["gpu"], "NVDEC, %": r["nvdec"], "GPU xotirasi, MB": r["gpu_xotira"]})
sigim = pd.DataFrame(qatorlar)
show(sigim.round(1))

_, ax = plt.subplots(figsize=(8, 4.5))
for usul, nom in NOMI.items():
    q = sigim[sigim["kirish"] == nom]
    ax.plot(q["kameralar"], q["kameraga ishlangan kadr/s"], marker="o", label=nom)
ax.axhline(15, color="gray", ls="--", lw=0.8)
ax.set_xlabel("kameralar soni (bitta jarayon)")
ax.set_ylabel("har kameraga ishlangan kadr/s")
ax.legend(fontsize=8)
fig("sigim.png", "Bitta Python jarayoni: kameralar ko'paygan sari har kameraga qancha kadr yetadi (kameralar sekundiga 15 kadr beradi).")
Chiqish
sinovdan oldin GPU xotirasi band: 1954 MB (16 GB dan)
kirishkameralarkameraga ishlangan kadr/stashlandi, %CPU, %GPU, %NVDEC, %GPU xotirasi, MB
1080p, OpenCV (CPU)1150.72.84.903 381
1080p, OpenCV (CPU)16150.632.917.203 598
1080p, OpenCV (CPU)3211.921.351.311.803 330
1080p, OpenCV (CPU)485.862.168.48.803 345
1080p, OpenCV (CPU)641.391.282.89.403 538
1080p, NVDEC + kichraytirish1150.72.43.92.23 693
1080p, NVDEC + kichraytirish16150.916.818.39.48 826
1080p, NVDEC + kichraytirish3214.72.625.519.621.214 494
1080p, NVDEC + kichraytirish48474.739.76347.815 839
1080p, NVDEC + kichraytirish642.385.352.163.964.815 800
360p ikkinchi oqim, OpenCV115122.203 002
360p ikkinchi oqim, OpenCV1615112.420.202 809
360p ikkinchi oqim, OpenCV3215119.11902 809
360p ikkinchi oqim, OpenCV48112720.515.202 985
360p ikkinchi oqim, OpenCV647.94922.71502 791

Bitta Python jarayoni: kameralar ko'paygan sari har kameraga qancha kadr yetadi (kameralar sekundiga 15 kadr beradi).

Bitta Python jarayoni: kameralar ko'paygan sari har kameraga qancha kadr yetadi (kameralar sekundiga 15 kadr beradi).

Bitta jarayon 1080p asosiy oqim bilan 16 kamerani, 360p ikkinchi oqim bilan 32 kamerani to'liq ko'taradi. Undan keyin kadrlar tashlana boshlaydi — lekin GPU ning yuklanishiga qarang: u hech qachon to'lmaydi. Protsessor ham to'liq band emas. Unda nima to'lyapti? Bitta Python jarayonining asosiy sikli: har bir kadr uchun predict ning tayyorlash va tozalash qismi, tracker, natijalarni ko'chirish — hammasi bitta ipda (Python GIL). Sikl sekundiga bir necha yuz kadrdan ko'pini ulgurmaydi.

NVDEC yo'li 48 kamerada boshqa sabab bilan qulaydi — oxirgi ustunga qarang. Har bir ffmpeg jarayoni GPU da o'z CUDA kontekstini ochadi va ~350 MB xotira oladi (16 dan 32 kameragacha xotira 5.7 GB ga oshdi); 48 kamerada 16 GB karta to'ladi. NVDEC ni o'nlab kamerada ishlatish uchun hamma dekoderlarni bitta jarayonda ushlab turadigan vosita kerak — masalan, NVIDIA DeepStream yoki PyNvVideoCodec (bu kitobda o'lchanmagan).

Buni tekshirish oson: bitta jarayon o'rniga bir nechtasini ishga tushiramiz, har biriga 32 kamera.

kod/18_apparat.pydavomi
python8 qator
qatorlar = []
for nusxa in (1, 2, 3):
    natija = ishchilar("sub", 32, nusxa)
    qatorlar.append({"jarayonlar": nusxa, "jami kameralar": 32 * nusxa, "kameraga ishlangan kadr/s": np.mean([r["ishlandi"] for r in natija]),
                     "jami ishlangan kadr/s": sum(r["ishlandi"] for r in natija) * 32, "GPU, %": natija[0]["gpu"], "CPU, %": natija[0]["cpu"],
                     "GPU xotirasi, MB (jarayonga)": (max(r["gpu_xotira"] for r in natija) - asos) / nusxa,
                     "RAM, MB (jarayonga)": np.mean([r["ram"] for r in natija])})
show(pd.DataFrame(qatorlar).round(1))
jarayonlarjami kameralarkameraga ishlangan kadr/sjami ishlangan kadr/sGPU, %CPU, %GPU xotirasi, MB (jarayonga)RAM, MB (jarayonga)
13215479.718.818.88372 603.4
26413.2841.84137.91 175.52 615.4
39610.91 048.85254.51 1472 616.9

Jarayonlar qo'shilgan sari tizimning umumiy o'tkazuvchanligi oshadi (sekundiga ~480 dan ~1050 kadrgacha) va GPU yuklanishi ko'tariladi — cheklov haqiqatan Python siklida edi. O'sish chiziqli emas: jarayonlar bir GPU ni navbat bilan ishlatadi va 8 ta protsessor ipini bo'lishadi. Uchta jarayon bilan bizning kompyuter sekundiga ~1050 kadrga ulgurdi — bu taxminan 70 kamera × 15 kadr/s. Har bir ishchi jarayon ~2.6 GB operativ xotira va ~1 GB GPU xotirasi oladi. Amaliy xulosa: kameralarni guruhlarga bo'lib, har guruhga alohida ishchi jarayon bering.

Va GPU siz — faqat protsessorda:

kod/18_apparat.pydavomi
python2 qator
show(pd.DataFrame([{"kameralar": n, **{k: v for k, v in ishchilar("sub", n, model="weights/yolo11n.pt", qurilma="cpu", soniya=15)[0].items()
                                          if k in ("ishlandi", "tashlandi", "cpu")}} for n in (1, 2, 4)]).round(1))
kameralarishlanditashlandicpu
115.11.335.8
2150.955.9
4151.386.9

Protsessorning o'zida (8 ip) YOLO11n to'rtta kamerani sekundiga 15 kadr bilan ko'taradi — batch bu yerda ham yordam beradi — lekin protsessor deyarli to'liq band bo'ladi va boshqa ishlarga joy qolmaydi. Kichik do'kon uchun bu yetishi mumkin; odam sanash uchun sekundiga 5 kadr ham yetadi — unda bir necha barobar ko'p kamera sig'adi.

Hisob-kitob qoidalari

O'lchovlardan loyihalash uchun qoidalar:

  1. Birinchi to'ladigan narsa — GPU emas. Kichik modelda (YOLO11n) tizimni dekodlash, tayyorlash va Python sikli cheklaydi. Kameralarni bir nechta ishchi jarayonga bo'ling.
  2. Ikkinchi oqimni ishlating. imgsz=640 bilan detection uchun kameraning 360p oqimi deyarli bir xil natija beradi va dekodlash bir necha barobar arzon. Asosiy oqim — yozib olish va qirqimlar uchun.
  3. Batch. Bir necha kameraning kadrlarini birga yuboring; TensorRT dvigatelini kerakli batch bilan eksport qiling.
  4. fps ni vazifaga moslang. Odam sanash va zonalar uchun sekundiga 5–10 kadr ko'pincha yetadi; tracker uchun past fps da ByteTrack ning track_buffer i va Kalman tezlik modeli muhimroq bo'ladi (15-bob). 15 fps o'rniga 10 fps — sig'im bir yarim barobar ko'p.
  5. Zaxira qoldiring. Sinovlarimiz faqat detection va tracker; real tizimda hodisalarni yozish, video saqlash va web-panel ham resurs oladi. O'lchangan sig'imning 60–70% i bilan loyihalang.
  6. Xotira. Har bir ishchi jarayon ~2.6 GB operativ xotira va ~1 GB GPU xotirasi (bizning o'lchov). NVDEC ni har kameraga alohida ffmpeg bilan ishlatsangiz, har biriga yana ~350 MB GPU xotirasi qo'shing.
  7. Tarmoq va disk (17-bob): kameralar soni × bitreyt. Yozib olish asosiy oqimda bo'lsa, disk hajmi — shu.

Bizning kompyuterning sig'imi — yuqoridagi jadvallarda. Boshqa kompyuterda bu sonlar boshqacha bo'ladi: bu bobning kodini (kod/18_kamera_ishchisi.py) o'sha kompyuterda ishga tushiring — bir necha daqiqada o'z sonlaringiz bo'ladi. Mijozga taklif shu o'lchovga tayanadi, taxminga emas.

Asosiy fikr

  • Tizim zanjir: dekodlash → tayyorlash → model → tozalash → tracker. Eng sekin bo'g'in sig'imni belgilaydi, va kichik modelda bu odatda model emas.
  • NVDEC protsessorni bo'shatadi, lekin 1080p kadrni Python ga ko'chirish qimmat: NVDEC ning o'zida kichraytiring yoki kameraning ikkinchi oqimini oling.
  • Batch va TensorRT modelni bir necha barobar tezlashtiradi; bitta Python jarayoni esa sekundiga bir necha yuz kadrdan ko'pini ulgurmaydi — bir nechta ishchi jarayon kerak.
  • Sig'imni o'z apparatingizda o'lchang: bu bobning ishchi skripti N ta kamerani real vaqtda soxtalashtiradi.

Mashqlar

  1. --usul sub bilan 32 kamerani 10 fps da sinang (skriptda FPS ni o'zgartiring). Sig'im qanchaga oshadi?
  2. Ishchi skriptga YOLO11n o'rniga YOLO11s dvigatelini bering (xuddi shunday eksport qiling). Endi birinchi bo'lib nima to'ladi — GPU mi yoki Python sikli?
  3. Asosiy siklda tracker ni o'chirib (faqat detection), bitta jarayonning sig'imini qayta o'lchang. Tracker qancha vaqt olayotgan ekan?

Bu bobni bepul o'qidingiz.

Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.

Telegramda yozish Narxlar