Question

Do any of you have an idea, how to distort an image in any quadrangle? I want to implement an image, which you can pull any corner in any direction, distorting the image. Anyone has an idea how to do that? I use and write stuff in android for a while now, but it does not seem like android has a function for that. I don't really feel like writing a new math library :).

Greetings, Can

Was it helpful?

Solution

Looks like you need Canvas.drawBitmapMesh . There is a sample in Android SDK showing how to use it.

You need to use Matrix for drawing your bitmap on Canvas. You can easily create such transformation which will fit your bitmap image into any quadrangle with Matrix.polyToPoly method. It will look like this:

matrix.setPolyToPoly(
        new float[] { 
            0, 0, 
            bitmap.getWidth(), 0
            0, bitmap.getHeight(),
            bitmap.getWidth(), bitmap.getHeight() 
        }, 0, 
        new float[] { 
            x0, y0, 
            x1, y1, 
            x2, y2,
            x3, y3
        }, 0, 
        4);

canvas.drawBitmap(bitmap, matrix, paint);

Where x0-x3, y0-y3 are your quadrangle vertex coordinates.

OTHER TIPS

@donmj . I fixed your code.

public class PerspectiveDistortView extends View implements View.OnTouchListener {

private Paint paintRect, paintCircle;
public int LEFT;
public int TOP;
public int RIGHT;
public int BOTTOM;
Point CIRCLE_TOP_LEFT;
Point CIRCLE_TOP_RIGHT;
Point CIRCLE_BOTTOM_LEFT;
Point CIRCLE_BOTTOM_RIGHT;
private int lastX, lastY;
Bitmap image;
Matrix matrix2;
boolean isTouchCirclePoints = true;

public PerspectiveDistortView(Context context) {
    super(context);
    // TODO Auto-generated constructor stub
    init();
}

public PerspectiveDistortView(Context context, AttributeSet attrs) {
    super(context, attrs);
    // TODO Auto-generated constructor stub
    init();
}

public PerspectiveDistortView(Context context, AttributeSet attrs,
                              int defStyle) {
    super(context, attrs, defStyle);
    // TODO Auto-generated constructor stub
    init();
}

private void init() {
    this.setOnTouchListener(this);
    paintRect = new Paint();
    paintRect.setColor(0xffff0000);
    paintRect.setAntiAlias(true);
    paintRect.setDither(true);
    paintRect.setStyle(Paint.Style.STROKE);
    paintRect.setStrokeJoin(Paint.Join.BEVEL);
    paintRect.setStrokeCap(Paint.Cap.BUTT);
    paintRect.setStrokeWidth(3);
    paintCircle = new Paint();
    paintCircle.setColor(0xff000000);
    paintCircle.setAntiAlias(true);
    paintCircle.setDither(true);
    paintCircle.setStyle(Paint.Style.FILL_AND_STROKE);
    paintCircle.setStrokeJoin(Paint.Join.BEVEL);
    paintCircle.setStrokeCap(Paint.Cap.BUTT);

    LEFT = 90;
    TOP = 40;
    RIGHT = 500;
    BOTTOM = 700;
    CIRCLE_TOP_LEFT = new Point(LEFT, TOP);
    CIRCLE_TOP_RIGHT = new Point(RIGHT, TOP);
    CIRCLE_BOTTOM_LEFT = new Point(LEFT, BOTTOM);
    CIRCLE_BOTTOM_RIGHT = new Point(RIGHT, BOTTOM);

    image = BitmapFactory.decodeResource(getResources(), R.drawable.penguins);

    matrix2 = new Matrix();
}

@Override
protected void onDraw(Canvas canvas) {
    // Free Transform bitmap
    int bw = image.getWidth();
    int bh = image.getHeight();

    float[] pts = {
            // source
            0, 0,
            0, bh,
            bw, bh,
            bw, 0,
            // destination
            0, 0,
            0, 0,
            0, 0,
            0, 0};
    pts[8] = CIRCLE_TOP_LEFT.x;
    pts[9] = CIRCLE_TOP_LEFT.y;
    pts[10] = CIRCLE_BOTTOM_LEFT.x;
    pts[11] = CIRCLE_BOTTOM_LEFT.y;
    pts[12] = CIRCLE_BOTTOM_RIGHT.x;
    pts[13] = CIRCLE_BOTTOM_RIGHT.y;
    pts[14] = CIRCLE_TOP_RIGHT.x;
    pts[15] = CIRCLE_TOP_RIGHT.y;

    matrix2.setPolyToPoly(pts, 0, pts, 8, 4);
    canvas.drawBitmap(image, matrix2, null);
    isTouchCirclePoints = false;

    // line left
    canvas.drawLine(CIRCLE_TOP_LEFT.x, CIRCLE_TOP_LEFT.y, CIRCLE_BOTTOM_LEFT.x, CIRCLE_BOTTOM_LEFT.y, paintRect);
    // line top
    canvas.drawLine(CIRCLE_TOP_LEFT.x, CIRCLE_TOP_LEFT.y, CIRCLE_TOP_RIGHT.x, CIRCLE_TOP_RIGHT.y, paintRect);
    // line right
    canvas.drawLine(CIRCLE_TOP_RIGHT.x, CIRCLE_TOP_RIGHT.y, CIRCLE_BOTTOM_RIGHT.x, CIRCLE_BOTTOM_RIGHT.y, paintRect);
    // line bottom
    canvas.drawLine(CIRCLE_BOTTOM_LEFT.x, CIRCLE_BOTTOM_LEFT.y, CIRCLE_BOTTOM_RIGHT.x, CIRCLE_BOTTOM_RIGHT.y, paintRect);
    // circle top left
    canvas.drawCircle(CIRCLE_TOP_LEFT.x, CIRCLE_TOP_LEFT.y, 10, paintCircle);
    // circle top right
    canvas.drawCircle(CIRCLE_TOP_RIGHT.x, CIRCLE_TOP_RIGHT.y, 10, paintCircle);
    // circle bottom left
    canvas.drawCircle(CIRCLE_BOTTOM_LEFT.x, CIRCLE_BOTTOM_LEFT.y, 10, paintCircle);
    // circle bottom right
    canvas.drawCircle(CIRCLE_BOTTOM_RIGHT.x, CIRCLE_BOTTOM_RIGHT.y, 10, paintCircle);
}

@Override
public boolean onTouch(View view, MotionEvent event) {
    lastX = (int) event.getX();
    lastY = (int) event.getY();

    if (inCircle(lastX, lastY, CIRCLE_TOP_LEFT.x, CIRCLE_TOP_LEFT.y, 40)) {
        isTouchCirclePoints = true;
        CIRCLE_TOP_LEFT.set(lastX, lastY);
    } else if (inCircle(lastX, lastY, CIRCLE_TOP_RIGHT.x, CIRCLE_TOP_RIGHT.y, 40)) {
        isTouchCirclePoints = true;
        CIRCLE_TOP_RIGHT.set(lastX, lastY);
    } else if (inCircle(lastX, lastY, CIRCLE_BOTTOM_LEFT.x, CIRCLE_BOTTOM_LEFT.y, 40)) {
        isTouchCirclePoints = true;
        CIRCLE_BOTTOM_LEFT.set(lastX, lastY);
    } else if (inCircle(lastX, lastY, CIRCLE_BOTTOM_RIGHT.x, CIRCLE_BOTTOM_RIGHT.y, 40)) {
        isTouchCirclePoints = true;
        CIRCLE_BOTTOM_RIGHT.set(lastX, lastY);
    }
    invalidate();
    return true;
}

private boolean inCircle(float x, float y, float circleCenterX, float circleCenterY, float circleRadius) {
    double dx = Math.pow(x - circleCenterX, 2);
    double dy = Math.pow(y - circleCenterY, 2);

    if ((dx + dy) < Math.pow(circleRadius, 2)) {
        return true;
    } else {
        return false;
    }
}
}

