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

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

الميزة غير مجمّعة مُجمَّعة
اسم المكتبة com.google.android.gms:play-services-mlkit-text-recognition

com.google.android.gms:play-services-mlkit-text-recognition-chinese

com.google.android.gms:play-services-mlkit-text-recognition-devanagari

com.google.android.gms:play-services-mlkit-text-recognition-japanese

com.google.android.gms:play-services-mlkit-text-recognition-korean

com.google.mlkit:text-recognition

com.google.mlkit:text-recognition-chinese

com.google.mlkit:text-recognition-devanagari

com.google.mlkit:text-recognition-japanese

com.google.mlkit:text-recognition-korean

التنفيذ يتم تنزيل النموذج ديناميكيًا من خلال "خدمات Google Play". يتم ربط النموذج بتطبيقك بشكل ثابت في وقت الإنشاء.
حجم التطبيق زيادة حجم تبلغ 260 كيلوبايت تقريبًا لكل بنية نص برمجي زيادة حجم تبلغ 4 ميغابايت تقريبًا لكل نص برمجي لكل بنية
وقت الإعداد قد تحتاج إلى الانتظار إلى أن يتم تنزيل النموذج قبل استخدامه لأول مرة. يتوفّر الطراز على الفور.
الأداء عرض فوري على معظم الأجهزة لمكتبة النصوص اللاتينية، ويكون أبطأ بالنسبة إلى الأجهزة الأخرى يتم عرض مكتبة الأحرف اللاتينية في الوقت الفعلي على معظم الأجهزة، ولكن ببطء أكبر على الأجهزة الأخرى.

جرّبه الآن

قبل البدء

  1. في ملف build.gradle على مستوى المشروع، احرص على تضمين مستودع Maven من Google في كلّ من القسمَين buildscript وallprojects.
  2. أضِف الملحقات لمكتبات ML Kit لنظام التشغيل Android إلى ملف Gradle على مستوى التطبيق الخاص بالوحدة، والذي يكون عادةً app/build.gradle:

    لتجميع النموذج مع تطبيقك:

    dependencies {
      // To recognize Latin script
      implementation 'com.google.mlkit:text-recognition:16.0.1'
    
      // To recognize Chinese script
      implementation 'com.google.mlkit:text-recognition-chinese:16.0.1'
    
      // To recognize Devanagari script
      implementation 'com.google.mlkit:text-recognition-devanagari:16.0.1'
    
      // To recognize Japanese script
      implementation 'com.google.mlkit:text-recognition-japanese:16.0.1'
    
      // To recognize Korean script
      implementation 'com.google.mlkit:text-recognition-korean:16.0.1'
    }
    

    لاستخدام النموذج في "خدمات Google Play":

    dependencies {
      // To recognize Latin script
      implementation 'com.google.android.gms:play-services-mlkit-text-recognition:19.0.1'
    
      // To recognize Chinese script
      implementation 'com.google.android.gms:play-services-mlkit-text-recognition-chinese:16.0.1'
    
      // To recognize Devanagari script
      implementation 'com.google.android.gms:play-services-mlkit-text-recognition-devanagari:16.0.1'
    
      // To recognize Japanese script
      implementation 'com.google.android.gms:play-services-mlkit-text-recognition-japanese:16.0.1'
    
      // To recognize Korean script
      implementation 'com.google.android.gms:play-services-mlkit-text-recognition-korean:16.0.1'
    }
    
  3. إذا اخترت استخدام النموذج في "خدمات Google Play"، يمكنك ضبط تطبيقك لتنزيل النموذج تلقائيًا على الجهاز بعد تثبيت تطبيقك من "متجر Play". لإجراء ذلك، أضِف ما يلي: بيان في ملف AndroidManifest.xml في تطبيقك:

    <application ...>
          ...
          <meta-data
              android:name="com.google.mlkit.vision.DEPENDENCIES"
              android:value="ocr" >
          <!-- To use multiple models: android:value="ocr,ocr_chinese,ocr_devanagari,ocr_japanese,ocr_korean,..." -->
    </application>
    

    يمكنك أيضًا التحقّق صراحةً من مدى توفّر النموذج وطلب تنزيل التطبيق. من خلال ModuleInstallClient API في "خدمات Google Play". إذا لم تفعِّل تنزيلات النموذج في وقت التثبيت أو طلبت تنزيلًا صريحًا، يتم تنزيل النموذج في المرة الأولى التي تشغّل فيها الماسح الضوئي. لا تؤدي الطلبات التي تقدّمها قبل اكتمال عملية التنزيل إلى أي نتائج.

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

أنشئ مثيلًا من TextRecognizer، مع تمرير الخيارات المرتبطة بالمكتبة التي أعلنت تبعيتك لها أعلاه:

Kotlin

// When using Latin script library
val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)

// When using Chinese script library
val recognizer = TextRecognition.getClient(ChineseTextRecognizerOptions.Builder().build())

// When using Devanagari script library
val recognizer = TextRecognition.getClient(DevanagariTextRecognizerOptions.Builder().build())

// When using Japanese script library
val recognizer = TextRecognition.getClient(JapaneseTextRecognizerOptions.Builder().build())

// When using Korean script library
val recognizer = TextRecognition.getClient(KoreanTextRecognizerOptions.Builder().build())

Java

// When using Latin script library
TextRecognizer recognizer =
  TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS);

// When using Chinese script library
TextRecognizer recognizer =
  TextRecognition.getClient(new ChineseTextRecognizerOptions.Builder().build());

// When using Devanagari script library
TextRecognizer recognizer =
  TextRecognition.getClient(new DevanagariTextRecognizerOptions.Builder().build());

// When using Japanese script library
TextRecognizer recognizer =
  TextRecognition.getClient(new JapaneseTextRecognizerOptions.Builder().build());

// When using Korean script library
TextRecognizer recognizer =
  TextRecognition.getClient(new KoreanTextRecognizerOptions.Builder().build());

2. تجهيز صورة الإدخال

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

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

استخدام media.Image

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

إذا كنت تستخدِم مكتبة CameraX، تحتسِب فئتَا 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);

استخدام عنوان URL للملف

لإنشاء 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();
            }
        }
    }
}

إرشادات حول إدخال الصور

  • لكي تتمكّن حزمة ML Kit من التعرّف على النص بدقة، يجب أن تحتوي صور الإدخال على نص يمثّله بيانات بكسل كافية. من الناحية المثالية، يجب ألا يقلّ حجم كل حرف عن 16×16 بكسل. ولا يوجد عمومًا على الدقة المحدد للأحرف الأكبر من 24×24 بكسل.

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

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

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

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

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