עומק גולמי של ARCore

1. מבוא

ARCore היא פלטפורמה ליצירת אפליקציות של מציאות רבודה (AR) במכשירים ניידים. ARCore Depth API של Google מספק גישה לתמונת עומק לכל פריים בסשן של ARCore. כל פיקסל בתמונת העומק מספק מדידת מרחק מהמצלמה לסביבה.

ממשק ה-API של Raw Depth מספק תמונות עומק שלא עוברות סינון במרחב המסך, שנועד להחליק את התוצאות ולבצע אינטרפולציה שלהן. הערכים האלה מדויקים יותר מבחינה גיאומטרית, אבל יכול להיות שהם יכללו נתונים חסרים ויהיו פחות תואמים לתמונה מהמצלמה המשויכת.

Codelab זה מציג כיצד להשתמש ב-Raw Depth API כדי לבצע ניתוח גיאומטרי תלת-ממדי של הסצנה. תבנו אפליקציה פשוטה עם תכונות AR שמשתמשת בנתוני עומק גולמיים כדי לזהות ולדמיין את הגיאומטריה של העולם.

ממשקי ה-API של עומק ועומק גולמי נתמכים רק בחלק מהמכשירים עם ARCore. ה-API של עומק זמין רק ב-Android.

מה תפַתחו

ב-Codelab הזה תלמדו איך ליצור אפליקציה שמשתמשת בתמונות עומק RAW לכל פריים כדי לבצע ניתוח גיאומטרי של העולם שסביבכם. האפליקציה הזו:

  1. בודקים אם מכשיר היעד תומך בעומק.
  2. שליפת תמונת העומק הגולמית לכל פריים של המצלמה.
  3. מבצעים הקרנה מחדש של תמונות עומק גולמיות לנקודות תלת-ממדיות ומסננים את הנקודות האלה על סמך רמת הביטחון והגיאומטריה.
  4. אפשר להשתמש בענן הנקודות של עומק הגולמי כדי לפלח אובייקטים תלת-ממדיים שמעניינים אתכם.

תצוגה מקדימה של מה שתבנו.

הערה: אם נתקלים בבעיות במהלך התהליך, אפשר לעבור לקטע האחרון כדי לקבל טיפים לפתרון בעיות.

2. דרישות מוקדמות

כדי להשלים את ה-Codelab הזה, תצטרכו חומרה ותוכנה ספציפיות.

דרישות חומרה

  • מכשיר שתומך ב-ARCore עם ניפוי באגים ב-USB שמופעל, שמחובר למחשב הפיתוח באמצעות כבל USB. המכשיר צריך לתמוך גם ב-Depth API.

דרישות תוכנה

3. הגדרה

הגדרת מחשב הפיתוח

מחברים את מכשיר ARCore למחשב באמצעות כבל USB. מוודאים שהמכשיר מאפשר ניפוי באגים ב-USB. פותחים טרמינל ומריצים את הפקודה adb devices, כמו שמוצג בהמשך:

adb devices

List of devices attached
<DEVICE_SERIAL_NUMBER>    device

הערך <DEVICE_SERIAL_NUMBER> יהיה מחרוזת ייחודית למכשיר שלכם. לפני שממשיכים, חשוב לוודא שמופיע מכשיר אחד בלבד.

הורדה והתקנה של הקוד

אפשר לשכפל את המאגר:

git clone https://github.com/googlecodelabs/arcore-rawdepthapi

אפשר גם להוריד קובץ ZIP ולחלץ אותו:

כדי להתחיל לעבוד עם הקוד, פועלים לפי השלבים הבאים.

  1. מפעילים את Android Studio ובוחרים באפשרות Open an existing Android Studio project (פתיחת פרויקט קיים של Android Studio).
  2. עוברים לספרייה המקומית שבה שמרתם את קובץ ה-ZIP של נתוני העומק הגולמיים.
  3. לוחצים לחיצה כפולה על הספרייה arcore_rawdepthapi_codelab.

הספרייה arcore_rawdepthapi_codelab היא פרויקט Gradle יחיד עם כמה מודולים. אם החלונית Project בפינה הימנית העליונה של Android Studio לא מוצגת כבר בחלונית Project, לוחצים על Projects בתפריט הנפתח.

התוצאה אמורה להיראות כך:

הפרויקט הזה מכיל את המודולים הבאים:

  • part0_work: אפליקציית המתחילים. צריך לערוך את המודול הזה כשעובדים עם ה-codelab הזה. כל שאר החלקים מכילים קוד הפניה.
  • part1: קוד לדוגמה שמראה איך העריכות צריכות להיראות אחרי שמסיימים את חלק 1.
  • part2: קוד הפניה כשמשלימים את חלק 2.
  • part3_completed: קוד הפניה כשמשלימים את חלק 3, שהוא סוף ה-codelab.

תעבדו במודול part0_work. יש גם פתרונות מלאים לכל חלק ב-Codelab. כל מודול הוא אפליקציה שאפשר לבנות.

4. הפעלת האפליקציה לתחילת הדרך

כדי להריץ את אפליקציית המתחילים Raw Depth, פועלים לפי השלבים הבאים.

  1. עוברים אל Run > Run...‎ > 'part0_work'.
  2. בתיבת הדו-שיח Select Deployment Target (בחירת יעד פריסה), בוחרים את המכשיר מהרשימה Connected Devices (מכשירים מחוברים) ולוחצים על OK (אישור).

