locateobject 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- locateobject-0.1.0/LICENSE +9 -0
- locateobject-0.1.0/PKG-INFO +194 -0
- locateobject-0.1.0/README.md +169 -0
- locateobject-0.1.0/pyproject.toml +40 -0
- locateobject-0.1.0/setup.cfg +4 -0
- locateobject-0.1.0/src/locateobject/__init__.py +22 -0
- locateobject-0.1.0/src/locateobject/classes.py +2255 -0
- locateobject-0.1.0/src/locateobject/locate.py +314 -0
- locateobject-0.1.0/src/locateobject.egg-info/PKG-INFO +194 -0
- locateobject-0.1.0/src/locateobject.egg-info/SOURCES.txt +11 -0
- locateobject-0.1.0/src/locateobject.egg-info/dependency_links.txt +1 -0
- locateobject-0.1.0/src/locateobject.egg-info/requires.txt +9 -0
- locateobject-0.1.0/src/locateobject.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
Copyright 2026 Joost Postma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: locateobject
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Locate real-world objects by triangulating bounding boxes from multiple geotagged images
|
|
5
|
+
Author: Joost Postma
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: triangulation,locateobject,geospatial,photogrammetry,panoramax,gis
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: numpy>=2.4
|
|
16
|
+
Requires-Dist: pandas>=3.0
|
|
17
|
+
Requires-Dist: shapely>=2.1
|
|
18
|
+
Requires-Dist: geopandas>=1.1
|
|
19
|
+
Requires-Dist: pyproj>=3.7
|
|
20
|
+
Requires-Dist: scipy>=1.17
|
|
21
|
+
Requires-Dist: scikit-learn>=1.8
|
|
22
|
+
Requires-Dist: geopy>=2.4
|
|
23
|
+
Requires-Dist: haversine>=2.9
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# locateobject
|
|
27
|
+
|
|
28
|
+
Locate real-world objects by triangulating bounding boxes from multiple geotagged images.
|
|
29
|
+
|
|
30
|
+
You give `locateobject` a set of annotations (one bounding box of one object in one image, plus the camera position and orientation). It works out where the objects are, groups annotations that belong to the same object, and returns the results as GeoDataFrames you can save straight to GeoPackage.
|
|
31
|
+
|
|
32
|
+
The package exposes a single function: `locate_object()`.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install locateobject
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Requires Python 3.11 or newer.
|
|
41
|
+
|
|
42
|
+
## How it works
|
|
43
|
+
|
|
44
|
+
1. **Bearing line per annotation.** The horizontal centre of the bounding box gives a compass bearing from the camera (using the image width, field of view and camera azimuth). The vertical size of the bounding box gives the angular size of the object. Combined with the minimum and maximum real-world size of the object, this gives a *line segment* along that bearing where the object can be.
|
|
45
|
+
2. **Intersections.** Segments from different annotations that cross are candidate object locations.
|
|
46
|
+
3. **Filtering.** An intersection is kept only if the angle between the two bearings is within the allowed range (a very small angle gives poor location precision) and the two annotations estimate a similar object size.
|
|
47
|
+
4. **Clustering.** Intersections are grouped with a recursive DBSCAN clustering until every cluster is tighter than `cluster_radius_m_max`. Each annotation line can belong to at most one cluster.
|
|
48
|
+
5. **Centroids.** Each cluster becomes a centroid (the object location). Annotation lines close to a centroid are assigned to the best-scoring centroid, and centroids supported by fewer than two annotations are dropped.
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from shapely.geometry import Point
|
|
54
|
+
from locateobject import locate_object
|
|
55
|
+
|
|
56
|
+
# A 360 degree panorama, 8192 px wide. The object is a 1 m tall sign
|
|
57
|
+
# about 10 m north of camera 1.
|
|
58
|
+
records = [
|
|
59
|
+
{
|
|
60
|
+
"origin": Point(5.0000, 52.0000), # (longitude, latitude)
|
|
61
|
+
"picture_field_of_view": 360,
|
|
62
|
+
"picture_azimuth": 0,
|
|
63
|
+
"picture_size_horizontal_px": 8192,
|
|
64
|
+
"picture_size_vertical_px": 4096,
|
|
65
|
+
"bbox": [[4046, 1000], [4146, 1000], [4146, 1130], [4046, 1130], [4046, 1000]],
|
|
66
|
+
"object_minimum_size_meters": 0.5,
|
|
67
|
+
"object_maximum_size_meters": 1.5,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"origin": Point(5.000146, 52.0000), # about 10 m east of camera 1
|
|
71
|
+
"picture_field_of_view": 360,
|
|
72
|
+
"picture_azimuth": 0,
|
|
73
|
+
"picture_size_horizontal_px": 8192,
|
|
74
|
+
"picture_size_vertical_px": 4096,
|
|
75
|
+
"bbox": [[3026, 1000], [3118, 1000], [3118, 1092], [3026, 1092], [3026, 1000]],
|
|
76
|
+
"object_minimum_size_meters": 0.5,
|
|
77
|
+
"object_maximum_size_meters": 1.5,
|
|
78
|
+
},
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
labels, centroids, clusters, inter_reliable, inter_all, annotations = locate_object(records)
|
|
82
|
+
|
|
83
|
+
print(labels) # centroid id for each input record, -1 if unassigned
|
|
84
|
+
print(centroids) # one row per located object
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Save the results for use in QGIS or other GIS software:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
centroids.to_file("result.gpkg", layer="centroids", driver="GPKG")
|
|
91
|
+
annotations.to_file("result.gpkg", layer="annotations", driver="GPKG")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Input
|
|
95
|
+
|
|
96
|
+
`records_input` is a list of dictionaries (`ObjectAnnotationInput`), one per object in one image:
|
|
97
|
+
|
|
98
|
+
| Key | Type | Description |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `origin` | `shapely.geometry.Point` | Camera position. `x` = longitude, `y` = latitude (EPSG:4326). |
|
|
101
|
+
| `picture_field_of_view` | `float` or `int` | Horizontal field of view of the image in degrees (360 for a full panorama). |
|
|
102
|
+
| `picture_azimuth` | `float` or `int` | Compass direction of the image centre in degrees (0 = north, clockwise). |
|
|
103
|
+
| `picture_size_horizontal_px` | `int` | Image width in pixels. |
|
|
104
|
+
| `picture_size_vertical_px` | `int` | Image height in pixels. |
|
|
105
|
+
| `bbox` | `list[list[int]]` | Polygon of the object in pixel coordinates, `[[x1, y1], [x2, y2], ...]`, at least 3 points, integers only. If the first and last point differ, the polygon is closed automatically. |
|
|
106
|
+
| `object_minimum_size_meters` | `float` or `int` | Smallest plausible real-world height of the object, in metres. |
|
|
107
|
+
| `object_maximum_size_meters` | `float` or `int` | Largest plausible real-world height of the object, in metres. |
|
|
108
|
+
|
|
109
|
+
The size of the object is taken from the **vertical** extent of the bounding box, so the minimum and maximum sizes refer to the object's height. The wider the range between minimum and maximum, the longer the line along which the object can be located.
|
|
110
|
+
|
|
111
|
+
Annotations with an invalid bounding box or a line that cannot be calculated are skipped with a warning.
|
|
112
|
+
|
|
113
|
+
## Parameters
|
|
114
|
+
|
|
115
|
+
| Parameter | Default | Description |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| `intersection_angle_deg_min` | `15` | Minimum angle in degrees between two bearings. Small angles give imprecise locations. |
|
|
118
|
+
| `intersection_angle_deg_max` | `None` (360) | Maximum angle between two bearings. Use it to exclude cameras that cannot plausibly see the same object. |
|
|
119
|
+
| `object_size_score` | `0.8` | Minimum size agreement (0 to 1) between the two annotations of an intersection. A score of 1 means identical size estimates. |
|
|
120
|
+
| `cluster_radius_m_max` | `1` | Maximum distance in metres between a cluster centre and its intersections. Also the maximum distance from a centroid to an annotation line for it to be assigned. |
|
|
121
|
+
| `cluster_min_size` | `1` | Minimum number of intersections for a cluster (DBSCAN `min_samples`). |
|
|
122
|
+
| `cluster_max_depth` | `None` | Maximum recursion depth of the clustering. By default it is derived from `cluster_epsilon_decrease`. |
|
|
123
|
+
| `cluster_epsilon_start` | `None` | Starting DBSCAN radius in metres. Defaults to `cluster_radius_m_max`. |
|
|
124
|
+
| `cluster_epsilon_decrease` | `0.8` | Factor by which the DBSCAN radius shrinks at each recursion level. |
|
|
125
|
+
|
|
126
|
+
## Output
|
|
127
|
+
|
|
128
|
+
`locate_object()` returns a tuple of six items:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
labels, centroids, clusters, intersections_reliable, intersections, annotations = locate_object(records)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
1. **`labels`**: list of integers, one per input record, in the same order. The `id` of the centroid the record belongs to, or `-1` if it was not assigned.
|
|
135
|
+
2. **`centroids`**: GeoDataFrame (points) with the final object locations.
|
|
136
|
+
3. **`clusters`**: GeoDataFrame (points) with the cluster centres before the final assignment.
|
|
137
|
+
4. **`intersections_reliable`**: GeoDataFrame (points) with the intersections that passed the angle and size filters.
|
|
138
|
+
5. **`intersections`**: GeoDataFrame (points) with all intersections found.
|
|
139
|
+
6. **`annotations`**: GeoDataFrame (lines) with the location line of every valid annotation.
|
|
140
|
+
|
|
141
|
+
All GeoDataFrames use `EPSG:4326`. If there is no result, empty GeoDataFrames are returned and `labels` contains only `-1`.
|
|
142
|
+
|
|
143
|
+
### Columns
|
|
144
|
+
|
|
145
|
+
**`centroids`**
|
|
146
|
+
|
|
147
|
+
| Column | Description |
|
|
148
|
+
|---|---|
|
|
149
|
+
| `id` | Centroid id, matches `labels` and `annotations.centroid_id`. |
|
|
150
|
+
| `object_size_meter` | Mean estimated object size in metres. |
|
|
151
|
+
| `object_size_meter_sd` | Standard deviation of the size estimates. |
|
|
152
|
+
| `object_azimuth` | Direction in degrees from which the object is seen head-on (the average viewing direction plus 180). |
|
|
153
|
+
|
|
154
|
+
**`clusters`**: `object_size_meter`, `object_size_meter_sd`, `object_azimuth` (as above).
|
|
155
|
+
|
|
156
|
+
**`intersections_reliable`** and **`intersections`**
|
|
157
|
+
|
|
158
|
+
| Column | Description |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `angle` | Angle in degrees between the two bearings. |
|
|
161
|
+
| `object_size_average_meter` | Average of the two size estimates at the intersection. |
|
|
162
|
+
| `object_size_score` | Size agreement between the two annotations (0 to 1). |
|
|
163
|
+
| `line1`, `line2` | Index in `records_input` of the two annotations. |
|
|
164
|
+
|
|
165
|
+
**`annotations`**
|
|
166
|
+
|
|
167
|
+
| Column | Description |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `id` | Index of the record in `records_input`. |
|
|
170
|
+
| `centroid_id` | Assigned centroid, or missing if the annotation was not assigned. |
|
|
171
|
+
| `origin_longitude`, `origin_latitude` | Camera position. |
|
|
172
|
+
| `line_azimuth` | Compass bearing of the line in degrees. |
|
|
173
|
+
| `object_distance_meter` | Distance from camera to the centroid (only if assigned). |
|
|
174
|
+
| `object_size_meter` | Object size estimated from this annotation at the centroid (only if assigned). |
|
|
175
|
+
|
|
176
|
+
## Notes and limitations
|
|
177
|
+
|
|
178
|
+
- The distance estimate assumes the field of view and pixel size describe an undistorted horizontal view (as in equirectangular panoramas). Strongly distorted images give less accurate results.
|
|
179
|
+
- Pixel coordinates have `x` increasing to the right, and `x` wraps around the image seam for panoramas.
|
|
180
|
+
- The intersection search works in a local metric projection and is meant for areas of limited size (around 100 km by 100 km).
|
|
181
|
+
- At least two annotations from different positions are needed to locate an object.
|
|
182
|
+
|
|
183
|
+
## Logging
|
|
184
|
+
|
|
185
|
+
The package uses the standard `logging` module and attaches a `NullHandler`, so it is silent by default. To see progress and warnings:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
import logging
|
|
189
|
+
logging.basicConfig(level=logging.INFO)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT, see the `LICENSE` file.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# locateobject
|
|
2
|
+
|
|
3
|
+
Locate real-world objects by triangulating bounding boxes from multiple geotagged images.
|
|
4
|
+
|
|
5
|
+
You give `locateobject` a set of annotations (one bounding box of one object in one image, plus the camera position and orientation). It works out where the objects are, groups annotations that belong to the same object, and returns the results as GeoDataFrames you can save straight to GeoPackage.
|
|
6
|
+
|
|
7
|
+
The package exposes a single function: `locate_object()`.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install locateobject
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires Python 3.11 or newer.
|
|
16
|
+
|
|
17
|
+
## How it works
|
|
18
|
+
|
|
19
|
+
1. **Bearing line per annotation.** The horizontal centre of the bounding box gives a compass bearing from the camera (using the image width, field of view and camera azimuth). The vertical size of the bounding box gives the angular size of the object. Combined with the minimum and maximum real-world size of the object, this gives a *line segment* along that bearing where the object can be.
|
|
20
|
+
2. **Intersections.** Segments from different annotations that cross are candidate object locations.
|
|
21
|
+
3. **Filtering.** An intersection is kept only if the angle between the two bearings is within the allowed range (a very small angle gives poor location precision) and the two annotations estimate a similar object size.
|
|
22
|
+
4. **Clustering.** Intersections are grouped with a recursive DBSCAN clustering until every cluster is tighter than `cluster_radius_m_max`. Each annotation line can belong to at most one cluster.
|
|
23
|
+
5. **Centroids.** Each cluster becomes a centroid (the object location). Annotation lines close to a centroid are assigned to the best-scoring centroid, and centroids supported by fewer than two annotations are dropped.
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from shapely.geometry import Point
|
|
29
|
+
from locateobject import locate_object
|
|
30
|
+
|
|
31
|
+
# A 360 degree panorama, 8192 px wide. The object is a 1 m tall sign
|
|
32
|
+
# about 10 m north of camera 1.
|
|
33
|
+
records = [
|
|
34
|
+
{
|
|
35
|
+
"origin": Point(5.0000, 52.0000), # (longitude, latitude)
|
|
36
|
+
"picture_field_of_view": 360,
|
|
37
|
+
"picture_azimuth": 0,
|
|
38
|
+
"picture_size_horizontal_px": 8192,
|
|
39
|
+
"picture_size_vertical_px": 4096,
|
|
40
|
+
"bbox": [[4046, 1000], [4146, 1000], [4146, 1130], [4046, 1130], [4046, 1000]],
|
|
41
|
+
"object_minimum_size_meters": 0.5,
|
|
42
|
+
"object_maximum_size_meters": 1.5,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"origin": Point(5.000146, 52.0000), # about 10 m east of camera 1
|
|
46
|
+
"picture_field_of_view": 360,
|
|
47
|
+
"picture_azimuth": 0,
|
|
48
|
+
"picture_size_horizontal_px": 8192,
|
|
49
|
+
"picture_size_vertical_px": 4096,
|
|
50
|
+
"bbox": [[3026, 1000], [3118, 1000], [3118, 1092], [3026, 1092], [3026, 1000]],
|
|
51
|
+
"object_minimum_size_meters": 0.5,
|
|
52
|
+
"object_maximum_size_meters": 1.5,
|
|
53
|
+
},
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
labels, centroids, clusters, inter_reliable, inter_all, annotations = locate_object(records)
|
|
57
|
+
|
|
58
|
+
print(labels) # centroid id for each input record, -1 if unassigned
|
|
59
|
+
print(centroids) # one row per located object
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Save the results for use in QGIS or other GIS software:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
centroids.to_file("result.gpkg", layer="centroids", driver="GPKG")
|
|
66
|
+
annotations.to_file("result.gpkg", layer="annotations", driver="GPKG")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Input
|
|
70
|
+
|
|
71
|
+
`records_input` is a list of dictionaries (`ObjectAnnotationInput`), one per object in one image:
|
|
72
|
+
|
|
73
|
+
| Key | Type | Description |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| `origin` | `shapely.geometry.Point` | Camera position. `x` = longitude, `y` = latitude (EPSG:4326). |
|
|
76
|
+
| `picture_field_of_view` | `float` or `int` | Horizontal field of view of the image in degrees (360 for a full panorama). |
|
|
77
|
+
| `picture_azimuth` | `float` or `int` | Compass direction of the image centre in degrees (0 = north, clockwise). |
|
|
78
|
+
| `picture_size_horizontal_px` | `int` | Image width in pixels. |
|
|
79
|
+
| `picture_size_vertical_px` | `int` | Image height in pixels. |
|
|
80
|
+
| `bbox` | `list[list[int]]` | Polygon of the object in pixel coordinates, `[[x1, y1], [x2, y2], ...]`, at least 3 points, integers only. If the first and last point differ, the polygon is closed automatically. |
|
|
81
|
+
| `object_minimum_size_meters` | `float` or `int` | Smallest plausible real-world height of the object, in metres. |
|
|
82
|
+
| `object_maximum_size_meters` | `float` or `int` | Largest plausible real-world height of the object, in metres. |
|
|
83
|
+
|
|
84
|
+
The size of the object is taken from the **vertical** extent of the bounding box, so the minimum and maximum sizes refer to the object's height. The wider the range between minimum and maximum, the longer the line along which the object can be located.
|
|
85
|
+
|
|
86
|
+
Annotations with an invalid bounding box or a line that cannot be calculated are skipped with a warning.
|
|
87
|
+
|
|
88
|
+
## Parameters
|
|
89
|
+
|
|
90
|
+
| Parameter | Default | Description |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| `intersection_angle_deg_min` | `15` | Minimum angle in degrees between two bearings. Small angles give imprecise locations. |
|
|
93
|
+
| `intersection_angle_deg_max` | `None` (360) | Maximum angle between two bearings. Use it to exclude cameras that cannot plausibly see the same object. |
|
|
94
|
+
| `object_size_score` | `0.8` | Minimum size agreement (0 to 1) between the two annotations of an intersection. A score of 1 means identical size estimates. |
|
|
95
|
+
| `cluster_radius_m_max` | `1` | Maximum distance in metres between a cluster centre and its intersections. Also the maximum distance from a centroid to an annotation line for it to be assigned. |
|
|
96
|
+
| `cluster_min_size` | `1` | Minimum number of intersections for a cluster (DBSCAN `min_samples`). |
|
|
97
|
+
| `cluster_max_depth` | `None` | Maximum recursion depth of the clustering. By default it is derived from `cluster_epsilon_decrease`. |
|
|
98
|
+
| `cluster_epsilon_start` | `None` | Starting DBSCAN radius in metres. Defaults to `cluster_radius_m_max`. |
|
|
99
|
+
| `cluster_epsilon_decrease` | `0.8` | Factor by which the DBSCAN radius shrinks at each recursion level. |
|
|
100
|
+
|
|
101
|
+
## Output
|
|
102
|
+
|
|
103
|
+
`locate_object()` returns a tuple of six items:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
labels, centroids, clusters, intersections_reliable, intersections, annotations = locate_object(records)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
1. **`labels`**: list of integers, one per input record, in the same order. The `id` of the centroid the record belongs to, or `-1` if it was not assigned.
|
|
110
|
+
2. **`centroids`**: GeoDataFrame (points) with the final object locations.
|
|
111
|
+
3. **`clusters`**: GeoDataFrame (points) with the cluster centres before the final assignment.
|
|
112
|
+
4. **`intersections_reliable`**: GeoDataFrame (points) with the intersections that passed the angle and size filters.
|
|
113
|
+
5. **`intersections`**: GeoDataFrame (points) with all intersections found.
|
|
114
|
+
6. **`annotations`**: GeoDataFrame (lines) with the location line of every valid annotation.
|
|
115
|
+
|
|
116
|
+
All GeoDataFrames use `EPSG:4326`. If there is no result, empty GeoDataFrames are returned and `labels` contains only `-1`.
|
|
117
|
+
|
|
118
|
+
### Columns
|
|
119
|
+
|
|
120
|
+
**`centroids`**
|
|
121
|
+
|
|
122
|
+
| Column | Description |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `id` | Centroid id, matches `labels` and `annotations.centroid_id`. |
|
|
125
|
+
| `object_size_meter` | Mean estimated object size in metres. |
|
|
126
|
+
| `object_size_meter_sd` | Standard deviation of the size estimates. |
|
|
127
|
+
| `object_azimuth` | Direction in degrees from which the object is seen head-on (the average viewing direction plus 180). |
|
|
128
|
+
|
|
129
|
+
**`clusters`**: `object_size_meter`, `object_size_meter_sd`, `object_azimuth` (as above).
|
|
130
|
+
|
|
131
|
+
**`intersections_reliable`** and **`intersections`**
|
|
132
|
+
|
|
133
|
+
| Column | Description |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `angle` | Angle in degrees between the two bearings. |
|
|
136
|
+
| `object_size_average_meter` | Average of the two size estimates at the intersection. |
|
|
137
|
+
| `object_size_score` | Size agreement between the two annotations (0 to 1). |
|
|
138
|
+
| `line1`, `line2` | Index in `records_input` of the two annotations. |
|
|
139
|
+
|
|
140
|
+
**`annotations`**
|
|
141
|
+
|
|
142
|
+
| Column | Description |
|
|
143
|
+
|---|---|
|
|
144
|
+
| `id` | Index of the record in `records_input`. |
|
|
145
|
+
| `centroid_id` | Assigned centroid, or missing if the annotation was not assigned. |
|
|
146
|
+
| `origin_longitude`, `origin_latitude` | Camera position. |
|
|
147
|
+
| `line_azimuth` | Compass bearing of the line in degrees. |
|
|
148
|
+
| `object_distance_meter` | Distance from camera to the centroid (only if assigned). |
|
|
149
|
+
| `object_size_meter` | Object size estimated from this annotation at the centroid (only if assigned). |
|
|
150
|
+
|
|
151
|
+
## Notes and limitations
|
|
152
|
+
|
|
153
|
+
- The distance estimate assumes the field of view and pixel size describe an undistorted horizontal view (as in equirectangular panoramas). Strongly distorted images give less accurate results.
|
|
154
|
+
- Pixel coordinates have `x` increasing to the right, and `x` wraps around the image seam for panoramas.
|
|
155
|
+
- The intersection search works in a local metric projection and is meant for areas of limited size (around 100 km by 100 km).
|
|
156
|
+
- At least two annotations from different positions are needed to locate an object.
|
|
157
|
+
|
|
158
|
+
## Logging
|
|
159
|
+
|
|
160
|
+
The package uses the standard `logging` module and attaches a `NullHandler`, so it is silent by default. To see progress and warnings:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
import logging
|
|
164
|
+
logging.basicConfig(level=logging.INFO)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
MIT, see the `LICENSE` file.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "locateobject"
|
|
7
|
+
dynamic = ["version"] # read from __version__ in __init__.py
|
|
8
|
+
description = "Locate real-world objects by triangulating bounding boxes from multiple geotagged images"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Joost Postma" }]
|
|
14
|
+
keywords = ["triangulation", "locateobject", "geospatial", "photogrammetry", "panoramax", "gis"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"numpy>=2.4",
|
|
23
|
+
"pandas>=3.0",
|
|
24
|
+
"shapely>=2.1",
|
|
25
|
+
"geopandas>=1.1",
|
|
26
|
+
"pyproj>=3.7",
|
|
27
|
+
"scipy>=1.17",
|
|
28
|
+
"scikit-learn>=1.8",
|
|
29
|
+
"geopy>=2.4",
|
|
30
|
+
"haversine>=2.9",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
# [project.urls]
|
|
34
|
+
# Source = "https://github.com/joost_72/objectlocation"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.dynamic]
|
|
37
|
+
version = { attr = "locateobject.__version__" }
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.packages.find]
|
|
40
|
+
where = ["src"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
# from importlib import resources
|
|
3
|
+
# import tomllib
|
|
4
|
+
|
|
5
|
+
from .locate import locate_object
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Version of the locateobject package
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
# Library rule: attach a NullHandler to the package's top-level logger.
|
|
12
|
+
# This silences "no handlers found" warnings if the caller configures nothing,
|
|
13
|
+
# and leaves all real handler/level setup to the application.
|
|
14
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
15
|
+
|
|
16
|
+
__all__ = ["locate_object"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Read URL of the Real Python feed from config file
|
|
20
|
+
# _cfg = tomllib.loads(resources.read_text("triangulation", "config.toml"))
|
|
21
|
+
# EARTH_RADIUS_M = _cfg["general"]["EARTH_RADIUS_M"]
|
|
22
|
+
# print(f'EARTH_RADIUS_M: {EARTH_RADIUS_M}')
|