RGBSurface.Pixel Reporting Wrong Colour?

I’m working on a game as a side project. For reasons that are not important I have these three pictures combined as an image set in the IDE:

MouseMap MouseMap@2x MouseMap@3x

I need to loop through every pixel in the image and determine if it is white (&cffffff), red (&cff0000), yellow (&cffff00), blue (&c0000ff) or green (&c00ff00).

Here is some code in the Open event of the main window:

// Get a correctly scaled temporary picture to draw to.
Var p As Picture = BitmapForCaching(MouseMap.Width, MouseMap.Height)

// Draw the mouse map. Xojo should pick the right one from the image set.
p.Graphics.DrawPicture(MouseMap, 0, 0)

// Loop through every pixel and make sure that it is either
// white, red, yellow, blue or green (they are the *only* colours).
For x As Integer = 0 To p.Graphics.Width - 1
  For y As Integer = 0 To p.Graphics.Height - 1
    Var pixel As Color = p.RGBSurface.Pixel(x, y)
    Select Case pixel
    Case &cffffff
      // White.
    Case &cff0000
      // Red.
    Case &cffff00
      // Yellow.
    Case &c0000ff
      // Blue.
    Case &c00ff00
      // Green.
    Else
      Raise New InvalidArgumentException("Unexpected colour")
    End Select
  Next y
Next x

When I run the project, it always breaks immediately (when x and y = 0 in the loop) saying the colour is invalid. In the debugger, I can see that the colour reported by the RGBSurface is &cEA402500 which is a shade of red:

Now I know for a fact that there are only 5 colours in each of the three images because they are hand drawn in a pixel editor with no anti-aliasing or alpha layer (Pixen for macOS).

Below is an example project including the assets showing the issue:

Example project (Xojo 2020 R2.1).

What is going on here and can anyone help please?

As you can see in the debugger the pixel returns &cEA402500 which is not one of the values your checking. Perhaps the color sheme is changed?

Try setting the alpha next to the color “Case &cffffff00” perhaps so i may compare differently ?

I smell a bug since you clearly know what colors are suppose to be returned.

Just tried that - no change unfortunately.

I really hope this isn’t a bug. It’s kind of a showstopper for me at present.

Feel free to use a colour picker on the images attached to the first post or the ones in the project. You’ll see they are uniform and only one of the five colours in the Select...Case.

1 Like

Due to compression, color spaces and conversion, they may not be what you specified when the images were created.

Look at the hue channel of the pixel and determine the distance between the read pixel and your markers (red, green, blue, yellow). White is grey, so hue won’t help, but brightness and saturation will.

Grrr. Why is this so complicated?

Are there algorithms to determine colour similarities that you know of @Sam_Rowlands?

You are taking your original image and drawing it into another picture - that’s where the disconnect is happening. It’s likely that some color data conversion is happening at that point; color profiles are usually the culprit.

You’re probably not going to get around this in the way you’re hoping, at least not easily. If you can explain how this data is being used, we may be able to suggest a way around the issue.

Essentially I’m using the graphic to determine the location of the mouse cursor when hovering over a hex game map. I’m using an offset coordinate system and have read loads of articles (this is a really good one).

For uninteresting reasons, I’m using the approach in this article which essentially carves the screen up into cells where each cell is one of those coloured images. If I know the local position in that cell (within that image) I can figure out the delta to apply to compute the tile I’m over.

That’s what I thought. Here’s what you do.

Add an extra row of pixels to the bottom of your image. The first pixel in this row should be your red, the second pixel will be your blue, etc.

After you draw your image into the internal picture, you read those values out and store them in variables:

redHitValue = myRGBSurface.Pixel(0, height-1)
blueHitValue = myRGBSurface.Pixel(1, height-1)

etc.

Later, when you’re checking the value under the mouse, you compare the value from the image to the values you’ve already stored:

mouseValue = myRGBSurface.Pixel(mouseX, mouseY)

select case mouseValue
case redHitValue:

case blueHitValue:

etc.

How does that sound?

3 Likes

You will get scaling with your method which will cause artifacts.

Turn off AntiAliased in your graphics object and directly draw the shape into the picture (not DrawPicture). That should give you an accurate set of colors.

Color by RGBsurface

If pixel.Red>253 And pixel.green>253 And pixel.Blue > 253 Then
w=w+1
Elseif pixel.Red>250 And pixel.green<5 And pixel.Blue < 6 Then
r=r+1
Elseif pixel.Red >250 And pixel.green>250 And pixel.Blue < 20 Then
yy=yy+1
Elseif pixel.Red<20 And pixel.green<10 And pixel.Blue > 250 Then
b=b+1
Elseif pixel.Red<40 And pixel.green>250 And pixel.Blue <10 Then
g=g+1

End If

It would seem that your monitor DPI is not divisible by the size of the images you have provided so the image is being scaled up internally.

As you’re on the boundary this takes into account (for example) the pixel to the left of the one being placed (0,0) which in this case is nothing so the very left pixel is antialiased down from full red to something else.

This is quite a common situation in 2d gaming when using tilesets and scaling as pixels are lifted from the tileset and placed into the view their neighbours tile can affect their edge color and if the tile is completely different (as is common in tilesets) then the edge colors are distorted.

To get around this your best bet is to slightly oversize your source image and bleed the colour into the non-displayed space to ensure that when this happens the color “next door” is the same as your current color so things dont change.

Turning off antialising to correct the problem will only be a stop gap as you will then end up with gaps and blur when the image is slightly scaled to fit the monitors dpi.

