Video oqim: kameradan tracker gacha
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:
- Kechikish. Tizim kadrni qancha kechikib ko'radi? "Kaskasiz odam" signali 10 soniya kechiksa, foydasi kam.
- Ortda qolish. Ishlov berish kadrlar kelishidan sekin bo'lsa, navbat o'sadi — va kechikish ham.
- 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):
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.
# 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.
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()
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.
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.")
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.
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.
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 iplari | sozlama | kechikish, ms (median) | 95% i shundan kam |
|---|---|---|---|
| 8 | standart | 545 | 728 |
| 4 | 4 | 279 | 475 |
| 2 | 2 | 146 | 301 |
| 1 | 1 | 82 | 448 |
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.
# 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.
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, s | voqea |
|---|---|
| 0.2 | k1: ulandi |
| 5 | KAMERA o'chdi |
| 5 | sodda: read() = False — sikl tugadi |
| 5 | k1: oqim uzildi |
| 5 | k1: ulanmadi (rtsp://***@127.0.0.1:8554/k1), 1 s dan keyin yana |
| 6 | k1: ulanmadi (rtsp://***@127.0.0.1:8554/k1), 2 s dan keyin yana |
| 8 | k1: ulanmadi (rtsp://***@127.0.0.1:8554/k1), 4 s dan keyin yana |
| 10 | KAMERA yondi |
| 12.1 | k1: ulandi |
| 20 | KAMERA qotdi |
| 26.1 | k1: oqim uzildi |
| 29.1 | k1: ulandi |
| 30 | k1: oqim uzildi |
| 30 | k1: ulanmadi (rtsp://***@127.0.0.1:8554/k1), 1 s dan keyin yana |
| 31 | k1: ulanmadi (rtsp://***@127.0.0.1:8554/k1), 2 s dan keyin yana |
| 33 | k1: ulanmadi (rtsp://***@127.0.0.1:8554/k1), 4 s dan keyin yana |
| 35 | KAMERA davom etdi |
| 37 | k1: ulandi |

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()danFalseoladi 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
readTimeouti). 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.
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))
| sozlama | 8 kamera parallel ochildi, s | eng tez, s | eng sekin, s | hammasi kadr berdi |
|---|---|---|---|---|
| rtsp_transport;tcp | 17.73 | 3.73 | 17.73 | True |
| rtsp_transport;tcp|analyzeduration;0|probesize;32 | 2.2 | 0.13 | 1 | True |
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.
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))
| kameralar | oqim | kameraga qabul, kadr/s | kameraga ishlangan, kadr/s | kechikish, ms (median) | 95% i shundan kam |
|---|---|---|---|---|---|
| 1 | 1080p | 15 | 15 | 89 | 169 |
| 4 | 1080p | 15 | 15 | 103 | 114 |
| 16 | 360p | 15 | 15 | 97 | 108 |
| 32 | 360p | 15.1 | 13 | 144 | 202 |
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:
- 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/tahlilbo'linishi kabi). - Parollar kodda emas. Muhit o'zgaruvchisi yoki
.envfayli (Git ga qo'shilmaydi); jurnalga yozishdan oldin URL ni yashiring (yashir). - 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.
- Bulut va P2P funksiyalarini o'chiring (ko'p kameralarda "cloud", "P2P", UPnP yoqilgan bo'ladi) va proshivkani yangilab turing.
- 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.
- 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
ISHLOVni 0.05 (kadrlar kelishidan tez) qilib, navbat tajribasini takrorlang. Sodda usulda ham kechikish o'sadimi?Oqimgajimlik()ni tekshiradigan kuzatuvchi ip qo'shing: kamera 10 soniyadan ko'p jim bo'lsa, jurnalga bir marta "OGOHLANTIRISH" yozsin, tiklanganda — "tiklandi".- Soxta kamerani
--fps 5bilan 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.