Detect passing of rectangle over yellow pixel

2019-07-14 06:14发布

问题:

I have a query regarding the best approach to detect when a moving and potentially rotated rectangle passes over a yellow pixel of a Panel's background image.

I have a method which accepts an Image and a Point, and returns true if that point is that of a yellow pixel. I require this colour detection for the function of my game, which resets the car (player) if it drives over the yellow borders of the track. This method is shown below:

private Boolean isYellow(Image image, Point point)
{
   Bitmap bitmap = new Bitmap(image);
   Color color = bitmap.GetPixel(point.X, point.Y);

   return (color.R > 220 && color.G > 220 && color.B < 200);
}

Previously, to detect if the player rectangle passes over yellow, I checked against the location of the rectangle, as provided by the X and Y values of the object. The issue with this is that the location is the top left corner of a horizontal rectangle, meaning the car can drive almost entirely off the track without detection occurring.

I'd like to fix this by checking all points covered by the rectangle. This is not as simple as it may seem as the rectangle is likely to be rotated. My drawing and movement logic is shown below:

public void draw(Graphics g)
{
   int dx = rectangle.X + (rectangle.Height / 2);
   int dy = rectangle.Y + (rectangle.Width / 2);

   g.ScaleTransform(xScale, yScale);
   g.TranslateTransform(dx, dy);
   g.RotateTransform((float) ((180 * angle) / Math.PI));
   g.TranslateTransform(-dx, -dy);
   g.DrawImage(image, rectangle.X, rectangle.Y);
   g.ResetTransform();
}

public void move(uRaceGame game, Panel panel)
{
   double cos = Math.Cos(angle), sin = Math.Sin(angle);
   int xLocation = 200;
   int yLocation = 200;

   xLocation = (int) Math.Floor(rectangle.X + (cos * game.moveDir * 60)); 
   yLocation = (int) Math.Floor(rectangle.Y + (sin * game.moveDir * 60)); 

   angle = (angle + (game.rotateDir * (Math.PI / 128))) % (Math.PI * 2);

   if (xLocation * xScale > panel.Width - (rectangle.Width * cos) || yLocation * yScale > panel.Height - (rectangle.Width * sin) - 5 || xLocation * xScale < 0 || yLocation * yScale < 5) return;

   rectangle.Location = new Point(xLocation, yLocation);
}

I tried but failed to create a method which translates the coords of the corner and figures out the middle of the rectangle, but this does not work, and the yellow detection fires in very obscure places:

public Point getCentre()
{
    int cX = (int) (rectangle.X + ((rectangle.Width / 2) / xScale)), cY = (int) (rectangle.Y + ((rectangle.Height / 2) / yScale));
    float tempX = (rectangle.X - cX), tempY = (rectangle.Y - cY);

    double rX = (tempX * Math.Cos(angle)) - (tempY * Math.Sin(angle));
    double rY = (tempX * Math.Sin(angle)) - (tempY * Math.Cos(angle));

    return new Point((int) ((rX + cX) * xScale), (int) ((rY + cY) * yScale));
}

I'd really appreciate any suggestions on how to tackle this. I included the translation and yellow detection code in case I'm miles off in my attempt and someone else has a better idea.

Thank you very much.

回答1:

There are two approaches that come to my mind:

  • You can create loops that go along the tilted sides of the car rectangle
  • Or you can copy the car to an untilted bitmap and loop over it normally.

Here is an example of the second approach.

It uses a LockBits method that detects Yellow with your code in a Bitmap.

And it prepares that bitmap by copying it from the original BackgroundImage un-rotated.

Here is the result, including a control Panel that shows the untilted Rectangle:



Here is the yellow finder function. It uses Lockbits for speed:

using System.Runtime.InteropServices;
using System.Drawing.Imaging;

public bool testForYellowBitmap(Bitmap bmp)
{
    Size s1 = bmp.Size;
    PixelFormat fmt = new PixelFormat();
    fmt = bmp.PixelFormat;
    Rectangle rect = new Rectangle(0, 0, s1.Width, s1.Height);
    BitmapData bmp1Data = bmp.LockBits(rect, ImageLockMode.ReadOnly, fmt);
    byte bpp1 = 4;
    if (fmt == PixelFormat.Format24bppRgb) bpp1 = 3;
    else if (fmt == PixelFormat.Format32bppArgb) bpp1 = 4; else return false; // throw!!
    int size1 = bmp1Data.Stride * bmp1Data.Height;
    byte[] data1 = new byte[size1];
    System.Runtime.InteropServices.Marshal.Copy(bmp1Data.Scan0, data1, 0, size1);
    for (int y = 0; y < s1.Height; y++)
    {
        for (int x = 0; x < s1.Width; x++)
        {
            Color c1;
            int index1 = y * bmp1Data.Stride + x * bpp1;
            if (bpp1 == 4)
                c1 = Color.FromArgb(data1[index1 + 3], data1[index1 + 2],
                                    data1[index1 + 1], data1[index1 + 0]);
            else c1 = Color.FromArgb(255, data1[index1 + 2], 
                                          data1[index1 + 1], data1[index1 + 0]);
            if (c1.R > 220 && c1.G > 220 && c1.B < 200) 
               { bmp.UnlockBits(bmp1Data); return true; }
        }
    }
    bmp.UnlockBits(bmp1Data);
    return false;
}

I prepare the Bitmap to compare in the MouseMove. The variables w, h, w2, h2 hold the width, height and halves of that of the car's size. The source bitmap is in drawPanel1.BackgroundImage. The current angle is in a TrackBar tr_a.Value. For further control I also display the rotated car rectangle in White.

