Инженерлік блог

Бір үзіліс кезіндегі түгендеу: Смартфон, Vertex AI Edge (LiteRT / TFLite) және Cloud Firestore арқылы 5 000 қорапты қалай санаймыз

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

1. Үш бөлімнен тұратын өндірістік драма: Қойманы қолмен санау неліктен үлкен азап?

Егер сіз өміріңізде бір рет болса да Рысқұлов даңғылындағы, Первомайкадағы немесе Алматының индустриалды аймағындағы көтерме қойма кешенінің түнгі ревизиясына қатысқан болсаңыз, «қайта есептеу» деген сөзді естігенде бойыңыз түршігетіні сөзсіз. Бұл кез келген логистикалық компанияның операциялық директоры мен қойма меңгерушісіне жақсы таныс нағыз өндірістік сынақ әрі жүйкеге түсетін үлкен жүктеме мен шаршау кезеңі.

Түнгі ревизияның дәстүрлі көрінісі мынадай: ауданы 4 500 шаршы метр болатын үлкен салқын ангар, биіктігі үш қабатты үйдей болатын 4 ярусты металл сөрелер, аяқ киім, тұрмыстық химия, құрылыс материалдары және электроника бұйымдары тиелген 5 000-нан астам картон қорап және қолында әрқайсысы $1 200 тұратын батырмалы өндірістік деректер жинау терминалы (ТСД) бар төрт маманнан тұратын шаршаған жұмыс тобы.

Түнгі сағат үшке таман жылытылмайтын суық қоймада нағыз өндірістік драма басталады. Минус он градус суықта брендтік ТСД құрылғыларының литий-ионды аккумуляторлары жылдам өшіп, әр жиырма минут сайын толығымен сөне бастайды. Оптикалық лазерлік сканерлер ұсақ өндірістік шаңға толып, жоғарғы сөрелердегі қажалған немесе мыжылған штрихкодтарды мүлдем оқымай қояды. Ал он төрт сағат бойы аяғынан тік тұрған қызметкер жылы бөлмеге тезірек жету үшін штрихкодтарды қарамай баса бастайды. Бұл өз кезегінде қойма есебінде өрескел қателіктерге алып келеді.

Аязда екі тәулік бойы тоқтаусыз жүрудің және мыңдаған қораптарды қолмен тасудың нәтижесі: 1.8 миллион теңге көлеміндегі тауар шатасуы (пересортица), дистрибьюторларға жүк көліктерін жөнелту кестесінің бұзылуы, логистика бөлімінің күйзелген жүйкесі және ішілген он төрт литр арзан кофе. Адам факторынан туындайтын осындай өндірістік мәселелерді біз «Gemini 2.5 Flash арқылы мата пішудегі ақауларды анықтау» мақаламызда қарастырған болатынбыз. Алайда бұл жолы қойма логистикасы бізге бұдан да қатаң инженерлік талап қойды — жүз пайыз толық оффлайн режим және нөлдік инференс кідірісі.

Сэндвич-панельдер мен профнастилден салынған кез келген қойма кешенінің басты жауы — радиосигналдардың таралмауы. Ангардың металл қаңқасы мен сөрелердің қалың қатарлары нағыз Фарадей торын түзеді. Мұндай жағдайда 4G/5G арқылы жоғары сапалы бейнеағынды бұлтқа үздіксіз жіберу туралы сөз қозғау мүмкін емес. Сөрелердің арасында байланыс үнемі үзіліп отырады. Ал бүкіл дәліздерге оптика тартып, өндірістік Wi-Fi 6 нүктелерін орнату сметасы қомақты 3.2 миллион теңгеге шығатын еді.

Бизнеске қарапайым, сенімді әрі қолжетімді шешім қажет болды: тексерушінің қолындағы кәдімгі Android-смартфон қатар бойымен жай жүріп өткен кезде секундына 30 кадр жиілікпен барлық көрініп тұрған қораптарды тауып, жақтаулармен белгілеп, нақты уақытта ешқандай желілік трафиксіз толық санап шығуы тиіс.

Төмендегі графикте қолмен есептеу мен мобильді компьютерлік көрудің нақты хронометражы мен дәлдік көрсеткіштері берілген:

5 000 қорапты түгендеу уақыты мен қателік деңгейін салыстыру

2. Неліктен бұлттық AI бұл жерде тиімсіз болды: Экономика мен физика заңдары

Заманауи үлкен тілдік және мультимодальді модельдермен шабыттанған кез келген әзірлеушінің алғашқы ойы: «Смартфон камерасының бейнеағынын тікелей Google Cloud Vision API немесе Gemini 2.5 Flash моделіне WebRTC арқылы стримдейік!» Бұл идея Google Cloud FinOps калькуляторы мен желілік кідірістер есебін көргенге дейін ғана тартымды әрі қарапайым болып көрінеді. Шын мәнінде бұл тәсіл бірден сәтсіздікке ұшырайды.

