3
############################################################################
7
# AUTHOR(S): Overall script by Michael Barton (ASU)
8
# Brovey transformation in i.fusion.brovey by Markus Neteler <<neteler at osgeo org>>
9
# i.fusion brovey converted to Python by Glynn Clements
10
# IHS and PCA transformation added by Michael Barton (ASU)
11
# histogram matching algorithm by Michael Barton and Luca Delucchi, Fondazione E. Mach (Italy)
12
# Thanks to Markus Metz for help with PCA inversion
13
# Thanks to Hamish Bowman for parallel processing algorithm
15
# PURPOSE: Sharpening of 3 RGB channels using a high-resolution panchromatic channel
17
# COPYRIGHT: (C) 2002-2012 by the GRASS Development Team
19
# This program is free software under the GNU General Public
20
# License (>=v2). Read the file COPYING that comes with GRASS
24
# Roller, N.E.G. and Cox, S., 1980. Comparison of Landsat MSS and merged MSS/RBV
25
# data for analysis of natural vegetation. Proc. of the 14th International
26
# Symposium on Remote Sensing of Environment, San Jose, Costa Rica, 23-30 April, pp. 1001-1007.
28
# Amarsaikhan, D., & Douglas, T. (2004). Data fusion and multisource image classification.
29
# International Journal of Remote Sensing, 25(17), 3529-3539.
31
# Behnia, P. (2005). Comparison between four methods for data fusion of ETM+
32
# multispectral and pan images. Geo-spatial Information Science, 8(2), 98-103
34
# for LANDSAT 5: see Pohl, C 1996 and others
36
#############################################################################
39
#% description: Image fusion algorithms to sharpen multispectral with high-res panchromatic channels
49
#%option G_OPT_R_INPUT
51
#% description: Name of raster map to be used for <red>
53
#%option G_OPT_R_INPUT
55
#% description: Name of raster map to be used for <green>
57
#%option G_OPT_R_INPUT
59
#% description: Name of raster map to be used for <blue>
61
#% option G_OPT_R_INPUT
63
#% description: Name of raster map to be used for high resolution panchromatic channel
65
#%option G_OPT_R_BASENAME_OUTPUT
69
#% description: Method for pan sharpening
70
#% options: brovey,ihs,pca
76
#% description: Serial processing rather than parallel processing
80
#% description: Rebalance blue channel for LANDSAT
91
import grass.script as grass
96
grass.fatal(_("Required dependency NumPy not found. Exiting."))
98
sharpen = options['method'] # sharpening algorithm
99
ms1 = options['blue'] # blue channel
100
ms2 = options['green'] # green channel
101
ms3 = options['red'] # red channel
102
pan = options['pan'] # high res pan channel
103
out = options['output'] # prefix for output RGB maps
104
bladjust = flags['l'] # adjust blue channel
105
sproc = flags['s'] # serial processing
107
outb = grass.core.find_file('%s_blue' % out)
108
outg = grass.core.find_file('%s_green' % out)
109
outr = grass.core.find_file('%s_red' % out)
111
if (outb['name'] != '' or outg['name'] != '' or outr['name'] != '') and not grass.overwrite():
112
grass.warning(_('Maps with selected output prefix names already exist.'
113
' Delete them or use overwrite flag'))
116
pid = str(os.getpid())
118
# get PAN resolution:
119
kv = grass.raster_info(map=pan)
122
panres = (nsres + ewres) / 2
124
# clone current region
125
grass.use_temp_region()
127
grass.run_command('g.region', res=panres, align=pan)
129
grass.message(_("Performing pan sharpening with hi res pan image: %f" % panres))
131
if sharpen == "brovey":
132
grass.verbose(_("Using Brovey algorithm"))
134
# pan/intensity histogram matching using linear regression
135
outname = 'tmp%s_pan1' % pid
136
panmatch1 = matchhist(pan, ms1, outname)
138
outname = 'tmp%s_pan2' % pid
139
panmatch2 = matchhist(pan, ms2, outname)
141
outname = 'tmp%s_pan3' % pid
142
panmatch3 = matchhist(pan, ms3, outname)
144
outr = '%s_red' % out
145
outg = '%s_green' % out
146
outb = '%s_blue' % out
148
# calculate brovey transformation
149
grass.message(_("Calculating Brovey transformation..."))
153
e = '''eval(k = "$ms1" + "$ms2" + "$ms3")
154
"$outr" = 1.0 * "$ms3" * "$panmatch3" / k
155
"$outg" = 1.0 * "$ms2" * "$panmatch2" / k
156
"$outb" = 1.0 * "$ms1" * "$panmatch1" / k'''
157
grass.mapcalc(e, outr=outr, outg=outg, outb=outb,
158
panmatch1=panmatch1, panmatch2=panmatch2,
159
panmatch3=panmatch3, ms1=ms1, ms2=ms2, ms3=ms3,
162
# parallel processing
163
pb = grass.mapcalc_start('%s_blue = (1.0 * %s * %s) / (%s + %s + %s)' %
164
(out, ms1, panmatch1, ms1, ms2, ms3),
166
pg = grass.mapcalc_start('%s_green = (1.0 * %s * %s) / (%s + %s + %s)' %
167
(out, ms2, panmatch2, ms1, ms2, ms3),
169
pr = grass.mapcalc_start('%s_red = (1.0 * %s * %s) / (%s + %s + %s)' %
170
(out, ms3, panmatch3, ms1, ms2, ms3),
178
grass.run_command('g.remove', flags='f', quiet=True, type='raster',
179
name='%s,%s,%s' % (panmatch1, panmatch2, panmatch3))
181
elif sharpen == "ihs":
182
grass.verbose(_("Using IHS<->RGB algorithm"))
183
# transform RGB channels into IHS color space
184
grass.message(_("Transforming to IHS color space..."))
185
grass.run_command('i.rgb.his', overwrite=True,
189
hue="tmp%s_hue" % pid,
190
intensity="tmp%s_int" % pid,
191
saturation="tmp%s_sat" % pid)
193
# pan/intensity histogram matching using linear regression
194
target = "tmp%s_int" % pid
195
outname = "tmp%s_pan_int" % pid
196
panmatch = matchhist(pan, target, outname)
198
# substitute pan for intensity channel and transform back to RGB color space
199
grass.message(_("Transforming back to RGB color space and sharpening..."))
200
grass.run_command('i.his.rgb', overwrite=True,
201
hue="tmp%s_hue" % pid,
202
intensity="%s" % panmatch,
203
saturation="tmp%s_sat" % pid,
205
green="%s_green" % out,
206
blue="%s_blue" % out)
209
grass.run_command('g.remove', flags='f', quiet=True, type='raster',
212
elif sharpen == "pca":
213
grass.verbose(_("Using PCA/inverse PCA algorithm"))
214
grass.message(_("Creating PCA images and calculating eigenvectors..."))
216
# initial PCA with RGB channels
217
pca_out = grass.read_command('i.pca', quiet=True, rescale='0,0',
218
input='%s,%s,%s' % (ms1, ms2, ms3),
219
output='tmp%s.pca' % pid)
221
grass.fatal(_("Input has no data. Check region settings."))
226
for l in pca_out.replace('(', ',').replace(')', ',').splitlines():
227
b1evect.append(float(l.split(',')[1]))
228
b2evect.append(float(l.split(',')[2]))
229
b3evect.append(float(l.split(',')[3]))
231
# inverse PCA with hi res pan channel substituted for principal component 1
232
pca1 = 'tmp%s.pca.1' % pid
233
pca2 = 'tmp%s.pca.2' % pid
234
pca3 = 'tmp%s.pca.3' % pid
235
b1evect1 = b1evect[0]
236
b1evect2 = b1evect[1]
237
b1evect3 = b1evect[2]
238
b2evect1 = b2evect[0]
239
b2evect2 = b2evect[1]
240
b2evect3 = b2evect[2]
241
b3evect1 = b3evect[0]
242
b3evect2 = b3evect[1]
243
b3evect3 = b3evect[2]
245
outname = 'tmp%s_pan' % pid
246
panmatch = matchhist(pan, ms1, outname)
248
grass.message(_("Performing inverse PCA ..."))
250
stats1 = grass.parse_command("r.univar", map=ms1, flags='g',
251
parse=(grass.parse_key_val,
253
stats2 = grass.parse_command("r.univar", map=ms2, flags='g',
254
parse=(grass.parse_key_val,
256
stats3 = grass.parse_command("r.univar", map=ms3, flags='g',
257
parse=(grass.parse_key_val,
260
b1mean = float(stats1['mean'])
261
b2mean = float(stats2['mean'])
262
b3mean = float(stats3['mean'])
266
e = '''eval(k = "$ms1" + "$ms2" + "$ms3")
267
"$outr" = 1.0 * "$ms3" * "$panmatch3" / k
268
"$outg" = 1.0 * "$ms2" * "$panmatch2" / k
269
"$outb" = 1.0* "$ms1" * "$panmatch1" / k'''
271
outr = '%s_red' % out
272
outg = '%s_green' % out
273
outb = '%s_blue' % out
275
cmd1 = "$outb = (1.0 * $panmatch * $b1evect1) + ($pca2 * $b2evect1) + ($pca3 * $b3evect1) + $b1mean"
276
cmd2 = "$outg = (1.0 * $panmatch * $b1evect2) + ($pca2 * $b2evect1) + ($pca3 * $b3evect2) + $b2mean"
277
cmd3 = "$outr = (1.0 * $panmatch * $b1evect3) + ($pca2 * $b2evect3) + ($pca3 * $b3evect3) + $b3mean"
279
cmd = '\n'.join([cmd1, cmd2, cmd3])
281
grass.mapcalc(cmd, outb=outb, outg=outg, outr=outr,
282
panmatch=panmatch, pca2=pca2, pca3=pca3,
283
b1evect1=b1evect1, b2evect1=b2evect1, b3evect1=b3evect1,
284
b1evect2=b1evect2, b2evect2=b2evect2, b3evect2=b3evect2,
285
b1evect3=b1evect3, b2evect3=b2evect3, b3evect3=b3evect3,
286
b1mean=b1mean, b2mean=b2mean, b3mean=b3mean,
289
# parallel processing
290
pb = grass.mapcalc_start('%s_blue = (%s * %f) + (%s * %f) + (%s * %f) + %f'
291
% (out, panmatch, b1evect1, pca2,
292
b2evect1, pca3, b3evect1, b1mean),
295
pg = grass.mapcalc_start('%s_green = (%s * %f) + (%s * %f) + (%s * %f) + %f'
296
% (out, panmatch, b1evect2, pca2,
297
b2evect2, pca3, b3evect2, b2mean),
300
pr = grass.mapcalc_start('%s_red = (%s * %f) + (%s * %f) + (%s * ''%f) + %f'
301
% (out, panmatch, b1evect3, pca2,
302
b2evect3, pca3, b3evect3, b3mean),
310
grass.run_command('g.remove', flags='f', quiet=True, type="raster",
311
pattern='tmp%s*,%s' % (pid, panmatch))
313
# Could add other sharpening algorithms here, e.g. wavelet transformation
315
grass.message(_("Assigning grey equalized color tables to output images..."))
316
# equalized grey scales give best contrast
317
for ch in ['red', 'green', 'blue']:
318
grass.run_command('r.colors', quiet=True, map="%s_%s" % (out, ch),
319
flags="e", col='grey')
321
# Landsat too blue-ish because panchromatic band less sensitive to blue
322
# light, so output blue channed can be modified
324
grass.message(_("Adjusting blue channel color table..."))
325
rules = grass.tempfile()
326
colors = open(rules, 'w')
327
colors.write('5 0 0 0\n20 200 200 200\n40 230 230 230\n67 255 255 255 \n')
330
grass.run_command('r.colors', map="%s_blue" % out, rules=rules)
334
grass.verbose(_("The following pan-sharpened output maps have been generated:"))
335
for ch in ['red', 'green', 'blue']:
336
grass.verbose(_("%s_%s") % (out, ch))
338
grass.verbose(_("To visualize output, run: g.region -p raster=%s_red" % out))
339
grass.verbose(_("d.rgb r=%s_red g=%s_green b=%s_blue" % (out, out, out)))
340
grass.verbose(_("If desired, combine channels into a single RGB map with 'r.composite'."))
341
grass.verbose(_("Channel colors can be rebalanced using i.colors.enhance."))
344
for ch in ['red', 'green', 'blue']:
345
grass.raster_history("%s_%s" % (out, ch))
347
# create a group with the three output
348
grass.run_command('i.group', group=out,
349
input="{n}_red,{n}_blue,{n}_green".format(n=out))
352
grass.run_command('g.remove', flags="f", type="raster",
353
pattern="tmp%s*" % pid, quiet=True)
356
def matchhist(original, target, matched):
357
# pan/intensity histogram matching using numpy arrays
358
grass.message(_("Histogram matching..."))
361
original = original.split('@')[0]
362
target = target.split('@')[0]
363
images = [original, target]
365
# create a dictionary to hold arrays for each image
369
# calculate number of cells for each grey value for for each image
370
stats_out = grass.pipe_command('r.stats', flags='cin', input=i,
372
stats = stats_out.communicate()[0].split('\n')[:-1]
373
stats_dict = dict(s.split(':', 1) for s in stats)
374
total_cells = 0 # total non-null cells
376
stats_dict[j] = int(stats_dict[j])
378
total_cells += stats_dict[j]
381
grass.fatal(_("Input has no data. Check region settings."))
383
# Make a 2x256 structured array for each image with a
384
# cumulative distribution function (CDF) for each grey value.
385
# Grey value is the integer (i4) and cdf is float (f4).
387
arrays[i] = np.zeros((256, ), dtype=('i4,f4'))
388
cum_cells = 0 # cumulative total of cells for sum of current and all lower grey values
390
for n in range(0, 256):
391
if str(n) in stats_dict:
392
num_cells = stats_dict[str(n)]
396
cum_cells += num_cells
398
# cdf is the the number of cells at or below a given grey value
399
# divided by the total number of cells
400
cdf = float(cum_cells) / float(total_cells)
402
# insert values into array
403
arrays[i][n] = (n, cdf)
405
# open file for reclass rules
406
outfile = open(grass.tempfile(), 'w')
408
for i in arrays[original]:
409
# for each grey value and corresponding cdf value in original, find the
410
# cdf value in target that is closest to the target cdf value
412
for j in arrays[target]:
413
# make a list of the difference between each original cdf value and
414
# the target cdf value
415
difference_list.append(abs(i[1] - j[1]))
417
# get the smallest difference in the list
418
min_difference = min(difference_list)
420
for j in arrays[target]:
421
# find the grey value in target that correspondes to the cdf
422
# closest to the original cdf
423
if j[1] == i[1] + min_difference or j[1] == i[1] - min_difference:
424
# build a reclass rules file from the original grey value and
425
# corresponding grey value from target
426
out_line = "%d = %d\n" % (i[0], j[0])
427
outfile.write(out_line)
432
# create reclass of target from reclass rules file
433
result = grass.core.find_file(matched, element='cell')
434
if result['fullname']:
435
grass.run_command('g.remove', flags='f', quiet=True, type='raster',
437
grass.run_command('r.reclass', input=original, out=matched,
440
grass.run_command('r.reclass', input=original, out=matched,
444
# remove the rules file
445
grass.try_remove(outfile.name)
447
# return reclass of target with histogram that matches original
450
if __name__ == "__main__":
451
options, flags = grass.parser()