מפעיל מחסן סורק פריטים בקצב מהיר. אם המצלמה קולטת קודים נוספים או מאחרת ב-1-2 שניות, מהירות המלאי יורדת. פתרנו בעיה זו עבור רשת הקמעונאות 'Products 24' — לאחר אופטימיזציה של הצינור, העיכוב ירד משתי שניות ל-300 אלפיות השנייה. במשך למעלה מ-10 שנים, יישמנו סריקת ברקוד ביותר מ-50 פרויקטים עבור קמעונאות, מחסנים וקופות עצמיות. נקודת הכאב המרכזית היא תצורת צינור לא אופטימלית: ללא הגבלת אזור הסריקה, המצלמה מעבדת פריימים מיותרים. הגישה שלנו בונה צינור מותאם לכל פלטפורמה — iOS AVCaptureSession או Android CameraX, ומבטיחה זיהוי יציב במכשירים מ-iPhone 6 ועד טלפוני Realme זולים. צרו קשר להערכת פרויקט וקבלו אומדן ראשוני.
iOS: AVCaptureSession + AVCaptureMetadataOutput
הגישה הקלאסית — צינור דרך AVCaptureSession:
let session = AVCaptureSession()
let device = AVCaptureDevice.default(for: .video)
guard let input = try? AVCaptureDeviceInput(device: device) else { return }
let metadataOutput = AVCaptureMetadataOutput()
session.addInput(input)
session.addOutput(metadataOutput)
metadataOutput.setMetadataObjectsDelegate(self, queue: .main)
metadataOutput.metadataObjectTypes = [.ean13, .ean8, .code128, .upce, .qr]
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.frame = view.bounds
previewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(previewLayer)
DispatchQueue.global(qos: .userInitiated).async {
session.startRunning()
}let session = AVCaptureSession() let device = AVCaptureDevice.default(for: .video) guard let input = try? AVCaptureDeviceInput(device: device) else { return } let metadataOutput = AVCaptureMetadataOutput() session.addInput(input) session.addOutput(metadataOutput) metadataOutput.setMetadataObjectsDelegate(self, queue: .main) metadataOutput.metadataObjectTypes = [.ean13, .ean8, .code128, .upce, .qr] let previewLayer = AVCaptureVideoPreviewLayer(session: session) previewLayer.frame = view.bounds previewLayer.videoGravity = .resizeAspectFill view.layer.addSublayer(previewLayer) DispatchQueue.global(qos: .userInitiated).async { session.startRunning() } תמיד על thread ברקע — על ה-thread הראשי הוא חוסם את ממשק המשתמש למשך 300–600 אלפיות השנייה בעת ההפעלה.
ה-delegate session.startRunning() מקבל את התוצאה:
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
guard let object = metadataObjects.first as? AVMetadataMachineReadableCodeObject, let code = object.stringValue else { return }
session.stopRunning()
handleCode(code)
} הבטחת זיהוי מיידי ב-iOS
השתמשו ב-AVCaptureMetadataOutputObjectsDelegate לאיזון בין איכות למהירות. ודאו ש-func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { guard let object = metadataObjects.first as? AVMetadataMachineReadableCodeObject, let code = object.stringValue else { return } session.stopRunning() handleCode(code) } נקרא על תור ברקע — קריאה על ה-thread הראשי חוסמת את ממשק המשתמש למשך 300–600 אלפיות השנייה. לסריקה מטווח קרוב, הגדירו AVCaptureVideoPreviewLayer.metadataOutputRectConverted(fromLayerRect:) ו-val cameraProviderFuture = ProcessCameraProvider.getInstance(context) cameraProviderFuture.addListener({ val cameraProvider = cameraProviderFuture.get() val preview = Preview.Builder().build() preview.setSurfaceProvider(previewView.surfaceProvider) val imageAnalysis = ImageAnalysis.Builder() .setTargetResolution(Size(1280, 720)) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() imageAnalysis.setAnalyzer(Executors.newSingleThreadExecutor(), BarcodeAnalyzer { barcode -> handleBarcode(barcode) }) cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, preview, imageAnalysis) }, ContextCompat.getMainExecutor(context)) . תצורה נכונה של אזור הסריקה מפחיתה את זמן הזיהוי ב-40%.
אזור סריקה
BarcodeAnalyzer מגביל את האזור שבו מחפשים קודים. הקואורדינטות במרחב מנורמל (0.0–1.0) עם צירים מוחלפים יחסית ל-UIKit. class BarcodeAnalyzer(private val onDetected: (String) -> Unit) : ImageAnalysis.Analyzer { private val scanner = BarcodeScanning.getClient( BarcodeScannerOptions.Builder().setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS).build() ) @androidx.camera.core.ExperimentalGetImage override fun analyze(imageProxy: ImageProxy) { val mediaImage = imageProxy.image ?: run { imageProxy.close(); return } val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) scanner.process(inputImage) .addOnSuccessListener { barcodes -> barcodes.firstOrNull()?.rawValue?.let { onDetected(it) } } .addOnCompleteListener { imageProxy.close() } } } ממיר מקואורדינטות UIKit לפורמט הנדרש.
ללא mobile_scanner, על מדפים עמוסים המצלמה עלולה לזהות ברקוד שכן במקום את זה שבכוונת הצלב. הניסיון שלנו מראה שהגדרת אזור נכונה מקצרת את זמן הזיהוי ב-40%.
Android: CameraX + ML Kit
Android: CameraX + ML Kit הוא הסטack המודרני המומלץ על ידי Google לאפליקציות המכוונות ל-API 21+. סריקת ברקוד של ML Kit (לפי תיעוד ML Kit) מספקת האצת חומרה במכשירים עם Android 8+.
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build()
preview.setSurfaceProvider(previewView.surfaceProvider)
val imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(Size(1280, 720))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
imageAnalysis.setAnalyzer(Executors.newSingleThreadExecutor(), BarcodeAnalyzer { barcode ->
handleBarcode(barcode)
})
cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, preview, imageAnalysis)
}, ContextCompat.getMainExecutor(context))final MobileScannerController controller = MobileScannerController(
detectionSpeed: DetectionSpeed.noDuplicates,
formats: [BarcodeFormat.ean13, BarcodeFormat.qr],
);
הוא קריטי. בלעדיו, פריימים מצטברים בתור והמפענח מתחיל לפגר ב-1-2 שניות.
BarcodeAnalyzer — מימוש של ImageAnalysis.Analyzer, המשתמש פנימית ב-ML Kit:
class BarcodeAnalyzer(private val onDetected: (String) -> Unit) : ImageAnalysis.Analyzer {
private val scanner = BarcodeScanning.getClient(
BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS)
.build()
)
@androidx.camera.core.ExperimentalGetImage
override fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image ?: run {
imageProxy.close()
return
}
val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
barcodes.firstOrNull()?.rawValue?.let { onDetected(it) }
}
.addOnCompleteListener { imageProxy.close() }
}
}imageProxy.close() ב-addOnCompleteListener הוא חובה — אחרת CameraX מפסיק לספק פריימים חדשים.
החשיבות של STRATEGY_KEEP_ONLY_LATEST ב-Android
STRATEGY_KEEP_ONLY_LATEST מבטיח שהמנתח מקבל רק את הפריים העדכני ביותר ללא הצטברות בתור. בלעדיו, פריימים מצטברים והמפענח פוגר ב-1-2 שניות — קריטי לתרחישים דינמיים. צינור ה-Android שלנו מהיר פי 2 ממימוש ZXing סטנדרטי.
השפעת הפוקוס האוטומטי על מכשירים זולים
במכשירי Android זולים (Realme C-series, Tecno), הפוקוס האוטומטי יכול להיות לא יציב. אנו משתמשים ב-CameraControl.startFocusAndMetering() עם FocusMeteringAction כדי לכפות פוקוס על המרכז כל 2 שניות. אנו גם מגדירים targetResolution ל-1280×720 — מספיק לזיהוי ברור אך עם פחות עומס על המעבד.
השוואת פלטפורמות: iOS לעומת Android
| פרמטר | iOS (AVCaptureSession) | Android (CameraX + ML Kit) |
|---|---|---|
| עיכוב פריים ראשון | ~300 אלפיות השנייה | ~150 אלפיות השנייה במכשירי Android 8+ |
| ניהול אזור | rectOfInterest (מנורמל, קואורדינטות מוחלפות) | Viewport דרך setCropRect ב-ImageAnalysis |
| תמיכת Push | רק דרך APNs | Firebase Cloud Messaging (FCM) |
| פוקוס אוטומטי | continuousAutoFocus + near | startFocusAndMetering() עם טיימר |
פורמטי ברקוד נתמכים
| פורמט | סוג | הערות |
|---|---|---|
| EAN-13 | 1D | סימון מוצר סטנדרטי |
| EAN-8 | 1D | גרסה מקוצרת |
| UPC-A | 1D | ארה"ב/קנדה |
| UPC-E | 1D | UPC קומפקטי |
| Code 128 | 1D | לוגיסטיקה |
| QR | 2D | נתונים וקישורים |
| Data Matrix | 2D | גדלים קטנים |
אנו תומכים בפורמטים נוספים לפי בקשה.
כיצד אנו משיגים זיהוי יציב במכשירים שונים
אנו בודקים סריקה ביותר מ-50 דגמים, כולל מותגים סיניים. עבור iOS אנו משתמשים ב-AVCaptureDevice.focusMode = .continuousAutoFocus; עבור Android, פוקוס אוטומטי כפוי עם טיימר. הגבלת אזור הסריקה דרך rectOfInterest ב-iOS ו-setCropRect ב-Android מבטלת זיהויים שגויים. כתוצאה מכך, דיוק הזיהוי עולה על 99% אפילו במכשירים עם מצלמות 8 MP.
מה כלול במימוש
בהזמנת מימוש סריקה turnkey, אנו מספקים:
- ממשק משתמש מותאם עם כוונת צלב (overlay) ואנימציות.
- תצורת אזור סריקה, פוקוס אוטומטי וטיפול בשגיאות.
- אינטגרציית מפענח (ML Kit, ZXing, SwiftCodeScanner) עם תמיכה בכל הפורמטים: EAN-13, EAN-8, UPC-A, UPC-E, Code 128, QR, Data Matrix.
- טיפול בתוצאות: שליחה לשרת, אחסון במסד נתונים מקומי או מילוי טפסים.
- בדיקות עומס על יותר מ-50 דגמי מכשירים (כולל מותגים סיניים).
- תיעוד אינטגרציה ותמיכה לאחר השחרור.
תהליך עבודה
- ניתוח דרישות. אנו קובעים רשימת פורמטים, סוגי מכשירים ודרישות מהירות.
- עיצוב. אנו בוחרים את הסטack האופטימלי: iOS — AVCaptureSession + AVFoundation, Android — CameraX + ML Kit. אופציונלית, פתרון חוצה פלטפורמות באמצעות Flutter.
- פיתוח. אנו כותבים קוד Swift/Kotlin לפי שיטות עבודה מומלצות: עיבוד אסינכרוני, הגבלת אזור, ניהול זיכרון.
- בדיקות. אנו מוודאים זיהוי על יותר מ-10 מכשירים, כולל זולים. מבצעים אופטימיזציה לתרחישים אמיתיים (תאורה, מרחק קוד).
- אינטגרציה. חיבור ל-API שלכם, הגדרת לוגים.
- שחרור. פרסום ל-App Store / Google Play, הגדרת TestFlight / Firebase App Distribution.
לוח זמנים: מימוש בסיסי (פלטפורמה אחת) — 1-3 ימים, פתרון מלא עם ממשק משתמש ואנליטיקה — עד 7 ימים. העלות מחושבת באופן אישי, לפי מספר הפלטפורמות ומורכבות האינטגרציה.
הגישה שלנו מפחיתה את עלויות האינטגרציה בחצי בהשוואה לרכישת SDK של צד שלישי. לקוחות חוסכים עד 40% מהתקציב בזכות תצורה אופטימלית. אנו מבטיחים פעולה יציבה על יותר מ-200 דגמי מכשירים, כולל מותגים סיניים. המהנדסים שלנו מחזיקים בהסמכות Apple Developer ו-Google Associate Android Developer.
קבלו מימוש סריקה turnkey אם אתם צריכים זיהוי מהיר ואמין באפליקציה שלכם. בקשו ייעוץ לפרויקט שלכם — ננתח את הדרישות ונציע את הפתרון הטוב ביותר.
דוגמת קוד Flutter
לפרויקטים חוצי פלטפורמות, אנו משתמשים בחבילת mobile_scanner עם API אחיד:
final MobileScannerController controller = MobileScannerController(
detectionSpeed: DetectionSpeed.noDuplicates,
formats: [BarcodeFormat.ean13, BarcodeFormat.qr],
);