There is an issue with your code. Although it is the correct method, you have inverted the float[] parameters, as appears in Android documentation:

public boolean setPolyToPoly (float[] src, int srcIndex, float[] dst, int dstIndex, int pointCount)
WHERE
src: The array of src [x,y] pairs (points)
...
dst: The array of dst [x,y] pairs (points)

So, according with your code, the matrix should be created as:

matrix.setPolyToPoly(
        new float[] {
            x0, y0,
            x1, y1,
            x2, y2,
            x3, y3},
    0,
new float[] { 
        0, 0, 
        bitmap.getWidth(), 0
        0, bitmap.getHeight(),
        bitmap.getWidth(), bitmap.getHeight() 
    }, 0, 
    4);

In this way, the app works fine, as shown in the image:

enter image description here

By the other hand, regarding of what user2498079 says in his comment about the computational problem in low end devices, you can use some easy-to-do techniques to reduce the source image size (and color depth, for example) before matrix conversion calculation. It'd make it easier for the low end phone to realize this task.

matrix.setPolyToPoly and canvas.drawBitmap can't solve all matrix transformations. Here I found a solution using canvas.drawBitmapMesh.

Distorting an image to a quadrangle fails in some cases on Android

I hope this helps. The corner top-left, top-right is working except for bottom-left and bottom-right. Can somebody add it. I can't figure out how to do the bottom parts. :)

