Revision c87139a7
Added by Jim Regetz over 12 years ago
- ID c87139a78344e4b0ff201ca1e3a4f5c55a5b6a98
terrain/research/oregon/arcgis/v2/multiscalesmooth.py | ||
---|---|---|
1 |
#!/usr/bin/env python |
|
2 |
|
|
3 |
############################################################################ |
|
4 |
# MODULE: r.plane for GRASS 5.7; based on r.plane for GRASS 5 |
|
5 |
# AUTHOR(S): Original algorithm and AML implementation by John |
|
6 |
# Gallant. Translation to GRASS/Python by Jim Regetz. |
|
7 |
# REFERENCES: |
|
8 |
# Gallant, John. 2001. Adaptive smoothing for noisy DEMs. |
|
9 |
# Geomorphometry 2011. |
|
10 |
# http://geomorphometry.org/system/files/Gallant2011geomorphometry.pdf |
|
11 |
# |
|
12 |
############################################################################# |
|
13 |
|
|
14 |
#%module |
|
15 |
#% description: Multiscale raster smoother |
|
16 |
#%end |
|
17 |
#%option |
|
18 |
#% key: input |
|
19 |
#% type: string |
|
20 |
#% gisprompt: old,cell,raster |
|
21 |
#% description: Raster input map |
|
22 |
#% required : yes |
|
23 |
#%end |
|
24 |
#%option |
|
25 |
#% key: output |
|
26 |
#% type: string |
|
27 |
#% gisprompt: new,cell,raster |
|
28 |
#% description: Smoothed output raster map |
|
29 |
#% required : yes |
|
30 |
#%end |
|
31 |
#%option |
|
32 |
#% key: sd |
|
33 |
#% type: string |
|
34 |
#% description: Noise standard deviation; can be a raster or a constant |
|
35 |
#% required : yes |
|
36 |
#%end |
|
37 |
#%option |
|
38 |
#% key: alpha |
|
39 |
#% type: double |
|
40 |
#% answer: 0.05 |
|
41 |
#% description: Alpha used for chi-square test [0-1] |
|
42 |
#% required : no |
|
43 |
#%end |
|
44 |
#%option |
|
45 |
#% key: region |
|
46 |
#% type: string |
|
47 |
#% gisprompt: old,cell,raster |
|
48 |
#% description: Map used to set extent; default is to use current region |
|
49 |
#% required : no |
|
50 |
#%end |
|
51 |
|
|
52 |
# NOTES |
|
53 |
# - make GRASS be quiet with this: |
|
54 |
# gs.os.environ['GRASS_VERBOSE'] = '0' |
|
55 |
|
|
56 |
import atexit |
|
57 |
import sys |
|
58 |
import math |
|
59 |
import grass.script as gs |
|
60 |
|
|
61 |
# create set to store names of temporary maps to be deleted upon exit |
|
62 |
tmp_rast = set() |
|
63 |
|
|
64 |
#@atexit.register |
|
65 |
def cleanup(): |
|
66 |
gs.message("Removing temporary files...", flag='i') |
|
67 |
for rast in tmp_rast: |
|
68 |
gs.run_command("g.remove", rast=rast, quiet=True) |
|
69 |
|
|
70 |
def report(): |
|
71 |
gs.debug("raster report...") |
|
72 |
for rast in tmp_rast: |
|
73 |
info = gs.raster_info(rast) |
|
74 |
gs.debug('%s: %s (%f-%f)' % (rast, info['datatype'], |
|
75 |
info['min'], info['max'])) |
|
76 |
|
|
77 |
def coarsen_region(factor=3): |
|
78 |
gs.run_command('g.region', |
|
79 |
rows=gs.region()['rows']/3, |
|
80 |
cols=gs.region()['cols']/3) |
|
81 |
|
|
82 |
def refine_region(factor=3): |
|
83 |
gs.run_command('g.region', |
|
84 |
rows=gs.region()['rows']*3, |
|
85 |
cols=gs.region()['cols']*3) |
|
86 |
|
|
87 |
def multiscalesmooth(input, smooth, sd, alpha=0.05): |
|
88 |
|
|
89 |
chisqa = (2.807 - 0.6422 * math.log10(alpha) - 3.410 * |
|
90 |
math.pow(alpha, 0.3411)) |
|
91 |
chisqb = (-5.871 - 3.675 * math.log10(alpha) + 4.690 * |
|
92 |
math.pow(alpha, 0.3377)) |
|
93 |
|
|
94 |
# set number of aggregation levels and neighborhood size |
|
95 |
NUM_LEVELS = 4 |
|
96 |
NUM_CELLS = 3 |
|
97 |
|
|
98 |
gs.message("Preparing initial grids", flag='i') |
|
99 |
|
|
100 |
# create copy of initial grid of values using current region |
|
101 |
gs.mapcalc('z0 = ${input}', input=input, quiet=True) |
|
102 |
tmp_rast.add('z0') |
|
103 |
|
|
104 |
# expand region to accommodate integer number of cells at coarsest |
|
105 |
# level, by adding cells to all sides (with one extra on top/right |
|
106 |
# if an odd number of cells needs to be added) |
|
107 |
max_size = NUM_CELLS**NUM_LEVELS |
|
108 |
region = gs.region() |
|
109 |
extra = region['cols'] % max_size |
|
110 |
if (0 < extra): |
|
111 |
addx = (max_size-extra)/2.0 |
|
112 |
else: |
|
113 |
addx = 0.0 |
|
114 |
extra = region['rows'] % max_size |
|
115 |
if (0 < extra): |
|
116 |
addy = (max_size-extra)/2.0 |
|
117 |
else: |
|
118 |
addy = 0.0 |
|
119 |
gs.run_command('g.region', flags='a', |
|
120 |
w = region['w']-math.floor(addx)*region['ewres'], |
|
121 |
e = region['e']+math.ceil(addx)*region['ewres'], |
|
122 |
s = region['s']-math.floor(addy)*region['nsres'], |
|
123 |
n = region['n']+math.ceil(addy)*region['nsres']) |
|
124 |
gs.debug('\n'.join(gs.parse_command('g.region', 'up').keys())) |
|
125 |
|
|
126 |
# create initial grid of variances; sd can be a raster or a constant |
|
127 |
gs.mapcalc('v0 = if(isnull(z0), null(), ${sd}^2)', sd=sd, quiet=True) |
|
128 |
tmp_rast.add('v0') |
|
129 |
# set initial "group variance" to individual msmt variance (noise) |
|
130 |
gs.run_command('g.copy', rast='v0,v.g') |
|
131 |
tmp_rast.add('v.g') |
|
132 |
# weights for aggregation, based on total variance |
|
133 |
gs.mapcalc('w = if(isnull(v0), 0, 1/v0)', quiet=True) |
|
134 |
tmp_rast.add('w') |
|
135 |
# squared weights |
|
136 |
gs.mapcalc('wsq = w^2', quiet=True) |
|
137 |
tmp_rast.add('wsq') |
|
138 |
# effective number of measurements |
|
139 |
gs.mapcalc('n = if(isnull(z0), 0, 1)', quiet=True) |
|
140 |
tmp_rast.add('n') |
|
141 |
|
|
142 |
# aggregate to broader scales |
|
143 |
for j in range(NUM_LEVELS): |
|
144 |
i = j + 1 |
|
145 |
gs.message('Aggregating from %d to %d' % (j, i), flag='i') |
|
146 |
gs.debug(_('Region dimensions: %d x %d' % (gs.region()['rows'], |
|
147 |
gs.region()['cols']))) |
|
148 |
|
|
149 |
# rename previous (finer scale) weights grid |
|
150 |
gs.run_command('g.rename', rast='w,w.finer', overwrite=True, |
|
151 |
quiet=True) |
|
152 |
tmp_rast.add('w.finer') |
|
153 |
|
|
154 |
# calc neighborhood weights, num cells, effective num cells |
|
155 |
coarsen_region() |
|
156 |
gs.run_command('r.resamp.stats', method='sum', input='w.finer', |
|
157 |
output='w', quiet=True) |
|
158 |
gs.run_command('r.resamp.stats', method='sum', input='wsq', |
|
159 |
output='wsq', overwrite=True, quiet=True) |
|
160 |
gs.run_command('r.resamp.stats', method='sum', input='n', |
|
161 |
output='n', overwrite=True, quiet=True) |
|
162 |
|
|
163 |
# calc variance-weighted neighborhood mean |
|
164 |
refine_region() |
|
165 |
gs.mapcalc('tmp = w.finer * z%d / w' % j, quiet=True) |
|
166 |
tmp_rast.add('tmp') |
|
167 |
coarsen_region() |
|
168 |
gs.run_command('r.resamp.stats', method='sum', input='tmp', |
|
169 |
output='z%d' % i, quiet=True) |
|
170 |
tmp_rast.add('z%d' % i) |
|
171 |
|
|
172 |
# calc between-cell variance, taken over neighborhood |
|
173 |
refine_region() |
|
174 |
gs.mapcalc('tmp = w.finer * (z%d - z%d)^2 / w' % (j, i), |
|
175 |
quiet=True) |
|
176 |
tmp_rast.add('tmp') |
|
177 |
coarsen_region() |
|
178 |
gs.run_command('r.resamp.stats', method='sum', input='tmp', |
|
179 |
output='v.bg', overwrite=True, quiet=True) |
|
180 |
tmp_rast.add('v.bg') |
|
181 |
|
|
182 |
# calc wtd avg of within-cell variance, taken over neighborhood |
|
183 |
if (i==1): |
|
184 |
gs.mapcalc('v.wg = 0', quiet=True) |
|
185 |
tmp_rast.add('v.wg') |
|
186 |
else: |
|
187 |
refine_region() |
|
188 |
gs.mapcalc('tmp = w.finer * v.g / w', quiet=True) |
|
189 |
tmp_rast.add('tmp') |
|
190 |
coarsen_region() |
|
191 |
gs.run_command('r.resamp.stats', method='sum', input='tmp', |
|
192 |
output='v.wg', overwrite=True, quiet=True) |
|
193 |
|
|
194 |
# calc total group variance |
|
195 |
# ~= total variance of cell vals in the underlying neighborhood |
|
196 |
gs.mapcalc('v.g = v.bg + v.wg', quiet=True) |
|
197 |
tmp_rast.add('v.g') |
|
198 |
|
|
199 |
# calc chisq critical values (where df = n.eff - 1) |
|
200 |
gs.mapcalc('chisq = 1 + ${chisqa}/sqrt(${df}) + ${chisqb}/${df}', |
|
201 |
chisqa=chisqa, chisqb=chisqb, df='(w^2/wsq - 1)', quiet=True) |
|
202 |
tmp_rast.add('chisq') |
|
203 |
# set coarsened cell variances: if group variance is small |
|
204 |
# relative to noise variance (mean of finer scale variances), |
|
205 |
# use variance of neighborhood mean instead of group variance |
|
206 |
gs.mapcalc('v%d = if(v.g/chisq < ${mv}, ${vm}, v.g)' % i, |
|
207 |
vm = '(1.0/w)', mv = '(n/w)', quiet=True) |
|
208 |
tmp_rast.add('v%d' % i) |
|
209 |
|
|
210 |
# arbitrary value used to fill null variance cells |
|
211 |
bigvar = gs.raster_info('v0')['max'] * 10 |
|
212 |
|
|
213 |
# prep for refinement phase |
|
214 |
gs.run_command('g.rename', rast='z%d,%s' % (NUM_LEVELS, smooth), |
|
215 |
quiet=True) |
|
216 |
tmp_rast.remove('z%d' % NUM_LEVELS) |
|
217 |
gs.run_command('g.rename', rast='v%d,v.smooth' % NUM_LEVELS, |
|
218 |
quiet=True) |
|
219 |
tmp_rast.add('v.smooth') |
|
220 |
tmp_rast.remove('v%d' % NUM_LEVELS) |
|
221 |
|
|
222 |
# smooth, refine and combine each layer in turn |
|
223 |
for j in reversed(range(NUM_LEVELS)): |
|
224 |
|
|
225 |
i = j + 1 |
|
226 |
gs.message('Refining from %d to %d' % (i, j), flag='i') |
|
227 |
|
|
228 |
refine_region() |
|
229 |
|
|
230 |
# create smoothed higher resolution versions of z and v |
|
231 |
#TODO: is this the same as circle with radius 2 in arc? |
|
232 |
#TODO: what's happening at the edges here??? |
|
233 |
#TODO: is order of ops faithful to aml w.r.t resolution changes? |
|
234 |
gs.run_command('r.neighbors', flags='c', input=smooth, |
|
235 |
output='zs', method='average', size=5, overwrite=True, |
|
236 |
quiet=True) |
|
237 |
tmp_rast.add('zs') |
|
238 |
gs.run_command('r.neighbors', flags='c', input='v.smooth', |
|
239 |
output='vs', method='average', size=5, overwrite=True, |
|
240 |
quiet=True) |
|
241 |
tmp_rast.add('vs') |
|
242 |
|
|
243 |
# combine two levels using least variance, using no-null |
|
244 |
# versions of finer z and v |
|
245 |
z_c = 'if(isnull(z%d), 0, z%d)' % (j, j) |
|
246 |
v_c = 'if(isnull(v%d), %f, v%d)' % (j, bigvar, j) |
|
247 |
gs.mapcalc('${smooth} = (${z_c}/${v_c} + zs/vs) / (1/${v_c} + 1/vs)', |
|
248 |
smooth=smooth, z_c=z_c, v_c=v_c, quiet=True) |
|
249 |
gs.mapcalc('v.smooth = 1 / (1/${v_c} + 1/vs)', v_c = v_c, |
|
250 |
quiet=True) |
|
251 |
|
|
252 |
if (1 <= gs.debug_level): |
|
253 |
report() |
|
254 |
|
|
255 |
cleanup() |
|
256 |
return None |
|
257 |
|
|
258 |
|
|
259 |
def main(): |
|
260 |
|
|
261 |
# process command options |
|
262 |
input = options['input'] |
|
263 |
if not gs.find_file(input)['file']: |
|
264 |
gs.fatal(_("Raster map <%s> not found") % input) |
|
265 |
|
|
266 |
smooth = options['output'] |
|
267 |
if gs.find_file(smooth)['file'] and not gs.overwrite(): |
|
268 |
gs.fatal(_("Output map <%s> already exists") % smooth) |
|
269 |
|
|
270 |
sd = options['sd'] |
|
271 |
try: |
|
272 |
sd = float(sd) |
|
273 |
except ValueError: |
|
274 |
if not gs.find_file(sd)['file']: |
|
275 |
gs.fatal(_("Raster map <%s> not found") % sd) |
|
276 |
|
|
277 |
alpha = float(options['alpha']) |
|
278 |
|
|
279 |
# set aside region for internal use |
|
280 |
gs.use_temp_region() |
|
281 |
|
|
282 |
# subset input if desired |
|
283 |
region = options.get('region') |
|
284 |
if region: |
|
285 |
if not gs.find_file(sd)['file']: |
|
286 |
gs.fatal(_("Raster map <%s> not found") % region) |
|
287 |
gs.message("Setting region to %s" % region, flag='i') |
|
288 |
gs.run_command('g.region', rast=region, align=input) |
|
289 |
else: |
|
290 |
gs.message("Using existing GRASS region", flag='i') |
|
291 |
gs.debug('='*50) |
|
292 |
gs.debug('\n'.join(gs.parse_command('g.region', 'p').keys())) |
|
293 |
gs.debug('='*50) |
|
294 |
|
|
295 |
multiscalesmooth(input, smooth, sd, alpha) |
|
296 |
|
|
297 |
# restore original region |
|
298 |
gs.del_temp_region() |
|
299 |
|
|
300 |
return None |
|
301 |
|
|
302 |
|
|
303 |
if __name__ == '__main__': |
|
304 |
options, flags = gs.parser() |
|
305 |
atexit.register(cleanup) |
|
306 |
main() |
Also available in: Unified diff
added GRASS/Python implementation of multiscale smoother