Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ead0636
Handle setting CRS to GCP when GCP file is selected
pierotofy Nov 14, 2025
2de865f
GCP client side validation, set CRS automatically to match GCP
pierotofy Nov 14, 2025
409d9e3
Bump version
pierotofy Nov 14, 2025
db5011a
Handle compound CRS raster imports
pierotofy Nov 14, 2025
6171189
Compute bounds manually in 4326, bypass GEOS
pierotofy Nov 14, 2025
d647630
Remove imports
pierotofy Nov 14, 2025
2ceeeba
Fix geometry fields in admin view
pierotofy Nov 14, 2025
02159c1
Merge branch 'master' of https://github.com/OpenDroneMap/WebODM into crs
pierotofy Nov 21, 2025
3b59f15
Display CRS name in export dialogs
pierotofy Nov 21, 2025
669d00d
Metadata endpoint CRS support, colormap encoding
pierotofy Nov 21, 2025
0dc845c
Read Z units with rasterio
pierotofy Nov 21, 2025
5b3ca6e
Merge branch 'master' of https://github.com/OpenDroneMap/WebODM into crs
pierotofy Nov 24, 2025
e595d40
Fix volume calculation on non-metric DEMs
pierotofy Nov 24, 2025
3fca4d8
Contours plugin non-metric support
pierotofy Nov 24, 2025
f486322
Copy vertical units information when exporting
pierotofy Nov 24, 2025
7223d87
Handle non-metric units in 3D view, add camera scale slider
pierotofy Nov 25, 2025
bb73330
Update README, add trademark doc
pierotofy Nov 25, 2025
f313ef9
Remove unused functions
pierotofy Nov 25, 2025
43eea1e
Test metadata endpoint
pierotofy Nov 25, 2025
3b04228
Update locales
pierotofy Nov 25, 2025
3b9a182
Update shield
pierotofy Nov 25, 2025
e929359
Update README.md
pierotofy Nov 25, 2025
9d46ca5
Purchase --> Installer
pierotofy Nov 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<img alt="WebODM" src="https://user-images.githubusercontent.com/1951843/34074943-8f057c3c-e287-11e7-924d-3ccafa60c43a.png" width="180">

