diff --git a/README.md b/README.md index 22dbac1b4..bc8708847 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ WebODM -![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). @@ -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 🤘 @@ -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) diff --git a/TRADEMARK.md b/TRADEMARK.md new file mode 100644 index 000000000..b7eed6d6e --- /dev/null +++ b/TRADEMARK.md @@ -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. \ No newline at end of file diff --git a/app/admin.py b/app/admin.py index 59ce7d40d..397040cc8 100644 --- a/app/admin.py +++ b/app/admin.py @@ -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) diff --git a/app/api/tasks.py b/app/api/tasks.py index 9e94de092..a5691c63c 100644 --- a/app/api/tasks.py +++ b/app/api/tasks.py @@ -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): @@ -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: @@ -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 diff --git a/app/api/tiler.py b/app/api/tiler.py index 79f31dcf1..7b84a68ee 100644 --- a/app/api/tiler.py +++ b/app/api/tiler.py @@ -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) @@ -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: @@ -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')) @@ -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: @@ -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")) @@ -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 @@ -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")) diff --git a/app/geoutils.py b/app/geoutils.py index 816a8f2bd..001fb3273 100644 --- a/app/geoutils.py +++ b/app/geoutils.py @@ -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) @@ -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 + diff --git a/app/models/task.py b/app/models/task.py index a6e9519d6..802f1cca1 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -22,8 +22,6 @@ import requests from PIL import Image Image.MAX_IMAGE_PIXELS = 4096000000 -from django.contrib.gis.gdal import GDALRaster -from django.contrib.gis.gdal import OGRGeometry from django.contrib.gis.geos import GEOSGeometry from django.contrib.postgres import fields from django.core.files.uploadedfile import InMemoryUploadedFile @@ -41,7 +39,7 @@ from app.pointcloud_utils import is_pointcloud_georeferenced from app.testwatch import testWatch from app.security import path_traversal_check -from app.geoutils import geom_transform +from app.geoutils import geom_transform, epsg_from_wkt, get_raster_bounds_wkt, get_srs_name_units_from_epsg from nodeodm import status_codes from nodeodm.models import ProcessingNode from pyodm.exceptions import NodeResponseError, NodeConnectionError, NodeServerError, OdmError @@ -1012,32 +1010,16 @@ def extract_assets_and_complete(self): except IOError as e: logger.warning("Cannot create Cloud Optimized GeoTIFF for %s (%s). This will result in degraded visualization performance." % (raster_path, str(e))) - # Read extent and SRID - raster = GDALRaster(raster_path) - extent = OGRGeometry.from_bbox(raster.extent) - - # Make sure PostGIS supports it - with connection.cursor() as cursor: - cursor.execute("SELECT SRID FROM spatial_ref_sys WHERE SRID = %s", [raster.srid]) - if cursor.rowcount == 0: - raise NodeServerError(gettext("Unsupported SRS %(code)s. Please make sure you picked a supported SRS.") % {'code': str(raster.srid)}) - - # It will be implicitly transformed into the SRID of the model’s field - # self.field = GEOSGeometry(...) - setattr(self, field, GEOSGeometry(extent.wkt, srid=raster.srid)) - - logger.info("Populated extent field with {} for {}".format(raster_path, self)) + # Read extent + extent_wkt = get_raster_bounds_wkt(raster_path) + if extent_wkt is not None: + extent = GEOSGeometry(extent_wkt, srid=4326) + setattr(self, field, extent) + logger.info("Populated extent field with {} for {}".format(raster_path, self)) + else: + logger.warning("Cannot populate extent field with {} for {}, not georeferenced".format(raster_path, self)) self.check_ept() - - # Flushes the changes to the *_extent fields - # and immediately reads them back into Python - # This is required because GEOS screws up the X/Y conversion - # from the raster CRS to 4326, whereas PostGIS seems to do it correctly :/ - self.status = status_codes.RUNNING # avoid telling clients that task is completed prematurely - self.save() - self.refresh_from_db() - self.update_available_assets_field() self.update_epsg_field() self.update_orthophoto_bands_field() @@ -1158,6 +1140,7 @@ def get_map_items(self): 'camera_shots': camera_shots, 'ground_control_points': ground_control_points, 'epsg': self.epsg, + 'srs': get_srs_name_units_from_epsg(self.epsg), 'orthophoto_bands': self.orthophoto_bands, 'crop': self.crop is not None, 'extent': self.get_extent(), @@ -1182,6 +1165,7 @@ def get_model_display_params(self): 'public': self.public, 'public_edit': self.public_edit, 'epsg': self.epsg, + 'srs': get_srs_name_units_from_epsg(self.epsg), 'crop_projected': self.get_projected_crop() } @@ -1225,8 +1209,18 @@ def update_epsg_field(self, commit=False): try: with rasterio.open(asset_path) as f: if f.crs is not None: - epsg = f.crs.to_epsg() - break # We assume all assets are in the same CRS + code = f.crs.to_epsg() + if code is not None: + epsg = code + break # We assume all assets are in the same CRS + else: + # Try to get code from WKT + wkt = f.crs.to_wkt() + if wkt is not None: + code = epsg_from_wkt(wkt) + if code is not None: + epsg = code + break except Exception as e: logger.warning(e) diff --git a/app/raster_utils.py b/app/raster_utils.py index 8ca669f67..1f4d051cb 100644 --- a/app/raster_utils.py +++ b/app/raster_utils.py @@ -142,6 +142,7 @@ def p(text, perc=0): with COGReader(input) as ds_src: src = ds_src.dataset profile = src.meta.copy() + units = ds_src.dataset.units win = Window(0, 0, src.width, src.height) # Output format @@ -349,6 +350,10 @@ def update_rgb_colorinterp(dst): elif dem: # Apply hillshading, colormaps to elevation with rasterio.open(output_raster, 'w', **profile) as dst: + # Copy units information + if export_format == "gtiff" and not rgb and len(units) == len(dst.units): + dst.units = units + for idx, (w, dst_w) in enumerate(subwins): p(f"Processing tile {idx}/{num_wins}", progress_per_win) diff --git a/app/static/app/js/ModelView.jsx b/app/static/app/js/ModelView.jsx index dfdcf38a7..bce007877 100644 --- a/app/static/app/js/ModelView.jsx +++ b/app/static/app/js/ModelView.jsx @@ -106,7 +106,8 @@ class TexturedModelMenu extends React.Component{ class CamerasMenu extends React.Component{ static propTypes = { - toggleCameras: PropTypes.func.isRequired + toggleCameras: PropTypes.func.isRequired, + changeCameraScale: PropTypes.func.isRequired } constructor(props){ @@ -117,20 +118,44 @@ class CamerasMenu extends React.Component{ } } + componentDidMount(){ + if (this.sldCameraSize){ + $(this.sldCameraSize).slider({ + min: 0.1, max: 4, step: 0.1, + value: 1.0, + slide: (event, ui) => { + this.props.changeCameraScale(ui.value); + } + }); + } + } + handleClick = (e) => { this.setState({showCameras: e.target.checked}); this.props.toggleCameras(e); } render(){ - return (); + return (
+
+
+
+ {_("Size")} +
this.sldCameraSize = domNode}>
+
+
); } } +const CAMERA_SCALES = { + 'm': 1.0, + 'ft': 3.28, + 'US survey foot': 3.28 +}; + class ModelView extends React.Component { static defaultProps = { task: null, @@ -155,7 +180,8 @@ class ModelView extends React.Component { initializingModel: false, texModelLoadProgress: null, selectedCamera: null, - modalOpen: false + modalOpen: false, + cameraScale: CAMERA_SCALES[props.task.srs.units] || 1.0 }; this.pointCloud = null; @@ -340,7 +366,10 @@ class ModelView extends React.Component { } if (this.hasCameras()){ - window.ReactDOM.render(, $("#cameras_button").get(0)); + window.ReactDOM.render(, $("#cameras_button").get(0)); }else{ $("#cameras").hide(); $("#cameras_container").hide(); @@ -479,13 +508,22 @@ class ModelView extends React.Component { if (!window.viewer) return; const us = getUnitSystem(); + + // GDAL --> Potree + const UNIT_MAP = { + 'm': 'm', + 'ft': 'ft', + 'US survey foot': 'ft (US)' + }; + + const dsUnit = UNIT_MAP[this.props.task.srs.units] || 'm'; if (us === 'metric'){ - window.viewer.setLengthUnitAndDisplayUnit('m', 'm'); + window.viewer.setLengthUnitAndDisplayUnit(dsUnit, 'm'); }else if (us === 'imperial'){ - window.viewer.setLengthUnitAndDisplayUnit('m', 'ft'); + window.viewer.setLengthUnitAndDisplayUnit(dsUnit, 'ft'); }else if (us === 'imperialUS'){ - window.viewer.setLengthUnitAndDisplayUnit('m', 'ft (US)'); + window.viewer.setLengthUnitAndDisplayUnit(dsUnit, 'ft (US)'); } } @@ -621,7 +659,7 @@ class ModelView extends React.Component { }); cameraMesh.matrixAutoUpdate = false; - let scale = 1.0; + let scale = this.state.cameraScale; // if (!this.pointCloud.projection) scale = 0.1; cameraMesh.matrix.set(...getMatrix(feat.properties.translation, feat.properties.rotation, scale).elements); @@ -662,6 +700,14 @@ class ModelView extends React.Component { }); } + changeCameraScale = (value) => { + if (this.cameraMeshes.length === 0) return; + + this.cameraMeshes.forEach(cam => { + cam.parent.scale.setScalar(value); + }); + } + loadGltf = (url, cb, onProgress) => { if (!this.gltfLoader) this.gltfLoader = new THREE.GLTFLoader(); if (!this.dracoLoader) { diff --git a/app/static/app/js/classes/Gcp.js b/app/static/app/js/classes/Gcp.js index c377d30b8..4280e9056 100644 --- a/app/static/app/js/classes/Gcp.js +++ b/app/static/app/js/classes/Gcp.js @@ -1,23 +1,22 @@ +import { _, interpolate } from './gettext'; + class Gcp{ constructor(text){ - this.text = text; - } - - // Scale the image location of GPCs - // according to the values specified in the map - // @param imagesRatioMap {Object} object in which keys are image names and values are scaling ratios - // example: {'DJI_0018.jpg': 0.5, 'DJI_0019.JPG': 0.25} - // @return {Gcp} a new GCP object - resize(imagesRatioMap, muteWarnings = false){ - // Make sure dict is all lower case and values are floats - let ratioMap = {}; - for (let k in imagesRatioMap) ratioMap[k.toLowerCase()] = parseFloat(imagesRatioMap[k]); + this.crs = ""; + this.errors = []; + // this.entries = []; - const lines = this.text.split(/\r?\n/); - let output = ""; + const lines = text.split(/\r?\n/); if (lines.length > 0){ - output += lines[0] + '\n'; // coordinate system description + this.crs = lines[0]; + + // Check header + let c = this.crs.toUpperCase(); + console.log(c); + if (!c.startsWith("WGS84") && !c.startsWith("+PROJ") && !c.startsWith("EPSG:")){ + this.errors.push(interpolate(_("Invalid CRS: %(line)s"), { line: this.crs } )); + } for (let i = 1; i < lines.length; i++){ let line = lines[i].trim(); @@ -25,34 +24,30 @@ class Gcp{ let parts = line.split(/\s+/); if (parts.length >= 6){ let [x, y, z, px, py, imagename, ...extracols] = parts; - let ratio = ratioMap[imagename.toLowerCase()]; px = parseFloat(px); py = parseFloat(py); - - if (ratio !== undefined){ - px *= ratio; - py *= ratio; - }else{ - if (!muteWarnings) console.warn(`${imagename} not found in ratio map. Are you missing some images?`); + x = parseFloat(x); + y = parseFloat(y); + z = parseFloat(y); + if (isNaN(px) || isNaN(py) || isNaN(x) || isNaN(y)){ + this.errors.push(interpolate(_("Invalid line %(num)s: %(line)s"), { num: i + 1, line })); + continue; } - - let extra = extracols.length > 0 ? ' ' + extracols.join(' ') : ''; - output += `${x} ${y} ${z} ${px.toFixed(8)} ${py.toFixed(8)} ${imagename}${extra}\n`; }else{ - if (!muteWarnings) console.warn(`Invalid GCP format at line ${i}: ${line}`); - output += line + '\n'; + this.errors.push(interpolate(_("Invalid line %(num)s: %(line)s"), { num: i + 1, line })); } } } - } - return new Gcp(output); + }else{ + this.errors.push(_("Empty GCP file")); + } } - toString(){ - return this.text; + valid(){ + return this.errors.length === 0; } } -module.exports = Gcp; +export default Gcp; diff --git a/app/static/app/js/components/EditTaskForm.jsx b/app/static/app/js/components/EditTaskForm.jsx index 4d76fa7a5..3426b7f1b 100644 --- a/app/static/app/js/components/EditTaskForm.jsx +++ b/app/static/app/js/components/EditTaskForm.jsx @@ -28,7 +28,8 @@ class EditTaskForm extends React.Component { inReview: PropTypes.bool, task: PropTypes.object, suggestedTaskName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - getCropPolygon: PropTypes.func + getCropPolygon: PropTypes.func, + getGcpFile: PropTypes.func }; constructor(props){ @@ -368,6 +369,16 @@ class EditTaskForm extends React.Component { } } + // If a processing node supports "crs" as an option + // and a GCP file is provided, and the user hasn't specified + // a preference, default to "gcp" (set the CRS to use the GCP's CRS) + if (this.props.getGcpFile){ + if (this.props.getGcpFile() && optionNames['crs']){ + let crsOpt = optsCopy.find(opt => opt.name === 'crs'); + if (!crsOpt) optsCopy.push({name: 'crs', value: 'gcp'}); + } + } + return optsCopy.filter(opt => optionNames[opt.name]); } diff --git a/app/static/app/js/components/ExportAssetPanel.jsx b/app/static/app/js/components/ExportAssetPanel.jsx index c24574527..cdc34239d 100644 --- a/app/static/app/js/components/ExportAssetPanel.jsx +++ b/app/static/app/js/components/ExportAssetPanel.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import '../css/ExportAssetPanel.scss'; import ErrorMessage from './ErrorMessage'; import Storage from '../classes/Storage'; -import { _ } from '../classes/gettext'; +import { _, interpolate } from '../classes/gettext'; import Utils from '../classes/Utils'; import Workers from '../classes/Workers'; @@ -74,7 +74,7 @@ export default class ExportAssetPanel extends React.Component { error: "", format: props.exportFormats[0], epsg: this.props.task.epsg || null, - customEpsg: Storage.getItem("last_export_custom_epsg") || "4326", + customEpsg: Storage.getItem("last_export_custom_epsg") || "3857", resample: 0, exporting: false, progress: null @@ -189,15 +189,21 @@ export default class ExportAssetPanel extends React.Component { render(){ const {epsg, customEpsg, exporting, format, resample, progress } = this.state; const { exportFormats } = this.props; - const utmEPSG = this.props.task.epsg; + const projEPSG = this.props.task.epsg; + let projSrsName = this.props.task.srs?.name; + if (!projSrsName && projEPSG) projSrsName = `EPSG:${projEPSG}`; + else if (projSrsName && projEPSG) projSrsName = `${projSrsName} (EPSG:${projEPSG})`; + + let resampleUnits = _("Meters").toLowerCase(); + if (this.props.task.srs?.units != "m") resampleUnits = _("Feet").toLowerCase(); const disabled = (epsg === "custom" && !customEpsg) || exporting; - let projection = utmEPSG ? (
- + let projection = projEPSG ? (
+
- + {projEPSG ? : ""} @@ -225,7 +231,7 @@ export default class ExportAssetPanel extends React.Component {
, this.isPointCloud() ?
- +
diff --git a/app/static/app/js/components/LayersControlLayer.jsx b/app/static/app/js/components/LayersControlLayer.jsx index 29e06764e..8f45713e0 100644 --- a/app/static/app/js/components/LayersControlLayer.jsx +++ b/app/static/app/js/components/LayersControlLayer.jsx @@ -372,7 +372,7 @@ export default class LayersControlLayer extends React.Component { let cmapValues = null; if (colorMap){ - cmapValues = (color_maps.find(c => c.key === colorMap) || {}).color_map; + cmapValues = (color_maps.find(c => c.key === colorMap) || {}).decoded_color_map; } let hmin = null; diff --git a/app/static/app/js/components/Map.jsx b/app/static/app/js/components/Map.jsx index a8e9d1c8b..39e93ef18 100644 --- a/app/static/app/js/components/Map.jsx +++ b/app/static/app/js/components/Map.jsx @@ -337,6 +337,22 @@ class Map extends React.Component { if (meta.task.crop) params.crop = 1; tileUrl = Utils.buildUrlWithQuery(tileUrl, params); } + + // Decode colormaps + if (Array.isArray(mres.color_maps)){ + mres.color_maps.forEach(cm => { + if (Array.isArray(cm.color_map)){ + cm.decoded_color_map = cm.color_map.map(v => { + return [ + (v >> 24) & 0xFF, // R + (v >> 16) & 0xFF, // G + (v >> 8) & 0xFF, // B + v & 0xFF // A + ]; + }); + } + }); + } const layer = Leaflet.tileLayer(tileUrl, { bounds, diff --git a/app/static/app/js/components/NewTaskPanel.jsx b/app/static/app/js/components/NewTaskPanel.jsx index 8ba6b4c02..3a44b9694 100644 --- a/app/static/app/js/components/NewTaskPanel.jsx +++ b/app/static/app/js/components/NewTaskPanel.jsx @@ -8,6 +8,7 @@ import MapPreview from './MapPreview'; import update from 'immutability-helper'; import PluginsAPI from '../classes/plugins/API'; import statusCodes from '../classes/StatusCodes'; +import Gcp from '../classes/Gcp'; import { _, interpolate } from '../classes/gettext'; class NewTaskPanel extends React.Component { @@ -45,6 +46,7 @@ class NewTaskPanel extends React.Component { loading: false, showMapPreview: false, dismissImageCountWarning: false, + showMalformedGcpErrors: false, }; this.save = this.save.bind(this); @@ -172,6 +174,32 @@ class NewTaskPanel extends React.Component { return this.mapPreview.getCropPolygon(); }; + getGcpFile = () => { + if (!this.props.getFiles) return null; + + const files = this.props.getFiles(); + for (let i = 0; i < files.length; i++){ + const f = files[i]; + if (f.type.indexOf("text") === 0 && ["geo.txt", "image_groups.txt"].indexOf(f.name.toLowerCase()) === -1){ + if (!f._gcp){ + const reader = new FileReader(); + reader.onload = (e) => { + if (e.target.result){ + const gcp = new Gcp(e.target.result); + if (!gcp.valid()){ + this.setState({showMalformedGcpErrors: true}); + } + f._gcp = gcp; + } + }; + reader.readAsText(f); + } + + return f; + } + } + } + handlePolygonChange = () => { if (this.taskForm) this.taskForm.forceUpdate(); } @@ -201,11 +229,17 @@ class NewTaskPanel extends React.Component { let filesCountOk = true; if (this.taskForm && !this.taskForm.checkFilesCount(this.props.filesCount)) filesCountOk = false; + let fileCountInfo = interpolate(_("%(count)s files selected."), { count: this.props.filesCount }); + let gcp = this.getGcpFile(); + if (gcp){ + fileCountInfo = interpolate(_("%(count)s files and GCP file (%(name)s) selected."), { count: this.props.filesCount - 1, name: gcp.name }); + } + return (
-

{interpolate(_("%(count)s files selected. Please check these additional options:"), { count: this.props.filesCount})}

+

{fileCountInfo} {_("Please check these additional options:")}

{this.props.filesCount === 999 && !this.state.dismissImageCountWarning ?
@@ -213,6 +247,16 @@ class NewTaskPanel extends React.Component {
: ""} + {gcp && gcp._gcp && this.state.showMalformedGcpErrors ? +
+ +
${_("GCP File")}`, + errors: "
    " + gcp._gcp.errors.map(err => `
  • ${err}
  • `) + "
" + })}}>
+
: ""} + + {!filesCountOk ?
{interpolate(_("Number of files selected exceeds the maximum of %(count)s allowed on this processing node."), { count: this.taskForm.selectedNodeMaxImages() })} @@ -236,6 +280,7 @@ class NewTaskPanel extends React.Component { inReview={this.state.inReview} suggestedTaskName={this.handleSuggestedTaskName} getCropPolygon={this.getCropPolygon} + getGcpFile={this.getGcpFile} ref={(domNode) => { if (domNode) this.taskForm = domNode; }} /> diff --git a/app/static/app/js/components/TaskListItem.jsx b/app/static/app/js/components/TaskListItem.jsx index 8787ecffb..496181639 100644 --- a/app/static/app/js/components/TaskListItem.jsx +++ b/app/static/app/js/components/TaskListItem.jsx @@ -572,7 +572,7 @@ class TaskListItem extends React.Component { }); } - if (editable || (!task.processing_node)){ + if (editable || (!task.processing_node && !imported)){ addActionButton(_("Edit"), "btn-primary pull-right edit-button", "glyphicon glyphicon-pencil", () => { this.startEditing(); }, { diff --git a/app/static/app/js/css/ExportAssetPanel.scss b/app/static/app/js/css/ExportAssetPanel.scss index deb878893..26b68fdb6 100644 --- a/app/static/app/js/css/ExportAssetPanel.scss +++ b/app/static/app/js/css/ExportAssetPanel.scss @@ -5,6 +5,9 @@ margin-bottom: 8px; } } + .crs{ + max-width: 100%; + } .dropdown-toggle{ padding: 5px !important; } diff --git a/app/static/app/js/translations/odm_autogenerated.js b/app/static/app/js/translations/odm_autogenerated.js index 338bcc2aa..1400ece43 100644 --- a/app/static/app/js/translations/odm_autogenerated.js +++ b/app/static/app/js/translations/odm_autogenerated.js @@ -1,95 +1,95 @@ // Auto-generated with extract_odm_strings.py, do not edit! -_("Do not attempt to merge partial reconstructions. This can happen when images do not have sufficient overlap or are isolated. Default: %(default)s"); -_("Set this parameter if you want to generate a Google Earth (KMZ) rendering of the orthophoto. Default: %(default)s"); -_("URL to a ClusterODM instance for distributing a split-merge workflow on multiple nodes in parallel. Default: %(default)s"); -_("Average number of images per submodel. When splitting a large dataset into smaller submodels, images are grouped into clusters. This value regulates the number of images that each cluster should have on average. Default: %(default)s"); -_("When processing multispectral datasets, you can specify the name of the primary band that will be used for reconstruction. It's recommended to choose a band which has sharp details and is in focus. Default: %(default)s"); -_("Build orthophoto overviews for faster display in programs such as QGIS. Default: %(default)s"); -_("During the orthophoto merging, skip expensive blending operation: %(default)s"); -_("Automatically compute image masks using AI to remove the background. Experimental. Default: %(default)s"); -_("Use this tag to build a DSM (Digital Surface Model, ground + objects) using a progressive morphological filter. Check the --dem* parameters for finer tuning. Default: %(default)s"); -_("Number of steps used to fill areas with gaps. Set to 0 to disable gap filling. Starting with a radius equal to the output resolution, N different DEMs are generated with progressively bigger radius using the inverse distance weighted (IDW) algorithm and merged together. Remaining gaps are then merged using nearest neighbor interpolation. Default: %(default)s"); -_("Simple Morphological Filter slope parameter (rise over run). Default: %(default)s"); -_("Use the camera parameters computed from another dataset instead of calculating them. Can be specified either as path to a cameras.json file or as a JSON string representing the contents of a cameras.json file. Default: %(default)s"); +_("The maximum output resolution of extracted video frames in pixels. Default: %(default)s"); +_("Skip alignment of submodels in split-merge. Useful if GPS is good enough on very large datasets. Default: %(default)s"); +_("Use a full 3D mesh to compute the orthophoto instead of a 2.5D mesh. This option is a bit faster and provides similar results in planar areas. Default: %(default)s"); +_("Geometric estimates improve the accuracy of the point cloud by computing geometrically consistent depthmaps but may not be usable in larger datasets. This flag disables geometric estimates. Default: %(default)s"); +_("Override the rolling shutter readout time for your camera sensor (in milliseconds), instead of using the rolling shutter readout database. Note that not all cameras are present in the database. Set to 0 to use the database value. Default: %(default)s"); +_("Run local bundle adjustment for every image added to the reconstruction and a global adjustment every 100 images. Speeds up reconstruction for very large datasets. Default: %(default)s"); +_("Skip generation of the orthophoto. This can save time if you only need 3D results or DEMs. Default: %(default)s"); _("Set the compression to use for orthophotos. Can be one of: %(choices)s. Default: %(default)s"); -_("The maximum vertex count of the output mesh. Default: %(default)s"); +_("Turn on rolling shutter correction. If the camera has a rolling shutter and the images were taken in motion, you can turn on this option to improve the accuracy of the results. See also --rolling-shutter-readout. Default: %(default)s"); +_("Maximum number of frames to extract from video files for processing. Set to 0 for no limit. Default: %(default)s"); +_("Skip generation of PDF report. This can save time if you don't need a report. Default: %(default)s"); +_("Computes an euclidean raster map for each DEM. The map reports the distance from each cell to the nearest NODATA value (before any hole filling takes place). This can be useful to isolate the areas that have been filled. Default: %(default)s"); +_("Use this tag to build a DTM (Digital Terrain Model, ground only) using a simple morphological filter. Check the --dem* and --smrf* parameters for finer tuning. Default: %(default)s"); +_("Automatically crop image outputs by creating a smooth buffer around the dataset boundaries, shrunk by N meters. Use 0 to disable cropping. Default: %(default)s"); +_("Turn off camera parameter optimization during bundle adjustment. This can be sometimes useful for improving results that exhibit doming/bowling or when images are taken with a rolling shutter camera. Default: %(default)s"); +_("Name of dataset (i.e subfolder name within project folder). Default: %(default)s"); +_("Classify the point cloud outputs. You can control the behavior of this option by tweaking the --dem-* parameters. Default: %(default)s"); +_("Do not use GPU acceleration, even if it's available. Default: %(default)s"); +_("Choose the algorithm for extracting keypoints and computing descriptors. Can be one of: %(choices)s. Default: %(default)s"); +_("Simple Morphological Filter elevation threshold parameter (meters). Default: %(default)s"); +_("Copy output results to this folder after processing."); _("Export the georeferenced point cloud in CSV format. Default: %(default)s"); -_("Use this tag if you have a GCP File but want to use the EXIF information for georeferencing instead. Default: %(default)s"); +_("Specify the distance between camera shot locations and the outer edge of the boundary when computing the boundary with --auto-boundary. Set to 0 to automatically choose a value. Default: %(default)s"); +_("Permanently delete all previous results and rerun the processing pipeline."); +_("Set a camera projection type. Manually setting a value can help improve geometric undistortion. By default the application tries to determine a lens type from the images metadata. Can be one of: %(choices)s. Default: %(default)s"); +_("Automatically compute image masks using AI to remove the sky. Experimental. Default: %(default)s"); +_("Number of steps used to fill areas with gaps. Set to 0 to disable gap filling. Starting with a radius equal to the output resolution, N different DEMs are generated with progressively bigger radius using the inverse distance weighted (IDW) algorithm and merged together. Remaining gaps are then merged using nearest neighbor interpolation. Default: %(default)s"); _("Rerun processing from this stage. Can be one of: %(choices)s. Default: %(default)s"); -_("Choose the structure from motion algorithm. For aerial datasets, if camera GPS positions and angles are available, triangulation can generate better results. For planar scenes captured at fixed altitude with nadir-only images, planar can be much faster. Can be one of: %(choices)s. Default: %(default)s"); +_("Orthophoto resolution in cm / pixel. Note that this value is capped by a ground sampling distance (GSD) estimate.Default: %(default)s"); +_("Radius of the overlap between submodels in meters. After grouping images into clusters, images that are closer than this radius to a cluster are added to the cluster. This is done to ensure that neighboring submodels overlap. All imagesneed GPS information. Default: %(default)s"); +_("DSM/DTM resolution in cm / pixel. Note that this value is capped by a ground sampling distance (GSD) estimate. Default: %(default)s"); +_("Use images' GPS exif data for reconstruction, even if there are GCPs present.This flag is useful if you have high precision GPS measurements. If there are no GCPs, this flag does nothing. Default: %(default)s"); +_("Generate OBJs that have a single material and a single texture file instead of multiple ones. Default: %(default)s"); +_("Simple Morphological Filter elevation scalar parameter. Default: %(default)s"); +_("Set a value in meters for the GPS Dilution of Precision (DOP) information for all images. If your images are tagged with high precision GPS information (RTK), this value will be automatically set accordingly. You can use this option to manually set it in case the reconstruction fails. Lowering this option can sometimes help control bowling-effects over large areas. Default: %(default)s"); _("GeoJSON polygon limiting the area of the reconstruction. Can be specified either as path to a GeoJSON file or as a JSON string representing the contents of a GeoJSON file. Default: %(default)s"); -_("Export the georeferenced point cloud in Entwine Point Tile (EPT) format. Default: %(default)s"); +_("Create Cloud-Optimized GeoTIFFs instead of normal GeoTIFFs. Default: %(default)s"); +_("Set this parameter if you want to generate a Google Earth (KMZ) rendering of the orthophoto. Default: %(default)s"); _("Skip normalization of colors across all images. Useful when processing radiometric data. Default: %(default)s"); -_("Export the georeferenced point cloud in LAS format. Default: %(default)s"); -_("Skip generation of the orthophoto. This can save time if you only need 3D results or DEMs. Default: %(default)s"); -_("Set a GPS offset in meters for the vertical axis (Z) by adding it to the altitude value of the GPS EXIF data. This does not change the value of any GCPs. This can be useful for example when adjusting from ellipsoidal to orthometric height. Default: %(default)s"); -_("Set a value in meters for the GPS Dilution of Precision (DOP) information for all images. If your images are tagged with high precision GPS information (RTK), this value will be automatically set accordingly. You can use this option to manually set it in case the reconstruction fails. Lowering this option can sometimes help control bowling-effects over large areas. Default: %(default)s"); -_("Delete heavy intermediate files to optimize disk space usage. This affects the ability to restart the pipeline from an intermediate stage, but allows datasets to be processed on machines that don't have sufficient disk space available. Default: %(default)s"); -_("Orthophoto resolution in cm / pixel. Note that this value is capped by a ground sampling distance (GSD) estimate.Default: %(default)s"); -_("Path to a GeoTIFF DEM or a LAS/LAZ point cloud that the reconstruction outputs should be automatically aligned to. Experimental. Default: %(default)s"); -_("Name of dataset (i.e subfolder name within project folder). Default: %(default)s"); -_("Run local bundle adjustment for every image added to the reconstruction and a global adjustment every 100 images. Speeds up reconstruction for very large datasets. Default: %(default)s"); _("Keep faces in the mesh that are not seen in any camera. Default: %(default)s"); -_("Skips dense reconstruction and 3D model generation. It generates an orthophoto directly from the sparse reconstruction. If you just need an orthophoto and do not need a full 3D model, turn on this option. Default: %(default)s"); -_("Automatically crop image outputs by creating a smooth buffer around the dataset boundaries, shrunk by N meters. Use 0 to disable cropping. Default: %(default)s"); -_("Use a full 3D mesh to compute the orthophoto instead of a 2.5D mesh. This option is a bit faster and provides similar results in planar areas. Default: %(default)s"); -_("Create Cloud-Optimized GeoTIFFs instead of normal GeoTIFFs. Default: %(default)s"); -_("Computes an euclidean raster map for each DEM. The map reports the distance from each cell to the nearest NODATA value (before any hole filling takes place). This can be useful to isolate the areas that have been filled. Default: %(default)s"); -_("Turn on rolling shutter correction. If the camera has a rolling shutter and the images were taken in motion, you can turn on this option to improve the accuracy of the results. See also --rolling-shutter-readout. Default: %(default)s"); +_("Choose the structure from motion algorithm. For aerial datasets, if camera GPS positions and angles are available, triangulation can generate better results. For planar scenes captured at fixed altitude with nadir-only images, planar can be much faster. Can be one of: %(choices)s. Default: %(default)s"); +_("Set feature extraction quality. Higher quality generates better features, but requires more memory and takes longer. Can be one of: %(choices)s. Default: %(default)s"); _("Generates a polygon around the cropping area that cuts the orthophoto around the edges of features. This polygon can be useful for stitching seamless mosaics with multiple overlapping orthophotos. Default: %(default)s"); -_("Set the radiometric calibration to perform on images. When processing multispectral and thermal images you should set this option to obtain reflectance/temperature values (otherwise you will get digital number values). [camera] applies black level, vignetting, row gradient gain/exposure compensation (if appropriate EXIF tags are found) and computes absolute temperature values. [camera+sun] is experimental, applies all the corrections of [camera], plus compensates for spectral radiance registered via a downwelling light sensor (DLS) taking in consideration the angle of the sun. Can be one of: %(choices)s. Default: %(default)s"); +_("Do not attempt to merge partial reconstructions. This can happen when images do not have sufficient overlap or are isolated. Default: %(default)s"); +_("Path to the file containing the ground control points used for georeferencing. The file needs to use the following format: EPSG: or <+proj definition>geo_x geo_y geo_z im_x im_y image_name [gcp_name] [extra1] [extra2]Default: %(default)s"); _("Set point cloud quality. Higher quality generates better, denser point clouds, but requires more memory and takes longer. Each step up in quality increases processing time roughly by a factor of 4x.Can be one of: %(choices)s. Default: %(default)s"); _("Generate static tiles for orthophotos and DEMs that are suitable for viewers like Leaflet or OpenLayers. Default: %(default)s"); -_("Geometric estimates improve the accuracy of the point cloud by computing geometrically consistent depthmaps but may not be usable in larger datasets. This flag disables geometric estimates. Default: %(default)s"); -_("Simple Morphological Filter elevation scalar parameter. Default: %(default)s"); -_("Skip generation of PDF report. This can save time if you don't need a report. Default: %(default)s"); -_("Classify the point cloud outputs. You can control the behavior of this option by tweaking the --dem-* parameters. Default: %(default)s"); -_("show this help message and exit"); -_("Choose what to merge in the merge step in a split dataset. By default all available outputs are merged. Options: %(choices)s. Default: %(default)s"); -_("Automatically compute image masks using AI to remove the sky. Experimental. Default: %(default)s"); -_("Displays version number and exits. "); +_("Decimate the points before generating the DEM. 1 is no decimation (full quality). 100 decimates ~99%% of the points. Useful for speeding up generation of DEM results in very large datasets. Default: %(default)s"); +_("Use the camera parameters computed from another dataset instead of calculating them. Can be specified either as path to a cameras.json file or as a JSON string representing the contents of a cameras.json file. Default: %(default)s"); +_("When processing multispectral datasets, ODM will automatically align the images for each band. If the images have been postprocessed and are already aligned, use this option. Default: %(default)s"); +_("The maximum number of processes to use in various processes. Peak memory requirement is ~1GB per thread and 2 megapixel image resolution. Default: %(default)s"); +_("During the orthophoto merging, skip expensive blending operation: %(default)s"); +_("When processing multispectral datasets, you can specify the name of the primary band that will be used for reconstruction. It's recommended to choose a band which has sharp details and is in focus. Default: %(default)s"); +_("Filters the point cloud by keeping only a single point around a radius N (in meters). This can be useful to limit the output resolution of the point cloud and remove duplicate points. Set to 0 to disable sampling. Default: %(default)s"); +_("Simple Morphological Filter slope parameter (rise over run). Default: %(default)s"); _("Perform image matching with the nearest images based on GPS exif data. Set to 0 to match by triangulation. Default: %(default)s"); -_("Rerun this stage only and stop. Can be one of: %(choices)s. Default: %(default)s"); -_("Simple Morphological Filter window radius parameter (meters). Default: %(default)s"); _("End processing at this stage. Can be one of: %(choices)s. Default: %(default)s"); -_("Perform image matching with the nearest N images based on image filename order. Can speed up processing of sequential images, such as those extracted from video. It is applied only on non-georeferenced datasets. Set to 0 to disable. Default: %(default)s"); -_("Choose the algorithm for extracting keypoints and computing descriptors. Can be one of: %(choices)s. Default: %(default)s"); -_("Set this parameter if you want to generate a PNG rendering of the orthophoto. Default: %(default)s"); -_("Maximum number of frames to extract from video files for processing. Set to 0 for no limit. Default: %(default)s"); -_("Specify the distance between camera shot locations and the outer edge of the boundary when computing the boundary with --auto-boundary. Set to 0 to automatically choose a value. Default: %(default)s"); -_("Path to the image groups file that controls how images should be split into groups. The file needs to use the following format: image_name group_nameDefault: %(default)s"); -_("Path to the project folder. Your project folder should contain subfolders for each dataset. Each dataset should have an \"images\" folder."); -_("Simple Morphological Filter elevation threshold parameter (meters). Default: %(default)s"); -_("Do not use GPU acceleration, even if it's available. Default: %(default)s"); -_("Override the rolling shutter readout time for your camera sensor (in milliseconds), instead of using the rolling shutter readout database. Note that not all cameras are present in the database. Set to 0 to use the database value. Default: %(default)s"); -_("Radius of the overlap between submodels in meters. After grouping images into clusters, images that are closer than this radius to a cluster are added to the cluster. This is done to ensure that neighboring submodels overlap. All imagesneed GPS information. Default: %(default)s"); -_("Skip alignment of submodels in split-merge. Useful if GPS is good enough on very large datasets. Default: %(default)s"); -_("The maximum number of processes to use in various processes. Peak memory requirement is ~1GB per thread and 2 megapixel image resolution. Default: %(default)s"); -_("Set this parameter if you want a striped GeoTIFF. Default: %(default)s"); -_("Set feature extraction quality. Higher quality generates better features, but requires more memory and takes longer. Can be one of: %(choices)s. Default: %(default)s"); -_("Decimate the points before generating the DEM. 1 is no decimation (full quality). 100 decimates ~99%% of the points. Useful for speeding up generation of DEM results in very large datasets. Default: %(default)s"); -_("Use this tag to build a DTM (Digital Terrain Model, ground only) using a simple morphological filter. Check the --dem* and --smrf* parameters for finer tuning. Default: %(default)s"); _("Generate OGC 3D Tiles outputs. Default: %(default)s"); -_("Generate OBJs that have a single material and a single texture file instead of multiple ones. Default: %(default)s"); -_("Generate single file Binary glTF (GLB) textured models. Default: %(default)s"); -_("Set a camera projection type. Manually setting a value can help improve geometric undistortion. By default the application tries to determine a lens type from the images metadata. Can be one of: %(choices)s. Default: %(default)s"); -_("Save the georeferenced point cloud in Cloud Optimized Point Cloud (COPC) format. Default: %(default)s"); -_("Ignore Ground Sampling Distance (GSD).A memory and processor hungry change relative to the default behavior if set to true. Ordinarily, GSD estimates are used to cap the maximum resolution of image outputs and resizes images when necessary, resulting in faster processing and lower memory usage. Since GSD is an estimate, sometimes ignoring it can result in slightly better image output quality. Never set --ignore-gsd to true unless you are positive you need it, and even then: do not use it. Default: %(default)s"); -_("Matcher algorithm, Fast Library for Approximate Nearest Neighbors or Bag of Words. FLANN is slower, but more stable. BOW is faster, but can sometimes miss valid matches. BRUTEFORCE is very slow but robust.Can be one of: %(choices)s. Default: %(default)s"); -_("Skip generation of a full 3D model. This can save time if you only need 2D results such as orthophotos and DEMs. Default: %(default)s"); -_("Path to the file containing the ground control points used for georeferencing. The file needs to use the following format: EPSG: or <+proj definition>geo_x geo_y geo_z im_x im_y image_name [gcp_name] [extra1] [extra2]Default: %(default)s"); +_("Path to the image groups file that controls how images should be split into groups. The file needs to use the following format: image_name group_nameDefault: %(default)s"); +_("Minimum number of features to extract per image. More features can be useful for finding more matches between images, potentially allowing the reconstruction of areas with little overlap or insufficient features. More features also slow down processing. Default: %(default)s"); +_("Export the georeferenced point cloud in Entwine Point Tile (EPT) format. Default: %(default)s"); +_("Perform image matching with the nearest N images based on image filename order. Can speed up processing of sequential images, such as those extracted from video. It is applied only on non-georeferenced datasets. Set to 0 to disable. Default: %(default)s"); _("Octree depth used in the mesh reconstruction, increase to get more vertices, recommended values are 8-12. Default: %(default)s"); -_("DSM/DTM resolution in cm / pixel. Note that this value is capped by a ground sampling distance (GSD) estimate. Default: %(default)s"); -_("Perform ground rectification on the point cloud. This means that wrongly classified ground points will be re-classified and gaps will be filled. Useful for generating DTMs. Default: %(default)s"); -_("Filters the point cloud by removing points that deviate more than N standard deviations from the local mean. Set to 0 to disable filtering. Default: %(default)s"); -_("Turn off camera parameter optimization during bundle adjustment. This can be sometimes useful for improving results that exhibit doming/bowling or when images are taken with a rolling shutter camera. Default: %(default)s"); -_("Filters the point cloud by keeping only a single point around a radius N (in meters). This can be useful to limit the output resolution of the point cloud and remove duplicate points. Set to 0 to disable sampling. Default: %(default)s"); +_("Simple Morphological Filter window radius parameter (meters). Default: %(default)s"); +_("Generate single file Binary glTF (GLB) textured models. Default: %(default)s"); +_("Set a GPS offset in meters for the vertical axis (Z) by adding it to the altitude value of the GPS EXIF data. This does not change the value of any GCPs. This can be useful for example when adjusting from ellipsoidal to orthometric height. Default: %(default)s"); _("Path to the image geolocation file containing the camera center coordinates used for georeferencing. If you don't have values for yaw/pitch/roll you can set them to 0. The file needs to use the following format: EPSG: or <+proj definition>image_name geo_x geo_y geo_z [yaw (degrees)] [pitch (degrees)] [roll (degrees)] [horz accuracy (meters)] [vert accuracy (meters)]Default: %(default)s"); -_("The maximum output resolution of extracted video frames in pixels. Default: %(default)s"); -_("Permanently delete all previous results and rerun the processing pipeline."); -_("When processing multispectral datasets, ODM will automatically align the images for each band. If the images have been postprocessed and are already aligned, use this option. Default: %(default)s"); +_("Skips dense reconstruction and 3D model generation. It generates an orthophoto directly from the sparse reconstruction. If you just need an orthophoto and do not need a full 3D model, turn on this option. Default: %(default)s"); +_("Filters the point cloud by removing points that deviate more than N standard deviations from the local mean. Set to 0 to disable filtering. Default: %(default)s"); +_("Delete heavy intermediate files to optimize disk space usage. This affects the ability to restart the pipeline from an intermediate stage, but allows datasets to be processed on machines that don't have sufficient disk space available. Default: %(default)s"); +_("Choose what to merge in the merge step in a split dataset. By default all available outputs are merged. Options: %(choices)s. Default: %(default)s"); +_("show this help message and exit"); +_("Set this parameter if you want a striped GeoTIFF. Default: %(default)s"); +_("Set the radiometric calibration to perform on images. When processing multispectral and thermal images you should set this option to obtain reflectance/temperature values (otherwise you will get digital number values). [camera] applies black level, vignetting, row gradient gain/exposure compensation (if appropriate EXIF tags are found) and computes absolute temperature values. [camera+sun] is experimental, applies all the corrections of [camera], plus compensates for spectral radiance registered via a downwelling light sensor (DLS) taking in consideration the angle of the sun. Can be one of: %(choices)s. Default: %(default)s"); +_("Automatically compute image masks using AI to remove the background. Experimental. Default: %(default)s"); +_("Skip generation of a full 3D model. This can save time if you only need 2D results such as orthophotos and DEMs. Default: %(default)s"); +_("Displays version number and exits. "); +_("Average number of images per submodel. When splitting a large dataset into smaller submodels, images are grouped into clusters. This value regulates the number of images that each cluster should have on average. Default: %(default)s"); +_("Path to a GeoTIFF DEM or a LAS/LAZ point cloud that the reconstruction outputs should be automatically aligned to. Experimental. Default: %(default)s"); +_("Path to the project folder. Your project folder should contain subfolders for each dataset. Each dataset should have an \"images\" folder."); +_("The maximum vertex count of the output mesh. Default: %(default)s"); +_("URL to a ClusterODM instance for distributing a split-merge workflow on multiple nodes in parallel. Default: %(default)s"); +_("Save the georeferenced point cloud in Cloud Optimized Point Cloud (COPC) format. Default: %(default)s"); _("Automatically set a boundary using camera shot locations to limit the area of the reconstruction. This can help remove far away background artifacts (sky, background landscapes, etc.). See also --boundary. Default: %(default)s"); -_("Use images' GPS exif data for reconstruction, even if there are GCPs present.This flag is useful if you have high precision GPS measurements. If there are no GCPs, this flag does nothing. Default: %(default)s"); -_("Minimum number of features to extract per image. More features can be useful for finding more matches between images, potentially allowing the reconstruction of areas with little overlap or insufficient features. More features also slow down processing. Default: %(default)s"); -_("Copy output results to this folder after processing."); +_("Build orthophoto overviews for faster display in programs such as QGIS. Default: %(default)s"); +_("Use this tag to build a DSM (Digital Surface Model, ground + objects) using a progressive morphological filter. Check the --dem* parameters for finer tuning. Default: %(default)s"); +_("Use this tag if you have a GCP File but want to use the EXIF information for georeferencing instead. Default: %(default)s"); +_("Perform ground rectification on the point cloud. This means that wrongly classified ground points will be re-classified and gaps will be filled. Useful for generating DTMs. Default: %(default)s"); +_("Export the georeferenced point cloud in LAS format. Default: %(default)s"); +_("Rerun this stage only and stop. Can be one of: %(choices)s. Default: %(default)s"); +_("Matcher algorithm, Fast Library for Approximate Nearest Neighbors or Bag of Words. FLANN is slower, but more stable. BOW is faster, but can sometimes miss valid matches. BRUTEFORCE is very slow but robust.Can be one of: %(choices)s. Default: %(default)s"); +_("Ignore Ground Sampling Distance (GSD).A memory and processor hungry change relative to the default behavior if set to true. Ordinarily, GSD estimates are used to cap the maximum resolution of image outputs and resizes images when necessary, resulting in faster processing and lower memory usage. Since GSD is an estimate, sometimes ignoring it can result in slightly better image output quality. Never set --ignore-gsd to true unless you are positive you need it, and even then: do not use it. Default: %(default)s"); +_("Set this parameter if you want to generate a PNG rendering of the orthophoto. Default: %(default)s"); diff --git a/app/templates/app/base.html b/app/templates/app/base.html index e7a895fa7..d18c47961 100644 --- a/app/templates/app/base.html +++ b/app/templates/app/base.html @@ -123,6 +123,9 @@