Yakuniy loyiha: qurilish maydonida kaska nazorati
Bu bobning kodi: kod/22_qurilish.py — kitob papkasidan ishga tushiring: python kod/22_qurilish.py
Vazifa
Mijoz — qurilish kompaniyasi. Talab bitta: "Maydonda kaskasiz odam bo'lsa, prorab darhol bilsin." Bu kitobdagi deyarli hamma narsa shu bitta talabda uchrashadi:
- odamni topish va kuzatish (I–II qismlar), kaskani topish (7-bob), "kaskasiz" ni mantiq bilan chiqarish (14-bob);
- piksellar yetadimi (17-bob): kaska uchun ~24 px, ya'ni odam ~170 px;
- real vaqt oqimi va uzilishlar (19-bob), hodisa, panel va Telegram (20-bob), Docker (21-bob).
Va 14-bobda qoldirilgan va'da: bitta kadr bo'yicha signal berish xavfli — kaskani vaqt bo'yicha tasdiqlash kerak. Bu bobda buni haqiqiy qurilish videosida, qo'lda belgilangan javob bilan o'lchaymiz.
Video va maxfiylik
Video — Pexels dan (video raqami 13921040): beton quyish paytidagi qurilish maydoni, 2560x1440, 30 fps, 20 soniya. Kamera bir joyda turadi, lekin sekin buriladi. Pexels litsenziyasi videoni bepul, tijoriy maqsadda ham ishlatishga va o'zgartirishga ruxsat beradi, bitta cheklov bilan: tanilishi mumkin bo'lgan odamlar "yomon ko'rinishda" ko'rsatilmasligi kerak. "Kaskasiz" deb belgilangan ishchi — aynan shunday holat. Shuning uchun kitobdagi hamma rasmlarda yuzlar xiralashtirilgan. Bu mijozning tizimi uchun ham to'g'ri qoida (13- va 19-boblar, ZRU-547): hodisa uchun yuz kerak emas.
# Qurilish maydoni videosi (22-bob): Pexels, 13921040, 2560x1440, 20 s, Pexels litsenziyasi pexels = root / "pexels" / "13921040_2560x1440.mp4" if not pexels.exists(): pexels.parent.mkdir(parents=True, exist_ok=True) so_rov = urllib.request.Request("https://videos.pexels.com/video-files/13921040/13921040-uhd_2560_1440_30fps.mp4", headers={"User-Agent": "Mozilla/5.0"}) # Python ning standart sarlavhasiga server 403 beradi with urllib.request.urlopen(so_rov) as r: pexels.write_bytes(r.read())
Javobni qo'lda belgilash
Tizimni baholash uchun to'g'ri javob kerak. Uni qo'lda belgiladik:
- Videodan har 40-kadrni oldik (boshi va oxiridagi qorayishdan tashqari) — 14 kadr.
- Har kadrda YOLO11n (1280, ishonch ≥ 0.4) topgan va balandligi ≥ 200 px bo'lgan har bir odam uchun bosh qismini kesdik — 153 ta qirqim.
- Har bir qirqimni ko'rib, uch belgidan birini qo'ydik: kaskali (1), kaskasiz (0), noaniq (null) — bosh ko'rinmaydi (engashgan, to'silgan) yoki qirqimda ikki odam bor va box kimniki ekani noaniq.
Ikki holat ataylab "kaskali" deb belgilandi, chunki ular modelni aldashi mumkin: oq kaskaga qo'shilgan keng qora soyabon (quyoshdan himoya) va kulrang kaskalar. Kaskasiz odamlar ikkita: oq ko'ylakli boshyalang odam va pushti kapyushonli odam. Belgilar faylga yozildi — kadr, box va belgi:
[ [40, 598, 511, 793, 985, 1], [40, 1700, 464, 1898, 902, null], [40, 1091, 570, 1252, 1004, 1], [40, 812, 434, 953, 908, 1], [40, 1025, 516, 1140, 964, 1], [40, 1910, 475, 2067, 865, 1], [40, 1268, 617, 1495, 934, null], [40, 1411, 496, 1649, 945, 1], [40, 1913, 477, 2068, 732, 1], [40, 1538, 474, 1658, 743, 1], [40, 1681, 464, 1780, 720, 1], [80, 475, 504, 651, 988, 1], [80, 629, 555, 838, 970, 1], [80, 1122, 600, 1354, 1024, 1], [80, 1710, 466, 1883, 906, 0], [80, 348, 656, 491, 928, 1], [80, 952, 628, 1106, 948, 1], [80, 816, 432, 953, 907, 1], [80, 1588, 533, 1725, 948, 1], [80, 1124, 516, 1234, 716, 1], [80, 1423, 749, 1661, 1026, null], [80, 1914, 480, 2049, 870, 1], [80, 1477, 499, 1624, 905, 1], [120, 556, 513, 728, 990, 1], [120, 681, 511, 821, 966, 1], [120, 1717, 463, 1884, 900, 0], [120, 968, 629, 1162, 948, 1], [120, 1137, 626, 1339, 1016, 1], [120, 360, 701, 490, 923, 1], [120, 1319, 520, 1458, 1004, 1], [120, 1584, 550, 1726, 941, 1], [120, 1387, 746, 1622, 1019, null], [120, 823, 435, 954, 903, 1], [120, 1912, 476, 2057, 858, 1], [120, 1476, 500, 1627, 939, 1], [160, 540, 507, 728, 987, 1], [160, 977, 628, 1163, 949, 1], [160, 1155, 568, 1285, 1033, 1], [160, 1625, 523, 1757, 950, 1], [160, 1760, 462, 1885, 882, 0], [160, 1235, 481, 1510, 940, 1], [160, 1434, 494, 1634, 872, 1], [160, 813, 435, 953, 895, 1], [160, 671, 514, 803, 972, null], [160, 1703, 504, 1814, 917, 1], [160, 1388, 484, 1512, 925, 1], [160, 1955, 461, 2068, 829, 1], [200, 1336, 590, 1460, 1055, 1], [200, 704, 514, 850, 1008, 1], [200, 897, 531, 1044, 984, 1], [200, 1412, 568, 1672, 951, 1], [200, 1937, 490, 2105, 944, 0], [200, 1812, 545, 1930, 969, 1], [200, 1151, 642, 1339, 964, 1], [200, 1883, 514, 1982, 944, 1], [200, 1628, 507, 1825, 874, 1], [200, 1004, 450, 1138, 927, 1], [200, 2080, 481, 2184, 726, 1], [240, 814, 514, 1006, 995, 1], [240, 1629, 506, 1780, 951, 1], [240, 1966, 494, 2119, 912, 0], [240, 1157, 638, 1331, 960, 1], [240, 1006, 436, 1146, 926, 1], [240, 2370, 497, 2467, 950, 1], [240, 1427, 595, 1603, 1045, 1], [240, 1746, 677, 1955, 949, null], [240, 1852, 520, 1996, 902, 1], [240, 1327, 613, 1465, 986, 1], [280, 814, 588, 1026, 985, null], [280, 1960, 519, 2134, 945, 1], [280, 2376, 488, 2560, 945, 1], [280, 1848, 525, 1988, 944, 1], [280, 1429, 545, 1599, 946, 1], [280, 1011, 442, 1147, 926, 1], [280, 1131, 515, 1299, 978, 1], [280, 1300, 764, 1514, 1040, null], [280, 1595, 600, 1712, 952, 1], [280, 1531, 597, 1712, 951, 1], [320, 524, 553, 679, 992, 0], [320, 846, 522, 1047, 983, 1], [320, 2385, 496, 2558, 950, 1], [320, 1968, 510, 2136, 946, 1], [320, 1013, 435, 1175, 934, 1], [320, 1154, 640, 1338, 958, 1], [320, 1850, 476, 1982, 936, 1], [320, 1620, 497, 1813, 921, 1], [320, 1421, 542, 1566, 776, 1], [320, 1456, 605, 1704, 1033, 1], [320, 1406, 543, 1569, 961, 1], [360, 834, 634, 1056, 985, 1], [360, 2016, 515, 2179, 943, 1], [360, 1658, 507, 1853, 953, 1], [360, 2382, 484, 2500, 951, 1], [360, 1296, 608, 1464, 988, 1], [360, 1950, 521, 2066, 938, 1], [360, 1437, 765, 1681, 1034, null], [360, 1843, 475, 1973, 922, 1], [360, 1010, 441, 1157, 739, 1], [360, 1161, 637, 1329, 955, 1], [360, 1057, 641, 1327, 957, 1], [400, 844, 556, 1061, 981, 1], [400, 2001, 515, 2176, 945, 1], [400, 1378, 560, 1565, 966, 1], [400, 1215, 568, 1344, 974, 1], [400, 1009, 442, 1166, 921, 1], [400, 1558, 565, 1737, 877, 1], [400, 1847, 630, 2016, 942, 1], [400, 1527, 802, 1791, 1029, null], [400, 2398, 490, 2506, 947, 1], [400, 1794, 501, 1966, 868, 1], [400, 2342, 493, 2461, 948, 1], [400, 1970, 512, 2090, 813, 1], [440, 420, 709, 630, 965, 0], [440, 881, 511, 1027, 958, 1], [440, 1468, 784, 1732, 1004, null], [440, 1138, 547, 1304, 966, 1], [440, 1785, 467, 2013, 926, 1], [440, 1342, 540, 1610, 941, 1], [440, 2006, 487, 2138, 915, 1], [440, 1770, 446, 1876, 916, 1], [440, 950, 417, 1096, 717, 1], [480, 1600, 487, 1745, 930, 1], [480, 877, 506, 1019, 960, 1], [480, 2390, 467, 2543, 913, 1], [480, 470, 760, 605, 962, 0], [480, 1162, 490, 1404, 944, 1], [480, 2013, 474, 2178, 919, 1], [480, 1835, 478, 2005, 921, 1], [480, 1304, 756, 1508, 1018, null], [480, 1118, 622, 1232, 945, 1], [480, 1421, 573, 1608, 923, 1], [480, 1792, 451, 1895, 910, 1], [520, 878, 567, 1066, 957, 1], [520, 1468, 772, 1665, 996, null], [520, 2372, 469, 2485, 920, 1], [520, 1221, 488, 1405, 998, 1], [520, 1771, 471, 2032, 926, 1], [520, 1965, 469, 2080, 894, 1], [520, 2004, 472, 2183, 924, 1], [520, 1388, 609, 1622, 921, null], [520, 963, 415, 1092, 696, 1], [560, 888, 535, 1080, 957, 1], [560, 1316, 534, 1525, 943, 1], [560, 1191, 539, 1337, 952, 1], [560, 1829, 480, 2063, 913, 1], [560, 2358, 476, 2484, 921, 1], [560, 1613, 656, 1829, 926, null], [560, 469, 760, 600, 967, 0], [560, 1985, 470, 2181, 924, 1], [560, 1114, 610, 1213, 938, 1], [560, 1455, 773, 1667, 1009, null], [560, 981, 418, 1097, 654, 1], [560, 2243, 468, 2372, 919, 1] ]
import json import os import signal import subprocess import sys import time import urllib.request from collections import defaultdict from pathlib import Path import cv2 import numpy as np import pandas as pd import torch import torchvision import matplotlib.pyplot as plt from ultralytics import YOLO from ultralytics.trackers import BOTSORT from ultralytics.utils import LOGGER, YAML, IterableSimpleNamespace from ultralytics.utils.checks import check_yaml LOGGER.setLevel("ERROR") VIDEO = "datasets/pexels/13921040_2560x1440.mp4" belgilar = json.loads(Path("datasets/pexels/13921040_kaska.json").read_text()) cap = cv2.VideoCapture(VIDEO) print(f"video: {int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))}x{int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))}, " f"{cap.get(cv2.CAP_PROP_FPS):.2f} fps, {int(cap.get(cv2.CAP_PROP_FRAME_COUNT))} kadr") jadval = pd.Series([{1: "kaskali", 0: "kaskasiz", None: "noaniq"}[b[5]] for b in belgilar]).value_counts() print("belgilar:", dict(jadval), "|", len({b[0] for b in belgilar}), "kadrda") def kadrlarni_ol(kerakli): """Videoni boshidan o'qib, faqat kerakli kadrlarni qaytaradi (1440p kadr ~11 MB — hammasini xotirada saqlamaymiz).""" cap, natija, k = cv2.VideoCapture(VIDEO), {}, 0 while len(natija) < len(kerakli): ok, rasm = cap.read() if not ok: break k += 1 if k in kerakli: natija[k] = rasm return natija kadrlar = kadrlarni_ol({b[0] for b in belgilar}) yuz = cv2.FaceDetectorYN.create("weights/face_detection_yunet_2023mar.onnx", "", (320, 320), 0.5) def yuzlarni_yashir(rasm, odamlar): """Rasmlar uchun: YuNet topgan yuzlar va har bir odam box idagi yuz sohasi pikselga aylantiriladi.""" r = rasm.copy() yuz.setInputSize((r.shape[1], r.shape[0])) _, topilgan = yuz.detect(r) zonalar = [] if topilgan is None else [(f[0], f[1], f[0] + f[2], f[1] + f[3]) for f in topilgan] for x1, y1, x2, y2 in odamlar: # yuz topilmasa ham: kaska ostidagi soha w, h = x2 - x1, y2 - y1 zonalar.append((x1 + 0.1 * w, y1 + 0.07 * h, x2 - 0.1 * w, y1 + 0.24 * h)) for x1, y1, x2, y2 in zonalar: x1, y1, x2, y2 = max(0, int(x1)), max(0, int(y1)), int(x2), int(y2) q = r[y1:y2, x1:x2] if q.size: kichik = cv2.resize(q, (max(1, q.shape[1] // 10), max(1, q.shape[0] // 10))) r[y1:y2, x1:x2] = cv2.resize(kichik, (q.shape[1], q.shape[0]), interpolation=cv2.INTER_NEAREST) return r odam_model = YOLO("weights/yolo11n.pt") k = max(kadrlar, key=lambda f: sum(b[5] == 0 for b in belgilar if b[0] == f)) # kaskasiz belgisi eng ko'p kadr odamlar = [b for b in belgilar if b[0] == k] hammasi = odam_model.predict(kadrlar[k], imgsz=1280, classes=[0], conf=0.1, device=0, verbose=False)[0].boxes.xyxy.cpu().numpy() _, ax = plt.subplots(figsize=(13, 7.3)) ax.imshow(yuzlarni_yashir(kadrlar[k], [*hammasi, *[b[1:5] for b in odamlar]])[:, :, ::-1]) # belgilanmaganlar ham for b in odamlar: rang = {1: "lime", 0: "red", None: "gray"}[b[5]] ax.add_patch(plt.Rectangle(b[1:3], b[3] - b[1], b[4] - b[2], fill=False, color=rang, lw=1.5)) ax.axis("off") fig("belgilar.png", f"{k}-kadr, qo'lda belgilangan javob: yashil — kaskali, qizil — kaskasiz, kulrang — noaniq. Yuzlar xiralashtirilgan.")
video: 2560x1440, 29.97 fps, 607 kadr
belgilar: {'kaskali': np.int64(128), 'noaniq': np.int64(16), 'kaskasiz': np.int64(9)} | 14 kadrda

