برچسب گذاری تصاویر با یک مدل سفارشی در اندروید

شما می‌توانید از کیت یادگیری ماشین برای تشخیص موجودیت‌ها در یک تصویر و برچسب‌گذاری آنها استفاده کنید. این API از طیف گسترده‌ای از مدل‌های طبقه‌بندی تصویر سفارشی پشتیبانی می‌کند. برای راهنمایی در مورد الزامات سازگاری مدل، محل یافتن مدل‌های از پیش آموزش‌دیده و نحوه آموزش مدل‌های خود، به مدل‌های سفارشی با کیت یادگیری ماشین مراجعه کنید.

دو راه برای ادغام برچسب‌گذاری تصویر با مدل‌های سفارشی وجود دارد: با ادغام خط لوله به عنوان بخشی از برنامه شما، یا با استفاده از یک خط لوله بدون دسته که به سرویس‌های Google Play وابسته است. اگر خط لوله بدون دسته را انتخاب کنید، برنامه شما کوچکتر خواهد بود. برای جزئیات بیشتر به جدول زیر مراجعه کنید.

بسته‌بندی شده بدون دسته بندی
نام کتابخانه com.google.mlkit:image-labeling-custom com.google.android.gms:play-services-mlkit-image-labeling-custom

پیاده‌سازی
پایپ‌لاین در زمان ساخت به صورت ایستا به برنامه شما متصل است. Pipeline به صورت پویا با استفاده از سرویس‌های Google Play دانلود می‌شود.
اندازه برنامه حدود ۳.۸ مگابایت افزایش حجم. حدود ۲۰۰ کیلوبایت افزایش حجم.
زمان اولیه سازی خط لوله فوراً در دسترس است. ممکن است لازم باشد قبل از اولین استفاده، منتظر دانلود شدن خط لوله باشید.
مرحله چرخه عمر API دسترسی عمومی (GA) بتا

دو راه برای ادغام یک مدل سفارشی وجود دارد: مدل را با قرار دادن آن در پوشه asset برنامه خود، به صورت بسته‌ای (bundle) درآورید، یا آن را به صورت پویا از Firebase دانلود کنید. جدول زیر این دو گزینه را با هم مقایسه می‌کند.

مدل بسته‌ای مدل میزبانی شده
این مدل بخشی از APK برنامه شماست که باعث افزایش حجم آن می‌شود. این مدل بخشی از APK شما نیست. با آپلود در فضای ابری میزبانی می‌شود. توصیه می‌کنیم از فضای ابری برای Firebase استفاده کنید.
این مدل بلافاصله در دسترس است، حتی زمانی که دستگاه اندروید آفلاین باشد برنامه شما باید شامل کدی باشد که مدل را بر اساس تقاضا دانلود کند
نیازی به پروژه Firebase نیست به یک پروژه Firebase نیاز دارد (در صورت استفاده از Cloud Storage برای Firebase).
برای به‌روزرسانی مدل، باید برنامه خود را دوباره منتشر کنید به‌روزرسانی‌های مدل را بدون انتشار مجدد برنامه خود، ارسال کنید
تست A/B داخلی ندارد تست A/B با پیکربندی از راه دور Firebase

امتحانش کن.

