Skip to content

Commit 376dfd6

Browse files
authored
Merge pull request #474 from hotosm/develop
Production Release 2025.02.01
2 parents 998f6c0 + 95733ee commit 376dfd6

File tree

58 files changed

+1663
-1361
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+1663
-1361
lines changed

.pre-commit-config.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,23 +79,23 @@ repos:
7979

8080
# Deps: ensure Python uv lockfile is up to date
8181
- repo: https://github.com/astral-sh/uv-pre-commit
82-
rev: 0.5.26
82+
rev: 0.5.29
8383
hooks:
8484
- id: uv-lock
8585
files: src/backend/pyproject.toml
8686
args: [--project, src/backend]
8787

8888
# Versioning: Commit messages & changelog
8989
- repo: https://github.com/commitizen-tools/commitizen
90-
rev: v4.1.1
90+
rev: v4.2.1
9191
hooks:
9292
- id: commitizen
9393
stages: [commit-msg]
9494

9595
# Lint / autoformat: Python code
9696
- repo: https://github.com/astral-sh/ruff-pre-commit
9797
# Ruff version.
98-
rev: "v0.9.4"
98+
rev: "v0.9.6"
9999
hooks:
100100
# Run the linter
101101
- id: ruff
@@ -107,7 +107,7 @@ repos:
107107

108108
# Autoformat: YAML, JSON, Markdown, etc.
109109
- repo: https://github.com/pycontribs/mirrors-prettier
110-
rev: v3.4.2
110+
rev: v3.5.0
111111
hooks:
112112
- id: prettier
113113
args:

docs/about/faq.md

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,86 @@
11
# ❓ Frequently Asked Questions ❓
22

3-
## Q. What problem do we solve with DroneTM?
3+
## General
4+
5+
### Q. Why should I consider DroneTM over other options?
6+
7+
As of 2025 there are 3 basic options for drones:
8+
9+
1. ~$20,000 and up professional fixed-wing mapping platform
10+
like a Sensefly Ebee or Wingtra One.
11+
2. ~$4,000-$10,000 enterprise quadcopter Like a DJI Mavic
12+
Enterprise, Autel Evo, or DJI Matrice.
13+
3. ~$1,000 micro-quadcopter like a DJI Mini 4 Pro.
14+
15+
#### Option 1: Expensive Route (Consultant)
16+
17+
If you have an expensive operator and want to go big (thousands of km² or more)
18+
and don't mind a lot more administrative hassles, permissions, and are willing
19+
to climb a learning curve, get a Sensefly Ebee or Wingtra One, which come with
20+
dedicated laptop-based planning suites to generate mapping flight missions.
21+
22+
#### Option 2: Mid-Level Cost (DIY Paid Software)
23+
24+
If you have a moderately expensive operator, but still not looking to get into
25+
industrial-scale mapping, you get something like a Mavic Enterprise, and use it
26+
with a commercial flight planning software like DroneDeploy.
27+
28+
This works pretty well (you still might want to use our Drone Tasking Manager to
29+
coordinate flight plans if you have more than one, but at least you can generate
30+
good mapping flight plans with commercial software).
31+
32+
#### Option 3: Low-Cost Community-Driven (Drone-TM)
33+
34+
We usually recommend the DJI Mini 4 Pro for local communities; it's by a
35+
substantial margin the best value for money in terms of image quality per dollar
36+
(from currently available drones). The "Fly More Combo" retails for USD$1050
37+
and comes with 3 batteries and a controller, so you can fly fairly consistently
38+
throughout the day straight after unboxing.
39+
40+
It has some major advantages:
41+
42+
- It's less than 250 grams, so it's very lightly regulated in many countries;
43+
in many places you don't need an operators licence, and often things like
44+
flight ceilings don't apply to micro-drones.
45+
- It has a remarkably good camera, creating imagery that rivals much more
46+
expensive drones.
47+
- It's surprisingly stable in the wind despite its small size.
48+
- It's really cheap (USD$1050 for the Fly More Combo, ~$750 for the drone with
49+
only 1 battery and basic controller) and widely available.
50+
- It's easy to get serviced because it's a very popular consumer drone.
51+
52+
However, the Mini 4 Pro has 2 main drawbacks:
53+
54+
1. It doesn't work with most mapping mission planning software suites (because
55+
DJI has still not released an SDK for it, despite claiming they intended to do so).
56+
2. It's not the fastest flyer, so it can't cover the amount of area that a more
57+
expensive drone can do in a day.
58+
59+
For the former drawback, we specifically developed our
60+
[Drone Tasking Manager](dronetm.org) to work well with the Mini 4 Pro because it's
61+
such an effective, accessible, and affordable option for communities. As far as we
62+
know our DroneTM is the only way to use the Mini 4 Pro for serious professional
63+
mapping, but it does work, and it actually works pretty well.
64+
65+
But be warned; you'd still be investing in a drone for which only our solution
66+
really permits professional mapping. Our solution is open source and free to
67+
use: no "freemium" licensing whereby you only get a limited functionality.
68+
69+
It's a Digital Public Good and we're not trying to make money off it, we're
70+
trying to empower communities with it.
71+
72+
For the latter drawback, it's really a question of who is flying. If it's a
73+
highly-paid person who travels away from home to cover a given area, you want
74+
a larger, faster drone to cover more area per day (you can cover 1-4 km² per
75+
day at 5cm GSD with good enough overlap for decent 3D using a Mini 4 Pro, whereas
76+
you can cover >20km²/day with a Sensefly Ebee or Wingtra One—but those are $20,000
77+
instead of $1050).
78+
79+
If it's local community members flying relatively close to home, it can be much more
80+
cost effective using a small fleet of cheaper drones like the Mini 4 Pro (plus it
81+
helps the local economy and people).
82+
83+
### Q. What problem do we solve with DroneTM?
484