1 Like

instead of color test maybe test inside of shape/polygon/region.

1 Like

Wow loads of help. Thank you!

TLDR - I’ve solved it.

As @MarkusR suggested, I decided to try to use maths to solve the problem rather than relying on a special tile. The advantage is that I can change the tile dimensions in my graphics editor and don’t have to remember to create a new “mouse map” image.

I think the suggestion by @Eric_Williams would likely work and is smart. Thanks @JulianS for the explanation of what was going wrong.

For those that are interested in hex maps what I did, is as follows:

First the screen is internally carved up into MouseCells (drawn in red):

The coordinates in red at the top of the red cells are the “mouse map” (row, column). The black coordinates in the centre of the hexes are actual in-world (row, column) coordinates.

We can see from the picture that if we know the row and column of a MouseCell we can compute the in-world tile index by adding a delta value to the MouseCell.Row and MouseCell.Column. The delta to add depends which part of the MouseCell the cursor is in:

First thing we need to do is convert the X,Y coordinates in the mouse map to local coordinates within a cell. We do this by using Mod with either column * cellWidth (for the X) or row * cellHeight (for Y). Incidentally, Mod is a great tool for wrapping a value into a range. Once we have X and Y for the cell (localX and localY) we can start determine which region of the cell that point is in.

Each MouseCell can be thought of as a hexagon with four surrounding regions. We need to determine which area (1 to 4) each point in the cell is in. There are four triangles and two rectangles to check:

In order to check if our local point is within one of the triangles or lower rectangles, we need the vertices (points):

Fortunately the vertices are easy to compute:

Var v1 As New Point(cellWidth / 2, 0)
Var v2 As New Point(cellWidth, tileHeight * 0.25)
Var v3 As New Point(cellWidth, tileHeight * 0.75)
Var v4 As New Point(cellWidth / 2, tileHeight)
Var v5 As New Point(0, tileHeight * 0.75)
Var v6 As New Point(0, tileHeight * 0.25)
Var va As New Point(0, 0)
Var vb As New Point(cellWidth, 0)
Var vc As New Point(cellWidth, tileHeight)
Var vd As New Point(0, tileHeight)

Now we can loop through every point in the mouse cell and determine which region it is and, therefore, what the delta to apply is:

// Loop through every point within the mouse map cell and determine
// which region it's in.
For x As Integer = 0 To cellWidth - 1
  For y As Integer = 0 To cellHeight - 1
    Var p As New Point(x, y)
    // We must check the triangles first, then the centre and then 
    // the rectangles.
    If PointInTriangle(p, va, v1, v6) Then
      // Top left triangle (4).
      mMouseMap(x, y) = New Delta(-1, -1)
    ElseIf PointInTriangle(p, v1, vb, v2) Then
      // Top right triangle (1).
      mMouseMap(x, y) = New Delta(-1, 0)
    ElseIf PointInTriangle(p, v4, v3, vc) Then
      // Bottom right triangle (2).
      mMouseMap(x, y) = New Delta(1, 0)
    ElseIf PointInTriangle(p, v4, v5, vd) Then
      // Bottom left triangle (3).
      mMouseMap(x, y) = New Delta(1, -1)
    ElseIf y <= tileHeight Then
      // Centre.
      mMouseMap(x, y) = New Delta(0, 0)
    ElseIf x <= cellWidth / 2 Then
      // Bottom left rectangle (3).
      mMouseMap(x, y) = New Delta(1, -1)
    Else
      // It's in the bottom right rectangle (2).
      mMouseMap(x, y) = New Delta(1, 0)
    End If
  Next y
Next x

Before you ask, mMouseMap() is a two dimensional array (mMouseMap(localX, localY)) where the value of each element is the delta to apply for both the row and the column. This is stored in the class Delta:

Class Delta
  Property Row As Integer
  Property Column As Integer

  Sub Constructor(row As Integer, column As Integer)
    Self.Row = row
    Self.Column = column
  End Sub
End Class

The PointInTriangle method determines if a point is within a triangle by checking which side of the half-plane created by the edges the point is:

Function PointInTriangle() As Boolean
  Var d1, d2, d3 As Double
  Var has_neg, has_pos As Boolean

  d1 = Sign(p, v1, v2)
  d2 = Sign(p, v2, v3)
  d3 = Sign(p, v3, v1)

  has_neg = (d1 < 0) Or (d2 < 0) Or (d3 < 0)
  has_pos = (d1 > 0) Or (d2 > 0) Or (d3 > 0)

  Return Not (has_neg And has_pos)
End Function

The Sign() function is a little helper:

Function Sign(p1 As Point, p2 As Point, p3 As Point) As Double
  Return (p1.X - p3.X) * (p2.Y - p3.Y) - (p2.X - p3.X) * (p1.Y - p3.Y)
End Function

At this point, we now have an array that we can quickly look up a local mouse cell coordinate and get the delta to apply. I create the mouse map array once when the game loads and that saves loads of maths on every mouse move.

1 Like

found a hexagon nerd website :exploding_head:

Yep. That site is crazy good. The pathfinding algorithms are excellent too.

Color is complicated, there are too many points of failure.

Now I’ve slept on it, you don’t need to use the HSV space, RGB would do. the solution I would propose is close to what @RudolfJ suggested.

Basically a fuzzy detection as opposed to a precise detection. if abs( source.channel - checkColor.channel) < gate then this channel is within range.

Or you can store the result of the abs( source.channel - checkColor.channel) and use the smallest value to determine which checkColor is it closest to.

1 Like