Считали по кассе и на глаз: в пятницу «много», во вторник «мало». Я собрал систему на потолочных камерах, которые там уже висели. Ночью она сама перегоняет запись на домашний компьютер, распознаёт людей и к утру кладёт отчёт по смене. 14 смен подряд без единого ручного запуска. Дальше про то, как это устроено, сколько стоило и где система до сих пор врёт.
Из этой системы я сделал продукт для клубов и баров: okovision.ru ↗
Владельцу нужна одна цифра: сколько человек было за ночь. Купить её негде. Камеры в клубе обычные, потолочные, их вешали для охраны, про аналитику тогда никто не думал. Считать людей надо было на том, что есть.
Ночью пишется смена, утром она сама доезжает до видеокарты и превращается в отчёт. Человек в цепочке не нужен.
Веб-панель крутится на домашней машине. В ней состояние смены, список ночей, отчёты на скачивание, диски и агенты всех трёх машин, ручной прогон и чистка. Сделал её, чтобы человек без доступа к консоли мог сам посмотреть цифры и забрать отчёт.
Сколько людей в зале, считаю нарастающим итогом входов и выходов с шагом пять минут. По кривой видно, как зал наполняется и когда начинает пустеть. На графике пятница 19 сентября 2026 года, пик 296 человек около трёх ночи.
Модель распознавания я взял готовую. Основная работа началась, когда всё вроде бы «заработало»: конвейер тёк, терял смены и молча жёг видеокарту. Ниже шесть таких находок, на каждую ушёл отдельный разбор.
Отметку «эту ночь уже забрали» клали внутрь очереди, а очередь чистит другая машина. Отметка исчезала, и смена ехала снова. Перенёс отметку в отдельную папку вне очереди, которую никто не чистит.
Файл качается кусками параллельно, а докачка считала прогресс по размеру файла. При записи со смещением размер становится финальным сразу, а середина ещё пустая: получался целый по размеру и битый по содержимому файл. Теперь на каждый кусок пишется строка в журнале, и файл не считается готовым, пока журнал не закрыт.
Один повреждённый файл вставал в очередь и валил цепочку сутки за сутками. Добавил проверку файла до постановки в очередь и потолок попыток: после второго провала смена помечается и не долбится бесконечно.
Ограничение времени висело на ожидании процесса, и вместе со спящим компьютером «уснул» счётчик. Разбор шёл 401 минуту при лимите 180. Переписал на настенные часы: опрос раз в десять секунд, решение по реальному времени.
Процесс жив, вывода нет, и так пять часов сорок минут работы в никуда. Сделал сторож тишины: пока шаг пишет в лог, он считается живым. Замолчал на 20 минут, значит убиваем и перезапускаем.
За десять дней 26 срывов драйвера: процесс умирал без трассировки, смена терялась. Разделил падения «по делу» и падения «по железу»: вторые перезапускаются до трёх раз. Одну субботу это и спасло: шаг упал на 78-й минуте и доехал сам.
# процесс живой, пока пишет в лог; замолчал надолго — значит завис
$beat = [hashtable]::Synchronized(@{ last = Get-Date })
$onData = {
if ($null -ne $EventArgs.Data) {
[Console]::Out.WriteLine($EventArgs.Data)
$Event.MessageData.last = Get-Date
}
}
Register-ObjectEvent -InputObject $p -EventName OutputDataReceived -Action $onData -MessageData $beat
while (-not $p.HasExited) {
$now = Get-Date
$silent = ($now - $beat.last).TotalMinutes
if ($silent -gt $STALL_MIN) { $p.Kill(); $rc = 125; break } # завис
if ($now -gt $deadline) { $p.Kill(); $rc = 124; break } # не уложился
Start-Sleep -Seconds 10
}
Два кода возврата я развёл нарочно: 125 значит «молчит», 124 значит «не уложился в лимит». Первый перезапускается сам, второй разбираю руками.
# каждый доехавший кусок — строка в журнале; размер файла ничего не доказывает
with open(out, "r+b") as f:
f.seek(offset)
f.write(chunk)
f.flush(); os.fsync(f.fileno())
with open(out + ".parts", "a") as j:
j.write("%d %d\n" % (offset, len(chunk)))
# файл готов, только если сошлись все куски
if missing:
sys.exit(2)
os.remove(out + ".parts")
Потом я добавил проверку файла сторонним инструментом, и только после неё файл попадает в очередь.
Проще всего проверить счётчик балансом: сколько людей за ночь вошло и сколько вышло. К утру клуб пустеет, значит числа должны сойтись. По журналу событий за 14 ночей подряд:
| Ночь | Вошло | Вышло | Расхождение | Пик в зале |
|---|---|---|---|---|
| 11.09 · пт | 2199 | 1675 | — | 644 |
| 12.09 · сб | 2000 | 1532 | — | 590 |
| 13.09 · вс | 398 | 347 | 0 по минимуму | 80 |
| 15.09 · вт | 319 | 271 | 0 по минимуму | 98 |
| 17.09 · чт | 370 | 319 | −1 | 96 |
| 19.09 · пт | 1160 | 998 | −1 | 296 |
| 20.09 · сб | 517 | 709 | −245 | сорван |
| 23.09 · вт | 363 | 365 | −3 | 45 |
Расхождение здесь показывает, насколько «в зале» уходило в минус за ночь. Ноль значит, что кривая ни разу не провалилась ниже пустого зала и входы с выходами сошлись. На 13 ночах из 14 провал не больше шести человек.
Проход и человек не одно и то же. Один гость за ночь выходит покурить четыре раза, охранник мотается туда-сюда всю смену. Сырые 1062 прохода в субботу вовсе не значат 1062 гостя. Пересчёт я сделал через повторность: сколько проходов в среднем приходится на одного опознанного человека. И он, по-честному, не работает:
Вывод, который я записал владельцу прямым текстом: считать проходы система умеет, считать людей — пока нет. Докручивать формулу дальше смысла нет. Лучше поставить у входа одну камеру на уровне лица: тогда опознание вырастет, и пересчёты станут не нужны.
Это единственная ночь, где счётчик сорвало. С 22:00 до 22:11 система показала до 59 выходов в минуту и увела зал в минус 245 человек, а дальше вся ночь считалась нормально. Модель тут ни при чём. Запись начинается в 22:00, счётчик присутствия стартует с нуля и не знает, что в зале уже есть люди. Все, кто вошёл до начала записи, выходят «в минус», и пик ночи считается от съехавшего нуля. Лечится калибровкой старта по минимуму кривой. На остальных тринадцати ночах минимум и так нулевой, поэтому всплыло это только раз.
Могу собрать конвейер из того, что у вас уже есть, и честно сказать, где он будет врать. Пишите в Telegram, отвечу сегодня.