‫Android Studio ייצור את האפליקציה הראשונית ויפעיל אותה במכשיר.

כשמפעילים את האפליקציה בפעם הראשונה, היא מבקשת הרשאת גישה למצלמה. מקישים על אישור כדי להמשיך.

בשלב הזה, האפליקציה לא עושה כלום.זו אפליקציית ה-AR הבסיסית ביותר, שמציגה שידור מהמצלמה של הסצנה, אבל לא עושה שום דבר אחר.הקוד הקיים דומה לדוגמה Hello AR שפורסמה עם ARCore SDK.

בשלב הבא, תשתמשו ב-Raw Depth API כדי לאחזר את הגיאומטריה של הסצנה שסביבכם.

5. הגדרת Raw Depth API (חלק 1)

מוודאים שמכשיר היעד תומך בנתוני עומק

לא בכל המכשירים שתומכים ב-ARCore אפשר להריץ את Depth API. לפני שמוסיפים פונקציונליות לאפליקציה בתוך הפונקציה onResume() של RawDepthCodelabActivity.java, שבה נוצר סשן חדש, צריך לוודא שמכשיר היעד תומך במידע על עומק.

איך מוצאים את הקוד הקיים:

// Create the ARCore session.
session = new Session(/* context= */ this);

כדאי לעדכן את האפליקציה כדי לוודא שהיא פועלת רק במכשירים שתומכים ב-Depth API.

// Create the ARCore session.
session = new Session(/* context= */ this);
if (!session.isDepthModeSupported(Config.DepthMode.RAW_DEPTH_ONLY)) {
  message =
     "This device does not support the ARCore Raw Depth API. See" +
     "https://developers.google.com/ar/devices for 
     a list of devices that do.";
}

הפעלת נתוני עומק גולמיים

ממשק ה-API של עומק גולמי מספק תמונת עומק לא מוחלקת ותמונת מהימנות תואמת שמכילה את מהימנות העומק של כל פיקסל בתמונת העומק הגולמי. כדי להפעיל את העומק הגולמי, מעדכנים את הקוד הבא מתחת להצהרת try-catch ששיניתם זה עתה.

try {
  // ************ New code to add ***************
  // Enable raw depth estimation and auto focus mode while ARCore is running.
  Config config = session.getConfig();
  config.setDepthMode(Config.DepthMode.RAW_DEPTH_ONLY);
  config.setFocusMode(Config.FocusMode.AUTO);
  session.configure(config);
  // ************ End new code to add ***************
  session.resume();
} catch (CameraNotAvailableException e) {
  messageSnackbarHelper.showError(this, "Camera not available. Try restarting the app.");
  session = null;
  return;
}

עכשיו סשן ה-AR מוגדר בצורה מתאימה, והאפליקציה יכולה להשתמש בתכונות שמבוססות על עומק.

שליחת קריאה ל-Depth API

לאחר מכן, קוראים ל-Depth API כדי לאחזר תמונות עומק לכל פריים. כדי ליצור קובץ חדש, צריך להוסיף את נתוני העומק למחלקה חדשה. לוחצים לחיצה ימנית על התיקייה rawdepth ובוחרים באפשרות New > Java Class. כך נוצר קובץ ריק. מוסיפים לכיתה את הפריטים הבאים:

src/main/java/com/google/ar/core/codelab/rawdepth/DepthData.java

package com.google.ar.core.codelab.rawdepth;

import android.media.Image;
import android.opengl.Matrix;

import com.google.ar.core.Anchor;
import com.google.ar.core.CameraIntrinsics;
import com.google.ar.core.Frame;
import com.google.ar.core.exceptions.NotYetAvailableException;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;

/**
 * Convert depth data from ARCore depth images to 3D pointclouds. Points are added by calling the
 * Raw Depth API, and reprojected into 3D space.
 */
public class DepthData {
    public static final int FLOATS_PER_POINT = 4; // X,Y,Z,confidence.

}

הכיתה הזו משמשת להמרת תמונות עומק לענני נקודות. ענני נקודות מייצגים את הגיאומטריה של הסצנה באמצעות רשימה של נקודות, שלכל אחת מהן יש קואורדינטה תלת-ממדית (x,‏ y,‏ z) וערך מהימנות בטווח 0 עד 1.

כדי לאכלס את הערכים האלה באמצעות Raw Depth API, מוסיפים את השיטה create() בתחתית המחלקה. בשיטה הזו מתבצעת שאילתה לגבי תמונות העומק והמהימנות העדכניות, והתוצאה היא ענן נקודות שמאוחסן. הנתונים בתמונות העומק והמהימנות יהיו זהים.

