Sử dụng Chiều sâu trong ứng dụng Android

Depth API (API Chiều sâu) giúp camera trên thiết bị hiểu được kích thước và hình dạng của các đối tượng thực trong một cảnh. Tính năng này sử dụng camera để tạo hình ảnh chiều sâu hay bản đồ độ sâu, do đó thêm một lớp hiện thực AR vào ứng dụng của bạn. Bạn có thể sử dụng thông tin do hình ảnh chiều sâu cung cấp để làm cho các đối tượng ảo xuất hiện chính xác ở phía trước hoặc phía sau các đối tượng trong thế giới thực, nhờ đó mang lại trải nghiệm sống động và chân thực cho người dùng.

Thông tin về độ sâu được tính toán từ chuyển động và có thể được kết hợp với thông tin từ cảm biến độ sâu phần cứng, chẳng hạn như cảm biến thời gian bay (ToF), nếu có. Thiết bị không cần có cảm biến ToF để hỗ trợ depth API.

Điều kiện tiên quyết

Đảm bảo rằng bạn hiểu rõ các khái niệm cơ bản về AR và cách định cấu hình phiên ARCore trước khi tiếp tục.

Chỉ cho phép những thiết bị hỗ trợ tính năng Chiều sâu

Nếu ứng dụng của bạn cần hỗ trợ API Độ sâu, do một phần cốt lõi của Trải nghiệm thực tế tăng cường phụ thuộc vào chiều sâu hoặc do không có phương án dự phòng phù hợp cho cho các phần của ứng dụng sử dụng chiều sâu, bạn có thể chọn hạn chế phân phối trong Cửa hàng Google Play để những thiết bị hỗ trợ depth API bằng cách thêm dòng sau vào AndroidManifest.xml của bạn, ngoài việc AndroidManifest.xml thay đổi được mô tả trong Hướng dẫn Bật ARCore:

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

Bật tính năng Chiều sâu

Trong phiên ARCore mới, hãy kiểm tra xem thiết bị của người dùng có hỗ trợ Chiều sâu hay không. Không phải thiết bị nào tương thích với ARCore cũng hỗ trợ depth API do những hạn chế về công suất xử lý. Để tiết kiệm tài nguyên, theo mặc định, độ sâu sẽ bị tắt trên ARCore. Bật chế độ chiều sâu để ứng dụng sử dụng 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)

Thu thập hình ảnh có chiều sâu

Gọi Frame.acquireDepthImage16Bits() để lấy hình ảnh chiều sâu cho khung hình hiện tại.

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

Hình ảnh được trả về cung cấp vùng đệm hình ảnh thô. Vùng đệm này có thể được truyền đến chương trình đổ bóng mảnh để sử dụng trên GPU cho từng đối tượng kết xuất cần che khuất. Thuộc tính này được định hướng trong OPENGL_NORMALIZED_DEVICE_COORDINATES và có thể được thay đổi thành TEXTURE_NORMALIZED bằng cách gọi Frame.transformCoordinates2d(). Khi có thể truy cập hình ảnh chiều sâu trong chương trình đổ bóng đối tượng, bạn có thể truy cập trực tiếp các số đo chiều sâu này để xử lý sự cố che khuất.

Tìm hiểu các giá trị độ sâu

Cho điểm A trên hình học thực tế đã quan sát và điểm 2D a đại diện cho cùng một điểm trong hình ảnh chiều sâu, giá trị được cho bởi Độ sâu API tại a bằng độ dài của CA được chiếu lên trục chính. Đây cũng có thể được gọi là toạ độ z của A so với camera máy chủ gốc C. Khi làm việc với depth API, bạn cần hiểu rằng giá trị độ sâu không phải là độ dài của tia CA mà là hình chiếu nội dung.

Sử dụng độ sâu trong chương trình đổ bóng

Phân tích cú pháp thông tin về chiều sâu của khung hình hiện tại

