Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I draw an image based on a simple polygon?

I'd like to copy a roughly rectangular area to a rectangular area. Example:

Both areas are defined by their corner points. The general direction is kept (no flipping etc).

Simply rotating the source image does not work since opposing sides may be of different length.

So far I found no way to do this in pure C# (except manual pixel copying), so I guess I have to resort to the Windows API or some 3rd party library?

like image 250
mafu Avatar asked Apr 20 '11 10:04

mafu


2 Answers

Since I could not find an answer, I wrote a naive implementation myself. It works reasonably well.

Examples

I drew all examples manually in Paint, so they are not very exact - it was just enough to test some basics.

a) Slight rotation.

Source:

source image

Result:

resulting image

b) Various sides

Source:

source 2

Result:

result 2

c) Perspective

Source:

source 3

Result:

result 3

Code

(it's specialized to my use case, but it should be easy to adapt):

// _Corners are, well, the 4 corners in the source image
// _Px is an array of pixels extracted from the source image

public void Rescale ()
{
    RescaleImage (
        _Corners[0],
        _Corners[1],
        _Corners[3],
        _Corners[2],
        100,
        100);
}

private void RescaleImage (PointF TL, PointF TR, PointF LL, PointF LR, int sx, int sy)
{
    var bmpOut = new Bitmap (sx, sy);

    for (int x = 0; x < sx; x++) {
        for (int y = 0; y < sy; y++) {
            /*
             * relative position
             */
            double rx = (double) x / sx;
            double ry = (double) y / sy;

            /*
             * get top and bottom position
             */
            double topX = TL.X + rx * (TR.X - TL.X);
            double topY = TL.Y + rx * (TR.Y - TL.Y);
            double bottomX = LL.X + rx * (LR.X - LL.X);
            double bottomY = LL.Y + rx * (LR.Y - LL.Y);

            /*
             * select center between top and bottom point
             */
            double centerX = topX + ry * (bottomX - topX);
            double centerY = topY + ry * (bottomY - topY);

            /*
             * store result
             */
            var c = PolyColor (centerX, centerY);
            bmpOut.SetPixel (x, y, c);
        }
    }

    bmpOut.Save (_Path + "out5 rescale out.bmp");
}

private Color PolyColor (double x, double y)
{
    // get fractions
    double xf = x - (int) x;
    double yf = y - (int) y;

    // 4 colors - we're flipping sides so we can use the distance instead of inverting it later
    Color cTL = _Px[(int) y + 1, (int) x + 1];
    Color cTR = _Px[(int) y + 1, (int) x + 0];
    Color cLL = _Px[(int) y + 0, (int) x + 1];
    Color cLR = _Px[(int) y + 0, (int) x + 0];

    // 4 distances
    double dTL = Math.Sqrt (xf * xf + yf * yf);
    double dTR = Math.Sqrt ((1 - xf) * (1 - xf) + yf * yf);
    double dLL = Math.Sqrt (xf * xf + (1 - yf) * (1 - yf));
    double dLR = Math.Sqrt ((1 - xf) * (1 - xf) + (1 - yf) * (1 - yf));

    // 4 parts
    double factor = 1.0 / (dTL + dTR + dLL + dLR);
    dTL *= factor;
    dTR *= factor;
    dLL *= factor;
    dLR *= factor;

    // accumulate parts
    double r = dTL * cTL.R + dTR * cTR.R + dLL * cLL.R + dLR * cLR.R;
    double g = dTL * cTL.G + dTR * cTR.G + dLL * cLL.G + dLR * cLR.G;
    double b = dTL * cTL.B + dTR * cTR.B + dLL * cLL.B + dLR * cLR.B;

    Color c = Color.FromArgb ((int) (r + 0.5), (int) (g + 0.5), (int) (b + 0.5));

    return c;
}
like image 125
mafu Avatar answered Oct 20 '22 02:10

mafu


Generally speaking, what you want to do is map the destination coordinates to the source coordinates through a transform function:

for (int y = 0; y < destHeight; y++) {
    for (x=0; x < destWidth; x++) {
        Color c = Transform(x, y, sourceImage, sourceTransform);
        SetPixel(destImage, x, y, c);
    }
}

Let's assume that sourceTransform is an object that encapsulates a transformation from source to dest coordinates (and vice versa).

Working in dest coordinates will make it easier to avoid that curve in your retransformed source image and will allow you to better antialias, as you can map the corners of the dest pixel to the source image and sample within it and interpolate/extrapolate.

In your case you're going to have a set of linear equations that do the mapping - in this case this is known as quadrilateral warping - see this previous question.

like image 21
plinth Avatar answered Oct 20 '22 01:10

plinth