שימוש בנתוני עומק באפליקציה ל-Android

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

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

דרישות מוקדמות

לפני שממשיכים, חשוב לוודא שאתם מבינים את המושגים הבסיסיים של AR ואת האופן שבו מגדירים סשן של ARCore.

הגבלת הגישה למכשירים שתומכים בעומק

אם נדרשת תמיכה ב-Depth API באפליקציה, כי חלק מרכזי מחוויית ה-AR מסתמך על העומק או כי אין חלופה חיננית לחלקים באפליקציה שמשתמשים בעומק, אפשר להגביל את הפצת האפליקציה בחנות Google Play למכשירים שתומכים ב-Depth API. לשם כך מוסיפים את השורה הבאה ל-AndroidManifest.xml, בנוסף לשינויי ה-ARC שמתוארים ב-AndroidManifest.xml:

<uses-feature android:name="com.google.ar.core.depth" />

הפעלת העומק

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

Java

Config config = session.getConfig();

// Check whether the user's device supports the Depth API.
boolean isDepthSupported = session.isDepthModeSupported(Config.DepthMode.AUTOMATIC);
if (isDepthSupported) {
  config.setDepthMode(Config.DepthMode.AUTOMATIC);
}
session.configure(config);

Kotlin

val config = session.config

// Check whether the user's device supports the Depth API.
val isDepthSupported = session.isDepthModeSupported(Config.DepthMode.AUTOMATIC)
if (isDepthSupported) {
  config.depthMode = Config.DepthMode.AUTOMATIC
}
session.configure(config)

משיגים תמונות עומק

אפשר להתקשר אל Frame.acquireDepthImage16Bits() כדי לקבל את תמונת העומק של הפריים הנוכחי.

Java

// Retrieve the depth image for the current frame, if available.
Image depthImage = null;
try {
  depthImage = frame.acquireDepthImage16Bits();
  // Use the depth image here.
} catch (NotYetAvailableException e) {
  // This means that depth data is not available yet.
  // Depth data will not be available if there are no tracked
  // feature points. This can happen when there is no motion, or when the
  // camera loses its ability to track objects in the surrounding
  // environment.
} finally {
  if (depthImage != null) {
    depthImage.close();
  }
}

Kotlin

// Retrieve the depth image for the current frame, if available.
try {
  frame.acquireDepthImage16Bits().use { depthImage ->
    // Use the depth image here.
  }
} catch (e: NotYetAvailableException) {
  // This means that depth data is not available yet.
  // Depth data will not be available if there are no tracked
  // feature points. This can happen when there is no motion, or when the
  // camera loses its ability to track objects in the surrounding
  // environment.
}

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

הסבר על ערכי העומק

בהינתן הנקודה A על הגיאומטריה בעולם האמיתי ונקודה דו-ממדית a שמייצגת את אותה נקודה בתמונת העומק, הערך שניתן על ידי ממשק ה-API של עומק ב-a שווה לאורך של CA הצפוי לציר העיקרי. אפשר לכנות אותה גם כקואורדינטת ה-z של A ביחס למקור המצלמה C. כשעובדים עם Depth API, חשוב להבין שערכי העומק הם לא האורך של הקרן CA עצמה, אלא התחזית שלו.

שימוש בעומק ההצללה

ניתוח נתוני העומק של המסגרת הנוכחית

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

// Use DepthGetMillimeters() and DepthGetVisibility() to parse the depth image
// for a given pixel, and compare against the depth of the object to render.
float DepthGetMillimeters(in sampler2D depth_texture, in vec2 depth_uv) {
  // Depth is packed into the red and green components of its texture.
  // The texture is a normalized format, storing millimeters.
  vec3 packedDepthAndVisibility = texture2D(depth_texture, depth_uv).xyz;
  return dot(packedDepthAndVisibility.xy, vec2(255.0, 256.0 * 255.0));
}

// Return a value representing how visible or occluded a pixel is relative
// to the depth image. The range is 0.0 (not visible) to 1.0 (completely
// visible).
float DepthGetVisibility(in sampler2D depth_texture, in vec2 depth_uv,
                         in float asset_depth_mm) {
  float depth_mm = DepthGetMillimeters(depth_texture, depth_uv);

  // Instead of a hard Z-buffer test, allow the asset to fade into the
  // background along a 2 * kDepthTolerancePerMm * asset_depth_mm
  // range centered on the background depth.
  const float kDepthTolerancePerMm = 0.015f;
  float visibility_occlusion = clamp(0.5 * (depth_mm - asset_depth_mm) /
    (kDepthTolerancePerMm * asset_depth_mm) + 0.5, 0.0, 1.0);

 // Use visibility_depth_near to set the minimum depth value. If using
 // this value for occlusion, avoid setting it too close to zero. A depth value
 // of zero signifies that there is no depth data to be found.
  float visibility_depth_near = 1.0 - InverseLerp(
      depth_mm, /*min_depth_mm=*/150.0, /*max_depth_mm=*/200.0);

  // Use visibility_depth_far to set the maximum depth value. If the depth
  // value is too high (outside the range specified by visibility_depth_far),
  // the virtual object may get inaccurately occluded at further distances
  // due to too much noise.
  float visibility_depth_far = InverseLerp(
      depth_mm, /*min_depth_mm=*/7500.0, /*max_depth_mm=*/8000.0);

  const float kOcclusionAlpha = 0.0f;
  float visibility =
      max(max(visibility_occlusion, kOcclusionAlpha),
          max(visibility_depth_near, visibility_depth_far));

  return visibility;
}

חסימת אובייקטים וירטואליים

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

// Occlude virtual objects by updating the object’s alpha channel based on its depth.
const float kMetersToMillimeters = 1000.0;

