Header Image
Header Image Jarl K. Holta

Best known as slacky.
I have nearly 20 years of self-taught programming experience, specialize in tackling challenges in computer vision and computational geometry.

Links

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Solving doors by Anchor in RS

75 views 1317 words

Solving doors in RS by Anchor

Code: https://gist.github.com/slackydev/9da5b818db0ecd89aa846af999db5314

You can see it in action here: https://i.giphy.com/8nwVZiukGLZe5njsmE.webp

This solution assumes that you have an anchor that is either perpendicular or parallel to the door.

Closed door as parallel to an anchor

image

Open door as perpendicular to anchor showing it's distance

image

This is the basis of this concept.

The function itself looks like:

function IsDoorOpen(Door, Anchor: TPointArray; Split: Double; Inverse: Boolean = False; UseMean: Boolean = False): EDoorState;

Argument description:

  Input: 
  Door:   The TPA of the door
  Anchor: The TPA of the Anchor
  Split:  This is the cutoff-point between a closed and an open door
  Inverse: If the door is not parallel then this should be True (auto False)
  UseMean: Mean may give less accurate distance value, so it may result in (more) false-positives
           But it may be less prone to noisy pixels from color-finding.  

  Return `dsOpen` if the door is open based on given input data 
  if the method fails it will return `dsUndefined`, otherwise dsFalse; `ds` is short for doorstate 

So for our example we get very a clear cutoff-point. Open door is registered as 75px, and a closed door registers as 90px. The difference between the two states is 90-75=15px. You may therefor now think that 7px or 8px is the cutoff, however it's a bit more involved than that. The code now transforms these two coordinates into 2d space using minimap-to-mainscreen. This transform makes the code invariable to zoom level, since the minimap isn't affected by zoom.

So at the top of the code you find

{$DEFINE DOOR_DEBUG_DISTANCE}

This plots the actual distance number in 2d minimap space from anchor to the door. So in this example we get:

10.58017063 and 14.25140381 pixels in 2d minimap space.

Now the cutoff is roughly the middle of those usually.

> 14.25140381 - 10.58017063 = 3.67123318
> 3.67123318 / 2            = 1.83561659
> 10.58017063 + 1.83561659  = 12.41578722

We now know the cutoff is about 12.4

state := IsDoorOpen(Door, Anchor, 12.4, False, False);

When we set UseMean=False the method will use the closest points in the two TPAs. So the pixel in the anchor that is nearest the door, and the pixel in the door that is nearest the anchor. This produces a very reliable, consistent distance measure when you can reliably capture the door and anchor TPAs.

image image


If we set UseMean=True then the middle point of the door and the middle point of the anchor will be used for our distance measurement, as shown in the images.

image image

Also here we got very good difference between the anchor and door in the states, 108px, and 93px. But this is actually more prone to error as the TPA will fluctuate more with varying distance to the door and different compass angles, since the mean is computed in two dimensions, while the game distance and angle is actually projected in 3 dimensions. So you have been warned. We counter some of this error by converting to minimap space, but this works much better for when we dont set UseMean=True.

> 16.11234665 - 14.2799902 = 3.67123318
> 1.83235645 / 2           = 0.916178225
> 14.2799902 + 0.916178225 = 15.196168425

state := IsDoorOpen(Door, Anchor, 15.2, False, True);


Let's have a peek at the finder code, you can use your own finders, but i'll just walk through the finding that is built in.

procedure FindDoorAndAnchor(WorldPos: TPoint; out Door, Anchor: TPointArray);
var
  ledger: TPointArray;
  ATPA: T2DPointArray;
  AnchorSearchBox, DoorSearchBox: TBox;
