التعرّف على النص في الصور باستخدام حزمة تعلّم الآلة على نظام التشغيل Android

تنظيم صفحاتك في مجموعات يمكنك حفظ المحتوى وتصنيفه حسب إعداداتك المفضّلة.

يمكنك استخدام حزمة تعلّم الآلة للتعرّف على النص في الصور أو الفيديوهات، مثل نص لافتة في الشارع. خصائص هذه الميزة الرئيسية هي:

واجهة برمجة تطبيقات التعرف على النصوص
الوصفالتعرّف على نص الكتابة بالأحرف اللاتينية في الصور أو الفيديوهات
اسم المكتبةcom.google.android.gms:play-services-mlkit-text-recognition
التنفيذيتم تنزيل المكتبة ديناميكيًا من خلال خدمات Google Play.
تأثير حجم التطبيق260 كيلوبايت
وقت الإعدادوقد نضطر إلى الانتظار حتى يتم تنزيل المكتبة قبل استخدامها للمرة الأولى.
الأداءفي الوقت الفعلي على معظم الأجهزة

تستخدم واجهة برمجة تطبيقات التعرف على النص مكتبة غير مجمّعة يجب تنزيلها. يتوفر لك خيار إجراء هذا التنزيل عند تثبيت التطبيق، أو عند إطلاقه لأول مرة، أو من خلال خدمات Google Play UnitInstallClient API. في كثير من الحالات، من المحتمل أن تكون تطبيقات Android أخرى قد نفّذت هذه الخطوة، وفي هذه الحالة تكون واجهة برمجة التطبيقات متاحة على الفور.

قبل البدء

  1. في ملف build.gradle على مستوى المشروع، تأكَّد من تضمين مستودع Google Maven في قسمَي buildscript وallprojects.
  2. أضِف المهام التابعة لمكتبات Android ML Kit إلى ملف الدرجات على مستوى التطبيق التابع للوحدة التنظيمية، والذي يكون عادةً app/build.gradle:
    dependencies {
      // ...
    
      implementation 'com.google.android.gms:play-services-mlkit-text-recognition:18.0.2'
    }
    
  3. اختياري ولكن يُنصح به: يمكنك ضبط تطبيقك لتنزيل نموذج تعلُّم الآلة على الجهاز تلقائيًا بعد تثبيت تطبيقك من "متجر Play". ولإجراء ذلك، أضِف البيان التالي إلى ملف AndroidManifest.xml لتطبيقك:

    <application ...>
      ...
      <meta-data
          android:name="com.google.mlkit.vision.DEPENDENCIES"
          android:value="ocr" />
      <!-- To use multiple models: android:value="ocr,model2,model3" -->
    </application>
    
    في حال عدم تفعيل عمليات تنزيل نموذج وقت التثبيت، سيتم تنزيل النموذج في أول مرة يتم فيها تشغيل أداة الرصد على الجهاز. لن تؤدي الطلبات التي تجريها قبل اكتمال عملية التنزيل إلى تقديم أي نتائج.

1. إنشاء مثيل لـ TextRecognizer

إنشاء مثيل لـ TextRecognizer:

Kotlin

val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)

Java

TextRecognizer recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS);

2- إعداد صورة الإدخال

للتعرّف على النص في صورة، يمكنك إنشاء كائن InputImage من Bitmap أو media.Image أو ByteBuffer أو مصفوفة بايت أو ملف على الجهاز. بعد ذلك، عليك تمرير الكائن InputImage إلى طريقة processImage TextRecognizer's.

يمكنك إنشاء كائن InputImage من مصادر مختلفة، وسيتم توضيح كل منها في ما يلي.

باستخدام media.Image

لإنشاء عنصر InputImage من كائن media.Image، مثلاً عند التقاط صورة من كاميرا الجهاز، مرِّر الكائن media.Image وتدوير الصورة إلى InputImage.fromMediaImage().

إذا كنت تستخدم مكتبة XX، تحسب الفئتان OnImageCapturedListener وImageAnalysis.Analyzer قيمة التدوير نيابةً عنك.

Kotlin

private class YourImageAnalyzer : ImageAnalysis.Analyzer {

    override fun analyze(imageProxy: ImageProxy) {
        val mediaImage = imageProxy.image
        if (mediaImage != null) {
            val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
            // Pass image to an ML Kit Vision API
            // ...
        }
    }
}