585
**Enhancing Emergency Response**
686
We address the need for faster and more precise deployment of drones during
@@ -22,7 +102,7 @@ platform, ensuring critical information is easily available to stakeholders for
22102

23103
---
24104

25-
## Q. How do I get started with DroneTM?
105+
### Q. How do I get started with DroneTM?
26106

27107
You can contribute to DroneTM in multiple ways:
28108

@@ -33,7 +113,7 @@ You can contribute to DroneTM in multiple ways:
33113

34114
---
35115

36-
## Q. What are the outputs I get from DroneTM? Are they raw drone imagery or the processed data like orthophotos and surface models?
116+
### Q. What are the outputs I get from DroneTM? Are they raw drone imagery or the processed data like orthophotos and surface models?
37117

38118
Currently, only processed data is available for download. The final outputs include:
39119

@@ -42,27 +122,27 @@ Currently, only processed data is available for download. The final outputs incl
42122

43123
---
44124

45-
## Q. Do I need an OSM account or need to create a new account to contribute to DroneTM?
125+
### Q. Do I need an OSM account or need to create a new account to contribute to DroneTM?
46126

47127
No, you don’t need an OSM account. You can simply sign up using your Google account.
48128

49129
---
50130

51-
## Q. What if the project area of my project is large and needs multiple flight operations?
131+
### Q. What if the project area of my project is large and needs multiple flight operations?
52132

53133
DroneTM divides large project areas into multiple tasks, allowing you to adjust the area per task.
54134
**Note:** The maximum project area allowed is 100 sq. km.
55135

56136
---
57137

58-
## Q. What if my project creation contains no-fly zones?
138+
### Q. What if my project creation contains no-fly zones?
59139

60140
During project creation, you can specify that the project area includes
61141
no-fly zones. There is a feature to draw these zones on the project map, and the flight plan will exclude any specified no-fly zones.
62142

63143
---
64144

65-
## Q. What Drones Are Supported?
145+
### Q. What Drones Are Supported?
66146

