r.fillnulls.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  1. #!/usr/bin/env python3
  2. #
  3. ############################################################################
  4. #
  5. # MODULE: r.fillnulls
  6. # AUTHOR(S): Markus Neteler
  7. # Updated to GRASS 5.7 by Michael Barton
  8. # Updated to GRASS 6.0 by Markus Neteler
  9. # Ring and zoom improvements by Hamish Bowman
  10. # Converted to Python by Glynn Clements
  11. # Added support for r.resamp.bspline by Luca Delucchi
  12. # Per hole filling with RST by Maris Nartiss
  13. # Speedup for per hole filling with RST by Stefan Blumentrath
  14. # PURPOSE: fills NULL (no data areas) in raster maps
  15. # The script respects a user mask (MASK) if present.
  16. #
  17. # COPYRIGHT: (C) 2001-2018 by the GRASS Development Team
  18. #
  19. # This program is free software under the GNU General Public
  20. # License (>=v2). Read the file COPYING that comes with GRASS
  21. # for details.
  22. #
  23. #############################################################################
  24. # %module
  25. # % description: Fills no-data areas in raster maps using spline interpolation.
  26. # % keyword: raster
  27. # % keyword: surface
  28. # % keyword: elevation
  29. # % keyword: interpolation
  30. # % keyword: splines
  31. # % keyword: no-data filling
  32. # %end
  33. # %option G_OPT_R_INPUT
  34. # %end
  35. # %option G_OPT_R_OUTPUT
  36. # %end
  37. # %option
  38. # % key: method
  39. # % type: string
  40. # % description: Interpolation method to use
  41. # % required: yes
  42. # % options: bilinear,bicubic,rst
  43. # % answer: rst
  44. # %end
  45. # %option
  46. # % key: tension
  47. # % type: double
  48. # % description: Spline tension parameter
  49. # % required : no
  50. # % answer : 40.
  51. # % guisection: RST options
  52. # %end
  53. # %option
  54. # % key: smooth
  55. # % type: double
  56. # % description: Spline smoothing parameter
  57. # % required : no
  58. # % answer : 0.1
  59. # % guisection: RST options
  60. # %end
  61. # %option
  62. # % key: edge
  63. # % type: integer
  64. # % description: Width of hole edge used for interpolation (in cells)
  65. # % required : no
  66. # % answer : 3
  67. # % options : 2-100
  68. # % guisection: RST options
  69. # %end
  70. # %option
  71. # % key: npmin
  72. # % type: integer
  73. # % description: Minimum number of points for approximation in a segment (>segmax)
  74. # % required : no
  75. # % answer : 600
  76. # % options : 2-10000
  77. # % guisection: RST options
  78. # %end
  79. # %option
  80. # % key: segmax
  81. # % type: integer
  82. # % description: Maximum number of points in a segment
  83. # % required : no
  84. # % answer : 300
  85. # % options : 2-10000
  86. # % guisection: RST options
  87. # %end
  88. # %option
  89. # % key: lambda
  90. # % type: double
  91. # % required: no
  92. # % multiple: no
  93. # % label: Tykhonov regularization parameter (affects smoothing)
  94. # % description: Used in bilinear and bicubic spline interpolation
  95. # % answer: 0.01
  96. # % guisection: Spline options
  97. # %end
  98. # %option G_OPT_MEMORYMB
  99. # %end
  100. import os
  101. import atexit
  102. import subprocess
  103. import grass.script as grass
  104. from grass.exceptions import CalledModuleError
  105. tmp_rmaps = list()
  106. tmp_vmaps = list()
  107. usermask = None
  108. mapset = None
  109. # what to do in case of user break:
  110. def cleanup():
  111. # delete internal mask and any TMP files:
  112. if len(tmp_vmaps) > 0:
  113. grass.run_command(
  114. "g.remove", quiet=True, flags="fb", type="vector", name=tmp_vmaps
  115. )
  116. if len(tmp_rmaps) > 0:
  117. grass.run_command(
  118. "g.remove", quiet=True, flags="fb", type="raster", name=tmp_rmaps
  119. )
  120. if usermask and mapset:
  121. if grass.find_file(usermask, mapset=mapset)["file"]:
  122. grass.run_command(
  123. "g.rename", quiet=True, raster=(usermask, "MASK"), overwrite=True
  124. )
  125. def main():
  126. global usermask, mapset, tmp_rmaps, tmp_vmaps
  127. input = options["input"]
  128. output = options["output"]
  129. tension = options["tension"]
  130. smooth = options["smooth"]
  131. method = options["method"]
  132. edge = int(options["edge"])
  133. segmax = int(options["segmax"])
  134. npmin = int(options["npmin"])
  135. lambda_ = float(options["lambda"])
  136. memory = options["memory"]
  137. quiet = True # FIXME
  138. mapset = grass.gisenv()["MAPSET"]
  139. unique = str(os.getpid()) # Shouldn't we use temp name?
  140. prefix = "r_fillnulls_%s_" % unique
  141. failed_list = (
  142. list()
  143. ) # a list of failed holes. Caused by issues with v.surf.rst. Connected with #1813
  144. # check if input file exists
  145. if not grass.find_file(input)["file"]:
  146. grass.fatal(_("Raster map <%s> not found") % input)
  147. # save original region
  148. reg_org = grass.region()
  149. # check if a MASK is already present
  150. # and remove it to not interfere with NULL lookup part
  151. # as we don't fill MASKed parts!
  152. if grass.find_file("MASK", mapset=mapset)["file"]:
  153. usermask = "usermask_mask." + unique
  154. grass.message(_("A user raster mask (MASK) is present. Saving it..."))
  155. grass.run_command("g.rename", quiet=quiet, raster=("MASK", usermask))
  156. # check if method is rst to use v.surf.rst
  157. if method == "rst":
  158. # idea: filter all NULLS and grow that area(s) by 3 pixel, then
  159. # interpolate from these surrounding 3 pixel edge
  160. filling = prefix + "filled"
  161. grass.use_temp_region()
  162. grass.run_command("g.region", align=input, quiet=quiet)
  163. region = grass.region()
  164. ns_res = region["nsres"]
  165. ew_res = region["ewres"]
  166. grass.message(_("Using RST interpolation..."))
  167. grass.message(_("Locating and isolating NULL areas..."))
  168. # creating binary (0/1) map
  169. if usermask:
  170. grass.message(_("Skipping masked raster parts"))
  171. grass.mapcalc(
  172. '$tmp1 = if(isnull("$input") && !($mask == 0 || isnull($mask)),1,null())',
  173. tmp1=prefix + "nulls",
  174. input=input,
  175. mask=usermask,
  176. )
  177. else:
  178. grass.mapcalc(
  179. '$tmp1 = if(isnull("$input"),1,null())',
  180. tmp1=prefix + "nulls",
  181. input=input,
  182. )
  183. tmp_rmaps.append(prefix + "nulls")
  184. # restoring user's mask, if present
  185. # to ignore MASKed original values
  186. if usermask:
  187. grass.message(_("Restoring user mask (MASK)..."))
  188. try:
  189. grass.run_command("g.rename", quiet=quiet, raster=(usermask, "MASK"))
  190. except CalledModuleError:
  191. grass.warning(_("Failed to restore user MASK!"))
  192. usermask = None
  193. # grow identified holes by X pixels
  194. grass.message(_("Growing NULL areas"))
  195. tmp_rmaps.append(prefix + "grown")
  196. try:
  197. grass.run_command(
  198. "r.grow",
  199. input=prefix + "nulls",
  200. radius=edge + 0.01,
  201. old=1,
  202. new=1,
  203. out=prefix + "grown",
  204. quiet=quiet,
  205. )
  206. except CalledModuleError:
  207. grass.fatal(
  208. _(
  209. "abandoned. Removing temporary map, restoring "
  210. "user mask if needed:"
  211. )
  212. )
  213. # assign unique IDs to each hole or hole system (holes closer than edge distance)
  214. grass.message(_("Assigning IDs to NULL areas"))
  215. tmp_rmaps.append(prefix + "clumped")
  216. try:
  217. grass.run_command(
  218. "r.clump",
  219. input=prefix + "grown",
  220. output=prefix + "clumped",
  221. quiet=quiet,
  222. )
  223. except CalledModuleError:
  224. grass.fatal(
  225. _(
  226. "abandoned. Removing temporary map, restoring "
  227. "user mask if needed:"
  228. )
  229. )
  230. # get a list of unique hole cat's
  231. grass.mapcalc(
  232. "$out = if(isnull($inp), null(), $clumped)",
  233. out=prefix + "holes",
  234. inp=prefix + "nulls",
  235. clumped=prefix + "clumped",
  236. )
  237. tmp_rmaps.append(prefix + "holes")
  238. # use new IDs to identify holes
  239. try:
  240. grass.run_command(
  241. "r.to.vect",
  242. flags="v",
  243. input=prefix + "holes",
  244. output=prefix + "holes",
  245. type="area",
  246. quiet=quiet,
  247. )
  248. except:
  249. grass.fatal(
  250. _(
  251. "abandoned. Removing temporary maps, restoring "
  252. "user mask if needed:"
  253. )
  254. )
  255. tmp_vmaps.append(prefix + "holes")
  256. # get a list of unique hole cat's
  257. cats_file_name = grass.tempfile(False)
  258. grass.run_command(
  259. "v.db.select",
  260. flags="c",
  261. map=prefix + "holes",
  262. columns="cat",
  263. file=cats_file_name,
  264. quiet=quiet,
  265. )
  266. cat_list = list()
  267. cats_file = open(cats_file_name)
  268. for line in cats_file:
  269. cat_list.append(line.rstrip("\n"))
  270. cats_file.close()
  271. os.remove(cats_file_name)
  272. if len(cat_list) < 1:
  273. grass.fatal(_("Input map has no holes. Check region settings."))
  274. # GTC Hole is NULL area in a raster map
  275. grass.message(_("Processing %d map holes") % len(cat_list))
  276. first = True
  277. hole_n = 1
  278. for cat in cat_list:
  279. holename = prefix + "hole_" + cat
  280. # GTC Hole is a NULL area in a raster map
  281. grass.message(_("Filling hole %s of %s") % (hole_n, len(cat_list)))
  282. hole_n = hole_n + 1
  283. # cut out only CAT hole for processing
  284. try:
  285. grass.run_command(
  286. "v.extract",
  287. input=prefix + "holes",
  288. output=holename + "_pol",
  289. cats=cat,
  290. quiet=quiet,
  291. )
  292. except CalledModuleError:
  293. grass.fatal(
  294. _(
  295. "abandoned. Removing temporary maps, restoring "
  296. "user mask if needed:"
  297. )
  298. )
  299. tmp_vmaps.append(holename + "_pol")
  300. # zoom to specific hole with a buffer of two cells around the hole to
  301. # remove rest of data
  302. try:
  303. grass.run_command(
  304. "g.region",
  305. vector=holename + "_pol",
  306. align=input,
  307. w="w-%d" % (edge * 2 * ew_res),
  308. e="e+%d" % (edge * 2 * ew_res),
  309. n="n+%d" % (edge * 2 * ns_res),
  310. s="s-%d" % (edge * 2 * ns_res),
  311. quiet=quiet,
  312. )
  313. except CalledModuleError:
  314. grass.fatal(
  315. _(
  316. "abandoned. Removing temporary maps, restoring "
  317. "user mask if needed:"
  318. )
  319. )
  320. # remove temporary map to not overfill disk
  321. try:
  322. grass.run_command(
  323. "g.remove",
  324. flags="fb",
  325. type="vector",
  326. name=holename + "_pol",
  327. quiet=quiet,
  328. )
  329. except CalledModuleError:
  330. grass.fatal(
  331. _(
  332. "abandoned. Removing temporary maps, restoring "
  333. "user mask if needed:"
  334. )
  335. )
  336. tmp_vmaps.remove(holename + "_pol")
  337. # copy only data around hole
  338. grass.mapcalc(
  339. "$out = if($inp == $catn, $inp, null())",
  340. out=holename,
  341. inp=prefix + "holes",
  342. catn=cat,
  343. )
  344. tmp_rmaps.append(holename)
  345. # If here loop is split into two, next part of loop can be run in parallel
  346. # (except final result patching)
  347. # Downside - on large maps such approach causes large disk usage
  348. # grow hole border to get it's edge area
  349. tmp_rmaps.append(holename + "_grown")
  350. try:
  351. grass.run_command(
  352. "r.grow",
  353. input=holename,
  354. radius=edge + 0.01,
  355. old=-1,
  356. out=holename + "_grown",
  357. quiet=quiet,
  358. )
  359. except CalledModuleError:
  360. grass.fatal(
  361. _(
  362. "abandoned. Removing temporary map, restoring "
  363. "user mask if needed:"
  364. )
  365. )
  366. # no idea why r.grow old=-1 doesn't replace existing values with NULL
  367. grass.mapcalc(
  368. '$out = if($inp == -1, null(), "$dem")',
  369. out=holename + "_edges",
  370. inp=holename + "_grown",
  371. dem=input,
  372. )
  373. tmp_rmaps.append(holename + "_edges")
  374. # convert to points for interpolation
  375. tmp_vmaps.append(holename)
  376. try:
  377. grass.run_command(
  378. "r.to.vect",
  379. input=holename + "_edges",
  380. output=holename,
  381. type="point",
  382. flags="z",
  383. quiet=quiet,
  384. )
  385. except CalledModuleError:
  386. grass.fatal(
  387. _(
  388. "abandoned. Removing temporary maps, restoring "
  389. "user mask if needed:"
  390. )
  391. )
  392. # count number of points to control segmax parameter for interpolation:
  393. pointsnumber = grass.vector_info_topo(map=holename)["points"]
  394. grass.verbose(_("Interpolating %d points") % pointsnumber)
  395. if pointsnumber < 2:
  396. grass.verbose(_("No points to interpolate"))
  397. failed_list.append(holename)
  398. continue
  399. # Avoid v.surf.rst warnings
  400. if pointsnumber < segmax:
  401. use_npmin = pointsnumber
  402. use_segmax = pointsnumber * 2
  403. else:
  404. use_npmin = npmin
  405. use_segmax = segmax
  406. # launch v.surf.rst
  407. tmp_rmaps.append(holename + "_dem")
  408. try:
  409. grass.run_command(
  410. "v.surf.rst",
  411. quiet=quiet,
  412. input=holename,
  413. elev=holename + "_dem",
  414. tension=tension,
  415. smooth=smooth,
  416. segmax=use_segmax,
  417. npmin=use_npmin,
  418. )
  419. except CalledModuleError:
  420. # GTC Hole is NULL area in a raster map
  421. grass.fatal(_("Failed to fill hole %s") % cat)
  422. # v.surf.rst sometimes fails with exit code 0
  423. # related bug #1813
  424. if not grass.find_file(holename + "_dem")["file"]:
  425. try:
  426. tmp_rmaps.remove(holename)
  427. tmp_rmaps.remove(holename + "_grown")
  428. tmp_rmaps.remove(holename + "_edges")
  429. tmp_rmaps.remove(holename + "_dem")
  430. tmp_vmaps.remove(holename)
  431. except:
  432. pass
  433. grass.warning(
  434. _(
  435. "Filling has failed silently. Leaving temporary maps "
  436. "with prefix <%s> for debugging."
  437. )
  438. % holename
  439. )
  440. failed_list.append(holename)
  441. continue
  442. # append hole result to interpolated version later used to patch into original DEM
  443. if first:
  444. tmp_rmaps.append(filling)
  445. grass.run_command(
  446. "g.region", align=input, raster=holename + "_dem", quiet=quiet
  447. )
  448. grass.mapcalc(
  449. "$out = if(isnull($inp), null(), $dem)",
  450. out=filling,
  451. inp=holename,
  452. dem=holename + "_dem",
  453. )
  454. first = False
  455. else:
  456. tmp_rmaps.append(filling + "_tmp")
  457. grass.run_command(
  458. "g.region",
  459. align=input,
  460. raster=(filling, holename + "_dem"),
  461. quiet=quiet,
  462. )
  463. grass.mapcalc(
  464. "$out = if(isnull($inp), if(isnull($fill), null(), $fill), $dem)",
  465. out=filling + "_tmp",
  466. inp=holename,
  467. dem=holename + "_dem",
  468. fill=filling,
  469. )
  470. try:
  471. grass.run_command(
  472. "g.rename",
  473. raster=(filling + "_tmp", filling),
  474. overwrite=True,
  475. quiet=quiet,
  476. )
  477. except CalledModuleError:
  478. grass.fatal(
  479. _(
  480. "abandoned. Removing temporary maps, restoring user "
  481. "mask if needed:"
  482. )
  483. )
  484. # this map has been removed. No need for later cleanup.
  485. tmp_rmaps.remove(filling + "_tmp")
  486. # remove temporary maps to not overfill disk
  487. try:
  488. tmp_rmaps.remove(holename)
  489. tmp_rmaps.remove(holename + "_grown")
  490. tmp_rmaps.remove(holename + "_edges")
  491. tmp_rmaps.remove(holename + "_dem")
  492. except:
  493. pass
  494. try:
  495. grass.run_command(
  496. "g.remove",
  497. quiet=quiet,
  498. flags="fb",
  499. type="raster",
  500. name=(
  501. holename,
  502. holename + "_grown",
  503. holename + "_edges",
  504. holename + "_dem",
  505. ),
  506. )
  507. except CalledModuleError:
  508. grass.fatal(
  509. _(
  510. "abandoned. Removing temporary maps, restoring "
  511. "user mask if needed:"
  512. )
  513. )
  514. try:
  515. tmp_vmaps.remove(holename)
  516. except:
  517. pass
  518. try:
  519. grass.run_command(
  520. "g.remove", quiet=quiet, flags="fb", type="vector", name=holename
  521. )
  522. except CalledModuleError:
  523. grass.fatal(
  524. _(
  525. "abandoned. Removing temporary maps, restoring user mask if needed:"
  526. )
  527. )
  528. # check if method is different from rst to use r.resamp.bspline
  529. if method != "rst":
  530. grass.message(_("Using %s bspline interpolation") % method)
  531. # clone current region
  532. grass.use_temp_region()
  533. grass.run_command("g.region", align=input)
  534. reg = grass.region()
  535. # launch r.resamp.bspline
  536. tmp_rmaps.append(prefix + "filled")
  537. # If there are no NULL cells, r.resamp.bslpine call
  538. # will end with an error although for our needs it's fine
  539. # Only problem - this state must be read from stderr
  540. new_env = dict(os.environ)
  541. new_env["LC_ALL"] = "C"
  542. if usermask:
  543. try:
  544. p = grass.core.start_command(
  545. "r.resamp.bspline",
  546. input=input,
  547. mask=usermask,
  548. output=prefix + "filled",
  549. method=method,
  550. ew_step=3 * reg["ewres"],
  551. ns_step=3 * reg["nsres"],
  552. lambda_=lambda_,
  553. memory=memory,
  554. flags="n",
  555. stderr=subprocess.PIPE,
  556. env=new_env,
  557. )
  558. stderr = grass.decode(p.communicate()[1])
  559. if "No NULL cells found" in stderr:
  560. grass.run_command(
  561. "g.copy", raster="%s,%sfilled" % (input, prefix), overwrite=True
  562. )
  563. p.returncode = 0
  564. grass.warning(
  565. _(
  566. "Input map <%s> has no holes. Copying to output without modification."
  567. )
  568. % (input,)
  569. )
  570. except CalledModuleError:
  571. grass.fatal(
  572. _("Failure during bspline interpolation. Error message: %s")
  573. % stderr
  574. )
  575. else:
  576. try:
  577. p = grass.core.start_command(
  578. "r.resamp.bspline",
  579. input=input,
  580. output=prefix + "filled",
  581. method=method,
  582. ew_step=3 * reg["ewres"],
  583. ns_step=3 * reg["nsres"],
  584. lambda_=lambda_,
  585. memory=memory,
  586. flags="n",
  587. stderr=subprocess.PIPE,
  588. env=new_env,
  589. )
  590. stderr = grass.decode(p.communicate()[1])
  591. if "No NULL cells found" in stderr:
  592. grass.run_command(
  593. "g.copy", raster="%s,%sfilled" % (input, prefix), overwrite=True
  594. )
  595. p.returncode = 0
  596. grass.warning(
  597. _(
  598. "Input map <%s> has no holes. Copying to output without modification."
  599. )
  600. % (input,)
  601. )
  602. except CalledModuleError:
  603. grass.fatal(
  604. _("Failure during bspline interpolation. Error message: %s")
  605. % stderr
  606. )
  607. # restoring user's mask, if present:
  608. if usermask:
  609. grass.message(_("Restoring user mask (MASK)..."))
  610. try:
  611. grass.run_command("g.rename", quiet=quiet, raster=(usermask, "MASK"))
  612. except CalledModuleError:
  613. grass.warning(_("Failed to restore user MASK!"))
  614. usermask = None
  615. # set region to original extents, align to input
  616. grass.run_command(
  617. "g.region",
  618. n=reg_org["n"],
  619. s=reg_org["s"],
  620. e=reg_org["e"],
  621. w=reg_org["w"],
  622. align=input,
  623. )
  624. # patch orig and fill map
  625. grass.message(_("Patching fill data into NULL areas..."))
  626. # we can use --o here as g.parser already checks on startup
  627. grass.run_command(
  628. "r.patch", input=(input, prefix + "filled"), output=output, overwrite=True
  629. )
  630. # restore the real region
  631. grass.del_temp_region()
  632. grass.message(_("Filled raster map is: %s") % output)
  633. # write cmd history:
  634. grass.raster_history(output)
  635. if len(failed_list) > 0:
  636. grass.warning(
  637. _(
  638. "Following holes where not filled. Temporary maps with are left "
  639. "in place to allow examination of unfilled holes"
  640. )
  641. )
  642. outlist = failed_list[0]
  643. for hole in failed_list[1:]:
  644. outlist = ", " + outlist
  645. grass.message(outlist)
  646. grass.message(_("Done."))
  647. if __name__ == "__main__":
  648. options, flags = grass.parser()
  649. atexit.register(cleanup)
  650. main()