123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- # MODULE: grass.jupyter.display
- #
- # AUTHOR(S): Vaclav Petras <wenzeslaus gmail com>
- #
- # PURPOSE: This module contains functions for non-interactive display
- # in Jupyter Notebooks
- #
- # COPYRIGHT: (C) 2021 Vaclav Petras, and by the GRASS Development Team
- #
- # This program is free software under the GNU General Public
- # License (>=v2). Read the file COPYING that comes with GRASS
- # for details.
- """Render 3D visualizations"""
- import os
- import tempfile
- import weakref
- import grass.script as gs
- from .display import GrassRenderer
- from .region import RegionManagerFor3D
- class Grass3dRenderer:
- """Creates and displays 3D visualization using GRASS GIS 3D rendering engine NVIZ.
- The 3D image is created using the *render* function which uses the *m.nviz.image*
- module in the background. Additional images can be
- placed on the image using the *overlay* attribute which is the 2D renderer, i.e.,
- has interface of the *GrassRenderer* class.
- Basic usage::
- >>> img = Grass3dRenderer()
- >>> img.render(elevation_map="elevation", color_map="elevation", perspective=20)
- >>> img.overlay.d_legend(raster="elevation", at=(60, 97, 87, 92))
- >>> img.show()
- For the OpenGL rendering with *m.nviz.image* to work, a display (screen) is needed.
- This is not guaranteed on headless systems such as continuous integration (CI) or
- Binder service(s). This class uses Xvfb and PyVirtualDisplay to support rendering
- in these environments.
- """
- def __init__(
- self,
- width: int = 600,
- height: int = 400,
- filename: str = None,
- mode: str = "fine",
- resolution_fine: int = 1,
- screen_backend: str = "auto",
- font: str = "sans",
- text_size: float = 12,
- renderer2d: str = "cairo",
- use_region: bool = False,
- saved_region: str = None,
- ):
- """Checks screen_backend and creates a temporary directory for rendering.
- :param width: width of image in pixels
- :param height: height of image in pixels
- :param filename: filename or path to save the resulting PNG image
- :param mode: 3D rendering mode (options: fine, coarse, both)
- :param resolution_fine: resolution multiplier for the fine mode
- :param screen_backend: backend for running the 3D rendering
- :param font: font to use in 2D rendering
- :param text_size: default text size in 2D rendering, usually overwritten
- :param renderer2d: GRASS 2D renderer driver (options: cairo, png)
- :param use_region: if True, use either current or provided saved region,
- else derive region from rendered layers
- :param saved_region: if name of saved_region is provided,
- this region is then used for rendering
- When *resolution_fine* is 1, rasters are used in the resolution according
- to the computational region as usual in GRASS GIS.
- Setting *resolution_fine* to values higher than one, causes rasters to
- be resampled to a coarser resolution (2 for twice as coarse than computational
- region resolution). This allows for fast rendering of large rasters without
- changing the computational region.
- By default (``screen_backend="auto"``), when
- pyvirtualdisplay Python package is present, the class assumes that it is
- running in a headless environment, so pyvirtualdisplay is used. When the
- package is not present, *m.nviz.image* is executed directly. When
- *screen_backend* is set to ``"pyvirtualdisplay"`` and the package cannot be
- imported, ValueError is raised. When *screen_backend* is set to ``"simple"``,
- *m.nviz.image* is executed directly. For other values of *screen_backend*,
- ValueError is raised.
- """
- self._width = width
- self._height = height
- self._mode = mode
- self._resolution_fine = resolution_fine
- # Temporary dir and files
- # Resource managed by weakref.finalize.
- self._tmpdir = (
- tempfile.TemporaryDirectory() # pylint: disable=consider-using-with
- )
- def cleanup(tmpdir):
- tmpdir.cleanup()
- weakref.finalize(self, cleanup, self._tmpdir)
- if filename:
- self._filename = filename
- else:
- self._filename = os.path.join(self._tmpdir.name, "map.png")
- # Screen backend
- try:
- # This tests availability of the module and needs to work even
- # when the package is not installed.
- # pylint: disable=import-outside-toplevel,unused-import
- import pyvirtualdisplay # noqa: F401
- pyvirtualdisplay_available = True
- except ImportError:
- pyvirtualdisplay_available = False
- if screen_backend == "auto" and pyvirtualdisplay_available:
- self._screen_backend = "pyvirtualdisplay"
- elif screen_backend == "auto":
- self._screen_backend = "simple"
- elif screen_backend == "pyvirtualdisplay" and not pyvirtualdisplay_available:
- raise ValueError(
- _(
- "Screen backend '{}' cannot be used "
- "because pyvirtualdisplay cannot be imported"
- ).format(screen_backend)
- )
- elif screen_backend in ["simple", "pyvirtualdisplay"]:
- self._screen_backend = screen_backend
- else:
- raise ValueError(
- _(
- "Screen backend '{}' does not exist. "
- "See documentation for the list of supported backends."
- ).format(screen_backend)
- )
- self.overlay = GrassRenderer(
- height=height,
- width=width,
- filename=self._filename,
- font=font,
- text_size=text_size,
- renderer=renderer2d,
- use_region=use_region,
- saved_region=saved_region,
- )
- # rendering region setting
- self._region_manager = RegionManagerFor3D(use_region, saved_region)
- @property
- def filename(self):
- """Filename or full path to the file with the resulting image.
- The value can be set during initialization. When the filename was not provided
- during initialization, a path to temporary file is returned. In that case, the
- file is guaranteed to exist as long as the object exists.
- """
- return self._filename
- @property
- def region_manager(self):
- """Region manager object"""
- return self._region_manager
- def render(self, **kwargs):
- """Run rendering using *m.nviz.image*.
- Keyword arguments are passed as parameters to the *m.nviz.image* module.
- Parameters set in constructor such as *mode* are used here unless another value
- is provided. Parameters related to size, file, and format are handled
- internally and will be ignored when passed here.
- Calling this function again, overwrites the previously rendered image,
- so typically, it is called only once.
- """
- module = "m.nviz.image"
- name = os.path.join(self._tmpdir.name, "nviz")
- ext = "tif"
- full_name = f"{name}.{ext}"
- kwargs["output"] = name
- kwargs["format"] = ext
- kwargs["size"] = (self._width, self._height)
- if "mode" not in kwargs:
- kwargs["mode"] = self._mode
- if "resolution_fine" not in kwargs:
- kwargs["resolution_fine"] = self._resolution_fine
- if self._screen_backend == "pyvirtualdisplay":
- import inspect # pylint: disable=import-outside-toplevel
- # This is imported only when needed and when the package is available,
- # but generally, it may not be available.
- # pylint: disable=import-outside-toplevel,import-error
- from pyvirtualdisplay import Display
- additional_kwargs = {}
- has_env_copy = False
- if "manage_global_env" in inspect.signature(Display).parameters:
- additional_kwargs["manage_global_env"] = False
- has_env_copy = True
- with Display(
- size=(self._width, self._height), **additional_kwargs
- ) as display:
- if has_env_copy:
- env = display.env()
- else:
- env = os.environ.copy()
- self._region_manager.set_region_from_command(env=env, **kwargs)
- self.overlay.region_manager.set_region_from_env(env)
- gs.run_command(module, env=env, **kwargs)
- else:
- env = os.environ.copy()
- self._region_manager.set_region_from_command(env=env, **kwargs)
- self.overlay.region_manager.set_region_from_env(env)
- gs.run_command(module, env=env, **kwargs)
- # Lazy import to avoid an import-time dependency on PIL.
- from PIL import Image # pylint: disable=import-outside-toplevel
- img = Image.open(full_name)
- img.save(self._filename)
- def show(self):
- """Displays a PNG image of map"""
- # Lazy import to avoid an import-time dependency on IPython.
- from IPython.display import Image # pylint: disable=import-outside-toplevel
- return Image(self._filename)
|