אפשרויות להגדרות אישיות

אתם מגדירים כל תרחיש לדוגמה של CameraX כדי לשלוט בהיבטים שונים של הפעולות בתרחיש לדוגמה.

לדוגמה, בתרחיש לדוגמה של צילום תמונות, אתם יכולים להגדיר יחס גובה-רוחב ומצב פלאש. הקוד הבא מציג דוגמה אחת:

Kotlin

val imageCapture = ImageCapture.Builder()
    .setFlashMode(...)
    .setTargetAspectRatio(...)
    .build()

Java

ImageCapture imageCapture =
    new ImageCapture.Builder()
        .setFlashMode(...)
        .setTargetAspectRatio(...)
        .build();

בנוסף לאפשרויות ההגדרה, בחלק מתרחישי השימוש נחשפים ממשקי API שמאפשרים לשנות את ההגדרות באופן דינמי אחרי שתרחיש השימוש נוצר. למידע על הגדרות ספציפיות לתרחישי שימוש שונים, ראו הטמעת תצוגה מקדימה, ניתוח תמונות וצילום תמונות.

CameraXConfig

כדי לפשט את התהליך, ל-CameraX יש הגדרות ברירת מחדל כמו מנהלי טיפול ומבצעים פנימיים שמתאימים לרוב תרחישי השימוש. עם זאת, אם לאפליקציה יש דרישות מיוחדות או שאתם מעדיפים להתאים אישית את ההגדרות האלה, CameraXConfig הוא הממשק למטרה הזו.

באמצעות CameraXConfig, אפליקציה יכולה לבצע את הפעולות הבאות:

מודל השימוש

כך משתמשים ב-CameraXConfig:

  1. יוצרים אובייקט CameraXConfig עם ההגדרות האישיות שלכם.
  2. מטמיעים את הממשק CameraXConfig.Provider ב-Application ומחזירים את האובייקט CameraXConfig ב-getCameraXConfig().
  3. מוסיפים את המחלקה Application לקובץ AndroidManifest.xml, כמו שמתואר כאן.

לדוגמה, דוגמת הקוד הבאה מגבילה את הרישום ביומן של CameraX להודעות שגיאה בלבד:

Kotlin

class CameraApplication : Application(), CameraXConfig.Provider {
   override fun getCameraXConfig(): CameraXConfig {
       return CameraXConfig.Builder.fromConfig(Camera2Config.defaultConfig())
           .setMinimumLoggingLevel(Log.ERROR).build()
   }
}

אם האפליקציה צריכה לדעת מהי ההגדרה של CameraX אחרי שהיא נקבעה, צריך לשמור עותק מקומי של האובייקט CameraXConfig.

הגבלת המצלמה

בפעם הראשונה שמפעילים את ProcessCameraProvider.getInstance(), ‏CameraX סופר את המצלמות שזמינות במכשיר ושולח שאילתות לגבי המאפיינים שלהן. מכיוון ש-CameraX צריך לתקשר עם רכיבי חומרה, התהליך הזה יכול להימשך זמן לא מבוטל לכל מצלמה, במיוחד במכשירים פשוטים. אם האפליקציה משתמשת רק במצלמות ספציפיות במכשיר, כמו המצלמה הקדמית שמוגדרת כברירת מחדל, אפשר להגדיר ל-CameraX להתעלם ממצלמות אחרות. כך אפשר לצמצם את זמן האחזור בהפעלה של המצלמות שהאפליקציה משתמשת בהן.

אם הערך של CameraSelector שמועבר אל CameraXConfig.Builder.setAvailableCamerasLimiter() מסנן מצלמה, CameraX מתנהג כאילו המצלמה הזו לא קיימת. לדוגמה, הקוד הבא מגביל את האפליקציה כך שתשתמש רק במצלמה האחורית שמוגדרת כברירת המחדל במכשיר:

Kotlin

class MainApplication : Application(), CameraXConfig.Provider {
   override fun getCameraXConfig(): CameraXConfig {
       return CameraXConfig.Builder.fromConfig(Camera2Config.defaultConfig())
              .setAvailableCamerasLimiter(CameraSelector.DEFAULT_BACK_CAMERA)
              .build()
   }
}

שרשורים

הרבה מממשקי ה-API של הפלטפורמה שעליהם מבוסס CameraX דורשים חסימה של תקשורת בין תהליכים (IPC) עם חומרה, שלפעמים יכולה לקחת מאות אלפיות השנייה כדי להגיב. לכן, CameraX מפעיל את ממשקי ה-API האלה רק מ-threads ברקע, כדי שה-thread הראשי לא ייחסם וממשק המשתמש יישאר חלק. ‫CameraX מנהל את ה-threads האלה ברקע באופן פנימי, כך שההתנהגות הזו נראית שקופה. עם זאת, באפליקציות מסוימות נדרשת שליטה קפדנית בשרשורים. CameraXConfig מאפשר לאפליקציה להגדיר את ה-threads ברקע שמשמשים את CameraXConfig.Builder.setCameraExecutor() ו-CameraXConfig.Builder.setSchedulerHandler().