private void drawPanel1_MouseMove(object sender, MouseEventArgs e)
{
    if (e.Button.HasFlag(MouseButtons.Left))
    {
        Size sz = drawPanel1.BackgroundImage.Size;
        Rectangle rectSrc = new Rectangle(e.X - w2, e.Y - h2, w, h);
        Rectangle rectTgt = new Rectangle(e.X - w, e.Y - h, 2 * w, 2 * h);

        using (Graphics g = drawPanel1.CreateGraphics())  // start optional
        {
            g.TranslateTransform(e.X, e.Y);
            g.RotateTransform(trb_a.Value);
            g.TranslateTransform(-e.X, -e.Y);
            drawPanel1.Refresh();
            g.DrawRectangle(Pens.White, rectSrc);
        }

        using (Graphics g = drawPanel2.CreateGraphics())
        {                                                      // end optional
            using (Bitmap bmp = new Bitmap(sz.Width, sz.Height))
            using (Graphics g2 = Graphics.FromImage(bmp))
            {
                g2.TranslateTransform(e.X, e.Y);
                g2.RotateTransform(-trb_a.Value);
                g2.TranslateTransform(-e.X, -e.Y);
                g2.DrawImage(drawPanel1.BackgroundImage, rectTgt, rectTgt, 
                             GraphicsUnit.Pixel);
                drawPanel2.Refresh();
                g.DrawImage(bmp, rectSrc, rectSrc, GraphicsUnit.Pixel);
                Text = testForYellowBitmap(bmp) ? "!!YELLOW!!" : "";
            }
        }
    }

The first approach would use a similar LockBits method, but with loops inside that go along the rotated sides of the car rectangle, using floats wth the loop variables to calculate the x-coordinates. Those data should be prepared on each change of car size or angle. The code is a little longer but should be a bit faster, too.

The advantage if the second approach is that by using a ClippingRegion on the Graphics object one could check an arbitrary shape while the first method can be easily modified for concave polygons but not for curved shapes.

Here is the adapted version of the checking code for the first version:

public bool testForYellowBitmapTilt(Bitmap bmp, List<int> leftPts, 
                                    List<int> rightPts, Point topLeft)
{
    Size s1 = bmp.Size;
    PixelFormat fmt = new PixelFormat();
    fmt = bmp.PixelFormat;
    Rectangle rect = new Rectangle(0, 0, s1.Width, s1.Height);
    BitmapData bmp1Data = bmp.LockBits(rect, ImageLockMode.ReadOnly, fmt);
    byte bpp1 = 4;
    if (fmt == PixelFormat.Format24bppRgb) bpp1 = 3;
    else if (fmt == PixelFormat.Format32bppArgb) bpp1 = 4; 
         else return false; // or throw!!
    if (leftPts.Count != rightPts.Count) return false; // or throw!!

    int size1 = bmp1Data.Stride * bmp1Data.Height;
    byte[] data1 = new byte[size1];
    System.Runtime.InteropServices.Marshal.Copy(bmp1Data.Scan0, data1, 0, size1);

    for (int y = 0; y < (leftPts.Count); y++)
    {
        for (int x = leftPts[y] + topLeft.X; x < rightPts[y] + topLeft.X; x++)
        {
            Color c1;

            int index1 = (y + topLeft.Y) * bmp1Data.Stride + x * bpp1;
            if (index1 > 0)
            {
                if (bpp1 == 4)
                    c1 = Color.FromArgb(data1[index1 + 3], data1[index1 + 2], 
                                        data1[index1 + 1], data1[index1 + 0]);
                else c1 = Color.FromArgb(255, data1[index1 + 2],
                                        data1[index1 + 1], data1[index1 + 0]);

                if (c1.R > 220 && c1.G > 220 && c1.B < 200) 
                   { bmp.UnlockBits(bmp1Data); return true; }
            }
        }
    }
    bmp.UnlockBits(bmp1Data);
    return false;
}

The left- and rightside coordinates are stored here:

List<int> leftPts = new List<int>();
List<int> rightPts = new List<int>();
Point top = Point.Empty;


void getOuterPoints(List<PointF> corners, out List<int> leftPts, 
                    out List<int> rightPts, out Point top)
{
    leftPts = new List<int>();
    rightPts = new List<int>();

    PointF left = corners.Select(x => x).OrderBy(x => x.X).First();
    PointF right = corners.Select(x => x).OrderByDescending(x => x.X).First();
            top = Point.Round(corners.Select(x => x).OrderBy(x => x.Y).First());
    PointF bottom = corners.Select(x => x).OrderByDescending(x => x.Y).First();

    int w1 = -(int)(top.X - left.X);
    int w2 = -(int)(left.X - bottom.X );
    int h1 = (int)(left.Y - top.Y);
    int h2 = (int)(bottom.Y - left.Y);

    float d1 = 1f * w1 / h1;
    float d2 = 1f * w2 / h2;

    for (int y = 0; y < h1; y++) leftPts.Add( (int)(y * d1) );
    for (int y = 0; y < h2; y++) leftPts.Add( (int)(y * d2 + w1));

    for (int y = 0; y < h2; y++) rightPts.Add( (int)(y * d2));
    for (int y = 0; y < h1; y++) rightPts.Add(  (int)(y * d1 + w2));
}

You need to feed in the four corners as a List<PointF> in any order; the top can be anything, it will be set in the method. The coodinates are relative to the car, so they don't change when the car moves..