قبل از اینکه شروع کنی

  1. در فایل build.gradle.kts در سطح پروژه، مطمئن شوید که مخزن Maven گوگل را هم در بخش‌های buildscript و هم allprojects خود وارد کرده‌اید.

  2. وابستگی‌های کتابخانه‌های اندروید ML Kit را به فایل gradle سطح برنامه ماژول خود که معمولاً app/build.gradle.kts است، اضافه کنید. بر اساس نیاز خود، یکی از وابستگی‌های زیر را انتخاب کنید:

    برای بسته‌بندی pipeline با برنامه‌تان:

    dependencies {
      // ...
      // Use this dependency to bundle the pipeline with your app
      implementation("com.google.mlkit:image-labeling-custom:17.0.3")
    }
    

    برای استفاده از pipeline در سرویس‌های گوگل پلی:

    dependencies {
      // ...
      // Use this dependency to use the dynamically downloaded pipeline in Google Play services
      implementation("com.google.android.gms:play-services-mlkit-image-labeling-custom:16.0.0-beta5")
    }
    
  3. اگر تصمیم دارید از pipeline در سرویس‌های Google Play استفاده کنید ، می‌توانید برنامه خود را طوری پیکربندی کنید که پس از نصب برنامه از Play Store، pipeline را به طور خودکار روی دستگاه دانلود کند. برای انجام این کار، اعلان زیر را به فایل AndroidManifest.xml برنامه خود اضافه کنید:

    <application ...>
        ...
        <meta-data
            android:name="com.google.mlkit.vision.DEPENDENCIES"
            android:value="custom_ica" />
        <!-- To use multiple downloads: android:value="custom_ica,download2,download3" -->
    </application>
    

    همچنین می‌توانید به صراحت در دسترس بودن خط لوله را بررسی کرده و از طریق API ModuleInstallClient سرویس‌های Google Play درخواست دانلود دهید.

    اگر دانلودهای خط لوله‌ای زمان نصب را فعال نکنید یا درخواست دانلود صریح ندهید، خط لوله در اولین باری که برچسب‌گذار را اجرا می‌کنید دانلود می‌شود. درخواست‌هایی که قبل از اتمام دانلود ارسال می‌کنید، هیچ نتیجه‌ای ندارند.

  4. اگر می‌خواهید مدلی را با استفاده از فضای ذخیره‌سازی ابری برای فایربیس دانلود کنید ، اگر قبلاً فایربیس را به پروژه اندروید خود اضافه نکرده‌اید، حتماً آن را اضافه کنید . این کار هنگام بسته‌بندی مدل لازم نیست.

۱. مدل را بارگذاری کنید

می‌توانید مدل را از یک منبع محلی یا یک منبع میزبانی شده از راه دور بارگذاری کنید.

پیکربندی یک منبع مدل محلی

برای اتصال مدل به برنامه خود:

  1. فایل مدل (که معمولاً به .tflite یا .lite ختم می‌شود) را در پوشه assets/ برنامه خود کپی کنید. (ممکن است لازم باشد ابتدا با کلیک راست روی app/ پوشه و سپس کلیک روی New > Folder > Assets Folder ، پوشه را ایجاد کنید.)

  2. ایجاد شیء LocalModel ، با مشخص کردن مسیر فایل مدل:

    کاتلین

    val localModel = LocalModel.Builder()
            .setAssetFilePath("model.tflite")
            // or .setAbsoluteFilePath(absolute path to model file)
            // or .setUri(URI to model file)
            .build()

    جاوا

    LocalModel localModel =
        new LocalModel.Builder()
            .setAssetFilePath("model.tflite")
            // or .setAbsoluteFilePath(absolute path to model file)
            // or .setUri(URI to model file)
            .build();

پیکربندی یک منبع مدل میزبانی شده از راه دور

برای استفاده از مدل میزبانی‌شده از راه دور، باید فایل مدل را با استفاده از منطق برنامه خود در حافظه محلی دستگاه دانلود کنید و سپس آن را به عنوان یک مدل محلی بارگذاری کنید. توصیه می‌کنیم برای میزبانی یک مدل از فضای ذخیره‌سازی ابری برای فایربیس استفاده کنید. برای جزئیات پیاده‌سازی، به راهنمای مهاجرت از فایربیس ML به فضای ذخیره‌سازی ابری مراجعه کنید.

پیکربندی برچسب‌گذار تصویر

پس از پیکربندی منابع مدل خود، یک شیء ImageLabeler از یکی از آنها ایجاد کنید.

گزینه‌های زیر موجود است:

گزینه‌ها
confidenceThreshold