Camera Executor

ה-executor של המצלמה משמש לכל הקריאות הפנימיות ל-API של פלטפורמת המצלמה, וגם לקריאות חוזרות (callbacks) מממשקי ה-API האלה. ‫CameraX מקצה ומנהלת Executor פנימי כדי לבצע את המשימות האלה. עם זאת, אם האפליקציה שלכם דורשת שליטה מחמירה יותר בשרשורים, השתמשו ב-CameraXConfig.Builder.setCameraExecutor().

רכיב handler של מתזמן

ה-scheduler handler משמש לתזמון משימות פנימיות במרווחי זמן קבועים, כמו ניסיון חוזר לפתוח את המצלמה כשהיא לא זמינה. ה-handler הזה לא מבצע משימות, אלא רק שולח אותן ל-executor של המצלמה. לפעמים משתמשים בו גם בפלטפורמות API מדור קודם שדורשות Handler לקריאות חוזרות (callbacks). במקרים כאלה, הקריאות החוזרות עדיין נשלחות רק ישירות למבצע של המצלמה. ‫CameraX מקצה ומנהל HandlerThread פנימי כדי לבצע את המשימות האלה, אבל אפשר לשנות אותו באמצעות CameraXConfig.Builder.setSchedulerHandler().

רישום ביומן

רישום ביומן של CameraX מאפשר לאפליקציות לסנן הודעות logcat, כי כדאי להימנע מהודעות מפורטות בקוד הייצור. ‫CameraX תומך בארבע רמות רישום ביומן, מהמפורטת ביותר ועד החמורה ביותר:

  • ‫Log.DEBUG (ברירת מחדל)
  • Log.INFO
  • Log.WARN
  • Log.ERROR

במסמכי התיעוד של Android Log מפורטים תיאורים של רמות היומן האלה. כדי להגדיר את רמת הרישום ביומן שמתאימה לאפליקציה, משתמשים ב-CameraXConfig.Builder.setMinimumLoggingLevel(int).

בחירה אוטומטית

‫CameraX מספקת באופן אוטומטי פונקציונליות שספציפית למכשיר שבו האפליקציה פועלת. לדוגמה, אם לא מציינים רזולוציה או אם הרזולוציה שצוינה לא נתמכת, CameraX קובעת באופן אוטומטי את הרזולוציה הטובה ביותר לשימוש. הספרייה מטפלת בכל זה, כך שאתם לא צריכים לכתוב קוד ספציפי למכשיר.

המטרה של CameraX היא להפעיל בהצלחה סשן של המצלמה. כלומר, CameraX מתפשרת על הרזולוציה ויחסי הגובה-רוחב בהתאם ליכולות של המכשיר. הפריצה יכולה לקרות מהסיבות הבאות:

  • המכשיר לא תומך ברזולוציה המבוקשת.
  • יש בעיות תאימות במכשיר, למשל מכשירים מדור קודם שדורשים רזולוציות מסוימות כדי לפעול כמו שצריך.
  • במכשירים מסוימים, פורמטים מסוימים זמינים רק ביחסי גובה-רוחב מסוימים.
  • המכשיר מעדיף את "mod16 הקרוב ביותר" לקידוד JPEG או וידאו. מידע נוסף זמין במאמר SCALER_STREAM_CONFIGURATION_MAP.

למרות ש-CameraX יוצר ומנהל את הסשן, תמיד צריך לבדוק את גודלי התמונות שמוחזרים בפלט של תרחיש השימוש בקוד, ולבצע התאמות בהתאם.

סיבוב

כברירת מחדל, סיבוב המצלמה מוגדר כך שיתאים לסיבוב של תצוגת ברירת המחדל במהלך יצירת תרחיש השימוש. במקרה הזה, כברירת מחדל, CameraX מפיק פלט שמאפשר לאפליקציה להתאים את מה שאתם מצפים לראות בתצוגה המקדימה. כדי לשנות את כיוון הסיבוב לערך מותאם אישית שתומך במכשירים עם כמה מסכים, צריך להעביר את כיוון המסך הנוכחי כשמגדירים אובייקטים של תרחישי שימוש, או באופן דינמי אחרי שהם נוצרו.

האפליקציה יכולה להגדיר את סיבוב היעד באמצעות הגדרות אישיות. לאחר מכן, האפליקציה יכולה לעדכן את הגדרות הסיבוב באמצעות השיטות מממשקי ה-API של תרחישי השימוש (כמו ImageAnalysis.setTargetRotation()), גם כשמחזור החיים נמצא במצב פעולה. אפשר להשתמש בזה כשהאפליקציה נעולה במצב לאורך, כך שלא מתבצעת הגדרה מחדש בזמן סיבוב, אבל תרחיש השימוש של התמונה או הניתוח צריך להיות מודע לסיבוב הנוכחי של המכשיר. לדוגמה, יכול להיות שצריך לזהות את כיוון הסיבוב כדי שהפנים יהיו בכיוון הנכון לצורך זיהוי פנים, או כדי שהתמונות יוגדרו לאורך או לרוחב.