Java

private class YourAnalyzer implements ImageAnalysis.Analyzer {

    @Override
    public void analyze(ImageProxy imageProxy) {
        Image mediaImage = imageProxy.getImage();
        if (mediaImage != null) {
          InputImage image =
                InputImage.fromMediaImage(mediaImage, imageProxy.getImageInfo().getRotationDegrees());
          // Pass image to an ML Kit Vision API
          // ...
        }
    }
}

إذا لم تكن تستخدم مكتبة كاميرا تمنحك درجة تدوير الصورة، يمكنك احتسابها من درجة تدوير الجهاز واتجاه جهاز استشعار الكاميرا في الجهاز:

Kotlin

private val ORIENTATIONS = SparseIntArray()

init {
    ORIENTATIONS.append(Surface.ROTATION_0, 0)
    ORIENTATIONS.append(Surface.ROTATION_90, 90)
    ORIENTATIONS.append(Surface.ROTATION_180, 180)
    ORIENTATIONS.append(Surface.ROTATION_270, 270)
}

/**
 * Get the angle by which an image must be rotated given the device's current
 * orientation.
 */
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Throws(CameraAccessException::class)
private fun getRotationCompensation(cameraId: String, activity: Activity, isFrontFacing: Boolean): Int {
    // Get the device's current rotation relative to its "native" orientation.
    // Then, from the ORIENTATIONS table, look up the angle the image must be
    // rotated to compensate for the device's rotation.
    val deviceRotation = activity.windowManager.defaultDisplay.rotation
    var rotationCompensation = ORIENTATIONS.get(deviceRotation)

    // Get the device's sensor orientation.
    val cameraManager = activity.getSystemService(CAMERA_SERVICE) as CameraManager
    val sensorOrientation = cameraManager
            .getCameraCharacteristics(cameraId)
            .get(CameraCharacteristics.SENSOR_ORIENTATION)!!

    if (isFrontFacing) {
        rotationCompensation = (sensorOrientation + rotationCompensation) % 360
    } else { // back-facing
        rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360
    }
    return rotationCompensation
}

Java

private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
static {
    ORIENTATIONS.append(Surface.ROTATION_0, 0);
    ORIENTATIONS.append(Surface.ROTATION_90, 90);
    ORIENTATIONS.append(Surface.ROTATION_180, 180);
    ORIENTATIONS.append(Surface.ROTATION_270, 270);
}

/**
 * Get the angle by which an image must be rotated given the device's current
 * orientation.
 */
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private int getRotationCompensation(String cameraId, Activity activity, boolean isFrontFacing)
        throws CameraAccessException {
    // Get the device's current rotation relative to its "native" orientation.
    // Then, from the ORIENTATIONS table, look up the angle the image must be
    // rotated to compensate for the device's rotation.
    int deviceRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
    int rotationCompensation = ORIENTATIONS.get(deviceRotation);

    // Get the device's sensor orientation.
    CameraManager cameraManager = (CameraManager) activity.getSystemService(CAMERA_SERVICE);
    int sensorOrientation = cameraManager
            .getCameraCharacteristics(cameraId)
            .get(CameraCharacteristics.SENSOR_ORIENTATION);

    if (isFrontFacing) {
        rotationCompensation = (sensorOrientation + rotationCompensation) % 360;
    } else { // back-facing
        rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360;
    }
    return rotationCompensation;
}

بعد ذلك، اضبط العنصر media.Image وقيمة درجة التدوير على InputImage.fromMediaImage():

Kotlin

val image = InputImage.fromMediaImage(mediaImage, rotation)

Java

InputImage image = InputImage.fromMediaImage(mediaImage, rotation);

استخدام معرف موارد منتظم (URI) للملف

لإنشاء عنصر InputImage من معرّف موارد منتظم (URI)، يجب تمرير سياق التطبيق ومعرّف الموارد المنتظم (URI) إلى InputImage.fromFilePath(). ويكون هذا الإجراء مفيدًا عندما تستخدم نيّة ACTION_GET_CONTENT لحثّ المستخدم على اختيار صورة من تطبيق معرض الصور.

Kotlin

val image: InputImage
try {
    image = InputImage.fromFilePath(context, uri)
} catch (e: IOException) {
    e.printStackTrace()
}

