opencoverage 0.2.0__py3-none-any.whl

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.
@@ -0,0 +1,35 @@
1
+ """Long-edge aligned coverage sweep pattern."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+
7
+ from opencoverage.models import FlightMission
8
+ from opencoverage.patterns.back_forth import BackAndForthPattern
9
+
10
+
11
+ class LongEdgePattern(BackAndForthPattern):
12
+ """Align sweep lines perpendicular to the polygon longest edge."""
13
+
14
+ def calculate(self) -> FlightMission:
15
+ rotation = self._long_edge_rotation()
16
+ waypoints = self.generate_rotated_pattern(rotation)
17
+ return self.build_mission(waypoints)
18
+
19
+ def _long_edge_rotation(self) -> float:
20
+ """Return rotation angle that aligns the longest edge with the x-axis."""
21
+ coords = list(self.survey_map.polygon.exterior.coords[:-1])
22
+ origin = coords[0]
23
+ target = coords[1]
24
+ max_distance = 0.0
25
+
26
+ for index, start in enumerate(coords):
27
+ end = coords[(index + 1) % len(coords)]
28
+ distance = (end[0] - start[0]) ** 2 + (end[1] - start[1]) ** 2
29
+ if distance > max_distance:
30
+ max_distance = distance
31
+ origin = start
32
+ target = end
33
+
34
+ edge_angle = math.atan2(target[1] - origin[1], target[0] - origin[0])
35
+ return math.pi / 2.0 - edge_angle
@@ -0,0 +1,38 @@
1
+ """Optimal sweep direction coverage pattern."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from opencoverage.gpu.optimal_sweep import find_optimal_sweep_angle
6
+ from opencoverage.models import FlightMission
7
+ from opencoverage.patterns.back_forth import BackAndForthPattern
8
+
9
+
10
+ class OptimalSweepPattern(BackAndForthPattern):
11
+ """
12
+ Find the sweep rotation that minimizes horizontal coverage width.
13
+
14
+ Uses a brute-force search over [0, pi). When ``gpu=True`` and CuPy is
15
+ installed, the search runs on the GPU.
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ uav,
21
+ camera,
22
+ survey_map,
23
+ *,
24
+ gpu: bool = False,
25
+ n_angle_steps: int = 10_000,
26
+ ) -> None:
27
+ super().__init__(uav, camera, survey_map)
28
+ self.gpu = gpu
29
+ self.n_angle_steps = n_angle_steps
30
+
31
+ def calculate(self) -> FlightMission:
32
+ angle = find_optimal_sweep_angle(
33
+ self.survey_map.vertices,
34
+ n_steps=self.n_angle_steps,
35
+ prefer_gpu=self.gpu,
36
+ )
37
+ waypoints = self.generate_rotated_pattern(angle)
38
+ return self.build_mission(waypoints)
@@ -0,0 +1,216 @@
1
+ """High-level mission planning API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from shapely.geometry import Polygon
8
+
9
+ from opencoverage.geometry.map import SurveyMap
10
+ from opencoverage.io.config import load_planner_config
11
+ from opencoverage.io.survey_input import read_survey_map
12
+ from opencoverage.mission_splitter import MissionSplitter
13
+ from opencoverage.models import (
14
+ FlightMission,
15
+ GeodeticCoordinate,
16
+ PinholeCamera,
17
+ SearchPatternType,
18
+ TargetPlanning,
19
+ UAV,
20
+ )
21
+ from opencoverage.patterns.back_forth import BackAndForthPattern
22
+ from opencoverage.patterns.base import SearchPattern
23
+ from opencoverage.patterns.following_wind import FollowingWindPattern
24
+ from opencoverage.patterns.long_edge import LongEdgePattern
25
+ from opencoverage.patterns.optimal_sweep import OptimalSweepPattern
26
+
27
+ PATTERN_ALIASES: dict[str, SearchPatternType] = {
28
+ "back_forth": SearchPatternType.BACK_AND_FORTH,
29
+ "back-and-forth": SearchPatternType.BACK_AND_FORTH,
30
+ "long_edge": SearchPatternType.LONG_EDGE,
31
+ "long-edge": SearchPatternType.LONG_EDGE,
32
+ "optimal_sweep": SearchPatternType.OPTIMAL_SWEEP,
33
+ "optimal-sweep": SearchPatternType.OPTIMAL_SWEEP,
34
+ "following_wind": SearchPatternType.FOLLOWING_WIND,
35
+ "following-wind": SearchPatternType.FOLLOWING_WIND,
36
+ }
37
+
38
+
39
+ def _resolve_pattern_type(pattern: str | SearchPatternType) -> SearchPatternType:
40
+ if isinstance(pattern, SearchPatternType):
41
+ return pattern
42
+ normalized = pattern.lower().replace(" ", "_")
43
+ if normalized not in PATTERN_ALIASES:
44
+ raise ValueError(f"Unknown pattern: {pattern}")
45
+ return PATTERN_ALIASES[normalized]
46
+
47
+
48
+ def _resolve_target(
49
+ target: str | TargetPlanning,
50
+ target_value: float | None,
51
+ config: dict | None,
52
+ ) -> tuple[TargetPlanning, float]:
53
+ if config is not None:
54
+ planner = config["planner"]
55
+ resolved_target = planner["target_planning"]
56
+ if target_value is None:
57
+ if resolved_target == TargetPlanning.RESOLUTION:
58
+ target_value = planner["spatial_resolution_mm"]
59
+ elif resolved_target == TargetPlanning.VELOCITY:
60
+ target_value = planner["target_velocity"]
61
+ else:
62
+ target_value = config["uav"]["min_height"]
63
+ return resolved_target, float(target_value)
64
+
65
+ if isinstance(target, str):
66
+ target_map = {
67
+ "resolution": TargetPlanning.RESOLUTION,
68
+ "velocity": TargetPlanning.VELOCITY,
69
+ "altitude": TargetPlanning.ALTITUDE,
70
+ }
71
+ resolved_target = target_map[target.lower()]
72
+ else:
73
+ resolved_target = target
74
+
75
+ if target_value is None:
76
+ raise ValueError("target_value is required when no config file is provided")
77
+
78
+ return resolved_target, float(target_value)
79
+
80
+
81
+ def _build_pattern(
82
+ pattern_type: SearchPatternType,
83
+ uav: UAV,
84
+ camera: PinholeCamera,
85
+ survey_map: SurveyMap,
86
+ *,
87
+ gpu: bool = False,
88
+ wind_direction_deg: float = 0.0,
89
+ n_angle_steps: int = 10_000,
90
+ ) -> SearchPattern:
91
+ if pattern_type == SearchPatternType.BACK_AND_FORTH:
92
+ return BackAndForthPattern(uav, camera, survey_map)
93
+ if pattern_type == SearchPatternType.LONG_EDGE:
94
+ return LongEdgePattern(uav, camera, survey_map)
95
+ if pattern_type == SearchPatternType.OPTIMAL_SWEEP:
96
+ return OptimalSweepPattern(
97
+ uav,
98
+ camera,
99
+ survey_map,
100
+ gpu=gpu,
101
+ n_angle_steps=n_angle_steps,
102
+ )
103
+ if pattern_type == SearchPatternType.FOLLOWING_WIND:
104
+ return FollowingWindPattern(
105
+ uav,
106
+ camera,
107
+ survey_map,
108
+ wind_direction_deg=wind_direction_deg,
109
+ )
110
+ raise ValueError(f"Unsupported pattern: {pattern_type}")
111
+
112
+
113
+ def _load_survey_map(
114
+ polygon: Polygon | str | Path,
115
+ *,
116
+ home: GeodeticCoordinate | None = None,
117
+ takeoff: GeodeticCoordinate | None = None,
118
+ reference: GeodeticCoordinate | None = None,
119
+ ) -> SurveyMap:
120
+ if isinstance(polygon, (str, Path)):
121
+ survey_map = read_survey_map(polygon)
122
+ elif isinstance(polygon, Polygon):
123
+ survey_map = SurveyMap.from_polygon(polygon, reference=reference)
124
+ else:
125
+ raise TypeError("polygon must be a Shapely Polygon or a file path")
126
+
127
+ if reference is not None:
128
+ survey_map.reference = reference
129
+ if home is not None:
130
+ survey_map.home = home
131
+ if takeoff is not None:
132
+ survey_map.takeoff = takeoff
133
+ return survey_map
134
+
135
+
136
+ def plan(
137
+ *,
138
+ polygon: Polygon | str | Path,
139
+ config: str | Path | None = None,
140
+ uav: UAV | None = None,
141
+ camera: PinholeCamera | None = None,
142
+ pattern: str | SearchPatternType = "back_forth",
143
+ lateral_overlap: float | None = None,
144
+ forward_overlap: float | None = None,
145
+ target: str | TargetPlanning = "velocity",
146
+ target_value: float | None = None,
147
+ home: GeodeticCoordinate | None = None,
148
+ takeoff: GeodeticCoordinate | None = None,
149
+ reference: GeodeticCoordinate | None = None,
150
+ split: bool = False,
151
+ gpu: bool = False,
152
+ wind_direction_deg: float = 0.0,
153
+ n_angle_steps: int = 10_000,
154
+ ) -> FlightMission | list[FlightMission]:
155
+ """
156
+ Plan a coverage mission for a survey polygon.
157
+
158
+ When ``split`` is True, the mission is divided into segments that respect
159
+ the UAV ``flight_time_s`` limit.
160
+ """
161
+ parsed_config = load_planner_config(config) if config is not None else None
162
+
163
+ if parsed_config is not None:
164
+ uav = uav or parsed_config["uav_model"]
165
+ camera = camera or parsed_config["camera_model"]
166
+ lateral_overlap = (
167
+ lateral_overlap if lateral_overlap is not None else parsed_config["planner"]["lateral_overlap"]
168
+ )
169
+ forward_overlap = (
170
+ forward_overlap
171
+ if forward_overlap is not None
172
+ else parsed_config["planner"]["forward_overlap"]
173
+ )
174
+ if isinstance(pattern, str) and pattern in {"back_forth", "back-and-forth"}:
175
+ pattern = parsed_config["planner"]["search_pattern"]
176
+ wind_direction_deg = parsed_config["planner"].get("wind_direction_deg", wind_direction_deg)
177
+ else:
178
+ if uav is None or camera is None:
179
+ raise ValueError("uav and camera are required when no config file is provided")
180
+ if lateral_overlap is None or forward_overlap is None:
181
+ raise ValueError("overlap values are required when no config file is provided")
182
+
183
+ pattern_type = _resolve_pattern_type(pattern)
184
+ survey_map = _load_survey_map(
185
+ polygon,
186
+ home=home,
187
+ takeoff=takeoff,
188
+ reference=reference,
189
+ )
190
+
191
+ resolved_target, resolved_target_value = _resolve_target(target, target_value, parsed_config)
192
+
193
+ search_pattern = _build_pattern(
194
+ pattern_type,
195
+ uav,
196
+ camera,
197
+ survey_map,
198
+ gpu=gpu,
199
+ wind_direction_deg=wind_direction_deg,
200
+ n_angle_steps=n_angle_steps,
201
+ )
202
+ search_pattern.set_search_parameters(
203
+ lateral_overlap=float(lateral_overlap),
204
+ forward_overlap=float(forward_overlap),
205
+ target_value=resolved_target_value,
206
+ target=resolved_target,
207
+ )
208
+ mission = search_pattern.calculate()
209
+
210
+ if parsed_config is not None:
211
+ mission.auto_takeoff = parsed_config["planner"]["auto_takeoff"]
212
+
213
+ should_split = bool(split)
214
+ if should_split:
215
+ return MissionSplitter(uav).split(mission)
216
+ return mission
@@ -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,30 @@
1
+ opencoverage/__init__.py,sha256=JXd5nL8Uitp0XQDkV16s22aK-_231O2e0XV4Z0BYFTI,534
2
+ opencoverage/cli.py,sha256=BRnYP-y3pgFU8JmlostHJk-R9bz1tIcVLfpEIs8K-Lc,1984
3
+ opencoverage/mission_splitter.py,sha256=1MaRZC0BtxlJZSoC2AxKrhETPfFQl4iB6JiVTzhb1Xw,2815
4
+ opencoverage/models.py,sha256=cgOk8zZqdwr9SsKGKp1i9Yj9agAlnuWSTUbidxYFIeg,3918
5
+ opencoverage/parameters.py,sha256=KUqQHSJdy3n-BiJ9OHRqbWT2u553uZN5jODYoH3simU,4459
6
+ opencoverage/planner.py,sha256=z_3nSwyCHENJUaDeAEa1AsOB4WCCYFWs1hELvK-GOm4,7423
7
+ opencoverage/geometry/__init__.py,sha256=MykmvDdZNt16vJdEMv106Ypk5MmEUhDqnDVd7A4C9rQ,250
8
+ opencoverage/geometry/map.py,sha256=ZmU4Oea96MOdmYAWv4ro1fS2zqDIgpSx8YBxImKN-X8,4415
9
+ opencoverage/geometry/transforms.py,sha256=tSy6dGkYNDJQ48-1Zp9hk_3Awl4rrgZPmJ1B-goEaXY,1268
10
+ opencoverage/gpu/__init__.py,sha256=brf3tSorembLz0m8gcQPlqSTQTslnx-sf5vDDxGskcw,164
11
+ opencoverage/gpu/_backend.py,sha256=BrM4u6bT812xBIoFlJjrk-ef9e-POZFNKvm1SJd-r3M,765
12
+ opencoverage/gpu/optimal_sweep.py,sha256=iUxfQM8IbX3a2eqp1i0V9D6ZG1J40bGfe0yNrRGhgVc,1124
13
+ opencoverage/io/__init__.py,sha256=SJY596r2dOc7LJgQ_B1eGGE9QxctgBBehygFAU22sPw,231
14
+ opencoverage/io/config.py,sha256=-yejas5meJ2llwJVoi48iGsgoztPMtLT-bsEx6nlyjA,3005
15
+ opencoverage/io/coords.py,sha256=XKVwxUBMnbMVuiKr6acqOp5MbA9VY9duEsAfOfHQsQ4,2031
16
+ opencoverage/io/kml.py,sha256=JwN_nyzQmJHPoy3s1HgZEVotQxK4vNIK9VvjxYCvKhw,1648
17
+ opencoverage/io/mission_planner.py,sha256=2p7GnUmjwGWjz1c-bzDrc4WUc-U6maH5DxsuzgPwQ4U,1218
18
+ opencoverage/io/qgc.py,sha256=spYHWjjLTDRWSLTUoMWQn1qgVWZxdj2NW08PA6GQtrQ,3232
19
+ opencoverage/io/survey_input.py,sha256=G_pJKmCfU9VN69V6WCVOsSVrBksNGAbEBfcB8piPqwk,660
20
+ opencoverage/patterns/__init__.py,sha256=z9BJVGU4Ow_emAdo3flsujNxON5iE212y3g6LaunUDI,505
21
+ opencoverage/patterns/back_forth.py,sha256=GCAkhrNfHV_I1Zx_jMOBTMgpQPjhLx8nOwkARtwi6Gw,2072
22
+ opencoverage/patterns/base.py,sha256=dOEHCl5stHJ-gMBPFks2lu-gQ-5crNQUxmz4unasSe4,2674
23
+ opencoverage/patterns/following_wind.py,sha256=3RzbHuaGw5NpWzxfZBlrxlOcPegn1I5jumV6JZNFyqI,907
24
+ opencoverage/patterns/long_edge.py,sha256=C9HQlX_d8f_B-2zkC3sdjD2nx4phokBM7IsQSBHS_Eo,1232
25
+ opencoverage/patterns/optimal_sweep.py,sha256=QlXmOaAwxgj_ifjlsIjCmE9INKtlSh7196K5fF0hD-s,1114
26
+ opencoverage-0.2.0.dist-info/METADATA,sha256=CIZ6Aw4wZfQmhvqhH3PeJ2kGq77X5WtAL2hUsdFlmsA,4115
27
+ opencoverage-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
28
+ opencoverage-0.2.0.dist-info/entry_points.txt,sha256=2MpVVSkV48CuLQ2dYOdN4utrtEmROnf3GgQtxXSBjbY,55
29
+ opencoverage-0.2.0.dist-info/licenses/LICENSE,sha256=WhZX_X8abVtWg47GfAfXZTXMRhThh4Hq48i17ru-Rls,1085
30
+ opencoverage-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ opencoverage = opencoverage.cli:main
@@ -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.