Search code examples
androidpngarcoresceneform

Combine image with video stream on Android


I am investigating Augmented Reality on Android.

I am using ARCore and Sceneform within an Android application.

I have tried out the sample projects and now would like to develop my own application.

One effect I would like to achieve is to combine/overlay an image (say .jpeg or .png) with a live feed from the devices onboard camera.

The image will have a transparent background that allows the user to see the live feed and image simultaneously

However I do not want the overlayed image to be a fixed/static watermark, When the user zooms in, out or pans the overlayed image must also zoom in, out and pan etc.

I do not wish the overplayed image to become 3d or anything of that nature.

Is this effect possible with Sceneform? or will I need to use other 3rd party libraries and/or tools to achieve the desired results.

UPDATE

The user is drawing on a blank sheet of white paper. The sheet of paper is orientated so that the user is comfortably drawing (either left or right handed). The user is free to move the sheet of paper while they complete their image.

An Android device is held above the sheet of paper filming the user drawing their selected image.

The live camera feed is being cast to a large TV or monitor screen.

To aid the user they have selected a static image to "trace" or "Copy".

This image is chosen on the Android device and is being combined with the live camera stream within the Android application.

The user can zoom in and out on their drawing and the combined live stream and selected static image will also zoom in and out, this will enable the user to make an accurate copy of the selected static image by drawing "Free Hand".

When the user looks directly at the sheet of paper, they only see their drawing.

When the user views the cast live stream of them drawing on the TV or monitor they see their drawing and the chosen static image superimposed. The user can control the transparency of the static image to assist them in making an accurate copy of it.


