Asosiy qismga o'tish

Video oqim: kameradan tracker gacha

19-bob 9 kod bloki 5862 so'z

Bu bobning kodi: kod/19_video_oqim.py — kitob papkasidan ishga tushiring: python kod/19_video_oqim.py

Fayl emas, oqim

Shu paytgacha videolarni diskdan o'qidik. Real tizimda kadrlar tarmoq orqali, to'xtovsiz keladi, va bu uchta yangi muammo tug'diradi:

  1. Kechikish. Tizim kadrni qancha kechikib ko'radi? "Kaskasiz odam" signali 10 soniya kechiksa, foydasi kam.
  2. Ortda qolish. Ishlov berish kadrlar kelishidan sekin bo'lsa, navbat o'sadi — va kechikish ham.
  3. Uzilishlar. Kamera o'chadi, tarmoq uziladi, kamera "qotib" qoladi. Tizim o'zi qayta ulanishi va buni xabar qilishi kerak.

Bu bobda uchalasini o'lchaymiz. Buning uchun kompyuterda haqiqiy RTSP server va soxta kameralar ishga tushiramiz.

RTSP va MediaMTX

IP kameralar videoni odatda RTSP protokoli bilan beradi: kamera — server, biz — mijoz. Manzil shunday ko'rinadi: rtsp://login:parol@192.168.1.64:554/yo'l. Yo'l ishlab chiqaruvchiga bog'liq — masalan, Hikvision da odatda /Streaming/Channels/101 (asosiy oqim) va /102 (ikkinchi oqim), Dahua da /cam/realmonitor?channel=1&subtype=0 va subtype=1. Aniq yo'lni kamera hujjatidan yoki ONVIF orqali oling.

Bizda haqiqiy kamera yo'q, shuning uchun o'rtaga MediaMTX ni qo'yamiz — ochiq kodli RTSP server (MIT litsenziyasi, bitta fayl). Soxta kamera videoni unga yuboradi (publish), bizning dastur undan o'qiydi (read). Real ob'ektda ham MediaMTX foydali: u har bir kameraga bitta ulanish ochadi va o'nlab o'quvchiga tarqatadi (kameralar ko'p ulanishni yoqtirmaydi), uzilganda kameraga o'zi qayta ulanadi va xohlasangiz videoni diskka yozadi.

O'rnatish (bir marta; kitob papkasidan):

Terminal
bash5 qator
mkdir -p tools/mediamtx && cd tools/mediamtx
curl -LO https://github.com/bluenviron/mediamtx/releases/download/v1.21.1/mediamtx_v1.21.1_linux_amd64.tar.gz
curl -LO https://github.com/bluenviron/mediamtx/releases/download/v1.21.1/checksums.sha256
sha256sum -c --ignore-missing checksums.sha256          # "OK" chiqishi shart
tar xzf mediamtx_v1.21.1_linux_amd64.tar.gz

Nazorat yig'indisini tekshirish majburiy qadam: internetdan olingan dasturga, ayniqsa kameralar oqimini ko'radigan dasturga, faqat asl ekaniga ishonch hosil qilgandan keyin ruxsat beriladi.

Soxta kamera: kadrga vaqt yozilgan

Kechikishni aniq o'lchash uchun har bir kadrning chap yuqori burchagiga u yuborilgan vaqtni yozamiz: millisekundlardagi vaqtning oxirgi 24 biti — 24 ta oq-qora katak. Kadr qabul qiluvchiga yetganda kodni o'qiymiz va hozirgi vaqtdan ayiramiz. Ikkalasi bitta kompyuterda — soat bir xil. Bu kechikish kodlash (x264), tarmoq, server, dekodlash va bizning ishlovimizni o'z ichiga oladi.

kod/19_soxta_kamera.pyyangi fayl
python123 qator
# Soxta IP kameralar: MOT17-09 kadrlarini RTSP serverga real vaqtda yuboradi (k1, k2, ...).
# Har kadrning chap yuqori burchagida — yuborilgan vaqt (ms) 24 ta oq-qora katak ko'rinishida.
# Misol: python kod/19_soxta_kamera.py --url rtsp://kamera:PAROL@127.0.0.1:8554/ --soni 4 --kenglik 640
import argparse
import signal
import subprocess
import sys
import threading
import time
from pathlib import Path

import cv2

BIT = 24


def katak(kenglik):
    return kenglik // 48                                                # 1920 da 40 px, 640 da 13 px


def vaqt_kodi(rasm, ms):
    k = katak(rasm.shape[1])
    for i in range(BIT):
        rasm[0:k, k * i:k * (i + 1)] = 255 if (ms >> (BIT - 1 - i)) & 1 else 0