Java

InputImage image;
try {
    image = InputImage.fromFilePath(context, uri);
} catch (IOException e) {
    e.printStackTrace();
}

باستخدام ByteBuffer أو ByteArray

لإنشاء عنصر InputImage من ByteBuffer أو ByteArray، يجب أولاً حساب درجة عرض الصور بالتناوب كما هو موضّح سابقًا في إدخال media.Image. بعد ذلك، يمكنك إنشاء العنصر InputImage باستخدام المخزن المؤقت أو المصفوفة، مع طول الصورة وعرضها وتنسيق ترميز الألوان ودرجة الدوران:

Kotlin

val image = InputImage.fromByteBuffer(
        byteBuffer,
        /* image width */ 480,
        /* image height */ 360,
        rotationDegrees,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
)
// Or:
val image = InputImage.fromByteArray(
        byteArray,
        /* image width */ 480,
        /* image height */ 360,
        rotationDegrees,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
)

Java

InputImage image = InputImage.fromByteBuffer(byteBuffer,
        /* image width */ 480,
        /* image height */ 360,
        rotationDegrees,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
);
// Or:
InputImage image = InputImage.fromByteArray(
        byteArray,
        /* image width */480,
        /* image height */360,
        rotation,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
);

باستخدام Bitmap

لإنشاء كائن InputImage من كائن Bitmap، عليك اتخاذ الإجراء التالي:

Kotlin

val image = InputImage.fromBitmap(bitmap, 0)

Java

InputImage image = InputImage.fromBitmap(bitmap, rotationDegree);

ويتم تمثيل الصورة من خلال عنصر Bitmap جنبًا إلى جنب مع درجات التدوير.

3- معالجة الصورة

تمرير الصورة إلى الطريقة process:

Kotlin

val result = recognizer.process(image)
        .addOnSuccessListener { visionText ->
            // Task completed successfully
            // ...
        }
        .addOnFailureListener { e ->
            // Task failed with an exception
            // ...
        }

Java

Task<Text> result =
        recognizer.process(image)
                .addOnSuccessListener(new OnSuccessListener<Text>() {
                    @Override
                    public void onSuccess(Text visionText) {
                        // Task completed successfully
                        // ...
                    }
                })
                .addOnFailureListener(
                        new OnFailureListener() {
                            @Override
                            public void onFailure(@NonNull Exception e) {
                                // Task failed with an exception
                                // ...
                            }
                        });

4. استخراج النص من مجموعات النصوص التي يتم التعرّف عليها

في حال نجحت عملية التعرّف على النص، يتم تمرير عنصر Text إلى المستمع الناجح. يحتوي الكائن Text على النص الكامل الذي يتم التعرف عليه في الصورة بالإضافة إلى صفر أو أكثر من كائنات TextBlock.

يمثّل كل TextBlock كتلة مستطيلة من النص، وتحتوي على صفر أو أكثر من كائنات Line. يمثّل كل كائن Line سطرًا نصيًا يحتوي على عناصر Element أو أكثر. يمثل كل كائن Element كلمة أو كيانًا يشبه الكلمة، والذي لا يتضمن أي عناصر Symbol أو أكثر. يمثل كل كائن Symbol حرفًا أو رقمًا أو كيانًا يشبه الكلمة.

بالنسبة إلى كل كائن في TextBlock وLine وElement وSymbol، يمكنك التعرّف على النص في المنطقة والإحداثيات المقيّدة للمنطقة وغيرها من السمات الأخرى، مثل معلومات التدوير ونتيجة الثقة وغير ذلك.

مثلاً:

Kotlin

val resultText = result.text
for (block in result.textBlocks) {
    val blockText = block.text
    val blockCornerPoints = block.cornerPoints
    val blockFrame = block.boundingBox
    for (line in block.lines) {
        val lineText = line.text
        val lineCornerPoints = line.cornerPoints
        val lineFrame = line.boundingBox
        for (element in line.elements) {
            val elementText = element.text
            val elementCornerPoints = element.cornerPoints
            val elementFrame = element.boundingBox
        }
    }
}

Java