יכול להיות שהנתונים של התמונות שצולמו יישמרו בלי מידע על סיבוב התמונה. נתוני Exif מכילים מידע על סיבוב התמונה, כדי שאפליקציות של גלריות יוכלו להציג את התמונה בכיוון הנכון אחרי השמירה.

כדי להציג נתונים של תצוגה מקדימה בכיוון הנכון, אתם יכולים להשתמש בפלט המטא-נתונים מ-Preview.PreviewOutput() כדי ליצור טרנספורמציות.

דוגמת הקוד הבאה מראה איך להגדיר את הסיבוב באירוע של שינוי כיוון:

Kotlin

override fun onCreate() {
    val imageCapture = ImageCapture.Builder().build()

    val orientationEventListener = object : OrientationEventListener(this as Context) {
        override fun onOrientationChanged(orientation : Int) {
            // Monitors orientation values to determine the target rotation value
            val rotation : Int = when (orientation) {
                in 45..134 -> Surface.ROTATION_270
                in 135..224 -> Surface.ROTATION_180
                in 225..314 -> Surface.ROTATION_90
                else -> Surface.ROTATION_0
            }

            imageCapture.targetRotation = rotation
        }
    }
    orientationEventListener.enable()
}

Java

@Override
public void onCreate() {
    ImageCapture imageCapture = new ImageCapture.Builder().build();

    OrientationEventListener orientationEventListener = new OrientationEventListener((Context)this) {
       @Override
       public void onOrientationChanged(int orientation) {
           int rotation;

           // Monitors orientation values to determine the target rotation value
           if (orientation >= 45 && orientation < 135) {
               rotation = Surface.ROTATION_270;
           } else if (orientation >= 135 && orientation < 225) {
               rotation = Surface.ROTATION_180;
           } else if (orientation >= 225 && orientation < 315) {
               rotation = Surface.ROTATION_90;
           } else {
               rotation = Surface.ROTATION_0;
           }

           imageCapture.setTargetRotation(rotation);
       }
    };

    orientationEventListener.enable();
}

בהתאם לסיבוב שהוגדר, בכל תרחיש שימוש נתוני התמונה מסובבים ישירות או שמועברים מטא-נתונים של סיבוב לצרכנים של נתוני התמונה שלא סובבו.

  • תצוגה מקדימה: פלט המטא-נתונים מסופק כדי שסיבוב רזולוציית היעד יהיה ידוע באמצעות Preview.getTargetRotation().
  • ‫ImageAnalysis: פלט המטא-נתונים מסופק כדי שקואורדינטות מאגר התמונות יהיו ידועות ביחס לקואורדינטות התצוגה.
  • ‫ImageCapture: המטא-נתונים של Exif, המאגר או גם המאגר וגם המטא-נתונים של התמונה משתנים כדי לציין את הגדרת הסיבוב. הערך שמשתנה תלוי בהטמעה של שכבת הפשטת החומרה (HAL).

מלבן חיתוך

כברירת מחדל, מלבן החיתוך הוא מלבן המאגר המלא. אפשר להתאים אישית את המדיניות הזו באמצעות כללי המדיניות ViewPort ו-UseCaseGroup. כשמקבלים את תרחישי השימוש ומגדירים את אזור התצוגה, CameraX מבטיח שכל מלבני החיתוך של תרחישי השימוש בקבוצה יצביעו על אותו אזור בחיישן המצלמה.

בקטע הקוד הבא אפשר לראות איך להשתמש בשתי המחלקות האלה:

Kotlin

val viewPort =  ViewPort.Builder(Rational(width, height), display.rotation).build()
val useCaseGroup = UseCaseGroup.Builder()
    .addUseCase(preview)
    .addUseCase(imageAnalysis)
    .addUseCase(imageCapture)
    .setViewPort(viewPort)
    .build()
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCaseGroup)

Java

ViewPort viewPort = new ViewPort.Builder(
         new Rational(width, height),
         getDisplay().getRotation()).build();
UseCaseGroup useCaseGroup = new UseCaseGroup.Builder()
    .addUseCase(preview)
    .addUseCase(imageAnalysis)
    .addUseCase(imageCapture)
    .setViewPort(viewPort)
    .build();
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCaseGroup);

‫ViewPort מגדיר את המלבן של המאגר שגלוי למשתמשי הקצה. לאחר מכן, CameraX מחשב את מלבן החיתוך הגדול ביותר האפשרי על סמך המאפיינים של אזור התצוגה והתרחישים לדוגמה המצורפים. בדרך כלל, כדי להשיג אפקט WYSIWYG, אפשר להגדיר את אזור התצוגה על סמך תרחיש השימוש בתצוגה המקדימה. דרך פשוטה לקבל את אזור התצוגה היא להשתמש ב-PreviewView.

בקטעי הקוד הבאים מוסבר איך לקבל את האובייקט ViewPort:

Kotlin

val viewport = findViewById<PreviewView>(R.id.preview_view).viewPort

Java

ViewPort viewPort = ((PreviewView)findViewById(R.id.preview_view)).getViewPort();

