Skip to content

Commit

Permalink
Adds support for automatically selecting the proper band filter
Browse files Browse the repository at this point in the history
  • Loading branch information
pierotofy committed Oct 3, 2023
1 parent 474e2d8 commit 530720b
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 15 deletions.
52 changes: 48 additions & 4 deletions app/api/formulas.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@
'BGRNReL',
'BGRReNL',

'RGBNRePL',

'L', # FLIR camera has a single LWIR band

# more?
Expand All @@ -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):
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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
20 changes: 18 additions & 2 deletions app/api/tiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -224,16 +230,23 @@ 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:
colormaps = ['rdylgn', 'spectral', 'rdylgn_r', 'spectral_r', 'rplumbo', 'discrete_ndvi',
'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:
Expand All @@ -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)


Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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})
return Response({'celery_task_id': celery_task_id, 'filename': filename})
43 changes: 43 additions & 0 deletions app/migrations/0039_task_orthophoto_bands.py
Original file line number Diff line number Diff line change
@@ -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),
]
7 changes: 6 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
14 changes: 9 additions & 5 deletions app/static/app/js/components/LayersControlLayer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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});
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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> : ""}

Expand Down
14 changes: 14 additions & 0 deletions app/static/app/js/components/Map.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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"){
Expand Down
2 changes: 1 addition & 1 deletion app/tests/test_api_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@ def test_task(self):

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))
Expand Down
2 changes: 1 addition & 1 deletion app/tests/test_formulas.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def test_algo_list(self):
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)
Expand Down

0 comments on commit 530720b

Please sign in to comment.