i.pansharpen.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. #!/usr/bin/env python
  2. ############################################################################
  3. #
  4. # MODULE: i.panmethod
  5. #
  6. # AUTHOR(S): Overall script by Michael Barton (ASU)
  7. # Brovey transformation in i.fusion.brovey by Markus Neteler <<neteler at osgeo org>>
  8. # i.fusion brovey converted to Python by Glynn Clements
  9. # IHS and PCA transformation added by Michael Barton (ASU)
  10. # histogram matching algorithm by Michael Barton and Luca Delucchi, Fondazione E. Mach (Italy)
  11. # Thanks to Markus Metz for help with PCA inversion
  12. # Thanks to Hamish Bowman for parallel processing algorithm
  13. #
  14. # PURPOSE: Sharpening of 3 RGB channels using a high-resolution panchromatic channel
  15. #
  16. # COPYRIGHT: (C) 2002-2012 by the GRASS Development Team
  17. #
  18. # This program is free software under the GNU General Public
  19. # License (>=v2). Read the file COPYING that comes with GRASS
  20. # for details.
  21. #
  22. # REFERENCES:
  23. # Roller, N.E.G. and Cox, S., 1980. Comparison of Landsat MSS and merged MSS/RBV
  24. # data for analysis of natural vegetation. Proc. of the 14th International
  25. # Symposium on Remote Sensing of Environment, San Jose, Costa Rica, 23-30 April, pp. 1001-1007.
  26. #
  27. # Amarsaikhan, D., & Douglas, T. (2004). Data fusion and multisource image classification.
  28. # International Journal of Remote Sensing, 25(17), 3529-3539.
  29. #
  30. # Behnia, P. (2005). Comparison between four methods for data fusion of ETM+
  31. # multispectral and pan images. Geo-spatial Information Science, 8(2), 98-103
  32. #
  33. # for LANDSAT 5: see Pohl, C 1996 and others
  34. #
  35. #############################################################################
  36. #%Module
  37. #% description: Image fusion algorithms to sharpen multispectral with high-res panchromatic channels
  38. #% keyword: imagery
  39. #% keyword: fusion
  40. #% keyword: sharpen
  41. #% keyword: Brovey
  42. #% keyword: IHS
  43. #% keyword: HIS
  44. #% keyword: PCA
  45. #% overwrite: yes
  46. #%End
  47. #%option G_OPT_R_INPUT
  48. #% key: red
  49. #% description: Name of raster map to be used for <red>
  50. #%end
  51. #%option G_OPT_R_INPUT
  52. #% key: green
  53. #% description: Name of raster map to be used for <green>
  54. #%end
  55. #%option G_OPT_R_INPUT
  56. #% key: blue
  57. #% description: Name of raster map to be used for <blue>
  58. #%end
  59. #% option G_OPT_R_INPUT
  60. #% key: pan
  61. #% description: Name of raster map to be used for high resolution panchromatic channel
  62. #%end
  63. #%option G_OPT_R_BASENAME_OUTPUT
  64. #%end
  65. #%option
  66. #% key: method
  67. #% description: Method for pan sharpening
  68. #% options: brovey,ihs,pca
  69. #% answer: ihs
  70. #% required: yes
  71. #%end
  72. #%flag
  73. #% key: s
  74. #% description: Serial processing rather than parallel processing
  75. #%end
  76. #%flag
  77. #% key: l
  78. #% description: Rebalance blue channel for LANDSAT
  79. #%end
  80. import os
  81. try:
  82. import numpy as np
  83. hasNumPy = True
  84. except ImportError:
  85. hasNumPy = False
  86. import grass.script as grass
  87. from grass.script.utils import decode
  88. # i18N
  89. import gettext
  90. gettext.install('grassmods', os.path.join(os.getenv("GISBASE"), 'locale'))
  91. def main():
  92. if not hasNumPy:
  93. grass.fatal(_("Required dependency NumPy not found. Exiting."))
  94. sharpen = options['method'] # sharpening algorithm
  95. ms1 = options['blue'] # blue channel
  96. ms2 = options['green'] # green channel
  97. ms3 = options['red'] # red channel
  98. pan = options['pan'] # high res pan channel
  99. out = options['output'] # prefix for output RGB maps
  100. bladjust = flags['l'] # adjust blue channel
  101. sproc = flags['s'] # serial processing
  102. outb = grass.core.find_file('%s_blue' % out)
  103. outg = grass.core.find_file('%s_green' % out)
  104. outr = grass.core.find_file('%s_red' % out)
  105. if (outb['name'] != '' or outg['name'] != '' or outr['name'] != '') and not grass.overwrite():
  106. grass.warning(_('Maps with selected output prefix names already exist.'
  107. ' Delete them or use overwrite flag'))
  108. return
  109. pid = str(os.getpid())
  110. # get PAN resolution:
  111. kv = grass.raster_info(map=pan)
  112. nsres = kv['nsres']
  113. ewres = kv['ewres']
  114. panres = (nsres + ewres) / 2
  115. # clone current region
  116. grass.use_temp_region()
  117. grass.run_command('g.region', res=panres, align=pan)
  118. grass.message(_("Performing pan sharpening with hi res pan image: %f" % panres))
  119. if sharpen == "brovey":
  120. grass.verbose(_("Using Brovey algorithm"))
  121. # pan/intensity histogram matching using linear regression
  122. outname = 'tmp%s_pan1' % pid
  123. panmatch1 = matchhist(pan, ms1, outname)
  124. outname = 'tmp%s_pan2' % pid
  125. panmatch2 = matchhist(pan, ms2, outname)
  126. outname = 'tmp%s_pan3' % pid
  127. panmatch3 = matchhist(pan, ms3, outname)
  128. outr = '%s_red' % out
  129. outg = '%s_green' % out
  130. outb = '%s_blue' % out
  131. # calculate brovey transformation
  132. grass.message(_("Calculating Brovey transformation..."))
  133. if sproc:
  134. # serial processing
  135. e = '''eval(k = "$ms1" + "$ms2" + "$ms3")
  136. "$outr" = 1.0 * "$ms3" * "$panmatch3" / k
  137. "$outg" = 1.0 * "$ms2" * "$panmatch2" / k
  138. "$outb" = 1.0 * "$ms1" * "$panmatch1" / k'''
  139. grass.mapcalc(e, outr=outr, outg=outg, outb=outb,
  140. panmatch1=panmatch1, panmatch2=panmatch2,
  141. panmatch3=panmatch3, ms1=ms1, ms2=ms2, ms3=ms3,
  142. overwrite=True)
  143. else:
  144. # parallel processing
  145. pb = grass.mapcalc_start('%s_blue = (1.0 * %s * %s) / (%s + %s + %s)' %
  146. (out, ms1, panmatch1, ms1, ms2, ms3),
  147. overwrite=True)
  148. pg = grass.mapcalc_start('%s_green = (1.0 * %s * %s) / (%s + %s + %s)' %
  149. (out, ms2, panmatch2, ms1, ms2, ms3),
  150. overwrite=True)
  151. pr = grass.mapcalc_start('%s_red = (1.0 * %s * %s) / (%s + %s + %s)' %
  152. (out, ms3, panmatch3, ms1, ms2, ms3),
  153. overwrite=True)
  154. pb.wait()
  155. pg.wait()
  156. pr.wait()
  157. # Cleanup
  158. grass.run_command('g.remove', flags='f', quiet=True, type='raster',
  159. name='%s,%s,%s' % (panmatch1, panmatch2, panmatch3))
  160. elif sharpen == "ihs":
  161. grass.verbose(_("Using IHS<->RGB algorithm"))
  162. # transform RGB channels into IHS color space
  163. grass.message(_("Transforming to IHS color space..."))
  164. grass.run_command('i.rgb.his', overwrite=True,
  165. red=ms3,
  166. green=ms2,
  167. blue=ms1,
  168. hue="tmp%s_hue" % pid,
  169. intensity="tmp%s_int" % pid,
  170. saturation="tmp%s_sat" % pid)
  171. # pan/intensity histogram matching using linear regression
  172. target = "tmp%s_int" % pid
  173. outname = "tmp%s_pan_int" % pid
  174. panmatch = matchhist(pan, target, outname)
  175. # substitute pan for intensity channel and transform back to RGB color space
  176. grass.message(_("Transforming back to RGB color space and sharpening..."))
  177. grass.run_command('i.his.rgb', overwrite=True,
  178. hue="tmp%s_hue" % pid,
  179. intensity="%s" % panmatch,
  180. saturation="tmp%s_sat" % pid,
  181. red="%s_red" % out,
  182. green="%s_green" % out,
  183. blue="%s_blue" % out)
  184. # Cleanup
  185. grass.run_command('g.remove', flags='f', quiet=True, type='raster',
  186. name=panmatch)
  187. elif sharpen == "pca":
  188. grass.verbose(_("Using PCA/inverse PCA algorithm"))
  189. grass.message(_("Creating PCA images and calculating eigenvectors..."))
  190. # initial PCA with RGB channels
  191. pca_out = grass.read_command('i.pca', quiet=True, rescale='0,0',
  192. input='%s,%s,%s' % (ms1, ms2, ms3),
  193. output='tmp%s.pca' % pid)
  194. if len(pca_out) < 1:
  195. grass.fatal(_("Input has no data. Check region settings."))
  196. b1evect = []
  197. b2evect = []
  198. b3evect = []
  199. for l in pca_out.replace('(', ',').replace(')', ',').splitlines():
  200. b1evect.append(float(l.split(',')[1]))
  201. b2evect.append(float(l.split(',')[2]))
  202. b3evect.append(float(l.split(',')[3]))
  203. # inverse PCA with hi res pan channel substituted for principal component 1
  204. pca1 = 'tmp%s.pca.1' % pid
  205. pca2 = 'tmp%s.pca.2' % pid
  206. pca3 = 'tmp%s.pca.3' % pid
  207. b1evect1 = b1evect[0]
  208. b1evect2 = b1evect[1]
  209. b1evect3 = b1evect[2]
  210. b2evect1 = b2evect[0]
  211. b2evect2 = b2evect[1]
  212. b2evect3 = b2evect[2]
  213. b3evect1 = b3evect[0]
  214. b3evect2 = b3evect[1]
  215. b3evect3 = b3evect[2]
  216. outname = 'tmp%s_pan' % pid
  217. panmatch = matchhist(pan, ms1, outname)
  218. grass.message(_("Performing inverse PCA ..."))
  219. stats1 = grass.parse_command("r.univar", map=ms1, flags='g',
  220. parse=(grass.parse_key_val,
  221. {'sep': '='}))
  222. stats2 = grass.parse_command("r.univar", map=ms2, flags='g',
  223. parse=(grass.parse_key_val,
  224. {'sep': '='}))
  225. stats3 = grass.parse_command("r.univar", map=ms3, flags='g',
  226. parse=(grass.parse_key_val,
  227. {'sep': '='}))
  228. b1mean = float(stats1['mean'])
  229. b2mean = float(stats2['mean'])
  230. b3mean = float(stats3['mean'])
  231. if sproc:
  232. # serial processing
  233. e = '''eval(k = "$ms1" + "$ms2" + "$ms3")
  234. "$outr" = 1.0 * "$ms3" * "$panmatch3" / k
  235. "$outg" = 1.0 * "$ms2" * "$panmatch2" / k
  236. "$outb" = 1.0* "$ms1" * "$panmatch1" / k'''
  237. outr = '%s_red' % out
  238. outg = '%s_green' % out
  239. outb = '%s_blue' % out
  240. cmd1 = "$outb = (1.0 * $panmatch * $b1evect1) + ($pca2 * $b2evect1) + ($pca3 * $b3evect1) + $b1mean"
  241. cmd2 = "$outg = (1.0 * $panmatch * $b1evect2) + ($pca2 * $b2evect1) + ($pca3 * $b3evect2) + $b2mean"
  242. cmd3 = "$outr = (1.0 * $panmatch * $b1evect3) + ($pca2 * $b2evect3) + ($pca3 * $b3evect3) + $b3mean"
  243. cmd = '\n'.join([cmd1, cmd2, cmd3])
  244. grass.mapcalc(cmd, outb=outb, outg=outg, outr=outr,
  245. panmatch=panmatch, pca2=pca2, pca3=pca3,
  246. b1evect1=b1evect1, b2evect1=b2evect1, b3evect1=b3evect1,
  247. b1evect2=b1evect2, b2evect2=b2evect2, b3evect2=b3evect2,
  248. b1evect3=b1evect3, b2evect3=b2evect3, b3evect3=b3evect3,
  249. b1mean=b1mean, b2mean=b2mean, b3mean=b3mean,
  250. overwrite=True)
  251. else:
  252. # parallel processing
  253. pb = grass.mapcalc_start('%s_blue = (%s * %f) + (%s * %f) + (%s * %f) + %f'
  254. % (out, panmatch, b1evect1, pca2,
  255. b2evect1, pca3, b3evect1, b1mean),
  256. overwrite=True)
  257. pg = grass.mapcalc_start('%s_green = (%s * %f) + (%s * %f) + (%s * %f) + %f'
  258. % (out, panmatch, b1evect2, pca2,
  259. b2evect2, pca3, b3evect2, b2mean),
  260. overwrite=True)
  261. pr = grass.mapcalc_start('%s_red = (%s * %f) + (%s * %f) + (%s * ''%f) + %f'
  262. % (out, panmatch, b1evect3, pca2,
  263. b2evect3, pca3, b3evect3, b3mean),
  264. overwrite=True)
  265. pr.wait()
  266. pg.wait()
  267. pb.wait()
  268. # Cleanup
  269. grass.run_command('g.remove', flags='f', quiet=True, type="raster",
  270. pattern='tmp%s*,%s' % (pid, panmatch))
  271. # Could add other sharpening algorithms here, e.g. wavelet transformation
  272. grass.message(_("Assigning grey equalized color tables to output images..."))
  273. # equalized grey scales give best contrast
  274. for ch in ['red', 'green', 'blue']:
  275. grass.run_command('r.colors', quiet=True, map="%s_%s" % (out, ch),
  276. flags="e", color='grey')
  277. # Landsat too blue-ish because panchromatic band less sensitive to blue
  278. # light, so output blue channed can be modified
  279. if bladjust:
  280. grass.message(_("Adjusting blue channel color table..."))
  281. rules = grass.tempfile()
  282. colors = open(rules, 'w')
  283. colors.write('5 0 0 0\n20 200 200 200\n40 230 230 230\n67 255 255 255 \n')
  284. colors.close()
  285. grass.run_command('r.colors', map="%s_blue" % out, rules=rules)
  286. os.remove(rules)
  287. # output notice
  288. grass.verbose(_("The following pan-sharpened output maps have been generated:"))
  289. for ch in ['red', 'green', 'blue']:
  290. grass.verbose(_("%s_%s") % (out, ch))
  291. grass.verbose(_("To visualize output, run: g.region -p raster=%s_red" % out))
  292. grass.verbose(_("d.rgb r=%s_red g=%s_green b=%s_blue" % (out, out, out)))
  293. grass.verbose(_("If desired, combine channels into a single RGB map with 'r.composite'."))
  294. grass.verbose(_("Channel colors can be rebalanced using i.colors.enhance."))
  295. # write cmd history:
  296. for ch in ['red', 'green', 'blue']:
  297. grass.raster_history("%s_%s" % (out, ch))
  298. # create a group with the three output
  299. grass.run_command('i.group', group=out,
  300. input="{n}_red,{n}_blue,{n}_green".format(n=out))
  301. # Cleanup
  302. grass.run_command('g.remove', flags="f", type="raster",
  303. pattern="tmp%s*" % pid, quiet=True)
  304. def matchhist(original, target, matched):
  305. # pan/intensity histogram matching using numpy arrays
  306. grass.message(_("Histogram matching..."))
  307. # input images
  308. original = original.split('@')[0]
  309. target = target.split('@')[0]
  310. images = [original, target]
  311. # create a dictionary to hold arrays for each image
  312. arrays = {}
  313. for i in images:
  314. # calculate number of cells for each grey value for for each image
  315. stats_out = grass.pipe_command('r.stats', flags='cin', input=i,
  316. sep=':')
  317. stats = decode(stats_out.communicate()[0]).split('\n')[:-1]
  318. stats_dict = dict(s.split(':', 1) for s in stats)
  319. total_cells = 0 # total non-null cells
  320. for j in stats_dict:
  321. stats_dict[j] = int(stats_dict[j])
  322. if j != '*':
  323. total_cells += stats_dict[j]
  324. if total_cells < 1:
  325. grass.fatal(_("Input has no data. Check region settings."))
  326. # Make a 2x256 structured array for each image with a
  327. # cumulative distribution function (CDF) for each grey value.
  328. # Grey value is the integer (i4) and cdf is float (f4).
  329. arrays[i] = np.zeros((256, ), dtype=('i4,f4'))
  330. cum_cells = 0 # cumulative total of cells for sum of current and all lower grey values
  331. for n in range(0, 256):
  332. if str(n) in stats_dict:
  333. num_cells = stats_dict[str(n)]
  334. else:
  335. num_cells = 0
  336. cum_cells += num_cells
  337. # cdf is the the number of cells at or below a given grey value
  338. # divided by the total number of cells
  339. cdf = float(cum_cells) / float(total_cells)
  340. # insert values into array
  341. arrays[i][n] = (n, cdf)
  342. # open file for reclass rules
  343. outfile = open(grass.tempfile(), 'w')
  344. for i in arrays[original]:
  345. # for each grey value and corresponding cdf value in original, find the
  346. # cdf value in target that is closest to the target cdf value
  347. difference_list = []
  348. for j in arrays[target]:
  349. # make a list of the difference between each original cdf value and
  350. # the target cdf value
  351. difference_list.append(abs(i[1] - j[1]))
  352. # get the smallest difference in the list
  353. min_difference = min(difference_list)
  354. for j in arrays[target]:
  355. # find the grey value in target that corresponds to the cdf
  356. # closest to the original cdf
  357. if j[1] == i[1] + min_difference or j[1] == i[1] - min_difference:
  358. # build a reclass rules file from the original grey value and
  359. # corresponding grey value from target
  360. out_line = "%d = %d\n" % (i[0], j[0])
  361. outfile.write(out_line)
  362. break
  363. outfile.close()
  364. # create reclass of target from reclass rules file
  365. result = grass.core.find_file(matched, element='cell')
  366. if result['fullname']:
  367. grass.run_command('g.remove', flags='f', quiet=True, type='raster',
  368. name=matched)
  369. grass.run_command('r.reclass', input=original, out=matched,
  370. rules=outfile.name)
  371. else:
  372. grass.run_command('r.reclass', input=original, out=matched,
  373. rules=outfile.name)
  374. # Cleanup
  375. # remove the rules file
  376. grass.try_remove(outfile.name)
  377. # return reclass of target with histogram that matches original
  378. return matched
  379. if __name__ == "__main__":
  380. options, flags = grass.parser()
  381. main()