از Depth در برنامه اندروید خود استفاده کنید

Depth API به دوربین دستگاه کمک می کند تا اندازه و شکل اشیاء واقعی را در یک صحنه درک کند. از دوربین برای ایجاد تصاویر عمق یا نقشه های عمق استفاده می کند و در نتیجه لایه ای از واقعیت واقعیت افزوده را به برنامه های شما اضافه می کند. می‌توانید از اطلاعات ارائه‌شده توسط یک تصویر عمقی استفاده کنید تا اشیاء مجازی را به‌طور دقیق در جلو یا پشت اشیاء دنیای واقعی نشان دهید و تجربه‌های واقعی و واقعی کاربر را امکان‌پذیر کنید.

اطلاعات عمق از حرکت محاسبه می شود و در صورت وجود ممکن است با اطلاعات یک حسگر عمق سخت افزاری مانند سنسور زمان پرواز (ToF) ترکیب شود. یک دستگاه برای پشتیبانی از Depth API به حسگر ToF نیاز ندارد .

پیش نیازها

قبل از ادامه، مطمئن شوید که مفاهیم اساسی AR و نحوه پیکربندی یک جلسه ARCore را درک کرده اید.

دسترسی به دستگاه های پشتیبانی شده از عمق را محدود کنید

اگر برنامه شما به پشتیبانی Depth API نیاز دارد، یا به این دلیل که بخش اصلی تجربه واقعیت افزوده به عمق متکی است، یا به این دلیل که برای بخش‌هایی از برنامه که از عمق استفاده می‌کنند، نسخه بازگشتی خوبی وجود ندارد، می‌توانید توزیع برنامه خود را در Google Play محدود کنید. با افزودن خط زیر به AndroidManifest.xml ، علاوه بر تغییرات AndroidManifest.xml که در راهنمای فعال کردن ARCore توضیح داده شده است، در دستگاه‌هایی که از Depth API پشتیبانی می‌کنند، ذخیره کنید:

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

Depth را فعال کنید

در جلسه ARCore جدید ، بررسی کنید که آیا دستگاه کاربر از عمق پشتیبانی می‌کند یا خیر. همه دستگاه‌های سازگار با ARCore از Depth API به دلیل محدودیت‌های قدرت پردازشی پشتیبانی نمی‌کنند. برای ذخیره منابع، عمق به طور پیش فرض در ARCore غیرفعال است. حالت عمق را فعال کنید تا برنامه شما از Depth API استفاده کند.

جاوا

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

کاتلین

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() را فراخوانی کنید.

جاوا

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

کاتلین

// 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 است و می توان آن را با فراخوانی Frame.transformCoordinates2d() به TEXTURE_NORMALIZED تغییر داد. هنگامی که تصویر عمق در یک شیدر قابل دسترسی است، این اندازه‌گیری‌های عمق را می‌توان مستقیماً برای کنترل انسداد مشاهده کرد.

درک مقادیر عمق

با توجه به نقطه A در هندسه دنیای واقعی مشاهده شده و یک نقطه 2 بعدی a همان نقطه را در تصویر عمق نشان می دهد، مقدار داده شده توسط Depth API در a برابر است با طول CA پیش بینی شده روی محور اصلی. این را می توان به عنوان مختصات z A نسبت به مبدا دوربین C نیز نام برد. هنگام کار با Depth API، درک این نکته مهم است که مقادیر عمق طول خود پرتو CA نیست، بلکه نمایش آن است.

از عمق در سایه زن استفاده کنید

اطلاعات عمق فریم فعلی را تجزیه کنید

از توابع کمکی DepthGetMillimeters() و DepthGetVisibility() در یک shader قطعه برای دسترسی به اطلاعات عمق برای موقعیت فعلی صفحه استفاده کنید. سپس از این اطلاعات برای مسدود کردن انتخابی بخش هایی از شی رندر شده استفاده کنید.

// 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 برای اهدافی غیر از مسدود کردن اشیاء مجازی یا تجسم داده‌های عمق، اطلاعات را از تصویر عمق استخراج کنید.

جاوا

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

کاتلین

/** 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:

جاوا

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

کاتلین

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 برای مختصات تصویر عمق:

جاوا

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

کاتلین

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

آزمون ضربه عمق

تست ضربه به کاربران اجازه می دهد تا اشیاء را در یک مکان واقعی در صحنه قرار دهند. پیش از این، آزمایش‌های ضربه‌ای فقط در هواپیماهای شناسایی‌شده انجام می‌شد و مکان‌ها را به سطوح بزرگ و مسطح محدود می‌کرد، مانند نتایج نشان‌داده‌شده توسط اندرویدهای سبز. تست‌های ضربه عمقی از اطلاعات عمق صاف و خام برای ارائه نتایج دقیق‌تر، حتی در سطوح غیرمسطح و با بافت کم بهره می‌برند. این با اندرویدهای قرمز نشان داده شده است.

برای استفاده از تست‌های ضربه‌ای با قابلیت عمق، hitTest() را فراخوانی کنید و DepthPoints را در لیست برگشتی بررسی کنید.

جاوا

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

کاتلین

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

بعدش چی