67147
Currently, DroneTM is tested on **DJI Mini 4 Pro**, and flight plans are optimized for its camera specifications.
68148
However, the system is compatible with any DJI drones that support waypoint features, such as:
@@ -86,7 +166,7 @@ drones, and create custom flight plans as per the specifications provided by the
86166
widening our support signficantly to cheap custom-made drones, and in future
87167
the HOT mapping drone.
88168

89-
## Q. What Drones Are Not Supported?
169+
### Q. What Drones Are Not Supported?
90170

91171
We don't have an exhaustive list of all unsupported drones, but in general, if
92172
the drone is not listed as supported, then DroneTM will probably not work with

src/backend/app/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "2025.2.0"
1+
__version__ = "2025.2.1"

src/backend/app/db/db_models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ class DbUserProfile(Base):
315315
organization_name = cast(str, Column(String, nullable=True))
316316
organization_address = cast(str, Column(String, nullable=True))
317317
job_title = cast(str, Column(String, nullable=True))
318+
oam_api_token = cast(str, Column(String, nullable=True))
318319

319320
notify_for_projects_within_km = cast(int, Column(SmallInteger, nullable=True))
320321
experience_years = cast(int, Column(SmallInteger, nullable=True))
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""oam-api-token
2+
3+
Revision ID: 7b6426a1bd67
4+
Revises: f78cde896334
5+
Create Date: 2025-02-11 05:39:04.833623
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
from alembic import op
12+
import sqlalchemy as sa
13+
from sqlalchemy.dialects import postgresql
14+
15+
# revision identifiers, used by Alembic.
16+
revision: str = "7b6426a1bd67"
17+
down_revision: Union[str, None] = "f78cde896334"
18+
branch_labels: Union[str, Sequence[str], None] = None
19+
depends_on: Union[str, Sequence[str], None] = None
20+
21+
22+
def upgrade() -> None:
23+
# ### commands auto generated by Alembic - please adjust! ###
24+
op.alter_column(
25+
"projects",
26+
"image_processing_status",
27+
existing_type=postgresql.ENUM(
28+
"NOT_STARTED",
29+
"PROCESSING",
30+
"SUCCESS",
31+
"FAILED",
32+
name="imageprocessingstatus",
33+
),
34+
nullable=True,
35+
existing_server_default=sa.text("'NOT_STARTED'::imageprocessingstatus"),
36+
)
37+
op.add_column(
38+
"user_profile", sa.Column("oam_api_token", sa.String(), nullable=True)
39+
)
40+
# ### end Alembic commands ###
41+
42+
43+
def downgrade() -> None:
44+
# ### commands auto generated by Alembic - please adjust! ###
45+
op.drop_column("user_profile", "oam_api_token")
46+
op.alter_column(
47+
"projects",
48+
"image_processing_status",
49+
existing_type=postgresql.ENUM(
50+
"NOT_STARTED",
51+
"PROCESSING",
52+
"SUCCESS",
53+
"FAILED",
54+
name="imageprocessingstatus",
55+
),
56+
nullable=False,
57+
existing_server_default=sa.text("'NOT_STARTED'::imageprocessingstatus"),
58+
)
59+
# ### end Alembic commands ###

src/backend/app/projects/image_processing.py

Lines changed: 41 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ async def download_images_from_s3(
9999
self,
100100
bucket_name: str,
101101
local_dir: str,
102-
task_id: Optional[uuid.UUID] = None,
102+
task_id: uuid.UUID,
103103
batch_size: int = 20,
104104
):
105105
"""
@@ -110,11 +110,7 @@ async def download_images_from_s3(
110110
:param task_id: Optional specific task ID
111111
:param batch_size: Number of images to download concurrently
112112
"""
113-
prefix = (
114-
f"dtm-data/projects/{self.project_id}/{task_id}"
115-
if task_id
116-
else f"dtm-data/projects/{self.project_id}"
117-
)
113+
prefix = f"dtm-data/projects/{self.project_id}/{task_id}"
118114
objects = list_objects_from_bucket(bucket_name, prefix)
119115

