i.pansharpen.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  1. #!/usr/bin/env python3
  2. ############################################################################
  3. #
  4. # MODULE: i.pansharpen
  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-2019 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. #%option
  73. #% key: bitdepth
  74. #% type: integer
  75. #% description: Bit depth of image (must be in range of 2-30)
  76. #% options: 2-32
  77. #% answer: 8
  78. #% required: yes
  79. #%end
  80. #%flag
  81. #% key: s
  82. #% description: Serial processing rather than parallel processing
  83. #%end
  84. #%flag
  85. #% key: l
  86. #% description: Rebalance blue channel for LANDSAT
  87. #%end
  88. #%flag
  89. #% key: r
  90. #% description: Rescale (stretch) the range of pixel values in each channel to the entire 0-255 8-bit range for processing (see notes)
  91. #%end
  92. import os
  93. try:
  94. import numpy as np
  95. hasNumPy = True
  96. except ImportError:
  97. hasNumPy = False
  98. import grass.script as grass
  99. def main():
  100. if not hasNumPy:
  101. grass.fatal(_("Required dependency NumPy not found. Exiting."))
  102. sharpen = options['method'] # sharpening algorithm
  103. ms1_orig = options['blue'] # blue channel
  104. ms2_orig = options['green'] # green channel
  105. ms3_orig = options['red'] # red channel
  106. pan_orig = options['pan'] # high res pan channel
  107. out = options['output'] # prefix for output RGB maps
  108. bits = options['bitdepth'] # bit depth of image channels
  109. bladjust = flags['l'] # adjust blue channel
  110. sproc = flags['s'] # serial processing
  111. rescale = flags['r'] # rescale to spread pixel values to entire 0-255 range
  112. # Checking bit depth
  113. bits = float(bits)
  114. if bits < 2 or bits > 30:
  115. grass.warning(_("Bit depth is outside acceptable range"))
  116. return
  117. outb = grass.core.find_file('%s_blue' % out)
  118. outg = grass.core.find_file('%s_green' % out)
  119. outr = grass.core.find_file('%s_red' % out)
  120. if (outb['name'] != '' or outg['name'] != '' or outr['name'] != '') and not grass.overwrite():
  121. grass.warning(_('Maps with selected output prefix names already exist.'
  122. ' Delete them or use overwrite flag'))
  123. return
  124. pid = str(os.getpid())
  125. # convert input image channels to 8 bit for processing
  126. ms1 = 'tmp%s_ms1' % pid
  127. ms2 = 'tmp%s_ms2' % pid
  128. ms3 = 'tmp%s_ms3' % pid
  129. pan = 'tmp%s_pan' % pid
  130. if rescale == False:
  131. if bits == 8:
  132. grass.message(_("Using 8bit image channels"))
  133. if sproc:
  134. # serial processing
  135. grass.run_command('g.copy', raster='%s,%s' % (ms1_orig, ms1),
  136. quiet=True, overwrite=True)
  137. grass.run_command('g.copy', raster='%s,%s' % (ms2_orig, ms2),
  138. quiet=True, overwrite=True)
  139. grass.run_command('g.copy', raster='%s,%s' % (ms3_orig, ms3),
  140. quiet=True, overwrite=True)
  141. grass.run_command('g.copy', raster='%s,%s' % (pan_orig, pan),
  142. quiet=True, overwrite=True)
  143. else:
  144. # parallel processing
  145. pb = grass.start_command('g.copy', raster='%s,%s' % (ms1_orig, ms1),
  146. quiet=True, overwrite=True)
  147. pg = grass.start_command('g.copy', raster='%s,%s' % (ms2_orig, ms2),
  148. quiet=True, overwrite=True)
  149. pr = grass.start_command('g.copy', raster='%s,%s' % (ms3_orig, ms3),
  150. quiet=True, overwrite=True)
  151. pp = grass.start_command('g.copy', raster='%s,%s' % (pan_orig, pan),
  152. quiet=True, overwrite=True)
  153. pb.wait()
  154. pg.wait()
  155. pr.wait()
  156. pp.wait()
  157. else:
  158. grass.message(_("Converting image chanels to 8bit for processing"))
  159. maxval = pow(2, bits) - 1
  160. if sproc:
  161. # serial processing
  162. grass.run_command('r.rescale', input=ms1_orig, from_='0,%f' % maxval,
  163. output=ms1, to='0,255', quiet=True, overwrite=True)
  164. grass.run_command('r.rescale', input=ms2_orig, from_='0,%f' % maxval,
  165. output=ms2, to='0,255', quiet=True, overwrite=True)
  166. grass.run_command('r.rescale', input=ms3_orig, from_='0,%f' % maxval,
  167. output=ms3, to='0,255', quiet=True, overwrite=True)
  168. grass.run_command('r.rescale', input=pan_orig, from_='0,%f' % maxval,
  169. output=pan, to='0,255', quiet=True, overwrite=True)
  170. else:
  171. # parallel processing
  172. pb = grass.start_command('r.rescale', input=ms1_orig, from_='0,%f' % maxval,
  173. output=ms1, to='0,255', quiet=True, overwrite=True)
  174. pg = grass.start_command('r.rescale', input=ms2_orig, from_='0,%f' % maxval,
  175. output=ms2, to='0,255', quiet=True, overwrite=True)
  176. pr = grass.start_command('r.rescale', input=ms3_orig, from_='0,%f' % maxval,
  177. output=ms3, to='0,255', quiet=True, overwrite=True)
  178. pp = grass.start_command('r.rescale', input=pan_orig, from_='0,%f' % maxval,
  179. output=pan, to='0,255', quiet=True, overwrite=True)
  180. pb.wait()
  181. pg.wait()
  182. pr.wait()
  183. pp.wait()
  184. else:
  185. grass.message(_("Rescaling image chanels to 8bit for processing"))
  186. min_ms1 = int(grass.raster_info(ms1_orig)['min'])
  187. max_ms1 = int(grass.raster_info(ms1_orig)['max'])
  188. min_ms2 = int(grass.raster_info(ms2_orig)['min'])
  189. max_ms2 = int(grass.raster_info(ms2_orig)['max'])
  190. min_ms3 = int(grass.raster_info(ms3_orig)['min'])
  191. max_ms3 = int(grass.raster_info(ms3_orig)['max'])
  192. min_pan = int(grass.raster_info(pan_orig)['min'])
  193. max_pan = int(grass.raster_info(pan_orig)['max'])
  194. maxval = pow(2, bits) - 1
  195. if sproc:
  196. # serial processing
  197. grass.run_command('r.rescale', input=ms1_orig, from_='%f,%f' % (min_ms1, max_ms1),
  198. output=ms1, to='0,255', quiet=True, overwrite=True)
  199. grass.run_command('r.rescale', input=ms2_orig, from_='%f,%f' % (min_ms2, max_ms2),
  200. output=ms2, to='0,255', quiet=True, overwrite=True)
  201. grass.run_command('r.rescale', input=ms3_orig, from_='%f,%f' % (min_ms3, max_ms3),
  202. output=ms3, to='0,255', quiet=True, overwrite=True)
  203. grass.run_command('r.rescale', input=pan_orig, from_='%f,%f' % (min_pan, max_pan),
  204. output=pan, to='0,255', quiet=True, overwrite=True)
  205. else:
  206. # parallel processing
  207. pb = grass.start_command('r.rescale', input=ms1_orig, from_='%f,%f' % (min_ms1, max_ms1),
  208. output=ms1, to='0,255', quiet=True, overwrite=True)
  209. pg = grass.start_command('r.rescale', input=ms2_orig, from_='%f,%f' % (min_ms2, max_ms2),
  210. output=ms2, to='0,255', quiet=True, overwrite=True)
  211. pr = grass.start_command('r.rescale', input=ms3_orig, from_='%f,%f' % (min_ms3, max_ms3),
  212. output=ms3, to='0,255', quiet=True, overwrite=True)
  213. pp = grass.start_command('r.rescale', input=pan_orig, from_='%f,%f' % (min_pan, max_pan),
  214. output=pan, to='0,255', quiet=True, overwrite=True)
  215. pb.wait()
  216. pg.wait()
  217. pr.wait()
  218. pp.wait()
  219. # get PAN resolution:
  220. kv = grass.raster_info(map=pan)
  221. nsres = kv['nsres']
  222. ewres = kv['ewres']
  223. panres = (nsres + ewres) / 2
  224. # clone current region
  225. grass.use_temp_region()
  226. grass.run_command('g.region', res=panres, align=pan)
  227. # Select sharpening method
  228. grass.message(_("Performing pan sharpening with hi res pan image: %f" % panres))
  229. if sharpen == "brovey":
  230. brovey(pan, ms1, ms2, ms3, out, pid, sproc)
  231. elif sharpen == "ihs":
  232. ihs(pan, ms1, ms2, ms3, out, pid, sproc)
  233. elif sharpen == "pca":
  234. pca(pan, ms1, ms2, ms3, out, pid, sproc)
  235. # Could add other sharpening algorithms here, e.g. wavelet transformation
  236. grass.message(_("Assigning grey equalized color tables to output images..."))
  237. # equalized grey scales give best contrast
  238. grass.message(_("setting pan-sharpened channels to equalized grey scale"))
  239. for ch in ['red', 'green', 'blue']:
  240. grass.run_command('r.colors', quiet=True, map="%s_%s" % (out, ch),
  241. flags="e", color='grey')
  242. # Landsat too blue-ish because panchromatic band less sensitive to blue
  243. # light, so output blue channed can be modified
  244. if bladjust:
  245. grass.message(_("Adjusting blue channel color table..."))
  246. blue_colors = ['0 0 0 0\n5% 0 0 0\n67% 255 255 255\n100% 255 255 255']
  247. # these previous colors are way too blue for landsat
  248. # blue_colors = ['0 0 0 0\n10% 0 0 0\n20% 200 200 200\n40% 230 230 230\n67% 255 255 255\n100% 255 255 255']
  249. bc = grass.feed_command('r.colors', quiet = True, map = "%s_blue" % out, rules = "-")
  250. bc.stdin.write(grass.encode('\n'.join(blue_colors)))
  251. bc.stdin.close()
  252. # output notice
  253. grass.verbose(_("The following pan-sharpened output maps have been generated:"))
  254. for ch in ['red', 'green', 'blue']:
  255. grass.verbose(_("%s_%s") % (out, ch))
  256. grass.verbose(_("To visualize output, run: g.region -p raster=%s_red" % out))
  257. grass.verbose(_("d.rgb r=%s_red g=%s_green b=%s_blue" % (out, out, out)))
  258. grass.verbose(_("If desired, combine channels into a single RGB map with 'r.composite'."))
  259. grass.verbose(_("Channel colors can be rebalanced using i.colors.enhance."))
  260. # write cmd history:
  261. for ch in ['red', 'green', 'blue']:
  262. grass.raster_history("%s_%s" % (out, ch))
  263. # create a group with the three outputs
  264. #grass.run_command('i.group', group=out,
  265. # input="{n}_red,{n}_blue,{n}_green".format(n=out))
  266. # Cleanup
  267. grass.message(_("cleaning up temp files"))
  268. try:
  269. grass.run_command('g.remove', flags="f", type="raster",
  270. pattern="tmp%s*" % pid, quiet=True)
  271. except:
  272. ""
  273. def brovey(pan, ms1, ms2, ms3, out, pid, sproc):
  274. grass.verbose(_("Using Brovey algorithm"))
  275. # pan/intensity histogram matching using linear regression
  276. grass.message(_("Pan channel/intensity histogram matching using linear regression"))
  277. outname = 'tmp%s_pan1' % pid
  278. panmatch1 = matchhist(pan, ms1, outname)
  279. outname = 'tmp%s_pan2' % pid
  280. panmatch2 = matchhist(pan, ms2, outname)
  281. outname = 'tmp%s_pan3' % pid
  282. panmatch3 = matchhist(pan, ms3, outname)
  283. outr = '%s_red' % out
  284. outg = '%s_green' % out
  285. outb = '%s_blue' % out
  286. # calculate brovey transformation
  287. grass.message(_("Calculating Brovey transformation..."))
  288. if sproc:
  289. # serial processing
  290. e = '''eval(k = "$ms1" + "$ms2" + "$ms3")
  291. "$outr" = 1 * round("$ms3" * "$panmatch3" / k)
  292. "$outg" = 1 * round("$ms2" * "$panmatch2" / k)
  293. "$outb" = 1 * round("$ms1" * "$panmatch1" / k)'''
  294. grass.mapcalc(e, outr=outr, outg=outg, outb=outb,
  295. panmatch1=panmatch1, panmatch2=panmatch2,
  296. panmatch3=panmatch3, ms1=ms1, ms2=ms2, ms3=ms3,
  297. overwrite=True)
  298. else:
  299. # parallel processing
  300. pb = grass.mapcalc_start('%s_blue = 1 * round((%s * %s) / (%s + %s + %s))' %
  301. (out, ms1, panmatch1, ms1, ms2, ms3),
  302. overwrite=True)
  303. pg = grass.mapcalc_start('%s_green = 1 * round((%s * %s) / (%s + %s + %s))' %
  304. (out, ms2, panmatch2, ms1, ms2, ms3),
  305. overwrite=True)
  306. pr = grass.mapcalc_start('%s_red = 1 * round((%s * %s) / (%s + %s + %s))' %
  307. (out, ms3, panmatch3, ms1, ms2, ms3),
  308. overwrite=True)
  309. pb.wait(), pg.wait(), pr.wait()
  310. try:
  311. pb.terminate(), pg.terminate(), pr.terminate()
  312. except:
  313. ""
  314. # Cleanup
  315. try:
  316. grass.run_command('g.remove', flags='f', quiet=True, type='raster',
  317. name='%s,%s,%s' % (panmatch1, panmatch2, panmatch3))
  318. except:
  319. ""
  320. def ihs(pan, ms1, ms2, ms3, out, pid, sproc):
  321. grass.verbose(_("Using IHS<->RGB algorithm"))
  322. # transform RGB channels into IHS color space
  323. grass.message(_("Transforming to IHS color space..."))
  324. grass.run_command('i.rgb.his', overwrite=True,
  325. red=ms3,
  326. green=ms2,
  327. blue=ms1,
  328. hue="tmp%s_hue" % pid,
  329. intensity="tmp%s_int" % pid,
  330. saturation="tmp%s_sat" % pid)
  331. # pan/intensity histogram matching using linear regression
  332. target = "tmp%s_int" % pid
  333. outname = "tmp%s_pan_int" % pid
  334. panmatch = matchhist(pan, target, outname)
  335. # substitute pan for intensity channel and transform back to RGB color space
  336. grass.message(_("Transforming back to RGB color space and sharpening..."))
  337. grass.run_command('i.his.rgb', overwrite=True,
  338. hue="tmp%s_hue" % pid,
  339. intensity="%s" % panmatch,
  340. saturation="tmp%s_sat" % pid,
  341. red="%s_red" % out,
  342. green="%s_green" % out,
  343. blue="%s_blue" % out)
  344. # Cleanup
  345. try:
  346. grass.run_command('g.remove', flags='f', quiet=True, type='raster',
  347. name=panmatch)
  348. except:
  349. ""
  350. def pca(pan, ms1, ms2, ms3, out, pid, sproc):
  351. grass.verbose(_("Using PCA/inverse PCA algorithm"))
  352. grass.message(_("Creating PCA images and calculating eigenvectors..."))
  353. # initial PCA with RGB channels
  354. pca_out = grass.read_command('i.pca', quiet=True, rescale='0,0',
  355. input='%s,%s,%s' % (ms1, ms2, ms3),
  356. output='tmp%s.pca' % pid)
  357. if len(pca_out) < 1:
  358. grass.fatal(_("Input has no data. Check region settings."))
  359. b1evect = []
  360. b2evect = []
  361. b3evect = []
  362. for l in pca_out.replace('(', ',').replace(')', ',').splitlines():
  363. b1evect.append(float(l.split(',')[1]))
  364. b2evect.append(float(l.split(',')[2]))
  365. b3evect.append(float(l.split(',')[3]))
  366. # inverse PCA with hi res pan channel substituted for principal component 1
  367. pca1 = 'tmp%s.pca.1' % pid
  368. pca2 = 'tmp%s.pca.2' % pid
  369. pca3 = 'tmp%s.pca.3' % pid
  370. b1evect1 = b1evect[0]
  371. b1evect2 = b1evect[1]
  372. b1evect3 = b1evect[2]
  373. b2evect1 = b2evect[0]
  374. b2evect2 = b2evect[1]
  375. b2evect3 = b2evect[2]
  376. b3evect1 = b3evect[0]
  377. b3evect2 = b3evect[1]
  378. b3evect3 = b3evect[2]
  379. # Histogram matching
  380. outname = 'tmp%s_pan1' % pid
  381. panmatch1 = matchhist(pan, ms1, outname)
  382. outname = 'tmp%s_pan2' % pid
  383. panmatch2 = matchhist(pan, ms2, outname)
  384. outname = 'tmp%s_pan3' % pid
  385. panmatch3 = matchhist(pan, ms3, outname)
  386. grass.message(_("Performing inverse PCA ..."))
  387. # Get mean value of each channel
  388. stats1 = grass.parse_command("r.univar", map=ms1, flags='g',
  389. parse=(grass.parse_key_val,
  390. {'sep': '='}))
  391. stats2 = grass.parse_command("r.univar", map=ms2, flags='g',
  392. parse=(grass.parse_key_val,
  393. {'sep': '='}))
  394. stats3 = grass.parse_command("r.univar", map=ms3, flags='g',
  395. parse=(grass.parse_key_val,
  396. {'sep': '='}))
  397. b1mean = float(stats1['mean'])
  398. b2mean = float(stats2['mean'])
  399. b3mean = float(stats3['mean'])
  400. if sproc:
  401. # serial processing
  402. outr = '%s_red' % out
  403. outg = '%s_green' % out
  404. outb = '%s_blue' % out
  405. cmd1 = "$outb = 1 * round(($panmatch1 * $b1evect1) + ($pca2 * $b1evect2) + ($pca3 * $b1evect3) + $b1mean)"
  406. cmd2 = "$outg = 1 * round(($panmatch2 * $b2evect1) + ($pca2 * $b2evect2) + ($pca3 * $b2evect3) + $b2mean)"
  407. cmd3 = "$outr = 1 * round(($panmatch3 * $b3evect1) + ($pca2 * $b3evect2) + ($pca3 * $b3evect3) + $b3mean)"
  408. cmd = '\n'.join([cmd1, cmd2, cmd3])
  409. grass.mapcalc(cmd, outb=outb, outg=outg, outr=outr,
  410. panmatch1=panmatch1,
  411. panmatch2=panmatch2,
  412. panmatch3=panmatch3,
  413. pca2=pca2,
  414. pca3=pca3,
  415. b1evect1=b1evect1,
  416. b2evect1=b2evect1,
  417. b3evect1=b3evect1,
  418. b1evect2=b1evect2,
  419. b2evect2=b2evect2,
  420. b3evect2=b3evect2,
  421. b1evect3=b1evect3,
  422. b2evect3=b2evect3,
  423. b3evect3=b3evect3,
  424. b1mean=b1mean,
  425. b2mean=b2mean,
  426. b3mean=b3mean,
  427. overwrite=True)
  428. else:
  429. # parallel processing
  430. pb = grass.mapcalc_start('%s_blue = 1 * round((%s * %f) + (%s * %f) + (%s * %f) + %f)'
  431. % (out,
  432. panmatch1,
  433. b1evect1,
  434. pca2,
  435. b1evect2,
  436. pca3,
  437. b1evect3,
  438. b1mean),
  439. overwrite=True)
  440. pg = grass.mapcalc_start('%s_green = 1 * round((%s * %f) + (%s * %f) + (%s * %f) + %f)'
  441. % (out,
  442. panmatch2,
  443. b2evect1,
  444. pca2,
  445. b2evect2,
  446. pca3,
  447. b2evect3,
  448. b2mean),
  449. overwrite=True)
  450. pr = grass.mapcalc_start('%s_red = 1 * round((%s * %f) + (%s * %f) + (%s * %f) + %f)'
  451. % (out,
  452. panmatch3,
  453. b3evect1,
  454. pca2,
  455. b3evect2,
  456. pca3,
  457. b3evect3,
  458. b3mean),
  459. overwrite=True)
  460. pb.wait(), pg.wait(), pr.wait()
  461. try:
  462. pb.terminate(), pg.terminate(), pr.terminate()
  463. except:
  464. ""
  465. # Cleanup
  466. grass.run_command('g.remove', flags='f', quiet=True, type='raster',
  467. name='%s,%s,%s' % (panmatch1, panmatch2, panmatch3))
  468. def matchhist(original, target, matched):
  469. # pan/intensity histogram matching using numpy arrays
  470. grass.message(_("Histogram matching..."))
  471. # input images
  472. original = original.split('@')[0]
  473. target = target.split('@')[0]
  474. images = [original, target]
  475. # create a dictionary to hold arrays for each image
  476. arrays = {}
  477. for img in images:
  478. # calculate number of cells for each grey value for for each image
  479. stats_out = grass.pipe_command('r.stats', flags='cin', input=img,
  480. sep=':')
  481. stats = grass.decode(stats_out.communicate()[0]).split('\n')[:-1]
  482. stats_dict = dict(s.split(':', 1) for s in stats)
  483. total_cells = 0 # total non-null cells
  484. for j in stats_dict:
  485. stats_dict[j] = int(stats_dict[j])
  486. if j != '*':
  487. total_cells += stats_dict[j]
  488. if total_cells < 1:
  489. grass.fatal(_("Input has no data. Check region settings."))
  490. # Make a 2x256 structured array for each image with a
  491. # cumulative distribution function (CDF) for each grey value.
  492. # Grey value is the integer (i4) and cdf is float (f4).
  493. arrays[img] = np.zeros((256, ), dtype=('i4,f4'))
  494. cum_cells = 0 # cumulative total of cells for sum of current and all lower grey values
  495. for n in range(0, 256):
  496. if str(n) in stats_dict:
  497. num_cells = stats_dict[str(n)]
  498. else:
  499. num_cells = 0
  500. cum_cells += num_cells
  501. # cdf is the the number of cells at or below a given grey value
  502. # divided by the total number of cells
  503. cdf = float(cum_cells) / float(total_cells)
  504. # insert values into array
  505. arrays[img][n] = (n, cdf)
  506. # open file for reclass rules
  507. outfile = open(grass.tempfile(), 'w')
  508. for i in arrays[original]:
  509. # for each grey value and corresponding cdf value in original, find the
  510. # cdf value in target that is closest to the target cdf value
  511. difference_list = []
  512. for j in arrays[target]:
  513. # make a list of the difference between each original cdf value and
  514. # the target cdf value
  515. difference_list.append(abs(i[1] - j[1]))
  516. # get the smallest difference in the list
  517. min_difference = min(difference_list)
  518. for j in arrays[target]:
  519. # find the grey value in target that corresponds to the cdf
  520. # closest to the original cdf
  521. if j[1] <= i[1] + min_difference and j[1] >= i[1] - min_difference:
  522. # build a reclass rules file from the original grey value and
  523. # corresponding grey value from target
  524. out_line = "%d = %d\n" % (i[0], j[0])
  525. outfile.write(out_line)
  526. break
  527. outfile.close()
  528. # create reclass of target from reclass rules file
  529. result = grass.core.find_file(matched, element='cell')
  530. if result['fullname']:
  531. grass.run_command('g.remove', flags='f', quiet=True, type='raster',
  532. name=matched)
  533. grass.run_command('r.reclass', input=original, out=matched,
  534. rules=outfile.name)
  535. else:
  536. grass.run_command('r.reclass', input=original, out=matched,
  537. rules=outfile.name)
  538. # Cleanup
  539. # remove the rules file
  540. grass.try_remove(outfile.name)
  541. # return reclass of target with histogram that matches original
  542. return matched
  543. if __name__ == "__main__":
  544. options, flags = grass.parser()
  545. main()