Нақты сандарға үңілейік. Компьютерлік көру жүйесінің бірқалыпты жұмыс істеуі үшін секундына кемінде 30 кадр (FPS) қажет. Егер қосымша әрбір кадрды бұлттық Object Detection API-ге жіберетін болса, бұл бір ғана қызметкер үшін сағатына 108 000 желілік HTTP POST-сұраныс тудырады. 1 000 сұраныс үшін $1.50 болатын Cloud Vision API тарифімен бір ғана үш сағаттық ревизия компанияға $486 (240 000 теңгеден астам) шығын әкеледі! Мұндай жобаның жылдық бұлттық шығыны жаңа қойма ғимаратын салу құнымен теңесер еді. Бұл кез келген бизнес үшін қаржылық тұрғыдан ақталмайды.

Екінші кедергі — желілік кідіріс (Round-Trip Time). Тіпті ең жақсы байланыс арнасының өзінде Алматыдан Google Cloud-тың Франкфурт немесе Варшава дата-орталықтарына дейінгі пинг 120–150 мс құрайды. Бұған кескінді өңдеуді, бұлттық кезекте тұруды және Bounding Box координаталары бар JSON-жауапты қайтаруды қосқанда, жалпы кідіріс 350–500 миллисекундқа дейін өседі. Секундына 1.2 метр жылдамдықпен жүріп келе жатқан қызметкер осы жарты секунд ішінде бір метрден артық алға кетіп қалады. Нәтижесінде экрандағы жақтаулар бос орындарға ығысып, адамды жаңылыстырады.

ОЗАТ инженерлік тобының шешімі біржақты болды: компьютерлік көру мен нысандарды қадағалаудың 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 профилімен оқытылды. Біз толық бүтін санды кванттауды (Full Integer INT8 Post-Training Quantization) қолдандық, бұл модель көлемін 48.2 МБ-тан бар болғаны 6.8 МБ-қа дейін қысқартты және mAP@0.5 дәлдігін толық сақтап қалды.

3-кезең: Центроидтық бақылау және жылдамдық векторы

Бір қорапты бейнеағындағы қатар кадрларда қайталап санауды болдырмау үшін құрылғыда траекторияларды бақылау алгоритмі орнатылды. Әрбір нақты затқа сөре кеңістігінде бірегей ID бекітіледі.

4-кезең: Firestore-ға идемпотентті синхрондау

Тексеру аяқталған соң нәтижелер алдымен жергілікті SQLite қорына сақталады, ал байланыс пайда болған кезде Cloud Firestore және 1С:WMS жүйесімен транзакциялық батчтар арқылы жаңартылады.

4. Бақылау математикасы: Процессорды жүктемей Double Counting-ті жеңу

Үздіксіз бейнеағын арқылы санаудағы басты математикалық қателік — қайта санау (Double Counting). Қызметкер сөре бойымен жай жүргенде, бір қорап объективте 25-тен 45 кадрға дейін көрініп тұрады. Егер детектор жай ғана қораптар санын қоса берсе, нәтиже отыз есе артып кетеді.

DeepSORT сияқты күрделі алгоритмдер мобильді құрылғының процессорын қатты қыздырып, кадр жиілігін 4–5 FPS-ке дейін түсіріп тастар еді. Сондықтан біз центроидтар, жылдамдық векторлары және Евклид арақашықтығы матрицасына негізделген жеңіл бақылау жүйесін құрдық:

  1. Центроидты есептеу: Әрбір табылған бокс үшін геометриялық орталық координаталары анықталады: $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. Тректің өмірлік циклінің күйлері:
    • TENTATIVE: Жаңа анықталған нысан (hits < 3), әзірге жалпы есепке қосылмайды.
    • CONFIRMED: Нысан қатарынан $ge 3$ кадрда көрінді, сөре есебі $+1$-ге артады.
    • LOST: Нысан уақытша көрінбей қалды; жадта 25 кадрға дейін сақталады.
    • DELETED: Нысан 25 кадрдан артық көрінбеді; жедел жадтан босатылады.

5. Смартфондағы оптикалық детектордың коды: LiteRT және нысандарды бақылау

Төменде камераның бейнеағынында қораптарды іздеуді, жабынды шекараларды сүзуді және бірегей нысандарды санауды жүзеге асыратын локалды Python инференс коды көрсетілген. Код GitHub-тағы OZAT ашық репозиторийімен толық синхрондалған.

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)

Ескерту: Бұл код толыққанды инференс модулі болып табылады. Ал Android/Kotlin мобильді қосымшасында осыған ұқсас C++ LiteRT C API компиляцияланған кітапханасы шақырылады.

6. Бұлтқа сенімді репликация: Cloud Firestore Sync Pipeline

