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

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

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

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

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

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

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

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

รูปภาพที่ส่งคืนจะมีบัฟเฟอร์รูปภาพแบบ RAW ซึ่งสามารถส่งผ่านไปยังตัวปรับแสงเงา Fragment สำหรับการใช้งานบน GPU สำหรับวัตถุที่แสดงผลแต่ละอ็อบเจกต์ ซึ่งอยู่ใน OPENGL_NORMALIZED_DEVICE_COORDINATES และจะเปลี่ยนเป็น TEXTURE_NORMALIZED ได้โดยโทรไปที่ Frame.transformCoordinates2d() เมื่อเข้าถึงรูปภาพความลึกภายในเครื่องมือปรับแสงเงาวัตถุ การวัดความลึกเหล่านี้จะเข้าถึงการวัดความลึกเหล่านี้ได้โดยตรงสำหรับการจัดการการซ้อนทับ

ทำความเข้าใจค่าความลึก

ได้จุด A บนรูปทรงเรขาคณิตที่พบได้จริงและจุด 2 มิติ a แสดงจุดเดียวกันในรูปภาพความลึก ซึ่งเป็นค่าที่ระบุโดยความลึก API ที่ a เท่ากับความยาวของ CA ที่คาดการณ์บนแกนหลัก นี้อาจเรียกว่าพิกัด z ของ A ที่สัมพันธ์กับกล้อง ต้นทาง C เมื่อทำงานกับ Depth API คุณควรเข้าใจว่า ค่าความลึกไม่ใช่ความยาวของแสง CA เอง แต่เป็นเส้นโครง จากส่วนนี้

ใช้ความลึกในตัวสร้างเฉดสี

แยกวิเคราะห์ข้อมูลความลึกสำหรับเฟรมปัจจุบัน

ใช้ฟังก์ชันตัวช่วย DepthGetMillimeters() และ DepthGetVisibility() ในตัวปรับแสงเงา Fragment เพื่อเข้าถึงข้อมูลความลึกสำหรับตำแหน่งปัจจุบันของหน้าจอ จากนั้นใช้ข้อมูลนี้เพื่อเลือกบางส่วนของออบเจ็กต์ที่แสดงผล

// 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 พาสหรือการแสดงผลต่อวัตถุ นั่นคือ การส่งการส่งต่อ ประสิทธิภาพของแต่ละวิธีขึ้นอยู่กับความซับซ้อนของสถานการณ์และข้อพิจารณาเฉพาะอื่นๆ ของแอป

การแสดงผลแบบ Forward Pass ต่อวัตถุ

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

การแสดงผลแบบ 2 บัตรผ่าน

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

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

หากต้องการใช้ 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 ทำให้ผู้ใช้สามารถวางวัตถุในตำแหน่งจริงของฉากได้ ก่อนหน้านี้การทดสอบ Hit จะทำได้เฉพาะในระนาบที่ตรวจพบเท่านั้น โดยจำกัดตำแหน่งไว้สำหรับพื้นราบขนาดใหญ่ เช่น ผลลัพธ์ที่แสดงโดย Android สีเขียว การทดสอบ 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 ซึ่งสาธิตวิธีต่างๆ ในการเข้าถึงข้อมูลความลึก