opencoverage 0.2.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.
- opencoverage-0.2.0/.gitignore +15 -0
- opencoverage-0.2.0/LICENSE +21 -0
- opencoverage-0.2.0/PKG-INFO +125 -0
- opencoverage-0.2.0/README.md +94 -0
- opencoverage-0.2.0/config/FireFlyTetracam.ini +28 -0
- opencoverage-0.2.0/config/RascalTetracam.ini +27 -0
- opencoverage-0.2.0/config/quad_tetracam.ini +27 -0
- opencoverage-0.2.0/pyproject.toml +56 -0
- opencoverage-0.2.0/src/opencoverage/__init__.py +26 -0
- opencoverage-0.2.0/src/opencoverage/cli.py +60 -0
- opencoverage-0.2.0/src/opencoverage/geometry/__init__.py +6 -0
- opencoverage-0.2.0/src/opencoverage/geometry/map.py +117 -0
- opencoverage-0.2.0/src/opencoverage/geometry/transforms.py +43 -0
- opencoverage-0.2.0/src/opencoverage/gpu/__init__.py +5 -0
- opencoverage-0.2.0/src/opencoverage/gpu/_backend.py +35 -0
- opencoverage-0.2.0/src/opencoverage/gpu/optimal_sweep.py +42 -0
- opencoverage-0.2.0/src/opencoverage/io/__init__.py +6 -0
- opencoverage-0.2.0/src/opencoverage/io/config.py +79 -0
- opencoverage-0.2.0/src/opencoverage/io/coords.py +72 -0
- opencoverage-0.2.0/src/opencoverage/io/kml.py +50 -0
- opencoverage-0.2.0/src/opencoverage/io/mission_planner.py +35 -0
- opencoverage-0.2.0/src/opencoverage/io/qgc.py +130 -0
- opencoverage-0.2.0/src/opencoverage/io/survey_input.py +21 -0
- opencoverage-0.2.0/src/opencoverage/mission_splitter.py +83 -0
- opencoverage-0.2.0/src/opencoverage/models.py +134 -0
- opencoverage-0.2.0/src/opencoverage/parameters.py +132 -0
- opencoverage-0.2.0/src/opencoverage/patterns/__init__.py +15 -0
- opencoverage-0.2.0/src/opencoverage/patterns/back_forth.py +57 -0
- opencoverage-0.2.0/src/opencoverage/patterns/base.py +74 -0
- opencoverage-0.2.0/src/opencoverage/patterns/following_wind.py +33 -0
- opencoverage-0.2.0/src/opencoverage/patterns/long_edge.py +35 -0
- opencoverage-0.2.0/src/opencoverage/patterns/optimal_sweep.py +38 -0
- opencoverage-0.2.0/src/opencoverage/planner.py +216 -0
- opencoverage-0.2.0/tests/test_back_forth.py +34 -0
- opencoverage-0.2.0/tests/test_config.py +14 -0
- opencoverage-0.2.0/tests/test_long_edge.py +26 -0
- opencoverage-0.2.0/tests/test_mission_splitter.py +22 -0
- opencoverage-0.2.0/tests/test_optimal_sweep.py +32 -0
- opencoverage-0.2.0/tests/test_parameters.py +55 -0
- opencoverage-0.2.0/tests/test_planner.py +56 -0
- opencoverage-0.2.0/tests/test_survey_input.py +19 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018-2026 J. Irving Vasquez-Gomez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: opencoverage
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Coverage path planning for UAV aerial surveying
|
|
5
|
+
Project-URL: Homepage, https://github.com/irvingvasquez/opencoverage
|
|
6
|
+
Project-URL: Repository, https://github.com/irvingvasquez/opencoverage
|
|
7
|
+
Project-URL: Documentation, https://github.com/irvingvasquez/opencoverage#readme
|
|
8
|
+
Project-URL: Issues, https://github.com/irvingvasquez/opencoverage/issues
|
|
9
|
+
Author-email: Juan Irving Vasquez <jvasquezg@ipn.mx>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: coverage-path-planning,drone,precision-agriculture,survey,uav
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: numpy>=1.24
|
|
23
|
+
Requires-Dist: pyproj>=3.6
|
|
24
|
+
Requires-Dist: shapely>=2.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
28
|
+
Provides-Extra: gpu
|
|
29
|
+
Requires-Dist: cupy-cuda12x>=13.0; extra == 'gpu'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# OpenCoverage
|
|
33
|
+
|
|
34
|
+
Coverage path planning for UAV aerial surveying. OpenCoverage computes waypoint
|
|
35
|
+
flight paths that fully cover a terrain polygon, with support for camera overlap,
|
|
36
|
+
flight constraints, and multiple sweep patterns.
|
|
37
|
+
|
|
38
|
+
Python reimplementation of the original UAV Planning library, using Shapely
|
|
39
|
+
instead of CGAL. CPU execution is the default; optional GPU acceleration via CuPy
|
|
40
|
+
is available for the optimal sweep search.
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- **Search patterns**: back-and-forth, long-edge, optimal sweep, following-wind
|
|
45
|
+
- **Planning targets**: spatial resolution, cruise velocity, or fixed altitude
|
|
46
|
+
- **Mission splitting** by UAV flight-time limits
|
|
47
|
+
- **Input formats**: KML polygons, Mission Planner `.poly` files, INI configuration
|
|
48
|
+
- **Output**: QGroundControl WPL 120 waypoint files
|
|
49
|
+
- **Optional GPU**: CuPy acceleration for optimal sweep angle search
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install -e ".[dev]"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Optional GPU support:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install -e ".[gpu]"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Quick start
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from shapely.geometry import Polygon
|
|
67
|
+
|
|
68
|
+
import opencoverage as oc
|
|
69
|
+
|
|
70
|
+
polygon = Polygon([(0, 0), (100, 0), (100, 80), (0, 80)])
|
|
71
|
+
|
|
72
|
+
mission = oc.plan(
|
|
73
|
+
polygon=polygon,
|
|
74
|
+
uav=oc.UAV(min_height=70, max_height=500, survey_velocity=17),
|
|
75
|
+
camera=oc.PinholeCamera(
|
|
76
|
+
pixel_size_mm=0.0032,
|
|
77
|
+
focal_length_mm=8.43,
|
|
78
|
+
sensor_width_mm=6.55,
|
|
79
|
+
sensor_height_mm=4.92,
|
|
80
|
+
capture_rate_s=3,
|
|
81
|
+
),
|
|
82
|
+
pattern="optimal_sweep",
|
|
83
|
+
lateral_overlap=0.8,
|
|
84
|
+
forward_overlap=0.7,
|
|
85
|
+
target="velocity",
|
|
86
|
+
target_value=17.0,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
print(len(mission.path), "waypoints at", mission.flight_height, "m")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Command line
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
opencoverage examples/sample_field.kml config/quad_tetracam.ini mission.txt
|
|
96
|
+
opencoverage examples/sample_field.poly config/quad_tetracam.ini mission.txt --split
|
|
97
|
+
opencoverage examples/sample_field.kml config/quad_tetracam.ini mission.txt --gpu
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Patterns
|
|
101
|
+
|
|
102
|
+
| Pattern | Description |
|
|
103
|
+
|---------|-------------|
|
|
104
|
+
| `back_forth` | Standard boustrophedon sweep |
|
|
105
|
+
| `long_edge` | Sweep lines perpendicular to the longest polygon edge |
|
|
106
|
+
| `optimal_sweep` | Minimize horizontal sweep width over rotation angles |
|
|
107
|
+
| `following_wind` | Align sweeps with meteorological wind direction |
|
|
108
|
+
|
|
109
|
+
## Project layout
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
src/opencoverage/
|
|
113
|
+
├── models.py # UAV, camera, mission dataclasses
|
|
114
|
+
├── parameters.py # Survey parameter formulas
|
|
115
|
+
├── planner.py # High-level plan() API
|
|
116
|
+
├── mission_splitter.py # Flight-time mission splitting
|
|
117
|
+
├── geometry/ # Shapely map and transforms
|
|
118
|
+
├── patterns/ # Coverage search patterns
|
|
119
|
+
├── io/ # Config, KML, QGC, coordinate conversion
|
|
120
|
+
└── gpu/ # Optional CuPy kernels
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT License. Copyright (c) J. Irving Vasquez-Gomez.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# OpenCoverage
|
|
2
|
+
|
|
3
|
+
Coverage path planning for UAV aerial surveying. OpenCoverage computes waypoint
|
|
4
|
+
flight paths that fully cover a terrain polygon, with support for camera overlap,
|
|
5
|
+
flight constraints, and multiple sweep patterns.
|
|
6
|
+
|
|
7
|
+
Python reimplementation of the original UAV Planning library, using Shapely
|
|
8
|
+
instead of CGAL. CPU execution is the default; optional GPU acceleration via CuPy
|
|
9
|
+
is available for the optimal sweep search.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Search patterns**: back-and-forth, long-edge, optimal sweep, following-wind
|
|
14
|
+
- **Planning targets**: spatial resolution, cruise velocity, or fixed altitude
|
|
15
|
+
- **Mission splitting** by UAV flight-time limits
|
|
16
|
+
- **Input formats**: KML polygons, Mission Planner `.poly` files, INI configuration
|
|
17
|
+
- **Output**: QGroundControl WPL 120 waypoint files
|
|
18
|
+
- **Optional GPU**: CuPy acceleration for optimal sweep angle search
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install -e ".[dev]"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Optional GPU support:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install -e ".[gpu]"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from shapely.geometry import Polygon
|
|
36
|
+
|
|
37
|
+
import opencoverage as oc
|
|
38
|
+
|
|
39
|
+
polygon = Polygon([(0, 0), (100, 0), (100, 80), (0, 80)])
|
|
40
|
+
|
|
41
|
+
mission = oc.plan(
|
|
42
|
+
polygon=polygon,
|
|
43
|
+
uav=oc.UAV(min_height=70, max_height=500, survey_velocity=17),
|
|
44
|
+
camera=oc.PinholeCamera(
|
|
45
|
+
pixel_size_mm=0.0032,
|
|
46
|
+
focal_length_mm=8.43,
|
|
47
|
+
sensor_width_mm=6.55,
|
|
48
|
+
sensor_height_mm=4.92,
|
|
49
|
+
capture_rate_s=3,
|
|
50
|
+
),
|
|
51
|
+
pattern="optimal_sweep",
|
|
52
|
+
lateral_overlap=0.8,
|
|
53
|
+
forward_overlap=0.7,
|
|
54
|
+
target="velocity",
|
|
55
|
+
target_value=17.0,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
print(len(mission.path), "waypoints at", mission.flight_height, "m")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Command line
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
opencoverage examples/sample_field.kml config/quad_tetracam.ini mission.txt
|
|
65
|
+
opencoverage examples/sample_field.poly config/quad_tetracam.ini mission.txt --split
|
|
66
|
+
opencoverage examples/sample_field.kml config/quad_tetracam.ini mission.txt --gpu
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Patterns
|
|
70
|
+
|
|
71
|
+
| Pattern | Description |
|
|
72
|
+
|---------|-------------|
|
|
73
|
+
| `back_forth` | Standard boustrophedon sweep |
|
|
74
|
+
| `long_edge` | Sweep lines perpendicular to the longest polygon edge |
|
|
75
|
+
| `optimal_sweep` | Minimize horizontal sweep width over rotation angles |
|
|
76
|
+
| `following_wind` | Align sweeps with meteorological wind direction |
|
|
77
|
+
|
|
78
|
+
## Project layout
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
src/opencoverage/
|
|
82
|
+
├── models.py # UAV, camera, mission dataclasses
|
|
83
|
+
├── parameters.py # Survey parameter formulas
|
|
84
|
+
├── planner.py # High-level plan() API
|
|
85
|
+
├── mission_splitter.py # Flight-time mission splitting
|
|
86
|
+
├── geometry/ # Shapely map and transforms
|
|
87
|
+
├── patterns/ # Coverage search patterns
|
|
88
|
+
├── io/ # Config, KML, QGC, coordinate conversion
|
|
89
|
+
└── gpu/ # Optional CuPy kernels
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT License. Copyright (c) J. Irving Vasquez-Gomez.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This is the configuration file for planning a UAV mission
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
[Planner]
|
|
6
|
+
SearchPattern = 2; 1:back and forth, 2:Long Edge, 3:Optimal,
|
|
7
|
+
AutoTakeOff = false; Autonomous takeoff and landing
|
|
8
|
+
LateralOverlap = 0.5;
|
|
9
|
+
FordwardOVerlap = 0.7;
|
|
10
|
+
SpatialResolution = 40; mm/pixel
|
|
11
|
+
TargetVelocity = 17; m/s
|
|
12
|
+
TargetPlanning = 2; Planning mode, 1 = resolution, 2 = velocity, 3 = altitude;
|
|
13
|
+
|
|
14
|
+
[UAV]
|
|
15
|
+
FlightTime = 300; Seconds, put 0 to do not consider flight time
|
|
16
|
+
Velocity = 17; m/s
|
|
17
|
+
MinHeight = 70; meters
|
|
18
|
+
MaxHeight = 500; meters
|
|
19
|
+
MinVelocity = 10; m/s
|
|
20
|
+
MaxVelocity = 20; m/s
|
|
21
|
+
|
|
22
|
+
[Camera]
|
|
23
|
+
PixelSize = 0.0032; mm
|
|
24
|
+
FocalLenght = 8.43; mm
|
|
25
|
+
SensorWidth = 6.55; mm
|
|
26
|
+
SensorHeight = 4.92; mm
|
|
27
|
+
CaptureRate = 3; seg
|
|
28
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This is the configuration file for planning a UAV mission
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
[Planner]
|
|
6
|
+
SearchPattern = 3; 1:back and forth, 2:Long Edge, 3:Optimal,
|
|
7
|
+
LateralOverlap = 0.6;
|
|
8
|
+
FordwardOVerlap = 0.7;
|
|
9
|
+
SpatialResolution = 100; mm/pixel
|
|
10
|
+
TargetVelocity = 10.0; m/s
|
|
11
|
+
TargetPlanning = 2; Planning mode, 1 = resolution, 2 = velocity, 3 = altitude;
|
|
12
|
+
AutoTakeOff = true; Autonomous takeoff and landing
|
|
13
|
+
|
|
14
|
+
[UAV]
|
|
15
|
+
FlightTime = 300;Seconds, put 0 to do not consider flight time
|
|
16
|
+
MinVelocity = 8.0;
|
|
17
|
+
MaxVelocity = 20.0;
|
|
18
|
+
MinHeight = 50; meters
|
|
19
|
+
MaxHeight = 500; meters
|
|
20
|
+
|
|
21
|
+
[Camera]
|
|
22
|
+
PixelSize = 0.0032; mm
|
|
23
|
+
FocalLenght = 8.43; mm
|
|
24
|
+
SensorWidth = 6.55; mm
|
|
25
|
+
SensorHeight = 4.92; mm
|
|
26
|
+
|
|
27
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Example configuration for a quadrotor with a Tetracam micro camera.
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
[Planner]
|
|
6
|
+
SearchPattern = 1
|
|
7
|
+
AutoTakeOff = false
|
|
8
|
+
LateralOverlap = 0.8
|
|
9
|
+
ForwardOverlap = 0.7
|
|
10
|
+
SpatialResolution = 40
|
|
11
|
+
TargetVelocity = 17
|
|
12
|
+
TargetPlanning = 2
|
|
13
|
+
|
|
14
|
+
[UAV]
|
|
15
|
+
FlightTime = 300
|
|
16
|
+
Velocity = 17
|
|
17
|
+
MinHeight = 70
|
|
18
|
+
MaxHeight = 500
|
|
19
|
+
MinVelocity = 5
|
|
20
|
+
MaxVelocity = 20
|
|
21
|
+
|
|
22
|
+
[Camera]
|
|
23
|
+
PixelSize = 0.0032
|
|
24
|
+
FocalLenght = 8.43
|
|
25
|
+
SensorWidth = 6.55
|
|
26
|
+
SensorHeight = 4.92
|
|
27
|
+
CaptureRate = 3
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "opencoverage"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Coverage path planning for UAV aerial surveying"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Juan Irving Vasquez", email = "jvasquezg@ipn.mx" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["uav", "drone", "coverage-path-planning", "survey", "precision-agriculture"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"numpy>=1.24",
|
|
28
|
+
"shapely>=2.0",
|
|
29
|
+
"pyproj>=3.6",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
gpu = ["cupy-cuda12x>=13.0"]
|
|
34
|
+
dev = ["pytest>=7.0", "ruff>=0.4"]
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
opencoverage = "opencoverage.cli:main"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/opencoverage"]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.sdist]
|
|
43
|
+
include = ["src/opencoverage", "config", "tests"]
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
testpaths = ["tests"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
line-length = 100
|
|
50
|
+
target-version = "py310"
|
|
51
|
+
|
|
52
|
+
[project.urls]
|
|
53
|
+
Homepage = "https://github.com/irvingvasquez/opencoverage"
|
|
54
|
+
Repository = "https://github.com/irvingvasquez/opencoverage"
|
|
55
|
+
Documentation = "https://github.com/irvingvasquez/opencoverage#readme"
|
|
56
|
+
Issues = "https://github.com/irvingvasquez/opencoverage/issues"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""OpenCoverage: coverage path planning for UAV aerial surveying."""
|
|
2
|
+
|
|
3
|
+
from opencoverage.mission_splitter import MissionSplitter
|
|
4
|
+
from opencoverage.models import (
|
|
5
|
+
FlightMission,
|
|
6
|
+
GeodeticCoordinate,
|
|
7
|
+
PinholeCamera,
|
|
8
|
+
SearchPatternType,
|
|
9
|
+
TargetPlanning,
|
|
10
|
+
UAV,
|
|
11
|
+
)
|
|
12
|
+
from opencoverage.planner import plan
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"FlightMission",
|
|
16
|
+
"GeodeticCoordinate",
|
|
17
|
+
"MissionSplitter",
|
|
18
|
+
"PinholeCamera",
|
|
19
|
+
"SearchPatternType",
|
|
20
|
+
"TargetPlanning",
|
|
21
|
+
"UAV",
|
|
22
|
+
"plan",
|
|
23
|
+
"__version__",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Command-line interface for OpenCoverage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from opencoverage.io.config import load_planner_config
|
|
8
|
+
from opencoverage.mission_splitter import MissionSplitter
|
|
9
|
+
from opencoverage.planner import plan
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> int:
|
|
13
|
+
"""Run mission planning from the command line."""
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
description="Plan a UAV coverage mission from a polygon and configuration file.",
|
|
16
|
+
)
|
|
17
|
+
parser.add_argument("polygon", help="Path to input polygon file (KML or Mission Planner)")
|
|
18
|
+
parser.add_argument("config", help="Path to INI configuration file")
|
|
19
|
+
parser.add_argument("output", help="Path to output QGroundControl waypoint file")
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"--gpu",
|
|
22
|
+
action="store_true",
|
|
23
|
+
help="Use CuPy for optimal sweep angle search when available",
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--split",
|
|
27
|
+
action="store_true",
|
|
28
|
+
help="Split the mission according to UAV flight time limits",
|
|
29
|
+
)
|
|
30
|
+
args = parser.parse_args()
|
|
31
|
+
|
|
32
|
+
parsed_config = load_planner_config(args.config)
|
|
33
|
+
result = plan(
|
|
34
|
+
polygon=args.polygon,
|
|
35
|
+
config=args.config,
|
|
36
|
+
gpu=args.gpu,
|
|
37
|
+
split=args.split,
|
|
38
|
+
)
|
|
39
|
+
missions = result if isinstance(result, list) else [result]
|
|
40
|
+
|
|
41
|
+
reference = missions[0].reference
|
|
42
|
+
if reference is None:
|
|
43
|
+
raise SystemExit("Mission is missing a geodetic reference point.")
|
|
44
|
+
|
|
45
|
+
if len(missions) == 1:
|
|
46
|
+
mission = missions[0]
|
|
47
|
+
mission.save_qgc(args.output, reference=reference)
|
|
48
|
+
print(f"Saved {len(mission.path)} waypoints to {args.output}")
|
|
49
|
+
print(f"Flight height: {mission.flight_height:.2f} m")
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
splitter = MissionSplitter(parsed_config["uav_model"])
|
|
53
|
+
saved = splitter.save_qgc_files(missions, args.output, reference=reference)
|
|
54
|
+
for path in saved:
|
|
55
|
+
print(f"Saved mission segment to {path}")
|
|
56
|
+
return 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Polygon map representation backed by Shapely."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from shapely.geometry import LineString, Point, Polygon
|
|
9
|
+
|
|
10
|
+
from opencoverage.geometry.transforms import rotate_polygon
|
|
11
|
+
from opencoverage.models import GeodeticCoordinate
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class SurveyMap:
|
|
16
|
+
"""Survey area defined as a local-meter polygon."""
|
|
17
|
+
|
|
18
|
+
polygon: Polygon
|
|
19
|
+
reference: GeodeticCoordinate | None = None
|
|
20
|
+
home: GeodeticCoordinate | None = None
|
|
21
|
+
takeoff: GeodeticCoordinate | None = None
|
|
22
|
+
_home_local: tuple[float, float] | None = field(default=None, repr=False)
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_polygon(cls, polygon: Polygon, **kwargs) -> SurveyMap:
|
|
26
|
+
"""Create a map from an existing Shapely polygon."""
|
|
27
|
+
if polygon.is_empty or not polygon.is_valid:
|
|
28
|
+
raise ValueError("Survey polygon must be non-empty and valid")
|
|
29
|
+
return cls(polygon=polygon, **kwargs)
|
|
30
|
+
|
|
31
|
+
def copy(self) -> SurveyMap:
|
|
32
|
+
"""Return a shallow copy of the map."""
|
|
33
|
+
return SurveyMap(
|
|
34
|
+
polygon=Polygon(self.polygon),
|
|
35
|
+
reference=self.reference,
|
|
36
|
+
home=self.home,
|
|
37
|
+
takeoff=self.takeoff,
|
|
38
|
+
_home_local=self._home_local,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def rotated_copy(self, angle_rad: float) -> SurveyMap:
|
|
42
|
+
"""Return a new map with the polygon rotated by angle_rad."""
|
|
43
|
+
rotated = rotate_polygon(self.polygon, angle_rad)
|
|
44
|
+
return SurveyMap(
|
|
45
|
+
polygon=rotated,
|
|
46
|
+
reference=self.reference,
|
|
47
|
+
home=self.home,
|
|
48
|
+
takeoff=self.takeoff,
|
|
49
|
+
_home_local=self._home_local,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def bounds(self) -> tuple[float, float, float, float]:
|
|
54
|
+
"""Return polygon axis-aligned bounds (minx, miny, maxx, maxy)."""
|
|
55
|
+
return self.polygon.bounds
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def vertices(self) -> np.ndarray:
|
|
59
|
+
"""Return exterior vertices as an (N, 2) array."""
|
|
60
|
+
coords = list(self.polygon.exterior.coords[:-1])
|
|
61
|
+
return np.asarray(coords, dtype=float)
|
|
62
|
+
|
|
63
|
+
def extreme_vertices(self) -> dict[str, tuple[float, float]]:
|
|
64
|
+
"""Return left, right, top, and bottom vertices of the polygon."""
|
|
65
|
+
coords = list(self.polygon.exterior.coords[:-1])
|
|
66
|
+
left = min(coords, key=lambda p: p[0])
|
|
67
|
+
right = max(coords, key=lambda p: p[0])
|
|
68
|
+
bottom = min(coords, key=lambda p: p[1])
|
|
69
|
+
top = max(coords, key=lambda p: p[1])
|
|
70
|
+
return {"left": left, "right": right, "bottom": bottom, "top": top}
|
|
71
|
+
|
|
72
|
+
def sweep_width(self) -> float:
|
|
73
|
+
"""Return horizontal extent used for back-and-forth sweep planning."""
|
|
74
|
+
extremes = self.extreme_vertices()
|
|
75
|
+
return extremes["right"][0] - extremes["left"][0]
|
|
76
|
+
|
|
77
|
+
def sweep_height(self) -> float:
|
|
78
|
+
"""Return vertical extent of the polygon."""
|
|
79
|
+
extremes = self.extreme_vertices()
|
|
80
|
+
return extremes["top"][1] - extremes["bottom"][1]
|
|
81
|
+
|
|
82
|
+
def intersect_vertical_line(self, x: float) -> list[tuple[float, float]]:
|
|
83
|
+
"""
|
|
84
|
+
Intersect the polygon with a vertical line at coordinate x.
|
|
85
|
+
|
|
86
|
+
Returns intersection points sorted by ascending y.
|
|
87
|
+
"""
|
|
88
|
+
minx, miny, maxx, maxy = self.bounds
|
|
89
|
+
line = LineString([(x, miny - 1.0), (x, maxy + 1.0)])
|
|
90
|
+
intersection = self.polygon.intersection(line)
|
|
91
|
+
|
|
92
|
+
points: list[tuple[float, float]] = []
|
|
93
|
+
if intersection.is_empty:
|
|
94
|
+
return points
|
|
95
|
+
|
|
96
|
+
if isinstance(intersection, Point):
|
|
97
|
+
points.append((intersection.x, intersection.y))
|
|
98
|
+
elif intersection.geom_type == "MultiPoint":
|
|
99
|
+
points.extend((point.x, point.y) for point in intersection.geoms)
|
|
100
|
+
elif intersection.geom_type == "LineString":
|
|
101
|
+
points.extend(intersection.coords)
|
|
102
|
+
elif intersection.geom_type == "MultiLineString":
|
|
103
|
+
for segment in intersection.geoms:
|
|
104
|
+
points.extend(segment.coords)
|
|
105
|
+
|
|
106
|
+
unique: list[tuple[float, float]] = []
|
|
107
|
+
seen: set[tuple[float, float]] = set()
|
|
108
|
+
for point in sorted(points, key=lambda p: p[1]):
|
|
109
|
+
key = (round(point[0], 9), round(point[1], 9))
|
|
110
|
+
if key not in seen:
|
|
111
|
+
seen.add(key)
|
|
112
|
+
unique.append(point)
|
|
113
|
+
return unique
|
|
114
|
+
|
|
115
|
+
def replace_polygon(self, polygon: Polygon) -> None:
|
|
116
|
+
"""Replace the internal polygon, for example after rotation."""
|
|
117
|
+
self.polygon = polygon
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Coordinate transforms for polygons and waypoint paths."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from shapely.affinity import rotate as shapely_rotate
|
|
9
|
+
from shapely.geometry import Polygon
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def rotate_polygon(polygon: Polygon, angle_rad: float, origin: tuple[float, float] = (0.0, 0.0)) -> Polygon:
|
|
13
|
+
"""Rotate a polygon by angle_rad around origin."""
|
|
14
|
+
return shapely_rotate(polygon, angle_rad, origin=origin, use_radians=True)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def rotate_points(
|
|
18
|
+
points: list[tuple[float, float]],
|
|
19
|
+
angle_rad: float,
|
|
20
|
+
origin: tuple[float, float] = (0.0, 0.0),
|
|
21
|
+
) -> list[tuple[float, float]]:
|
|
22
|
+
"""Rotate a list of 2D points by angle_rad around origin."""
|
|
23
|
+
if not points:
|
|
24
|
+
return []
|
|
25
|
+
|
|
26
|
+
ox, oy = origin
|
|
27
|
+
cos_a = math.cos(angle_rad)
|
|
28
|
+
sin_a = math.sin(angle_rad)
|
|
29
|
+
rotated: list[tuple[float, float]] = []
|
|
30
|
+
|
|
31
|
+
for x, y in points:
|
|
32
|
+
dx = x - ox
|
|
33
|
+
dy = y - oy
|
|
34
|
+
rotated.append((ox + cos_a * dx - sin_a * dy, oy + sin_a * dx + cos_a * dy))
|
|
35
|
+
|
|
36
|
+
return rotated
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def rotation_matrix(angle_rad: float) -> np.ndarray:
|
|
40
|
+
"""Return a 2x2 rotation matrix for angle_rad."""
|
|
41
|
+
cos_a = math.cos(angle_rad)
|
|
42
|
+
sin_a = math.sin(angle_rad)
|
|
43
|
+
return np.array([[cos_a, -sin_a], [sin_a, cos_a]], dtype=float)
|