float asset_depth_mm = v_ViewPosition.z * kMetersToMillimeters * -1.;

// Compute the texture coordinates to sample from the depth image.
vec2 depth_uvs = (u_DepthUvTransform * vec3(v_ScreenSpacePosition.xy, 1)).xy;

gl_FragColor.a *= DepthGetVisibility(u_DepthTexture, depth_uvs, asset_depth_mm);

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

רינדור לפי אובייקט, העברה קדימה

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

עיבוד בשני שלבים

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

חילוץ מרחק מתמונת עומק

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

Java

/** Obtain the depth in millimeters for depthImage at coordinates (x, y). */
public int getMillimetersDepth(Image depthImage, int x, int y) {
  // The depth image has a single plane, which stores depth for each
  // pixel as 16-bit unsigned integers.
  Image.Plane plane = depthImage.getPlanes()[0];
  int byteIndex = x * plane.getPixelStride() + y * plane.getRowStride();
  ByteBuffer buffer = plane.getBuffer().order(ByteOrder.nativeOrder());
  return Short.toUnsignedInt(buffer.getShort(byteIndex));
}

Kotlin

/** Obtain the depth in millimeters for [depthImage] at coordinates ([x], [y]). */
fun getMillimetersDepth(depthImage: Image, x: Int, y: Int): UInt {
  // The depth image has a single plane, which stores depth for each
  // pixel as 16-bit unsigned integers.
  val plane = depthImage.planes[0]
  val byteIndex = x * plane.pixelStride + y * plane.rowStride
  val buffer = plane.buffer.order(ByteOrder.nativeOrder())
  val depthSample = buffer.getShort(byteIndex)
  return depthSample.toUInt()
}

המרת קואורדינטות בין תמונות מצלמה לתמונות עומק

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

כדי לקבל קואורדינטות תמונה של עומק עבור קואורדינטות בתמונה של המעבד:

Java

float[] cpuCoordinates = new float[] {cpuCoordinateX, cpuCoordinateY};
float[] textureCoordinates = new float[2];
frame.transformCoordinates2d(
    Coordinates2d.IMAGE_PIXELS,
    cpuCoordinates,
    Coordinates2d.TEXTURE_NORMALIZED,
    textureCoordinates);
if (textureCoordinates[0] < 0 || textureCoordinates[1] < 0) {
  // There are no valid depth coordinates, because the coordinates in the CPU image are in the
  // cropped area of the depth image.
  return null;
}
return new Pair<>(
    (int) (textureCoordinates[0] * depthImage.getWidth()),
    (int) (textureCoordinates[1] * depthImage.getHeight()));

Kotlin

val cpuCoordinates = floatArrayOf(cpuCoordinateX.toFloat(), cpuCoordinateY.toFloat())
val textureCoordinates = FloatArray(2)
frame.transformCoordinates2d(
  Coordinates2d.IMAGE_PIXELS,
  cpuCoordinates,
  Coordinates2d.TEXTURE_NORMALIZED,
  textureCoordinates,
)
if (textureCoordinates[0] < 0 || textureCoordinates[1] < 0) {
  // There are no valid depth coordinates, because the coordinates in the CPU image are in the
  // cropped area of the depth image.
  return null
}
return (textureCoordinates[0] * depthImage.width).toInt() to
  (textureCoordinates[1] * depthImage.height).toInt()

כדי לקבל קואורדינטות תמונה של המעבד (CPU) עבור קואורדינטות של תמונת עומק:

Java

float[] textureCoordinates =
    new float[] {
      (float) depthCoordinateX / (float) depthImage.getWidth(),
      (float) depthCoordinateY / (float) depthImage.getHeight()
    };
float[] cpuCoordinates = new float[2];
frame.transformCoordinates2d(
    Coordinates2d.TEXTURE_NORMALIZED,
    textureCoordinates,
    Coordinates2d.IMAGE_PIXELS,
    cpuCoordinates);
return new Pair<>((int) cpuCoordinates[0], (int) cpuCoordinates[1]);

Kotlin

val textureCoordinates =
  floatArrayOf(
    depthCoordinatesX.toFloat() / depthImage.width.toFloat(),
    depthCoordinatesY.toFloat() / depthImage.height.toFloat(),
  )
val cpuCoordinates = FloatArray(2)
frame.transformCoordinates2d(
  Coordinates2d.TEXTURE_NORMALIZED,
  textureCoordinates,
  Coordinates2d.IMAGE_PIXELS,
  cpuCoordinates,
)
return cpuCoordinates[0].toInt() to cpuCoordinates[1].toInt()

בדיקת עומק

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

כדי להשתמש בבדיקות היטים התומכות בעומק, מתקשרים אל hitTest() ומחפשים את DepthPoints ברשימת החזרה.

Java

// Create a hit test using the Depth API.
List<HitResult> hitResultList = frame.hitTest(tap);
for (HitResult hit : hitResultList) {
  Trackable trackable = hit.getTrackable();
  if (trackable instanceof Plane
      || trackable instanceof Point
      || trackable instanceof DepthPoint) {
    useHitResult(hit);
    break;
  }
}

Kotlin

// Create a hit test using the Depth API.
val hitResult =
  frame
    .hitTest(tap)
    .filter {
      val trackable = it.trackable
      trackable is Plane || trackable is Point || trackable is DepthPoint
    }
    .firstOrNull()
useHitResult(hitResult)

מה השלב הבא?

  • מפעילים חישה מדויקת יותר באמצעות Raw Depth API.
  • כדאי לנסות את ARCore Depth Lab, שמדגים דרכים שונות לגשת לנתוני עומק.