Sử dụng các hàm trợ giúp DepthGetMillimeters()DepthGetVisibility() trong chương trình đổ bóng mảnh để truy cập vào thông tin chuyên sâu cho vị trí màn hình hiện tại. Sau đó, hãy sử dụng thông tin này để che khuất có chọn lọc các phần của đối tượng được kết xuất.

// 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;
}

Che khuất đối tượng ảo

Phủ các đối tượng ảo trong phần nội dung của chương trình đổ bóng mảnh. Cập nhật kênh alpha của đối tượng dựa trên độ sâu của kênh. Thao tác này sẽ kết xuất một đối tượng bị che khuất.

// 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);

Bạn có thể kết xuất hiệu ứng che khuất bằng cách sử dụng phương pháp kết xuất hai chiều hoặc kết xuất theo từng đối tượng và kết xuất chuyển tiếp. Hiệu quả của mỗi phương pháp phụ thuộc vào mức độ phức tạp của cảnh và các yếu tố khác cần cân nhắc dành riêng cho ứng dụng.

Kết xuất chuyển tiếp trên mỗi đối tượng

Tính năng kết xuất theo từng đối tượng xác định độ che khuất từng pixel của đối tượng trong chương trình đổ bóng chất liệu. Nếu các pixel không hiển thị, thì chúng sẽ bị cắt bớt, thường là bằng cách phối màu alpha, nhờ đó mô phỏng việc che khuất trên thiết bị của người dùng.

Kết xuất hai lượt

Với tính năng kết xuất hai lượt, lượt truyền đầu tiên sẽ kết xuất tất cả nội dung ảo vào một vùng đệm trung gian. Lượt thứ hai kết hợp cảnh ảo với nền dựa trên sự chênh lệch giữa độ sâu của thế giới thực với độ sâu của cảnh ảo. Phương pháp này không yêu cầu thêm hoạt động đổ bóng dành riêng cho đối tượng nào và thường tạo ra kết quả đồng nhất hơn phương thức chuyển tiến.

Trích xuất khoảng cách từ hình ảnh chiều sâu

Để sử dụng depth API cho các mục đích khác ngoài mục đích che khuất đối tượng ảo hoặc trực quan hoá dữ liệu về độ sâu, hãy trích xuất thông tin từ hình ảnh chiều sâu.

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

Chuyển đổi toạ độ giữa hình ảnh máy ảnh và hình ảnh chiều sâu

Hình ảnh thu được bằng getCameraImage() có thể có tỷ lệ khung hình khác so với hình ảnh chiều sâu. Trong trường hợp này, hình ảnh chiều sâu là phần cắt của hình ảnh camera và không phải tất cả pixel trong hình ảnh camera đều có số liệu ước tính độ sâu hợp lệ tương ứng.

Để lấy toạ độ hình ảnh chiều sâu cho toạ độ trên hình ảnh 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()

Để lấy toạ độ hình ảnh CPU cho toạ độ hình ảnh chiều sâu:

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

Kiểm tra nhấn chi tiết

Kiểm thử nhấn cho phép người dùng đặt đối tượng tại vị trí thực tế trong cảnh. Trước đây, các cuộc thử nghiệm va chạm chỉ có thể được tiến hành trên các máy bay được phát hiện, giới hạn các vị trí ở các bề mặt lớn và phẳng, chẳng hạn như kết quả do Android màu xanh lá thể hiện. Kiểm tra lượt truy cập chuyên sâu tận dụng cả thông tin chiều sâu mượt mà lẫn thông tin chiều sâu thô để cung cấp kết quả lượt truy cập chính xác hơn, ngay cả trên bề mặt không phải là mặt phẳng và có kết cấu thấp. Điều này được thể hiện với Android màu đỏ.

Để sử dụng kiểm tra lượt truy cập được bật theo chiều sâu, hãy gọi hitTest() và kiểm tra DepthPoints trong danh sách trả về.

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)

Bước tiếp theo