public static FloatBuffer create(Frame frame, Anchor cameraPoseAnchor) {
    try {
        Image depthImage = frame.acquireRawDepthImage16Bits();
        Image confidenceImage = frame.acquireRawDepthConfidenceImage();

        // Retrieve the intrinsic camera parameters corresponding to the depth image to
        // transform 2D depth pixels into 3D points. See more information about the depth values
        // at
        // https://developers.google.com/ar/develop/java/depth/overview#understand-depth-values.

        final CameraIntrinsics intrinsics = frame.getCamera().getTextureIntrinsics();
        float[] modelMatrix = new float[16];
        cameraPoseAnchor.getPose().toMatrix(modelMatrix, 0);
        final FloatBuffer points = convertRawDepthImagesTo3dPointBuffer(
                depthImage, confidenceImage, intrinsics, modelMatrix);

        depthImage.close();
        confidenceImage.close();

        return points;
    } catch (NotYetAvailableException e) {
        // This normally means that depth data is not available yet.
        // This is normal, so you don't have to spam the logcat with this.
    }
    return null;
}

acquireCameraImage()

acquireDepthImage16Bits()

acquireRawDepthImage16Bits()

acquireRawDepthConfidenceImage()

בנוסף, הקוד שומר את העוגן של המצלמה בשלב הזה, כדי שאפשר יהיה להמיר את נתוני העומק לקואורדינטות עולמיות באמצעות קריאה לשיטת עזר convertRawDepthImagesTo3dPointBuffer(). שיטת העזר הזו לוקחת כל פיקסל בתמונת העומק ומשתמשת בפונקציות הפנימיות (intrinsics) של המצלמה כדי לבטל את ההטלה של העומק לנקודה תלת-ממדית ביחס למצלמה. לאחר מכן, נקודת העיגון של המצלמה משמשת להמרת מיקום הנקודה לקואורדינטות עולמיות. כל פיקסל קיים מומר לנקודה תלת-ממדית (ביחידות של מטרים) ומאוחסן לצד רמת הביטחון שלו.

מוסיפים את שיטת העזר הבאה לקובץ DepthData.java:

/** Apply camera intrinsics to convert depth image into a 3D pointcloud. */
    private static FloatBuffer convertRawDepthImagesTo3dPointBuffer(
            Image depth, Image confidence, CameraIntrinsics cameraTextureIntrinsics, float[] modelMatrix) {
        // Java uses big endian so change the endianness to ensure
        // that the depth data is in the correct byte order.
        final Image.Plane depthImagePlane = depth.getPlanes()[0];
        ByteBuffer depthByteBufferOriginal = depthImagePlane.getBuffer();
        ByteBuffer depthByteBuffer = ByteBuffer.allocate(depthByteBufferOriginal.capacity());
        depthByteBuffer.order(ByteOrder.LITTLE_ENDIAN);
        while (depthByteBufferOriginal.hasRemaining()) {
            depthByteBuffer.put(depthByteBufferOriginal.get());
        }
        depthByteBuffer.rewind();
        ShortBuffer depthBuffer = depthByteBuffer.asShortBuffer();

        final Image.Plane confidenceImagePlane = confidence.getPlanes()[0];
        ByteBuffer confidenceBufferOriginal = confidenceImagePlane.getBuffer();
        ByteBuffer confidenceBuffer = ByteBuffer.allocate(confidenceBufferOriginal.capacity());
        confidenceBuffer.order(ByteOrder.LITTLE_ENDIAN);
        while (confidenceBufferOriginal.hasRemaining()) {
            confidenceBuffer.put(confidenceBufferOriginal.get());
        }
        confidenceBuffer.rewind();

        // To transform 2D depth pixels into 3D points, retrieve the intrinsic camera parameters
        // corresponding to the depth image. See more information about the depth values at
        // https://developers.google.com/ar/develop/java/depth/overview#understand-depth-values.
        final int[] intrinsicsDimensions = cameraTextureIntrinsics.getImageDimensions();
        final int depthWidth = depth.getWidth();
        final int depthHeight = depth.getHeight();
        final float fx =
                cameraTextureIntrinsics.getFocalLength()[0] * depthWidth / intrinsicsDimensions[0];
        final float fy =
                cameraTextureIntrinsics.getFocalLength()[1] * depthHeight / intrinsicsDimensions[1];
        final float cx =
                cameraTextureIntrinsics.getPrincipalPoint()[0] * depthWidth / intrinsicsDimensions[0];
        final float cy =
                cameraTextureIntrinsics.getPrincipalPoint()[1] * depthHeight / intrinsicsDimensions[1];

        // Allocate the destination point buffer. If the number of depth pixels is larger than
        // `maxNumberOfPointsToRender` we uniformly subsample. The raw depth image may have
        // different resolutions on different devices.
        final float maxNumberOfPointsToRender = 20000;
        int step = (int) Math.ceil(Math.sqrt(depthWidth * depthHeight / maxNumberOfPointsToRender));

        FloatBuffer points = FloatBuffer.allocate(depthWidth / step * depthHeight / step * FLOATS_PER_POINT);
        float[] pointCamera = new float[4];
        float[] pointWorld = new float[4];

        for (int y = 0; y < depthHeight; y += step) {
            for (int x = 0; x < depthWidth; x += step) {
                // Depth images are tightly packed, so it's OK to not use row and pixel strides.
                int depthMillimeters = depthBuffer.get(y * depthWidth + x); // Depth image pixels are in mm.
                if (depthMillimeters == 0) {
                    // Pixels with value zero are invalid, meaning depth estimates are missing from
                    // this location.
                    continue;
                }
                final float depthMeters = depthMillimeters / 1000.0f; // Depth image pixels are in mm.

                // Retrieve the confidence value for this pixel.
                final byte confidencePixelValue =
                        confidenceBuffer.get(
                                y * confidenceImagePlane.getRowStride()
                                        + x * confidenceImagePlane.getPixelStride());
                final float confidenceNormalized = ((float) (confidencePixelValue & 0xff)) / 255.0f;

                // Unproject the depth into a 3D point in camera coordinates.
                pointCamera[0] = depthMeters * (x - cx) / fx;
                pointCamera[1] = depthMeters * (cy - y) / fy;
                pointCamera[2] = -depthMeters;
                pointCamera[3] = 1;

                // Apply model matrix to transform point into world coordinates.
                Matrix.multiplyMV(pointWorld, 0, modelMatrix, 0, pointCamera, 0);
                points.put(pointWorld[0]); // X.
                points.put(pointWorld[1]); // Y.
                points.put(pointWorld[2]); // Z.
                points.put(confidenceNormalized);
            }
        }

        points.rewind();
        return points;
    }