![Build Status](https://img.shields.io/github/actions/workflow/status/OpenDroneMap/WebODM/build-and-publish.yml?branch=master) ![Version](https://img.shields.io/github/v/release/OpenDroneMap/WebODM) [![Translated](https://hosted.weblate.org/widgets/webodm/-/svg-badge.svg)](https://hosted.weblate.org/engage/webodm/) [![Download](https://img.shields.io/badge/Download-%E2%86%93-pink)](#getting-started) [![Purchase](https://img.shields.io/badge/Purchase-%F0%9F%9B%92-white)](https://opendronemap.org/webodm/download/)
![Build Status](https://img.shields.io/github/actions/workflow/status/OpenDroneMap/WebODM/build-and-publish.yml?branch=master) ![Version](https://img.shields.io/github/v/release/OpenDroneMap/WebODM) [![Translated](https://hosted.weblate.org/widgets/webodm/-/svg-badge.svg)](https://hosted.weblate.org/engage/webodm/) [![Download](https://img.shields.io/badge/Download-%E2%86%93-pink)](#getting-started) [![Installer](https://img.shields.io/badge/Installer-%F0%9F%9B%92-white)](https://opendronemap.org/webodm/download/) [![Lightning](https://img.shields.io/badge/Lightning-%E2%98%81-white)](https://webodm.net/)

A user-friendly, commercial grade software for drone image processing. Generate georeferenced maps, point clouds, elevation models and textured 3D models from aerial images. It supports multiple engines for processing, currently [ODM](https://github.com/OpenDroneMap/ODM), [MicMac](https://github.com/OpenDroneMap/NodeMICMAC/) and [LGT](https://webodm.net/lgt).

Expand Down Expand Up @@ -477,7 +477,7 @@ There are many ways to contribute back to the project:
- Help us [translate](#translations) WebODM in your language.
- Help us classify [point cloud datasets](https://github.com/OpenDroneMap/ODMSemantic3D).
- Spread the word about WebODM and OpenDroneMap on social media.
- While we don't accept donations, you can purchase an [installer](https://webodm.org/download#installer), a [book](https://odmbook.com/) or a [sponsor package](https://github.com/users/pierotofy/sponsorship).
- While we don't accept donations, you can purchase an [installer](https://webodm.org/download#installer), [cloud processing](https://webodm.net) or a [book](https://odmbook.com/).
- You can [pledge funds](https://fund.webodm.org) for getting new features built and bug fixed.
- Become a contributor 🤘

Expand Down Expand Up @@ -538,4 +538,4 @@ WebODM is licensed under the terms of the [GNU Affero General Public License v3.

# Trademark

See [Trademark Guidelines](https://github.com/OpenDroneMap/documents/blob/master/TRADEMARK.md)
See [Trademark Guidelines](https://github.com/OpenDroneMap/WebODM/blob/master/TRADEMARK.md)
98 changes: 98 additions & 0 deletions TRADEMARK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# WebODM Trademark Guidelines

WebODM is an open source project and [UAV4GEO](https://uav4geo.com) is the steward of its trademark. Because the code is available to download and modify, proper use of the WebODM trademark is essential to inform users whether or not UAV4GEO stands behind a product or service. When using WebODM trademarks you must comply with these guidelines.

The trademark names include:

* WebODM
* WebODM Lightning

The trademark logos include:

![WebODM](https://opendronemap.org/wp-content/uploads/2018/07/webodm-icon-64x64.png)

However, this is not a complete list of names, logos, and brand features, all of which are subject to these guidelines.

If you want to report misuse of a WebODM trademark, please contact us via https://uav4geo.com/contact

## When do I need specific permission to use a WebODM trademark?

You may do the following without receiving specific permission from UAV4GEO:

* Use WebODM wordmarks in text to truthfully refer to and/or link to unmodified WebODM programs, products, services and technologies.
* Use WebODM logos in visuals to truthfully refer to and/or to link to the applicable programs, products, services and technologies hosted on UAV4GEO servers.
* Use WebODM wordmarks to explain that your software is based on WebODM's open source code, or is compatible with WebODM's software.
* Describe a social media account, page, or community in accordance with the [Social Media Guidelines](#social-media-guidelines).

All other uses of a WebODM trademark require our prior written permission. This includes any use of a WebODM trademark in a domain name. Contact us at https://uav4geo.com/contact for more information.

## When allowed, how should I use a WebODM trademark?

### General Guidelines

#### Do:

* Use the WebODM trademark exactly as shown in the list above.
* Use WebODM wordmarks only as an adjective, never as a noun or verb. Do not use them in plural or possessive forms. Instead, use the generic term for the WebODM product or service following the trademark, for example: WebODM processing software.

#### Don't:

* Don't use WebODM trademarks in the name of your business, product, service, app, domain name, publication, or other offering.
* Don't use marks, logos, company names, slogans, domain names, or designs that are confusingly similar to WebODM trademarks.
* Don't use WebODM trademarks in a way that incorrectly implies affiliation with, or sponsorship, endorsement, or approval by UAV4GEO of your products or services.
* Don't display WebODM trademarks more prominently than your product, service, or company name.
* Don't use WebODM trademarks on merchandise for sale (e.g., selling t-shirts, mugs, etc.)
* Don't use WebODM trademarks for any other form of commercial use (e.g. offering technical support services), unless such use is limited to a truthful and descriptive reference (e.g. Independent technical support for WebODM's software).
* Don't modify WebODM trademarks, abbreviate them, or combine them with any other symbols, words, or images, or incorporate them into a tagline or slogan.

### Social Media Guidelines

In addition to the General Guidelines above, the name and handle of your social media account and any and all pages cannot begin with an WebODM trademark. In addition, WebODM logos cannot be used in a way that might suggest affiliation with WebODM, including, but not limited to, the account, profile, or header images. The only exception to these requirements is if you've received prior permission from WebODM.

For example, you cannot name your account, page, or community "WebODM Representatives" or "WebODM Software". However, it would be acceptable to name your account, page, or community "Fans of WebODM" or "Information about WebODM Software" as long as you do not use the WebODM trademarks or WebODM logos or otherwise suggest any affiliation with WebODM.

### Open Source Project Guidelines

WebODM's license and code says what you can and cannot do with the code itself but does not give permission to use WebODM's trademarks. If you choose to build on or modify WebODM's open source code for your own project,

#### You Must:

* Follow the terms of the Open Source License(s) for WebODM software products and code.
* Choose branding, logos, and trademarks that denotes your own unique identity so as to clearly signal to users that there is no affiliation with or endorsement by WebODM.
* Follow the General Guidelines, above.

#### You Must NOT:

* Use any WebODM trademark in connection with the user-facing name or branding of your project.
* Use any WebODM trademark or any part of any WebODM trademark to incorrectly suggest or give the impression your software is actually published by, affiliated with, or endorsed by WebODM.

For example, please do not name your project, [Something]-WebODM

#### You May:

* State in words (not using logos or images) that your product "works with" or "is compatible" with WebODM, if that is true.
* State in words (not using logos or images) that your project is based on WebODM open source technology, if that is true, as long as you also include a statement that your project is not officially associated with WebODM or its products.

For instance, you may state that your project:

"is proudly built from WebODM's open source software"

as long as you also include the statement equally prominently:

"[Brand Name] and [Product Name] are not officially associated with WebODM or its products."

### Cloud-based Services

If you offer a cloud-based service that provides remote access to WebODM software, it is important that you do so in a way that does not confuse users about who is offering the service. You must not use WebODM trademarks in the name of your product or service.

You may state in words (not using logos or images) that your product or service features or provides access to unaltered WebODM software, if this is true.

For example:

Acceptable: [Your Product Name] featuring WebODM's processing software

Incorrect: WebODM [Your Product Name]

### Community Guidelines

Various permissions to use WebODM Trademarks have been provided to various members of the community, and these WebODM Trademark Guidelines do not alter any such previously granted permissions.
23 changes: 22 additions & 1 deletion app/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,28 @@ def has_add_permission(self, request):
list_display = ('id', 'name', 'project', 'processing_node', 'created_at', 'status', 'last_error')
list_filter = ('status', 'project',)
search_fields = ('id', 'name', 'project__name')

exclude = ('orthophoto_extent', 'dsm_extent', 'dtm_extent', 'crop', )
readonly_fields = ('orthophoto_extent_wkt', 'dsm_extent_wkt', 'dtm_extent_wkt', 'crop_wkt', )

def orthophoto_extent_wkt(self, obj):
if obj.orthophoto_extent:
return obj.orthophoto_extent.wkt
return None

def dsm_extent_wkt(self, obj):
if obj.dsm_extent:
return obj.dsm_extent.wkt
return None

def dtm_extent_wkt(self, obj):
if obj.dtm_extent:
return obj.dtm_extent.wkt
return None

def crop_wkt(self, obj):
if obj.crop:
return obj.crop.wkt
return None

admin.site.register(Task, TaskAdmin)

Expand Down
6 changes: 5 additions & 1 deletion app/api/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from app.security import path_traversal_check
from django.utils.translation import gettext_lazy as _
from .fields import PolygonGeometryField
from app.geoutils import geom_transform_wkt_bbox
from app.geoutils import geom_transform_wkt_bbox, get_srs_name_units_from_epsg
from webodm import settings

def flatten_files(request_files):
Expand All @@ -59,6 +59,7 @@ class TaskSerializer(serializers.ModelSerializer):
extent = serializers.SerializerMethodField()
tags = TagsField(required=False)
crop = PolygonGeometryField(required=False, allow_null=True)
srs = serializers.SerializerMethodField()

def get_processing_node_name(self, obj):
if obj.processing_node is not None:
Expand Down Expand Up @@ -90,6 +91,9 @@ def get_can_rerun_from(self, obj):

def get_extent(self, obj):
return obj.get_extent()

def get_srs(self, obj):
return get_srs_name_units_from_epsg(obj.epsg)

class Meta:
model = models.Task
Expand Down
39 changes: 36 additions & 3 deletions app/api/tiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@
from .hillshade import LightSource
from .formulas import lookup_formula, get_algorithm_list, get_auto_bands
from .tasks import TaskNestedView
from app.geoutils import geom_transform_wkt_bbox
from app.geoutils import geom_transform_wkt_bbox, get_rasterio_to_meters_factor
from rest_framework import exceptions
from rest_framework.response import Response
from worker.tasks import export_raster, export_pointcloud
from django.utils.translation import gettext as _
import warnings
from functools import lru_cache

# Disable: NotGeoreferencedWarning: Dataset has no geotransform, gcps, or rpcs. The identity matrix be returned.
warnings.filterwarnings("ignore", category=NotGeoreferencedWarning)
Expand All @@ -47,7 +48,19 @@
for custom_colormap in custom_colormaps:
colormap = colormap.register(custom_colormap)


@lru_cache(maxsize=128)
def get_colormap_encoded_values(cmap):
values = colormap.get(cmap).values()
# values = [[R, G, B, A], [R, G, B, A], ...]
encoded_values = []
for rgba in values:
# Pack R, G, B, A (each 0-255) into a 32-bit integer
# Format: 0xRRGGBBAA (R in most significant byte)
encoded = (rgba[0] << 24) | (rgba[1] << 16) | (rgba[2] << 8) | rgba[3]
encoded_values.append(encoded)

return encoded_values

def get_zoom_safe(src_dst):
minzoom, maxzoom = src_dst.spatial_info["minzoom"], src_dst.spatial_info["maxzoom"]
if maxzoom < minzoom:
Expand Down Expand Up @@ -171,8 +184,13 @@ def get(self, request, pk=None, project_pk=None, tile_type=""):
raster_path = get_raster_path(task, tile_type)
if not os.path.isfile(raster_path):
raise exceptions.NotFound()

to_meter = 1.0
try:
with COGReader(raster_path) as src:
if tile_type in ['dsm', 'dtm']:
to_meter = get_rasterio_to_meters_factor(src.dataset)

band_count = src.dataset.meta['count']
if boundaries_feature is not None:
cutline = create_cutline(src.dataset, boundaries_feature, CRS.from_string('EPSG:4326'))
Expand Down Expand Up @@ -265,13 +283,22 @@ def get(self, request, pk=None, project_pk=None, tile_type=""):
info['color_maps'] = []
info['algorithms'] = algorithms
info['auto_bands'] = auto_bands

if to_meter != 1.0:
for b in info['statistics']:
info['statistics'][b]['min'] *= to_meter
info['statistics'][b]['max'] *= to_meter
info['statistics'][b]['std'] *= to_meter
info['statistics'][b]['percentiles'][0] *= to_meter
info['statistics'][b]['percentiles'][1] *= to_meter
info['statistics'][b]['histogram'][1] = [n * to_meter for n in info['statistics'][b]['histogram'][1]]

if colormaps:
for cmap in colormaps:
try:
info['color_maps'].append({
'key': cmap,
'color_map': colormap.get(cmap).values(),
'color_map': get_colormap_encoded_values(cmap),
'label': cmap_labels.get(cmap, cmap)
})
except FileNotFoundError:
Expand Down Expand Up @@ -369,6 +396,7 @@ def get(self, request, pk=None, project_pk=None, tile_type="", z="", x="", y="",
if not os.path.isfile(url):
raise exceptions.NotFound()

to_meter = 1.0
with COGReader(url) as src:
if not src.tile_exists(z, x, y):
raise exceptions.NotFound(_("Outside of bounds"))
Expand All @@ -393,6 +421,9 @@ def get(self, request, pk=None, project_pk=None, tile_type="", z="", x="", y="",
else:
vrt_options = None

if tile_type in ['dsm', 'dtm']:
to_meter = get_rasterio_to_meters_factor(src.dataset)

# Handle N-bands datasets for orthophotos (not plant health)
if tile_type == 'orthophoto' and expr is None:
ci = src.dataset.colorinterp
Expand Down Expand Up @@ -451,6 +482,8 @@ def get(self, request, pk=None, project_pk=None, tile_type="", z="", x="", y="",
intensity = None
try:
rescale_arr = list(map(float, rescale.split(",")))
if tile_type in ['dsm', 'dtm']:
rescale_arr = [v / to_meter for v in rescale_arr]
except ValueError:
raise exceptions.ValidationError(_("Invalid rescale value"))

Expand Down
83 changes: 83 additions & 0 deletions app/geoutils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import rasterio.warp
import numpy as np
from rasterio.crs import CRS
from rasterio.warp import transform_bounds
from osgeo import osr, gdal
from functools import lru_cache
osr.DontUseExceptions()

SUPPORTED_UNITS = ["m", "ft", "US survey foot"]
UNIT_TO_M = {
"m": 1.0,
"ft": 0.3048,
"US survey foot": 1200.0 / 3937.0,
}

# GEOS has some weird bug where
# we can't simply call geom.tranform(srid)
Expand Down Expand Up @@ -79,3 +90,75 @@ def geom_transform(geom, epsg):
return list(zip(tx, ty))
else:
raise ValueError("Cannot transform complex geometries to WKT")


def epsg_from_wkt(wkt):
srs = osr.SpatialReference()
if srs.ImportFromWkt(wkt) != 0:
return None

epsg = srs.GetAuthorityCode(None)
if epsg is not None:
return None

# Try to get the 2D component
if srs.IsCompound():
if srs.DemoteTo2D() != 0:
return None

epsg = srs.GetAuthorityCode(None)
if epsg is not None:
return epsg


def get_raster_bounds_wkt(raster_path, target_srs="EPSG:4326"):
with rasterio.open(raster_path) as src:
if src.crs is None:
return None

left, bottom, right, top = src.bounds
w, s, e, n = transform_bounds(
src.crs,
target_srs,
left, bottom, right, top
)

wkt = f"POLYGON(({w} {s}, {w} {n}, {e} {n}, {e} {s}, {w} {s}))"
return wkt

@lru_cache(maxsize=1000)
def get_srs_name_units_from_epsg(epsg):
if epsg is None:
return {'name': '', 'units': 'm'}

srs = osr.SpatialReference()
if srs.ImportFromEPSG(epsg) != 0:
return {'name': '', 'units': 'm'}

name = srs.GetAttrValue("PROJCS")
if name is None:
name = srs.GetAttrValue("GEOGCS")

if name is None:
return {'name': '', 'units': 'm'}

units = srs.GetAttrValue('UNIT')
if units is None:
units = 'm' # Default to meters
elif units not in SUPPORTED_UNITS:
units = 'm' # Unsupported

return {'name': name, 'units': units}

def get_rasterio_to_meters_factor(rasterio_ds):
if isinstance(rasterio_ds, str):
with rasterio.open(rasterio_ds, 'r') as ds:
return get_rasterio_to_meters_factor(ds)

units = rasterio_ds.units
if len(units) >= 1:
unit = units[0]
if unit is not None and unit != "" and unit in SUPPORTED_UNITS:
return UNIT_TO_M.get(unit, 1.0)
return 1.0

Loading