Geofencing with Pytrack



  • Hi,

    I’ve got a Lopy with Pytrack and managed to print lat/long coordinates.

    As a second step, I’d like to add geofence capabilities. I’ve found the Picket library which seems ideal for this: https://github.com/sam-drew/picket

    Has anyone used this library before? Not 100% sure whether it is compatible with Lopy given that the library is Python and not Micropython.

    As a first test, I took one of the examples from here: https://github.com/sam-drew/picket/blob/master/test_picket.py

    import picket
    
    def test_in_fence_easy():
    	my_fence = picket.Fence()
    
    	my_fence.add_point((1, 1))
    	my_fence.add_point((3, 1))
    	my_fence.add_point((3, 3))
    	my_fence.add_point((1, 3))
    
    	assert my_fence.check_point((2, 2))
    

    I only get a blank line as a result when I run this with the Pymark plugin in Atom. Was hoping for something like ‘True’ or ‘False’ depending on whether the point is inside or outside the polygon.

    What am I missing? I'd really appreciate if someone can point me in the right direction.

    Thanks for your help!

    Disclaimer: I’m a complete beginner who is nonetheless fascinated by this stuff. Hoping that you can provide simple answers to my (stupid) questions :) Thank you!



  • @jcaron that seems to have fixed it. Initial results look positive, will do some more testing in the next days.

    Thank you so much for sorting this out!



  • @newkit OK, so a few issues:

    • My python is indeed rusty and I used Javascript object syntax rather than python dict. So it's fence['geometry']['coordinates'], not fence.geometry.coordinates and found_fence['properties']['name'], not found_fence.properties.name.
    • The list of coordinates is in an inner array inside coordinates (I suppose your JSON file can have multiple polygons for a single feature?), so it's really fence['geometry']['coordinates'][0]
    • The list of fences is not at the top level in the JSON, but within features, so it's polygondata['features'], not polygondata.
    • PiP expects x and y as separate args, not an array, so it's PiP(coords[0], coords[1], fence['geometry']['coordinates'][0])
    • () where not in the same place in the print when a match is found.

    Final result (tested on python 3.7, don't have a Pycom device at hand right now, you'll have to change the json import):

    import json
    input_file = open ('polygon.json')
    polygondata = json.load(input_file)
    
    def PiP(x: int, y: int, poly) -> bool:
    
        num = len(poly)
        i = 0
        j = num - 1
        c = False
        for i in range(num):
            if ((poly[i][1] > y) != (poly[j][1] > y)) and \
                    (x < poly[i][0] + (poly[j][0] - poly[i][0]) * (y - poly[i][1]) /
                                      (poly[j][1] - poly[i][1])):
                c = not c
            j = i
        return c
    
    def check_all_fences(coords, fences):
     for fence in fences:
      if PiP(coords[0], coords[1], fence['geometry']['coordinates'][0]):
       return fence
     return None
    
    coords = (48, 0)
    #coords = l76.coordinates()
    found_fence = check_all_fences(coords, polygondata['features'])
    
    if (found_fence != None):
        print ("Coords in fence {}".format(found_fence['properties']['name']))
    else:
        print ("Coords not in any fence")
    

    Note that in addition to the previous caveats, it only tests again the first set of coordinates in a feature.



  • Thanks @jcaron, that makes sense. But it just seems to shift the error to geometry:

    Traceback (most recent call last):
      File "main.py", line 60, in <module>
      File "main.py", line 55, in check_all_fences
    AttributeError: 'str' object has no attribute 'geometry'
    

    The previous json file showed the same coordinates error as in the previous post. Initially I thought my json format was the issue, therefore I created the new file.

    I also tried various changes around this code:

    with open('polygon.json') as j:
        polygondata = json.load(j)
    

    E.g. json.loads(j), json.load(‘j’), or

    input_file = open ('polygon.json')
    polygondata = json.load(input_file)
    

    It doesn't solve the issue though.



  • @newkit Your JSON is not structured as it was in your previous post. It had coordinates as a direct attribute of each fence, while it is now one level down inside geometry:

        {
          "type": "Feature",
          "properties": {
            "name": "Fence 1"
          },
          "geometry": {
            "type": "Polygon",
            "coordinates": [
    

    So it should be fence.geometry.coordinates instead. Of course the code assumes you only have polygons at this stage, if you have other types the code would need to be adapted.

    Likewise, the name is also one level down inside properties, so it should be found_fence.properties.name a bit further down.



  • Ok, I’ve tried lots of different things in the meanwhile. But I just don’t get it to work. Frustrating!

    @jcaron @Gijs I know I probably pushed the limits of your willingness to help a newbie with his questions. Really appreciate you got me this far! I feel like I’m so close though to get this to work. Perhaps you can have another look?

    I’ve put the earlier PiP error to the side for now, as I can circumvent it by including the function directly in main.py.

    However I’m going in circles regarding the coordinates error:

    Traceback (most recent call last):
      File "main.py", line 60, in <module>
      File "main.py", line 55, in check_all_fences
    AttributeError: 'str' object has no attribute 'coordinates'
    

    Line 55 is the third line below:

    def check_all_fences(coords, fences):
     for fence in fences:
      if PiP(coords, fence.coordinates):
       return fence
     return None
    
    coords = l76.coordinates()
    found_fence = check_all_fences(coords, polygondata)
    
    if (found_fence != None):
        print ("Coords in fence {}").format(found_fence.name)
    else:
        print ("Coords not in any fence")
    

    This is the PiP function:

    def PiP(x: int, y: int, poly) -> bool:
    
        num = len(poly)
        i = 0
        j = num - 1
        c = False
        for i in range(num):
            if ((poly[i][1] > y) != (poly[j][1] > y)) and \
                    (x < poly[i][0] + (poly[j][0] - poly[i][0]) * (y - poly[i][1]) /
                                      (poly[j][1] - poly[i][1])):
                c = not c
            j = i
        return c
    

    I've created a new JSON file in the meanwhile: polygon.py (saved as .py as I can't upload json)

    Please let me know if you need any more info.

    Thank you!



  • Thanks for clarifying @jcaron

    I’ve tested the code. First, I encountered a few synthax errors in my project which I was able to fix. But now I’m stuck with a couple of errors regarding my PiP function and the coordinates from the json file.

    First the PiP error:

    Traceback (most recent call last):
      File "main.py", line 46, in <module>
      File "main.py", line 41, in check_all_fences
    NameError: name 'PiP' isn't defined
    
    

    I’ve tested integrating PiP directly into main.py, which is working. So that means the function itself is ok, but I’m not importing it properly from polygoncheck.py (see file in previous post) into main.py?

    I've got the following in main.py:

    import polygoncheck
    from polygoncheck import PolygonCheck
    

    Now the coordinates error:

    Traceback (most recent call last):
      File "main.py", line 60, in <module>
      File "main.py", line 55, in check_all_fences
    AttributeError: 'dict' object has no attribute 'coordinates'
    

    This seems to suggest something is wrong with my json structure (see previous post). Or perhaps I’m not correctly loading the json data into main.py?

    with open('polygon.json') as j:
        polygondata = json.load(j)
    

    I've tested the code above by printing the polygondata, which is working. So it looks like it's the json format.

    Any ideas on both errors?

    And lastly a very stupid question :) how do you highight in light red in the text editor?



  • @newkit

    • fences is the list of fences the check_all_fences function will check against the provided coordinates. When we call the function, we pass polygondata which is the list you loaded from the JSON file.

    • fence is each individual fence in turn (for a in b is a loop which will run for each element of b, with the current element being available as ainside the loop).

    • fence.coordinates is the list of points of a given fence, from the coordinates field of each fence in you JSON file.

    fences is local to the check_all_fences function. fence is local to the for ... in loop.



  • @jcaron Great, I will test this. Thanks!

    I've to admit though I'm slightly confused with the different terminology. Coords and polygondata is clear, but what exactly does fence, fences, and fence.coordinates refer to?



  • @newkit You should not call PiP directly with the full list, it expects just one polygon at a time. You can it in the loop (instead of the check_point).

    Probably something along the lines of:

    def check_all_fences(coords, fences):
     for fence in fences:
      if PiP(coords, fence.coordinates):
       return fence
     return None
    
    coords = l76.coordinates()
    found_fence = check_all_fences(coords, polygondata)
    if (found_fence != None):
      print "Coords in fence {}".format(found_fence.name)
    else:
      print "Coords not in any fence"
    

    I didn't try it and my Python is rusty but it shouldn't be very far from that.



  • Thanks for your previous input @jcaron and @Gijs

    I had some more time now to work on this. However, I’m getting lost in a few places. Just a quick reminder of what I’m trying to achieve: check whether my lopy coordinates are inside a set of static polygons stored in a json file. If yes, I want to print the name of the specific polygon that matches my coordinates.

    So here’s my progress:

    I stored a few test polygons in a json file. Initially I tried a geojson file, as this seemed more appropriate for polygons. However, it does not seem possible to upload geojson to the Lopy (via Atom). Should this generally be possible?

    My json file has the following format:

    [
    {
    	"name":"Polygon1",
    	"type": "Polygon",
    	"coordinates": [[40,0],[40,1],[41,1],[41,0],[40,0]]
    },
    {
            "name":"Polygon2",’
    	"type": "Polygon",
    	"coordinates": [[50,0],[50,1],[51,1],[51,0],[50,0]]
    },
    …
    ]
    

    Does that seem correct?

    I’ve stored the json file in my project and then load the data into the main.py
    with:

    import json
    with open('polygon.json') as j:
         polygondata = json.load(j) 
    

    I know @Gijs suggested json.dumps but if I understand it correctly this is to convert from Python to JSON. Since I created a separate JSON file, I assume I need to use json.load to convert this data to Python in order to use it in main.py. Correct?

    The next element is the PiP function. I’ve created the PiP function in a separate file in the library called Polygoncheck.py

    cbcecb80-e53c-4a5a-90b5-beb35b8f0621-image.png

    class PolygonCheck:
    
    def PiP(x: int, y: int, poly) -> bool:
    
        num = len(poly)
        i = 0
        j = num - 1
        c = False
        for i in range(num):
            if ((poly[i][1] > y) != (poly[j][1] > y)) and \
                    (x < poly[i][0] + (poly[j][0] - poly[i][0]) * (y - poly[i][1]) /
                                      (poly[j][1] - poly[i][1])):
                c = not c
            j = i
        return c
    

    Now comes the code from @jcaron:

    coords = l76.coordinates()
    if (PiP(coords[0], coords[1], polygondata)):
    

    I’ve changes ‘fences’ to ‘polygondata’, assuming this needs to take the data from my json file?

    Should the above work together with my PiP function in its current state? Or do I need to tweak it?

    I assume the code just generally checks whether my lopy coordinates fall within any of the polygons?

    If so, how do identify (and print) the specific polygon the coordinates fall into?

    I remember that’s where the loop function from @Gijs comes in.

    def check_all_fences(fences, coordinate):
     for n in fences:
      if not n.check_point(coordinate)
       return False #point is outside one of the fences
     return True # point is inside all fences
    

    But how do I combine both pieces of code together?

    As you can see, lots of questions and I probably mixed up a few things here.

    Hoping you can help me untangle this and bring everything together.

    Thank you!



  • @newkit said in Geofencing with Pytrack:

    Do you think that should work or does it need tweaking?

    A quick glance does not reveal any obvious issues, but I really didn't go through it in any detail.

    Do I store this function in main.py or as a separate file in the library?

    That's your choice.

    Also I do like the solution to store the fences in a JSON or GeoJSON file. At the same moment, I’m anticipating 30 – 60 static fences. The device won’t have internet connectivity. So I guess I need to store the json file in the library as well?

    As a file somewhere in the filesystem, yes.

    Now I’m left wondering how to I combine the above function with my json file and l76 coordinates. Would something like this work?

    if(fence.json.PiP(l76.coordinates())):
    

    The function is not a method of a class, but takes the "fence" (polygon) as an argument. It also expects the coordinates as separate arguments rather than a tuple. So it would probably look more like:

    coords = l76.coordinates()
    if (PiP(coords[0], coords[1], fence)):
    

    or something similar.

    How can I print the name of the specific fence my coordinates are in?

    Or perhaps the loop function suggested by @Gijs is a better approach? Just not quite sure how to bring everything together.

    Yes, you would do that in your loop. If your JSON is structured like:

    [
      {
        "name": "Home",
        "points": [[40,0],[40,1],[41,1],[41,0]]
      },
      {
        ... other fences
      }
    ]
    

    Then you just iterate over the array, and you avec access to the points (to call PiP) and name (to display it if that matches) at each iteration.



  • Ok I have a clearer idea now how everything fits together, but still have quite a few ‘beginners’ questions around the details. Thanks for your patience :)

    I do like the shorter JS code (thanks @jcaron), as I don't really need 90% from the picket library.

    Can't translate it though, as my coding skills are minimal. But I found the below Python code which seems to do the same thing.

    def PiP(x: int, y: int, poly) -> bool:
    
        num = len(poly)
        i = 0
        j = num - 1
        c = False
        for i in range(num):
            if ((poly[i][1] > y) != (poly[j][1] > y)) and \
                    (x < poly[i][0] + (poly[j][0] - poly[i][0]) * (y - poly[i][1]) /
                                      (poly[j][1] - poly[i][1])):
                c = not c
            j = i
        return c
    
    

    Do you think that should work or does it need tweaking?

    Do I store this function in main.py or as a separate file in the library?

    Also I do like the solution to store the fences in a JSON or GeoJSON file. At the same moment, I’m anticipating 30 – 60 static fences. The device won’t have internet connectivity. So I guess I need to store the json file in the library as well?

    Now I’m left wondering how to I combine the above function with my json file and l76 coordinates. Would something like this work?

    if(fence.json.PiP(l76.coordinates())):
    

    How can I print the name of the specific fence my coordinates are in?

    Or perhaps the loop function suggested by @Gijs is a better approach? Just not quite sure how to bring everything together.



  • @GreatMinds that's excellent input. You have given me lots to think about. Will probably take a little while for my 'not so great' mind to digest :) In the meanwhile, have a great weekend!


  • Global Moderator

    @jcaron great minds think alike :)



  • @newkit Where you store the fences is up to you. But are they static? If they change over time you probably want to download somehow (if you have Internet connectivity on your device) and store them as a separate JSON file, for instance.

    The library you use only has the concept of a single fence, not a collection of fences, so you would need to iterate over each fence and check if the point is inside. If you have a large number of fences you may need some way to filter fences that have any chance of being relevant. If the number is really large, you probably need to use some form of spatial index.

    Note that when I looked at the source the other day I found the code to be very complex, it seemed to be there was a much quicker and shorter way of doing it, so I checked back code from a different project (in Javascript), and checking whether a point is inside a polygon can indeed be done in a much much much more compact manner.

    Here's the JS code I use, not really sure where it came from, though:

          function pointInPolygon(x, y, points)
          {
              var inside = false;
              for (var i = 0, j = points.length - 1; i < points.length; j = i++)
              {
                      var xi = points[i][0], yi = points[i][1];
                      var xj = points[j][0], yj = points[j][1];
      
                      var intersect = ((yi > y) != (yj > y)) &&
                                      (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
                      if (intersect)
                              inside = !inside;
              }
              return inside;
          }
    

    I think conceptually it does the same thing (uses the ray-casting algorithm), but as you can see, it takes a dozen lines instead of 200 (it does not include all the class semantics, though). It should be trivial to translate to Python.


  • Global Moderator

    For a large number of fences, I would probably load them from a JSON file, making it easy to change any coordinates if necessary, and reupload the file.
    You can use the following to read JSON files and convert them:

    import json
    f = open('fence.json', 'r')
    data = json.dumps(f)
    

    You can find more info about using JSON files in python on the internet

    Next, you could make a function in main.py that loops through all the fences to check them. I would probably go about creating an array of all your fences and looping through them, something like this:

    def check_all_fences(fences, coordinate):
     for n in fences:
      if not n.check_point(coordinate)
       return False #point is outside one of the fences
     return True # point is inside all fences
    
    


  • @jcaron @Gijs This seems to work reliably now. Thank you both!

    I’d like to ask a few follow-up questions:

    Let’s say I want to build a large number of fences. Do I store this in main.py or is it more efficient to store this in the library somehow. If the latter, what’s the best way to do this?

    And then in order to check if coordinates are inside any of the fences, do I need if statements in main.py for each fence? Or is there perhaps a more elegant solution to check for all fences at the same time? This function would need to be able to identify the specific fence and call a specific action based on that.



  • @jcaron Of course, you’re right! I copied the coordinates from a polygon creator tool without noticing the different order.

    Now that I switched the coordinates, it seems to work. Will do some further testing.

    Thanks for taking an interest in this project!



  • @newkit The point you check needs to have the same lat/long order as the points you provide.

    As l76.coordinates() returns (lat, long), you should provide the points of your fence in the same format: lat, long, not long, lat.

    This is a common issue with coordinates, not everybody agrees in which order they should be given.


Log in to reply
 

Pycom on Twitter