Asosiy qismga o'tish

Yakuniy loyiha: qurilish maydonida kaska nazorati

22-bob 8 kod bloki 6530 so'z

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.

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

  1. Videodan har 40-kadrni oldik (boshi va oxiridagi qorayishdan tashqari) — 14 kadr.
  2. Har kadrda YOLO11n (1280, ishonch ≥ 0.4) topgan va balandligi ≥ 200 px bo'lgan har bir odam uchun bosh qismini kesdik — 153 ta qirqim.
  3. 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:

datasets/pexels/13921040_kaska.jsonyangi fayl
matn155 qator
[
[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]
]
kod/22_qurilish.pyyangi fayl · 1-qator: from kitob_yordam import fig, show
python74 qator
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.")
Chiqish
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.

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.

kod/22_qurilish.pydavomi
python36 qator
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))
usulkaskali odamlarulardan soxta 'kaskasiz'kaskasiz odamlarulardan topildi
butun kadr, 6401281493
butun kadr, 1280128494
har odam qirqimi (asl kadr)128997

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").

kod/22_qurilish.pydavomi
python50 qator
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%}")
Chiqish
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, ssignallarto'g'risoxtatekshirib bo'lmaydio'tkazib yuborilgan kaskasiz treklar
091433540
0.52131081
1123541
263211

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:

kod/22_qurilish.pydavomi
python10 qator
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).

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).

kod/22_qurilish_ishchi.pyyangi fayl
python103 qator
# 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.

kod/22_qurilish.pydavomi
python28 qator
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)
Chiqish
ishchi: {'kadrlar': 304, 'kadr/s': 6.7, 'signallar': 8, 'yuborildi': {'201': 8}}
Telegram xabarlari: 8
trekkadr → baza, skadr → Telegram, ssignal
150.181.19yuborildi
110.191.19yuborildi
250.160.21yuborildi
10.140.66yuborildi
2180.170.45yuborildi
1200.160.17yuborildi
2770.131.09yuborildi
4240.150.69yuborildi

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:

  1. Tizim yordamchi, hakam emas. Signal — "tekshiring" degani. Prorab xabarni ko'rib, qaror qiladi; tizim hech kimni avtomatik jarimaga tortmaydi.
  2. Engashgan va to'silgan ishchilar soxta signal beradi. Tasdiq oynasini uzaytirish ularni kamaytiradi, lekin haqiqiy kaskasizni ham kechroq topadi — bu mijoz bilan kelishiladigan murosa.
  3. 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.
  4. Qorong'i, tuman, qarama-qarshi yorug'lik — bu videoda sinalmagan. Pilotda mijozning o'z kameralaridan tun va yomon ob-havo kadrlarini ham belgilang.
  5. 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).
  6. 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

  1. Tasdiq ulushini (--ulush) 0.6 va 0.9 qilib jadvalni qayta hisoblang. Soxta signallar va o'tkazib yuborilganlar qanday o'zgaradi?
  2. Yuklab olingan boshqa Pexels videosida (masalan, 9227135 — ishchilar kaskasiz, kamera qo'lda) tizimni sinang. Kamera harakatlanganda nima buziladi? GMC yordam beradimi?
  3. Signalga dalil qo'shing: signal paytidagi odam qirqimini yuzi xiralashtirilgan holda saqlang va Telegram ga sendPhoto bilan 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.

Telegramda yozish Narxlar