ARCore

主要功能
运动跟踪:让手机可以理解和跟踪它相对于现实世界的位置。

环境理解:让手机可以检测各类表面(例如地面、咖啡桌或墙壁等水平、垂直和倾斜表面)的大小和位置。

光估测:让手机可以估测环境当前的光照条件。

增强图像:摄像头视野中检测到图像时,告诉您这些图像在 AR 会话中的物理位置。

面部识别:摄像头视野中检测到人脸时,告诉您人脸在 AR 会话中的物理位置及相关信息。
 

 

https:///Terran-Marine/ARCoreMeasuredDistance

 

Android 使用Arcore 实现多点测距
已更新第二版,详情见github链接github源码 点这里 <==
主要使用了Anchor(锚点),Pose (姿势/姿态),Node(节点),Vector3(三维向量)

github源码 点这里 <==

1.准备

一台支持Arcore的手机
 依赖arcore和sceneform
     implementation ':core:1.5.0'
     implementation '.sceneform:core:1.5.0'
     implementation '.sceneform.ux:sceneform-ux:1.5.0'
 1
 2
 3
 -布局文件使用sceneform提供的fragment      <fragment
         android:id="@+id/UI_ArSceneView"
         android:name="com.gj.arcoredraw.MyArFragment"
         android:layout_width="match_parent"
         android:layout_height="match_parent" />
 1
 2
 3
 4
 5
 import com.blankj.utilcode.util.ToastUtils;
 import .core.exceptions.UnavailableApkTooOldException;
 import .core.exceptions.UnavailableArcoreNotInstalledException;
 import .core.exceptions.UnavailableDeviceNotCompatibleException;
 import .core.exceptions.UnavailableException;
 import .core.exceptions.UnavailableSdkTooOldException;
 import .sceneform.ux.ArFragment;//可以直接使用ArFragment   我这里为了中文提示
 public class MyArFragment extends ArFragment {
     @Override
     protected void handleSessionException(UnavailableException sessionException) {
         String message;
         if (sessionException instanceof UnavailableArcoreNotInstalledException) {
             message = "请安装ARCore";
         } else if (sessionException instanceof UnavailableApkTooOldException) {
             message = "请升级ARCore";
         } else if (sessionException instanceof UnavailableSdkTooOldException) {
             message = "请升级app";
         } else if (sessionException instanceof UnavailableDeviceNotCompatibleException) {
             message = "当前设备部不支持AR";
         } else {
             message = "未能创建AR会话,请查看机型适配,arcore版本与系统版本";
             String var3 = String.valueOf(sessionException);
         }
         ToastUtils.showLong(message);
     }
 }
 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 2.监听点击 生成锚点
 **设置ArFragment的Tap监听 **
         (UI_ArSceneView as MyArFragment).setOnTapArPlaneListener { hitResult, plane, motionEvent ->
              val currentAnchor=hitResult.createAnchor()
         }1
 2
 3
 4
 3.计算两个锚点之间的距离
 val startPose = endAnchor.pose
 val endPose = startAnchor.pose
 val dx = startPose.tx() - endPose.tx()
 val dy = startPose.ty() - endPose.ty()
 val dz = startPose.tz() - endPose.tz()
 val length = Math.sqrt((dx * dx + dy * dy + dz * dz).toDouble())
 anchorInfoBean.dataText = "距离为${decimalFormat.format(length)}m"1
 2
 3
 4
 5
 6
 7
 8
 4.UI 划线 (两个锚点在ui上连接划线)
 private fun drawLine(firstAnchor: Anchor, secondAnchor: Anchor) {
     val firstAnchorNode = AnchorNode(firstAnchor)    val secondAnchorNode = AnchorNode(secondAnchor)
     firstAnchorNode.setParent((UI_ArSceneView as MyArFragment).arSceneView.scene)
     val firstWorldPosition = firstAnchorNode.worldPosition
     val secondWorldPosition = secondAnchorNode.worldPosition
     val difference = Vector3.subtract(firstWorldPosition, secondWorldPosition)
     val directionFromTopToBottom = difference.normalized()
     val rotationFromAToB = Quaternion.lookRotation(directionFromTopToBottom, Vector3.up())
     MaterialFactory.makeOpaqueWithColor(this@MainActivity, .sceneform.rendering.Color(0f, 191f, 255f))
             .thenAccept { material ->
                 val lineMode = ShapeFactory.makeCube(Vector3(0.01f, 0.01f, difference.length()), Vector3.zero(), material)
                 val lineNode = Node()
                 lineNode.setParent(firstAnchorNode)
                 lineNode.renderable = lineMode
                 lineNode.worldPosition = Vector3.add(firstWorldPosition, secondWorldPosition).scaled(0.5f)
                 lineNode.worldRotation = rotationFromAToB
             }
 }
 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 5.自定义Node 始终面向相机
  override fun onUpdate(p0: FrameTime?) {
         scene?.let { scene ->
             val cameraPosition = scene.camera.worldPosition
             val nodePosition = this@FaceToCameraNode.worldPosition
             val direction = Vector3.subtract(cameraPosition, nodePosition)
             this@FaceToCameraNode.worldRotation = Quaternion.lookRotation(direction, Vector3.up())
         }
     }
 1
 2
 3
 4
 5
 6
 7
 8
 UI这里有点复杂,主要难点在Vector3(空间向量)这里.用的是数学知识了.
 1.先用两个Anchor 获得 世界坐标(worldPosition)
 2.使用向量计算两点空间差
 3.使用起始Anchor ,生成一个Node.附加到arSceneView上
 4.使用MaterialFactory 创建一个Material.再创建一个 Node使用刚才的材质,附加到之前的Node上.github源码 点这里 <==
 
 