120116
if not objects:
@@ -123,16 +119,26 @@ async def download_images_from_s3(
123119

124120
log.info(f"Downloading images from S3 for task {task_id}...")
125121

122+
accepted_file_extensions = (".jpg", ".jpeg", ".png", ".txt", ".laz")
123+
126124
s3_download_url = settings.S3_DOWNLOAD_ROOT
127125
if s3_download_url:
128-
object_urls = [f"{s3_download_url}/{obj.object_name}" for obj in objects]
126+
object_urls = [
127+
f"{s3_download_url}/{obj.object_name}"
128+
for obj in objects
129+
if obj.object_name.lower().endswith(accepted_file_extensions)
130+
]
129131
else:
130132
# generate pre-signed URL for each object
131133
object_urls = [
132-
get_presigned_url(bucket_name, obj.object_name, 12) for obj in objects
134+
get_presigned_url(bucket_name, obj.object_name, 12)
135+
for obj in objects
136+
if obj.object_name.lower().endswith(accepted_file_extensions)
133137
]
134138

135139
total_files = len(object_urls)
140+
log.info(f"{total_files} images found in S3 for task {task_id}")
141+
136142
async with aiohttp.ClientSession() as session:
137143
for i in range(0, total_files, batch_size):
138144
batch = object_urls[i : i + batch_size]
@@ -145,7 +151,7 @@ async def download_images_from_s3(
145151
session,
146152
url,
147153
os.path.join(
148-
local_dir, f"{uuid.uuid4()}_file_{i + j + 1}.jpg"
154+
local_dir, f"{task_id}_file_{i + j + 1}.jpg"
149155
), # unique image name are maintained with uuid
150156
)
151157
for j, url in enumerate(batch)
@@ -303,40 +309,33 @@ async def process_images_from_s3(
303309
bucket_name, name=name, options=options, webhook=webhook
304310
)
305311

312+
# If webhook is passed, webhook does this job.
306313
if not webhook:
307-
# If webhook is passed, webhook does this job.
308-
if not webhook:
309-
# Monitor task progress
310-
self.monitor_task(task)
311-
312-
# Optionally, download results
313-
output_file_path = f"/tmp/{self.project_id}"
314-
path_to_download = self.download_results(
315-
task, output_path=output_file_path
316-
)
317-
318-
# Upload the results into s3
319-
s3_path = (
320-
f"dtm-data/projects/{self.project_id}/{self.task_id}/assets.zip"
321-
)
322-
add_file_to_bucket(bucket_name, path_to_download, s3_path)
323-
# now update the task as completed in Db.
324-
# Call the async function using asyncio
325-
326-
# Update background task status to COMPLETED
327-
update_task_status_sync = async_to_sync(task_logic.update_task_state)
328-
update_task_status_sync(
329-
self.db,
330-
self.project_id,
331-
self.task_id,
332-
self.user_id,
333-
"Task completed.",
334-
State.IMAGE_UPLOADED,
335-
State.IMAGE_PROCESSING_FINISHED,
336-
timestamp(),
337-
)
338-
return task
339-
314+
# Monitor task progress
315+
self.monitor_task(task)
316+
317+
# Optionally, download results
318+
output_file_path = f"/tmp/{self.project_id}"
319+
path_to_download = self.download_results(task, output_path=output_file_path)
320+
321+
# Upload the results into s3
322+
s3_path = f"dtm-data/projects/{self.project_id}/{self.task_id}/assets.zip"
323+
add_file_to_bucket(bucket_name, path_to_download, s3_path)
324+
# now update the task as completed in Db.
325+
# Call the async function using asyncio
326+
327+
# Update background task status to COMPLETED
328+
update_task_status_sync = async_to_sync(task_logic.update_task_state)
329+
update_task_status_sync(
330+
self.db,
331+
self.project_id,
332+
self.task_id,
333+
self.user_id,
334+
"Task completed.",
335+
State.IMAGE_UPLOADED,
336+
State.IMAGE_PROCESSING_FINISHED,
337+
timestamp(),
338+
)
340339
return task
341340

342341
async def process_images_for_all_tasks(

0 commit comments

Comments
 (0)