String resultText = result.getText();
for (Text.TextBlock block : result.getTextBlocks()) {
    String blockText = block.getText();
    Point[] blockCornerPoints = block.getCornerPoints();
    Rect blockFrame = block.getBoundingBox();
    for (Text.Line line : block.getLines()) {
        String lineText = line.getText();
        Point[] lineCornerPoints = line.getCornerPoints();
        Rect lineFrame = line.getBoundingBox();
        for (Text.Element element : line.getElements()) {
            String elementText = element.getText();
            Point[] elementCornerPoints = element.getCornerPoints();
            Rect elementFrame = element.getBoundingBox();
            for (Text.Symbol symbol : element.getSymbols()) {
                String symbolText = symbol.getText();
                Point[] symbolCornerPoints = symbol.getCornerPoints();
                Rect symbolFrame = symbol.getBoundingBox();
            }
        }
    }
}

إرشادات الصور المدخلة

  • لكي تتعرّف مجموعة أدوات تعلّم الآلة على النص بدقّة، يجب أن تحتوي صور الإدخال على نص يمثل بيانات بكسل كافية. من المفترض أن يكون حجم كل حرف 16x16 بكسل على الأقل. وبوجه عام، لا تتوفّر دقة دقة أعلى من الأحرف 24x24 بكسل.

    على سبيل المثال، قد تعمل صورة مقاس 640x480 بشكلٍ جيد لمسح بطاقة نشاط تجاري تشغل العرض الكامل للصورة. لمسح مستند ضوئيًا على ورقة بحجم حرف، قد يُطلب منك صورة بحجم 720×1280 بكسل.

  • يمكن أن يؤثر ضعف تركيز الصورة في دقة التعرف على النص. وإذا لم تكن تحصل على نتائج مقبولة، جرِّب أن تطلب من المستخدم التقاط الصورة.

  • إذا أردت التعرّف على النص في تطبيق في الوقت الفعلي، عليك مراعاة السمات العامة للصور المدخلة. ويمكن معالجة الصور الأصغر حجمًا بشكل أسرع. لتقليل وقت الاستجابة، تأكَّد من أنّ النص يشغل أكبر قدر ممكن من الصورة، والتقط الصور بدقة أقل (مع مراعاة متطلبات الدقة المذكورة أعلاه). ولمزيد من المعلومات، يُرجى الاطّلاع على نصائح لتحسين الأداء.

نصائح لتحسين الأداء

  • إذا كنت تستخدم Camera أو واجهة برمجة التطبيقات camera2، يمكنك تقييد الاتصالات إلى أداة الرصد. وفي حال توفّر إطار فيديو جديد أثناء تشغيل أداة الرصد، أفلِت الإطار. يمكنك الاطّلاع على السمة VisionProcessorBase في نموذج البدء السريع للحصول على مثال.
  • إذا كنت تستخدم واجهة برمجة تطبيقات CameraX، تأكد من ضبط استراتيجية الضغط على القيمة التلقائية ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST. وهذا يضمن تسليم صورة واحدة فقط للتحليل في كل مرة. وإذا تم إنشاء المزيد من الصور عندما تكون أداة التحليل مشغولة، سيتم إدراجها تلقائيًا ولن يتم وضعها في قائمة الانتظار للتسليم. بعد أن يتم إغلاق الصورة التي يتم تحليلها من خلال استدعاء ImageProxy.close()، سيتم تسليم الصورة التالية التالية.
  • إذا كنت تستخدم ناتج أداة الكشف لتراكب الرسومات على الصورة المُدخلة، عليك أولاً الحصول على النتيجة من حزمة تعلّم الآلة، ثم عرض الصورة والتراكب على خطوة واحدة. ويتم العرض على مساحة العرض مرة واحدة فقط لكل إطار إدخال. يمكنك الاطّلاع على السمتَين CameraSourcePreview و GraphicOverlay في نموذج التطبيق السريع للبدء كمثال.
  • إذا كنت تستخدم واجهة برمجة التطبيقات للكاميرا 2، التقِط صورًا بتنسيق ImageFormat.YUV_420_888. إذا كنت تستخدم واجهة برمجة تطبيقات الكاميرا القديمة، التقِط صورًا بتنسيق ImageFormat.NV21.
  • ننصحك بالتقاط الصور بدقة أقل. ويُرجى أيضًا مراعاة متطلّبات أبعاد الصورة في واجهة برمجة التطبيقات هذه.