Инженерный блог

Инвентаризация за перекур: Считаем 5 000 коробок на складе через смартфон с Vertex AI Edge (LiteRT / TFLite) и Cloud Firestore

26.08.2026
Шарафутдинов Р.

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 спроектировала оптимизированный алгоритм ассоциации траекторий на базе центроидов и евклидова расстояния с оценкой вектора скорости:

  1. Вычисление центроидов детекций: Для каждого найденного Bounding Box определяется геометрический центр на плоскости кадра: $mathbf{C}_k = left( rac{x_{min} + x_{max}}{2}, rac{y_{min} + y_{max}}{2} ight)$
  2. Оценка вектора скорости объекта: Скорость смещения центроида между соседними кадрами во времени $Delta t$: $ ec{v}_k = left( rac{Delta x}{Delta t}, rac{Delta y}{Delta t} ight)$
  3. Матрица евклидовых расстояний: Вычисляется расстояние между спрогнозированным положением существующего трека и новыми центроидами текущего кадра: $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}$
  4. Динамический строб ассоциации (Gating Threshold): Сопоставление считается валидным, если расстояние не превышает адаптивный порог: $D_{ ext{gate}} = maxleft(55.0,; | ec{v}_j| cdot 1.8 + 35.0 ight)$
  5. Конечный автомат жизненного цикла трека (State Machine):
    • TENTATIVE: Трек только появился (hits < 3), счетчик не увеличивается.
    • CONFIRMED: Трек стабильно наблюдается $ge 3$ кадров подряд, счетчик стеллажа инкрементируется на $+1$.
    • LOST: Объект временно перекрыт или вышел из кадра; хранится в памяти до 25 кадров.
    • DELETED: Объект отсутствует более 25 кадров; память освобождается.

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-системы:

  1. Внутренние слои паллет (слепые зоны геометрии): Оптическая камера фиксирует только внешние видимые грани коробок. Если паллета собрана плотным кубом $3 imes 3 imes 3$, центральные скрытые коробки оптически недоступны. Для таких паллет применяется расчетный коэффициент объемного заполнения (Volumetric Fill Factor) из WMS-базы.
  2. Сильно деформированные и смятые коробки: Коробки с нарушением геометрической целостности более чем на 35% детектируются с пониженным скором уверенности ($0.45 ext{–}0.55$) и требуют подтверждения оператора в один тап по экрану.
  3. Минимальный уровень освещенности: При освещенности ниже 15 люкс в неосвещенных тупиках ангара на матрице камеры возникает шум. В этом режиме приложение автоматически включает встроенную светодиодную вспышку смартфона (Torch Mode).
  4. Ограничение скорости перемещения: При быстром шаге оператора (> 1.8 м/с) возникает смаз изображения (Motion Blur). Оптимальная скорость стабильного сканирования составляет 1.0–1.2 м/с.
  5. Блики многослойной стрейч-пленки: При наложении более 5 слоев глянцевой пленки возможны ложные зеркальные отражения. Проблема решается легким отклонением угла съемки на $15^circ$ относительно нормали паллеты.
  6. Смешанные паллеты с мелкой штучной продукцией (Multi-SKU): Для паллет с десятками разнородных мелких упаковок требуется дополнительный проход со сканированием группового QR-кода паллеты.

💡 Совет OZAT: Готовы к внедрению? Рассчитайте архитектуру и бюджет через Scope Builder или пройдите бесплатный ИИ-аудит.

Рустам Шарафутдинов

Рустам Шарафутдинов

Автор инженерного блога

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

Экспертность: GCP, Kubernetes, Микросервисы, React, Node.js

Комментарии (0)