diff --git a/app/api/formulas.py b/app/api/formulas.py index 457303200..21d4e2971 100644 --- a/app/api/formulas.py +++ b/app/api/formulas.py @@ -156,6 +156,8 @@ 'BGRNReL', 'BGRReNL', + 'RGBNRePL', + 'L', # FLIR camera has a single LWIR band # more? @@ -171,7 +173,7 @@ def lookup_formula(algo, band_order = 'RGB'): if algo not in algos: raise ValueError("Cannot find algorithm " + algo) - + input_bands = tuple(b for b in re.split(r"([A-Z][a-z]*)", band_order) if b != "") def repl(matches): @@ -193,7 +195,7 @@ def get_algorithm_list(max_bands=3): if k.startswith("_"): continue - cam_filters = get_camera_filters_for(algos[k], max_bands) + cam_filters = get_camera_filters_for(algos[k]['expr'], max_bands) if len(cam_filters) == 0: continue @@ -206,9 +208,9 @@ def get_algorithm_list(max_bands=3): return res -def get_camera_filters_for(algo, max_bands=3): +@lru_cache(maxsize=100) +def get_camera_filters_for(expr, max_bands=3): result = [] - expr = algo['expr'] pattern = re.compile("([A-Z]+?[a-z]*)") bands = list(set(re.findall(pattern, expr))) for f in camera_filters: @@ -226,3 +228,45 @@ def get_camera_filters_for(algo, max_bands=3): return result +@lru_cache(maxsize=1) +def get_bands_lookup(): + bands_aliases = { + 'R': ['red', 'r'], + 'G': ['green', 'g'], + 'B': ['blue', 'b'], + 'N': ['nir', 'n'], + 'Re': ['rededge', 're'], + 'P': ['panchro', 'p'], + 'L': ['lwir', 'l'] + } + bands_lookup = {} + for band in bands_aliases: + for a in bands_aliases[band]: + bands_lookup[a] = band + return bands_lookup + +def get_auto_bands(orthophoto_bands, formula): + algo = algos.get(formula) + if not algo: + raise ValueError("Cannot find formula: " + formula) + + max_bands = len(orthophoto_bands) - 1 # minus alpha + filters = get_camera_filters_for(algo['expr'], max_bands) + if not filters: + raise valueError(f"Cannot find filters for {algo} with max bands {max_bands}") + + bands_lookup = get_bands_lookup() + band_order = "" + + for band in orthophoto_bands: + if band['name'] == 'alpha' or (not band['description']): + continue + f_band = bands_lookup.get(band['description'].lower()) + + if f_band is not None: + band_order += f_band + + if band_order in filters: + return band_order, True + else: + return filters[0], False # Fallback diff --git a/app/api/tiler.py b/app/api/tiler.py index 91a496e14..855eef569 100644 --- a/app/api/tiler.py +++ b/app/api/tiler.py @@ -23,7 +23,7 @@ from app.raster_utils import extension_for_export_format, ZOOM_EXTRA_LEVELS from .hsvblend import hsv_blend from .hillshade import LightSource -from .formulas import lookup_formula, get_algorithm_list +from .formulas import lookup_formula, get_algorithm_list, get_auto_bands from .tasks import TaskNestedView from rest_framework import exceptions from rest_framework.response import Response @@ -141,6 +141,12 @@ def get(self, request, pk=None, project_pk=None, tile_type=""): if boundaries_feature == '': boundaries_feature = None if boundaries_feature is not None: boundaries_feature = json.loads(boundaries_feature) + + is_auto_bands_match = False + is_auto_bands = False + if bands == 'auto' and formula: + is_auto_bands = True + bands, is_auto_bands_match = get_auto_bands(task.orthophoto_bands, formula) try: expr, hrange = lookup_formula(formula, bands) if defined_range is not None: @@ -224,6 +230,8 @@ def get(self, request, pk=None, project_pk=None, tile_type=""): colormaps = [] algorithms = [] + auto_bands = {'filter': '', 'match': None} + if tile_type in ['dsm', 'dtm']: colormaps = ['viridis', 'jet', 'terrain', 'gist_earth', 'pastel1'] elif formula and bands: @@ -231,9 +239,14 @@ def get(self, request, pk=None, project_pk=None, tile_type=""): 'better_discrete_ndvi', 'viridis', 'plasma', 'inferno', 'magma', 'cividis', 'jet', 'jet_r'] algorithms = *get_algorithm_list(band_count), + if is_auto_bands: + auto_bands['filter'] = bands + auto_bands['match'] = is_auto_bands_match info['color_maps'] = [] info['algorithms'] = algorithms + info['auto_bands'] = auto_bands + if colormaps: for cmap in colormaps: try: @@ -254,6 +267,7 @@ def get(self, request, pk=None, project_pk=None, tile_type=""): info['maxzoom'] += ZOOM_EXTRA_LEVELS info['minzoom'] -= ZOOM_EXTRA_LEVELS info['bounds'] = {'value': src.bounds, 'crs': src.dataset.crs} + return Response(info) @@ -296,6 +310,8 @@ def get(self, request, pk=None, project_pk=None, tile_type="", z="", x="", y="", if color_map == '': color_map = None if hillshade == '' or hillshade == '0': hillshade = None if tilesize == '' or tilesize is None: tilesize = 256 + if bands == 'auto' and formula: + bands, _ = get_auto_bands(task.orthophoto_bands, formula) try: tilesize = int(tilesize) @@ -611,4 +627,4 @@ def post(self, request, pk=None, project_pk=None, asset_type=None): else: celery_task_id = export_pointcloud.delay(url, epsg=epsg, format=export_format).task_id - return Response({'celery_task_id': celery_task_id, 'filename': filename}) \ No newline at end of file + return Response({'celery_task_id': celery_task_id, 'filename': filename}) diff --git a/app/migrations/0039_task_orthophoto_bands.py b/app/migrations/0039_task_orthophoto_bands.py new file mode 100644 index 000000000..c801ab853 --- /dev/null +++ b/app/migrations/0039_task_orthophoto_bands.py @@ -0,0 +1,43 @@ +# Generated by Django 2.2.27 on 2023-10-02 10:21 + +import rasterio +import os +import django.contrib.postgres.fields.jsonb +from django.db import migrations +from webodm import settings + +def update_orthophoto_bands_fields(apps, schema_editor): + Task = apps.get_model('app', 'Task') + + for t in Task.objects.all(): + + bands = [] + orthophoto_path = os.path.join(settings.MEDIA_ROOT, "project", str(t.project.id), "task", str(t.id), "assets", "odm_orthophoto", "odm_orthophoto.tif") + + if os.path.isfile(orthophoto_path): + try: + with rasterio.open(orthophoto_path) as f: + names = [c.name for c in f.colorinterp] + for i, n in enumerate(names): + bands.append({ + 'name': n, + 'description': f.descriptions[i] + }) + except Exception as e: + print(e) + + print("Updating {} (with orthophoto bands: {})".format(t, str(bands))) + + t.orthophoto_bands = bands + t.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0038_remove_task_console_output'), + ] + + operations = [ + migrations.RunPython(update_orthophoto_bands_fields), + ] diff --git a/app/models/task.py b/app/models/task.py index 1f0c4c0c6..89bcb85c0 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -1024,7 +1024,12 @@ def update_orthophoto_bands_field(self, commit=False): if os.path.isfile(orthophoto_path): with rasterio.open(orthophoto_path) as f: - bands = [c.name for c in f.colorinterp] + names = [c.name for c in f.colorinterp] + for i, n in enumerate(names): + bands.append({ + 'name': n, + 'description': f.descriptions[i] + }) self.orthophoto_bands = bands if commit: self.save() diff --git a/app/static/app/js/components/LayersControlLayer.jsx b/app/static/app/js/components/LayersControlLayer.jsx index 36509ad53..2aaaf531a 100644 --- a/app/static/app/js/components/LayersControlLayer.jsx +++ b/app/static/app/js/components/LayersControlLayer.jsx @@ -134,7 +134,7 @@ export default class LayersControlLayer extends React.Component { // Check if bands need to be switched const algo = this.getAlgorithm(e.target.value); - if (algo && algo['filters'].indexOf(bands) === -1) bands = algo['filters'][0]; // Pick first + if (algo && algo['filters'].indexOf(bands) === -1 && bands !== "auto") bands = algo['filters'][0]; // Pick first this.setState({formula: e.target.value, bands}); } @@ -262,7 +262,7 @@ export default class LayersControlLayer extends React.Component { render(){ const { colorMap, bands, hillshade, formula, histogramLoading, exportLoading } = this.state; const { meta, tmeta } = this; - const { color_maps, algorithms } = tmeta; + const { color_maps, algorithms, auto_bands } = tmeta; const algo = this.getAlgorithm(formula); let cmapValues = null; @@ -298,13 +298,17 @@ export default class LayersControlLayer extends React.Component { {bands !== "" && algo ?