diff --git a/examples/image-3d.py b/examples/image-3d.py new file mode 100644 index 0000000..ff9109b --- /dev/null +++ b/examples/image-3d.py @@ -0,0 +1,76 @@ +import os +import matplotlib.image as mpl_img + +import gsp +from gsp_matplotlib import glm +from common.launcher import parse_args + +# import gsp +__dirname__ = os.path.dirname(os.path.abspath(__file__)) + +# Parse command line arguments +core, visual, render = parse_args() + +# Create a canvas and a viewport + +canvas = core.Canvas(256, 256, 100.0) +viewport = core.Viewport(canvas, 0, 0, 256, 256, [1, 1, 1, 1]) + +# Create a cube with paths + +cube_path_positions = glm.vec3(8) +cube_path_positions[...] = [ + (-1.0, -1.0, +1.0), + (+1.0, -1.0, +1.0), + (-1.0, +1.0, +1.0), + (+1.0, +1.0, +1.0), + (-1.0, -1.0, -1.0), + (+1.0, -1.0, -1.0), + (-1.0, +1.0, -1.0), + (+1.0, +1.0, -1.0), +] +cube_path_face_indices = [ + [0, 1], + [1, 3], + [3, 2], + [2, 0], + [4, 5], + [5, 7], + [7, 6], + [6, 4], + [0, 4], + [1, 5], + [2, 6], + [3, 7], +] + +colormap = gsp.transform.Colormap("gray", vmin=0.0, vmax=0.75) +depth = gsp.transform.Out("screen[paths].z") +paths_visual = visual.Paths( + cube_path_positions, + cube_path_face_indices, + line_colors=colormap(depth), + line_widths=5.0 * (1 - 1.25 * depth), + line_styles=gsp.core.LineStyle.solid, + line_joins=gsp.core.LineJoin.round, + line_caps=gsp.core.LineCap.round, +) +paths_visual.render(viewport) + +# Read the image_data numpy array from a file and create a texture + +image_path = f"{__dirname__}/images/UV_Grid_Sm.jpg" +image_data = mpl_img.imread(image_path) +texture = core.Texture(image_data, image_data.shape) + +# Create an image visual +image_visual = visual.Image( + positions=[[-1, 1, -1]], + texture_2d=texture, + image_extent=(-1, 1, -1, 1), +) +image_visual.render(viewport) + +# Show or save the result + +render(canvas, [viewport], [paths_visual, image_visual]) diff --git a/examples/images/UV_Grid_Sm.jpg b/examples/images/UV_Grid_Sm.jpg new file mode 100644 index 0000000..bcc6ee1 Binary files /dev/null and b/examples/images/UV_Grid_Sm.jpg differ diff --git a/gsp/core/__init__.py b/gsp/core/__init__.py index 67ca6fa..2390f37 100644 --- a/gsp/core/__init__.py +++ b/gsp/core/__init__.py @@ -2,11 +2,12 @@ # Authors: Nicolas P .Rougier # License: BSD 3 clause -from . data import Data -from . list import List -from . buffer import Buffer -from . canvas import Canvas -from . viewport import Viewport -from . types import Color, Marker, Measure -from . types import Matrix, Vec2, Vec3, Vec4 -from . types import LineCap, LineStyle, LineJoin +from .data import Data +from .list import List +from .buffer import Buffer +from .canvas import Canvas +from .viewport import Viewport +from .texture import Texture +from .types import Color, Marker, Measure +from .types import Matrix, Vec2, Vec3, Vec4 +from .types import LineCap, LineStyle, LineJoin diff --git a/gsp/core/texture.py b/gsp/core/texture.py new file mode 100644 index 0000000..e17fb95 --- /dev/null +++ b/gsp/core/texture.py @@ -0,0 +1,30 @@ +# Package: Graphic Server Protocol +# Authors: Nicolas P .Rougier +# License: BSD 3 clause +from __future__ import annotations + +from gsp import Object +from gsp.io.command import command +import numpy as np + + +class Texture(Object): + """ + A texture is a rectangular two-dimensional image that can be + applied to a surface in 3D space. + """ + + @command("core.Texture") + def __init__(self, texture_data: np.ndarray, shape: tuple): + """ + A texture is a rectangular two-dimensional image. + + Parameters + ---------- + + texture_data: + The image data of the texture. + shape: + The shape of the texture (height, width, channels). + """ + Object.__init__(self) diff --git a/gsp/visual/__init__.py b/gsp/visual/__init__.py index a24e2e7..ec15618 100644 --- a/gsp/visual/__init__.py +++ b/gsp/visual/__init__.py @@ -2,12 +2,13 @@ # Authors: Nicolas P .Rougier # License: BSD 3 clause -from . visual import Visual -from . pixels import Pixels -from . points import Points -from . markers import Markers -from . segments import Segments -from . paths import Paths -from . polygons import Polygons -# from . image import Image +from .visual import Visual +from .pixels import Pixels +from .points import Points +from .markers import Markers +from .segments import Segments +from .paths import Paths +from .polygons import Polygons +from .image import Image + # from . mesh import Mesh diff --git a/gsp/visual/image.py b/gsp/visual/image.py new file mode 100644 index 0000000..b098f72 --- /dev/null +++ b/gsp/visual/image.py @@ -0,0 +1,33 @@ +# Package: Graphic Server Protocol +# Authors: Nicolas P .Rougier +# License: BSD 3 clause + +import numpy as np +from gsp.visual import Visual +from gsp.core import Buffer, Color, Texture +from gsp.transform import Transform +from gsp.io.command import command + + +class Image(Visual): + + @command("visual.Image") + def __init__(self, positions: Transform | Buffer, texture_2d: Texture, image_extent: tuple): + + super().__init__() + + # These variables are available prior to rendering + self._in_variables = { + "positions": positions, + "texture_2d": texture_2d, + "image_extent": image_extent, + "viewport": None, + } + + # These variables exists only during rendering and are + # available on server side only. We have thus to make + # sure they are not tracked. + n = len(positions) + self._out_variables = { + "screen[positions]": np.empty((n, 3), np.float32), + } diff --git a/gsp_matplotlib/core/__init__.py b/gsp_matplotlib/core/__init__.py index 0b30ae8..8284da8 100644 --- a/gsp_matplotlib/core/__init__.py +++ b/gsp_matplotlib/core/__init__.py @@ -2,10 +2,12 @@ # Authors: Nicolas P .Rougier # License: BSD 3 clause -#from . data import Data -from . list import List -from . buffer import Buffer -from . canvas import Canvas -from . viewport import Viewport +# from . data import Data +from .list import List +from .buffer import Buffer +from .canvas import Canvas +from .viewport import Viewport +from .texture import Texture from gsp.core import Color, Marker, Measure -#from . types import LineCap, LineStyle, LineJoin + +# from . types import LineCap, LineStyle, LineJoin diff --git a/gsp_matplotlib/core/texture.py b/gsp_matplotlib/core/texture.py new file mode 100644 index 0000000..ace80f8 --- /dev/null +++ b/gsp_matplotlib/core/texture.py @@ -0,0 +1,26 @@ +# Package: Graphic Server Protocol / Matplotlib +# Authors: Nicolas P .Rougier +# License: BSD 3 clause +# from __future__ import annotations +import numpy as np +from gsp import core + + +class Texture(core.Texture): + + __doc__ = core.Texture.__doc__ + + def __init__(self, texture_data: np.ndarray, shape: tuple): + + super().__init__(texture_data=texture_data, shape=shape) + + self._texture_data = texture_data.flatten() + self._shape = shape + + @property + def data(self) -> np.ndarray: + return self._texture_data + + @property + def shape(self) -> tuple: + return self._shape diff --git a/gsp_matplotlib/visual/__init__.py b/gsp_matplotlib/visual/__init__.py index 4f8c89d..d4e82a7 100644 --- a/gsp_matplotlib/visual/__init__.py +++ b/gsp_matplotlib/visual/__init__.py @@ -2,11 +2,12 @@ # Authors: Nicolas P .Rougier # License: BSD 3 clause -from . pixels import Pixels -from . points import Points -from . markers import Markers -from . segments import Segments -from . paths import Paths -from . polygons import Polygons -# from . image import Image +from .pixels import Pixels +from .points import Points +from .markers import Markers +from .segments import Segments +from .paths import Paths +from .polygons import Polygons +from .image import Image + # from . mesh import Mesh diff --git a/gsp_matplotlib/visual/image.py b/gsp_matplotlib/visual/image.py new file mode 100644 index 0000000..a68cba5 --- /dev/null +++ b/gsp_matplotlib/visual/image.py @@ -0,0 +1,98 @@ +# Package: Graphic Server Protocol / Matplotlib +# Authors: Nicolas P .Rougier +# License: BSD 3 clause + +import numpy as np +from gsp import visual +from gsp.io.command import command +from gsp.transform import Transform +from gsp.core import Buffer, Color, Matrix +import matplotlib.image as mpl_img +from gsp_matplotlib import glm +from gsp_matplotlib.core.viewport import Viewport +from gsp_matplotlib.core.texture import Texture + + +class Image(visual.Image): + __doc__ = visual.Image.__doc__ + + @command("visual.Image") + def __init__( + self, + positions: Transform | Buffer, + texture_2d: Texture, + image_extent: tuple = (-1, 1, -1, 1), + ) -> None: + """ + Initialize an Image object. + + Parameters: + positions (Transform | Buffer): A (N, 3) array of XYZ positions in object space. + texture_2d (Texture): A Texture object containing the image to display. + image_extent (tuple): A tuple (left, right, bottom, top) defining the extent of the image in object space. + """ + + super().__init__(positions, texture_2d, image_extent, __no_command__=True) + + self._positions = positions + self._texture_2d = texture_2d + self._image_extent = image_extent + + def render( + self, + viewport: Viewport, + model: Matrix | None = None, + view: Matrix | None = None, + proj: Matrix | None = None, + ): + super().render(viewport, model, view, proj) + + model = model if model is not None else self._model + view = view if view is not None else self._view + proj = proj if proj is not None else self._proj + + # Disable tracking for newly created glm.ndarray (or else, + # this will create GSP buffers) + tracker = glm.ndarray.tracked.__tracker_class__ + glm.ndarray.tracked.__tracker_class__ = None + + # Create the collection if necessary + if viewport not in self._viewports: + axe_image = mpl_img.AxesImage( + viewport._axes, + data=self._texture_2d.data.reshape(self._texture_2d.shape), + ) + self._viewports[viewport] = axe_image + viewport._axes.add_image(axe_image) + + # This is necessary for measure transforms that need to be + # kept up to date with canvas size + canvas = viewport._canvas._figure.canvas + canvas.mpl_connect("resize_event", lambda event: self.render(viewport)) + + # If render has been called without model/view/proj, we don't + # render Such call is only used to declare that this visual is + # to be rendered on that viewport. + if self._transform is None: + # Restore tracking + glm.ndarray.tracked.__tracker_class__ = tracker + return + + axe_image: mpl_img.AxesImage = self._viewports[viewport] + positions4d = glm.to_vec4(self._positions) @ self._transform.T + positions3d = glm.to_vec3(positions4d) + # FIXME here image_extent is divided by W after rotation + # but there is nothing to compensate for the camera z + # - should i divide by the camera's zoom ? + projected_extent = ( + positions3d[0, 0] + self._image_extent[0] / positions4d[0, 3], + positions3d[0, 0] + self._image_extent[1] / positions4d[0, 3], + positions3d[0, 1] + self._image_extent[2] / positions4d[0, 3], + positions3d[0, 1] + self._image_extent[3] / positions4d[0, 3], + ) + axe_image.set_extent(projected_extent) + + self.set_variable("screen[positions]", positions3d) + + # Restore tracking + glm.ndarray.tracked.__tracker_class__ = tracker