public class PerspectiveDistortView extends View  implements OnTouchListener {

private Paint paintRect, paintCircle;
public int LEFT;
public int TOP;
public int RIGHT;
public int BOTTOM;
Point CIRCLE_TOP_LEFT;
Point CIRCLE_TOP_RIGHT;
Point CIRCLE_BOTTOM_LEFT;
Point CIRCLE_BOTTOM_RIGHT;
private int lastX, lastY;
Bitmap image;
Rect src, dst;
Matrix matrix2;
boolean isTouchCirclePoints = true;
float deform2 = 5f;

public PerspectiveDistortView(Context context) {
    super(context);
    // TODO Auto-generated constructor stub
    init();
}

public PerspectiveDistortView(Context context, AttributeSet attrs) {
    super(context, attrs);
    // TODO Auto-generated constructor stub
    init();
}

public PerspectiveDistortView(Context context, AttributeSet attrs,
        int defStyle) {
    super(context, attrs, defStyle);
    // TODO Auto-generated constructor stub
    init();
}

private void init(){
    this.setOnTouchListener(this);
    paintRect = new Paint();
    paintRect.setColor(0xffff0000);
    paintRect.setAntiAlias(true);
    paintRect.setDither(true);
    paintRect.setStyle(Paint.Style.STROKE);
    paintRect.setStrokeJoin(Paint.Join.BEVEL);
    paintRect.setStrokeCap(Paint.Cap.BUTT);
    paintRect.setStrokeWidth(3);
    paintCircle = new Paint();
    paintCircle.setColor(0xff000000);
    paintCircle.setAntiAlias(true);
    paintCircle.setDither(true);
    paintCircle.setStyle(Paint.Style.FILL_AND_STROKE);
    paintCircle.setStrokeJoin(Paint.Join.BEVEL);
    paintCircle.setStrokeCap(Paint.Cap.BUTT);

    LEFT = 90;
    TOP = 40;
    RIGHT = 500;
    BOTTOM = 700;
    CIRCLE_TOP_LEFT = new Point(LEFT, TOP);
    CIRCLE_TOP_RIGHT = new Point(RIGHT, TOP);
    CIRCLE_BOTTOM_LEFT = new Point(LEFT, BOTTOM);
    CIRCLE_BOTTOM_RIGHT = new Point(RIGHT, BOTTOM);

    image = BitmapFactory.decodeResource(getResources(), R.drawable.ai);

    src = new Rect();
    dst = new Rect();

    matrix2 = new Matrix();
}

@Override
protected void onDraw(Canvas canvas) {
    // draw image
    src.left = LEFT;
    src.top = TOP;
    src.right = RIGHT;
    src.bottom = BOTTOM + image.getHeight();

    dst.left = CIRCLE_TOP_LEFT.x;
    dst.top = CIRCLE_TOP_LEFT.y;
    dst.right = CIRCLE_TOP_RIGHT.x;
    dst.bottom = CIRCLE_BOTTOM_RIGHT.y;

    // Free Transform bitmap
        int bw = image.getWidth();
        int bh = image.getHeight();
        RectF src = new RectF(LEFT, TOP, bw, bh);
        RectF dst = new RectF(CIRCLE_TOP_LEFT.x + 35, CIRCLE_TOP_LEFT.y + 30, CIRCLE_TOP_RIGHT.x, CIRCLE_BOTTOM_RIGHT.y);
        matrix2.setRectToRect(src, dst, ScaleToFit.FILL);

        float[] pts = {
                       // source
                       0, 0, 
                       0, bh, 
                       bw, bh, 
                       bw, 0,
                       // destination
                       0, 0,
                       0, 0, 
                       0, 0, 
                       0, 0};
        matrix2.mapPoints(pts, 8, pts, 0, 4);
        int DX = 100;
        pts[10] -= CIRCLE_TOP_LEFT.x - LEFT; 
        pts[12] -= CIRCLE_TOP_RIGHT.x - RIGHT;
        pts[13] += 0;
        pts[14] += 0;
        pts[15] += CIRCLE_TOP_RIGHT.y - CIRCLE_TOP_LEFT.y;

        matrix2.setPolyToPoly(pts, 0, pts, 8, 4);
        canvas.drawBitmap(image, matrix2, null);
        isTouchCirclePoints = false;

    // line left
    canvas.drawLine(CIRCLE_TOP_LEFT.x, CIRCLE_TOP_LEFT.y, CIRCLE_BOTTOM_LEFT.x, CIRCLE_BOTTOM_LEFT.y, paintRect);
    // line top
    canvas.drawLine(CIRCLE_TOP_LEFT.x, CIRCLE_TOP_LEFT.y, CIRCLE_TOP_RIGHT.x, CIRCLE_TOP_RIGHT.y, paintRect);
    // line right
    canvas.drawLine(CIRCLE_TOP_RIGHT.x, CIRCLE_TOP_RIGHT.y, CIRCLE_BOTTOM_RIGHT.x, CIRCLE_BOTTOM_RIGHT.y, paintRect);
    // line bottom
    canvas.drawLine(CIRCLE_BOTTOM_LEFT.x, CIRCLE_BOTTOM_LEFT.y, CIRCLE_BOTTOM_RIGHT.x, CIRCLE_BOTTOM_RIGHT.y, paintRect);
    // circle top left
    canvas.drawCircle(CIRCLE_TOP_LEFT.x, CIRCLE_TOP_LEFT.y, 10, paintCircle);
    // circle top right
    canvas.drawCircle(CIRCLE_TOP_RIGHT.x, CIRCLE_TOP_RIGHT.y, 10, paintCircle);
    // circle bottom left
    canvas.drawCircle(CIRCLE_BOTTOM_LEFT.x, CIRCLE_BOTTOM_LEFT.y, 10, paintCircle);
    // circle bottom right
    canvas.drawCircle(CIRCLE_BOTTOM_RIGHT.x, CIRCLE_BOTTOM_RIGHT.y, 10, paintCircle);
}

@Override
public boolean onTouch(View view, MotionEvent event) {
    lastX = (int) event.getX();
    lastY = (int)event.getY();
    if (inCircle(lastX, lastY, CIRCLE_TOP_LEFT.x, CIRCLE_TOP_LEFT.y, 40))
    {
        isTouchCirclePoints = true;
        CIRCLE_TOP_LEFT.set(lastX, lastY);
    } else if (inCircle(lastX, lastY, CIRCLE_TOP_RIGHT.x, CIRCLE_TOP_RIGHT.y, 40))
    {
        isTouchCirclePoints = true;
        CIRCLE_TOP_RIGHT.set(lastX, lastY);
    } else if (inCircle(lastX, lastY, CIRCLE_BOTTOM_LEFT.x, CIRCLE_BOTTOM_LEFT.y, 40))
    {
        isTouchCirclePoints = true;
        CIRCLE_BOTTOM_LEFT.set(lastX, lastY);
    } else if (inCircle(lastX, lastY, CIRCLE_BOTTOM_RIGHT.x, CIRCLE_BOTTOM_RIGHT.y, 40))
    {
        isTouchCirclePoints = true;
        CIRCLE_BOTTOM_RIGHT.set(lastX, lastY);
    }
    invalidate();
    return true;
}

private boolean inCircle(float x, float y, float circleCenterX, float circleCenterY, float circleRadius) {
    double dx = Math.pow(x - circleCenterX, 2);
    double dy = Math.pow(y - circleCenterY, 2);

    if ((dx + dy) < Math.pow(circleRadius, 2)) {
        return true;
    } else {
        return false;
    }
}

}

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top