Adds support for automatically selecting the proper band filter

pull/1412/head
Piero Toffanin 2023-10-03 15:13:56 -04:00
rodzic 474e2d844b
commit 530720b699
9 zmienionych plików z 141 dodań i 15 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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})
return Response({'celery_task_id': celery_task_id, 'filename': filename})

Wyświetl plik

@ -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),
]

Wyświetl plik

@ -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()

Wyświetl plik

@ -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 ?
<div className="row form-group form-inline">
<label className="col-sm-3 control-label">{_("Filter:")}</label>
<label className="col-sm-3 control-label">{_("Bands:")}</label>
<div className="col-sm-9 ">
{histogramLoading ?
<i className="fa fa-circle-notch fa-spin fa-fw" /> :
<select className="form-control" value={bands} onChange={this.handleSelectBands}>
[<select className="form-control" value={bands} onChange={this.handleSelectBands} title={auto_bands.filter !== "" && bands == "auto" ? auto_bands.filter : ""}>
<option key="auto" value="auto">{_("Automatic")}</option>
{algo.filters.map(f => <option key={f} value={f}>{f}</option>)}
</select>}
</select>,
bands == "auto" && !auto_bands.match ?
<i style={{marginLeft: '4px'}} title={interpolate(_("Not every band for %(name)s could be automatically identified."), {name: algo.id}) + "\n" + _("Your sensor might not have the proper bands for using this algorithm.")} className="fa fa-exclamation-circle info-button"></i>
: ""]}
</div>
</div> : ""}

Wyświetl plik

@ -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"){

Wyświetl plik

@ -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))

Wyświetl plik

@ -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)

2
locale

@ -1 +1 @@
Subproject commit d253dd5770c42a0705d0f861db3314b08f230a68
Subproject commit 0a68f6ed5172a8838571a9872bcc3cb4f310794c