DGtal  1.5.beta
Display3D: a stream mechanism for displaying 3D DGtal objects

This part of the manual describes how to visualize 3D objects and how to import them from binary file (.obj or pgm3d)

Author
Bertrand Kerautret, Martial Tola, Aline Martin, David Coeurjolly

Display3D: a stream mechanism from abstract class Display3D

The semi abstract template class Display3D defines the stream mechanism to display 3d primitive (like PointVector, DigitalSetBySTLSet, Object ...). The class Viewer3D, Board3D and Board3DTo2D implement two different ways to display 3D objects. The first one (Viewer3D), permits an interactive visualization (based on OpenGL). The second (Board3D) provides mechanism to export the 3D objects to and Wavefront OBJ format. The last one (Board3DTo2D) provides 3D visualization to 2D vector format (using a projection mechanism based on the CAIRO library).

Display3D have two template parameters which correspond to the digital space and the Khalimsky space used to put the figures. From the Digital Space and Khalimsky Space, we use the associated embedding mechanism to convert digital objects to structures in \( \mathbb{R}^n\).

Viewer3D and Board3DTo2D allow to set and change the camera point of view for the visualization.

Interactive visualization from Viewer3D

The class Viewer3D inherits from the base class QGLViewer (which is based on QGLwidget). It permits to display simple 3D shapes. LibQGLViewer ( http://www.libqglviewer.com ) is a C++ library based on QT allowing to access to simple 3D features like camera moving, mouse, keyboard interaction, clipping plane .... etc.

It possess the additional functionality to display 2D slice image from a volume one.

First to use the Viewer3D stream, you need to include the following headers:

#include "DGtal/io/3dViewers/Viewer3D.h"

The following code snippet defines three points and a rectangular domain in Z3. It then displays them in a Viewer3D object. The full code is in viewer3D-1-points.cpp.

The first step to visualize 3D object with Viewer3D is to create a QApplication from the main():

using namespace DGtal;
using namespace Z3i;
QApplication application(argc,argv);
Viewer3D<> viewer;
viewer.show();
DGtal is the top-level namespace which contains all DGtal functions and types.

Then we can display some 3D primitives:

Point p1( 0, 0, 0 );
Point p2( 5, 5 ,5 );
Point p3( 2, 3, 4 );
Domain domain( p1, p2 );
viewer << domain;
viewer << p1 << p2 << p3;
viewer << Viewer3D<>::updateDisplay;
MyPointD Point
Definition: testClone2.cpp:383
Domain domain
HyperRectDomain< Space > Domain

You should obtain the following visualization:

Digital point visualization with Viewer3D.

Interactive change of rendering mode <br>

By default the rendering mode of the Viewer3D is defined by melange of diffuse (Lambertian) and specular parts. The user can swith to the following mode by using the key P :

type default mixte metallic plastic lambertian
mode 0 1 2 3
Ex:

Light source position modes

The light source position is by default defined according to the camera position and its position will not change even after camera moves (default mode). By this way, if you move the camera, the object will be always illuminated (see images of mode 0 of the following tabular). This default light position could be changed interactively by a mouse move in the Viewer3D (with the key SHIFT+CTRL (SHIFT+CMD on mac)).

There exists a second mode where the light source position will be fixed according the main scene axis. In this case, even if the camera move, the light source will have the same position towards the object of the scene (see for instance images of mode 1 of the following tabular). As for the previous mode, the default light position could be changed interactively by a mouse move in the Viewer3D (with the key SHIFT+CTRL (SHIFT+CMD on mac)).

To change between the two light position mode you can use the Key P in the viewer3D.

Light position mode default position after camera move
0
1
Note
Note that you can display the camera settings in the console (key C) which can be used in Board2Dto3D described in the following.

Ball display modes

The Viewer3D class has a special mode to display balls (added from the Display3D method addBall() ). By default, the balls are constructed with OpenGl quadrangulated sphere which can be slow if the number of ball is huge. In the latter case, it is possible to use OpenGL points to increase display performance. To use this mode, you have just to activate it with the method setUseGLPointForBalls() when needed:

viewer.setUseGLPointForBalls(true);
viewer.addBall(Z3i::RealPoint(10.3, 11.3, 4.0);
Space::RealPoint RealPoint
Definition: StdDefs.h:170

By changing the mode you will obtain such display:

ball display default mode ball display with OpenGL point mode

You can also change the ball display mode interactively by using the key O.

Alternative visualization without QGLViewer dependency

There are two ways to obtain an alternative visualization without the dependency of the 3D interactive viewer:

  • Static display by using Board2Dto3D.
  • Export the 3d objects into 3d files (OBJ format) with Board3D.

Static display

The same visualization can be obtain with the Board2Dto3D class. You just need to adapt the camera settings (see example io/boards/dgtalBoard3DTo2D-1-points.cpp).

Board3DTo2D<Space, KSpace> board;
board << domain;
board << p1 << p2 << p3;
board << CameraPosition(2.500000, 2.500000, 16.078199)
<< CameraDirection(0.000000, 0.000000, -1.000000)
<< CameraUpVector(0.000000, 1.000000, 0.000000);
board << CameraZNearFar(4.578200, 22.578199);
board << SetMode3D(board.className(), "WireFrameMode");
board.saveCairo("dgtalBoard3DTo2D-1-points.png", Board3DTo2D<Space, KSpace>::CairoPNG, 600*2, 400*2);

This example should provides a comparable visualization.

Export objects with Board3D

To export 3d objects into OBJ format you need simply to use the Board3D class which inherits to the Display3D class. You can for instance follow these steps:

Board3D<> board;
board << SetMode3D(domain.className(), "Paving");
board << p1 << p2 << p3;
board << shape_set;
board.saveOBJ("dgtalBoard3D-1-points.obj");

And then visualize the resulting obj and mtl file by using for instance blender:

Visualisation of exported OBJ file with blender.
Advanced:
By setting a second parameter to true when calling the saveOBJ, the geometrical objects will be scaled so that they fit in a [-1/2, 1/2]^3 domain.
Note
You do not need a Board3D to export an OBJ file. There are custom methods to export OBJ files for some classes:

Visualization of DigitalSet and digital objects

The Viewer3D class allows also to display directly a DigitalSet. The first step is to create a DigitalSet for example from the Shape class.

QApplication application(argc,argv);
typedef Viewer3D<> MyViewer;
MyViewer viewer;
viewer.show();
Point p1( 0, 0, 0 );
Point p2( 10, 10 , 10 );
Domain domain( p1, p2 );
viewer << domain;
DigitalSet shape_set( domain );
Shapes<Domain>::addNorm1Ball( shape_set, Point( 5, 5, 5 ), 2 );
Shapes<Domain>::addNorm2Ball( shape_set, Point( 3, 3, 3 ), 2 );
shape_set.erase(Point(3,3,3));
shape_set.erase(Point(6,6,6));
viewer << shape_set << MyViewer::updateDisplay;
static void addNorm1Ball(TDigitalSet &aSet, const Point &aCenter, UnsignedInteger aRadius)
static void addNorm2Ball(TDigitalSet &aSet, const Point &aCenter, UnsignedInteger aRadius)
Z2i::DigitalSet DigitalSet

You should obtain the following visualization (see example: viewer3D-2-sets.cpp ):

Digital point visualization with Viewer3D.

Mode selection: the example of digital objects in 3D

As for Board2D, a mode can be choosen to display elements (SetMode3D). You just have to specify the classname (the easiest way is to call the method className() on an instance of the correct type and the desired mode (a string).

Object6_18 shape( dt6_18, shape_set );
viewer << SetMode3D( shape.className(), "DrawAdjacencies" );
viewer << shape;
Object< DT6_18, DigitalSet > Object6_18
Definition: StdDefs.h:174

or change the couple of adjacency

Object18_6 shape2( dt18_6, shape_set );
viewer << SetMode3D( shape2.className(), "DrawAdjacencies" );
viewer << shape2;
Object< DT18_6, DigitalSet > Object18_6
Definition: StdDefs.h:178

You should obtain the two following visualizations (see example: viewer3D-3-objects.cpp ):

6-18 digital Adjacencies visualization with Viewer3D.
18-6 digital Adjacencies visualization with Viewer3D.

Note that digital set was displayed with transparency by setting a custom colors.

Useful modes for several 3D drawable elements

Listing of different modes

As for Board2D the object can be displayed with different possible mode:

Note that for KhalimskyCell and SignedKhalimskyCell the default colors (with CustomColors3D objects) can be changed only with the empty mode ("") and the "IllustrationCustomColor" mode.

"*": partially for (Board3DTo2D), see issue 582. "**": only for Viewer3D.

Examples with Objet modes

The file viewer3D-4-modes.cpp illustrates several possible modes to display these objects:

We can display the set of point and the domain

Point p1( -1, -1, -2 );
Point p2( 2, 2, 3 );
Domain domain( p1, p2 );
Point p3( 1, 1, 1 );
Point p4( 2, -1, 3 );
Point p5( -1, 2, 3 );
Point p6( 0, 0, 0 );
Point p0( 0, 2, 1 );

without mode change (see image (a)):

viewer << p1 << p2 << p3<< p4<< p5 << p6 << p0;
viewer << domain;

We can change the mode for displaying the domain (see image (b)):

viewer << p1 << p2 << p3<< p4<< p5 << p6 << p0;
viewer << SetMode3D(domain.className(), "PavingGrids");
viewer << domain;

(Note that to avoid transparency displaying artifacts, we need to display the domain after the voxel elements included in the domain)

It is also possible to change the mode for displaying the voxels: (see image (c))

viewer << domain;
viewer << SetMode3D( p1.className(), "Grid" );
viewer << p1 << p2 << p3<< p4<< p5 << p6 << p0;

we obtain the following visualizations:

(a) Default visualization of a digital point sets with the associated domain
(b) visualization using Paving mode for the domain.
(c) visualization using Paving mode for the voxels.

Illustrating KhalimskyCell with the "Illustration" mode

The "Illustration" mode is defined to construct illustrations composed of KhalimskyCell. In particular it permits to increase the space between cells and improve the display visibility. It can be used typically as follows: First you need to add the following header:

#include "DGtal/io/DrawWithDisplay3DModifier.h"

From a SignedKhalimskyCell (SCell in DGtal::Z3i) you have to select the "Illustration" mode :

SCell v = K.sSpel( Point( 0, 0, 0 ), KSpace::POS ); // +v
viewer << SetMode3D( v.className(), "Illustration" );
Z3i::SCell SCell
KSpace K

Then, to display a surfel with its associated voxel, you need to transform the surfel by constructing a shifted and resized version (DGtal::TransformedKSSurfel) according to its associated voxel:

SCell sx = K.sIncident( v, 0, true ); // surfel further along x
class to modify the position and scale to construct better illustration mode.

You will obtain such type of illustration (obtained from the example viewer3D-4bis-illustrationMode.cpp ).

Illustration of the "Illustration" KhalimskyCell mode.
Advanced:
There exists a specific method to display surfels (Khalimsky cells of dimension 2 in a space of dimension 3) as quadrilaterals where the user can prescribe a unitary normal vector. In Viewer3D, the normal vector is used in the rendering process (useful to check the geometrical consistency of a normal vector field). Basic usage is:
Display3DFactory<Space,KSpace>::drawOrientedSurfelWithNormal( aViewer, aSurfel, theSurfelSign, aNormalVector);
static void drawOrientedSurfelWithNormal(Display &display, const typename KSpace::SCell &aSignedCell, const RealVector &aNormal, const bool enableDoubleFace=false)

or if the surfel is not oriented (unsigned khalimsky cell).

static void drawUnorientedSurfelWithNormal(Display &display, const typename KSpace::Cell &anObject, const RealVector &aNormal, const bool enableDoubleFace=false)

In the later case, the quadrilateral vertices are oriented such that the dot product between the normal vector and the quad canonical normal vector is positive. Finally, these two methods accept a last boolean parameter such that if true, the quad is geometrically duplicated with opposite normal vector (double-quad rendering).

Changing the style for displaying drawable elements.

As for Board2D, it is possible to custom the way to display 3D elements by using an instance of the following classes:

  • CustomColors3D: to change the color used to display surface primitive (GL_QUADS) and the pen color (LINE/POINTS) ;

The custom color can be applied by an instance of the CustomColors3D as follow:

viewer << CustomColors3D(Color(250, 0,0),Color(250, 0,0));
viewer << p4 << p5 ;

The example viewer3D-5-custom.cpp illustrates some possible customs :

Example of several custom display .

Adding clipping planes

It also possible through the stream mechanism to add clipping plane with the object ClippingPlane. We just have to add the real plane equation and adding as for displaying an element. The file viewer3D-6-clipping.cpp gives a simple example.

From displaying a digital set defined from a Norm2 ball,

Point p1( 0, 0, 0 );
Point p2( 20, 20, 20 );
Domain domain(p1, p2);
DigitalSet shape_set( domain );
Shapes<Domain>::addNorm2Ball( shape_set, Point( 10, 10, 10 ), 7 );
viewer << SetMode3D( shape_set.className(), "Both" );
viewer << shape_set;
viewer << CustomColors3D(Color(250, 200,0, 100),Color(250, 200,0, 20));
viewer << SetMode3D( p1.className(), "Paving" );

we can add for instance two differents clipping planes:

viewer << ClippingPlane(1,0,0,-4.9);
viewer << ClippingPlane(0,1,0.3,-10);
(a) visualization of the initial set.
(b) visualization after adding the first clipping plane (0,1,0.3,-10).
(c) visualization after adding a second clipping plane (1,0,0,-4.9) .

It also possible to remove the visualization of the transparent clipping plane by adding boolean option:

viewer << ClippingPlane(0,1,0.3,-10, false);

Adding 2D image visualization in 3D

Adding 2D slice images

With the Viewer3D class it is possible to display 2D slice image from a volume one. It can be done in few steps (see example of io/viewers/viewer3D-8-2DSliceImages.cpp) :

// Extracting the 2D images from the 3D one and from a given dimension.
// First image the teenth Z slice (dim=2)
Image3D::Value, DGtal::functors::Identity > MySliceImageAdapter;
// Define the functor to recover a 2D domain from the 3D one in the Z direction (2):
DGtal::functors::Projector<DGtal::Z2i::Space> transTo2DdomainFunctorZ; transTo2DdomainFunctorZ.initRemoveOneDim(2);
DGtal::Z2i::Domain domain2DZ(transTo2DdomainFunctorZ(imageVol.domain().lowerBound()),
transTo2DdomainFunctorZ(imageVol.domain().upperBound()));
// Define the functor to associate 2D coordinates to the 3D one by giving the direction Z (2) and the slide numnber (10):
DGtal::functors::Projector<DGtal::Z3i::Space> aSliceFunctorZ(10); aSliceFunctorZ.initAddOneDim(2);
// We can now obtain the slice image (a ConstImageAdapter):
const auto identityFunctor = DGtal::functors::Identity();
MySliceImageAdapter aSliceImageZ(imageVol, domain2DZ, aSliceFunctorZ, identityFunctor );
// Second image the fiftieth Y slice (dim=1)
// Define the functor to recover a 2D domain from the 3D one in the Y direction (1):
DGtal::functors::Projector<DGtal::Z2i::Space> transTo2DdomainFunctorY; transTo2DdomainFunctorY.initRemoveOneDim(1);
DGtal::Z2i::Domain domain2DY(transTo2DdomainFunctorY(imageVol.domain().lowerBound()),
transTo2DdomainFunctorY(imageVol.domain().upperBound()));
// Define the functor to associate 2D coordinates to the 3D one by giving the direction Y (1) and the slide numnber (50):
DGtal::functors::Projector<DGtal::Z3i::Space> aSliceFunctorY(50); aSliceFunctorY.initAddOneDim(1);
// We can now obtain the slice image (a ConstImageAdapter):
MySliceImageAdapter aSliceImageY(imageVol, domain2DY, aSliceFunctorY, identityFunctor );
Aim: implements a const image adapter with a given domain (i.e. a subdomain) and 2 functors : g for d...
Aim: Define a simple default functor that just returns its argument.
Aim: Functor that maps a point P of dimension i to a point Q of dimension j. The member myDims is an ...
void initRemoveOneDim(const Dimension &dimRemoved)

And the display them using the classic stream operator:

viewer << aSliceImageZ;
viewer << aSliceImageY;

Finally you can adjust the image setting with the Display3DModifier UpdateImagePosition and UpdateImageData object:

viewer << DGtal::UpdateImagePosition<Z3i::Space, Z3i::KSpace>(1, MyViewer::yDirection, 0.0, 50.0, 0.0);
viewer << DGtal::UpdateImageData<MySliceImageAdapter>(0, aSliceImageZ, 0, 0, 10);
viewer << MyViewer::updateDisplay;

You will obtain such a visualization:

Illustration of the 2D image slice visualization.

You can also change the default mode by using:

viewer << SetMode3D(aSliceImageZ.className(), "BoundingBox");
viewer << MyViewer::updateDisplay;

and by changing the "BoundingBox" mode by "InterGrid" you will obtain the following visualization:

Illustration of the 2D image slice visualization with InterGrid mode.

See more details on this example io/viewers/viewer3D-8-2DSliceImages.cpp or from the DGtalTools repository with DGtalTools/visualization/3dImageViewer.cpp viewer.

Adding 2D images (from any embedding)

The slice images are not the only way to display 2D images in 3D. A 2D image can also be extracted and embedded in 3D by using a single embedding functor (Point2DEmbedderIn3D). The example io/viewers/viewer3D-8bis-2Dimages.cpp illustrates such a display.

First we need to add the header file associated with the Point2DEmbedderIn3D:

#include "DGtal/kernel/BasicPointFunctors.h"

Then, the type definition of ConstImageAdapter is added:

The resulting 2D domain can be deduced from the width used in the functor:

DGtal::Z3i::Point ptCenter(50, 62, 28);
const int IMAGE_PATCH_WIDTH = 20;
// Setting the image domain of the resulting image to be displayed in 3D:
const int IMAGE_PATCH_WIDTH

The embedder then be used to extract the image:

// Extracting images from 3D embeder
ptCenter+DGtal::Z3i::Point(static_cast<int>(200.0*cos(alpha)),static_cast<int>(100.0*sin(alpha))),
DGtal::Z3i::RealPoint(cos(alpha),sin(alpha),cos(2.0*alpha)),
ImageAdapterExtractor extractedImage(imageVol, domainImage2D, embedder, idV);
Aim: Functor that embeds a 2D point into a 3D space from two axis vectors and an origin point given i...

and used to display the image with the correct coordinates:

//Display image and update its position with embeder
viewer << extractedImage;
viewer << DGtal::UpdateImage3DEmbedding<Z3i::Space, Z3i::KSpace>(pos,
embedder(Z2i::RealPoint(0,0)),
embedder(domainImage2D.upperBound()),
Space::RealPoint RealPoint
Definition: StdDefs.h:97

This example will produce such a visualization:

Illustration of the 2D image visualization.

Adding 3D image visualization

In the same way a 3D image can be displayed. By following the same stream operator you will obtain such example of display:

Example of 3D image visualization with also digital sets.

See more details in the example: io/viewers/viewer3D-9-3Dimages.cpp

Customizing Slice Image visualization

By default an image is displayed in gray scale levels from its scalar values. However it is possible to display color texture image by using the object AddTextureImage2DWithFunctor or AddTextureImage3DWithFunctor (of the DrawWithDisplay3DModifier class) with the RGBmode which allows to interpret the scalar as a color value. A color functor can also be specified to generate a given color.

For instance the previous examples can easily displayed with color map:

First we generate a color functor to generate unsigned integer interpreted as RGB color:

  • Adding the header to use hueShadeMap:
    #include "DGtal/io/DrawWithDisplay3DModifier.h"
    #include "DGtal/io/viewers/DrawWithViewer3DModifier.h"
    #include "DGtal/io/colormaps/HueShadeColorMap.h"
    #include "DGtal/io/Color.h"
    with a functor to transform integer representing grayscale to integer representing color:
    struct hueFct{
    inline
    unsigned int operator() (unsigned char aVal) const
    {
    HueShadeColorMap<unsigned char> hueShade(0,255);
    Color col = hueShade((unsigned char)aVal);
    return (((unsigned int) col.red()) << 16)| (((unsigned int) col.green()) << 8)|((unsigned int) col.blue());
    }
    };
  • Instead to use directly the image and the stream operator we add the AddTextureImage2DWithFunctor object:
    viewer << AddTextureImage2DWithFunctor<MySliceImageAdapter, hueFct, Z3i::Space, Z3i::KSpace> (aSliceImageZ, hueFct(), Viewer3D<Z3i::Space, Z3i::KSpace>::RGBMode);
    viewer << AddTextureImage2DWithFunctor<MySliceImageAdapter, hueFct, Z3i::Space, Z3i::KSpace> (aSliceImageY, hueFct(), Viewer3D<Z3i::Space, Z3i::KSpace>::RGBMode);
  • Note that also can update the image position image data as in the previous example and we need to recall the functor function if we want to keep the same data interpretation :
    viewer << DGtal::UpdateImagePosition<Z3i::Space, Z3i::KSpace>(3, MyViewer::yDirection, 500.0, 50.0, 0.0);
    viewer << DGtal::UpdateImageData<MySliceImageAdapter, hueFct>(2, aSliceImageZ, 500, 0, 10, 0.0, MyViewer::zDirection, hueFct());
    viewer << MyViewer::updateDisplay;
    You will obtain such a visualization:
    Illustration of the 2D image slice visualization.