diff --git a/app/api/formulas.py b/app/api/formulas.py index 45730320..21d4e297 100644 --- a/app/api/formulas.py +++ b/app/api/formulas.py @@ -156,6 +156,8 @@ camera_filters = [ '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 91a496e1..855eef56 100644 --- a/app/api/tiler.py +++ b/app/api/tiler.py @@ -23,7 +23,7 @@ from .custom_colormaps_helper import custom_colormaps 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 @@ class Metadata(TaskNestedView): 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 @@ class Metadata(TaskNestedView): 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 @@ class Metadata(TaskNestedView): '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 @@ class Metadata(TaskNestedView): 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 @@ class Tiles(TaskNestedView): 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 @@ class Export(TaskNestedView): 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 00000000..c801ab85 --- /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 1f0c4c0c..89bcb85c 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -1024,7 +1024,12 @@ class Task(models.Model): 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 36509ad5..2aaaf531 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 ?
- +
{histogramLoading ? : - + {algo.filters.map(f => )} - } + , + bands == "auto" && !auto_bands.match ? + + : ""]}
: ""} diff --git a/app/static/app/js/components/Map.jsx b/app/static/app/js/components/Map.jsx index b93ff561..1e78675c 100644 --- a/app/static/app/js/components/Map.jsx +++ b/app/static/app/js/components/Map.jsx @@ -94,6 +94,16 @@ class Map extends React.Component { return ""; } + hasBands = (bands, orthophoto_bands) => { + if (!orthophoto_bands) return false; + console.log(orthophoto_bands) + for (let i = 0; i < bands.length; i++){ + if (orthophoto_bands.find(b => b.description !== null && b.description.toLowerCase() === bands[i].toLowerCase()) === undefined) return false; + } + + return true; + } + loadImageryLayers(forceAddLayers = false){ // Cancel previous requests if (this.tileJsonRequests) { @@ -131,7 +141,11 @@ class Map extends React.Component { // Single band, probably thermal dataset, in any case we can't render NDVI // because it requires 3 bands metaUrl += "?formula=Celsius&bands=L&color_map=magma"; + }else if (meta.task && meta.task.orthophoto_bands){ + let formula = this.hasBands(["red", "green", "nir"], meta.task.orthophoto_bands) ? "NDVI" : "VARI"; + metaUrl += `?formula=${formula}&bands=auto&color_map=rdylgn`; }else{ + // This should never happen? metaUrl += "?formula=NDVI&bands=RGN&color_map=rdylgn"; } }else if (type == "dsm" || type == "dtm"){ diff --git a/app/tests/test_api_task.py b/app/tests/test_api_task.py index 1abbff4e..bf3af575 100644 --- a/app/tests/test_api_task.py +++ b/app/tests/test_api_task.py @@ -679,7 +679,7 @@ class TestApiTask(BootTransactionTestCase): for k in algos: a = algos[k] - filters = get_camera_filters_for(a) + filters = get_camera_filters_for(a['expr']) for f in filters: params.append(("orthophoto", "formula={}&bands={}&color_map=rdylgn".format(k, f), status.HTTP_200_OK)) diff --git a/app/tests/test_formulas.py b/app/tests/test_formulas.py index 76077d47..ab9117da 100644 --- a/app/tests/test_formulas.py +++ b/app/tests/test_formulas.py @@ -38,7 +38,7 @@ class TestFormulas(TestCase): bands = list(set(re.findall(pattern, f))) self.assertTrue(len(bands) <= 3) - self.assertTrue(get_camera_filters_for(algos['VARI']) == ['RGB']) + self.assertTrue(get_camera_filters_for(algos['VARI']['expr']) == ['RGB']) # Request algorithms with more band filters al = get_algorithm_list(max_bands=5) diff --git a/locale b/locale index d253dd57..0a68f6ed 160000 --- a/locale +++ b/locale @@ -1 +1 @@ -Subproject commit d253dd5770c42a0705d0f861db3314b08f230a68 +Subproject commit 0a68f6ed5172a8838571a9872bcc3cb4f310794c