ใช้ความลึกในแอป Android

Depth API ช่วยให้กล้องของอุปกรณ์ทำความเข้าใจขนาดและรูปร่างของวัตถุจริงในฉาก ซึ่งจะใช้กล้องเพื่อสร้างรูปภาพที่มีความลึกหรือแผนที่ที่มีความลึก ซึ่งจะช่วยเพิ่มความสมจริงของ AR ลงในแอป คุณสามารถใช้ข้อมูลที่ได้จากรูปภาพที่มีความลึกเพื่อทำให้วัตถุเสมือนจริงปรากฏด้านหน้าหรือด้านหลังวัตถุในโลกจริงอย่างแม่นยำ ซึ่งช่วยให้ผู้ใช้ได้รับประสบการณ์ที่สมจริงและสมจริง

ข้อมูลความลึกจะคำนวณจากการเคลื่อนไหวและอาจรวมกับข้อมูลจากเซ็นเซอร์วัดความลึกของฮาร์ดแวร์ เช่น เซ็นเซอร์เวลาการบิน (ToF) (หากมี) อุปกรณ์ไม่ต้องใช้เซ็นเซอร์ ToF เพื่อรองรับ Depth API

ข้อกำหนดเบื้องต้น

ตรวจสอบว่าคุณเข้าใจแนวคิด AR พื้นฐาน และวิธีกำหนดค่าเซสชัน ARCore ก่อนดำเนินการต่อ

จำกัดการเข้าถึงเฉพาะอุปกรณ์ที่รองรับความลึก

หากแอปของคุณต้องใช้การรองรับ Depth API ไม่ว่าจะเพราะส่วนหลักของประสบการณ์การใช้งาน AR ต้องอาศัยความลึก หรือเพราะส่วนต่างๆ ของแอปที่ใช้ความลึกไม่มีให้ใช้งานแล้ว ก็เลือกจำกัดการเผยแพร่แอปใน Google Play Store กับอุปกรณ์ที่รองรับ Depth API โดยเพิ่มบรรทัดต่อไปนี้ลงใน AndroidManifest.xml นอกเหนือจาก คู่มือการเปลี่ยนแปลง AR ที่อธิบายไว้ในAndroidManifest.xml

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

เปิดใช้ความลึก

ในเซสชัน ARCore ใหม่ ให้ตรวจสอบว่าอุปกรณ์ของผู้ใช้รองรับ Depth หรือไม่ อุปกรณ์ที่เข้ากันได้กับ 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 ในเรขาคณิตที่สังเกตได้และจุด 2 มิติ a ที่แทนจุดเดียวกันในภาพความลึก ค่าที่ได้จาก Depth 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);

คุณสามารถแสดงผลการบังโดยใช้การแสดงผลแบบ 2-Pass หรือการแสดงผลต่อออบเจ็กต์แบบ Forward-Pass ประสิทธิภาพของแต่ละวิธีขึ้นอยู่กับความซับซ้อนของฉากและข้อควรพิจารณาอื่นๆ ของแอปโดยเฉพาะ

การแสดงผลต่อออบเจ็กต์, Forward-Pass

การแสดงผลแบบ Forward-Pass ต่อวัตถุจะเป็นตัวกำหนดการบดบังของแต่ละพิกเซลของวัตถุในตัวให้เฉดสีวัสดุ หากมองไม่เห็นพิกเซล ภาพมักจะถูกตัดออกด้วยการผสมอัลฟ่า ดังนั้นจึงจำลองการบดบังบนอุปกรณ์ของผู้ใช้

การแสดงภาพแบบ 2-Pass

เมื่อมีการแสดงผลแบบ 2-Pass คำสั่งแรกจะแสดงเนื้อหาเสมือนทั้งหมดลงในบัฟเฟอร์ตัวกลาง ด่านที่ 2 จะผสานฉากเสมือนจริงเข้ากับพื้นหลังตามความแตกต่างระหว่างความลึกในโลกจริงกับความลึกของฉากเสมือนจริง วิธีนี้ไม่ต้องใช้ตัวปรับเฉดสีเฉพาะวัตถุเพิ่มเติม และโดยทั่วไปจะให้ผลลัพธ์ที่มีลักษณะแบบเดียวกันมากกว่าวิธี Forward-Pass

ดึงระยะห่างจากรูปภาพที่มีความลึก

หากต้องการใช้ 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() อาจมีสัดส่วนภาพต่างจากรูปภาพที่มีความลึก ในกรณีนี้ ภาพที่มีความลึกคือการครอบตัดภาพจากกล้อง และบางพิกเซลในภาพจากกล้องไม่ได้มีค่าประมาณความลึกที่ถูกต้อง

วิธีรับพิกัดรูปภาพความลึกสำหรับพิกัดในอิมเมจ CPU

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()

การทดสอบ Hit ความลึก

Hit-test ช่วยให้ผู้ใช้วางวัตถุในตำแหน่งสถานที่จริงในฉากได้ ซึ่งก่อนหน้านี้ การทดสอบ Hit จะทำได้บนระนาบที่ตรวจพบเท่านั้น โดยจํากัดสถานที่ให้มีพื้นผิวราบเรียบขนาดใหญ่เท่านั้น เช่น ผลการค้นหาจาก Android สีเขียว การทดสอบ Hit ความลึกใช้ประโยชน์จากทั้งข้อมูลความลึกที่ไม่เรียบและข้อมูลดิบเพื่อให้ผลของ Hit ที่แม่นยำมากขึ้น แม้จะอยู่บนพื้นผิวที่ไม่ใช่แบบระนาบหรือมีพื้นผิวต่ำ ซึ่งจะแสดงพร้อม Android สีแดง

หากต้องการใช้การทดสอบ Hit ที่เปิดใช้ความลึก ให้เรียก 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 ซึ่งสาธิตวิธีต่างๆ ในการเข้าถึงข้อมูลเชิงลึก