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!
-
Anyone able to help? Thanks in advance
-
Hi,
I do not know what you are doing wrong. When I try thepicket.pytogether withtest_picket.pyinserted inmain.pyand call one of the functions, it works fine for me>>> test_in_fence_easy() (3.0, 2.0) (3, 1) (3, 3) pointA[1] <= pointB[1] True True (1.0, 2.0) (1, 3) (1, 1) pointA[1] >= pointB[1] True TrueGijs
-
@Gijs hartelijk bedankt :) really appreciate you looking into this and helping out a beginner!
I wasn’t able to check your response before, but now I managed to replicate it. Very happy that the library is working!
I’m trying to combine the pytrack example with the picket library, so that I can check whether the lopy coordinates are in a pre-defined fence and call specific actions based on that.
My current project structure looks like this:

As you can see, I added picket.py into the library folder. I hope that’s correct?Now I’m wondering how to integrate the relevant picket element:
def test_in_fence_real_latlong_large_scale(): my_fence = picket.Fence() my_fence.add_point((44.030013, -44.309738)) my_fence.add_point((45.032447, 66.784004)) my_fence.add_point((-28.837860, 59.928536)) my_fence.add_point((-24.759721, -20.930834)) assert my_fence.check_point((0.447227, 24.244944))into the main.py file:
while (True): coord = l76.coordinates() #f.write("{} - {}\n".format(coord, rtc.now())) print("{} - {} - {}".format(coord, rtc.now(), gc.mem_free()))It is straightforward to replace the fence coordinates. But how do I combine
my_fence.check_point with coord = l76.coordinates()And how do I add an if true condition to that?
-
As you can see, I added picket.py into the library folder. I hope that’s correct?
Yes its correct like that!
But how do I combine my_fence.check_point with coord = l76.coordinates()
As
l76.coordinates()returns a tuple with information,(lat, long), you can directly insert that in themy_fence.check_pointfunction as it requires a tuple input, this will returnTrueorFalsefor inside / outside respectively. You can test it like so:if(my_fence.check_point(l76.coordinates())): ...(I did not test this specifically, but it should work like that)
Gijs
-
@Gijs said in Geofencing with Pytrack:
Gijs
Happy Friday Gijs. Only time of the week I manage to find some time to work on this project.
Your reply looks very promising. To test this I’ve added the following section to main.py:
my_fence = picket.Fence() my_fence.add_point((-0.9558105, 51.6759616)) my_fence.add_point((0.6976318, 51.6589266)) my_fence.add_point((0.7196045, 50.9203505)) my_fence.add_point((-0.9228516, 50.9064969)) if(my_fence.check_point(l76.coordinates())): print('device inside fence') else: print('device outside fence')However, I don't fully understand the output I get:
(5071.07, -0.5725584) (-0.9558105, 51.67596) (0.6976318, 51.65893) pointA[1] >= pointB[1] False False (2.251526, -0.5725584) (0.6976318, 51.65893) (0.7196045, 50.92035) pointA[1] >= pointB[1] False False (-6101.892, -0.5725584) (0.7196045, 50.92035) (-0.9228517, 50.9065) pointA[1] >= pointB[1] False False (1.282171, -0.5725584) (-0.9228517, 50.9065) (-0.9558105, 51.67596) pointA[1] <= pointB[1] False False [] [] device outside fenceLooking at picket.py, the output seems to correspond rather with check_in_bounds than with check_point to see if point is within polygon.
At the bottom it prints ‘device outside fence’, but my coordinates should be inside. That's the part I'm really interested in. Again my aim is to create a polygon, check if device lat/long are inside, and call if/else. Looking at the printed coordinates in the output, I can only see my latitude but not longitude.
I think my problem is that I just don’t fully understand yet how the picket.py is working, and how I can use it to achieve my aim. Perhaps I need to tweak it for my purpose.
I’m hoping you can shed some light on this.
Thank you!
-
@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, notlong, lat.This is a common issue with coordinates, not everybody agrees in which order they should be given.
-
@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!
-
@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.
-
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 -
@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.
-
@jcaron great minds think alike :)
-
@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!
-
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 cDo 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.
-
@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.
-
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

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 cNow 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 fencesBut 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 You should not call
PiPdirectly with the full list, it expects just one polygon at a time. You can it in the loop (instead of thecheck_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.
-
@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?
-
-
fencesis the list of fences thecheck_all_fencesfunction will check against the provided coordinates. When we call the function, we passpolygondatawhich is the list you loaded from the JSON file. -
fenceis each individual fence in turn (for a in bis a loop which will run for each element ofb, with the current element being available asainside the loop). -
fence.coordinatesis the list of points of a givenfence, from thecoordinatesfield of each fence in you JSON file.
fencesis local to thecheck_all_fencesfunction.fenceis local to thefor ... inloop. -
-
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 definedI’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 PolygonCheckNow 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?
Hello! It looks like you're interested in this conversation, but you don't have an account yet.
Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.
With your input, this post could be even better 💗
Register Login