בדוגמה הקודמת, מה שהאפליקציה מקבלת מ-ImageAnalysis ומ-ImageCapture תואם למה שמשתמש הקצה רואה ב-PreviewView, בהנחה שסוג קנה המידה של PreviewView מוגדר לברירת המחדל, FILL_CENTER. אחרי שמחילים את מלבן החיתוך והסיבוב על מאגר הנתונים הזמני של הפלט, התמונה מכל תרחישי השימוש זהה, אבל יכול להיות שהרזולוציות שונות. למידע נוסף על אופן היישום של פרטי הטרנספורמציה, ראו פלט טרנספורמציה.

בחירת מצלמה

‫CameraX בוחרת באופן אוטומטי את המצלמה שהכי מתאימה לדרישות ולתרחישי השימוש של האפליקציה. אם אתם רוצים להשתמש במכשיר אחר, יש כמה אפשרויות:

בדוגמת הקוד הבאה אפשר לראות איך יוצרים CameraSelector כדי להשפיע על בחירת המכשיר:

Kotlin

fun selectExternalOrBestCamera(provider: ProcessCameraProvider):CameraSelector? {
   val cam2Infos = provider.availableCameraInfos.map {
       Camera2CameraInfo.from(it)
   }.sortedByDescending {
       // HARDWARE_LEVEL is Int type, with the order of:
       // LEGACY < LIMITED < FULL < LEVEL_3 < EXTERNAL
       it.getCameraCharacteristic(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
   }

   return when {
       cam2Infos.isNotEmpty() -> {
           CameraSelector.Builder()
               .addCameraFilter {
                   it.filter { camInfo ->
                       // cam2Infos[0] is either EXTERNAL or best built-in camera
                       val thisCamId = Camera2CameraInfo.from(camInfo).cameraId
                       thisCamId == cam2Infos[0].cameraId
                   }
               }.build()
       }
       else -> null
    }
}

// create a CameraSelector for the USB camera (or highest level internal camera)
val selector = selectExternalOrBestCamera(processCameraProvider)
processCameraProvider.bindToLifecycle(this, selector, preview, analysis)

לבחור כמה מצלמות בו-זמנית

החל מגרסה 1.3 של CameraX, אפשר גם לבחור כמה מצלמות בו-זמנית. לדוגמה, אתם יכולים לקשר למצלמה קדמית ואחורית כדי לצלם תמונות או סרטונים משתי נקודות המבט בו-זמנית.

כשמשתמשים בתכונה "מצלמה כפולה", המכשיר יכול להפעיל שתי מצלמות עם עדשות שפונות לכיוונים שונים בו-זמנית, או להפעיל שתי מצלמות אחוריות בו-זמנית. בלוק הקוד הבא מראה איך להגדיר שתי מצלמות כשקוראים ל-bindToLifecycle, ואיך לקבל את שני האובייקטים של המצלמה מהאובייקט ConcurrentCamera שמוחזר.

Kotlin

// Build ConcurrentCameraConfig
val primary = ConcurrentCamera.SingleCameraConfig(
    primaryCameraSelector,
    useCaseGroup,
    lifecycleOwner
)

val secondary = ConcurrentCamera.SingleCameraConfig(
    secondaryCameraSelector,
    useCaseGroup,
    lifecycleOwner
)

val concurrentCamera = cameraProvider.bindToLifecycle(
    listOf(primary, secondary)
)

val primaryCamera = concurrentCamera.cameras[0]
val secondaryCamera = concurrentCamera.cameras[1]

Java

// Build ConcurrentCameraConfig
SingleCameraConfig primary = new SingleCameraConfig(
    primaryCameraSelector,
    useCaseGroup,
    lifecycleOwner
);

SingleCameraConfig secondary = new SingleCameraConfig(
    primaryCameraSelector,
    useCaseGroup,
    lifecycleOwner
);

ConcurrentCamera concurrentCamera =  
    mCameraProvider.bindToLifecycle(Arrays.asList(primary, secondary));

Camera primaryCamera = concurrentCamera.getCameras().get(0);
Camera secondaryCamera = concurrentCamera.getCameras().get(1);

רזולוציית המצלמה

אתם יכולים לאפשר ל-CameraX להגדיר את הרזולוציה של התמונה על סמך שילוב של יכולות המכשיר, רמת החומרה שנתמכת במכשיר, תרחיש השימוש ויחס הגובה-רוחב שסופק. לחלופין, אפשר להגדיר רזולוציית יעד ספציפית או יחס גובה-רוחב ספציפי בתרחישי שימוש שתומכים בהגדרה הזו.

רזולוציה אוטומטית

‫CameraX יכולה לקבוע באופן אוטומטי את הגדרות הרזולוציה הטובות ביותר על סמך תרחישי השימוש שצוינו ב-cameraProcessProvider.bindToLifecycle(). בכל פעם שאפשר, ציינו את כל תרחישי השימוש שצריכים לפעול בו-זמנית בסשן אחד בקריאה אחת ל-bindToLifecycle(). ‫CameraX קובעת את הרזולוציות על סמך קבוצת תרחישי השימוש שמוגדרים, תוך התחשבות ברמת החומרה הנתמכת של המכשיר ובשונות הספציפית למכשיר (כאשר מכשיר חורג מהגדרות הסטרימינג הזמינות או לא עומד בהן). המטרה היא לאפשר לאפליקציה לפעול במגוון רחב של מכשירים, תוך צמצום נתיבי הקוד הספציפיים למכשיר.

יחס הגובה-רוחב שמוגדר כברירת מחדל לצילום תמונות ולניתוח תמונות הוא 4:3.

לתרחישי שימוש יש יחס גובה-רוחב שניתן להגדרה, כדי שהאפליקציה תוכל לציין את יחס הגובה-רוחב הרצוי על סמך עיצוב ממשק המשתמש. הפלט של CameraX מותאם ליחסי הגובה-רוחב שהתבקשו, בהתאם ליכולות של המכשיר. אם אין רזולוציה שמתאימה בדיוק, נבחרת הרזולוציה שעומדת ברוב התנאים. לכן, האפליקציה קובעת איך המצלמה מופיעה באפליקציה, ו-CameraX קובעת את הגדרות הרזולוציה הטובות ביותר של המצלמה כדי לעמוד בדרישות האלה במכשירים שונים.

לדוגמה, אפליקציה יכולה:

  • הגדרת יעד רזולוציה של 4:3 או 16:9 לתרחיש לדוגמה
  • ציינו רזולוציה מותאמת אישית, ו-CameraX ינסה למצוא את ההתאמה הכי קרובה אליה
  • צריך לציין יחס גובה-רוחב לחיתוך של ImageCapture

‫CameraX בוחרת באופן אוטומטי את הרזולוציות של משטח Camera2 הפנימי. בטבלה הבאה מפורטות הרזולוציות:

תרחיש לדוגמה רזולוציית משטח פנימי רזולוציית נתוני הפלט
תצוגה מקדימה יחס גובה-רוחב: הרזולוציה שהכי מתאימה ליעד של ההגדרה. רזולוציה של משטח פנימי. המטא-נתונים מסופקים כדי לאפשר לתצוגה לחתוך, לשנות את קנה המידה ולסובב את יחס הגובה-רוחב של היעד.
רזולוציית ברירת המחדל: הרזולוציה הגבוהה ביותר של התצוגה המקדימה, או הרזולוציה הגבוהה ביותר שמועדפת על המכשיר שתואמת ליחס הגובה-רוחב של התצוגה המקדימה.
רזולוציה מקסימלית: גודל התצוגה המקדימה, שמתייחס לגודל שהכי מתאים לרזולוציית המסך של המכשיר, או ל-1080p ‏ (1920x1080), הקטן מביניהם.
ניתוח תמונות יחס גובה-רוחב: הרזולוציה שהכי מתאימה ליעד של ההגדרה. רזולוציה של משטח פנימי.
רזולוציית ברירת המחדל: הגדרת ברירת המחדל של רזולוציית היעד היא 640x480. שינוי של רזולוציית היעד ושל יחס הגובה-רוחב התואם יוביל לרזולוציה הנתמכת הטובה ביותר.
רזולוציה מקסימלית: הרזולוציה המקסימלית של פלט המצלמה בפורמט YUV_420_888, שמאוחזרת מ-StreamConfigurationMap.getOutputSizes(). רזולוציית היעד מוגדרת כ-640x480 כברירת מחדל, כך שאם רוצים רזולוציה גדולה יותר מ-640x480, צריך להשתמש ב-setTargetResolution() וב-setTargetAspectRatio() כדי לקבל את הרזולוציה הקרובה ביותר מבין הרזולוציות הנתמכות.
צילום תמונות יחס גובה-רוחב: יחס הגובה-רוחב שהכי מתאים להגדרה. רזולוציה של משטח פנימי.
רזולוציית ברירת המחדל: הרזולוציה הגבוהה ביותר שזמינה, או הרזולוציה הגבוהה ביותר שמועדפת על המכשיר שתואמת ליחס הגובה-רוחב של ImageCapture.
רזולוציה מקסימלית: הרזולוציה המקסימלית של הפלט של המצלמה בפורמט JPEG. אפשר להשתמש ב-StreamConfigurationMap.getOutputSizes() כדי לאחזר את זה.

מציינים רזולוציה

אפשר להגדיר רזולוציות ספציפיות כשיוצרים תרחישים לדוגמה באמצעות השיטה setTargetResolution(Size resolution), כמו שמוצג בדוגמת הקוד הבאה:

Kotlin

val imageAnalysis = ImageAnalysis.Builder()
    .setTargetResolution(Size(1280, 720))
    .build()

Java

ImageAnalysis imageAnalysis =
  new ImageAnalysis.Builder()
    .setTargetResolution(new Size(1280, 720))
    .build();

אי אפשר להגדיר גם יחס גובה-רוחב וגם רזולוציה לאותו תרחיש לדוגמה. אם תעשו את זה, תופיע השגיאה IllegalArgumentException כשתבנו את אובייקט ההגדרות האישיות.

מציינים את הרזולוציה Size במסגרת הקואורדינטות אחרי סיבוב הגדלים הנתמכים לפי סיבוב היעד. לדוגמה, מכשיר עם כיוון טבעי לאורך, בכיוון יעד טבעי, שמבקש תמונה לאורך, יכול לציין 480x640. אותו מכשיר, מסובב ב-90 מעלות ומטרגט כיוון לרוחב, יכול לציין 640x480.

רזולוציית היעד מנסה לקבוע גבול מינימלי לרזולוציית התמונה. הרזולוציה בפועל של התמונה היא הרזולוציה הזמינה הקרובה ביותר בגודל שלא קטן מרזולוציית היעד, כפי שנקבעה על ידי הטמעת המצלמה.

עם זאת, אם אין רזולוציה ששווה לרזולוציית היעד או גדולה ממנה, נבחרת הרזולוציה הזמינה הקרובה ביותר שקטנה מרזולוציית היעד. רזולוציות עם אותו יחס גובה-רוחב כמו של Size שצוין מקבלות עדיפות גבוהה יותר מרזולוציות עם יחסי גובה-רוחב שונים.

‫CameraX מחילה את הרזולוציה המתאימה ביותר על סמך הבקשות. אם הצורך העיקרי הוא לעמוד בדרישות של יחס הגובה-רוחב, צריך לציין רק את setTargetAspectRatio, ו-CameraX יקבע רזולוציה ספציפית שמתאימה למכשיר. אם הצורך העיקרי של האפליקציה הוא לציין רזולוציה כדי לייעל את עיבוד התמונה (לדוגמה, תמונה קטנה או בינונית על סמך יכולת העיבוד של המכשיר), צריך להשתמש ב-setTargetResolution(Size resolution).

אם האפליקציה דורשת רזולוציה מדויקת, עיינו בטבלה שבמאמר createCaptureSession() כדי לקבוע אילו רזולוציות מקסימליות נתמכות בכל רמת חומרה. כדי לבדוק את הרזולוציות הספציפיות שנתמכות במכשיר הנוכחי, ראו StreamConfigurationMap.getOutputSizes(int).

אם האפליקציה פועלת ב-Android מגרסה 10 ואילך, אפשר להשתמש ב-isSessionConfigurationSupported() כדי לאמת SessionConfiguration ספציפי.

שליטה בפלט של המצלמה

בנוסף לאפשרות להגדיר את פלט המצלמה לפי הצורך לכל תרחיש שימוש, ב-CameraX מיושמים גם הממשקים הבאים כדי לתמוך בפעולות מצלמה שמשותפות לכל תרחישי השימוש הקשורים:

  • CameraControl מאפשר להגדיר תכונות נפוצות של המצלמה.
  • CameraInfo מאפשרת לשלוח שאילתות לגבי המצבים של תכונות המצלמה הנפוצות האלה.

אלה התכונות של המצלמה שנתמכות ב-CameraControl:

קבלת מופעים של CameraControl ו-CameraInfo

מאחזרים מופעים של CameraControl ו-CameraInfo באמצעות האובייקט Camera שמוחזר על ידי ProcessCameraProvider.bindToLifecycle(). הקוד הבא מציג דוגמה:

Kotlin

val camera = processCameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview)