קבלת נתוני עומק גולמיים עדכניים לכל פריים

משנים את האפליקציה כך שתאחזר נתוני עומק ותתאים אותם לקואורדינטות עולמיות לכל תנוחה.

ב-RawDepthCodelabActivity.java, בשיטה onDrawFrame(), מחפשים את השורות הקיימות:

Frame frame = session.update();
Camera camera = frame.getCamera();

// If the frame is ready, render the camera preview image to the GL surface.
backgroundRenderer.draw(frame);

מוסיפים את השורות הבאות ממש מתחת:

// Retrieve the depth data for this frame.
FloatBuffer points = DepthData.create(frame, session.createAnchor(camera.getPose()));
if (points == null) {
  return;
}

if (messageSnackbarHelper.isShowing() && points != null) {
  messageSnackbarHelper.hide(this);
}

6. עיבוד נתוני העומק (חלק 2)

עכשיו שיש לכם ענן נקודות עומק שאפשר לעבוד איתו, הגיע הזמן לראות איך הנתונים נראים כשהם מוצגים במסך.

הוספת רכיב עיבוד להצגת נקודות עומק

מוסיפים רכיב עיבוד כדי להציג את נקודות העומק.

קודם מוסיפים מחלקה חדשה שתכיל את לוגיקת העיבוד. המחלקה הזו מבצעת את פעולות ה-OpenGL כדי לאתחל את ההצללות (shaders) לצורך הצגת ענן הנקודות של העומק.

הוספת המחלקה DepthRenderer

  1. לוחצים לחיצה ימנית על ספריית קובצי המקור rendering.
  2. יש לבחור באפשרות New > Java Class.
  3. נותנים לכיתה את השם DepthRenderer.

מאכלסים את המחלקה הזו באמצעות הקוד הבא:

src/main/java/com/google/ar/core/codelab/common/rendering/DepthRenderer.java

package com.google.ar.core.codelab.common.rendering;

import android.content.Context;
import android.opengl.GLES20;
import android.opengl.Matrix;

import com.google.ar.core.Camera;
import com.google.ar.core.codelab.rawdepth.DepthData;

import java.io.IOException;
import java.nio.FloatBuffer;

public class DepthRenderer {
    private static final String TAG = DepthRenderer.class.getSimpleName();

    // Shader names.
    private static final String VERTEX_SHADER_NAME = "shaders/depth_point_cloud.vert";
    private static final String FRAGMENT_SHADER_NAME = "shaders/depth_point_cloud.frag";

    public static final int BYTES_PER_FLOAT = Float.SIZE / 8;
    private static final int BYTES_PER_POINT = BYTES_PER_FLOAT * DepthData.FLOATS_PER_POINT;
    private static final int INITIAL_BUFFER_POINTS = 1000;

    private int arrayBuffer;
    private int arrayBufferSize;

    private int programName;
    private int positionAttribute;
    private int modelViewProjectionUniform;
    private int pointSizeUniform;

    private int numPoints = 0;

    public DepthRenderer() {}

    public void createOnGlThread(Context context) throws IOException {
        ShaderUtil.checkGLError(TAG, "Bind");

        int[] buffers = new int[1];
        GLES20.glGenBuffers(1, buffers, 0);
        arrayBuffer = buffers[0];
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, arrayBuffer);

        arrayBufferSize = INITIAL_BUFFER_POINTS * BYTES_PER_POINT;
        GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, arrayBufferSize, null, GLES20.GL_DYNAMIC_DRAW);
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);

        ShaderUtil.checkGLError(TAG, "Create");

        int vertexShader =
                ShaderUtil.loadGLShader(TAG, context, GLES20.GL_VERTEX_SHADER, VERTEX_SHADER_NAME);
        int fragmentShader =
                ShaderUtil.loadGLShader(TAG, context, GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER_NAME);

        programName = GLES20.glCreateProgram();
        GLES20.glAttachShader(programName, vertexShader);
        GLES20.glAttachShader(programName, fragmentShader);
        GLES20.glLinkProgram(programName);
        GLES20.glUseProgram(programName);

        ShaderUtil.checkGLError(TAG, "Program");

        positionAttribute = GLES20.glGetAttribLocation(programName, "a_Position");
        modelViewProjectionUniform = GLES20.glGetUniformLocation(programName, "u_ModelViewProjection");
        // Sets the point size, in pixels.
        pointSizeUniform = GLES20.glGetUniformLocation(programName, "u_PointSize");

        ShaderUtil.checkGLError(TAG, "Init complete");
    }
}

