פעילויות ובקשות של צילום מצלמה

הערה: הדף הזה מתייחס לחבילה Camera2. אם האפליקציה לא דורשת תכונות ספציפיות ברמה נמוכה מ-Camera2, מומלץ להשתמש ב-CameraX. גם CameraX וגם Camera2 תומכים ב-Android מגרסה 5.0 (רמת API‏ 21) ואילך.

במכשיר Android אחד יכולות להיות כמה מצלמות. כל מצלמה היא CameraDevice, ו-CameraDevice יכול להפיק יותר משידור אחד בו-זמנית.

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

איור 1. איור מתוך Building a Universal Camera App (Google I/O ‘18)

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

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

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

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

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

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

שימוש בתרחישי שימוש ב-Stream לשיפור הביצועים

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

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

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

תרחישי שימוש ב-Stream כוללים:

  • ‫DEFAULT: כולל את כל ההתנהגויות הקיימות של האפליקציה. זה שווה ערך לאי-הגדרה של תרחיש שימוש בסטרימינג.

  • ‫PREVIEW: מומלץ לתצוגת הצילום או לניתוח תמונות בתוך האפליקציה.

  • ‫STILL_CAPTURE: אופטימיזציה לצילום באיכות גבוהה וברזולוציה גבוהה, ולא צפוי לשמור על קצב פריימים כמו בתצוגה המקדימה.

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

  • ‫VIDEO_CALL: מומלץ לשימוש במצלמה לאורך זמן, כשיש חשש להתרוקנות הסוללה.

  • ‫PREVIEW_VIDEO_STILL: מומלץ לאפליקציות של רשתות חברתיות או לתרחישים לדוגמה של סטרימינג יחיד. זהו שידור רב-תכליתי.

  • ‫VENDOR_START: משמש לתרחישים לדוגמה שמוגדרים על ידי OEM.

יצירת CameraCaptureSession

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

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

// Retrieve the target surfaces, which might be coming from a number of places:
// 1. SurfaceView, if you want to display the image directly to the user
// 2. ImageReader, if you want to read each frame or perform frame-by-frame
// analysis
// 3. OpenGL Texture or TextureView, although discouraged for maintainability
// reasons
// 4. RenderScript.Allocation, if you want to do parallel processing
val surfaceView = findViewById<SurfaceView>(
    // ...
)
val imageReader = ImageReader.newInstance(
    // ...
)

// Remember to call this only *after* SurfaceHolder.Callback.surfaceCreated().
val previewSurface = surfaceView.holder.surface
val imReaderSurface = imageReader.surface
val targets = listOf(previewSurface, imReaderSurface)

// Create a capture session using the predefined targets; this also involves
// defining the session state callback to be notified of when the session is
// ready.
// Setup Stream Use Case while setting up your Output Configuration.
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
fun configureSession(device: CameraDevice, targets: List<Surface>) {
    val configs = mutableListOf<OutputConfiguration>()
    val streamUseCase = CameraMetadata
        .SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL

    targets.forEach {
        val config = OutputConfiguration(it)
        config.streamUseCase = streamUseCase.toLong()
        configs.add(config)
    }
    // ...
    device.createCaptureSession(session)
}

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

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

ה-framework מנסה לעשות כמיטב יכולתו, אבל יכול להיות ששילובים מסוימים של הגדרות Surface לא יפעלו, ויגרמו לבעיות כמו אי-יצירת הסשן, שגיאת זמן ריצה כששולחים בקשה או פגיעה בביצועים. המסגרת מספקת הבטחות לשילובים ספציפיים של מכשיר, פלטפורמה ופרמטרים של בקשות. מידע נוסף מופיע במאמרי העזרה של createCaptureSession().

בקשות CaptureRequest בודדות

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

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

כדי ליצור בקשת תיעוד של SurfaceView באמצעות התבנית שמיועדת לתצוגה מקדימה בלי שינויים, משתמשים ב-CameraDevice.TEMPLATE_PREVIEW:

val session: CameraCaptureSession = /* ... */ // from CameraCaptureSession.StateCallback
val captureRequest = session.device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
captureRequest.addTarget(previewSurface)

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

val session: CameraCaptureSession = ...  // from CameraCaptureSession.StateCallback
val captureRequest: CaptureRequest.Builder = ...  // from CameraDevice.createCaptureRequest()

// The first null argument corresponds to the capture callback, which you
// provide if you want to retrieve frame metadata or keep track of failed capture
// requests that can indicate dropped frames; the second null argument
// corresponds to the Handler used by the asynchronous callback, which falls
// back to the current thread's looper if null.
session.capture(captureRequest.build(), null, null)

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

בקשות CaptureRequest חוזרות

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

val session: CameraCaptureSession = ... // from CameraCaptureSession.StateCallback
val captureRequest: CaptureRequest.Builder = ... // from CameraDevice.createCaptureRequest()

// This keeps sending the capture request as frequently as possible until
// the session is torn down or session.stopRepeating() is called.
session.setRepeatingRequest(captureRequest.build(), null, null)

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

בקשות CaptureRequest משולבות

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

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

val session: CameraCaptureSession = /* ... */ // from CameraCaptureSession.StateCallback

// Create the repeating request and dispatch it.
val repeatingRequest = session.device.createCaptureRequest(
    CameraDevice.TEMPLATE_PREVIEW
)
repeatingRequest.addTarget(previewSurface)
session.setRepeatingRequest(repeatingRequest.build(), null, null)

// Some time later...

// Create the single request and dispatch it.
// NOTE: This can disrupt the ongoing repeating request momentarily.
val singleRequest = session.device.createCaptureRequest(
    CameraDevice.TEMPLATE_STILL_CAPTURE
)
singleRequest.addTarget(imReaderSurface)
session.capture(singleRequest.build(), null, null)

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

איור 2. איור של תור בקשות לסשן המצלמה המתמשך

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

  • מוסיפים את יעדי הפלט מבקשה א' לבקשה ב'. כך, כשהפריים של B מוכן, הוא מועתק ליעדי הפלט של A. לדוגמה, זה חיוני כשמצלמים תמונות מסרטון כדי לשמור על קצב פריימים יציב. בקוד שלמעלה, מוסיפים את singleRequest.addTarget(previewSurface) לפני שיוצרים את הבקשה.

  • משתמשים בשילוב של תבניות שנועדו לפעול בתרחיש הספציפי הזה, כמו אפס השהיית תריס.