// For performing operations that affect all outputs.
val cameraControl = camera.cameraControl
// For querying information and states.
val cameraInfo = camera.cameraInfo

Java

Camera camera = processCameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview);

// For performing operations that affect all outputs.
CameraControl cameraControl = camera.getCameraControl();
// For querying information and states.
CameraInfo cameraInfo = camera.getCameraInfo();

לדוגמה, אפשר לשלוח פעולות של שינוי מרחק התצוגה ופעולות אחרות של CameraControl אחרי קריאה ל-bindToLifecycle(). אחרי שעוצרים או משמידים את הפעילות שמשמשת לקישור המופע של המצלמה, CameraControl לא יכול יותר לבצע פעולות ומחזיר ListenableFuture שנכשל.

שינוי מרחק התצוגה

ב-CameraControl יש שתי שיטות לשינוי רמת הזום:

  • setZoomRatio() מגדירה את הזום לפי יחס הזום.

    היחס חייב להיות בטווח שבין CameraInfo.getZoomState().getValue().getMinZoomRatio() ל-CameraInfo.getZoomState().getValue().getMaxZoomRatio(). אחרת, הפונקציה מחזירה ListenableFuture שנכשל.

  • ‫setLinearZoom() מגדיר את הזום הנוכחי עם ערך זום לינארי בטווח שבין 0 ל-1.0.

    היתרון של זום לינארי הוא ששדה הראייה (FOV) משתנה בהתאם לשינויים בזום. לכן הוא אידיאלי לשימוש עם תצוגת Slider.