עיבוד נתוני העומק

לאחר מכן, מציינים את המקור של הצללות העיבוד. מוסיפים את update()השיטה הבאה בתחתית המחלקה DepthRenderer. בשיטה הזו, המידע העדכני על העומק משמש כקלט, ונתוני הענן של הנקודות מועתקים ל-GPU.

    /**
     * Update the OpenGL buffer contents to the provided point. Repeated calls with the same point
     * cloud will be ignored.
     */
    public void update(FloatBuffer points) {
        ShaderUtil.checkGLError(TAG, "Update");
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, arrayBuffer);

        // If the array buffer is not large enough to fit the new point cloud, resize it.
        points.rewind();
        numPoints = points.remaining() / DepthData.FLOATS_PER_POINT;
        if (numPoints * BYTES_PER_POINT > arrayBufferSize) {
            while (numPoints * BYTES_PER_POINT > arrayBufferSize) {
                arrayBufferSize *= 2;
            }
            GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, arrayBufferSize, null, GLES20.GL_DYNAMIC_DRAW);
        }

        GLES20.glBufferSubData(
                GLES20.GL_ARRAY_BUFFER, 0, numPoints * BYTES_PER_POINT, points);
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);

        ShaderUtil.checkGLError(TAG, "Update complete");
    }

כדי להציג את הנתונים האחרונים במסך, מוסיפים method‏ draw() לחלק התחתון של המחלקה DepthRenderer. בשיטה הזו, המידע של ענן הנקודות התלת-ממדי מוקרן בחזרה לשידור מהמצלמה כדי שאפשר יהיה לרנדר אותו במסך.

    /** Render the point cloud. The ARCore point cloud is given in world space. */
    public void draw(Camera camera) {
        float[] projectionMatrix = new float[16];
        camera.getProjectionMatrix(projectionMatrix, 0, 0.1f, 100.0f);
        float[] viewMatrix = new float[16];
        camera.getViewMatrix(viewMatrix, 0);
        float[] viewProjection = new float[16];
        Matrix.multiplyMM(viewProjection, 0, projectionMatrix, 0, viewMatrix, 0);

        ShaderUtil.checkGLError(TAG, "Draw");

        GLES20.glUseProgram(programName);
        GLES20.glEnableVertexAttribArray(positionAttribute);
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, arrayBuffer);
        GLES20.glVertexAttribPointer(positionAttribute, 4, GLES20.GL_FLOAT, false, BYTES_PER_POINT, 0);
        GLES20.glUniformMatrix4fv(modelViewProjectionUniform, 1, false, viewProjection, 0);
        // Set point size to 5 pixels.
        GLES20.glUniform1f(pointSizeUniform, 5.0f);

        GLES20.glDrawArrays(GLES20.GL_POINTS, 0, numPoints);
        GLES20.glDisableVertexAttribArray(positionAttribute);
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);

        ShaderUtil.checkGLError(TAG, "Draw complete");
    }

אפשר להגדיר את גודל הנקודה לגדלים שונים בפיקסלים באמצעות המשתנה pointSizeUniform. בדוגמה של האפליקציה, הערך של pointSizeUniform מוגדר ל-5 פיקסלים.

הוספת הצללות חדשות

יש הרבה דרכים להציג נתוני עומק באפליקציה. כאן תוכלו להוסיף כמה הצללות וליצור הדמיה פשוטה של מיפוי צבעים.

מוסיפים shaders חדשים של .vert ו-.frag לספרייה src/main/assets/shaders/.

הוספת shader חדש מסוג ‎ .vert

ב-Android Studio:

  1. לוחצים לחיצה ימנית על ספריית ה-shaders.
  2. בוחרים באפשרות 'חדש' -> 'קובץ'.
  3. צריך לתת שם ל-depth_point_cloud.vert
  4. מגדירים אותו כקובץ טקסט.

בקובץ ‎ .vert החדש, מוסיפים את הקוד הבא:

src/main/assets/shaders/depth_point_cloud.vert

uniform mat4 u_ModelViewProjection;
uniform float u_PointSize;

attribute vec4 a_Position;

varying vec4 v_Color;

// Return an interpolated color in a 6 degree polynomial interpolation.
vec3 GetPolynomialColor(in float x,
  in vec4 kRedVec4, in vec4 kGreenVec4, in vec4 kBlueVec4,
  in vec2 kRedVec2, in vec2 kGreenVec2, in vec2 kBlueVec2) {
  // Moves the color space a little bit to avoid pure red.
  // Removes this line for more contrast.
  x = clamp(x * 0.9 + 0.03, 0.0, 1.0);
  vec4 v4 = vec4(1.0, x, x * x, x * x * x);
  vec2 v2 = v4.zw * v4.z;
  return vec3(
    dot(v4, kRedVec4) + dot(v2, kRedVec2),
    dot(v4, kGreenVec4) + dot(v2, kGreenVec2),
    dot(v4, kBlueVec4) + dot(v2, kBlueVec2)
  );
}