80-kadr, qo'lda belgilangan javob: yashil — kaskali, qizil — kaskasiz, kulrang — noaniq. Yuzlar xiralashtirilgan.
Bitta kadrda: kaskani qayerdan qidirish kerak
Kaska modeli (7-bob) va 14-bobdagi mantiq: odam box ining bosh qismida (yuqoridan 30% gacha) kaska box ining markazi bo'lsa — kaskali, aks holda — kaskasiz. Savol — modelga nima beramiz? 17-bob ikki yo'lni ko'rsatgan: butun kadrni (model uni 640 yoki 1280 ga kichraytiradi) yoki har bir odamni asl kadrdan qirqib — kaska o'z piksellarini yo'qotmaydi.
ppe = YOLO("out/train/ppe_coco/weights/best.pt") # 7-bob: 0 — helmet KASKA = 0 def boshida(odam, kaskalar, ulush=0.3): """14-bob: kaska box ining markazi odamning bosh sohasidami.""" x1, y1, x2, y2 = odam return any(x1 <= (b[0] + b[2]) / 2 <= x2 and y1 - 0.1 * (y2 - y1) <= (b[1] + b[3]) / 2 <= y1 + ulush * (y2 - y1) for b in kaskalar) def qirqib_tekshir(rasm, odamlar): """Har bir odam asl kadrdan qirqiladi (tepasida 10% zaxira), kaska modeli qirqimlarda. Qaytaradi: True — kaskasiz.""" qirqim, siljish = [], [] for x1, y1, x2, y2 in (map(int, o[:4]) for o in odamlar): ya = max(0, y1 - (y2 - y1) // 10) qirqim.append(rasm[ya:max(ya + 2, y2), max(0, x1):max(x1 + 2, x2)]) siljish.append((max(0, x1), ya)) natija = ppe.predict(qirqim, imgsz=640, conf=0.25, device=0, verbose=False) if qirqim else [] return [not boshida(o[:4], r.boxes.xyxy.cpu().numpy()[r.boxes.cls.cpu().numpy() == KASKA] + [dx, dy, dx, dy]) for o, r, (dx, dy) in zip(odamlar, natija, siljish)] def butun_kadrda(rasm, odamlar, imgsz): r = ppe.predict(rasm, imgsz=imgsz, conf=0.25, device=0, verbose=False)[0] kaskalar = r.boxes.xyxy.cpu().numpy()[r.boxes.cls.cpu().numpy() == KASKA] return [not boshida(o[:4], kaskalar) for o in odamlar] usullar = {"butun kadr, 640": lambda r, o: butun_kadrda(r, o, 640), "butun kadr, 1280": lambda r, o: butun_kadrda(r, o, 1280), "har odam qirqimi (asl kadr)": qirqib_tekshir} qatorlar = [] for nom, usul in usullar.items(): juft = [] for k in sorted({b[0] for b in belgilar}): odamlar = [b for b in belgilar if b[0] == k] juft += [(b[5], kaskasiz) for b, kaskasiz in zip(odamlar, usul(kadrlar[k], [b[1:5] for b in odamlar])) if b[5] is not None] kaskali, kaskasiz = [p for g, p in juft if g == 1], [p for g, p in juft if g == 0] qatorlar.append({"usul": nom, "kaskali odamlar": len(kaskali), "ulardan soxta 'kaskasiz'": sum(kaskali), "kaskasiz odamlar": len(kaskasiz), "ulardan topildi": sum(kaskasiz)}) show(pd.DataFrame(qatorlar))
| usul | kaskali odamlar | ulardan soxta 'kaskasiz' | kaskasiz odamlar | ulardan topildi |
|---|---|---|---|---|
| butun kadr, 640 | 128 | 14 | 9 | 3 |
| butun kadr, 1280 | 128 | 4 | 9 | 4 |
| har odam qirqimi (asl kadr) | 128 | 9 | 9 | 7 |
Butun kadrni 640 ga kichraytirish — eng yomoni: kaskali odamlarning o'ndan ko'prog'i "kaskasiz" deb chiqdi, haqiqiy kaskasizlarning esa uchdan biri topildi. 1280 da soxta signallar kamaydi, lekin kaskasizlar baribir kam topildi — ularning ko'pchiligining bosh sohasiga biror kaska box i tushgan (olomonda bu ko'pincha qo'shnining kaskasi). Asl kadrdan qirqish kaskasizlarning ko'pini topdi — 17-bobdagi xulosa amalda. Evaziga kaskalilarning bir qismi ham "kaskasiz" deb chiqdi. Har kadrda xabar yuborsak, bu soxta xabarlar oqimi bo'lardi.
Vaqt bo'yicha tasdiq
Endi butun video: har kadrda odamlar, tracker (kamera buriladi — shuning uchun 11-bobdagi GMC li BoT-SORT) va har bir trek uchun har kadrda "kaskasiz?" qarori (qirqim usuli). Signal qoidasi: trek oxirgi T soniyaning 80% ida kaskasiz bo'lsa — signal, bir trek uchun bir marta. T = 0 (har bir kadr qarori — darhol signal), 0.5, 1 va 2 soniya.
Trekning to'g'ri javobi — u belgilangan kadrlardagi qaysi box ga to'g'ri kelishidan (IoU ≥ 0.5): belgilangan kadrlardan o'tmagan trek larni tekshira olmaymiz ("tekshirib bo'lmaydi").
tracker = BOTSORT(IterableSimpleNamespace(**YAML.load(check_yaml("botsort.yaml"))), frame_rate=30) yozuv = [] # (kadr, trek, x1, y1, x2, y2, kaskasiz) t0, cap, k = time.time(), cv2.VideoCapture(VIDEO), 0 while True: ok, rasm = cap.read() if not ok: break k += 1 r = odam_model.predict(rasm, imgsz=1280, classes=[0], conf=0.1, device=0, verbose=False)[0] treklar = tracker.update(r.boxes.cpu().numpy(), rasm) yozuv += [(k, int(t[4]), *map(float, t[:4]), int(x)) for t, x in zip(treklar, qirqib_tekshir(rasm, treklar))] Y = np.array(yozuv) print(f"{k} kadr, {time.time() - t0:.0f} s | treklar: {len(np.unique(Y[:, 1]))}") javob = defaultdict(list) # trek -> belgilar for k in sorted({b[0] for b in belgilar}): B = np.array([b[1:5] for b in belgilar if b[0] == k], float) L = [b[5] for b in belgilar if b[0] == k] T = Y[Y[:, 0] == k] M = torchvision.ops.box_iou(torch.tensor(B), torch.tensor(T[:, 2:6])).numpy() for i, j in zip(*np.nonzero(M >= 0.5)): if M[i, j] == M[i].max() == M[:, j].max() and L[i] is not None: javob[int(T[j, 1])].append(L[i]) trek_javob = {t: v[0] for t, v in javob.items() if len(set(v)) == 1} print("javobi ma'lum treklar:", len(trek_javob), "| kaskasiz:", sum(v == 0 for v in trek_javob.values()), "| belgilari qarama-qarshi:", sum(len(set(v)) > 1 for v in javob.values())) def signallar(oyna): natija = {} for tid in np.unique(Y[:, 1]).astype(int): q = Y[Y[:, 1] == tid] d = q[np.argsort(q[:, 0]), 6] for i in range(oyna - 1, len(d)): if d[i - oyna + 1:i + 1].mean() >= 0.8: natija[tid] = int(q[np.argsort(q[:, 0])][i, 0]) break return natija qatorlar = [] for soniya in (0, 0.5, 1, 2): s = signallar(max(1, round(soniya * 30))) qatorlar.append({"tasdiq, s": soniya, "signallar": len(s), "to'g'ri": sum(trek_javob.get(t) == 0 for t in s), "soxta": sum(trek_javob.get(t) == 1 for t in s), "tekshirib bo'lmaydi": sum(t not in trek_javob for t in s), "o'tkazib yuborilgan kaskasiz treklar": sum(v == 0 and t not in s for t, v in trek_javob.items())}) show(pd.DataFrame(qatorlar)) s2 = signallar(60) for t, v in trek_javob.items(): if v == 0 and t not in s2: q = Y[Y[:, 1] == t] print(f"2 s da o'tkazib yuborilgan kaskasiz trek {t}: {len(q)} kadr, 'kaskasiz' deb baholangani {q[:, 6].mean():.0%}")
607 kadr, 35 s | treklar: 130 javobi ma'lum treklar: 52 | kaskasiz: 4 | belgilari qarama-qarshi: 0 2 s da o'tkazib yuborilgan kaskasiz trek 19: 172 kadr, 'kaskasiz' deb baholangani 13%
| tasdiq, s | signallar | to'g'ri | soxta | tekshirib bo'lmaydi | o'tkazib yuborilgan kaskasiz treklar |
|---|---|---|---|---|---|
| 0 | 91 | 4 | 33 | 54 | 0 |
| 0.5 | 21 | 3 | 10 | 8 | 1 |
| 1 | 12 | 3 | 5 | 4 | 1 |
| 2 | 6 | 3 | 2 | 1 | 1 |
Jadvaldagi eng muhim ustun — "signallar": prorab 20 soniyada nechta xabar oladi. Tasdiqsiz — o'nlab xabar, ularning ko'pchiligi soxta yoki tekshirib bo'lmaydigan: bunday tizimni birinchi kuniyoq o'chirib qo'yishadi. 2 soniyalik tasdiq xabarlarni o'n barobardan ko'proq kamaytiradi va haqiqiy kaskasizlarning ko'pini saqlab qoladi. Qolgan soxta signallar nima ekanini ko'ramiz:
signal_kadrlari = kadrlarni_ol(set(s2.values())) _, axes = plt.subplots(1, len(s2), figsize=(2.3 * len(s2), 3.2)) for ax, (tid, k) in zip(np.atleast_1d(axes), s2.items()): x1, y1, x2, y2 = map(int, Y[(Y[:, 0] == k) & (Y[:, 1] == tid)][0, 2:6]) h = y2 - y1 rasm = yuzlarni_yashir(signal_kadrlari[k], [(x1, y1, x2, y2)])[max(0, y1 - h // 10):y1 + h // 2, max(0, x1):x2] ax.imshow(rasm[:, :, ::-1]) ax.set_title({0: "to'g'ri", 1: "soxta"}.get(trek_javob.get(tid), "tekshirib\nbo'lmaydi") + f"\ntrek {tid}, {k / 30:.1f} s", fontsize=9) ax.axis("off") fig("signallar.png", "2 soniyalik tasdiqdan o'tgan signallar: signal paytidagi bosh qismi (yuzlar xiralashtirilgan).")

2 soniyalik tasdiqdan o'tgan signallar: signal paytidagi bosh qismi (yuzlar xiralashtirilgan).
To'g'ri signallar — oq ko'ylakli boshyalang odam va pushti kapyushonli odam. Soxta signallarning sabablari real maydonga xos: engashgan ishchi (kaska qiya, model uni kaska deb tanimaydi) va armatura ortidagi ishchi (kaska to'silgan). Bir holat bu videoga xos: videoning oxiridagi qorayish paytida paydo bo'lgan trek — rasm qorong'i, lekin bu yana o'sha pushti kapyushonli odam. O'tkazib yuborilgan kaskasiz trek esa uzun, lekin model uni kadrlarning ozgina qismida "kaskasiz" deb baholagan (jadval ostidagi qator): bu oq ko'ylakli odam videoning boshida kaskali ishchi bilan yonma-yon turadi (belgilash qirqimlarida ko'rinadi), va qo'shnining kaskasi uning bosh sohasiga tushadi. Bu — "bosh sohasi" qoidasining zaif joyi; uni kaskaning odamga tegishliligini aniqroq tekshirish (masalan, kaska box i odamning yuqori chetiga yopishganmi) bilan kuchaytirish mumkin.
Butun tizimda
Endi haqiqiy tizim: video soxta kamera orqali RTSP da (2560x1440, 30 fps, 8 Mbit/s), ishchi uni real vaqtda o'qiydi va qoidani qo'llaydi, hodisalar 20-bobdagi Django va Postgres ga, signal — Telegram ga. Ishchi — 20-bobdagi tahlilchining yangi varianti: Yuboruvchi ni o'shandan oladi, qoidasi esa bu bobdagi — asl kadrdan qirqib tekshirish va vaqt bo'yicha tasdiq. Tasdiq kadrlar soni bilan emas, soniyalar bilan: real tizimda kadrlar tezligi o'zgaradi (18-bob).
# Qurilish maydoni ishchisi: RTSP -> YOLO11n (odamlar) -> BoT-SORT (GMC) -> kaska (asl kadrdan qirqim) -> vaqt bo'yicha tasdiq -> API. # Maxfiy sozlamalar muhitdan: RTSP_ASOS, ISHCHI_TOKEN. Misol: python kod/22_qurilish_ishchi.py --kamera k1 import argparse import json import os import sys import time from collections import defaultdict, deque import numpy as np sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from oqim import Oqim # noqa: E402 from ultralytics import YOLO # noqa: E402 from ultralytics.trackers import BOTSORT # noqa: E402 from ultralytics.utils import LOGGER, YAML, IterableSimpleNamespace # noqa: E402 from ultralytics.utils.checks import check_yaml # noqa: E402 LOGGER.setLevel("ERROR") tahlilchi = __import__("20_tahlilchi") # Yuboruvchi va vaqt_iso — 20-bobdan kodni_oqi = __import__("19_soxta_kamera").kodni_oqi KASKA = 0 def boshida(odam, kaskalar, ulush=0.3): x1, y1, x2, y2 = odam return any(x1 <= (b[0] + b[2]) / 2 <= x2 and y1 - 0.1 * (y2 - y1) <= (b[1] + b[3]) / 2 <= y1 + ulush * (y2 - y1) for b in kaskalar) def kaskasizmi(ppe, rasm, treklar): """Har bir trek uchun: asl kadrdan qirqim, kaska modeli, kaska boshidami. True — kaskasiz.""" qirqim, siljish = [], [] for x1, y1, x2, y2 in (map(int, t[:4]) for t in treklar): ya = max(0, y1 - (y2 - y1) // 10) qirqim.append(rasm[ya:max(ya + 2, y2), max(0, x1):max(x1 + 2, x2)]) siljish.append((max(0, x1), ya)) natija = ppe.predict(qirqim, imgsz=640, conf=0.25, device=0, verbose=False) if qirqim else [] return [not boshida(t[:4], r.boxes.xyxy.cpu().numpy()[r.boxes.cls.cpu().numpy() == KASKA] + [dx, dy, dx, dy]) for t, r, (dx, dy) in zip(treklar, natija, siljish)] def main(): p = argparse.ArgumentParser() p.add_argument("--kamera", default="k1") p.add_argument("--api", default="http://127.0.0.1:18000/api/hodisalar/") p.add_argument("--odam-model", default="weights/yolo11n.pt") p.add_argument("--kaska-model", default="out/train/ppe_coco/weights/best.pt") p.add_argument("--tasdiq", type=float, default=2.0, help="shuncha soniya davomida kaskasiz bo'lsa — signal") p.add_argument("--ulush", type=float, default=0.8, help="tasdiq oynasidagi 'kaskasiz' kadrlar ulushi") p.add_argument("--soniya", type=float, default=0, help="0 — to'xtovsiz") p.add_argument("--vaqt-kodi", action="store_true") a = p.parse_args() asos, token = os.environ["RTSP_ASOS"], os.environ["ISHCHI_TOKEN"] odam, ppe = YOLO(a.odam_model), YOLO(a.kaska_model) bosh = np.zeros((1440, 2560, 3), np.uint8) # qizdirish: birinchi kadrlar kechikmasin odam.predict(bosh, imgsz=1280, device=0, verbose=False) ppe.predict([bosh[:400, :200]] * 8, imgsz=640, device=0, verbose=False) tracker = BOTSORT(IterableSimpleNamespace(**YAML.load(check_yaml("botsort.yaml"))), frame_rate=30) yuboruvchi = tahlilchi.Yuboruvchi(a.api, token) yuboruvchi.start() oqim = Oqim(a.kamera, asos + a.kamera, jurnal=lambda m: print(m, flush=True), max_pauza=2) oqim.start() print("tayyor", flush=True) tarix, birinchi, signal_berildi, kadrlar = defaultdict(deque), {}, set(), 0 t0 = time.time() while not a.soniya or time.time() - t0 < a.soniya: rasm = oqim.ol() if rasm is None: time.sleep(0.003) continue kadrlar += 1 hozir = time.time() ms = int(hozir * 1000) if a.vaqt_kodi: ms = ms - ((ms - kodni_oqi(rasm)) % 2 ** 24) r = odam.predict(rasm, imgsz=1280, classes=[0], conf=0.1, device=0, verbose=False)[0] treklar = tracker.update(r.boxes.cpu().numpy(), rasm) for t, kaskasiz in zip(treklar, kaskasizmi(ppe, rasm, treklar)): tid = int(t[4]) birinchi.setdefault(tid, hozir) d = tarix[tid] d.append((hozir, kaskasiz)) while d[0][0] < hozir - a.tasdiq: # faqat oxirgi `tasdiq` soniya d.popleft() ulush = sum(x for _, x in d) / len(d) if tid not in signal_berildi and hozir - birinchi[tid] >= a.tasdiq and ulush >= a.ulush: signal_berildi.add(tid) # bitta trek — bitta signal yuboruvchi.qosh({"kamera": a.kamera, "tur": "kaskasiz", "vaqt": tahlilchi.vaqt_iso(ms), "trek": tid, "kalit": f"{a.kamera}:kaskasiz:{tid}:{ms}", "malumot": {"ulush": round(ulush, 2), "box": [round(float(v)) for v in t[:4]]}}) oqim.toxtat() oqim.join(timeout=5) kutish = time.time() + 30 while yuboruvchi.navbat and time.time() < kutish: time.sleep(0.2) print(json.dumps({"kadrlar": kadrlar, "kadr/s": round(kadrlar / (time.time() - t0), 1), "signallar": len(signal_berildi), "yuborildi": yuboruvchi.hisob}), flush=True) if __name__ == "__main__": main()
Ishga tushiramiz. Soxta kamera 12 soniya kutib boshlaydi (ishchi modellarni yuklab, qizdirib ulgursin) va videoni bir marta ko'rsatadi.
sys.path.insert(0, "kod") import psycopg from tizim import jarayon, postgres, rtsp PY, MANAGE = sys.executable, ["loyiha/manage.py"] token, tg = os.urandom(24).hex(), f"{int.from_bytes(os.urandom(4), 'big')}:{os.urandom(18).hex()}" tinch = {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL} xabarlar = Path("out/telegram/xabarlar.jsonl") xabarlar.unlink(missing_ok=True) with postgres() as pg: env = {**pg, "DJANGO_SECRET_KEY": os.urandom(32).hex(), "ISHCHI_TOKENLAR": token, "TELEGRAM_TOKEN": tg, "TELEGRAM_CHAT": "-1001234567890", "TELEGRAM_API": "http://127.0.0.1:18081"} subprocess.run([PY, *MANAGE, "migrate", "--noinput"], env={**os.environ, **env}, check=True, capture_output=True) with jarayon([PY, *MANAGE, "runserver", "127.0.0.1:18000", "--noreload"], env, **tinch), \ jarayon([PY, "kod/20_soxta_telegram.py"], env, **tinch), jarayon([PY, *MANAGE, "signal_yubor"], env, **tinch), \ rtsp(1, 2560, video=VIDEO, fps=30, bitreyt="8000k", bir_marta=True, kechikish=12) as r: with jarayon([PY, "kod/22_qurilish_ishchi.py", "--soniya", "45", "--vaqt-kodi"], {"RTSP_ASOS": r["asos"], "ISHCHI_TOKEN": token}, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True) as w: natija = json.loads(w.communicate(timeout=240)[0].strip().splitlines()[-1]) time.sleep(4) with psycopg.connect(host=pg["PG_HOST"], port=pg["PG_PORT"], user="kitob", password=pg["PG_PAROL"], dbname="kuzatuv") as db: hodisalar = db.execute("select trek, extract(epoch from vaqt), extract(epoch from yaratildi - vaqt), " "extract(epoch from yuborildi - vaqt), signal from hodisalar_hodisa order by vaqt").fetchall() print("ishchi:", natija) boshi = min(float(h[1]) for h in hodisalar) if hodisalar else 0 show(pd.DataFrame([{"trek": h[0], "kadr → baza, s": round(float(h[2]), 2), "kadr → Telegram, s": round(float(h[3]), 2) if h[3] else None, "signal": h[4]} for h in hodisalar])) print("Telegram xabarlari:", len(xabarlar.read_text(encoding="utf-8").splitlines()) if xabarlar.exists() else 0)
ishchi: {'kadrlar': 304, 'kadr/s': 6.7, 'signallar': 8, 'yuborildi': {'201': 8}}
Telegram xabarlari: 8
| trek | kadr → baza, s | kadr → Telegram, s | signal |
|---|---|---|---|
| 15 | 0.18 | 1.19 | yuborildi |
| 11 | 0.19 | 1.19 | yuborildi |
| 25 | 0.16 | 0.21 | yuborildi |
| 1 | 0.14 | 0.66 | yuborildi |
| 218 | 0.17 | 0.45 | yuborildi |
| 120 | 0.16 | 0.17 | yuborildi |
| 277 | 0.13 | 1.09 | yuborildi |
| 424 | 0.15 | 0.69 | yuborildi |
Butun zanjir ishladi: har bir "kaskasiz" signal bazaga va Telegram ga yetdi, kadrdan bazagacha ~0.15 soniyada, Telegram gacha bir soniyaga yaqin. Signallar soni oflayn tahlildagiga yaqin, lekin bir xil emas: bu yerda video siqilgan oqim sifatida keladi va ishchi har kadrni emas, sekundiga bir necha kadrni ko'radi — trek lar boshqacha bo'linadi. Qaysi signal to'g'ri ekanini bu yerda tekshira olmaymiz: buning uchun signal bilan birga dalil rasmi kerak (3-mashq). To'g'rilik haqidagi raqamlar — yuqoridagi oflayn tahlildan.
Ishchi sekundiga atigi bir necha kadrni ko'rdi: har bir 2560x1440 kadrda odam detektori (1280), kamera harakatini topish (GMC) va har bir odam uchun kaska modeli. Kaskani tasdiqlash uchun bu yetadi — tasdiq soniyalarda o'lchanadi, kadrlarda emas. Ko'p kamerali ob'ekt uchun tezlashtirish yo'llari 18-bobda: TensorRT dvigatellari, bir nechta ishchi jarayon, kaskani har kadrda emas, har bir necha kadrda tekshirish.
Mijozga nima deymiz
Loyihani topshirishda raqamlar bilan birga cheklovlar ham aytiladi — aks holda birinchi soxta signalda ishonch yo'qoladi:
- Tizim yordamchi, hakam emas. Signal — "tekshiring" degani. Prorab xabarni ko'rib, qaror qiladi; tizim hech kimni avtomatik jarimaga tortmaydi.
- Engashgan va to'silgan ishchilar soxta signal beradi. Tasdiq oynasini uzaytirish ularni kamaytiradi, lekin haqiqiy kaskasizni ham kechroq topadi — bu mijoz bilan kelishiladigan murosa.
- Kamera joylashuvi (17-bob): kaska ~24 px dan kichik bo'lsa, tizim ishlamaydi. Pilotdan oldin har bir kamera uchun eng uzoq ish joyidagi odam necha piksel ekanini o'lchang.
- Qorong'i, tuman, qarama-qarshi yorug'lik — bu videoda sinalmagan. Pilotda mijozning o'z kameralaridan tun va yomon ob-havo kadrlarini ham belgilang.
- Maxfiylik: tizim yuzni tanimaydi va saqlamaydi, hodisada faqat vaqt, kamera va trek. Dalil sifatida rasm kerak bo'lsa — yuzlari xiralashtirilgan va saqlash muddati belgilangan (masalan, 30 kun) bo'lsin; bu mijoz bilan yozma kelishiladi (IV qism).
- Baholash mijozning o'z videosida (16-bob): bir necha daqiqalik videoni birgalikda belgilash va shu bobdagi jadvallarni takrorlash — bu pilotning qabul qilish mezoni bo'ladi.
Asosiy fikr
- Kaskani asl kadrdan qirqib tekshirish butun kadrni kichraytirishdan ko'p kaskasizni topdi — 17-bobdagi piksel hisobi amalda.
- Bitta kadr qarori yolg'on signallar oqimini beradi. Vaqt bo'yicha tasdiq (oxirgi 2 soniyaning 80% i) xabarlarni o'n barobardan ko'proq kamaytirdi va haqiqiy kaskasizlarni saqlab qoldi.
- Qolgan xatolar real maydonga xos: engashish, to'silish, qorong'ilik. Ularni mijozga oldindan aytish — ishonchning bir qismi.
- Butun zanjir — kamera, RTSP, YOLO, tracker, kaska, tasdiq, API, Postgres, Telegram — bitta kompyuterda real vaqtda ishladi.
Mashqlar
- Tasdiq ulushini (
--ulush) 0.6 va 0.9 qilib jadvalni qayta hisoblang. Soxta signallar va o'tkazib yuborilganlar qanday o'zgaradi? - Yuklab olingan boshqa Pexels videosida (masalan, 9227135 — ishchilar kaskasiz, kamera qo'lda) tizimni sinang. Kamera harakatlanganda nima buziladi? GMC yordam beradimi?
- Signalga dalil qo'shing: signal paytidagi odam qirqimini yuzi xiralashtirilgan holda saqlang va Telegram ga
sendPhotobilan yuboring. Rasmlar qancha vaqt saqlanishi kerak va kim ko'ra oladi — javobni yozma qoida qilib yozing.
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.