حداقل امتیاز اطمینان برچسب‌های شناسایی‌شده. در صورت عدم تنظیم، از هر آستانه طبقه‌بندی‌کننده‌ای که توسط فراداده مدل مشخص شده باشد، استفاده خواهد شد. اگر مدل حاوی هیچ فراداده‌ای نباشد یا فراداده آستانه طبقه‌بندی‌کننده‌ای را مشخص نکند، از آستانه پیش‌فرض ۰.۰ استفاده خواهد شد.

maxResultCount

حداکثر تعداد برچسب‌هایی که باید برگردانده شوند. اگر تنظیم نشود، مقدار پیش‌فرض ۱۰ استفاده خواهد شد.

اگر فقط یک مدلِ بسته‌بندی‌شده‌ی محلی دارید، کافیست یک برچسب‌گذار از شیء LocalModel خود ایجاد کنید:

کاتلین

val customImageLabelerOptions = CustomImageLabelerOptions.Builder(localModel)
    .setConfidenceThreshold(0.5f)
    .setMaxResultCount(5)
    .build()
val labeler = ImageLabeling.getClient(customImageLabelerOptions)

جاوا

CustomImageLabelerOptions customImageLabelerOptions =
        new CustomImageLabelerOptions.Builder(localModel)
            .setConfidenceThreshold(0.5f)
            .setMaxResultCount(5)
            .build();
ImageLabeler labeler = ImageLabeling.getClient(customImageLabelerOptions);

اگر مدلی دارید که از راه دور میزبانی می‌شود، باید قبل از اجرای آن، بررسی کنید که دانلود شده باشد.

اگرچه شما فقط باید قبل از اجرای برچسب‌گذار این موضوع را تأیید کنید، اما اگر هم یک مدل میزبانی‌شده از راه دور و هم یک مدل بسته‌بندی‌شده محلی دارید، انجام این بررسی هنگام نمونه‌سازی برچسب‌گذار تصویر منطقی است: اگر مدل از راه دور دانلود شده است، یک برچسب‌گذار از آن و در غیر این صورت از مدل محلی ایجاد کنید.

کاتلین

val modelFile = File(context.cacheDir, "my_downloaded_model.tflite")
val model = if (modelFile.exists()) {
    // Use the downloaded model if available
    LocalModel.Builder().setAbsoluteFilePath(modelFile.absolutePath).build()
} else {
    // Fall back to the bundled model
    LocalModel.Builder().setAssetFilePath("model.tflite").build()
}
val options = CustomImageLabelerOptions.Builder(model)
    .setConfidenceThreshold(0.5f)
    .setMaxResultCount(5)
    .build()
val labeler = ImageLabeling.getClient(options)

جاوا

File modelFile = new File(context.getCacheDir(), "my_downloaded_model.tflite");
LocalModel model;
if (modelFile.exists()) {
    // Use the downloaded model if available
    model = new LocalModel.Builder().setAbsoluteFilePath(modelFile.getAbsolutePath()).build();
} else {
    // Fall back to the bundled model
    model = new LocalModel.Builder().setAssetFilePath("model.tflite").build();
}
CustomImageLabelerOptions options = new CustomImageLabelerOptions.Builder(model)
    .setConfidenceThreshold(0.5f)
    .setMaxResultCount(5)
    .build();
ImageLabeler labeler = ImageLabeling.getClient(options);

اگر فقط یک مدل میزبانی‌شده از راه دور دارید، باید عملکردهای مرتبط با مدل را غیرفعال کنید - مثلاً بخشی از رابط کاربری خود را خاکستری کنید یا پنهان کنید - تا زمانی که تأیید کنید مدل دانلود شده است.

کاتلین

val localFile = File(context.cacheDir, "my_remote_model.tflite")
if (localFile.exists()) {
    initializeLabeler(localFile)
} else {
    showLoadingUI()
    val storage = Firebase.storage
    val modelRef = storage.getReferenceFromUrl("gs://YOUR_BUCKET/path/to/model.tflite")
    modelRef.getFile(localFile)
        .addOnSuccessListener {
            hideLoadingUI()
            initializeLabeler(localFile)
        }
        .addOnFailureListener {
            showErrorUI()
        }
}