// Return a smooth Percept colormap based upon the Turbo colormap.
vec3 PerceptColormap(in float x) {
  const vec4 kRedVec4 = vec4(0.55305649, 3.00913185, -5.46192616, -11.11819092);
  const vec4 kGreenVec4 = vec4(0.16207513, 0.17712472, 15.24091500, -36.50657960);
  const vec4 kBlueVec4 = vec4(-0.05195877, 5.18000081, -30.94853351, 81.96403246);
  const vec2 kRedVec2 = vec2(27.81927491, -14.87899417);
  const vec2 kGreenVec2 = vec2(25.95549545, -5.02738237);
  const vec2 kBlueVec2 = vec2(-86.53476570, 30.23299484);
  const float kInvalidDepthThreshold = 0.01;
  return step(kInvalidDepthThreshold, x) *
         GetPolynomialColor(x, kRedVec4, kGreenVec4, kBlueVec4,
                            kRedVec2, kGreenVec2, kBlueVec2);
}

void main() {
   // Color the pointcloud by height.
   float kMinHeightMeters = -2.0f;
   float kMaxHeightMeters = 2.0f;
   float normalizedHeight = clamp((a_Position.y - kMinHeightMeters) / (kMaxHeightMeters - kMinHeightMeters), 0.0, 1.0);
   v_Color = vec4(PerceptColormap(normalizedHeight), 1.0);
   gl_Position = u_ModelViewProjection * vec4(a_Position.xyz, 1.0);
   gl_PointSize = u_PointSize;
}

ה-shader הזה משתמש במיפוי הצבעים של Turbo כדי לשפר את ההצגה החזותית. הקוד מבצע את הפעולות הבאות:

  1. הפונקציה מחזירה את הגובה של כל נקודה (ציר ה-Y בקואורדינטות עולמיות).
  2. חישוב של צבע שמשויך לגובה (אדום=נמוך, כחול=גבוה).
  3. הפונקציה מחשבת את מיקום המסך של כל נקודה.
  4. מגדיר את הגודל (בפיקסלים) של כל נקודה, כפי שמוגדר בשיטה DepthRenderer.update().

יוצרים shader של פרגמנט באותה ספרייה ונותנים לו את השם depth_point_cloud.frag, תוך חזרה על אותם השלבים שמתוארים בקטע הזה.

אחר כך מוסיפים את הקוד הבא לקובץ החדש כדי לעבד כל נקודה כקודקוד יחיד בצבע אחיד, כפי שמוגדר ב-vertex shader.

src/main/assets/shaders/depth_point_cloud.frag

precision mediump float;
varying vec4 v_Color;

void main() {
    gl_FragColor = v_Color;
}

כדי להחיל את העיבוד הזה, מוסיפים קריאות למחלקה DepthRenderer בתוך RawDepthCodelabActivity.

src/main/java/com/google/ar/core/codelab/common/rendering/RawDepthCodelabActivity.java

import com.google.ar.core.codelab.common.rendering.DepthRenderer;

בחלק העליון של הכיתה, מוסיפים חבר פרטי לצד backgroundRenderer.

private final DepthRenderer depthRenderer = new DepthRenderer();

צריך לאתחל את depthRenderer בתוך RawDepthCodelabActivity.onSurfaceCreated(), בדיוק כמו את backgroundRenderer הקיים.

depthRenderer.createOnGlThread(/*context=*/ this);

מוסיפים את הקוד הבא בסוף של בלוק try-catch בתוך onDrawFrame כדי להציג את העומק האחרון של המסגרת הנוכחית.

// Visualize depth points.
depthRenderer.update(points);
depthRenderer.draw(camera);

בעקבות השינויים האלה, האפליקציה אמורה להיבנות בהצלחה ולהציג את ענן הנקודות של העומק.

דוגמה להמחשה של ענן נקודות עומק גולמי

  • כל נקודת דגימה צבועה לפי העומק שלה.
  • נקודות אדומות קרובות, נקודות ירוקות או כחולות רחוקות יותר
  • אפשר לראות נתונים חסרים או 'חורים' באזורים עם תכונות תמונה לא מספיקות, כמו קירות או תקרות לבנים ריקים.
  • אפשר לשנות את גודל הנקודה המעובדת על ידי שינוי השורה GLES20.glUniform1f(pointSizeUniform, 5.0f); בתוך DepthRenderer.draw(). בצד ימין מוצגים גדלי הנקודות 5 ו-10.

7. ניתוח ענני נקודות בתלת-ממד (חלק 3)

אפשר לנתח נתוני עומק אחרי שמוודאים שהם קיימים בסשן AR. כלי חשוב לניתוח עומק הוא ערך המהימנות של כל פיקסל. אפשר להשתמש בערכי מהימנות כדי לנתח ענני נקודות תלת-ממדיים.

פסילת פיקסלים עם רמת סמך נמוכה

אחזרת את ערך המהימנות של כל פיקסל עומק ושמרת אותו לצד כל נקודה בתוך DepthData, אבל עדיין לא השתמשת בו.