begin
  // rough area of where the anchor and door is (world map coordinates)
  AnchorSearchBox := RSW.GetTileMSEx(WorldPos, [4262, 1964]).Expand(30).Bounds();
  DoorSearchBox   := RSW.GetTileMSEx(WorldPos, [4248, 1958]).Expand(30).Bounds();

  DoorSearchBox.LimitTo(MainScreen.Bounds);
  AnchorSearchBox.LimitTo(MainScreen.Bounds);

  // find the anchor, and the door tpas (regular color-data from ACA)
  srl.FindColors(anchor, CTS2(12308179, 1, 0.01, 0.01), AnchorSearchBox);
  srl.FindColors(door,   CTS2(2060172, 10, 0.06, 1.33), DoorSearchBox);

  // basic noise filtering
  ATPA := door.Erode(1).Grow(1).Cluster(2);
  door := ATPA.Biggest();

  // basic noise filtering, small object, so I dont erode noise.
  ATPA := anchor.Cluster(2);
  anchor := ATPA.Biggest();
end;

First we define where the anchor is in terms of world coord, and where the door is in terms of world coord. [4262, 1964], [4248, 1958] are the world coord positions for anchor and door respectively. Replace this with your locations.

  AnchorSearchBox := RSW.GetTileMSEx(WorldPos, [4262, 1964]).Expand(30).Bounds();
  DoorSearchBox   := RSW.GetTileMSEx(WorldPos, [4248, 1958]).Expand(30).Bounds();
function TRSWalker.GetTileMSEx(Me, Loc: TPoint; Height:Double=0; Offx,Offy:Double=0): TRectangle; 

GetTileMSEx takes a high as well, for rough estimate, if your anchor is higher up than ground level you probably want to pass it's height (roughly). The top of a desk is about 4 high, while the top of a door is about 8 high. Often you can just drop passing height for our usage, but tweaking these may actually result in slightly better finding, notably when you pass anchor's height. Door is not as important, but passing 4 (average of the door height) may yield better finding.

But instead if that I actually just call .Expand(30) for both in the above code to get a larger search area. You can therefor leave this as is I guess.

  // find the anchor, and the door tpas (regular color-data from ACA)
  srl.FindColors(anchor, CTS2(1776416, 1,  0.01, 0.01), AnchorSearchBox);
  srl.FindColors(door,   CTS2(1993096, 13, 0.08, 1.02), DoorSearchBox);

Then there is the color-finding, as described you want to pass it the color of the anchor (first one) and the color of the door (second one). For this ACA in simba will help you a lot! You want to do this for both the anchor and the door.

image


Picking an anchor, anchors should be an easy to find item that is in visible range from where you want to open the door from, that is also visible from the angles your script operates from. Often you want to pick something very distinctive, i chose the sign in my example since that sign will never change color, it will always be in the same position as well, and is perfectly parallel to the closed door, this is an optimal result.

Anchor should always be parallel to the closed door, or parallel to the door when it's open. Meaning the knob on the door should point towards the anchor. Naturally finding such a perfect anchor is not always possible. But if the anchor is facing away from the door(knob) when it's closed it will not work at all.

The regions to look for an anchor can be look at like this:

image

The curve marked in black is the direction of the door, how it opens in my example. Due to this the arc marked in green is where you want to look for an anchor. While the area marked in red will cause the anchor to fail. This is not a perfectly accurate description, but should give you a decent pinpoint when solving for your door. Generally speaking, just avoid getting close to diagonals, as they will not work with this design, a diagonal anchor will cause the distance to never change between door states.


When dealing with anchors that get closer to a diagonal, because you can't find any better reliable anchor you may need to rotate the camera a few times while debugging the distances, and tweak the split distance. This can often be done by eyeballing it, at least after a bit of practice.


Looking back at the function definition:

function IsDoorOpen(Door, Anchor: TPointArray; Split: Double; Inverse: Boolean = False; UseMean: Boolean = False): EDoorState;

I want to look at the parameter named Inverse, the code assumes that the closed door is parallel to an anchor (knob points towards anchor). However if the door is open when it is parallel to the anchor you need to set Inverse to True.