private fun initializeLabeler(modelFile: File) {
    val localModel = LocalModel.Builder().setAbsoluteFilePath(modelFile.absolutePath).build()
    val options = CustomImageLabelerOptions.Builder(localModel).build()
    val labeler = ImageLabeling.getClient(options)
    enableMLFeatures(labeler)
}

جاوا

File localFile = new File(context.getCacheDir(), "my_remote_model.tflite");
if (localFile.exists()) {
    initializeLabeler(localFile);
} else {
    showLoadingUI();
    FirebaseStorage storage = FirebaseStorage.getInstance();
    StorageReference modelRef = storage.getReferenceFromUrl("gs://YOUR_BUCKET/path/to/model.tflite");
    modelRef.getFile(localFile)
        .addOnSuccessListener(new OnSuccessListener<FileDownloadTask.TaskSnapshot>() {
            @Override
            public void onSuccess(FileDownloadTask.TaskSnapshot taskSnapshot) {
                hideLoadingUI();
                initializeLabeler(localFile);
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception exception) {
                showErrorUI();
            }
        });
}

private void initializeLabeler(File modelFile) {
    LocalModel localModel = new LocalModel.Builder().setAbsoluteFilePath(modelFile.getAbsolutePath()).build();
    CustomImageLabelerOptions options = new CustomImageLabelerOptions.Builder(localModel).build();
    ImageLabeler labeler = ImageLabeling.getClient(options);
    enableMLFeatures(labeler);
}

۲. تصویر ورودی را آماده کنید

سپس، برای هر تصویری که می‌خواهید برچسب‌گذاری کنید، یک شیء InputImage از تصویر خود ایجاد کنید. برچسب‌گذار تصویر زمانی که از Bitmap یا اگر از camera2 API استفاده می‌کنید، از YUV_420_888 media.Image استفاده می‌کنید، سریع‌تر اجرا می‌شود، که در صورت امکان توصیه می‌شوند.

شما می‌توانید یک شیء InputImage را از منابع مختلفی ایجاد کنید که هر کدام در زیر توضیح داده شده‌اند.

استفاده از یک media.Image

برای ایجاد یک شیء InputImage از یک شیء media.Image ، مانند زمانی که از دوربین یک دستگاه تصویر می‌گیرید، شیء media.Image و چرخش تصویر را به InputImage.fromMediaImage() ارسال کنید.

اگر از کتابخانه CameraX استفاده می‌کنید، کلاس‌های OnImageCapturedListener و ImageAnalysis.Analyzer مقدار چرخش را برای شما محاسبه می‌کنند.

کاتلین

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
            // ...
        }
    }
}

جاوا

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
          // ...
        }
    }
}

اگر از کتابخانه دوربینی که درجه چرخش تصویر را به شما بدهد استفاده نمی‌کنید، می‌توانید آن را از درجه چرخش دستگاه و جهت سنسور دوربین در دستگاه محاسبه کنید:

کاتلین

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
}

جاوا

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() ارسال کنید:

کاتلین

val image = InputImage.fromMediaImage(mediaImage, rotation)

Java

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

استفاده از یک URI فایل

برای ایجاد یک شیء InputImage از یک URI فایل، متن برنامه و URI فایل را به InputImage.fromFilePath() ارسال کنید. این زمانی مفید است که از یک ACTION_GET_CONTENT برای وادار کردن کاربر به انتخاب یک تصویر از برنامه گالری خود استفاده می‌کنید.

کاتلین

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 را با بافر یا آرایه، به همراه ارتفاع، عرض، فرمت کدگذاری رنگ و درجه چرخش تصویر ایجاد کنید:

کاتلین

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
)

جاوا

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 ، تعریف زیر را انجام دهید:

کاتلین

val image = InputImage.fromBitmap(bitmap, 0)

