استخدام ميزة "عمق العمل" في تطبيق Android

تساعد Depth API كاميرا الجهاز في فهم حجم وشكل العناصر الحقيقية في المشهد. فهو يستخدم الكاميرا لإنشاء صور معمّقة أو خرائط معمّقة، ما يضيف طبقة من الواقع المعزّز إلى تطبيقاتك. يمكنك استخدام المعلومات المقدَّمة من خلال صورة العمق لجعل العناصر الافتراضية تظهر بدقة أمام أو خلف كائنات العالم الواقعي، ما يُتيح تجارب مستخدم غامرة وواقعية.

يتم احتساب معلومات العمق من الحركة ويمكن دمجها مع معلومات من مستشعر عمق الجهاز، مثل أداة استشعار وقت الطيران، إن توفرت. لا يحتاج الجهاز إلى أداة استشعار ToF للتوافق مع واجهة برمجة التطبيقات Depth API.

المتطلبات الأساسية

قبل المتابعة، تأكد من فهم مفاهيم الواقع المعزّز الأساسية وكيفية ضبط جلسة ARCore.

حظر الوصول إلى الأجهزة المتوافقة مع Depth

إذا كان تطبيقك يتطلّب دعم واجهة برمجة التطبيقات Depth API، إما لأنّ جزءًا أساسيًا من تجربة الواقع المعزّز يعتمد على العمق، أو لعدم توفّر إجراء احتياطي مناسب لأجزاء التطبيق التي تستخدم العمق، يمكنك اختيار تقييد توزيع تطبيقك في "متجر Google Play" على الأجهزة المتوافقة مع Depth API من خلال إضافة السطر التالي إلى AndroidManifest.xml، بالإضافة إلى تفعيل إرشادات 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.
}

توفر الصورة التي يتم عرضها المخزن المؤقت للصور الأولية، والذي يمكن تمريره إلى أداة تظليل الأجزاء لاستخدامه في وحدة معالجة الرسومات لكل كائن يتم عرضه ليتم إتلافه. يتم توجيهه في OPENGL_NORMALIZED_DEVICE_COORDINATES ويمكن تغييره إلى TEXTURE_NORMALIZED من خلال الاتصال بـ Frame.transformCoordinates2d(). بعد الوصول إلى صورة العمق في أداة تظليل العناصر، يمكن الوصول إلى قياسات العمق هذه مباشرةً للتعامل مع التظليل.

فهم قيم العمق

بناءً على النقطة A في الهندسة الواقعية المرصودة والنقطة الثنائية الأبعاد a التي تمثل النقطة نفسها في صورة العمق، فإن القيمة المقدَّمة من خلال واجهة برمجة التطبيقات للعمق في 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() مختلفة عن الصور ذات العمق. في هذه الحالة، تكون صورة العمق هي جزء من صورة الكاميرا، ولا تمتلك كل وحدات البكسل في صورة الكاميرا تقديرًا صالحًا للعمق المقابلة.

للحصول على إحداثيات العمق للإحداثيات على صورة وحدة المعالجة المركزية (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()

اختبار النتائج المتعمقة

تسمح اختبارات النتائج للمستخدمين بوضع الكائنات في موقع واقعي في المشهد. في السابق، لم يكن بالإمكان إجراء اختبارات النتائج إلا على الطائرات المكتشفة، لحصر المواقع الجغرافية على الأسطح المستوية والكبيرة، كما هو الحال في النتائج التي تعرضها أجهزة 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 الذي يعرض طرقًا مختلفة للوصول إلى بيانات العمق.