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

1. מבוא

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

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

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

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

מה תפַתחו

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

  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 ובוחרים באפשרות פתיחת פרויקט קיים של Android Studio.
  2. עוברים לספרייה המקומית שבה אחסנתם את קובץ ה-Raw Depth ZIP.
  3. לוחצים לחיצה כפולה על הספרייה arcore_rawdepthapi_codelab.

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

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

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

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

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

4. הפעלת האפליקציה לתחילת פעולה

כדי להפעיל את האפליקציה Raw Depth Starter, יש לפעול לפי השלבים הבאים.

  1. עוברים אל Run > הפעלה... &gt; 'part0_work'
  2. בתיבת הדו-שיח Select Deployment Target בוחרים את המכשיר מהרשימה מכשירים מחוברים ולוחצים על OK.

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

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

בשלב הזה, האפליקציה לא עושה דבר.זהו אפליקציית ה-AR הבסיסית ביותר, שמציגה תצוגת מצלמה של הסצנה, אבל לא עושה שום דבר אחר.הקוד הקיים דומה לדוגמת ה-AR של Hello שפורסמה עם 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.";
}

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

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

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 על ידי הוספת method create() בתחתית המחלקה. השיטה הזו מפעילה שאילתות על תמונות העומק והמהימנות העדכניות ביותר, ומאחסנת את ה-pointcloud שנוצר. תמונות העומק והמהימנות יכילו נתונים תואמים.

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

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

מוסיפים את שיטת המסייע הבאה ל-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)

עכשיו, אחרי שסיימתם לשחק ב-pointcloud, הגיע הזמן לראות איך הנתונים נראים במסך.

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

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

קודם כל, מוסיפים מחלקה חדשה שתכיל את לוגיקת הרינדור. הכיתה הזו מבצעת את פעולות OpenGL כדי לאתחל תוכנות הצללה (shader) כדי להציג באופן חזותי את Cloud pointcloud.

הוספת מחלקה של 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");
    }
}

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

לאחר מכן, מציינים את המקור של תוכנות הצללה לרינדור. מוסיפים את ה-method הבא update() בתחתית המחלקה DepthRenderer. השיטה הזו מקבלת את פרטי העומק העדכניים כקלט, ומעתיקה את נתוני ה-pointcloud ל-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 פיקסלים באפליקציה לדוגמה.

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

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

הוספה של תוכנות הצללה חדשות .vert ו-.frag לספרייה src/main/assets/shaders/.

הוספת תוכנת הצללה (shader) חדשה מסוג .vert

ב-Android Studio:

  1. לחיצה ימנית על ספריית תוכנות ההצללה
  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;
}

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

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

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

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

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

צריך להוסיף את הקוד הבא בסוף הבלוק של ההתנסות בתוך onDrawFrame כדי להציג את העומק העדכני של המסגרת הנוכחית.

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

אחרי השינויים האלה, האפליקציה אמורה להתפתח בהצלחה ולהציג את Cloud pointcloud.

דוגמה להמחשה חזותית של עומק RAW ב-pointcloud

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

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

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

ביטול התוקף של פיקסלים ברמת סמך נמוכה

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

הערכים של confidenceNormalized נעים בין 0 ל-1, כאשר 0 מציין רמת סמך נמוכה ו-1 מציין רמת סמך מלאה. משנים את ה-method 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 *********

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

רווח בר-סמך >= 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;
 }

עכשיו יוצגו רק נקודות ודאות גבוהות ונקודות קרוב.

סינון מרחקים

הגבלת ה-pointcloud להיות בטווח של 1.5 מטר מהמצלמה.

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

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

בשלב הזה יישאר רק "לא מישור" נקודות שנוטים לייצג משטחים על אובייקטים בסביבה. מוסיפים את השיטה 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);
            }
        }
    }

אפשר להוסיף את השיטה הזו ל-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 הזה מכיל אלגוריתם אשכולות של Pointcloud מאוד פשוט. מעדכנים את ה-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();

וב-method onSurfaceCreated(), מוסיפים את הקוד הבא לצד כלי הרינדור האחרים:

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. מחברים מכשיר שתומך ב-ARCore באמצעות USB.
  2. כדי להריץ את הפרויקט, לוחצים על הלחצן ► בסרגל התפריטים.
  3. ממתינים להשלמת תהליך היצירה של האפליקציה ופריסת הפריסה במכשיר.

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

הפעלת ניפוי באגים ב-USB

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

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

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

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

9. מזל טוב

כל הכבוד! יצרת בהצלחה את אפליקציית המציאות הרבודה (Augmented Reality) הראשונה שלך, שמבוססת על עומק, באמצעות ARCore Raw Depth API של Google. אנחנו מצפים בקוצר רוח לראות את היצירה שלכם.

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

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

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

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

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

cd <path to Android SDK>

tools/bin/sdkmanager --licenses

שאלות נפוצות