Инвентаризация за перекур: Считаем 5 000 коробок на складе через смартфон с Vertex AI Edge (LiteRT / TFLite) и Cloud Firestore
1. Трагедия в трех актах: Почему ручная инвентаризация на складе — это боль
Если вы хоть раз в жизни присутствовали на ночной ревизии оптового распределительного центра в районе промзоны на Рыскулова, на Первомайских прудах или в индустриальной зоне Алматы, у вас наверняка начинает предательски дергаться глаз при одном только слове «переучет». Это совершенно особый вид производственного чистилища, знакомый каждому операционному директору и начальнику складской логистики.
Классическая картина ночного переучета выглядит так: гигантский неотапливаемый ангар площадью 4 500 квадратных метров, четырехъярусные металлические стеллажи высотой с трехэтажный дом, более 5 000 картонных коробок с обувью, сантехникой, бытовой химией и электроникой, и четыре героических ревизора в зимних бушлатах с промышленными терминалами сбора данных (ТСД) по $1 200 за штуку.
К трем часам ночи на неотапливаемом складе разворачивается настоящая драма. Литий-ионные аккумуляторы брендовых ТСД на морозе в минус десять градусов начинают стремительно деградировать и выключаться каждые двадцать минут. Оптические лазерные сканеры покрываются тончайшим слоем складской пыли и отказываются считывать потертые штрихкоды на верхних ярусах. Уставший кладовщик, проведя на ногах уже четырнадцать часов, начинает механически «пробивать» соседние паллеты не глядя, лишь бы согреться в бытовке.
Итог двух суток непрерывного хождения по морозу и перекладывания коробок руками: пересортица на 1.8 миллиона тенге, сорванный график отгрузки фур региональным дистрибьюторам, сожженные нервы отдела логистики и четырнадцать литров выпитого растворимого кофе 3-в-1. Похожие инфраструктурные боли с человеческим фактором мы уже подробно разбирали в нашем инженерном разборе «Оптический раскрой ткани без брака в Алматы через Gemini 2.5 Flash», однако складская логистика выкатила нам еще более бескомпромиссные требования — стопроцентный оффлайн-режим и абсолютно нулевая задержка инференса прямо в руках кладовщика.
Главный враг любого логистического хаба из сэндвич-панелей и профнастила — это физика распространения радиоволн. Металлический каркас ангара и плотные ряды стеллажей создают идеальную клетку Фарадея. Ни о каком стабильном 4G/5G стриминге видеопотока высокого разрешения в облако здесь не может быть и речи. Связь в узких проходах между стеллажами непрерывно падает до уровня Edge или исчезает вовсе. Прокладка оптоволоконных трасс и установка десятков промышленных точек доступа Wi-Fi 6 с бесшовным роумингом по смете подрядчиков выливалась в внушительные 3.2 миллиона тенге.
Бизнесу требовалось элегантное, надежное и доступное решение: обычный рабочий Android-смартфон в руках кладовщика, который во время неспешного шага вдоль ряда с частотой 30 кадров в секунду находит, выделяет рамками и с абсолютной точностью пересчитывает все видимые коробки на стеллажах без единого байта сетевого трафика в реальном времени.
Ниже на графике приведены фактические данные сравнительного хронометража и точности ручного аудита против мобильного компьютерного зрения:
Сравнение времени инвентаризации 5 000 коробок и процента ошибок
2. Почему наивный облачный AI здесь мгновенно умер: Экономика и физика
Первый импульс любого разработчика, вдохновленного маркетинговыми презентациями современных мультимодальных моделей: «А давайте просто настроим WebRTC-стрим с камеры смартфона в Google Cloud Vision API или будем слать кадры пачками в Gemini 2.5 Flash через серверный прокси!» Идея звучит привлекательно и современно ровно до тех пор, пока вы не возьмете в руки калькулятор и спецификацию задержек сетевых протоколов.
Давайте проведем честный расчет сетевой и финансовой нагрузки. Видеопоток для плавной детекции требует частоты не менее 30 кадров в секунду (FPS). Если приложение будет слать каждый отдельный кадр в облачный Object Detection API, это сгенерирует 108 000 сетевых HTTP POST-запросов в час на одного ревизора. При стандартной стоимости $1.50 за 1 000 вызовов Cloud Vision API одна трехчасовая смена ревизии обойдется компании в $486 (более 240 000 тенге) только за потребление API! Для ежемесячных плановых и внеплановых проверок годовой чек за облако превысил бы стоимость постройки нового небольшого склада.
Второй непреодолимый барьер — задержка передачи сигнала (Round-Trip Time). Даже при идеальном канале связи пинг от Алматы до ближайших европейских дата-центров Google Cloud (Франкфурт или Варшава) составляет порядка 120–150 мс. Добавьте к этому время сериализации изображения, время инференса в очереди облака и время обратной доставки JSON-ответа с координатами Bounding Boxes — суммарная задержка возрастает до 350–500 мс. Кладовщик, идущий со скоростью 1.2 метра в секунду, за полсекунды смещает камеру почти на метр. В результате подсвеченные на экране рамки «плывут», накладываются на пустоту и полностью дезориентируют человека.
Вывод для команды OZAT был однозначным: 100% компьютерного зрения и алгоритмов трекинга обязаны выполняться локально на кремнии смартфона (Edge AI). А Google Cloud используется как высокопроизводительная фабрика моделей: мы размечаем и храним датасет в Cloud Storage, обучаем глубокую сверточную нейросеть в Vertex AI, квантуем ее в оптимизированный бинарник Google AI Edge LiteRT (TensorFlow Lite INT8) и развертываем на устройства персонала.
3. Архитектура Edge Vision: От обучения в Vertex AI до LiteRT INT8
Чтобы построить модель, устойчивую к перепадам освещения, пыли, бликам стрейч-пленки и угловым перекрытиям коробок, мы развернули сквозной конвейер на базе Google Cloud Vertex AI и среды исполнения Google AI Edge LiteRT:
Этап 1: Сбор датасета и синтетическая аугментация
Мы отсняли 350 паллет в различных условиях освещения. Через конвейер Vertex AI Dataset провели агрессивную аугментацию: пространственные повороты $pm 15^circ$, изменение гаммы, имитацию засветов фонаря и запыленности оптики, расширив выборку до 3 200 валидированных кадров.
Этап 2: Vertex AI Edge Training и INT8 квантование
Модель обучалась с профилем Mobile high accuracy. При экспорте в LiteRT мы применили полное целочисленное квантование (Full Integer INT8 Post-Training Quantization), сократив вес графа с 48.2 МБ до 6.8 МБ без деградации метрики mAP@0.5.
Этап 3: Центроидный трекинг с оценкой скорости
Чтобы исключить повторный подсчет одной и той же коробки в соседних кадрах видеопотока, на устройстве работает векторный трекер траекторий. Каждому физическому объекту присваивается персистентный ID с фильтрацией ложных срабатываний.
Этап 4: Идемпотентная репликация в Firestore
По завершении прохода результаты сохраняются в локальный буфер SQLite, а при выходе в зону Wi-Fi или сотовой связи отправляются транзакционным пакетом в Cloud Firestore и WMS-систему предприятия.
4. Математика трекинга: Предотвращение Double Counting без перегрева CPU
Главный математический вызов при непрерывном оптическом подсчете коробок на видеопотоке — устранение эффекта повторного счета (Double Counting). Когда оператор идет вдоль стеллажа, одна и та же коробка попадает в поле зрения камеры на протяжении 25–45 последовательных кадров. Если просто суммировать детекции на каждом кадре, итоговый результат завысится в тридцать раз.
Тяжелые трекеры класса DeepSORT с вычислением признаков ReID на мобильном процессоре мгновенно просаживают производительность до 4–6 FPS и перегревают чип. Команда OZAT спроектировала оптимизированный алгоритм ассоциации траекторий на базе центроидов и евклидова расстояния с оценкой вектора скорости:
- Вычисление центроидов детекций: Для каждого найденного Bounding Box определяется геометрический центр на плоскости кадра: $mathbf{C}_k = left( rac{x_{min} + x_{max}}{2}, rac{y_{min} + y_{max}}{2} ight)$
- Оценка вектора скорости объекта: Скорость смещения центроида между соседними кадрами во времени $Delta t$: $ec{v}_k = left( rac{Delta x}{Delta t}, rac{Delta y}{Delta t} ight)$
- Матрица евклидовых расстояний: Вычисляется расстояние между спрогнозированным положением существующего трека и новыми центроидами текущего кадра: $D_{ij} = sqrt{(x_i - (x_j + v_{x,j} cdot Delta t))^2 + (y_i - (y_j + v_{y,j} cdot Delta t))^2}$
- Динамический строб ассоциации (Gating Threshold): Сопоставление считается валидным, если расстояние не превышает адаптивный порог: $D_{ ext{gate}} = maxleft(55.0,; |ec{v}_j| cdot 1.8 + 35.0 ight)$
- Конечный автомат жизненного цикла трека (State Machine):
- TENTATIVE: Трек только появился (hits < 3), счетчик не увеличивается.
- CONFIRMED: Трек стабильно наблюдается $ge 3$ кадров подряд, счетчик стеллажа инкрементируется на $+1$.
- LOST: Объект временно перекрыт или вышел из кадра; хранится в памяти до 25 кадров.
- DELETED: Объект отсутствует более 25 кадров; память освобождается.
Раскрой без брака: Как швейный цех на 12 швей в Алматы экономит 800 000 ₸ на ткани с помощью Gemini 2.5 Flash (Vision) и Cloud Storage
5. Промышленный код детектора: LiteRT, NPU и многообъектный трекинг
Ниже представлен модуль оптической детекции и трекинга на Python с поддержкой аппаратного ускорения NNAPI Delegate и фильтрацией траекторий. Исходный код синхронизирован с открытым инженерным репозиторием OZAT на GitHub.
import time
import math
import numpy as np
from dataclasses import dataclass, field
from enum import Enum
from typing import List, Dict, Tuple, Optional
# Google AI Edge LiteRT (formerly TensorFlow Lite Runtime)
try:
import ai_edge_litert.interpreter as litert
except ImportError:
import tflite_runtime.interpreter as litert
class TrackState(Enum):
TENTATIVE = "TENTATIVE" # Initial detection phase (< 3 consecutive hits)
CONFIRMED = "CONFIRMED" # Confirmed object (>= 3 hits), counter incremented
LOST = "LOST" # Temporarily occluded / out-of-frame (kept up to 25 frames)
DELETED = "DELETED" # Expired track, cleaned from memory
@dataclass
class BoundingBox:
ymin: float
xmin: float
ymax: float
xmax: float
score: float
class_id: int
@property
def centroid(self) -> Tuple[float, float]:
"""Calculates centroid C_k = ((xmin + xmax)/2, (ymin + ymax)/2) in pixel space."""
return ((self.xmin + self.xmax) / 2.0, (self.ymin + self.ymax) / 2.0)
@dataclass
class Track:
track_id: int
centroid: Tuple[float, float]
velocity: Tuple[float, float] = (0.0, 0.0) # (vx, vy) in pixels/sec
last_timestamp: float = field(default_factory=time.time)
hits: int = 1
misses: int = 0
state: TrackState = TrackState.TENTATIVE
is_counted: bool = False
def predict_position(self, current_time: float) -> Tuple[float, float]:
"""Predicts position based on velocity: x_pred = x + vx * dt, y_pred = y + vy * dt."""
dt = max(current_time - self.last_timestamp, 1e-4)
return (
self.centroid[0] + self.velocity[0] * dt,
self.centroid[1] + self.velocity[1] * dt
)
def update(self, new_centroid: Tuple[float, float], current_time: float):
"""Updates track centroid, velocity, and state machine."""
dt = max(current_time - self.last_timestamp, 1e-4)
vx = (new_centroid[0] - self.centroid[0]) / dt
vy = (new_centroid[1] - self.centroid[1]) / dt
alpha = 0.6
self.velocity = (
alpha * vx + (1.0 - alpha) * self.velocity[0],
alpha * vy + (1.0 - alpha) * self.velocity[1]
)
self.centroid = new_centroid
self.last_timestamp = current_time
self.hits += 1
self.misses = 0
if self.state == TrackState.TENTATIVE and self.hits >= 3:
self.state = TrackState.CONFIRMED
class LiteRTBoxDetector:
"""LiteRT INT8 Object Detector with Hardware Acceleration Delegate."""
def __init__(self, model_path: str = "models/warehouse_boxes_int8.tflite", use_nnapi: bool = True):
delegates = []
if use_nnapi:
try:
nnapi_delegate = litert.load_delegate("libnnapi_delegate.so")
delegates.append(nnapi_delegate)
except Exception as err:
print(f"[WARN] NNAPI Delegate unavailable, falling back to multi-core CPU: {err}")
self.interpreter = litert.Interpreter(
model_path=model_path,
experimental_delegates=delegates,
num_threads=4
)
self.interpreter.allocate_tensors()
self.input_details = self.interpreter.get_input_details()
self.output_details = self.interpreter.get_output_details()
self.input_shape = self.input_details[0]['shape']
def infer(self, frame_rgb: np.ndarray, score_threshold: float = 0.55) -> List[BoundingBox]:
input_data = np.expand_dims(frame_rgb, axis=0)
if self.input_details[0]['dtype'] == np.uint8:
input_tensor = input_data.astype(np.uint8)
else:
input_tensor = (input_data / 255.0).astype(np.float32)
self.interpreter.set_tensor(self.input_details[0]['index'], input_tensor)
self.interpreter.invoke()
boxes = self.interpreter.get_tensor(self.output_details[0]['index'])[0]
classes = self.interpreter.get_tensor(self.output_details[1]['index'])[0]
scores = self.interpreter.get_tensor(self.output_details[2]['index'])[0]
detected_boxes: List[BoundingBox] = []
for i in range(len(scores)):
if scores[i] >= score_threshold:
detected_boxes.append(BoundingBox(
ymin=float(boxes[i][0]),
xmin=float(boxes[i][1]),
ymax=float(boxes[i][2]),
xmax=float(boxes[i][3]),
score=float(scores[i]),
class_id=int(classes[i])
))
return detected_boxes
class WarehouseCentroidTracker:
"""Anti-Double Counting Centroid Tracker with State Machine & Dynamic Gating."""
def __init__(self, max_missed_frames: int = 25):
self.next_track_id = 1
self.tracks: Dict[int, Track] = {}
self.max_missed_frames = max_missed_frames
self.total_unique_boxes_counted = 0
def update(self, detections: List[BoundingBox], frame_width: int, frame_height: int) -> int:
current_time = time.time()
centroids = [
(box.centroid[0] * frame_width, box.centroid[1] * frame_height)
for box in detections
]
if not self.tracks:
for c in centroids:
self._register_track(c, current_time)
return self.total_unique_boxes_counted
track_ids = list(self.tracks.keys())
predicted_positions = [self.tracks[tid].predict_position(current_time) for tid in track_ids]
assigned_tracks = set()
assigned_centroids = set()
if centroids and predicted_positions:
dist_matrix = np.zeros((len(track_ids), len(centroids)), dtype=np.float32)
for i, p_pos in enumerate(predicted_positions):
for j, c in enumerate(centroids):
dist_matrix[i, j] = math.hypot(p_pos[0] - c[0], p_pos[1] - c[1])
row_indices = np.argsort(dist_matrix.min(axis=1))
for r in row_indices:
tid = track_ids[r]
track = self.tracks[tid]
speed = math.hypot(track.velocity[0], track.velocity[1])
dynamic_gate = max(55.0, speed * 1.8 + 35.0)
col = np.argmin(dist_matrix[r])
if col not in assigned_centroids and dist_matrix[r, col] <= dynamic_gate:
track.update(centroids[col], current_time)
if track.state == TrackState.CONFIRMED and not track.is_counted:
track.is_counted = True
self.total_unique_boxes_counted += 1
assigned_tracks.add(tid)
assigned_centroids.add(col)
for tid in track_ids:
if tid not in assigned_tracks:
self.tracks[tid].misses += 1
self.tracks[tid].state = TrackState.LOST
if self.tracks[tid].misses > self.max_missed_frames:
self.tracks[tid].state = TrackState.DELETED
del self.tracks[tid]
for j, c in enumerate(centroids):
if j not in assigned_centroids:
self._register_track(c, current_time)
return self.total_unique_boxes_counted
def _register_track(self, centroid: Tuple[float, float], current_time: float):
self.tracks[self.next_track_id] = Track(
track_id=self.next_track_id,
centroid=centroid,
last_timestamp=current_time,
state=TrackState.TENTATIVE
)
self.next_track_id += 1Смотреть код на GitHub (OZAT-kz)
Примечание: Приведенный код является законченным модулем инференса. В мобильном приложении на Kotlin/Flutter вызывается аналогичный скомпилированный C++ пайплайн LiteRT C API.
6. Отказоустойчивая синхронизация: Идемпотентный пайплайн Cloud Firestore
При выходе ревизора из «мертвой зоны» металлического ангара мобильное приложение выполняет транзакционную синхронизацию накопленных данных. Чтобы исключить риск повторного зачисления остатков при сбоях нестабильного Wi-Fi соединения, используется защита на основе клиентского UUID (clientBatchId) и транзакций Firestore.
import { Firestore, FieldValue, Timestamp } from '@google-cloud/firestore';
import { v4 as uuidv4 } from 'uuid';
const firestore = new Firestore({
projectId: process.env.GOOGLE_CLOUD_PROJECT || 'ozat-warehouse-prod',
databaseId: process.env.FIRESTORE_DATABASE_ID || '(default)'
});
export interface InventoryScanBatch {
clientBatchId: string; // Unique UUID v4 generated on mobile edge device
warehouseId: string; // E.g., 'wh-almaty-ryskulova-01'
aisleZone: string; // E.g., 'rack-row-B4-tier2'
skuCategory: string; // E.g., 'footwear-sport-sneakers'
countedBoxes: number; // Total confirmed boxes from LiteRT tracker
operatorId: string; // Employee identifier
scannedAtIso: string; // Edge scan timestamp
confidenceScoreAvg: number; // Optical detector confidence mean
}
export interface SyncResponse {
success: boolean;
status: 'PROCESSED' | 'DUPLICATE_IGNORED' | 'ERROR';
batchId: string;
totalRackCount: number;
syncedAt: string;
}
/**
* Idempotently syncs offline warehouse scan batches into Cloud Firestore.
* Prevents double-accounting when edge devices retry over unstable Wi-Fi.
*/
export async function syncWarehouseBatch(batch: InventoryScanBatch): Promise<SyncResponse> {
const idempotencyRef = firestore.collection('idempotency_keys').doc(batch.clientBatchId);
const warehouseRackRef = firestore
.collection('warehouses')
.doc(batch.warehouseId)
.collection('inventory_racks')
.doc(batch.aisleZone);
const auditLogRef = firestore.collection('inventory_audit_logs').doc();
try {
const result = await firestore.runTransaction(async (transaction) => {
// Step 1: Idempotency Lock Check
const idempotencyDoc = await transaction.get(idempotencyRef);
if (idempotencyDoc.exists) {
const existingData = idempotencyDoc.data();
console.warn(`[IDEMPOTENCY] Batch ${batch.clientBatchId} was already synced at ${existingData?.processedAt?.toDate()}`);
return {
success: true,
status: 'DUPLICATE_IGNORED' as const,
batchId: batch.clientBatchId,
totalRackCount: existingData?.recordedRackTotal || 0,
syncedAt: existingData?.processedAt?.toDate()?.toISOString() || new Date().toISOString()
};
}
// Step 2: Read current rack state or initialize
const rackDoc = await transaction.get(warehouseRackRef);
const currentRackBoxes = rackDoc.exists ? (rackDoc.data()?.totalBoxes || 0) : 0;
const newTotal = currentRackBoxes + batch.countedBoxes;
// Step 3: Atomic Mutation of Rack Inventory
transaction.set(warehouseRackRef, {
aisleZone: batch.aisleZone,
skuCategory: batch.skuCategory,
totalBoxes: newTotal,
lastOperatorId: batch.operatorId,
lastConfidenceScore: batch.confidenceScoreAvg,
updatedAt: FieldValue.serverTimestamp()
}, { merge: true });
// Step 4: Write Immutable Audit Log
transaction.set(auditLogRef, {
auditId: auditLogRef.id,
batchId: batch.clientBatchId,
warehouseId: batch.warehouseId,
aisleZone: batch.aisleZone,
addedBoxes: batch.countedBoxes,
resultingTotal: newTotal,
operatorId: batch.operatorId,
scannedAt: Timestamp.fromDate(new Date(batch.scannedAtIso)),
syncedAt: FieldValue.serverTimestamp()
});
// Step 5: Seal Idempotency Key
transaction.set(idempotencyRef, {
batchId: batch.clientBatchId,
warehouseId: batch.warehouseId,
aisleZone: batch.aisleZone,
recordedRackTotal: newTotal,
processedAt: FieldValue.serverTimestamp()
});
return {
success: true,
status: 'PROCESSED' as const,
batchId: batch.clientBatchId,
totalRackCount: newTotal,
syncedAt: new Date().toISOString()
};
});
return result;
} catch (error: any) {
console.error(`[SYNC_ERROR] Failed to sync batch ${batch.clientBatchId}:`, error);
throw new Error(`Firestore transactional sync failed: ${error.message}`);
}
}Смотреть код на GitHub (OZAT-kz)
7. Аппаратные бенчмарки: Сравнение CPU, GPU Delegate и NPU
В ходе стресс-тестирования на устройствах Google Pixel 7a (Tensor G2), Samsung Galaxy A54 (Exynos 1380) и Xiaomi Redmi Note 13 Pro (Snapdragon 7s Gen 2) мы измерили задержки инференса, энергопотребление и температурные режимы при длительной непрерывной съемке:
- Базовая модель Float32 (CPU 4 ядра): Время инференса $p50$ составляет 94.2 мс (10.6 FPS). Смартфон разогревается до 42°C уже к четвертой минуте сканирования, а батарея разряжается на 1% каждые 90 секунд из-за троттлинга процессора.
- Квантованная модель INT8 (CPU 4 ядра): Время инференса снижается до 38.6 мс (25.9 FPS), температура стабилизируется на уровне 36°C.
- Квантованная модель INT8 (GPU Delegate OpenCL): Задержка составляет 21.0 мс (47.6 FPS), энергопотребление умеренное, температура 33°C.
- Квантованная модель INT8 (NNAPI Delegate / NPU): Задержка достигает рекордных 18.4 мс (54.3 FPS) ($p95 = 21.2 ext{ мс}$, $p99 = 24.8 ext{ мс}$). Видеопоток 30 FPS обрабатывается с двойным запасом производительности, температура устройства составляет 31°C, а расход аккумулятора снижается в 3.8 раза по сравнению с Float32 CPU.
Результаты бенчмарков производительности представлены на графике:
Задержка инференса нейросети и FPS на мобильном устройстве
8. Экономика проекта и FinOps: Сколько стоит пересчет склада?
Давайте сведем детальную смету совокупной стоимости владения (TCO) системы инспекции на базе Vertex AI Edge и сравним ее с традиционным ручным переучетом:
Структура расходов и совокупная стоимость владения (TCO):
- Обучение модели в Vertex AI Edge (2.5 Node Hours): $18.40 (≈ 9 200 ₸ разово)
- Хранение датасета в Cloud Storage (20 ГБ): $0.46 / месяц (≈ 230 ₸)
- Операции записи в Cloud Firestore (200 000 док/мес): $0.36 / месяц (≈ 180 ₸)
- Оборудование для инспекции (смартфоны сотрудников BYOD): 0 ₸ (используются имеющиеся устройства)
- Итоговые ежемесячные облачные расходы: ≈ 410 ₸ / месяц
До внедрения компьютерного зрения компания ежемесячно выплачивала 480 000 ₸ в виде ночных надбавок четырем ревизорам. Дополнительные прямые потери от пересортицы и штрафов за задержку утренних отгрузок фур составляли в среднем 650 000 ₸ в месяц. Внедрение Vertex AI Edge обеспечило чистую ежемесячную экономию более 1 000 000 ₸, а проект окупил разовые инвестиции в первую же неделю промышленной эксплуатации.
Для масштабирования корпоративной аналитики рекомендуем ознакомиться с нашей услугой «BigQuery и сквозная аналитика данных», а также с архитектурным кейсом «Кофе за 40 секунд: Firebase Realtime Database и Gemini Live Audio».
9. Инженерный аудит: Ограничения и компромиссы решения
В соответствии с принципами инженерной честности OZAT, мы открыто фиксируем технические ограничения и эксплуатационные компромиссы разработанной Edge-системы:
- Внутренние слои паллет (слепые зоны геометрии): Оптическая камера фиксирует только внешние видимые грани коробок. Если паллета собрана плотным кубом $3 imes 3 imes 3$, центральные скрытые коробки оптически недоступны. Для таких паллет применяется расчетный коэффициент объемного заполнения (Volumetric Fill Factor) из WMS-базы.
- Сильно деформированные и смятые коробки: Коробки с нарушением геометрической целостности более чем на 35% детектируются с пониженным скором уверенности ($0.45 ext{–}0.55$) и требуют подтверждения оператора в один тап по экрану.
- Минимальный уровень освещенности: При освещенности ниже 15 люкс в неосвещенных тупиках ангара на матрице камеры возникает шум. В этом режиме приложение автоматически включает встроенную светодиодную вспышку смартфона (Torch Mode).
- Ограничение скорости перемещения: При быстром шаге оператора (> 1.8 м/с) возникает смаз изображения (Motion Blur). Оптимальная скорость стабильного сканирования составляет 1.0–1.2 м/с.
- Блики многослойной стрейч-пленки: При наложении более 5 слоев глянцевой пленки возможны ложные зеркальные отражения. Проблема решается легким отклонением угла съемки на $15^circ$ относительно нормали паллеты.
- Смешанные паллеты с мелкой штучной продукцией (Multi-SKU): Для паллет с десятками разнородных мелких упаковок требуется дополнительный проход со сканированием группового QR-кода паллеты.
💡 Совет OZAT: Готовы к внедрению? Рассчитайте архитектуру и бюджет через Scope Builder или пройдите бесплатный ИИ-аудит.

Рустам Шарафутдинов
Эксперт в области архитектуры Google Cloud и Senior Full-Stack разработчик с более чем 15-летним опытом. Специализируется на отказоустойчивых архитектурах, оптимизации высоконагруженных проектов и интеграции AI (Vertex AI).