Robert C Robert C - 2 months ago 22
C# Question

Detect passing of rectangle over yellow pixel

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.

TaW TaW
Answer

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:


enter image description here


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 for loop variables..