Project

General

Profile

« Previous | Next » 

Revision c87139a7

Added by Jim Regetz over 12 years ago

  • ID c87139a78344e4b0ff201ca1e3a4f5c55a5b6a98

added GRASS/Python implementation of multiscale smoother

View differences:

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