Solution

  • I think what you are looking for is to use AR to display an image so that the image stays in place, for example over a sheet of paper in order to act as a guide for drawing a copy of the image on the paper.

    There are 2 parts to this. First is to locate the sheet of paper, the second is to place the image over the paper and keep it there as the phone moves around.

    Locating the sheet of paper can be done just by detecting the plane with the paper (having some contrast, or pattern or something vs. a plain white sheet of paper will help), then tap on where the center of the page should be. This is done in the HelloSceneform sample.

    If you want to have a more accurate bounding of the paper, you could tap the 4 corners of the paper, and then create anchors there. To do this register a plane tapped listener in onCreate()

       arFragment.setOnTapArPlaneListener(this::onPlaneTapped);
    

    Then in onPlaneTapped, create the 4 anchorNodes. Once you have 4, initialize the drawing to be displayed.

    private void onPlaneTapped(HitResult hitResult, Plane plane, MotionEvent event) {
        if (cornerAnchors.size() != 4) {
            AnchorNode corner = createCornerNode(hitResult.createAnchor());
            arFragment.getArSceneView().getScene().addChild(corner);
            cornerAnchors.add(corner);
        }
    
        if (cornerAnchors.size() == 4 && drawingNode == null) {
            initializeDrawing();
        }
    }
    

    To initialize the drawing, create a Sceneform Texture from the bitmap or drawable. This can be from a resource or a file URL. You want the texture to show the whole image, and scale as the model holding it is resized.

    private void initializeDrawing() {
        Texture.Sampler sampler = Texture.Sampler.builder()
                .setWrapMode(Texture.Sampler.WrapMode.CLAMP_TO_EDGE)
                .setMagFilter(Texture.Sampler.MagFilter.NEAREST)
                .setMinFilter(Texture.Sampler.MinFilter.LINEAR_MIPMAP_LINEAR)
                .build();
        Texture.builder()
                .setSource(this, R.drawable.logo_google_developers)
                .setSampler(sampler)
                .build()
                .thenAccept(texture -> {
                    MaterialFactory.makeTransparentWithTexture(this, texture)
                            .thenAccept(this::buildDrawingRenderable);
                });
    }
    

    The model to hold the texture is just a flat quad sized to the smallest dimension between the corners. This is the same logic as laying out a quad using OpenGL.

    private void buildDrawingRenderable(Material material) {
    
        Integer[] indices = {
                0, 1, 3, 3, 1, 2
        };
    
        //Calculate the center of the corners.
        float min_x = Float.MAX_VALUE;
        float max_x = Float.MIN_VALUE;
        float min_z = Float.MAX_VALUE;
        float max_z = Float.MIN_VALUE;
        for (AnchorNode node : cornerAnchors) {
            float x = node.getWorldPosition().x;
            float z = node.getWorldPosition().z;
            min_x = Float.min(min_x, x);
            max_x = Float.max(max_x, x);
            min_z = Float.min(min_z, z);
            max_z = Float.max(max_z, z);
        }
    
        float width = Math.abs(max_x - min_x);
        float height = Math.abs(max_z - min_z);
        float extent = Math.min(width / 2, height / 2);
    
        Vertex[] vertices = {
                Vertex.builder()
                        .setPosition(new Vector3(-extent, 0, extent))
                        .setUvCoordinate(new Vertex.UvCoordinate(0, 1)) // top left
                        .build(),
                Vertex.builder()
                        .setPosition(new Vector3(extent, 0, extent))
                        .setUvCoordinate(new Vertex.UvCoordinate(1, 1)) // top right
                        .build(),
                Vertex.builder()
                        .setPosition(new Vector3(extent, 0, -extent))
                        .setUvCoordinate(new Vertex.UvCoordinate(1, 0)) // bottom right
                        .build(),
                Vertex.builder()
                        .setPosition(new Vector3(-extent, 0, -extent))
                        .setUvCoordinate(new Vertex.UvCoordinate(0, 0)) // bottom left
                        .build()
        };
    
        RenderableDefinition.Submesh[] submeshes = {
                RenderableDefinition.Submesh.builder().
                        setMaterial(material)
                        .setTriangleIndices(Arrays.asList(indices))
                        .build()
        };
    
        RenderableDefinition def = RenderableDefinition.builder()
                .setSubmeshes(Arrays.asList(submeshes))
    
                .setVertices(Arrays.asList(vertices)).build();
    
        ModelRenderable.builder().setSource(def)
                .setRegistryId("drawing").build()
                .thenAccept(this::positionDrawing);
    }
    

    The last part is to position the quad in the center of the corners, and create a Transformable node so the image can be nudged into position, rotated, or scaled to be the perfect size.

    private void positionDrawing(ModelRenderable drawingRenderable) {
    
    
        //Calculate the center of the corners.
        float min_x = Float.MAX_VALUE;
        float max_x = Float.MIN_VALUE;
        float min_z = Float.MAX_VALUE;
        float max_z = Float.MIN_VALUE;
        for (AnchorNode node : cornerAnchors) {
            float x = node.getWorldPosition().x;
            float z = node.getWorldPosition().z;
            min_x = Float.min(min_x, x);
            max_x = Float.max(max_x, x);
            min_z = Float.min(min_z, z);
            max_z = Float.max(max_z, z);
        }
    
        Vector3 center = new Vector3((min_x + max_x) / 2f,
                cornerAnchors.get(0).getWorldPosition().y, (min_z + max_z) / 2f);
    
        Anchor centerAnchor = null;
        Vector3 screenPt = arFragment.getArSceneView().getScene().getCamera().worldToScreenPoint(center);
        List<HitResult> hits = arFragment.getArSceneView().getArFrame().hitTest(screenPt.x, screenPt.y);
        for (HitResult hit : hits) {
            if (hit.getTrackable() instanceof Plane) {
                centerAnchor = hit.createAnchor();
                break;
            }
        }
    
        AnchorNode centerNode = new AnchorNode(centerAnchor);
        centerNode.setParent(arFragment.getArSceneView().getScene());
    
        drawingNode = new TransformableNode(arFragment.getTransformationSystem());
        drawingNode.setParent(centerNode);
        drawingNode.setRenderable(drawingRenderable);
    }