CameraInfo.getZoomState() מחזירה LiveData של מצב הזום הנוכחי. הערך משתנה כשהמצלמה מאותחלת או אם רמת הזום מוגדרת באמצעות setZoomRatio() או setLinearZoom(). קריאה לאחת מהשיטות מגדירה את הערכים שמגבים את ZoomState.getZoomRatio() ואת ZoomState.getLinearZoom(). האפשרות הזו שימושית אם רוצים להציג טקסט של יחס הזום לצד פס הזזה. פשוט צופים ב-ZoomState LiveData כדי לעדכן את שניהם בלי לבצע המרה.

הערך ListenableFuture שמוחזר על ידי שני ממשקי ה-API מאפשר לאפליקציות לקבל התראה כשבקשה חוזרת עם ערך הזום שצוין הושלמה. בנוסף, אם מגדירים ערך זום חדש בזמן שהפעולה הקודמת עדיין מתבצעת, ה-ListenableFuture של פעולת הזום הקודמת נכשל מיד.

לפיד

‫CameraControl.enableTorch(boolean) מפעיל או משבית את הפנס.

אפשר להשתמש ב-CameraInfo.getTorchState() כדי לשלוח שאילתה לגבי המצב הנוכחי של הפנס. כדי לבדוק אם יש פנס זמין, אפשר לבדוק את הערך שמוחזר על ידי CameraInfo.hasFlashUnit(). אם לא, קריאה ל-CameraControl.enableTorch(boolean) גורמת ל-ListenableFuture שמוחזר להסתיים מיד עם תוצאה של כישלון, ומגדירה את מצב הפנס ל-TorchState.OFF.