Тексеруші өз бөлімін аяқтағанда, мобильді қосымша ревизияның толық пакетін жасайды. Ангардан әкімшілік ғимаратқа өту кезінде Wi-Fi байланысы үзілсе де, идемпотенттік жүйе (clientBatchId және Firestore Transactions) деректердің қайталануына немесе жоғалуына жол бермейді.

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, Samsung Galaxy A54 және Xiaomi Redmi Note 13 Pro құрылғыларында жүргізілген кең ауқымды сынақтар барысында кванттау мен аппараттық үдеткіштердің инференс жылдамдығы мен қызу деңгейіне әсері жан-жақты өлшенді:

  • Базалық Float32 моделі (Стандартты CPU 4 ядро): Бір кадрды өңдеу уақыты $p50$ бойынша 94.2 мс (10.6 FPS) болды. Смартфон төртінші минутта 42°C-қа дейін қызып, процессордың троттлингіне байланысты батарея әр 90 секундта 1%-ға азайды.
  • Квантталған INT8 моделі (CPU 4 ядро): Кадрды өңдеу уақыты 38.6 мс (25.9 FPS) деңгейіне түсті, температура 36°C шамасында тұрақталды.
  • Квантталған INT8 моделі (GPU Delegate): Кідіріс 21.0 мс (47.6 FPS) құрады, температура 33°C болды.
  • INT8 квантталған моделі (NNAPI / NPU Delegate): Кадрды өңдеу уақыты керемет 18.4 мс (54.3 FPS) деңгейіне дейін қысқарды ($p95 = 21.2 ext{ мс}$, $p99 = 24.8 ext{ мс}$). 30 FPS бейнеағыны екі еселік өнімділік қорымен өте бірқалыпты жұмыс істеді, құрылғының температурасы 31°C шамасында тұрақты болды, ал энергия шығыны Float32 CPU-мен салыстырғанда 3.8 есе азайды.

Өнімділік бенчмарктері төмендегі графикте көрсетілген:

Мобильді құрылғыдағы нейрожелі кідірісі және FPS

8. Жобаның экономикасы және FinOps: Қойманы есептеу қанша тұрады?

Жобаның нақты іске қосу шығындары мен экономикалық қайтарымын ешқандай жарнамалық әсірелеусіз нақты есептеп көрейік:

Шығындар құрылымы және жиынтық иелену құны (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 ₸)
  • Сканерлеу жабдықтары (Қызметкерлердің Android-смартфондары): 0 ₸ (қолда бар BYOD құрылғылары)
  • Ай сайынғы жиынтық бұлттық шығын: ≈ 410 ₸ / айына

Жүйе енгізілгенге дейін қойма басшылығы төрт тексерушінің түнгі ауысымдарына айына 480 000 ₸ үстемеақы төлейтін. Бұған қоса тауар шатасуынан болған тікелей шығындар мен таңғы жөнелтілімдердің кешігуінен болған айыппұлдар айына орташа есеппен 650 000 теңгені құрайтын. Компьютерлік көруді енгізу нәтижесінде ай сайынғы таза үнемдеу 1 000 000 теңгеден асты, ал жүйе алғашқы аптада-ақ өзін толық ақтады.

Егер сіз де өз қоймалық есебіңіз бен логистикаңызды оңтайландырғыңыз келсе, BigQuery және сквозная аналитика қызметімізбен, сондай-ақ «Gemini Live Audio арқылы кофені 40 секундта дайындау» кейсімізбен танысыңыз.

9. Инженерлік аудит: Жүйенің шектеулері мен компромисстері

Инженерлік аудит: жүйенің нақты шектеулері мен компромисстері

ОЗАТ-тың кәсіби инженерлік ашықтық қағидатына сай, біз жүйенің нақты техникалық шектеулері мен пайдалану кезіндегі компромисстерін ашық көрсетеміз:

  1. Паллетаның ішкі қабаттары (Көрінбейтін блоктар): Оптикалық камера тек сыртқы қабырғалардағы қораптарды ғана көреді. Егер паллета $3 imes 3 imes 3$ куб болып жиналса, ортадағы қорапты көру мүмкін емес. Мұндай қаптамалар үшін WMS базасындағы көлемдік толтыру коэффициенті (Volumetric Fill Factor) қолданылады.
  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-кодын қосымша сканерлеу ұсынылады.

💡 ОЗАТ кеңесі: Енгізуге дайынсыз ба? Архитектура мен бюджетті Scope Builder арқылы есептеңіз немесе тегін ЖИ-аудиттен өтіңіз.

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

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

Инженерлік блог авторы

Google Cloud архитектурасы саласындағы сарапшы және 15 жылдан астам тәжірибесі бар Senior Full-Stack әзірлеушісі. Ақауға төзімді архитектураларға, жоғары жүктемелі жобаларды оңтайландыруға және AI (Vertex AI) интеграциясына маманданған.

Сараптама: GCP, Kubernetes, Микросервистер, React, Node.js

Пікірлер (0)