הערכים של confidenceNormalized נעים בין 0 ל-1, כאשר 0 מציין מהימנות נמוכה ו-1 מציין מהימנות מלאה. משנים את השיטה convertRawDepthImagesTo3dPointBuffer() במחלקה DepthData כדי למנוע שמירה של פיקסלים שרמת המהימנות שלהם נמוכה מדי ולא שימושית.

final float confidenceNormalized = ((float) (confidencePixelValue & 0xff)) / 255.0f;

// ******** New code to add ************
if (confidenceNormalized < 0.3) {
   // Ignores "low-confidence" pixels.
   continue;
}
// ******** End of new code to add *********

כדאי לנסות ערכי סף שונים לרמת המהימנות כדי לראות כמה נקודות עומק נשמרות בכל רמה.

Confidence >= 0.1

רווח בר-סמך >= 0.3

רמת הסמך >= 0.5

רמת ודאות >= 0.7

רווח בר-סמך >= 0.9

סינון פיקסלים לפי מרחק

אפשר גם לסנן פיקסלים של עומק לפי מרחק. השלבים הבאים מתייחסים לגיאומטריה קרוב למצלמה. כדי לבצע אופטימיזציה של הביצועים, אפשר להתעלם מנקודות שמרוחקות מדי.

מעדכנים את הקוד לבדיקת רמת הביטחון שהוספתם עם הקוד הבא:

src/main/java/com/google/ar/core/codelab/rawdepth/DepthData.java

if (confidenceNormalized < 0.3 || depthMeters > 1.5) {
    // Ignore "low-confidence" pixels or depth that is too far away.
   continue;
 }

עכשיו יוצגו רק נקודות שקרובות למיקום שלכם ונקודות שרמת המהימנות שלהן גבוהה.

סינון לפי מרחק

מגביל את ענן הנקודות למרחק של עד 1.5 מטרים מהמצלמה.

השוואה בין נקודות ומישורים בתלת-ממד

אפשר להשוות בין נקודות ומישורים גיאומטריים בתלת-ממד ולהשתמש בהם כדי לסנן אחד את השני, למשל להסיר נקודות שקרובות למישורי AR שנצפו.

בשלב הזה יישארו רק נקודות 'לא מישוריות' שמייצגות בדרך כלל משטחים באובייקטים בסביבה. מוסיפים את ה-method‏ filterUsingPlanes() לתחתית המחלקה DepthData. בשיטה הזו מתבצעת איטרציה בין הנקודות הקיימות, כל נקודה נבדקת מול כל מישור, וכל נקודה שקרובה מדי למישור AR נפסלת. כך נשארים אזורים לא מישוריים שמדגישים אובייקטים בסצנה.

src/main/java/com/google/ar/core/codelab/rawdepth/DepthData.java

    public static void filterUsingPlanes(FloatBuffer points, Collection<Plane> allPlanes) {
        float[] planeNormal = new float[3];

        // Allocate the output buffer.
        int numPoints = points.remaining() / DepthData.FLOATS_PER_POINT;

        // Check each plane against each point.
        for (Plane plane : allPlanes) {
            if (plane.getTrackingState() != TrackingState.TRACKING || plane.getSubsumedBy() != null) {
                continue;
            }

            // Compute the normal vector of the plane.
            Pose planePose = plane.getCenterPose();
            planePose.getTransformedAxis(1, 1.0f, planeNormal, 0);

            // Filter points that are too close to the plane.
            for (int index = 0; index < numPoints; ++index) {
                // Retrieves the next point.
                final float x = points.get(FLOATS_PER_POINT * index);
                final float y = points.get(FLOATS_PER_POINT * index + 1);
                final float z = points.get(FLOATS_PER_POINT * index + 2);

                // Transform point to be in world coordinates, to match plane info.
                float distance = (x - planePose.tx()) * planeNormal[0]
                        + (y - planePose.ty()) * planeNormal[1]
                        + (z - planePose.tz()) * planeNormal[2];
                // Controls the size of objects detected.
                // Smaller values mean smaller objects will be kept.
                // Larger values will only allow detection of larger objects, but also helps reduce noise.
                if (Math.abs(distance) > 0.03) {
                    continue;  // Keep this point, since it's far enough away from the plane.
                }

                // Invalidate points that are too close to planar surfaces.
                points.put(FLOATS_PER_POINT * index, 0);
                points.put(FLOATS_PER_POINT * index + 1, 0);
                points.put(FLOATS_PER_POINT * index + 2, 0);
                points.put(FLOATS_PER_POINT * index + 3, 0);
            }
        }
    }

אפשר להוסיף את ה-method הזה ל- RawDepthCodelabActivity ב-method onDrawFrame:

//  ********** New code to add ************
  // Filter the depth data.
  DepthData.filterUsingPlanes(points, session.getAllTrackables(Plane.class));
//  ********** End new code to add *******

  // Visualize depth points.
  depthRenderer.update(points);
  depthRenderer.draw(camera);

אם מריצים את ה-Codelab עכשיו, מוצג רק חלק מהנקודות. הנקודות האלה מייצגות את האובייקטים בסצנה, בלי להתייחס למשטחים השטוחים שעליהם האובייקטים מונחים. אפשר להשתמש בנתונים האלה כדי להעריך את הגודל והמיקום של אובייקטים על ידי קיבוץ נקודות יחד.