Java

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

تصویر توسط یک شیء Bitmap به همراه درجه چرخش نمایش داده می‌شود.

۳. برچسب‌گذار تصویر را اجرا کنید

برای برچسب‌گذاری اشیاء در یک تصویر، شیء image را به متد process() در ImageLabeler ارسال کنید.

کاتلین

labeler.process(image)
        .addOnSuccessListener { labels ->
            // Task completed successfully
            // ...
        }
        .addOnFailureListener { e ->
            // Task failed with an exception
            // ...
        }

جاوا

labeler.process(image)
        .addOnSuccessListener(new OnSuccessListener<List<ImageLabel>>() {
            @Override
            public void onSuccess(List<ImageLabel> labels) {
                // Task completed successfully
                // ...
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // Task failed with an exception
                // ...
            }
        });

۴. دریافت اطلاعات در مورد موجودیت‌های برچسب‌گذاری‌شده

اگر عملیات برچسب‌گذاری تصویر با موفقیت انجام شود، فهرستی از اشیاء ImageLabel به شنونده‌ی موفقیت ارسال می‌شود. هر شیء ImageLabel نشان‌دهنده‌ی چیزی است که در تصویر برچسب‌گذاری شده است. می‌توانید توضیحات متنی هر برچسب (در صورت وجود در فراداده‌ی فایل مدل LiteRT)، امتیاز اطمینان و اندیس آن را دریافت کنید. برای مثال:

کاتلین

for (label in labels) {
    val text = label.text
    val confidence = label.confidence
    val index = label.index
}

جاوا

for (ImageLabel label : labels) {
    String text = label.getText();
    float confidence = label.getConfidence();
    int index = label.getIndex();
}

نکاتی برای بهبود عملکرد در زمان واقعی

اگر می‌خواهید تصاویر را در یک برنامه‌ی بلادرنگ برچسب‌گذاری کنید، برای دستیابی به بهترین نرخ فریم، این دستورالعمل‌ها را دنبال کنید:

  • اگر از API Camera یا camera2 استفاده می‌کنید، فراخوانی‌های throttle به برچسب‌گذار تصویر را متوقف کنید. اگر در حین اجرای برچسب‌گذار تصویر، یک فریم ویدیویی جدید در دسترس قرار گرفت، فریم را رها کنید. برای مثال، به کلاس VisionProcessorBase در برنامه نمونه شروع سریع مراجعه کنید.
  • اگر از API CameraX استفاده می‌کنید، مطمئن شوید که استراتژی فشار معکوس (backpressure strategy) روی مقدار پیش‌فرض خود یعنی ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST تنظیم شده است. این تضمین می‌کند که فقط یک تصویر در هر زمان برای تجزیه و تحلیل تحویل داده می‌شود. اگر تصاویر بیشتری هنگام مشغول بودن تحلیلگر تولید شوند، به طور خودکار حذف می‌شوند و برای تحویل در صف قرار نمی‌گیرند. پس از بسته شدن تصویر در حال تجزیه و تحلیل با فراخوانی ImageProxy.close()، آخرین تصویر بعدی تحویل داده می‌شود.
  • اگر از خروجی برچسب‌گذار تصویر برای همپوشانی گرافیک‌ها روی تصویر ورودی استفاده می‌کنید، ابتدا نتیجه را از ML Kit دریافت کنید، سپس تصویر و همپوشانی را در یک مرحله رندر کنید. این کار فقط یک بار برای هر فریم ورودی روی سطح نمایشگر رندر می‌شود. برای مثال به کلاس‌های CameraSourcePreview و GraphicOverlay در برنامه نمونه شروع سریع مراجعه کنید.
  • اگر از API دوربین ۲ استفاده می‌کنید، تصاویر را با فرمت ImageFormat.YUV_420_888 ضبط کنید. اگر از API دوربین قدیمی‌تر استفاده می‌کنید، تصاویر را با فرمت ImageFormat.NV21 ضبط کنید.