כשהפנס מופעל, הוא נשאר דולק בזמן צילום תמונות וסרטונים, בלי קשר להגדרת מצב הפלאש. מצב הפלאש שמוגדר באמצעות ImageCapture.setFlashMode() פועל רק כשהפנס מושבת.

מיקוד ומדידה

CameraControl.startFocusAndMetering() מפעיל מדידה של מיקוד אוטומטי (AF), חשיפה אוטומטית (AE) ואיזון לבן אוטומטי (AWB) על ידי הגדרת אזורי מדידה על סמך ה-FocusMeteringAction שצוין. אתם יכולים להשתמש בשיטה הזו כדי להטמיע תכונה של מיקוד בנגיעה באפליקציות של מצלמה.

MeteringPoint

כדי להתחיל, יוצרים MeteringPoint באמצעות MeteringPointFactory.createPoint(float x, float y, float size). MeteringPoint מייצג נקודה אחת במצלמה Surface. הוא מאוחסן בצורה מנורמלת כדי שיהיה אפשר להמיר אותו לקואורדינטות של החיישן לצורך ציון אזורי AF/AE/AWB.

הגודל של MeteringPoint נע בין 0 ל-1, וגודל ברירת המחדל הוא 0.15f. כשמפעילים את MeteringPointFactory.createPoint(float x, float y, float size), ‏CameraX יוצר אזור מלבני שמרכזו ב-(x, y) עבור ה-size שסופק.

הקוד הבא מדגים איך ליצור MeteringPoint:

// Use PreviewView.getMeteringPointFactory if PreviewView is used for preview.
previewView.setOnTouchListener { view, motionEvent ->
    if (motionEvent.action == MotionEvent.ACTION_UP) {
        val meteringPoint = previewView.meteringPointFactory
            .createPoint(motionEvent.x, motionEvent.y)
        // Build and start a FocusMeteringAction with meteringPoint.
        view.performClick()
    }
    true
}

// Use DisplayOrientedMeteringPointFactory if SurfaceView / TextureView is used
// for preview. Note that if the preview is scaled or cropped in the View,
// it's the application's responsibility to transform the coordinates properly
// so that the width and height of this factory represents the full Preview FOV.
// And the (x,y) passed to create MeteringPoint might need to be adjusted with
// the offsets.
val displayFactory = DisplayOrientedMeteringPointFactory(
    surfaceView.display,
    camera.cameraInfo,
    surfaceView.width.toFloat(),
    surfaceView.height.toFloat()
)

// Use SurfaceOrientedMeteringPointFactory if the point is specified in
// ImageAnalysis ImageProxy.
val surfaceFactory = SurfaceOrientedMeteringPointFactory(
    imageWidth.toFloat(),
    imageHeight.toFloat(),
    imageAnalysis
)

startFocusAndMetering ו-FocusMeteringAction

כדי להפעיל את startFocusAndMetering(), האפליקציות צריכות ליצור FocusMeteringAction, שמורכב מאובייקט אחד או יותר של MeteringPoint עם שילובים אופציונליים של מצבי מדידה מ-FLAG_AF, מ-FLAG_AE ומ-FLAG_AWB. הקוד הבא מדגים את השימוש הזה:

// meteringPointFactory is any MeteringPointFactory from the previous example.
val meteringPoint1 = meteringPointFactory.createPoint(x1, y1)
val meteringPoint2 = meteringPointFactory.createPoint(x2, y2)
val action = FocusMeteringAction.Builder(meteringPoint1) // default AF|AE|AWB
    // Optionally add meteringPoint2 for AF/AE.
    .addPoint(meteringPoint2, FLAG_AF or FLAG_AE)
    // The action is canceled in 3 seconds (if not set, default is 5s).
    .setAutoCancelDuration(3, TimeUnit.SECONDS)
    .build()