תה

מיקרופון

אוזניות

כרית

נקודות באשכול

ה-Codelab הזה מכיל אלגוריתם פשוט מאוד של אשכולות ענני נקודות. מעדכנים את ה-codelab כדי לקבץ את ענני הנקודות שאוחזרו לאשכולות שמוגדרים על ידי תיבות תוחמות שמוגדרות לפי צירים.

src/main/java/com/google/ar/core/codelab/rawdepth/RawDepthCodelabActivity.java

import com.google.ar.core.codelab.common.helpers.AABB;
import com.google.ar.core.codelab.common.helpers.PointClusteringHelper;
import com.google.ar.core.codelab.common.rendering.BoxRenderer;
import java.util.List;

מוסיפים BoxRenderer לכיתה הזו בחלק העליון של הקובץ, עם שאר הרכיבים שמעבדים את התצוגה.

private final BoxRenderer boxRenderer = new BoxRenderer();

ובתוך השיטה onSurfaceCreated(), מוסיפים את השורה הבאה לצד שאר רכיבי ה-renderer:

boxRenderer.createOnGlThread(/*context=*/this);

לבסוף, מוסיפים את השורות הבאות ל-onDrawFrame() בתוך RawDepthCodelabActivity כדי לקבץ את העננים של הנקודות שאוחזרו לאשכולות ולעבד את התוצאות כתיבות תוחמות שמוגדרות בהתאם לצירים.

      // Visualize depth points.
      depthRenderer.update(points);
      depthRenderer.draw(camera);

// ************ New code to add ***************

      // Draw boxes around clusters of points.
      PointClusteringHelper clusteringHelper = new PointClusteringHelper(points);
      List<AABB> clusters = clusteringHelper.findClusters();
      for (AABB aabb : clusters) {
        boxRenderer.draw(aabb, camera);
      }

// ************ End new code to add ***************

תה

מיקרופון

אוזניות

כרית

עכשיו אפשר לאחזר את נתוני העומק הגולמיים באמצעות סשן ARCore, להמיר את נתוני העומק לענני נקודות תלת-ממדיים ולבצע פעולות סינון ועיבוד בסיסיות על הנקודות האלה.

8. Build-Run-Test

מפתחים, מריצים ובודקים את האפליקציה.

איך יוצרים ומריצים את האפליקציה

כדי ליצור ולהפעיל את האפליקציה:

  1. מחברים באמצעות USB מכשיר שתומך ב-ARCore.
  2. מריצים את הפרויקט באמצעות הלחצן ► בסרגל התפריטים.
  3. ממתינים עד שהאפליקציה תיבנה ותיפרס במכשיר.

בפעם הראשונה שמנסים לפרוס את האפליקציה במכשיר, צריך

אישור לניפוי באגים ב-USB

במכשיר. לוחצים על 'אישור' כדי להמשיך.

בפעם הראשונה שמריצים את האפליקציה במכשיר, תתבקשו לאשר לאפליקציה להשתמש במצלמה של המכשיר. כדי להמשיך להשתמש בתכונות ה-AR, צריך לאשר גישה.

בדיקת האפליקציה

כשמריצים את האפליקציה, אפשר לבדוק את ההתנהגות הבסיסית שלה על ידי החזקת המכשיר, תנועה במרחב וסריקה איטית של אזור מסוים. כדאי לנסות לאסוף נתונים למשך 10 שניות לפחות ולסרוק את האזור מכמה כיוונים לפני שממשיכים לשלב הבא.

9. מזל טוב

מזל טוב! הצלחת ליצור ולהפעיל את האפליקציה הראשונה שלך למציאות רבודה שמבוססת על עומק, באמצעות Google's ARCore Raw Depth API. אנחנו סקרנים לראות מה תבנו!

10. פתרון בעיות

הגדרת מכשיר Android לפיתוח

  1. מחברים את המכשיר למחשב הפיתוח באמצעות כבל USB. אם אתם מפתחים באמצעות Windows, יכול להיות שתצטרכו להתקין את דרייבר של התקן USB המתאים למכשיר שלכם.
  2. כדי להפעיל את ניפוי באגים ב-USB בחלון אפשרויות למפתחים:
  • פותחים את אפליקציית ההגדרות.
  • אם במכשיר שלכם פועלת גרסה Android v8.0 ואילך, בוחרים באפשרות מערכת.
  • גוללים לחלק התחתון של המסך ובוחרים באפשרות מידע על הטלפון.
  • גוללים לתחתית המסך ומקישים שבע פעמים על מספר Build.
  • חוזרים למסך הקודם, גוללים לתחתית ולוחצים על אפשרויות למפתחים.
  • בחלון אפשרויות למפתחים, גוללים למטה כדי למצוא את האפשרות ניפוי באגים ב-USB ומפעילים אותה.

מידע מפורט יותר על התהליך הזה זמין באתר המפתחים של Android.

אם נתקלתם בכשל בבנייה שקשור לרישיונות (Failed to install the following Android SDK packages as some licences have not been accepted), אתם יכולים להשתמש בפקודות הבאות כדי לבדוק את הרישיונות האלה ולאשר אותם:

cd <path to Android SDK>

tools/bin/sdkmanager --licenses

שאלות נפוצות