Search code examples
androidbitmapopengl-esperspectivecameravirtual-reality

How to transform an Android bitmap to wrap a cylinder and change perspective


I wrote a sample app that allows the Android user to take a picture and have the text content from a view as an overlay on the image and saved to a gallery album:

Text on Camera Image

What I would like to to is transform the text bitmap before joining the two images. Specifically, I'd like to make the text curve up on the sides (simulating wrapping around a cylinder), and make it larger at the top than the bottom (simulating a top down perspective), as illustrated here:

Transformed Text on Camera Image

There is no need to interpret the camera image in order to determine the amount of curvature or perspective change. The question is how to manipulate the bitmap such that the two transforms can be made.

Here's the code I used to get the non-transformed text into the camera image and into the gallery:

private void combinePictureWithText(String fileName) {
    Log.v(TAG, "combinePictureWithText");

    int targetW = getWindowManager().getDefaultDisplay().getWidth();
    int targetH = getWindowManager().getDefaultDisplay().getHeight();

    /* Get the size of the image */
    BitmapFactory.Options bmOptions = new BitmapFactory.Options();
    bmOptions.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(fileName, bmOptions);
    int photoW = bmOptions.outWidth;
    int photoH = bmOptions.outHeight;

    /* Figure out which way needs to be reduced less */
    int scaleFactor = 1;
    if ((targetW > 0) || (targetH > 0)) {
        scaleFactor = Math.min(photoW/targetW, photoH/targetH);
    }
    Log.v(TAG, "Scale Factor: " + scaleFactor);

    /* Set bitmap options to scale the image decode target */
    bmOptions.inJustDecodeBounds = false;
    bmOptions.inSampleSize = scaleFactor;
    bmOptions.inPurgeable = true;

    mBeerLayout.setDrawingCacheEnabled(true);
    Bitmap mDrawingCache = mBeerLayout.getDrawingCache();

    Bitmap cameraBitmap = BitmapFactory.decodeFile(fileName, bmOptions);
    Bitmap textBitmap = Bitmap.createBitmap(mDrawingCache);
    Bitmap combinedBitmap = Bitmap.createBitmap(targetW, targetH, Bitmap.Config.ARGB_8888);
    Canvas comboImage = new Canvas(combinedBitmap);
    cameraBitmap = Bitmap.createScaledBitmap(cameraBitmap, targetW, targetH, true);
    comboImage.drawBitmap(cameraBitmap, 0, 0, null);
    comboImage.drawBitmap(textBitmap, 0, 0, null); // WAS: matrix (instead of 0, 0)

    /* Save to the file system */
    Log.v(TAG, "save combined picture to the file system");
    try {
        File aFile = new File(fileName);
        if (aFile.exists()) {
            Log.v(TAG, "File " + fileName + "  existed. Deleting it.");
            //aFile.delete();
        } else {
            Log.v(TAG, "File " + fileName + " did not exist.");
        }
        FileOutputStream out = new FileOutputStream(fileName);
        combinedBitmap.compress(Bitmap.CompressFormat.JPEG, 90, out);
        out.flush();
        out.close();
        Log.v(TAG, "Saved " + fileName);
    } catch (Exception e) {
        Log.v(TAG, "Failed in file output stream " + e.getMessage());
    }

    /* Associate the Bitmap to the ImageView */
    //mImageView.setImageBitmap(combinedBitmap); // DRS was "bitmap"
    //mImageView.setVisibility(View.VISIBLE);

    /* Add as a gallery picture */
    Log.v(TAG, "galleryAddPic");
    Intent mediaScanIntent = new Intent("android.intent.action.MEDIA_SCANNER_SCAN_FILE");
    File f = new File(fileName);
    Uri contentUri = Uri.fromFile(f);

    mediaScanIntent.setData(contentUri);
    this.sendBroadcast(mediaScanIntent);
}

This question/answer might provide details for how to do the perspective alteration, but I do not believe it answers the question for simulation of wrapping text around a cylinder.


Solution

  • One solution to this problem is to use Canvas.drawBitmapMesh(). This solution has the benefit of not requiring the added complexity of OpenGL.

    The idea behind using a mesh is that whatever bitmap is fed into the function is warped to fit the defined points. The image shows what's happening with just 5 points per ellipse: mesh definition So the portion of the bitmap that falls in the left, rectangular area will be manipulated to fit in the shape of the left polygon of the area defined by the vertices that we calculate.

    Modeling portions of two ellipses provide the values the mesh.

    There are going to be many ways to compute vertices that would do generally what is required, and this implementation is not necessarily the best, most efficient, most understandable, etc, but it has the benefit of doing what is asked about in the original question.

    private static float[] computeVerticesFromEllipse(int width, int height, int steps, float curveFactor, float topBottomRatio) {
        double p = width / 2d;
        double q = 0d;
        double a = width / 2d;
        double b = curveFactor * a;
    
        float[] verticies = new float[(steps-1) * 4];
        int j = 0;
    
        double increment = width / (double)steps;
    
        for (int i = 1; i < steps; i++, j=j+2) {
            verticies[j] = (float)(increment * (double)i);
            verticies[j+1] =-(float) (-Math.sqrt((1-Math.pow(((increment * (double)i)-p), 2)/Math.pow(a,2)) * Math.pow(b,2)) + q);
            Log.v(TAG, "Point, " + verticies[j] + ", " + verticies[j+1] + ", " + j + ", " +(j+1));
        }
    
        double width2 = topBottomRatio * width;
        p = width2 / 2d;
        q = (width - width2) / 2d;
        a = width2 / 2d;
        b = curveFactor * a;
        increment = width2 / (double)steps;
    
        double shift = width * (1d - topBottomRatio) / 2d;
    
        for (int i = 1; i < steps; i++, j=j+2) {
            verticies[j] = (float)(increment * (double)i) + (float)shift;
            verticies[j+1] =(float) -(-Math.sqrt((1-Math.pow(((increment * (double)i)-p), 2)/Math.pow(a,2)) * Math.pow(b,2)) + q)+ height;
            Log.v(TAG, "Point, " + verticies[j] + ", " + verticies[j+1] + ", " + j + ", " +(j+1));
        }
    
        return verticies;
    }
    

    To integrate this new method into the code of the original question, we define some items to control the look of the result (curveFactor, topBottomRatio). The steps variable can be adjusted to get a smoother looking result. Finally, we defined items to enter into the drawBitmapMesh() method (columns, scaledVertices). Then, rather than using drawBitmap(), we use drawBitmapMesh().

        // 2017 03 22 - Added 5
        float curveFactor = 0.4f;
        float topBottomRatio = 0.7f;
        int steps = 48;
        float[] scaledVertices = computeVerticesFromEllipse(textBitmap.getWidth(), textBitmap.getHeight(), steps, curveFactor, topBottomRatio);
        int columns = (scaledVertices.length / 4) - 1; // divide by 2 since for two points, divide by 2 again for two rows
    
        Bitmap combinedBitmap = Bitmap.createBitmap(targetW, targetH, Bitmap.Config.ARGB_8888);
        Canvas comboImage = new Canvas(combinedBitmap);
        cameraBitmap = Bitmap.createScaledBitmap(cameraBitmap, targetW, targetH, true);
        comboImage.drawBitmap(cameraBitmap, 0, 0, null);
        // 2017 03 22 - Commented 1, Added 1
        // comboImage.drawBitmap(textBitmap, 0, 0, null); 
        comboImage.drawBitmapMesh(textBitmap, columns, 1, scaledVertices, 0, null, 0, null);
    

    Android Layout wrapped around a cylinder