val result = cameraControl.startFocusAndMetering(action)
// Adds listener to the ListenableFuture if you need to know the focusMetering
// result.
result.addListener({
    // result.get().isFocusSuccessful returns if the auto focus is successful
    // or not.
}, ContextCompat.getMainExecutor(this))

כפי שמוצג בקוד שלמעלה, startFocusAndMetering() מקבל FocusMeteringAction שמורכב מ-MeteringPoint אחד עבור אזורי מדידה של AF/AE/AWB ו-MeteringPoint נוסף עבור AF ו-AE בלבד.

לא כל מכשיר תומך ב-AF/AE/AWB ובמספר אזורים, ולכן CameraX מבצע את FocusMeteringAction בצורה הטובה ביותר. ‫CameraX משתמש במספר המקסימלי של אובייקטים מסוג MeteringPoint שנתמכים, לפי הסדר שבו נוספו הנקודות. המערכת מתעלמת מכל הנקודות שנוספו אחרי המספר המקסימלי. לדוגמה, אם מספקים ל-FocusMeteringAction שלושה אובייקטים מסוג MeteringPoint בפלטפורמה שתומכת רק בשניים, המערכת תשתמש רק בשתי הנקודות הראשונות. ה-MeteringPoint הסופי לא נכלל ב-CameraX.

פיצוי חשיפה

פיצוי חשיפה שימושי כשאפליקציות צריכות לכוונן את ערכי החשיפה (EV) מעבר לתוצאת הפלט של החשיפה האוטומטית (AE). ערכי פיצוי החשיפה משולבים באופן הבא כדי לקבוע את החשיפה הנדרשת לתנאי התמונה הנוכחיים:

Exposure = ExposureCompensationIndex * ExposureCompensationStep

‫CameraX מספקת את הפונקציה CameraControl.setExposureCompensationIndex() כדי להגדיר את פיצוי החשיפה כערך אינדקס.

ערכי אינדקס חיוביים מבהירים את התמונה, וערכים שליליים מעמעמים אותה. אפליקציות יכולות לשלוח שאילתות לגבי הטווח הנתמך באמצעות ExposureState.getExposureCompensationRange(). אם הערך נתמך, ה-ListenableFuture שמוחזר מסתיים כשהערך מופעל בהצלחה בבקשת הצילום. אם האינדקס שצוין נמצא מחוץ לטווח הנתמך, setExposureCompensationIndex() גורם ל-ListenableFuture שמוחזר להסתיים מיד עם תוצאה של כישלון.

‫CameraX שומרת רק את הבקשה האחרונה של setExposureCompensationIndex() שעדיין לא טופלה. אם קוראים לפונקציה שוב לפני ש-CameraX מחילה את הבקשה הקודמת, CameraX מבטלת את הבקשה הקודמת.

קטע הקוד הבא מגדיר אינדקס פיצוי חשיפה ורושם קריאה חוזרת (callback) למקרה שהבקשה לשינוי החשיפה בוצעה:

camera.cameraControl.setExposureCompensationIndex(exposureCompensationIndex)
    .addListener({
        // Get the current exposure compensation index, it might be
        // different from the asked value in case this request was
        // canceled by a newer setting request.
        val currentExposureIndex = camera.cameraInfo.exposureState.exposureCompensationIndex
        // Handle the updated exposure compensation index.
    }, mainExecutor)
  • הפונקציה CameraInfo.getExposureState() מאחזרת את הערך הנוכחי של ExposureState, כולל:

    • התמיכה בבקרת פיצוי חשיפה.
    • האינדקס הנוכחי של פיצוי החשיפה.
    • טווח האינדקס של פיצוי החשיפה.
    • שלב פיצוי החשיפה שמשמש לחישוב ערך פיצוי החשיפה.

לדוגמה, הקוד הבא מאתחל את ההגדרות של חשיפה SeekBar עם הערכים הנוכחיים של ExposureState. ערך ההתקדמות של SeekBar מתחיל ב-0, ולכן הקוד מקזז אותו לפי הגבול התחתון של טווח פיצוי החשיפה:

val exposureState = camera.cameraInfo.exposureState
val range = exposureState.exposureCompensationRange
binding.seekBar.apply {
    isEnabled = exposureState.isExposureCompensationSupported
    max = range.upper - range.lower
    progress = exposureState.exposureCompensationIndex - range.lower
    setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
        override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
            if (fromUser) {
                camera.cameraControl.setExposureCompensationIndex(progress + range.lower)
            }
        }
        override fun onStartTrackingTouch(seekBar: SeekBar) {}
        override fun onStopTrackingTouch(seekBar: SeekBar) {}
    })
}

מקורות מידע נוספים

איפה אפשר למצוא מידע נוסף על CameraX?

Codelab

  • תחילת העבודה עם CameraX
  • דוגמת קוד

  • אפליקציות לדוגמה של CameraX
  • קהילת המפתחים

    קבוצת הדיון של Android CameraX