背景:需要识别物体,并且测量物体的尺寸
     思路:识别物体
1、百度云,阿里云等有很多识别,如果有API识别,就用吧,比较容易,具体接入可以看官方文档
2、另外一种方式就是opencv分析了。这一点我也在研究,我现在识别的代码就不贴了。
  物体尺寸测量
 由于我们是根据图片识别物体,所以返回的是某一帧的图片上的像素点A(x1,y1)到B(x2,y2)。要转换成空间坐标系的点。再计算距离。这里我用的基于安卓的Arcore来实现的。好吧,废话不多说,上代码,获取某一帧图,然后发送给识别检测的服务器
 在Activity中arFragment.getArSceneView().getScene().addOnUpdateListener(this::onUpdateFrame);
 onUpdataFrame:发送请求用的是okhttputil,在github上面搜一下就知道了
 第一步:frame.acquireCameraImage();获得某一帧图片
 第二步:OkHttpUtils.post()发送请求
 第三步:List<HitResult> hitResults1 = arFragment.getArSceneView().getArFrame().hitTest(positionResults.getPositionResults().get(0).getX(), positionResults.getPositionResults().get(0).getY()); 根据返回的像素点得到空间碰撞的HitResults
 第四步: showDistance(hitResults1.get(0), hitResults2.get(0));展示距离吧private void onUpdateFrame(FrameTime frameTime) {
     Frame frame = arFragment.getArSceneView().getArFrame();
     // If there is no frame, just return.
     if (frame == null) {
         return;
     }
 Image image = null;
 try {
     image = frame.acquireCameraImage();
     Bitmap bitmap = imageToBitmap(image);
     if (bitmap != null) {
         File file = getFile(bitmap);
         OkHttpUtils.post()
                 .addFile("file", "pic" + imageIndex + ".png", file)//
                 .url("xxxxx")
                 .build()
                 .execute(new Callback() {
                     @Override
                     public Object parseNetworkResponse(Response response, int id) throws Exception {
                         try {
                             String str = response.body().string();
                             positionResults = new Gson().fromJson(str, PositionResults.class);
                             return positionResults;
                         } catch (Exception e) {
                             e.printStackTrace();
                         }
                         return null;
                     }
                     @Override
                     public void onError(Call call, Exception e, int id) {
                         System.out.println("error!!!!!");
                     }
                     @Override
                     public void onResponse(Object response, int id) {
                         System.out.println("response");
                     }
                 });
                         List<HitResult> hitResults1 = arFragment.getArSceneView().getArFrame().hitTest(positionResults.getPositionResults().get(0).getX(),
                                 positionResults.getPositionResults().get(0).getY());
                         List<HitResult> hitResults2 = arFragment.getArSceneView().getArFrame().hitTest(positionResults.getPositionResults().get(1).getX(),
                                 positionResults.getPositionResults().get(1).getY());
                         if (hitResults1.size() > 0 && hitResults2.size() > 0) {
                             showDistance(hitResults1.get(0), hitResults2.get(0));
                         }
                     }
                 }
             } catch (Exception e) {
                 e.printStackTrace();
             } finally {
 //                    sendFlag=true;
                 if (null != image) {
                     image.close();
                 }
             }
 showDistance:private void showDistance(HitResult hitResult1, HitResult hitResult2) {
     Anchor anchor1 = hitResult1.createAnchor();
     AnchorNode firstAnchorNode = new AnchorNode(anchor1);    Anchor anchor2 = hitResult2.createAnchor();
     AnchorNode secondAnchorNode = new AnchorNode(anchor2);
     secondAnchorNode.setParent(arFragment.getArSceneView().getScene());    double ddx = (firstAnchorNode.getWorldPosition().x - secondAnchorNode.getWorldPosition().x);
     double ddy = (firstAnchorNode.getWorldPosition().y - secondAnchorNode.getWorldPosition().y);
     double ddz = (firstAnchorNode.getWorldPosition().z - secondAnchorNode.getWorldPosition().z);    float ndl = (float) Math.sqrt(ddx * ddx + ddy * ddy + ddz * ddz);
 Toast toast1 =
         Toast.makeText(this, "dl is" + dl + "M,firstV:(" + firstV.x + "," + firstV.y + "," + firstV.z + "),secendV:("
                         + secondV.x + "," + secondV.y + "," + secondV.z + ")",
                 Toast.LENGTH_LONG);
 toast1.setGravity(Gravity.CENTER, 0, 0);
 toast1.show();
 」但是这样还是不够直观,所以在两点之间画根直线吧
private void addLineBetweenPoints(Scene scene, Vector3 from, Vector3 to) {
     // prepare an anchor position
     Quaternion camQ = scene.getCamera().getWorldRotation();
     float[] f1 = new float[]{to.x, to.y, to.z};
     float[] f2 = new float[]{camQ.x, camQ.y, camQ.z, camQ.w};
     Pose anchorPose = new Pose(f1, f2);    // make an ARCore Anchor
     Anchor anchor = arFragment.getArSceneView().getSession().createAnchor(anchorPose);
     // Node that is automatically positioned in world space based on the ARCore Anchor.
     AnchorNode anchorNode = new AnchorNode(anchor);
     anchorNode.setParent(scene);    // Compute a line's length
     float lineLength = Vector3.subtract(from, to).length();    // Prepare a color
     Color colorOrange = new Color(android.graphics.Color.parseColor("#ffa71c"));    // 1. make a material by the color
     MaterialFactory.makeOpaqueWithColor(this, colorOrange)
             .thenAccept(material -> {
                 // 2. make a model by the material
                 ModelRenderable model = ShapeFactory.makeCylinder(0.0025f, lineLength,
                         new Vector3(0f, lineLength / 2, 0f), material);
                 model.setShadowReceiver(false);
                 model.setShadowCaster(false);                // 3. make node
                 Node node = new Node();
                 node.setRenderable(model);
                 node.setParent(anchorNode);                // 4. set rotation
                 final Vector3 difference = Vector3.subtract(to, from);
                 final Vector3 directionFromTopToBottom = difference.normalized();
                 final Quaternion rotationFromAToB =
                         Quaternion.lookRotation(directionFromTopToBottom, Vector3.up());
                 node.setWorldRotation(Quaternion.multiply(rotationFromAToB,
                         Quaternion.axisAngle(new Vector3(1.0f, 0.0f, 0.0f), 90)));
             });
 }


 

本方案使用目前最火的AR库,实现测量真实世界纸箱的体积。设备支持ARCore、ARKit即可!

简要:通过AR提供的识别平面的功能,找到箱子所在的平面;在平面上标出箱子底部的三个顶点,这三个顶点就能确认箱子的底部面积;通过滑动条调节测量绘制出立方体模型,立方体模型体积即实物的体积(AR库已经实现了虚拟世界和真实世界的1:1比例)。

实现步骤简要:
平面识别
这是AR库提供的功能,打开摄像头后,拿着手机对着桌面来回平移一小段距离,即可把平面识别出来。识别平面效率跟手机移动方式有关,因为AR库识别平面是通过处理画面特征点和三角测量运算出来的。要注意的是:目标平面最好是纹理图案比较复杂的,空白平面和反光平面都会加大识别难度;另外,AR库为了做三角测量计算,手机需要平移,手机原地自转是很难识别出平面的。

绘制底面
绘制立方体底面需要找到箱底三个顶点,找顶点方式很多,我们项目最终方案是通过深度学习的方式,自动找出箱子的顶点二维信息,通过一些简单算法能把二维坐标转化三维坐标。这里讲述最容易实现的方式,就是手动找顶点,Unity有发射线的方法,手触摸手机屏幕,从摄像头发出一条射线,射线射在平面上,击中平面的交点就是我们要找的三维点信息。用这种方式击中箱底三个顶点,找到顶点的三维坐标信息。这三个点就能构建出三维空间中立方体的底面。

确定高度
绘制出底面后,我们就可以计算箱底面积了,但我们要测的是箱子体积,所以还要知道箱子的高度。我们是有方法直接找到高度的,在这先留一手,讲述最容易实现的方法。使用简单的方式实现,就是通过滑动条来确定高度,自动赋予一个高度给立方体模型即可。可看演示视频的效果。

演示视频:

Youku:
视频地址:https://v.youku.com/v_show/id_XMzczNDc3ODUwOA==.html?spm=a2hzp.8244740.0.0

YouTube:
===============================================================

后续开发了 《乐测AR》 项目

这是一款结合了增强现实技术(AR)与人工智能技术(AI),提供规则物体与非规则物体的体积测量的手机端APP。

《乐测AR》应用了目前最火热的增强现实与人工智能技术,即AR与AI技术。用户使用手机摄像头拍摄周边环境,通过AR的SLAM技术让手机理解真实环境,构建出虚拟的三维世界。同时,结合AI技术对图像进行处理,找到我们需要测量的物体(如纸箱)在虚拟三维世界的成像,里面包括该物体的位置与形状信息。最后根据这些信息进行三维重构,恢复出物体的形状,最终计算出该物体的体积。