def kodni_oqi(rasm):
    k = katak(rasm.shape[1])
    return sum(int(rasm[k // 2, k * i + k // 2].mean() > 128) << (BIT - 1 - i) for i in range(BIT))


class Kadrlar:
    """MOT17-09 kadrlari kerakli o'lchamda. Kichik o'lchamdagilar xotirada saqlanadi — hamma kameralar uchun bitta nusxa."""
    def __init__(self, kenglik, fps):
        self.yollar = sorted(Path("datasets/MOT17/train/MOT17-09-FRCNN/img1").iterdir())[::30 // fps]
        self.w, self.h, self.kesh, self.qulf = kenglik, kenglik * 9 // 16, {}, threading.Lock()

    def __getitem__(self, i):
        i %= len(self.yollar)
        with self.qulf:
            if i in self.kesh:
                return self.kesh[i]
        rasm = cv2.imread(str(self.yollar[i]))
        if self.w != rasm.shape[1]:
            rasm = cv2.resize(rasm, (self.w, self.h), interpolation=cv2.INTER_AREA)
        if self.w < 1920:
            with self.qulf:
                self.kesh[i] = rasm
        return rasm


class VideoKadrlar:
    """Video fayldan ketma-ket kadrlar, tugasa — boshidan yoki (bir_marta) to'xtaydi (22-bob). Bitta kamera uchun."""
    def __init__(self, yol, kenglik, bir_marta=False):
        self.yol, self.cap, self.w, self.h, self.qulf = yol, cv2.VideoCapture(yol), kenglik, kenglik * 9 // 16, threading.Lock()
        self.bir_marta = bir_marta

    def __getitem__(self, i):
        with self.qulf:
            ok, rasm = self.cap.read()
            if not ok and self.bir_marta:
                toxtat()
            if not ok:
                self.cap = cv2.VideoCapture(self.yol)
                ok, rasm = self.cap.read()
        return rasm if rasm.shape[1] == self.w else cv2.resize(rasm, (self.w, self.h), interpolation=cv2.INTER_AREA)


ENKODERLAR = []                                                         # to'xtatishda hammasini yopamiz


def kamera(url, kadrlar, fps, bitreyt):
    def ffmpeg():
        p = subprocess.Popen(["ffmpeg", "-loglevel", "quiet", "-f", "rawvideo", "-pix_fmt", "bgr24", "-s", f"{kadrlar.w}x{kadrlar.h}",
                              "-r", str(fps), "-i", "-", "-c:v", "libx264", "-preset", "veryfast", "-tune", "zerolatency",
                              "-b:v", bitreyt, "-maxrate", bitreyt, "-bufsize", bitreyt, "-g", str(2 * fps), "-pix_fmt", "yuv420p",
                              "-f", "rtsp", "-rtsp_transport", "tcp", url], stdin=subprocess.PIPE)
        ENKODERLAR.append(p)
        return p

    enkoder, keyingi, i = ffmpeg(), time.perf_counter(), 0
    while True:
        rasm = kadrlar[i].copy()
        keyingi = max(keyingi + 1 / fps, time.perf_counter() - 1)      # ko'p ortda qolsak, hammasini birdan quvmaymiz
        time.sleep(max(0.0, keyingi - time.perf_counter()))
        vaqt_kodi(rasm, int(time.time() * 1000) % 2 ** BIT)
        try:
            enkoder.stdin.write(rasm.tobytes())
        except BrokenPipeError:                                          # server uzdi — haqiqiy kamera kabi qayta ulanamiz
            enkoder.kill()
            time.sleep(1)
            enkoder = ffmpeg()
        i += 1


def toxtat(*_):
    for p in ENKODERLAR:
        p.kill()
    sys.exit(0)


def main():
    p = argparse.ArgumentParser()
    p.add_argument("--url", required=True, help="server manzili; oxiriga k1, k2, ... qo'shiladi")
    p.add_argument("--soni", type=int, default=1)
    p.add_argument("--fps", type=int, default=15)
    p.add_argument("--kenglik", type=int, default=1920)
    p.add_argument("--bitreyt", default="4000k")
    p.add_argument("--video", help="MOT17 o'rniga shu video fayl (bitta kamera)")
    p.add_argument("--bir-marta", action="store_true", help="video bir marta ko'rsatiladi, keyin kamera to'xtaydi")
    p.add_argument("--kechikish", type=float, default=0, help="shuncha soniyadan keyin yuborishni boshlash")
    a = p.parse_args()
    signal.signal(signal.SIGTERM, toxtat)
    time.sleep(a.kechikish)
    kadrlar = VideoKadrlar(a.video, a.kenglik, a.bir_marta) if a.video else Kadrlar(a.kenglik, a.fps)
    for n in range(1, a.soni + 1):
        threading.Thread(target=kamera, args=(f"{a.url}k{n}", kadrlar, a.fps, a.bitreyt), daemon=True).start()
    while True:
        time.sleep(1)


if __name__ == "__main__":
    main()

Server va kameralarni har bir tajriba uchun with blokida ishga tushiramiz — xato bo'lsa ham hammasi yopiladi. Parollar kodda emas: muhit o'zgaruvchisidan olinadi, bo'lmasa har safar tasodifiy yaratiladi. Server faqat shu kompyuterdan (127.0.0.1), faqat TCP orqali va faqat RTSP bilan ishlaydi; boshqa protokollari (RTMP, HLS, WebRTC, SRT) o'chirilgan. Ikki foydalanuvchi: kamera faqat yubora oladi, tahlil faqat o'qiy oladi.

kod/19_video_oqim.pyyangi fayl · 1-qator: from kitob_yordam import fig, show
python87 qator
import contextlib
import os
import re
import secrets
import signal
import subprocess
import sys
import threading
import time
from pathlib import Path

import cv2
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

sys.path.insert(0, "kod")                                              # kod/oqim.py va kod/19_soxta_kamera.py
kodni_oqi = __import__("19_soxta_kamera").kodni_oqi

KAMERA_PAROL = os.environ.get("KAMERA_PAROL") or secrets.token_urlsafe(16)
TAHLIL_PAROL = os.environ.get("TAHLIL_PAROL") or secrets.token_urlsafe(16)
SERVER = "127.0.0.1:8554"
SOZLAMA = Path("out/rtsp/mediamtx.yml")

def yashir(url):
    """Jurnalga yozishdan oldin paroldan tozalaymiz."""
    return re.sub(r"//[^/@]+@", "//***@", url)

@contextlib.contextmanager
def rtsp_tizim(soni=1, kenglik=1920, **kamera_sozlama):
    SOZLAMA.parent.mkdir(parents=True, exist_ok=True)
    SOZLAMA.write_text(f"""logLevel: error
rtsp: true
rtspTransports: [tcp]
rtspAddress: {SERVER}
rtmp: false
hls: false
webrtc: false
srt: false
moq: false
api: false
metrics: false
pprof: false
playback: false
authInternalUsers:
  - user: kamera
    pass: {KAMERA_PAROL}
    ips: ["127.0.0.1"]
    permissions:
      - action: publish
  - user: tahlil
    pass: {TAHLIL_PAROL}
    ips: ["127.0.0.1"]
    permissions:
      - action: read
paths:
  all_others:
""")
    server = subprocess.Popen(["tools/mediamtx/mediamtx", str(SOZLAMA)])
    time.sleep(1)
    kameralar = subprocess.Popen([sys.executable, "kod/19_soxta_kamera.py", "--url", f"rtsp://kamera:{KAMERA_PAROL}@{SERVER}/",
                                  "--soni", str(soni), "--kenglik", str(kenglik), "--bitreyt", "4000k" if kenglik >= 1920 else "512k",
                                  *[x for k, v in kamera_sozlama.items() for x in (f"--{k}", str(v))]])
    try:
        time.sleep(3)
        yield kameralar
    finally:
        kameralar.terminate()
        kameralar.wait()
        server.terminate()
        server.wait()
        SOZLAMA.unlink()                                                  # parollar yozilgan fayl ishdan keyin qolmaydi

def url(nom):
    return f"rtsp://tahlil:{TAHLIL_PAROL}@{SERVER}/{nom}"

def kechikish_ms(rasm):
    return (int(time.time() * 1000) - kodni_oqi(rasm)) % 2 ** 24

with rtsp_tizim():
    tinglash = subprocess.run(["ss", "-ltnup"], capture_output=True, text=True).stdout
    print("MediaMTX tinglayotgan manzillar:", sorted({q.split()[4] for q in tinglash.splitlines() if "mediamtx" in q}))
    for nom, u in (("parolsiz", f"rtsp://{SERVER}/k1"), ("noto'g'ri parol", f"rtsp://tahlil:xato@{SERVER}/k1"),
                   ("kamera paroli bilan o'qish", f"rtsp://kamera:{KAMERA_PAROL}@{SERVER}/k1"), ("tahlil paroli bilan", url("k1"))):
        cap = cv2.VideoCapture(u, cv2.CAP_FFMPEG)
        print(f"{nom:28s} {yashir(u):40s} ochildimi: {cap.isOpened()}")
        cap.release()
Chiqish
MediaMTX tinglayotgan manzillar: ['127.0.0.1:8554']
parolsiz                     rtsp://127.0.0.1:8554/k1                 ochildimi: False
noto'g'ri parol              rtsp://***@127.0.0.1:8554/k1             ochildimi: False
kamera paroli bilan o'qish   rtsp://***@127.0.0.1:8554/k1             ochildimi: False
tahlil paroli bilan          rtsp://***@127.0.0.1:8554/k1             ochildimi: True

Server faqat 127.0.0.1:8554 ni tinglaydi. Parolsiz, noto'g'ri parol bilan va kamera foydalanuvchisi bilan o'qib bo'lmaydi (u faqat yuborish huquqiga ega) — faqat tahlil. Jurnalda parol ko'rinmaydi.

Ortda qolish: navbat qanday o'sadi

Kamera sekundiga 15 kadr (har 67 ms da bitta) yuboradi. Bizning ishlov har kadrga 100 ms oladi deylik — og'ir model yoki ko'p kamera. Oddiy usul — cap.read() ni siklda chaqirish va har kadrni ishlash. Muqobili — alohida ipda to'xtovsiz o'qish va faqat oxirgi kadrni saqlash (18-bobdagi soxta kamera kabi): asosiy sikl ulgurmagan kadrlar tashlab yuboriladi.

kod/19_video_oqim.pydavomi
python42 qator
ISHLOV = 0.1                                                          # har kadrga 100 ms "ishlov"

def sodda(soniya=30):
    cap, t0, q = cv2.VideoCapture(url("k1"), cv2.CAP_FFMPEG, [cv2.CAP_PROP_N_THREADS, 1]), time.time(), []
    while time.time() - t0 < soniya:
        ok, rasm = cap.read()
        q.append((time.time() - t0, kechikish_ms(rasm)))
        time.sleep(ISHLOV)
    cap.release()
    return np.array(q)

def oxirgi_kadr(soniya=30):
    holat = {"kadr": None, "ish": True}
    def oqi():
        cap = cv2.VideoCapture(url("k1"), cv2.CAP_FFMPEG, [cv2.CAP_PROP_N_THREADS, 1])
        while holat["ish"]:
            ok, rasm = cap.read()
            if ok:
                holat["kadr"] = rasm
        cap.release()
    threading.Thread(target=oqi, daemon=True).start()
    t0, q = time.time(), []
    while time.time() - t0 < soniya:
        rasm, holat["kadr"] = holat["kadr"], None
        if rasm is None:
            time.sleep(0.005)
            continue
        q.append((time.time() - t0, kechikish_ms(rasm)))
        time.sleep(ISHLOV)
    holat["ish"] = False
    return np.array(q)

with rtsp_tizim():
    natija = {"har kadrni o'qish (sodda)": sodda(), "faqat oxirgi kadr (alohida ip)": oxirgi_kadr()}
_, ax = plt.subplots(figsize=(9, 4))
for nom, q in natija.items():
    ax.plot(q[:, 0], q[:, 1] / 1000, label=nom)
    print(f"{nom}: ishlangan kadrlar {len(q)}, kechikish boshida {q[:15, 1].mean() / 1000:.1f} s, oxirida {q[-15:, 1].mean() / 1000:.1f} s")
ax.set_xlabel("vaqt, s")
ax.set_ylabel("kechikish, s")
ax.legend()
fig("navbat.png", "Ishlov (100 ms) kadrlar kelishidan (67 ms) sekin: sodda o'qishda kechikish har soniya o'sadi, oxirgi kadrni olishda — o'zgarmaydi.")
Chiqish
har kadrni o'qish (sodda): ishlangan kadrlar 285, kechikish boshida 2.2 s, oxirida 12.6 s
faqat oxirgi kadr (alohida ip): ishlangan kadrlar 266, kechikish boshida 0.2 s, oxirida 0.1 s

Ishlov (100 ms) kadrlar kelishidan (67 ms) sekin: sodda o'qishda kechikish har soniya o'sadi, oxirgi kadrni olishda — o'zgarmaydi.

Ishlov (100 ms) kadrlar kelishidan (67 ms) sekin: sodda o'qishda kechikish har soniya o'sadi, oxirgi kadrni olishda — o'zgarmaydi.

Sodda usulda kechikish tinmay o'sadi: har kadrda 33 ms ortda qolamiz va eski kadrlar tarmoq buferlarida yig'iladi. 30 soniyadan keyin tizim o'n soniyadan ko'proq oldingi voqeani ko'rsatadi — va vaqt o'tgan sari yomonlashadi. Bu xato yashirin: kod ishlayapti, kadrlar kelyapti, faqat kech. Oxirgi kadr usulida esa kechikish joyida turadi — tizim ulgurmagan kadrlarni tashlaydi, lekin doim hozirgi holatni ko'radi. Real vaqt tizimi uchun bu to'g'ri tanlov.

Yashirin kechikish: dekoder iplari

Ishlovsiz, faqat o'qiganda ham kechikish kutilganidan katta chiqdi. Sababi — OpenCV ning dekoderi: u H.264 ni bir nechta ipda kadrlarni parallel dekodlaydi, va har bir qo'shimcha ip bitta kadr kechikish qo'shadi. Iplar sonini o'zgartirib o'lchaymiz.

kod/19_video_oqim.pydavomi
python13 qator
qatorlar = []
with rtsp_tizim():
    for iplar in (0, 4, 2, 1):
        cap = cv2.VideoCapture(url("k1"), cv2.CAP_FFMPEG, [cv2.CAP_PROP_N_THREADS, iplar] if iplar else [])
        t0, q = time.time(), []
        while time.time() - t0 < 10:
            ok, rasm = cap.read()
            if ok:
                q.append(kechikish_ms(rasm))
        qatorlar.append({"dekoder iplari": int(cap.get(cv2.CAP_PROP_N_THREADS)), "sozlama": "standart" if not iplar else f"{iplar}",
                         "kechikish, ms (median)": np.median(q[15:]), "95% i shundan kam": np.percentile(q[15:], 95)})
        cap.release()
show(pd.DataFrame(qatorlar).round(0))
dekoder iplarisozlamakechikish, ms (median)95% i shundan kam
8standart545728
44279475
22146301
1182448

Standart holatda OpenCV protsessorning hamma iplarini oladi — bizda 8 ta, va kechikishning katta qismi shu. Har bir ip taxminan bitta kadr (67 ms) qo'shadi. Kamera oqimi uchun bitta ip yetadi: 18-bobda bitta yadro 1080p ni sekundiga 300 dan ortiq kadr dekodlashini ko'rgan edik, kamera esa 15 ta beradi. Qolgan kechikish — kodlash, tarmoq va buferlar — 100 ms dan kam.

Uzilishlar: kamera o'chsa yoki qotsa

Kamera bilan ikki xil muammo bo'ladi. U o'chadi (qayta yuklanadi, tok, tarmoq kabeli) — ulanish uziladi. Yoki qotadi — ulanish ochiq, lekin kadr kelmaydi; bu ancha yomon, chunki read() hech narsa demay kutib turadi (OpenCV da standart kutish — 30 soniya). Ishonchli o'quvchi uch narsa qiladi: qisqa kutish muddatlari (ochish va o'qish — 3 soniya), uzilganda o'sib boruvchi pauza bilan qayta ulanish (1, 2, 4, ... soniya — kamera qayta yuklanayotganda uni so'rovlarga ko'mmaslik uchun) va oxirgi kadr qachon kelganini eslab qolish — "kamera jim" signali uchun. Buni keyingi boblarda ham ishlatamiz, shuning uchun alohida faylga yozamiz.

kod/oqim.pyyangi fayl
python61 qator
# Ishonchli RTSP o'quvchi: alohida ip, faqat oxirgi kadr, kutish muddatlari, qayta ulanish, holat.
import os
import re
import threading
import time

import cv2

# OpenCV FFmpeg sozlamalarini muhit o'zgaruvchisidan o'qiydi: faqat TCP va oqimni uzoq "tahlil qilmaslik" —
# aks holda har bir ochish ~2 s davom etadi va OpenCV ochishlarni navbat bilan bajaradi.
FFMPEG = "rtsp_transport;tcp|analyzeduration;0|probesize;32"
PARAMLAR = [cv2.CAP_PROP_N_THREADS, 1,                                  # 1 dekoder ipi — kechikish past
            cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 3000, cv2.CAP_PROP_READ_TIMEOUT_MSEC, 3000]


def yashir(url):
    return re.sub(r"//[^/@]+@", "//***@", url)


class Oqim(threading.Thread):
    def __init__(self, nom, url, jurnal=print, max_pauza=30):
        super().__init__(daemon=True, name=nom)
        self.nom, self.url, self.jurnal, self.max_pauza = nom, url, jurnal, max_pauza
        self.kadr, self.kadr_vaqti, self.qabul, self.olindi, self.ulanishlar = None, 0.0, 0, 0, 0
        self.ish, self.qulf = True, threading.Lock()

    def run(self):
        pauza = 1.0
        while self.ish:
            os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = FFMPEG               # har ochishdan oldin: boshqa kod o'zgartirgan bo'lishi mumkin
            cap = cv2.VideoCapture(self.url, cv2.CAP_FFMPEG, PARAMLAR)
            if not cap.isOpened():
                self.jurnal(f"{self.nom}: ulanmadi ({yashir(self.url)}), {pauza:.0f} s dan keyin yana")
                time.sleep(pauza)
                pauza = min(pauza * 2, self.max_pauza)
                continue
            self.ulanishlar += 1
            self.jurnal(f"{self.nom}: ulandi")
            pauza = 1.0
            while self.ish:
                ok, rasm = cap.read()
                if not ok:
                    self.jurnal(f"{self.nom}: oqim uzildi")
                    break
                with self.qulf:
                    self.kadr, self.kadr_vaqti, self.qabul = rasm, time.time(), self.qabul + 1
            cap.release()

    def ol(self):
        """Oxirgi yangi kadr yoki None (oxirgi chaqiruvdan beri yangi kadr kelmagan bo'lsa)."""
        with self.qulf:
            rasm, self.kadr = self.kadr, None
        self.olindi += rasm is not None
        return rasm

    def jimlik(self):
        """Oxirgi kadrdan beri necha soniya o'tdi — "kamera jim" signali uchun."""
        return time.time() - self.kadr_vaqti if self.kadr_vaqti else float("inf")

    def toxtat(self):
        self.ish = False

Sinov: 50 soniya davomida kamerani 5-soniyada o'chiramiz, 10-soniyada qayta yoqamiz, 20-soniyada qotiramiz (jarayonni SIGSTOP bilan to'xtatamiz — ulanish ochiq qoladi) va 35-soniyada davom ettiramiz. Sodda o'quvchi va Oqim birga ishlaydi.

kod/19_video_oqim.pydavomi
python55 qator
from oqim import Oqim

T0 = time.time()
jurnal, kadrlar = [], {"sodda": [], "Oqim": []}

def yoz(matn):
    jurnal.append({"vaqt, s": round(time.time() - T0, 1), "voqea": matn})

def sodda_oquvchi():
    cap = cv2.VideoCapture(url("k1"), cv2.CAP_FFMPEG, [cv2.CAP_PROP_N_THREADS, 1])
    while time.time() - T0 < 50:
        ok, _ = cap.read()
        if not ok:
            yoz("sodda: read() = False — sikl tugadi")
            return
        kadrlar["sodda"].append(time.time() - T0)

with rtsp_tizim() as kamera:
    T0 = time.time()
    oqim = Oqim("k1", url("k1"), jurnal=yoz)
    oqim.start()
    threading.Thread(target=sodda_oquvchi, daemon=True).start()
    def yig():
        while time.time() - T0 < 50:
            if oqim.ol() is not None:
                kadrlar["Oqim"].append(time.time() - T0)
            time.sleep(0.005)
    threading.Thread(target=yig, daemon=True).start()
    rejalar = [(5, "KAMERA o'chdi"), (10, "KAMERA yondi"), (20, "KAMERA qotdi"), (35, "KAMERA davom etdi"), (50, None)]
    for vaqt, voqea in rejalar:
        time.sleep(max(0.0, vaqt - (time.time() - T0)))
        if voqea:
            yoz(voqea)
        if voqea == "KAMERA o'chdi":
            kamera.terminate()
            kamera.wait()
        elif voqea == "KAMERA yondi":
            kamera = subprocess.Popen([sys.executable, "kod/19_soxta_kamera.py", "--url", f"rtsp://kamera:{KAMERA_PAROL}@{SERVER}/"])
        elif voqea == "KAMERA qotdi":
            os.kill(kamera.pid, signal.SIGSTOP)
        elif voqea == "KAMERA davom etdi":
            os.kill(kamera.pid, signal.SIGCONT)
    oqim.toxtat()
    kamera.terminate()
    kamera.wait()
show(pd.DataFrame(jurnal), 40)

_, axes = plt.subplots(2, 1, figsize=(11, 3.6), sharex=True)
for ax, (nom, q) in zip(axes, kadrlar.items()):
    ax.hist(q, bins=np.arange(0, 51, 1), color="C0")
    ax.axvspan(5, 10, color="red", alpha=0.15)
    ax.axvspan(20, 35, color="orange", alpha=0.2)
    ax.set_ylabel(f"{nom}\nkadr/s", fontsize=9)
axes[1].set_xlabel("vaqt, s  (qizil — kamera o'chiq, sariq — kamera qotgan)")
fig("uzilish.png", "Har soniyada qabul qilingan kadrlar. Sodda o'quvchi birinchi uzilishda butunlay to'xtaydi; Oqim ikkala holatdan keyin ham o'zi tiklanadi.")
vaqt, svoqea
0.2k1: ulandi
5KAMERA o'chdi
5sodda: read() = False — sikl tugadi
5k1: oqim uzildi
5k1: ulanmadi (rtsp://***@127.0.0.1:8554/k1), 1 s dan keyin yana
6k1: ulanmadi (rtsp://***@127.0.0.1:8554/k1), 2 s dan keyin yana
8k1: ulanmadi (rtsp://***@127.0.0.1:8554/k1), 4 s dan keyin yana
10KAMERA yondi
12.1k1: ulandi
20KAMERA qotdi
26.1k1: oqim uzildi
29.1k1: ulandi
30k1: oqim uzildi
30k1: ulanmadi (rtsp://***@127.0.0.1:8554/k1), 1 s dan keyin yana
31k1: ulanmadi (rtsp://***@127.0.0.1:8554/k1), 2 s dan keyin yana
33k1: ulanmadi (rtsp://***@127.0.0.1:8554/k1), 4 s dan keyin yana
35KAMERA davom etdi
37k1: ulandi

Har soniyada qabul qilingan kadrlar. Sodda o'quvchi birinchi uzilishda butunlay to'xtaydi; Oqim ikkala holatdan keyin ham o'zi tiklanadi.

Har soniyada qabul qilingan kadrlar. Sodda o'quvchi birinchi uzilishda butunlay to'xtaydi; Oqim ikkala holatdan keyin ham o'zi tiklanadi.

Jurnal va rasm uzilishlar bilan ishlashni ko'rsatadi:

  • Sodda o'quvchi birinchi uzilishda read() dan False oladi va sikl tugaydi. Kamera qaytib kelsa ham tizim ko'r qoladi — va agar hech kim jurnalga qaramasa, buni haftalab hech kim sezmaydi.
  • Oqim kamera o'chganda o'sib boruvchi pauza (1, 2, 4 s) bilan urinib turadi va kamera qaytgach keyingi urinishda ulanadi — bu safar 2 soniyada. Pauza o'sgan sari bu vaqt ham uzayadi (max_pauza — 30 s): uzoq uzilishdan keyin tiklanish o'n soniyalab olishi mumkin, evaziga o'chgan kamerani so'rovlarga ko'mmaymiz.
  • Qotgan kamera: Oqim uzilishni 6 soniyada aniqladi (o'qish kutish muddati 3 s bo'lsa ham) va qayta ulandi — server kamerani hali "tirik" deb bilardi. 10 soniyadan keyin MediaMTX jim kamerani o'zi uzdi (uning readTimeout i). Kamera "tirilgach", u serverga qayta ulandi va Oqim oqimni 2 soniyada yana topdi.

Muhim tafsilot — jimlik(). Kamera o'chganini yoki qotganini tizim jurnalga yozishi yetarli emas: odam bilishi kerak. Keyingi bobda "kamera 30 soniyadan beri jim" holatini Telegram signaliga aylantiramiz.

Ko'p kamerani ochish: navbat

Birinchi marta ko'p kamerali quvurni ishga tushirganimizda kameralar birma-bir, har biri ~2 soniyada ulangan edi — 16 kamera yarim daqiqada. Sababi ikkita. Birinchisi: oqimni ochganda FFmpeg uni "tahlil qiladi" — ma'lumot yig'adi, odatda keyingi to'liq kadrgacha (bizda har 2 soniyada). Ikkinchisi: OpenCV oqim ochishni bitta umumiy qulf ostida bajaradi — parallel iplardan ochsak ham, ular navbat bilan ochiladi. Tahlilni o'chirsak-chi? Kamera oqim haqidagi asosiy ma'lumotni RTSP ulanishning boshida (SDP da) baribir yuboradi.

kod/19_video_oqim.pydavomi
python22 qator
def ochish_vaqtlari(sozlama):
    os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = sozlama
    vaqtlar, ochildi = {}, {}
    def och(i):
        t = time.time()
        cap = cv2.VideoCapture(url(f"k{i}"), cv2.CAP_FFMPEG, [cv2.CAP_PROP_N_THREADS, 1, cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 10000])
        vaqtlar[i], ochildi[i] = time.time() - t, cap.isOpened() and cap.read()[0]
        cap.release()
    t0 = time.time()
    iplar = [threading.Thread(target=och, args=(i,)) for i in range(1, 9)]
    for ip in iplar:
        ip.start()
    for ip in iplar:
        ip.join()
    return {"sozlama": sozlama or "standart", "8 kamera parallel ochildi, s": time.time() - t0,
            "eng tez, s": min(vaqtlar.values()), "eng sekin, s": max(vaqtlar.values()), "hammasi kadr berdi": all(ochildi.values())}

avvalgi = os.environ.get("OPENCV_FFMPEG_CAPTURE_OPTIONS", "")
with rtsp_tizim(8, 640):
    jadval = pd.DataFrame([ochish_vaqtlari(s) for s in ("rtsp_transport;tcp", "rtsp_transport;tcp|analyzeduration;0|probesize;32")])
os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = avvalgi
show(jadval.round(2))
sozlama8 kamera parallel ochildi, seng tez, seng sekin, shammasi kadr berdi
rtsp_transport;tcp17.733.7317.73True
rtsp_transport;tcp|analyzeduration;0|probesize;322.20.131True

Standart holatda 8 kamera navbat bilan ochiladi — oxirgisi eng birinchisidan bir necha barobar kech. Tahlilsiz esa hammasi bir soniya ichida ochiladi va hammasi kadr beradi. Farq ish boshida emas, tiklanishda seziladi: tok o'chib yonsa yoki kommutator qayta yuklansa, kamera boshiga ~2 soniya hisobida 32 kamerali tizim bir daqiqadan ko'proq "ko'r" qoladi. Shuning uchun kod/oqim.py bu sozlamalarni o'zi o'rnatadi.

Hammasi birga: N ta kamera, YOLO va tracker

Endi to'liq quvur: N ta soxta kamera RTSP orqali, har biriga Oqim, asosiy siklda TensorRT (18-bob, 16 talik batch) va har kameraga ByteTrack. O'lchaymiz: kadr kamerada yaratilgandan tracker natijasi tayyor bo'lgunicha qancha vaqt o'tadi. Ko'p kamerali sinovlarda 18-bobning tavsiyasi bo'yicha 360p ikkinchi oqim.

kod/19_video_oqim.pydavomi
python45 qator
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")
model = YOLO("weights/yolo11n_384x640_b16.engine", task="detect")
for b in range(1, 17):
    model.predict([np.zeros((360, 640, 3), np.uint8)] * b, imgsz=(384, 640), device=0, verbose=False)
bytetrack = IterableSimpleNamespace(**YAML.load(check_yaml("bytetrack.yaml")))

def quvur(soni, kenglik, soniya=20):
    with rtsp_tizim(soni, kenglik):
        oqimlar = [Oqim(f"k{i}", url(f"k{i}"), jurnal=lambda m: None) for i in range(1, soni + 1)]
        for o in oqimlar:
            o.start()
        muddat = time.time() + 30
        while min(o.qabul for o in oqimlar) == 0:                       # hamma kamera ulanguncha (ko'pi bilan 30 s)
            if time.time() > muddat:
                raise RuntimeError("kameralar 30 s ichida ulanmadi")
            time.sleep(0.2)
        trackerlar = [BYTETracker(bytetrack, frame_rate=15) for _ in oqimlar]
        for o in oqimlar:
            o.qabul = o.olindi = 0
        kechikish, t0 = [], time.perf_counter()
        while time.perf_counter() - t0 < soniya:
            tayyor = [(i, r) for i, o in enumerate(oqimlar) if (r := o.ol()) is not None]
            if not tayyor:
                time.sleep(0.001)
                continue
            for j in range(0, len(tayyor), 16):
                qism = tayyor[j:j + 16]
                natija = model.predict([r for _, r in qism], imgsz=(384, 640), device=0, verbose=False, classes=[0])
                for (i, rasm), r in zip(qism, natija):
                    trackerlar[i].update(r.boxes.cpu().numpy(), rasm)
                    kechikish.append(kechikish_ms(rasm))
        vaqt = time.perf_counter() - t0
        for o in oqimlar:
            o.toxtat()
    return {"kameralar": soni, "oqim": "1080p" if kenglik == 1920 else "360p",
            "kameraga qabul, kadr/s": sum(o.qabul for o in oqimlar) / vaqt / soni,
            "kameraga ishlangan, kadr/s": sum(o.olindi for o in oqimlar) / vaqt / soni,
            "kechikish, ms (median)": np.median(kechikish), "95% i shundan kam": np.percentile(kechikish, 95)}

show(pd.DataFrame([quvur(n, w) for n, w in ((1, 1920), (4, 1920), (16, 640), (32, 640))]).round(1))
kameralaroqimkameraga qabul, kadr/skameraga ishlangan, kadr/skechikish, ms (median)95% i shundan kam
11080p151589169
41080p1515103114
16360p151597108
32360p15.113144202

Kameradan tracker natijasigacha — 16 kameragacha ~0.1 soniya (95% i ham 0.12 s dan kam). 32 kamerada kechikish biroz oshdi va har kameraga 15 o'rniga 13 kadr ishlandi — bitta Python jarayonining chegarasi (18-bob); bu yerda u RTSP dekodlashni ham o'z iplarida bajaradi. Kadrlar tashlansa ham kechikish (oxirgi kadr usuli tufayli) o'smaydi. Bu inson reaksiyasidan tez: signal tizimi uchun kechikish muammo emas, agar oxirgi kadr usuli va bitta dekoder ipi bo'lsa.

Xavfsizlik: kamera tarmog'i

Kamera tizimi — uydagi yoki korxonadagi eng ko'p hujumga uchraydigan qurilmalardan biri. Minimal qoidalar:

  1. Standart parollarni almashtiring va har bir kameraga alohida, uzun parol qo'ying. RTSP uchun faqat o'qish huquqli alohida foydalanuvchi yarating — tahlil dasturi kamera sozlamalarini o'zgartira olmasin (bizning kamera / tahlil bo'linishi kabi).
  2. Parollar kodda emas. Muhit o'zgaruvchisi yoki .env fayli (Git ga qo'shilmaydi); jurnalga yozishdan oldin URL ni yashiring (yashir).
  3. Kamera tarmog'ini ajrating (alohida VLAN yoki alohida kommutator): kameralar internetga chiqmasin, ofis kompyuterlari kameralarga to'g'ridan-to'g'ri kira olmasin. Faqat tahlil serveri kameralarni ko'radi.
  4. Bulut va P2P funksiyalarini o'chiring (ko'p kameralarda "cloud", "P2P", UPnP yoqilgan bo'ladi) va proshivkani yangilab turing.
  5. Server faqat kerakli portni ochsin. Bizning MediaMTX sozlamasi kabi: bitta protokol, bitta manzil, qolgani o'chiq. Tashqaridan ko'rish kerak bo'lsa — VPN orqali, portni internetga ochmasdan.
  6. Ma'lumotlar (13-bob): video va yuzlar — shaxsga doir ma'lumot. O'zbekistonda ularni qayta ishlashga ZRU-547 qonuni amal qiladi; ma'lumotlar mamlakat ichidagi serverda saqlanadi (27-1-modda). Tizimni ob'ektning o'zida yoki mahalliy serverda ishga tushirish bu talabni sodda bajaradi.

Asosiy fikr

  • Real vaqt tizimi har kadrni emas, oxirgi kadrni ishlaydi: alohida ip o'qiydi, asosiy sikl ulgurmaganini tashlaydi. Aks holda kechikish tinmay o'sadi va buni hech kim sezmaydi.
  • OpenCV ning standart dekoderi har bir ip uchun bitta kadr kechikish qo'shadi — kamera oqimi uchun CAP_PROP_N_THREADS = 1.
  • Uzilishlar — odatiy holat: qisqa kutish muddatlari, o'sib boruvchi pauza bilan qayta ulanish va "oxirgi kadr qachon kelgan" hisobi (kod/oqim.py).
  • To'liq quvur (RTSP → YOLO → tracker) 16 kamerada ~0.1 s, 32 kamerada ~0.15 s kechikish beradi. Ko'p kamerani tez tiklash uchun FFmpeg ning oqim tahlilini o'chiring — aks holda kameralar navbat bilan, har biri ~2 s da ochiladi. Kameralar va server — alohida tarmoqda, alohida parollar bilan, faqat kerakli port ochiq.

Mashqlar

  1. ISHLOV ni 0.05 (kadrlar kelishidan tez) qilib, navbat tajribasini takrorlang. Sodda usulda ham kechikish o'sadimi?
  2. Oqim ga jimlik() ni tekshiradigan kuzatuvchi ip qo'shing: kamera 10 soniyadan ko'p jim bo'lsa, jurnalga bir marta "OGOHLANTIRISH" yozsin, tiklanganda — "tiklandi".
  3. Soxta kamerani --fps 5 bilan ishga tushiring. Kechikish qanday o'zgaradi? Dekoder iplari tajribasini takrorlang — endi har bir ip necha millisekund qo'shadi?

Bu bobni bepul o'qidingiz.

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

Telegramda yozish Narxlar