Skip to content

Commit

Permalink
Merge pull request #1412 from pierotofy/autobands
Browse files Browse the repository at this point in the history
Adds support for automatically selecting the proper band filter
  • Loading branch information
pierotofy authored Oct 4, 2023
2 parents 474e2d8 + 49c9f2d commit 2d5a403
Show file tree
Hide file tree
Showing 13 changed files with 276 additions and 103 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ A user-friendly, commercial grade software for drone image processing. Generate

Windows and macOS users can purchase an automated [installer](https://www.opendronemap.org/webodm/download#installer), which makes the installation process easier.

To install WebODM manually, these steps should get you up and running:
There's also a cloud-hosted version of WebODM available from [webodm.net](https://webodm.net).

* Install the following applications (if they are not installed already):
To install WebODM manually on your machine:

* Install the following applications:
- [Git](https://git-scm.com/downloads)
- [Docker](https://www.docker.com/)
- [Docker-compose](https://docs.docker.com/compose/install/)
Expand Down
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
23 changes: 21 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, _discard_ = get_auto_bands(task.orthophoto_bands, formula)

try:
tilesize = int(tilesize)
Expand Down Expand Up @@ -544,6 +560,9 @@ def post(self, request, pk=None, project_pk=None, asset_type=None):
raise exceptions.ValidationError(_("Both formula and bands parameters are required"))

if formula and bands:
if bands == 'auto':
bands, _discard_ = get_auto_bands(task.orthophoto_bands, formula)

try:
expr, _discard_ = lookup_formula(formula, bands)
except ValueError as e:
Expand Down Expand Up @@ -611,4 +630,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 key="sel" 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 key="ico" 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;

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
Loading

0 comments on commit 2d5a403

Please sign in to comment.