vedo.file_io

Submodule to read/write meshes and other objects in different formats, and other I/O functionalities.

   1import glob
   2import os
   3import time
   4from tempfile import NamedTemporaryFile, TemporaryDirectory
   5from typing import Any, List, Tuple, Union
   6
   7import numpy as np
   8
   9import vedo.vtkclasses as vtki  # a wrapper for lazy imports
  10
  11import vedo
  12from vedo import settings
  13from vedo import colors
  14from vedo import utils
  15from vedo.assembly import Assembly
  16from vedo.image import Image
  17from vedo.pointcloud import Points
  18from vedo.mesh import Mesh
  19from vedo.volume import Volume
  20
  21__docformat__ = "google"
  22
  23__doc__ = """
  24Submodule to read/write meshes and other objects in different formats,
  25and other I/O functionalities.
  26"""
  27
  28__all__ = [
  29    "load",
  30    "read",
  31    "download",
  32    "gunzip",
  33    "loadStructuredPoints",
  34    "loadStructuredGrid",
  35    "write",
  36    "save",
  37    "export_window",
  38    "import_window",
  39    "load_obj",
  40    "screenshot",
  41    "ask",
  42    "Video",
  43]
  44
  45
  46# example web page for X3D
  47_x3d_html_template = """
  48<!DOCTYPE html>
  49<html lang="en">
  50<head>
  51  <meta charset="UTF-8">
  52  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  53  <title> vedo with x3d </title>
  54
  55  <!-- THESE ARE THE RELEVANT LINES: -->
  56  <script src='https://www.x3dom.org/download/x3dom.js'> </script>
  57  <link rel='stylesheet' type='text/css' href='https://www.x3dom.org/download/x3dom.css'/>
  58
  59  <style>
  60     table, td, th { border: 1px solid black; background-color: powderblue;}
  61     table {width: 70%; border-collapse: collapse;}
  62     table th {width: 35%;}
  63  </style>
  64</head>
  65
  66<body style="font-family: Verdana">
  67  <h1>Example html generated by vedo</h1>
  68  This example loads a 3D scene from file ~fileoutput generated by
  69  <a href="https://github.com/marcomusy/vedo">vedo</a>
  70  (see <a href="https://github.com/marcomusy/vedo/tree/master/examples/other/export_x3d.py">export_x3d.py</a>).
  71  <br><br>
  72
  73
  74  <!-- THESE ARE THE RELEVANT LINES: -->
  75  <x3d width='~widthpx' height='~heightpx'>
  76     <scene>
  77        <Inline url="~fileoutput"> </Inline>
  78     </scene>
  79  </x3d>
  80
  81  <h3>Nothing shows up above this line?</h3>
  82  Enable your browser to load local files:
  83  <br><b>Firefox</b>: type <code>about:config</code> in the URL bar and
  84  change <code>privacy.file_unique_origin</code> from <code>True</code> to <code>False</code>
  85  <br><b>Chrome</b>: from terminal type:
  86  <code>google-chrome --enable-webgl --allow-file-access-from-files</code>
  87  (see <a href="https://cmatskas.com/interacting-with-local-data-files-using-chrome/">here</a>)
  88
  89  <br>
  90  <h3>Controls:</h3>
  91  <h4><strong>Examine Mode (activate with key 'e'):</strong></h4>
  92  <table>
  93     <tbody>
  94        <tr class="even description">
  95           <th>Button</th>
  96           <th>Function</th>
  97        </tr>
  98        <tr>
  99           <td>Left Button / Left Button + Shift</td>
 100           <td>Rotate</td>
 101        </tr>
 102        <tr>
 103           <td>Mid Button / Left Button + Ctl</td>
 104           <td>Pan</td>
 105        </tr>
 106        <tr>
 107           <td>Right Button / Wheel / Left Button + Alt</td>
 108           <td>Zoom</td>
 109        </tr>
 110        <tr>
 111           <td>Left double click</td>
 112           <td>Set center of rotation</td>
 113        </tr>
 114     </tbody>
 115  </table>
 116  <h4><strong>Walk Mode (activate with key 'w'):</strong></h4>
 117  <table>
 118     <tbody>
 119        <tr class="even description">
 120           <th>Button</th>
 121           <th>Function</th>
 122        </tr>
 123        <tr>
 124           <td>Left Button</td>
 125           <td>Move forward</td>
 126        </tr>
 127        <tr>
 128           <td>Right Button</td>
 129           <td>Move backward</td>
 130        </tr>
 131     </tbody>
 132  </table>
 133  <h4><strong>Fly Mode (activate with key 'f'):</strong></h4>
 134  <table>
 135     <tbody>
 136        <tr class="even description">
 137           <th>Button</th>
 138           <th>Function</th>
 139        </tr>
 140        <tr>
 141           <td>Left Button</td>
 142           <td>Move forward</td>
 143        </tr>
 144        <tr>
 145           <td>Right Button</td>
 146           <td>Move backward</td>
 147        </tr>
 148     </tbody>
 149  </table>
 150  <h3>Non-interactive camera movement</h3>
 151  <table>
 152     <tbody>
 153        <tr class="even description">
 154           <th>Key</th>
 155           <th>Function</th>
 156        </tr>
 157        <tr>
 158           <td>r</td>
 159           <td>reset view</td>
 160        </tr>
 161        <tr>
 162           <td>a</td>
 163           <td>show all</td>
 164        </tr>
 165        <tr>
 166           <td>u</td>
 167           <td>upright</td>
 168        </tr>
 169     </tbody>
 170  </table>
 171</body>
 172</html>
 173"""
 174
 175########################################################################
 176def load(inputobj: Union[list, str, os.PathLike], unpack=True, force=False) -> Any:
 177    """
 178    Load any vedo objects from file or from the web.
 179
 180    The output will depend on the file extension. See examples below.
 181    Unzip is made on the fly, if file ends with `.gz`.
 182    Can load an object directly from a URL address.
 183
 184    Arguments:
 185        unpack : (bool)
 186            unpack MultiBlockData into a flat list of objects.
 187        force : (bool)
 188            when downloading a file ignore any previous cached downloads and force a new one.
 189
 190    Example:
 191        ```python
 192        from vedo import dataurl, load, show
 193        # Return a list of 2 meshes
 194        g = load([dataurl+'250.vtk', dataurl+'270.vtk'])
 195        show(g)
 196        # Return a list of meshes by reading all files in a directory
 197        # (if directory contains DICOM files then a Volume is returned)
 198        g = load('mydicomdir/')
 199        show(g)
 200        ```
 201    """
 202    if isinstance(inputobj, list):
 203        inputobj = [str(f) for f in inputobj]
 204    else:
 205        inputobj = str(inputobj)
 206
 207    acts = []
 208    if utils.is_sequence(inputobj):
 209        flist = inputobj
 210    elif isinstance(inputobj, str) and inputobj.startswith("https://"):
 211        flist = [inputobj]
 212    else:
 213        flist = utils.humansort(glob.glob(inputobj))
 214
 215    for fod in flist:
 216
 217        if fod.startswith("https://"):
 218            fod = download(fod, force=force, verbose=False)
 219
 220        if os.path.isfile(fod):  ### it's a file
 221
 222            if fod.endswith(".gz"):
 223                fod = gunzip(fod)
 224
 225            a = _load_file(fod, unpack)
 226            acts.append(a)
 227
 228        elif os.path.isdir(fod):  ### it's a directory or DICOM
 229            flist = os.listdir(fod)
 230            if ".dcm" in flist[0]:  ### it's DICOM
 231                reader = vtki.new("DICOMImageReader")
 232                reader.SetDirectoryName(fod)
 233                reader.Update()
 234                image = reader.GetOutput()
 235                vol = Volume(image)
 236                try:
 237                    vol.metadata["PixelSpacing"] = reader.GetPixelSpacing()
 238                    vol.metadata["Width"] = reader.GetWidth()
 239                    vol.metadata["Height"] = reader.GetHeight()
 240                    vol.metadata["PositionPatient"] = reader.GetImagePositionPatient()
 241                    vol.metadata["OrientationPatient"] = reader.GetImageOrientationPatient()
 242                    vol.metadata["BitsAllocated"] = reader.GetBitsAllocated()
 243                    vol.metadata["PixelRepresentation"] = reader.GetPixelRepresentation()
 244                    vol.metadata["NumberOfComponents"] = reader.GetNumberOfComponents()
 245                    vol.metadata["TransferSyntaxUID"] = reader.GetTransferSyntaxUID()
 246                    vol.metadata["RescaleSlope"] = reader.GetRescaleSlope()
 247                    vol.metadata["RescaleOffset"] = reader.GetRescaleOffset()
 248                    vol.metadata["PatientName"] = reader.GetPatientName()
 249                    vol.metadata["StudyUID"] = reader.GetStudyUID()
 250                    vol.metadata["StudyID"] = reader.GetStudyID()
 251                    vol.metadata["GantryAngle"] = reader.GetGantryAngle()
 252                except Exception as e:
 253                    vedo.logger.warning(f"Cannot read DICOM metadata: {e}")
 254                acts.append(vol)
 255
 256            else:  ### it's a normal directory
 257                utils.humansort(flist)
 258                for ifile in flist:
 259                    a = _load_file(fod + "/" + ifile, unpack)
 260                    acts.append(a)
 261        else:
 262            vedo.logger.error(f"in load(), cannot find {fod}")
 263
 264    if len(acts) == 1:
 265        if "numpy" in str(type(acts[0])):
 266            return acts[0]
 267        if not acts[0]:
 268            vedo.logger.error(f"in load(), cannot load {inputobj}")
 269        return acts[0]
 270
 271    if len(acts) == 0:
 272        vedo.logger.error(f"in load(), cannot load {inputobj}")
 273        return None
 274
 275    else:
 276        return acts
 277
 278########################################################################
 279def _load_file(filename, unpack):
 280    fl = str(filename).lower()
 281
 282    ########################################################## other formats:
 283    if fl.endswith(".xml") or fl.endswith(".xml.gz") or fl.endswith(".xdmf"):
 284        # Fenics tetrahedral file
 285        objt = loadDolfin(filename)
 286    elif fl.endswith(".neutral") or fl.endswith(".neu"):  # neutral tets
 287        objt = loadNeutral(filename)
 288    elif fl.endswith(".gmsh"):  # gmesh file
 289        objt = loadGmesh(filename)
 290    elif fl.endswith(".pcd"):  # PCL point-cloud format
 291        objt = loadPCD(filename)
 292        objt.properties.SetPointSize(2)
 293    elif fl.endswith(".off"):
 294        objt = loadOFF(filename)
 295    elif fl.endswith(".step") or fl.endswith(".stp"):
 296        objt = loadSTEP(filename)
 297    elif fl.endswith(".3ds"):  # 3ds format
 298        objt = load3DS(filename)
 299    elif fl.endswith(".wrl"):
 300        importer = vtki.new("VRMLImporter")
 301        importer.SetFileName(filename)
 302        importer.Read()
 303        importer.Update()
 304        actors = importer.GetRenderer().GetActors()  # vtkActorCollection
 305        actors.InitTraversal()
 306        wacts = []
 307        for i in range(actors.GetNumberOfItems()):
 308            act = actors.GetNextActor()
 309            m = Mesh(act.GetMapper().GetInput())
 310            m.actor = act
 311            wacts.append(m)
 312        objt = Assembly(wacts)
 313
 314    ######################################################## volumetric:
 315    elif fl.endswith((".tif", ".tiff", ".slc", ".vti", ".mhd", ".nrrd", ".nii", ".dem")):
 316        img = loadImageData(filename)
 317        objt = Volume(img)
 318
 319    ######################################################### 2D images:
 320    elif fl.endswith((".jpg", ".jpeg",".png", ".bmp")):
 321        if ".png" in fl:
 322            picr = vtki.new("PNGReader")
 323        elif ".jpg" in fl or ".jpeg" in fl:
 324            picr = vtki.new("JPEGReader")
 325        elif ".bmp" in fl:
 326            picr = vtki.new("BMPReader")
 327        elif ".gif" in fl:
 328            from PIL import Image as PILImage, ImageSequence
 329
 330            img = PILImage.open(filename)
 331            frames = []
 332            for frame in ImageSequence.Iterator(img):
 333                a = np.array(frame.convert("RGB").getdata(), dtype=np.uint8)
 334                a = a.reshape([frame.size[1], frame.size[0], 3])
 335                frames.append(Image(a))
 336            return frames
 337
 338        picr.SetFileName(filename)
 339        picr.Update()
 340        objt = Image(picr.GetOutput())
 341
 342    ######################################################### multiblock:
 343    elif fl.endswith(".vtm") or fl.endswith(".vtmb"):
 344        mbread = vtki.new("XMLMultiBlockDataReader")
 345        mbread.SetFileName(filename)
 346        mbread.Update()
 347        mb = mbread.GetOutput()
 348        if unpack:
 349            acts = []
 350            for i in range(mb.GetNumberOfBlocks()):
 351                b = mb.GetBlock(i)
 352                if isinstance(
 353                    b,
 354                    (
 355                        vtki.vtkPolyData,
 356                        vtki.vtkStructuredGrid,
 357                        vtki.vtkRectilinearGrid,
 358                    ),
 359                ):
 360                    acts.append(Mesh(b))
 361                elif isinstance(b, vtki.vtkImageData):
 362                    acts.append(Volume(b))
 363                elif isinstance(b, vtki.vtkUnstructuredGrid):
 364                    acts.append(vedo.UnstructuredGrid(b))
 365            return acts
 366        return mb
 367
 368    ######################################################### assembly:
 369    elif fl.endswith(".npy"):
 370        data = np.load(filename, allow_pickle=True)
 371        try:
 372            # old format with a single object
 373            meshs = [from_numpy(dd) for dd in data]
 374        except TypeError:
 375            data = data.item()
 376            meshs = []
 377            for ad in data["objects"][0]["parts"]:
 378                obb = from_numpy(ad)
 379                meshs.append(obb)
 380        return Assembly(meshs)
 381
 382    ###########################################################
 383    elif fl.endswith(".geojson"):
 384        return loadGeoJSON(filename)
 385
 386    elif fl.endswith(".pvd"):
 387        return loadPVD(filename)
 388
 389    ########################################################### polygonal mesh:
 390    else:
 391        if fl.endswith(".vtk"):  # read all legacy vtk types
 392            reader = vtki.new("DataSetReader")
 393            reader.ReadAllScalarsOn()
 394            reader.ReadAllVectorsOn()
 395            reader.ReadAllTensorsOn()
 396            reader.ReadAllFieldsOn()
 397            reader.ReadAllNormalsOn()
 398            reader.ReadAllColorScalarsOn()
 399        elif fl.endswith(".ply"):
 400            reader = vtki.new("PLYReader")
 401        elif fl.endswith(".obj"):
 402            reader = vtki.new("OBJReader")
 403            reader.SetGlobalWarningDisplay(0) # suppress warnings issue #980
 404        elif fl.endswith(".stl"):
 405            reader = vtki.new("STLReader")
 406        elif fl.endswith(".byu") or fl.endswith(".g"):
 407            reader = vtki.new("BYUReader")
 408        elif fl.endswith(".foam"):  # OpenFoam
 409            reader = vtki.new("OpenFOAMReader")
 410        elif fl.endswith(".pvd"):
 411            reader = vtki.new("XMLGenericDataObjectReader")
 412        elif fl.endswith(".vtp"):
 413            reader = vtki.new("XMLPolyDataReader")
 414        elif fl.endswith(".vts"):
 415            reader = vtki.new("XMLStructuredGridReader")
 416        elif fl.endswith(".vtu"):
 417            reader = vtki.new("XMLUnstructuredGridReader")
 418        elif fl.endswith(".vtr"):
 419            reader = vtki.new("XMLRectilinearGridReader")
 420        elif fl.endswith(".pvtr"):
 421            reader = vtki.new("XMLPRectilinearGridReader")
 422        elif fl.endswith("pvtu"):
 423            reader = vtki.new("XMLPUnstructuredGridReader")
 424        elif fl.endswith(".txt") or fl.endswith(".xyz") or fl.endswith(".dat"):
 425            reader = vtki.new("ParticleReader")  # (format is x, y, z, scalar)
 426        elif fl.endswith(".facet"):
 427            reader = vtki.new("FacetReader")
 428        else:
 429            return None
 430
 431        reader.SetFileName(filename)
 432        reader.Update()
 433        routput = reader.GetOutput()
 434
 435        if not routput:
 436            vedo.logger.error(f"unable to load {filename}")
 437            return None
 438
 439        if isinstance(routput, vtki.vtkUnstructuredGrid):
 440            objt = vedo.UnstructuredGrid(routput)
 441
 442        else:
 443            objt = Mesh(routput)
 444            if fl.endswith(".txt") or fl.endswith(".xyz") or fl.endswith(".dat"):
 445                objt.point_size(4)
 446
 447    objt.filename = filename
 448    objt.file_size, objt.created = file_info(filename)
 449    return objt
 450
 451
 452def download(url: str, force=False, verbose=True) -> str:
 453    """
 454    Retrieve a file from a URL, save it locally and return its path.
 455    Use `force=True` to force a reload and discard cached copies.
 456    """
 457    if not url.startswith("https://"):
 458        # assume it's a file so no need to download
 459        return url
 460    url = url.replace("www.dropbox", "dl.dropbox")
 461
 462    if "github.com" in url:
 463        url = url.replace("/blob/", "/raw/")
 464
 465    basename = os.path.basename(url)
 466
 467    if "?" in basename:
 468        basename = basename.split("?")[0]
 469
 470    home_directory = os.path.expanduser("~")
 471    cachedir = os.path.join(home_directory, settings.cache_directory, "vedo")
 472    fname = os.path.join(cachedir, basename)
 473    # Create the directory if it does not exist
 474    if not os.path.exists(cachedir):
 475        os.makedirs(cachedir)
 476
 477    if not force and os.path.exists(fname):
 478        if verbose:
 479            colors.printc("reusing cached file:", fname)
 480        return fname
 481
 482    try:
 483        from urllib.request import urlopen, Request
 484        req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
 485        if verbose:
 486            colors.printc("reading", basename, "from", url.split("/")[2][:40], "...", end="")
 487
 488    except ImportError:
 489        import urllib2 # type: ignore
 490        import contextlib
 491        urlopen = lambda url_: contextlib.closing(urllib2.urlopen(url_))
 492        req = url
 493        if verbose:
 494            colors.printc("reading", basename, "from", url.split("/")[2][:40], "...", end="")
 495
 496    with urlopen(req) as response, open(fname, "wb") as output:
 497        output.write(response.read())
 498
 499    if verbose:
 500        colors.printc(" done.")
 501    return fname
 502
 503
 504########################################################################
 505# def download_new(url, to_local_file="", force=False, verbose=True):
 506#     """
 507#     Downloads a file from `url` to `to_local_file` if the local copy is outdated.
 508
 509#     Arguments:
 510#         url : (str)
 511#             The URL to download the file from.
 512#         to_local_file : (str)
 513#             The local file name to save the file to.
 514#             If not specified, the file name will be the same as the remote file name
 515#             in the directory specified by `settings.cache_directory + "/vedo"`.
 516#         force : (bool)
 517#             Force a new download even if the local file is up to date.
 518#         verbose : (bool)
 519#             Print verbose messages.
 520#     """
 521#     if not url.startswith("https://"):
 522#         if os.path.exists(url):
 523#             # Assume the url is already the local file path
 524#             return url
 525#         else:
 526#             raise FileNotFoundError(f"File not found: {url}")
 527
 528#     from datetime import datetime
 529#     import requests
 530
 531#     url = url.replace("www.dropbox", "dl.dropbox")
 532
 533#     if "github.com" in url:
 534#         url = url.replace("/blob/", "/raw/")
 535
 536#     # Get the user's home directory
 537#     home_directory = os.path.expanduser("~")
 538
 539#     # Define the path for the cache directory
 540#     cachedir = os.path.join(home_directory, settings.cache_directory, "vedo")
 541
 542#     # Create the directory if it does not exist
 543#     if not os.path.exists(cachedir):
 544#         os.makedirs(cachedir)
 545
 546#     if not to_local_file:
 547#         basename = os.path.basename(url)
 548#         if "?" in basename:
 549#             basename = basename.split("?")[0]
 550#         to_local_file = os.path.join(cachedir, basename)
 551#         if verbose: print(f"Using local file name: {to_local_file}")
 552
 553#     # Check if the local file exists and get its last modified time
 554#     if os.path.exists(to_local_file):
 555#         to_local_file_modified_time = os.path.getmtime(to_local_file)
 556#     else:
 557#         to_local_file_modified_time = 0
 558
 559#     # Send a HEAD request to get last modified time of the remote file
 560#     response = requests.head(url)
 561#     if 'Last-Modified' in response.headers:
 562#         remote_file_modified_time = datetime.strptime(
 563#             response.headers['Last-Modified'], '%a, %d %b %Y %H:%M:%S GMT'
 564#         ).timestamp()
 565#     else:
 566#         # If the Last-Modified header not available, assume file needs to be downloaded
 567#         remote_file_modified_time = float('inf')
 568
 569#     # Download the file if the remote file is newer
 570#     if force or remote_file_modified_time > to_local_file_modified_time:
 571#         response = requests.get(url)
 572#         with open(to_local_file, 'wb') as file:
 573#             file.write(response.content)
 574#             if verbose: print(f"Downloaded file from {url} -> {to_local_file}")
 575#     else:
 576#         if verbose: print("Local file is up to date.")
 577#     return to_local_file
 578
 579
 580########################################################################
 581def gunzip(filename: str) -> str:
 582    """Unzip a `.gz` file to a temporary file and returns its path."""
 583    if not filename.endswith(".gz"):
 584        # colors.printc("gunzip() error: file must end with .gz", c='r')
 585        return filename
 586
 587    import gzip
 588
 589    tmp_file = NamedTemporaryFile(delete=False)
 590    tmp_file.name = os.path.join(
 591        os.path.dirname(tmp_file.name), os.path.basename(filename).replace(".gz", "")
 592    )
 593    inF = gzip.open(filename, "rb")
 594    with open(tmp_file.name, "wb") as outF:
 595        outF.write(inF.read())
 596    inF.close()
 597    return tmp_file.name
 598
 599########################################################################
 600def file_info(file_path: str) -> Tuple[str, str]:
 601    """Return the file size and creation time of input file"""
 602    siz, created = "", ""
 603    if os.path.isfile(file_path):
 604        f_info = os.stat(file_path)
 605        num = f_info.st_size
 606        for x in ["B", "KB", "MB", "GB", "TB"]:
 607            if num < 1024.0:
 608                break
 609            num /= 1024.0
 610        siz = "%3.1f%s" % (num, x)
 611        created = time.ctime(os.path.getmtime(file_path))
 612    return siz, created
 613
 614
 615###################################################################
 616def loadStructuredPoints(filename: Union[str, os.PathLike], as_points=True):
 617    """
 618    Load and return a `vtkStructuredPoints` object from file.
 619
 620    If `as_points` is True, return a `Points` object
 621    instead of a `vtkStructuredPoints`.
 622    """
 623    filename = str(filename)
 624    reader = vtki.new("StructuredPointsReader")
 625    reader.SetFileName(filename)
 626    reader.Update()
 627    if as_points:
 628        v2p = vtki.new("ImageToPoints")
 629        v2p.SetInputData(reader.GetOutput())
 630        v2p.Update()
 631        pts = Points(v2p.GetOutput())
 632        return pts
 633    return reader.GetOutput()
 634
 635########################################################################
 636def loadStructuredGrid(filename: Union[str, os.PathLike]):
 637    """Load and return a `vtkStructuredGrid` object from file."""
 638    filename = str(filename)
 639    if filename.endswith(".vts"):
 640        reader = vtki.new("XMLStructuredGridReader")
 641    else:
 642        reader = vtki.new("StructuredGridReader")
 643    reader.SetFileName(filename)
 644    reader.Update()
 645    return reader.GetOutput()
 646
 647
 648###################################################################
 649def load3DS(filename: Union[str, os.PathLike]) -> Assembly:
 650    """Load `3DS` file format from file."""
 651    filename = str(filename)
 652    renderer = vtki.vtkRenderer()
 653    renWin = vtki.vtkRenderWindow()
 654    renWin.AddRenderer(renderer)
 655
 656    importer = vtki.new("3DSImporter")
 657    importer.SetFileName(filename)
 658    importer.ComputeNormalsOn()
 659    importer.SetRenderWindow(renWin)
 660    importer.Update()
 661
 662    actors = renderer.GetActors()  # vtkActorCollection
 663    acts = []
 664    for i in range(actors.GetNumberOfItems()):
 665        a = actors.GetItemAsObject(i)
 666        acts.append(a)
 667    del renWin
 668
 669    wrapped_acts = []
 670    for a in acts:
 671        try:
 672            newa = Mesh(a.GetMapper().GetInput())
 673            newa.actor = a
 674            wrapped_acts.append(newa)
 675            # print("loaded 3DS object", [a])
 676        except:
 677            print("ERROR: cannot load 3DS object part", [a])
 678    return vedo.Assembly(wrapped_acts)
 679
 680########################################################################
 681def loadOFF(filename: Union[str, os.PathLike]) -> Mesh:
 682    """Read the OFF file format (polygonal mesh)."""
 683    filename = str(filename)
 684    with open(filename, "r", encoding="UTF-8") as f:
 685        lines = f.readlines()
 686
 687    vertices = []
 688    faces = []
 689    NumberOfVertices = 0
 690    i = -1
 691    for text in lines:
 692        if len(text) == 0:
 693            continue
 694        if text == "\n":
 695            continue
 696        if "#" in text:
 697            continue
 698        if "OFF" in text:
 699            continue
 700
 701        ts = text.split()
 702        n = len(ts)
 703
 704        if not NumberOfVertices and n > 1:
 705            NumberOfVertices, NumberOfFaces = int(ts[0]), int(ts[1])
 706            continue
 707        i += 1
 708
 709        if i < NumberOfVertices and n == 3:
 710            x, y, z = float(ts[0]), float(ts[1]), float(ts[2])
 711            vertices.append([x, y, z])
 712
 713        ids = []
 714        if NumberOfVertices <= i < (NumberOfVertices + NumberOfFaces + 1) and n > 2:
 715            ids += [int(xx) for xx in ts[1:]]
 716            faces.append(ids)
 717
 718    return Mesh(utils.buildPolyData(vertices, faces))
 719
 720def loadSTEP(filename: Union[str, os.PathLike], deflection=1.0) -> Mesh:
 721    """
 722    Reads a 3D STEP file and returns its mesh representation as vertices and triangles.
 723
 724    Parameters:
 725    - filename (str): Path to the STEP file.
 726    - deflection (float): Linear deflection for meshing accuracy (smaller values yield finer meshes).
 727
 728    Returns:
 729    - vertices (list of tuples): List of (x, y, z) coordinates of the mesh vertices.
 730    - triangles (list of tuples): List of (i, j, k) indices representing the triangles.
 731
 732    Raises:
 733    - Exception: If the STEP file cannot be read.
 734    """
 735    try:
 736        from OCC.Core.STEPControl import STEPControl_Reader  # type: ignore
 737        from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh  # type: ignore
 738        from OCC.Core.TopExp import TopExp_Explorer  # type: ignore
 739        from OCC.Core.TopoDS import topods  # type: ignore
 740        from OCC.Core.BRep import BRep_Tool  # type: ignore
 741        from OCC.Core.TopAbs import TopAbs_FACE  # type: ignore
 742        from OCC.Core.TopLoc import TopLoc_Location  # type: ignore
 743    except ImportError:
 744        raise ImportError(
 745            "OCC library not found.\n\nPlease install 'pythonocc-core'. "
 746            "You can install it using the following command:\n"
 747            "\t\tconda install -c conda-forge pythonocc-core"
 748        )
 749
 750    # Initialize the STEP reader
 751    reader = STEPControl_Reader()
 752    status = reader.ReadFile(str(filename))
 753    if status != 1:  # Check if reading was successful (IFSelect_RetDone = 1)
 754        raise Exception("Error reading STEP file")
 755
 756    # Transfer the STEP data into a shape
 757    reader.TransferRoots()
 758    shape = reader.OneShape()
 759
 760    # Mesh the shape with the specified deflection
 761    mesh = BRepMesh_IncrementalMesh(shape, deflection)
 762    mesh.Perform()
 763
 764    # Extract vertices and triangles
 765    explorer = TopExp_Explorer(shape, TopAbs_FACE)
 766    vertices = []
 767    triangles = []
 768    vertex_index = 0
 769
 770    # Iterate over all faces in the shape
 771    while explorer.More():
 772        face = topods.Face(explorer.Current())
 773        location = TopLoc_Location()
 774        triangulation = BRep_Tool.Triangulation(face, location)
 775
 776        if triangulation:
 777            # Extract vertices from the triangulation
 778            for i in range(1, triangulation.NbNodes() + 1):
 779                point = triangulation.Node(i).Transformed(location.Transformation())
 780                vertices.append((point.X(), point.Y(), point.Z()))
 781
 782            # Extract triangles with adjusted indices
 783            for i in range(1, triangulation.NbTriangles() + 1):
 784                triangle = triangulation.Triangle(i)
 785                n1, n2, n3 = triangle.Get()  # 1-based indices
 786                triangles.append((
 787                    n1 + vertex_index - 1,
 788                    n2 + vertex_index - 1,
 789                    n3 + vertex_index - 1
 790                ))
 791
 792            # Update the vertex index offset for the next face
 793            vertex_index += triangulation.NbNodes()
 794
 795        explorer.Next()
 796
 797    # Create a mesh object
 798    mesh = Mesh([vertices, triangles])
 799    return mesh
 800
 801########################################################################
 802def loadGeoJSON(filename: Union[str, os.PathLike]) -> Mesh:
 803    """Load GeoJSON files."""
 804    filename = str(filename)
 805    jr = vtki.new("GeoJSONReader")
 806    jr.SetFileName(filename)
 807    jr.Update()
 808    return Mesh(jr.GetOutput())
 809
 810########################################################################
 811def loadDolfin(filename: Union[str, os.PathLike]) -> Union[Mesh, "vedo.TetMesh", None]:
 812    """
 813    Reads a `Fenics/Dolfin` file format (.xml or .xdmf).
 814
 815    Return a `Mesh` or a `TetMesh` object.
 816    """
 817    filename = str(filename)
 818    try:
 819        import dolfin
 820    except ImportError:
 821        vedo.logger.error("loadDolfin(): dolfin module not found. Install with:")
 822        vedo.logger.error("  conda create -n fenics -c conda-forge fenics")
 823        vedo.logger.error("  conda install conda-forge::mshr")
 824        vedo.logger.error("  conda activate fenics")
 825        return None
 826
 827    if filename.lower().endswith(".xdmf"):
 828        f = dolfin.XDMFFile(filename)
 829        m = dolfin.Mesh()
 830        f.read(m)
 831    else:
 832        m = dolfin.Mesh(filename)
 833
 834    cells = m.cells()
 835    verts = m.coordinates()
 836
 837    if cells.size and verts.size:
 838        if len(cells[0]) == 4:  # tetrahedral mesh
 839            return vedo.TetMesh([verts, cells])
 840        elif len(cells[0]) == 3:  # triangular mesh
 841            return Mesh([verts, cells])
 842
 843    return None
 844
 845
 846########################################################################
 847def loadPVD(filename: Union[str, os.PathLike]) -> Union[List[Any], None]:
 848    """Read paraview files."""
 849    filename = str(filename)
 850    import xml.etree.ElementTree as et
 851
 852    tree = et.parse(filename)
 853
 854    dname = os.path.dirname(filename)
 855    if not dname:
 856        dname = "."
 857
 858    listofobjs = []
 859    for coll in tree.getroot():
 860        for dataset in coll:
 861            fname = dataset.get("file")
 862            if not fname:
 863                continue
 864            ob = load(dname + "/" + fname)
 865            tm = dataset.get("timestep")
 866            if tm:
 867                ob.time = tm
 868            listofobjs.append(ob)
 869    if len(listofobjs) == 1:
 870        return listofobjs[0]
 871    if len(listofobjs) == 0:
 872        return None
 873    return listofobjs
 874
 875########################################################################
 876def loadNeutral(filename: Union[str, os.PathLike]) -> "vedo.TetMesh":
 877    """
 878    Reads a `Neutral` tetrahedral file format.
 879
 880    Returns an `TetMesh` object.
 881    """
 882    filename = str(filename)
 883    with open(filename, "r", encoding="UTF-8") as f:
 884        lines = f.readlines()
 885
 886    ncoords = int(lines[0])
 887    coords = []
 888    for i in range(1, ncoords + 1):
 889        x, y, z = lines[i].split()
 890        coords.append([float(x), float(y), float(z)])
 891
 892    ntets = int(lines[ncoords + 1])
 893    idolf_tets = []
 894    for i in range(ncoords + 2, ncoords + ntets + 2):
 895        text = lines[i].split()
 896        v0, v1, v2, v3 = int(text[1])-1, int(text[2])-1, int(text[3])-1, int(text[4])-1
 897        idolf_tets.append([v0, v1, v2, v3])
 898
 899    return vedo.TetMesh([coords, idolf_tets])
 900
 901########################################################################
 902def loadGmesh(filename: Union[str, os.PathLike]) -> Mesh:
 903    """Reads a `gmesh` file format. Return an `Mesh` object."""
 904    filename = str(filename)
 905    with open(filename, "r", encoding="UTF-8") as f:
 906        lines = f.readlines()
 907
 908    nnodes = 0
 909    index_nodes = 0
 910    for i, line in enumerate(lines):
 911        if "$Nodes" in line:
 912            index_nodes = i + 1
 913            nnodes = int(lines[index_nodes])
 914            break
 915    node_coords = []
 916    for i in range(index_nodes + 1, index_nodes + 1 + nnodes):
 917        cn = lines[i].split()
 918        node_coords.append([float(cn[1]), float(cn[2]), float(cn[3])])
 919
 920    nelements = 0
 921    index_elements = 0
 922    for i, line in enumerate(lines):
 923        if "$Elements" in line:
 924            index_elements = i + 1
 925            nelements = int(lines[index_elements])
 926            break
 927    elements = []
 928    for i in range(index_elements + 1, index_elements + 1 + nelements):
 929        ele = lines[i].split()
 930        elements.append([int(ele[-3]), int(ele[-2]), int(ele[-1])])
 931
 932    poly = utils.buildPolyData(node_coords, elements, index_offset=1)
 933    return Mesh(poly)
 934
 935########################################################################
 936def loadPCD(filename: Union[str, os.PathLike]) -> Points:
 937    """Return a `Mesh` made of only vertex points
 938    from the `PointCloud` library file format.
 939
 940    Returns an `Points` object.
 941    """
 942    filename = str(filename)
 943    with open(filename, "r", encoding="UTF-8") as f:
 944        lines = f.readlines()
 945
 946    start = False
 947    pts = []
 948    N, expN = 0, 0
 949    for text in lines:
 950        if start:
 951            if N >= expN:
 952                break
 953            l = text.split()
 954            pts.append([float(l[0]), float(l[1]), float(l[2])])
 955            N += 1
 956        if not start and "POINTS" in text:
 957            expN = int(text.split()[1])
 958        if not start and "DATA ascii" in text:
 959            start = True
 960    if expN != N:
 961        vedo.logger.warning(f"Mismatch in PCD file {expN} != {len(pts)}")
 962    poly = utils.buildPolyData(pts)
 963    return Points(poly).point_size(4)
 964
 965#########################################################################
 966def from_numpy(d: dict) -> Mesh:
 967    """Create a Mesh object from a dictionary."""
 968    # recreate a mesh from numpy arrays
 969    keys = d.keys()
 970
 971    points = d["points"]
 972    cells = d["cells"] if "cells" in keys else None
 973    lines = d["lines"] if "lines" in keys else None
 974
 975    msh = Mesh([points, cells, lines])
 976
 977    if "pointdata" in keys and isinstance(d["pointdata"], dict):
 978        for arrname, arr in d["pointdata"].items():
 979            msh.pointdata[arrname] = arr
 980    if "celldata" in keys and isinstance(d["celldata"], dict):
 981        for arrname, arr in d["celldata"].items():
 982            msh.celldata[arrname] = arr
 983    if "metadata" in keys and isinstance(d["metadata"], dict):
 984        for arrname, arr in d["metadata"].items():
 985            msh.metadata[arrname] = arr
 986
 987    prp = msh.properties
 988    prp.SetAmbient(d['ambient'])
 989    prp.SetDiffuse(d['diffuse'])
 990    prp.SetSpecular(d['specular'])
 991    prp.SetSpecularPower(d['specularpower'])
 992    prp.SetSpecularColor(d['specularcolor'])
 993
 994    prp.SetInterpolation(0)
 995    # prp.SetInterpolation(d['shading'])
 996
 997    prp.SetOpacity(d['alpha'])
 998    prp.SetRepresentation(d['representation'])
 999    prp.SetPointSize(d['pointsize'])
1000    if d['color'] is not None:
1001        msh.color(d['color'])
1002    if "lighting_is_on" in d.keys():
1003        prp.SetLighting(d['lighting_is_on'])
1004    # Must check keys for backwards compatibility:
1005    if "linecolor" in d.keys() and d['linecolor'] is not None:
1006        msh.linecolor(d['linecolor'])
1007    if "backcolor" in d.keys() and d['backcolor'] is not None:
1008        msh.backcolor(d['backcolor'])
1009
1010    if d['linewidth'] is not None:
1011        msh.linewidth(d['linewidth'])
1012    if "edge_visibility" in d.keys():
1013        prp.SetEdgeVisibility(d['edge_visibility']) # new
1014
1015    lut_list  = d["LUT"]
1016    lut_range = d["LUT_range"]
1017    ncols = len(lut_list)
1018    lut = vtki.vtkLookupTable()
1019    lut.SetNumberOfTableValues(ncols)
1020    lut.SetRange(lut_range)
1021    for i in range(ncols):
1022        r, g, b, a = lut_list[i]
1023        lut.SetTableValue(i, r, g, b, a)
1024    lut.Build()
1025    msh.mapper.SetLookupTable(lut)
1026    msh.mapper.SetScalarRange(lut_range)
1027
1028    try: # NEW in vedo 5.0
1029        arname = d["array_name_to_color_by"]
1030        msh.mapper.SetArrayName(arname)
1031        msh.mapper.SetInterpolateScalarsBeforeMapping(
1032            d["interpolate_scalars_before_mapping"])
1033        msh.mapper.SetUseLookupTableScalarRange(
1034            d["use_lookup_table_scalar_range"])
1035        msh.mapper.SetScalarRange(d["scalar_range"])
1036        msh.mapper.SetScalarVisibility(d["scalar_visibility"])
1037        msh.mapper.SetScalarMode(d["scalar_mode"])
1038        msh.mapper.SetColorMode(d["color_mode"])
1039        if d["scalar_visibility"]:
1040            if d["scalar_mode"] == 1:
1041                msh.dataset.GetPointData().SetActiveScalars(arname)
1042            if d["scalar_mode"] == 2:
1043                msh.dataset.GetCellData().SetActiveScalars(arname)
1044
1045        if "texture_array" in keys and d["texture_array"] is not None:
1046            # recreate a vtkTexture object from numpy arrays:
1047            t = vtki.vtkTexture()
1048            t.SetInterpolate(d["texture_interpolate"])
1049            t.SetRepeat(d["texture_repeat"])
1050            t.SetQuality(d["texture_quality"])
1051            t.SetColorMode(d["texture_color_mode"])
1052            t.SetMipmap(d["texture_mipmap"])
1053            t.SetBlendingMode(d["texture_blending_mode"])
1054            t.SetEdgeClamp(d["texture_edge_clamp"])
1055            t.SetBorderColor(d["texture_border_color"])
1056            msh.actor.SetTexture(t)
1057            tcarray = None
1058            for arrname in msh.pointdata.keys():
1059                if "Texture" in arrname or "TCoord" in arrname:
1060                    tcarray = arrname
1061                    break
1062            if tcarray is not None:
1063                t.SetInputData(vedo.Image(d["texture_array"]).dataset)
1064                msh.pointdata.select_texture_coords(tcarray)
1065
1066        # print("color_mode", d["color_mode"])
1067        # print("scalar_mode", d["scalar_mode"])
1068        # print("scalar_range", d["scalar_range"])
1069        # print("scalar_visibility", d["scalar_visibility"])
1070        # print("array_name_to_color_by", arname)
1071    except KeyError:
1072        pass
1073
1074    if "time" in keys: msh.time = d["time"]
1075    if "name" in keys: msh.name = d["name"]
1076    # if "info" in keys: msh.info = d["info"]
1077    if "filename" in keys: msh.filename = d["filename"]
1078    if "pickable" in keys: msh.pickable(d["pickable"])
1079    if "dragable" in keys: msh.draggable(d["dragable"])
1080    return msh
1081
1082#############################################################################
1083def _import_npy(fileinput: Union[str, os.PathLike]) -> "vedo.Plotter":
1084    """Import a vedo scene from numpy format."""
1085    fileinput = str(fileinput)
1086
1087    fileinput = download(fileinput, verbose=False, force=True)
1088    if fileinput.endswith(".npy"):
1089        data = np.load(fileinput, allow_pickle=True, encoding="latin1").flatten()[0]
1090    elif fileinput.endswith(".npz"):
1091        data = np.load(fileinput, allow_pickle=True)["vedo_scenes"][0]
1092
1093    if "use_parallel_projection" in data.keys():
1094        vedo.settings.use_parallel_projection = data["use_parallel_projection"]
1095    if "use_polygon_offset" in data.keys():
1096        vedo.settings.use_polygon_offset = data["use_polygon_offset"]
1097    if "polygon_offset_factor" in data.keys():
1098        vedo.settings.polygon_offset_factor = data["polygon_offset_factor"]
1099    if "polygon_offset_units" in data.keys():
1100        vedo.settings.polygon_offset_units = data["polygon_offset_units"]
1101    if "interpolate_scalars_before_mapping" in data.keys():
1102        vedo.settings.interpolate_scalars_before_mapping = data["interpolate_scalars_before_mapping"]
1103    if "default_font" in data.keys():
1104        vedo.settings.default_font = data["default_font"]
1105    if "use_depth_peeling" in data.keys():
1106        vedo.settings.use_depth_peeling = data["use_depth_peeling"]
1107
1108    axes = data.pop("axes", 4) # UNUSED
1109    title = data.pop("title", "")
1110    backgrcol  = data.pop("backgrcol", "white")
1111    backgrcol2 = data.pop("backgrcol2", None)
1112    cam = data.pop("camera", None)
1113
1114    if data["shape"] != (1, 1):
1115        data["size"] = "auto"  # disable size
1116
1117    plt = vedo.Plotter(
1118        size=data["size"],  # not necessarily a good idea to set it
1119        axes=axes,          # must be zero to avoid recreating the axes
1120        title=title,
1121        bg=backgrcol,
1122        bg2=backgrcol2,
1123    )
1124
1125    if cam:
1126        if "pos" in cam.keys():
1127            plt.camera.SetPosition(cam["pos"])
1128        if "focalPoint" in cam.keys(): # obsolete
1129            plt.camera.SetFocalPoint(cam["focalPoint"])
1130        if "focal_point" in cam.keys():
1131            plt.camera.SetFocalPoint(cam["focal_point"])
1132        if "viewup" in cam.keys():
1133            plt.camera.SetViewUp(cam["viewup"])
1134        if "distance" in cam.keys():
1135            plt.camera.SetDistance(cam["distance"])
1136        if "clippingRange" in cam.keys(): # obsolete
1137            plt.camera.SetClippingRange(cam["clippingRange"])
1138        if "clipping_range" in cam.keys():
1139            plt.camera.SetClippingRange(cam["clipping_range"])
1140        if "parallel_scale" in cam.keys():
1141            plt.camera.SetParallelScale(cam["parallel_scale"])
1142
1143    ##############################################
1144    objs = []
1145    for d in data["objects"]:
1146        ### Mesh
1147        if d['type'].lower() == 'mesh':
1148            obj = from_numpy(d)
1149
1150        ### Assembly
1151        elif d['type'].lower() == 'assembly':
1152            assacts = []
1153            for ad in d["actors"]:
1154                assacts.append(from_numpy(ad))
1155            obj = Assembly(assacts)
1156            obj.SetScale(d["scale"])
1157            obj.SetPosition(d["position"])
1158            obj.SetOrientation(d["orientation"])
1159            obj.SetOrigin(d["origin"])
1160
1161        ### Volume
1162        elif d['type'].lower() == 'volume':
1163            obj = Volume(d["array"])
1164            obj.spacing(d["spacing"])
1165            obj.origin(d["origin"])
1166            if "jittering" in d.keys(): obj.jittering(d["jittering"])
1167            obj.mode(d["mode"])
1168            obj.color(d["color"])
1169            obj.alpha(d["alpha"])
1170            obj.alpha_gradient(d["alphagrad"])
1171
1172        ### TetMesh
1173        elif d['type'].lower() == 'tetmesh':
1174            raise NotImplementedError("TetMesh not supported yet")
1175
1176        ### ScalarBar2D
1177        elif d['type'].lower() == 'scalarbar2d':
1178            raise NotImplementedError("ScalarBar2D not supported yet")
1179
1180        ### Image
1181        elif d['type'].lower() == 'image':
1182            obj = Image(d["array"])
1183            obj.alpha(d["alpha"])
1184            obj.actor.SetScale(d["scale"])
1185            obj.actor.SetPosition(d["position"])
1186            obj.actor.SetOrientation(d["orientation"])
1187            obj.actor.SetOrigin(d["origin"])
1188
1189        ### Text2D
1190        elif d['type'].lower() == 'text2d':
1191            obj = vedo.shapes.Text2D(d["text"], font=d["font"], c=d["color"])
1192            obj.pos(d["position"]).size(d["size"])
1193            obj.background(d["bgcol"], d["alpha"])
1194            if d["frame"]:
1195                obj.frame(d["bgcol"])
1196
1197        else:
1198            obj = None
1199            # vedo.logger.warning(f"Cannot import object {d}")
1200
1201        if obj:
1202            keys = d.keys()
1203            if "time" in keys: obj.time = d["time"]
1204            if "name" in keys: obj.name = d["name"]
1205            # if "info" in keys: obj.info = d["info"]
1206            if "filename" in keys: obj.filename = d["filename"]
1207            objs.append(obj)
1208
1209    plt.add(objs)
1210    plt.resetcam = False
1211    return plt
1212
1213###########################################################
1214def loadImageData(filename: Union[str, os.PathLike]) -> Union[vtki.vtkImageData, None]:
1215    """Read and return a `vtkImageData` object from file."""
1216    filename = str(filename)
1217    if ".ome.tif" in filename.lower():
1218        reader = vtki.new("OMETIFFReader")
1219        # print("GetOrientationType ", reader.GetOrientationType())
1220        reader.SetOrientationType(vedo.settings.tiff_orientation_type)
1221    elif ".tif" in filename.lower():
1222        reader = vtki.new("TIFFReader")
1223        # print("GetOrientationType ", reader.GetOrientationType())
1224        reader.SetOrientationType(vedo.settings.tiff_orientation_type)
1225    elif ".slc" in filename.lower():
1226        reader = vtki.new("SLCReader")
1227        if not reader.CanReadFile(filename):
1228            vedo.logger.error(f"sorry, bad SLC file {filename}")
1229            return None
1230    elif ".vti" in filename.lower():
1231        reader = vtki.new("XMLImageDataReader")
1232    elif ".mhd" in filename.lower():
1233        reader = vtki.new("MetaImageReader")
1234    elif ".dem" in filename.lower():
1235        reader = vtki.new("DEMReader")
1236    elif ".nii" in filename.lower():
1237        reader = vtki.new("NIFTIImageReader")
1238    elif ".nrrd" in filename.lower():
1239        reader = vtki.new("NrrdReader")
1240        if not reader.CanReadFile(filename):
1241            vedo.logger.error(f"sorry, bad NRRD file {filename}")
1242            return None
1243    else:
1244        vedo.logger.error(f"cannot read file {filename}")
1245        return None
1246    reader.SetFileName(filename)
1247    reader.Update()
1248    return reader.GetOutput()
1249
1250###########################################################
1251def write(objct: Any, fileoutput: Union[str, os.PathLike], binary=True) -> Any:
1252    """
1253    Write object to file. Same as `save()`.
1254
1255    Supported extensions are:
1256
1257    - `vtk, vti, ply, obj, stl, byu, vtp, vti, mhd, xyz, xml, tif, png, bmp`
1258    """
1259    fileoutput = str(fileoutput)
1260
1261    ###############################
1262    if isinstance(objct, Assembly):
1263        dd = to_numpy(objct)
1264        sdict = {"objects": [dd]}
1265        np.save(fileoutput, sdict)
1266        return objct
1267
1268    ###############################
1269    obj = objct.dataset
1270
1271    try:
1272        # check if obj is a Mesh.actor and has a transform
1273        M = objct.actor.GetMatrix()
1274        if M and not M.IsIdentity():
1275            obj = objct.apply_transform_from_actor()
1276            obj = objct.dataset
1277            vedo.logger.info(
1278                f"object '{objct.name}' "
1279                "was manually moved. Writing uses current position."
1280            )
1281    except:
1282        pass
1283
1284    fr = fileoutput.lower()
1285    if fr.endswith(".vtk"):
1286        writer = vtki.new("DataSetWriter")
1287    elif fr.endswith(".ply"):
1288        writer = vtki.new("PLYWriter")
1289        writer.AddComment("PLY file generated by vedo")
1290        lut = objct.mapper.GetLookupTable()
1291        if lut:
1292            pscal = obj.GetPointData().GetScalars()
1293            if not pscal:
1294                pscal = obj.GetCellData().GetScalars()
1295            if pscal and pscal.GetName():
1296                writer.SetArrayName(pscal.GetName())
1297            writer.SetLookupTable(lut)
1298    elif fr.endswith(".stl"):
1299        writer = vtki.new("STLWriter")
1300    elif fr.endswith(".vtp"):
1301        writer = vtki.new("XMLPolyDataWriter")
1302    elif fr.endswith(".vtu"):
1303        writer = vtki.new("XMLUnstructuredGridWriter")
1304    elif fr.endswith(".xyz"):
1305        writer = vtki.new("SimplePointsWriter")
1306    elif fr.endswith(".facet"):
1307        writer = vtki.new("FacetWriter")
1308    elif fr.endswith(".vti"):
1309        writer = vtki.new("XMLImageDataWriter")
1310    elif fr.endswith(".vtr"):
1311        writer = vtki.new("XMLRectilinearGridWriter")
1312    elif fr.endswith(".vtm"):
1313        g = vtki.new("MultiBlockDataGroupFilter")
1314        for ob in objct:
1315            try:
1316                g.AddInputData(ob)
1317            except TypeError:
1318                vedo.logger.warning(f"cannot save object of type {type(ob)}")
1319        g.Update()
1320        mb = g.GetOutputDataObject(0)
1321        wri = vtki.new("vtkXMLMultiBlockDataWriter")
1322        wri.SetInputData(mb)
1323        wri.SetFileName(fileoutput)
1324        wri.Write()
1325        return objct
1326    elif fr.endswith(".mhd"):
1327        writer = vtki.new("MetaImageWriter")
1328    elif fr.endswith(".nii"):
1329        writer = vtki.new("NIFTIImageWriter")
1330    elif fr.endswith(".png"):
1331        writer = vtki.new("PNGWriter")
1332    elif fr.endswith(".jpg"):
1333        writer = vtki.new("JPEGWriter")
1334    elif fr.endswith(".bmp"):
1335        writer = vtki.new("BMPWriter")
1336    elif fr.endswith(".tif") or fr.endswith(".tiff"):
1337        writer = vtki.new("TIFFWriter")
1338        writer.SetFileDimensionality(len(obj.GetDimensions()))
1339    elif fr.endswith(".obj"):
1340        with open(fileoutput, "w", encoding="UTF-8") as outF:
1341            outF.write("# OBJ file format with ext .obj\n")
1342            outF.write("# File generated by vedo\n")
1343
1344            for p in objct.vertices:
1345                outF.write("v {:.8g} {:.8g} {:.8g}\n".format(*p))
1346
1347            ptxt = objct.dataset.GetPointData().GetTCoords()
1348            if ptxt:
1349                ntxt = utils.vtk2numpy(ptxt)
1350                for vt in ntxt:
1351                    outF.write("vt " + str(vt[0]) + " " + str(vt[1]) + " 0.0\n")
1352
1353            if isinstance(objct, Mesh):
1354                for i, f in enumerate(objct.cells):
1355                    fs = ""
1356                    for fi in f:
1357                        if ptxt:
1358                            fs += f" {fi+1}/{fi+1}"
1359                        else:
1360                            fs += f" {fi+1}"
1361                    outF.write(f"f{fs}\n")
1362
1363                for l in objct.lines:
1364                    ls = ""
1365                    for li in l:
1366                        ls += str(li + 1) + " "
1367                    outF.write(f"l {ls}\n")
1368        return objct
1369
1370    elif fr.endswith(".off"):
1371        with open(fileoutput, "w", encoding="UTF-8") as outF:
1372            outF.write("OFF\n")
1373            outF.write(str(objct.npoints) + " " + str(objct.ncells) + " 0\n\n")
1374            for p in objct.vertices:
1375                outF.write(" ".join([str(i) for i in p]) + "\n")
1376            for c in objct.cells:
1377                outF.write(str(len(c)) + " " + " ".join([str(i) for i in c]) + "\n")
1378        return objct
1379
1380    elif fr.endswith(".xml"):  # write tetrahedral dolfin xml
1381        vertices = objct.vertices.astype(str)
1382        faces = np.array(objct.cells).astype(str)
1383        ncoords = vertices.shape[0]
1384        with open(fileoutput, "w", encoding="UTF-8") as outF:
1385            outF.write('<?xml version="1.0" encoding="UTF-8"?>\n')
1386            outF.write('<dolfin xmlns:dolfin="http://www.fenicsproject.org">\n')
1387
1388            if len(faces[0]) == 4:  # write tetrahedral mesh
1389                ntets = faces.shape[0]
1390                outF.write('  <mesh celltype="tetrahedron" dim="3">\n')
1391                outF.write('    <vertices size="' + str(ncoords) + '">\n')
1392                for i in range(ncoords):
1393                    x, y, z = vertices[i]
1394                    outF.write('      <vertex index="'+str(i)+'" x="'+x+'" y="'+y+'" z="'+z+'"/>\n')
1395                outF.write('    </vertices>\n')
1396                outF.write('    <cells size="' + str(ntets) + '">\n')
1397                for i in range(ntets):
1398                    v0, v1, v2, v3 = faces[i]
1399                    outF.write('     <tetrahedron index="'+str(i)
1400                               + '" v0="'+v0+'" v1="'+v1+'" v2="'+v2+'" v3="'+v3+'"/>\n')
1401
1402            elif len(faces[0]) == 3:  # write triangle mesh
1403                ntri = faces.shape[0]
1404                outF.write('  <mesh celltype="triangle" dim="2">\n')
1405                outF.write('    <vertices size="' + str(ncoords) + '">\n')
1406                for i in range(ncoords):
1407                    x, y, _ = vertices[i]
1408                    outF.write('      <vertex index="'+str(i)+'" x="'+x+'" y="'+y+'"/>\n')
1409                outF.write('    </vertices>\n')
1410                outF.write('    <cells size="' + str(ntri) + '">\n')
1411                for i in range(ntri):
1412                    v0, v1, v2 = faces[i]
1413                    outF.write('     <triangle index="'+str(i)+'" v0="'+v0+'" v1="'+v1+'" v2="'+v2+'"/>\n')
1414
1415            outF.write("    </cells>\n")
1416            outF.write("  </mesh>\n")
1417            outF.write("</dolfin>\n")
1418        return objct
1419
1420    else:
1421        vedo.logger.error(f"Unknown format {fileoutput}, file not saved")
1422        return objct
1423
1424    try:
1425        if binary:
1426            writer.SetFileTypeToBinary()
1427        else:
1428            writer.SetFileTypeToASCII()
1429    except AttributeError:
1430        pass
1431
1432    try:
1433        writer.SetInputData(obj)
1434        writer.SetFileName(fileoutput)
1435        writer.Write()
1436    except:
1437        vedo.logger.error(f"could not save {fileoutput}")
1438    return objct
1439
1440def save(obj: Any, fileoutput="out.png", binary=True) -> Any:
1441    """Save an object to file. Same as `write()`."""
1442    return write(obj, fileoutput, binary)
1443
1444def read(obj: Any, unpack=True, force=False) -> Any:
1445    """Read an object from file. Same as `load()`."""
1446    return load(obj, unpack, force)
1447
1448###############################################################################
1449def export_window(fileoutput: Union[str, os.PathLike], binary=False, plt=None) -> "vedo.Plotter":
1450    """
1451    Exporter which writes out the rendered scene into an HTML, X3D or Numpy file.
1452
1453    Example:
1454        - [export_x3d.py](https://github.com/marcomusy/vedo/tree/master/examples/other/export_x3d.py)
1455
1456        Check out the HTML generated webpage [here](https://vedo.embl.es/examples/embryo.html).
1457
1458        <img src='https://user-images.githubusercontent.com/32848391/57160341-c6ffbd80-6de8-11e9-95ff-7215ce642bc5.jpg' width="600"/>
1459
1460    .. note::
1461        the rendering window can also be exported to `numpy` file `scene.npz`
1462        by pressing `E` key at any moment during visualization.
1463    """
1464    fileoutput = str(fileoutput)
1465    if plt is None:
1466        plt = vedo.plotter_instance
1467
1468    fr = fileoutput.lower()
1469    ####################################################################
1470    if fr.endswith(".npy") or fr.endswith(".npz"):
1471        _export_npy(plt, fileoutput)
1472
1473    ####################################################################
1474    elif fr.endswith(".x3d"):
1475        # obj = plt.get_actors()
1476        # if plt.axes_instances:
1477        #     obj.append(plt.axes_instances[0])
1478
1479        # for a in obj:
1480        #     if isinstance(a, Assembly):
1481        #         plt.remove(a)
1482        #         plt.add(a.unpack())
1483
1484        plt.render()
1485
1486        exporter = vtki.new("X3DExporter")
1487        exporter.SetBinary(binary)
1488        exporter.FastestOff()
1489        exporter.SetInput(plt.window)
1490        exporter.SetFileName(fileoutput)
1491        # exporter.WriteToOutputStringOn()
1492        exporter.Update()
1493        exporter.Write()
1494
1495        wsize = plt.window.GetSize()
1496        x3d_html = _x3d_html_template.replace("~fileoutput", fileoutput)
1497        x3d_html = x3d_html.replace("~width",  str(wsize[0]))
1498        x3d_html = x3d_html.replace("~height", str(wsize[1]))
1499        with open(fileoutput.replace(".x3d", ".html"), "w", encoding="UTF-8") as outF:
1500            outF.write(x3d_html)
1501
1502    ####################################################################
1503    elif fr.endswith(".html"):
1504        savebk = vedo.notebook_backend
1505        vedo.notebook_backend = "k3d"
1506        vedo.settings.default_backend = "k3d"
1507        # acts = plt.get_actors()
1508        plt = vedo.backends.get_notebook_backend(plt.objects)
1509
1510        with open(fileoutput, "w", encoding="UTF-8") as fp:
1511            fp.write(plt.get_snapshot())
1512
1513        vedo.notebook_backend = savebk
1514        vedo.settings.default_backend = savebk
1515
1516    else:
1517        vedo.logger.error(f"export extension {fr.split('.')[-1]} is not supported")
1518
1519    return plt
1520
1521#########################################################################
1522def to_numpy(act: Any) -> dict:
1523    """Encode a vedo object to numpy format."""
1524
1525    ########################################################
1526    def _fillcommon(obj, adict):
1527        adict["filename"] = obj.filename
1528        adict["name"] = obj.name
1529        adict["time"] = obj.time
1530        adict["rendered_at"] = obj.rendered_at
1531        try:
1532            adict["transform"] = obj.transform.matrix
1533        except AttributeError:
1534            adict["transform"] = np.eye(4)
1535
1536    ####################################################################
1537    try:
1538        obj = act.retrieve_object()
1539    except AttributeError:
1540        obj = act
1541
1542    adict = {}
1543    adict["type"] = "unknown"
1544
1545    ######################################################## Points/Mesh
1546    if isinstance(obj, (Points, vedo.UnstructuredGrid)):
1547        adict["type"] = "Mesh"
1548        _fillcommon(obj, adict)
1549
1550        if isinstance(obj, vedo.UnstructuredGrid):
1551            # adict["type"] = "UnstructuredGrid"
1552            # adict["cells"] = obj.cells_as_flat_array
1553            poly = obj._actor.GetMapper().GetInput()
1554            mapper = obj._actor.GetMapper()
1555        else:
1556            poly = obj.dataset
1557            mapper = obj.mapper
1558
1559        adict["points"] = obj.vertices.astype(float)
1560
1561        adict["cells"] = None
1562        if poly.GetNumberOfPolys():
1563            adict["cells"] = obj.cells_as_flat_array
1564
1565        adict["lines"] = None
1566        if poly.GetNumberOfLines():
1567            adict["lines"] = obj.lines#_as_flat_array
1568
1569        adict["pointdata"] = {}
1570        for iname in obj.pointdata.keys():
1571            if "normals" in iname.lower():
1572                continue
1573            adict["pointdata"][iname] = obj.pointdata[iname]
1574
1575        adict["celldata"] = {}
1576        for iname in obj.celldata.keys():
1577            if "normals" in iname.lower():
1578                continue
1579            adict["celldata"][iname] = obj.celldata[iname]
1580
1581        adict["metadata"] = {}
1582        for iname in obj.metadata.keys():
1583            adict["metadata"][iname] = obj.metadata[iname]
1584
1585        # NEW in vedo 5.0
1586        adict["scalar_mode"] = mapper.GetScalarMode()
1587        adict["array_name_to_color_by"] = mapper.GetArrayName()
1588        adict["color_mode"] = mapper.GetColorMode()
1589        adict["interpolate_scalars_before_mapping"] = mapper.GetInterpolateScalarsBeforeMapping()
1590        adict["use_lookup_table_scalar_range"] = mapper.GetUseLookupTableScalarRange()
1591        adict["scalar_range"] = mapper.GetScalarRange()
1592        adict["scalar_visibility"] = mapper.GetScalarVisibility()
1593        adict["pickable"] = obj.actor.GetPickable()
1594        adict["dragable"] = obj.actor.GetDragable()
1595
1596        # adict["color_map_colors"]  = mapper.GetColorMapColors()   #vtkUnsignedCharArray
1597        # adict["color_coordinates"] = mapper.GetColorCoordinates() #vtkFloatArray
1598        texmap = mapper.GetColorTextureMap()  #vtkImageData
1599        if texmap:
1600            adict["color_texture_map"] = vedo.Image(texmap).tonumpy()
1601            # print("color_texture_map", adict["color_texture_map"].shape)
1602
1603        adict["texture_array"] = None
1604        texture = obj.actor.GetTexture()
1605        if texture:
1606            adict["texture_array"] = vedo.Image(texture.GetInput()).tonumpy()
1607            adict["texture_interpolate"] = texture.GetInterpolate()
1608            adict["texture_repeat"] = texture.GetRepeat()
1609            adict["texture_quality"] = texture.GetQuality()
1610            adict["texture_color_mode"] = texture.GetColorMode()
1611            adict["texture_mipmap"] = texture.GetMipmap()
1612            adict["texture_blending_mode"] = texture.GetBlendingMode()
1613            adict["texture_edge_clamp"] = texture.GetEdgeClamp()
1614            adict["texture_border_color"] = texture.GetBorderColor()
1615            # print("tonumpy: texture", obj.name, adict["texture_array"].shape)
1616
1617        adict["LUT"] = None
1618        adict["LUT_range"] = None
1619        lut = mapper.GetLookupTable()
1620        if lut:
1621            nlut = lut.GetNumberOfTableValues()
1622            lutvals = []
1623            for i in range(nlut):
1624                v4 = lut.GetTableValue(i)  # (r, g, b, alpha)
1625                lutvals.append(v4)
1626            adict["LUT"] = np.array(lutvals, dtype=np.float32)
1627            adict["LUT_range"] = np.array(lut.GetRange())
1628
1629        prp = obj.properties
1630        adict["alpha"] = prp.GetOpacity()
1631        adict["representation"] = prp.GetRepresentation()
1632        adict["pointsize"] = prp.GetPointSize()
1633
1634        adict["linecolor"] = None
1635        adict["linewidth"] = None
1636        adict["edge_visibility"] = prp.GetEdgeVisibility() # new in vedo 5.0
1637        if prp.GetEdgeVisibility():
1638            adict["linewidth"] = prp.GetLineWidth()
1639            adict["linecolor"] = prp.GetEdgeColor()
1640
1641        adict["ambient"] = prp.GetAmbient()
1642        adict["diffuse"] = prp.GetDiffuse()
1643        adict["specular"] = prp.GetSpecular()
1644        adict["specularpower"] = prp.GetSpecularPower()
1645        adict["specularcolor"] = prp.GetSpecularColor()
1646        adict["shading"] = prp.GetInterpolation()  # flat phong..:
1647        adict["color"] = prp.GetColor()
1648        adict["lighting_is_on"] = prp.GetLighting()
1649        adict["backcolor"] = None
1650        if obj.actor.GetBackfaceProperty():
1651            adict["backcolor"] = obj.actor.GetBackfaceProperty().GetColor()
1652
1653    ######################################################## Volume
1654    elif isinstance(obj, Volume):
1655        adict["type"] = "Volume"
1656        _fillcommon(obj, adict)
1657        adict["array"] = obj.tonumpy()
1658        adict["mode"] = obj.mode()
1659        adict["spacing"] = obj.spacing()
1660        adict["origin"] = obj.origin()
1661
1662        prp = obj.properties
1663        ctf = prp.GetRGBTransferFunction()
1664        otf = prp.GetScalarOpacity()
1665        gotf = prp.GetGradientOpacity()
1666        smin, smax = ctf.GetRange()
1667        xs = np.linspace(smin, smax, num=256, endpoint=True)
1668        cols, als, algrs = [], [], []
1669        for x in xs:
1670            cols.append(ctf.GetColor(x))
1671            als.append(otf.GetValue(x))
1672            if gotf:
1673                algrs.append(gotf.GetValue(x))
1674        adict["color"] = cols
1675        adict["alpha"] = als
1676        adict["alphagrad"] = algrs
1677
1678    ######################################################## Image
1679    elif isinstance(obj, Image):
1680        adict["type"] = "Image"
1681        _fillcommon(obj, adict)
1682        adict["array"] = obj.tonumpy()
1683        adict["scale"] = obj.actor.GetScale()
1684        adict["position"] = obj.actor.GetPosition()
1685        adict["orientation"] = obj.actor.GetOrientation()
1686        adict['origin'] = obj.actor.GetOrigin()
1687        adict["alpha"] = obj.alpha()
1688
1689    ######################################################## Text2D
1690    elif isinstance(obj, vedo.Text2D):
1691        adict["type"] = "Text2D"
1692        adict["rendered_at"] = obj.rendered_at
1693        adict["text"] = obj.text()
1694        adict["position"] = obj.GetPosition()
1695        adict["color"] = obj.properties.GetColor()
1696        adict["font"] = obj.fontname
1697        adict["size"] = obj.properties.GetFontSize() / 22.5
1698        adict["bgcol"] = obj.properties.GetBackgroundColor()
1699        adict["alpha"] = obj.properties.GetBackgroundOpacity()
1700        adict["frame"] = obj.properties.GetFrame()
1701
1702    ######################################################## Assembly
1703    elif isinstance(obj, Assembly):
1704        adict["type"] = "Assembly"
1705        _fillcommon(obj, adict)
1706        adict["parts"] = []
1707        for a in obj.unpack():
1708            adict["parts"].append(to_numpy(a))
1709
1710    else:
1711        # vedo.logger.warning(f"to_numpy: cannot export object of type {type(obj)}")
1712        pass
1713
1714    return adict
1715
1716
1717#########################################################################
1718def _export_npy(plt, fileoutput="scene.npz") -> None:
1719
1720    fileoutput = str(fileoutput)
1721
1722    sdict = {}
1723    sdict["shape"] = plt.shape
1724    sdict["sharecam"] = plt.sharecam
1725    sdict["camera"] = dict(
1726        pos=plt.camera.GetPosition(),
1727        focal_point=plt.camera.GetFocalPoint(),
1728        viewup=plt.camera.GetViewUp(),
1729        distance=plt.camera.GetDistance(),
1730        clipping_range=plt.camera.GetClippingRange(),
1731        parallel_scale=plt.camera.GetParallelScale(),
1732    )
1733    sdict["position"] = plt.pos
1734    sdict["size"] = plt.size
1735    sdict["axes"] = 0
1736    sdict["title"] = plt.title
1737    sdict["backgrcol"] = colors.get_color(plt.renderer.GetBackground())
1738    sdict["backgrcol2"] = None
1739    if plt.renderer.GetGradientBackground():
1740        sdict["backgrcol2"] = plt.renderer.GetBackground2()
1741    sdict["use_depth_peeling"] = plt.renderer.GetUseDepthPeeling()
1742    sdict["use_parallel_projection"] = plt.camera.GetParallelProjection()
1743    sdict["default_font"] = vedo.settings.default_font
1744
1745    sdict["objects"] = []
1746
1747    actors = plt.get_actors(include_non_pickables=True)
1748    # this ^ also retrieves Actors2D
1749    allobjs = []
1750    for i, a in enumerate(actors):
1751
1752        if not a.GetVisibility():
1753            continue
1754
1755        try:
1756            ob = a.retrieve_object()
1757            # print("get_actors",[ob], ob.name)
1758            if isinstance(ob, Assembly):
1759                asse_scale = ob.GetScale()
1760                asse_pos = ob.GetPosition()
1761                asse_ori = ob.GetOrientation()
1762                asse_org = ob.GetOrigin()
1763                for elem in ob.unpack():
1764                    elem.name = f"ASSEMBLY{i}_{ob.name}_{elem.name}"
1765                    # elem.info.update({"assembly": ob.name}) # TODO
1766                    # elem.info.update({"assembly_scale": asse_scale})
1767                    # elem.info.update({"assembly_position": asse_pos})
1768                    # elem.info.update({"assembly_orientation": asse_ori})
1769                    # elem.info.update({"assembly_origin": asse_org})
1770                    elem.metadata["assembly"] = ob.name
1771                    elem.metadata["assembly_scale"] = asse_scale
1772                    elem.metadata["assembly_position"] = asse_pos
1773                    elem.metadata["assembly_orientation"] = asse_ori
1774                    elem.metadata["assembly_origin"] = asse_org
1775                    allobjs.append(elem)
1776            else:
1777                allobjs.append(ob)
1778
1779        except AttributeError:
1780            # print()
1781            # vedo.logger.warning(f"Cannot retrieve object of type {type(a)}")
1782            pass
1783
1784    for a in allobjs:
1785        # print("to_numpy(): dumping", [a], a.name)
1786        # try:
1787        npobj = to_numpy(a)
1788        sdict["objects"].append(npobj)
1789        # except AttributeError:
1790        #     vedo.logger.warning(f"Cannot export object of type {type(a)}")
1791
1792    if fileoutput.endswith(".npz"):
1793        np.savez_compressed(fileoutput, vedo_scenes=[sdict])
1794    else:
1795        np.save(fileoutput, [sdict])
1796
1797
1798########################################################################
1799def import_window(fileinput: Union[str, os.PathLike]) -> Union["vedo.Plotter", None]:
1800    """
1801    Import a whole scene from a Numpy NPZ file.
1802
1803    Returns:
1804        `vedo.Plotter` instance
1805    """
1806    fileinput = str(fileinput)
1807
1808    if fileinput.endswith(".npy") or fileinput.endswith(".npz"):
1809        return _import_npy(fileinput)
1810
1811    # elif ".obj" in fileinput.lower():
1812    #     meshes = load_obj(fileinput, mtl_file, texture_path)
1813    #     plt = vedo.Plotter()
1814    #     plt.add(meshes)
1815    #     return plt
1816
1817    # elif fileinput.endswith(".h5") or fileinput.endswith(".hdf5"):
1818    #     return _import_hdf5(fileinput) # in store/file_io_HDF5.py
1819
1820    return None
1821
1822
1823def load_obj(fileinput: Union[str, os.PathLike], mtl_file=None, texture_path=None) -> List[Mesh]:
1824    """
1825    Import a set of meshes from a OBJ wavefront file.
1826
1827    Arguments:
1828        mtl_file : (str)
1829            MTL file for OBJ wavefront files
1830        texture_path : (str)
1831            path of the texture files directory
1832
1833    Returns:
1834        `list(Mesh)`
1835    """
1836    fileinput = str(fileinput)
1837
1838    window = vtki.vtkRenderWindow()
1839    window.SetOffScreenRendering(1)
1840    renderer = vtki.vtkRenderer()
1841    window.AddRenderer(renderer)
1842
1843    importer = vtki.new("OBJImporter")
1844    importer.SetFileName(fileinput)
1845    if mtl_file is None:
1846        mtl_file = fileinput.replace(".obj", ".mtl").replace(".OBJ", ".MTL")
1847    if os.path.isfile(mtl_file):
1848        importer.SetFileNameMTL(mtl_file)
1849    if texture_path is None:
1850        texture_path = fileinput.replace(".obj", ".txt").replace(".OBJ", ".TXT")
1851    # since the texture_path may be a directory which contains textures
1852    if os.path.exists(texture_path):
1853        importer.SetTexturePath(texture_path)
1854    importer.SetRenderWindow(window)
1855    importer.Update()
1856
1857    actors = renderer.GetActors()
1858    actors.InitTraversal()
1859    objs = []
1860    for _ in range(actors.GetNumberOfItems()):
1861        vactor = actors.GetNextActor()
1862        msh = Mesh(vactor)
1863        msh.name = "OBJMesh"
1864        msh.copy_properties_from(vactor)
1865        tx = vactor.GetTexture()
1866        if tx:
1867            msh.texture(tx)
1868        objs.append(msh)
1869    return objs
1870
1871
1872##########################################################
1873def screenshot(filename="screenshot.png", scale=1, asarray=False) -> Union["vedo.Plotter", np.ndarray, None]:
1874    """
1875    Save a screenshot of the current rendering window.
1876
1877    Alternatively, press key `Shift-S` in the rendering window to save a screenshot.
1878    You can also use keyword `screenshot` in `show(..., screenshot="pic.png")`.
1879
1880    Arguments:
1881        scale : (int)
1882            Set image magnification as an integer multiplicative factor.
1883            E.g. setting a magnification of 2 produces an image twice as large,
1884            but 10x slower to generate.
1885        asarray : (bool)
1886            Return a numpy array of the image
1887    """
1888    filename = str(filename)
1889    # print("calling screenshot", filename, scale, asarray)
1890
1891    if not vedo.plotter_instance or not vedo.plotter_instance.window:
1892        # vedo.logger.error("in screenshot(), rendering window is not present, skip.")
1893        return vedo.plotter_instance  ##########
1894
1895    if vedo.plotter_instance.renderer:
1896        vedo.plotter_instance.renderer.ResetCameraClippingRange()
1897
1898    if asarray and scale == 1 and not vedo.plotter_instance.offscreen:
1899        nx, ny = vedo.plotter_instance.window.GetSize()
1900        arr = vtki.vtkUnsignedCharArray()
1901        vedo.plotter_instance.window.GetRGBACharPixelData(0, 0, nx-1, ny-1, 0, arr)
1902        narr = vedo.vtk2numpy(arr).T[:3].T.reshape([ny, nx, 3])
1903        narr = np.flip(narr, axis=0)
1904        return narr  ##########
1905
1906    ###########################
1907    if filename.endswith(".pdf"):
1908        writer = vtki.new("GL2PSExporter")
1909        writer.SetRenderWindow(vedo.plotter_instance.window)
1910        writer.Write3DPropsAsRasterImageOff()
1911        writer.SilentOn()
1912        writer.SetSortToBSP()
1913        writer.SetFileFormatToPDF()
1914        writer.SetFilePrefix(filename.replace(".pdf", ""))
1915        writer.Write()
1916        return vedo.plotter_instance  ##########
1917
1918    elif filename.endswith(".svg"):
1919        writer = vtki.new("GL2PSExporter")
1920        writer.SetRenderWindow(vedo.plotter_instance.window)
1921        writer.Write3DPropsAsRasterImageOff()
1922        writer.SilentOn()
1923        writer.SetSortToBSP()
1924        writer.SetFileFormatToSVG()
1925        writer.SetFilePrefix(filename.replace(".svg", ""))
1926        writer.Write()
1927        return vedo.plotter_instance  ##########
1928
1929    elif filename.endswith(".eps"):
1930        writer = vtki.new("GL2PSExporter")
1931        writer.SetRenderWindow(vedo.plotter_instance.window)
1932        writer.Write3DPropsAsRasterImageOff()
1933        writer.SilentOn()
1934        writer.SetSortToBSP()
1935        writer.SetFileFormatToEPS()
1936        writer.SetFilePrefix(filename.replace(".eps", ""))
1937        writer.Write()
1938        return vedo.plotter_instance  ##########
1939
1940    if settings.screeshot_large_image:
1941        w2if = vtki.new("RenderLargeImage")
1942        w2if.SetInput(vedo.plotter_instance.renderer)
1943        w2if.SetMagnification(scale)
1944    else:
1945        w2if = vtki.new("WindowToImageFilter")
1946        w2if.SetInput(vedo.plotter_instance.window)
1947        if hasattr(w2if, "SetScale"):
1948            w2if.SetScale(int(scale), int(scale))
1949        if settings.screenshot_transparent_background:
1950            w2if.SetInputBufferTypeToRGBA()
1951        w2if.ReadFrontBufferOff()  # read from the back buffer
1952    w2if.Update()
1953
1954    if asarray:
1955        pd = w2if.GetOutput().GetPointData()
1956        npdata = utils.vtk2numpy(pd.GetArray("ImageScalars"))
1957        # npdata = npdata[:, [0, 1, 2]]  # remove alpha channel, issue #1199
1958        ydim, xdim, _ = w2if.GetOutput().GetDimensions()
1959        npdata = npdata.reshape([xdim, ydim, -1])
1960        npdata = np.flip(npdata, axis=0)
1961        return npdata ###########################
1962
1963    if filename.lower().endswith(".png"):
1964        writer = vtki.new("PNGWriter")
1965        writer.SetFileName(filename)
1966        writer.SetInputData(w2if.GetOutput())
1967        writer.Write()
1968    elif filename.lower().endswith(".jpg") or filename.lower().endswith(".jpeg"):
1969        writer = vtki.new("JPEGWriter")
1970        writer.SetFileName(filename)
1971        writer.SetInputData(w2if.GetOutput())
1972        writer.Write()
1973    else:  # add .png
1974        writer = vtki.new("PNGWriter")
1975        writer.SetFileName(filename + ".png")
1976        writer.SetInputData(w2if.GetOutput())
1977        writer.Write()
1978    return vedo.plotter_instance
1979
1980
1981def ask(*question, **kwarg) -> str:
1982    """
1983    Ask a question from command line. Return the answer as a string.
1984    See function `colors.printc()` for the description of the keyword options.
1985
1986    Arguments:
1987        options : (list)
1988            a python list of possible answers to choose from.
1989        default : (str)
1990            the default answer when just hitting return.
1991
1992    Example:
1993    ```python
1994    import vedo
1995    res = vedo.ask("Continue?", options=['Y','n'], default='Y', c='g')
1996    print(res)
1997    ```
1998    """
1999    kwarg.update({"end": " "})
2000    if "invert" not in kwarg:
2001        kwarg.update({"invert": True})
2002    if "box" in kwarg:
2003        kwarg.update({"box": ""})
2004
2005    options = kwarg.pop("options", [])
2006    default = kwarg.pop("default", "")
2007    if options:
2008        opt = "["
2009        for o in options:
2010            opt += o + "/"
2011        opt = opt[:-1] + "]"
2012        colors.printc(*question, opt, **kwarg)
2013    else:
2014        colors.printc(*question, **kwarg)
2015
2016    try:
2017        resp = input()
2018    except Exception:
2019        resp = ""
2020        return resp
2021
2022    if options:
2023        if resp not in options:
2024            if default and str(repr(resp)) == "''":
2025                return default
2026            colors.printc("Please choose one option in:", opt, italic=True, bold=False)
2027            kwarg["options"] = options
2028            return ask(*question, **kwarg)  # ask again
2029    return resp
2030
2031
2032##############################################################################################
2033class Video:
2034    """
2035    Generate a video from a rendering window.
2036    """
2037
2038    def __init__(self, name="movie.mp4", duration=None, fps=24, scale=1, backend="imageio"):
2039        """
2040        Class to generate a video from the specified rendering window.
2041        Program `ffmpeg` is used to create video from each generated frame.
2042
2043        Arguments:
2044            name : (Union[str, os.PathLike])
2045                name of the output file.
2046            duration : (float)
2047                set the total `duration` of the video and recalculates `fps` accordingly.
2048            fps : (int)
2049                set the number of frames per second.
2050            scale : (int)
2051                set the image magnification as an integer multiplicative factor.
2052            backend : (str)
2053                the backend engine to be used `['imageio', 'ffmpeg', 'cv']`
2054
2055        Examples:
2056            - [make_video.py](https://github.com/marcomusy/vedo/tree/master/examples/other/make_video.py)
2057
2058            ![](https://user-images.githubusercontent.com/32848391/50739007-2bfc2b80-11da-11e9-97e6-620a3541a6fa.jpg)
2059        """
2060        self.name = str(name)
2061        self.duration = duration
2062        self.backend = backend
2063        self.fps = float(fps)
2064        self.command = "ffmpeg -loglevel panic -y -r"
2065        self.options = "-b:v 8000k"
2066        self.scale = scale
2067
2068        self.frames = []
2069        self.tmp_dir = TemporaryDirectory()
2070        self.get_filename = lambda x: os.path.join(self.tmp_dir.name, x)
2071        colors.printc(":video:  Video file", self.name, "is open... ", c="m", end="")
2072
2073    def add_frame(self) -> "Video":
2074        """Add frame to current video."""
2075        fr = self.get_filename(str(len(self.frames)) + ".png")
2076        screenshot(fr, scale=self.scale)
2077        self.frames.append(fr)
2078        return self
2079
2080    def pause(self, pause=0) -> "Video":
2081        """Insert a `pause`, in seconds."""
2082        fr = self.frames[-1]
2083        n = int(self.fps * pause)
2084        for _ in range(n):
2085            fr2 = self.get_filename(str(len(self.frames)) + ".png")
2086            self.frames.append(fr2)
2087            os.system("cp -f %s %s" % (fr, fr2))
2088        return self
2089
2090    def action(self, elevation=(0, 80), azimuth=(0, 359), cameras=(), resetcam=False) -> "Video":
2091        """
2092        Automatic shooting of a static scene by specifying rotation and elevation ranges.
2093
2094        Arguments:
2095            elevation : list
2096                initial and final elevation angles
2097            azimuth_range : list
2098                initial and final azimuth angles
2099            cameras : list
2100                list of cameras to go through, each camera can be dictionary or a vtkCamera
2101        """
2102        if not self.duration:
2103            self.duration = 5
2104
2105        plt = vedo.plotter_instance
2106        if not plt:
2107            vedo.logger.error("No vedo plotter found, cannot make video.")
2108            return self
2109        n = int(self.fps * self.duration)
2110
2111        cams = []
2112        for cm in cameras:
2113            cams.append(utils.camera_from_dict(cm))
2114        nc = len(cams)
2115
2116        plt.show(resetcam=resetcam, interactive=False)
2117
2118        if nc:
2119            for i in range(n):
2120                plt.move_camera(cams, i / (n-1))
2121                plt.render()
2122                self.add_frame()
2123
2124        else:  ########################################
2125
2126            for i in range(n):
2127                plt.camera.Elevation((elevation[1] - elevation[0]) / n)
2128                plt.camera.Azimuth((azimuth[1] - azimuth[0]) / n)
2129                plt.render()
2130                self.add_frame()
2131
2132        return self
2133
2134    def close(self) -> None:
2135        """
2136        Render the video and write it to file.
2137        """
2138        if self.duration:
2139            self.fps = int(len(self.frames) / float(self.duration) + 0.5)
2140            colors.printc("recalculated fps:", self.fps, c="m", end="")
2141        else:
2142            self.fps = int(self.fps)
2143
2144        ########################################
2145        if self.backend == "ffmpeg":
2146            out = os.system(
2147                self.command
2148                + " "
2149                + str(self.fps)
2150                + " -i "
2151                + f"'{self.tmp_dir.name}'"
2152                + os.sep
2153                + "%01d.png "
2154                + self.options
2155                + " "
2156                + f"'{self.name}'"
2157            )
2158            if out:
2159                vedo.logger.error(f":noentry: backend {self.backend} returning error: {out}")
2160            else:
2161                colors.printc(f":save: saved to {self.name}", c="m")
2162
2163        ########################################
2164        elif "cv" in self.backend:
2165            try:
2166                import cv2  # type: ignore
2167            except ImportError:
2168                vedo.logger.error("opencv is not installed")
2169                return
2170
2171            cap = cv2.VideoCapture(os.path.join(self.tmp_dir.name, "%1d.png"))
2172            fourcc = cv2.VideoWriter_fourcc(*"mp4v")
2173            if vedo.plotter_instance:
2174                w, h = vedo.plotter_instance.window.GetSize()
2175                writer = cv2.VideoWriter(self.name, fourcc, self.fps, (w, h), True)
2176            else:
2177                vedo.logger.error("No vedo plotter found, cannot make video.")
2178                return
2179
2180            while True:
2181                ret, frame = cap.read()
2182                if not ret:
2183                    break
2184                writer.write(frame)
2185
2186            cap.release()
2187            writer.release()
2188
2189        ########################################
2190        elif "imageio" in self.backend:
2191            try:
2192                import imageio
2193            except ImportError:
2194                vedo.logger.error("Please install imageio with:\n pip install imageio[ffmpeg]")
2195                return
2196
2197            if self.name.endswith(".mp4"):
2198                writer = imageio.get_writer(self.name, fps=self.fps)
2199            elif self.name.endswith(".gif"):
2200                writer = imageio.get_writer(self.name, mode="I", duration=1 / self.fps)
2201            elif self.name.endswith(".webm"):
2202                writer = imageio.get_writer(self.name, format="webm", fps=self.fps)
2203            else:
2204                vedo.logger.error(f"Unknown format of {self.name}.")
2205                return
2206
2207            for f in utils.humansort(self.frames):
2208                image = imageio.v3.imread(f)
2209                try:
2210                    writer.append_data(image)
2211                except TypeError:
2212                    vedo.logger.error(f"Could not append data to video {self.name}")
2213                    vedo.logger.error("Please install imageio with: pip install imageio[ffmpeg]")
2214                    break
2215            try:
2216                writer.close()
2217                colors.printc(f"... saved as {self.name}", c="m")
2218            except:
2219                colors.printc(f":noentry: Could not save video {self.name}", c="r")
2220
2221        # finalize cleanup
2222        self.tmp_dir.cleanup()
2223
2224    def split_frames(self, output_dir="video_frames", prefix="frame_", file_format="png") -> None:
2225        """Split an existing video file into frames."""
2226        try:
2227            import imageio
2228        except ImportError:
2229            vedo.logger.error("\nPlease install imageio with:\n pip install imageio")
2230            return
2231
2232        # Create the output directory if it doesn't exist
2233        if not os.path.exists(output_dir):
2234            os.makedirs(output_dir)
2235
2236        # Create a reader object to read the video
2237        reader = imageio.get_reader(self.name)
2238
2239        # Loop through each frame of the video and save it as image
2240        print()
2241        for i, frame in utils.progressbar(
2242            enumerate(reader), title=f"writing {file_format} frames", c="m", width=20
2243        ):
2244            output_file = os.path.join(output_dir, f"{prefix}{str(i).zfill(5)}.{format}")
2245            imageio.imwrite(output_file, frame, format=file_format)
def load(inputobj: Union[list, str, os.PathLike], unpack=True, force=False) -> Any:
177def load(inputobj: Union[list, str, os.PathLike], unpack=True, force=False) -> Any:
178    """
179    Load any vedo objects from file or from the web.
180
181    The output will depend on the file extension. See examples below.
182    Unzip is made on the fly, if file ends with `.gz`.
183    Can load an object directly from a URL address.
184
185    Arguments:
186        unpack : (bool)
187            unpack MultiBlockData into a flat list of objects.
188        force : (bool)
189            when downloading a file ignore any previous cached downloads and force a new one.
190
191    Example:
192        ```python
193        from vedo import dataurl, load, show
194        # Return a list of 2 meshes
195        g = load([dataurl+'250.vtk', dataurl+'270.vtk'])
196        show(g)
197        # Return a list of meshes by reading all files in a directory
198        # (if directory contains DICOM files then a Volume is returned)
199        g = load('mydicomdir/')
200        show(g)
201        ```
202    """
203    if isinstance(inputobj, list):
204        inputobj = [str(f) for f in inputobj]
205    else:
206        inputobj = str(inputobj)
207
208    acts = []
209    if utils.is_sequence(inputobj):
210        flist = inputobj
211    elif isinstance(inputobj, str) and inputobj.startswith("https://"):
212        flist = [inputobj]
213    else:
214        flist = utils.humansort(glob.glob(inputobj))
215
216    for fod in flist:
217
218        if fod.startswith("https://"):
219            fod = download(fod, force=force, verbose=False)
220
221        if os.path.isfile(fod):  ### it's a file
222
223            if fod.endswith(".gz"):
224                fod = gunzip(fod)
225
226            a = _load_file(fod, unpack)
227            acts.append(a)
228
229        elif os.path.isdir(fod):  ### it's a directory or DICOM
230            flist = os.listdir(fod)
231            if ".dcm" in flist[0]:  ### it's DICOM
232                reader = vtki.new("DICOMImageReader")
233                reader.SetDirectoryName(fod)
234                reader.Update()
235                image = reader.GetOutput()
236                vol = Volume(image)
237                try:
238                    vol.metadata["PixelSpacing"] = reader.GetPixelSpacing()
239                    vol.metadata["Width"] = reader.GetWidth()
240                    vol.metadata["Height"] = reader.GetHeight()
241                    vol.metadata["PositionPatient"] = reader.GetImagePositionPatient()
242                    vol.metadata["OrientationPatient"] = reader.GetImageOrientationPatient()
243                    vol.metadata["BitsAllocated"] = reader.GetBitsAllocated()
244                    vol.metadata["PixelRepresentation"] = reader.GetPixelRepresentation()
245                    vol.metadata["NumberOfComponents"] = reader.GetNumberOfComponents()
246                    vol.metadata["TransferSyntaxUID"] = reader.GetTransferSyntaxUID()
247                    vol.metadata["RescaleSlope"] = reader.GetRescaleSlope()
248                    vol.metadata["RescaleOffset"] = reader.GetRescaleOffset()
249                    vol.metadata["PatientName"] = reader.GetPatientName()
250                    vol.metadata["StudyUID"] = reader.GetStudyUID()
251                    vol.metadata["StudyID"] = reader.GetStudyID()
252                    vol.metadata["GantryAngle"] = reader.GetGantryAngle()
253                except Exception as e:
254                    vedo.logger.warning(f"Cannot read DICOM metadata: {e}")
255                acts.append(vol)
256
257            else:  ### it's a normal directory
258                utils.humansort(flist)
259                for ifile in flist:
260                    a = _load_file(fod + "/" + ifile, unpack)
261                    acts.append(a)
262        else:
263            vedo.logger.error(f"in load(), cannot find {fod}")
264
265    if len(acts) == 1:
266        if "numpy" in str(type(acts[0])):
267            return acts[0]
268        if not acts[0]:
269            vedo.logger.error(f"in load(), cannot load {inputobj}")
270        return acts[0]
271
272    if len(acts) == 0:
273        vedo.logger.error(f"in load(), cannot load {inputobj}")
274        return None
275
276    else:
277        return acts

Load any vedo objects from file or from the web.

The output will depend on the file extension. See examples below. Unzip is made on the fly, if file ends with .gz. Can load an object directly from a URL address.

Arguments:
  • unpack : (bool) unpack MultiBlockData into a flat list of objects.
  • force : (bool) when downloading a file ignore any previous cached downloads and force a new one.
Example:
from vedo import dataurl, load, show
# Return a list of 2 meshes
g = load([dataurl+'250.vtk', dataurl+'270.vtk'])
show(g)
# Return a list of meshes by reading all files in a directory
# (if directory contains DICOM files then a Volume is returned)
g = load('mydicomdir/')
show(g)
def read(obj: Any, unpack=True, force=False) -> Any:
1445def read(obj: Any, unpack=True, force=False) -> Any:
1446    """Read an object from file. Same as `load()`."""
1447    return load(obj, unpack, force)

Read an object from file. Same as load().

def download(url: str, force=False, verbose=True) -> str:
453def download(url: str, force=False, verbose=True) -> str:
454    """
455    Retrieve a file from a URL, save it locally and return its path.
456    Use `force=True` to force a reload and discard cached copies.
457    """
458    if not url.startswith("https://"):
459        # assume it's a file so no need to download
460        return url
461    url = url.replace("www.dropbox", "dl.dropbox")
462
463    if "github.com" in url:
464        url = url.replace("/blob/", "/raw/")
465
466    basename = os.path.basename(url)
467
468    if "?" in basename:
469        basename = basename.split("?")[0]
470
471    home_directory = os.path.expanduser("~")
472    cachedir = os.path.join(home_directory, settings.cache_directory, "vedo")
473    fname = os.path.join(cachedir, basename)
474    # Create the directory if it does not exist
475    if not os.path.exists(cachedir):
476        os.makedirs(cachedir)
477
478    if not force and os.path.exists(fname):
479        if verbose:
480            colors.printc("reusing cached file:", fname)
481        return fname
482
483    try:
484        from urllib.request import urlopen, Request
485        req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
486        if verbose:
487            colors.printc("reading", basename, "from", url.split("/")[2][:40], "...", end="")
488
489    except ImportError:
490        import urllib2 # type: ignore
491        import contextlib
492        urlopen = lambda url_: contextlib.closing(urllib2.urlopen(url_))
493        req = url
494        if verbose:
495            colors.printc("reading", basename, "from", url.split("/")[2][:40], "...", end="")
496
497    with urlopen(req) as response, open(fname, "wb") as output:
498        output.write(response.read())
499
500    if verbose:
501        colors.printc(" done.")
502    return fname

Retrieve a file from a URL, save it locally and return its path. Use force=True to force a reload and discard cached copies.

def gunzip(filename: str) -> str:
582def gunzip(filename: str) -> str:
583    """Unzip a `.gz` file to a temporary file and returns its path."""
584    if not filename.endswith(".gz"):
585        # colors.printc("gunzip() error: file must end with .gz", c='r')
586        return filename
587
588    import gzip
589
590    tmp_file = NamedTemporaryFile(delete=False)
591    tmp_file.name = os.path.join(
592        os.path.dirname(tmp_file.name), os.path.basename(filename).replace(".gz", "")
593    )
594    inF = gzip.open(filename, "rb")
595    with open(tmp_file.name, "wb") as outF:
596        outF.write(inF.read())
597    inF.close()
598    return tmp_file.name

Unzip a .gz file to a temporary file and returns its path.

def loadStructuredPoints(filename: Union[str, os.PathLike], as_points=True):
617def loadStructuredPoints(filename: Union[str, os.PathLike], as_points=True):
618    """
619    Load and return a `vtkStructuredPoints` object from file.
620
621    If `as_points` is True, return a `Points` object
622    instead of a `vtkStructuredPoints`.
623    """
624    filename = str(filename)
625    reader = vtki.new("StructuredPointsReader")
626    reader.SetFileName(filename)
627    reader.Update()
628    if as_points:
629        v2p = vtki.new("ImageToPoints")
630        v2p.SetInputData(reader.GetOutput())
631        v2p.Update()
632        pts = Points(v2p.GetOutput())
633        return pts
634    return reader.GetOutput()

Load and return a vtkStructuredPoints object from file.

If as_points is True, return a Points object instead of a vtkStructuredPoints.

def loadStructuredGrid(filename: Union[str, os.PathLike]):
637def loadStructuredGrid(filename: Union[str, os.PathLike]):
638    """Load and return a `vtkStructuredGrid` object from file."""
639    filename = str(filename)
640    if filename.endswith(".vts"):
641        reader = vtki.new("XMLStructuredGridReader")
642    else:
643        reader = vtki.new("StructuredGridReader")
644    reader.SetFileName(filename)
645    reader.Update()
646    return reader.GetOutput()

Load and return a vtkStructuredGrid object from file.

def write(objct: Any, fileoutput: Union[str, os.PathLike], binary=True) -> Any:
1252def write(objct: Any, fileoutput: Union[str, os.PathLike], binary=True) -> Any:
1253    """
1254    Write object to file. Same as `save()`.
1255
1256    Supported extensions are:
1257
1258    - `vtk, vti, ply, obj, stl, byu, vtp, vti, mhd, xyz, xml, tif, png, bmp`
1259    """
1260    fileoutput = str(fileoutput)
1261
1262    ###############################
1263    if isinstance(objct, Assembly):
1264        dd = to_numpy(objct)
1265        sdict = {"objects": [dd]}
1266        np.save(fileoutput, sdict)
1267        return objct
1268
1269    ###############################
1270    obj = objct.dataset
1271
1272    try:
1273        # check if obj is a Mesh.actor and has a transform
1274        M = objct.actor.GetMatrix()
1275        if M and not M.IsIdentity():
1276            obj = objct.apply_transform_from_actor()
1277            obj = objct.dataset
1278            vedo.logger.info(
1279                f"object '{objct.name}' "
1280                "was manually moved. Writing uses current position."
1281            )
1282    except:
1283        pass
1284
1285    fr = fileoutput.lower()
1286    if fr.endswith(".vtk"):
1287        writer = vtki.new("DataSetWriter")
1288    elif fr.endswith(".ply"):
1289        writer = vtki.new("PLYWriter")
1290        writer.AddComment("PLY file generated by vedo")
1291        lut = objct.mapper.GetLookupTable()
1292        if lut:
1293            pscal = obj.GetPointData().GetScalars()
1294            if not pscal:
1295                pscal = obj.GetCellData().GetScalars()
1296            if pscal and pscal.GetName():
1297                writer.SetArrayName(pscal.GetName())
1298            writer.SetLookupTable(lut)
1299    elif fr.endswith(".stl"):
1300        writer = vtki.new("STLWriter")
1301    elif fr.endswith(".vtp"):
1302        writer = vtki.new("XMLPolyDataWriter")
1303    elif fr.endswith(".vtu"):
1304        writer = vtki.new("XMLUnstructuredGridWriter")
1305    elif fr.endswith(".xyz"):
1306        writer = vtki.new("SimplePointsWriter")
1307    elif fr.endswith(".facet"):
1308        writer = vtki.new("FacetWriter")
1309    elif fr.endswith(".vti"):
1310        writer = vtki.new("XMLImageDataWriter")
1311    elif fr.endswith(".vtr"):
1312        writer = vtki.new("XMLRectilinearGridWriter")
1313    elif fr.endswith(".vtm"):
1314        g = vtki.new("MultiBlockDataGroupFilter")
1315        for ob in objct:
1316            try:
1317                g.AddInputData(ob)
1318            except TypeError:
1319                vedo.logger.warning(f"cannot save object of type {type(ob)}")
1320        g.Update()
1321        mb = g.GetOutputDataObject(0)
1322        wri = vtki.new("vtkXMLMultiBlockDataWriter")
1323        wri.SetInputData(mb)
1324        wri.SetFileName(fileoutput)
1325        wri.Write()
1326        return objct
1327    elif fr.endswith(".mhd"):
1328        writer = vtki.new("MetaImageWriter")
1329    elif fr.endswith(".nii"):
1330        writer = vtki.new("NIFTIImageWriter")
1331    elif fr.endswith(".png"):
1332        writer = vtki.new("PNGWriter")
1333    elif fr.endswith(".jpg"):
1334        writer = vtki.new("JPEGWriter")
1335    elif fr.endswith(".bmp"):
1336        writer = vtki.new("BMPWriter")
1337    elif fr.endswith(".tif") or fr.endswith(".tiff"):
1338        writer = vtki.new("TIFFWriter")
1339        writer.SetFileDimensionality(len(obj.GetDimensions()))
1340    elif fr.endswith(".obj"):
1341        with open(fileoutput, "w", encoding="UTF-8") as outF:
1342            outF.write("# OBJ file format with ext .obj\n")
1343            outF.write("# File generated by vedo\n")
1344
1345            for p in objct.vertices:
1346                outF.write("v {:.8g} {:.8g} {:.8g}\n".format(*p))
1347
1348            ptxt = objct.dataset.GetPointData().GetTCoords()
1349            if ptxt:
1350                ntxt = utils.vtk2numpy(ptxt)
1351                for vt in ntxt:
1352                    outF.write("vt " + str(vt[0]) + " " + str(vt[1]) + " 0.0\n")
1353
1354            if isinstance(objct, Mesh):
1355                for i, f in enumerate(objct.cells):
1356                    fs = ""
1357                    for fi in f:
1358                        if ptxt:
1359                            fs += f" {fi+1}/{fi+1}"
1360                        else:
1361                            fs += f" {fi+1}"
1362                    outF.write(f"f{fs}\n")
1363
1364                for l in objct.lines:
1365                    ls = ""
1366                    for li in l:
1367                        ls += str(li + 1) + " "
1368                    outF.write(f"l {ls}\n")
1369        return objct
1370
1371    elif fr.endswith(".off"):
1372        with open(fileoutput, "w", encoding="UTF-8") as outF:
1373            outF.write("OFF\n")
1374            outF.write(str(objct.npoints) + " " + str(objct.ncells) + " 0\n\n")
1375            for p in objct.vertices:
1376                outF.write(" ".join([str(i) for i in p]) + "\n")
1377            for c in objct.cells:
1378                outF.write(str(len(c)) + " " + " ".join([str(i) for i in c]) + "\n")
1379        return objct
1380
1381    elif fr.endswith(".xml"):  # write tetrahedral dolfin xml
1382        vertices = objct.vertices.astype(str)
1383        faces = np.array(objct.cells).astype(str)
1384        ncoords = vertices.shape[0]
1385        with open(fileoutput, "w", encoding="UTF-8") as outF:
1386            outF.write('<?xml version="1.0" encoding="UTF-8"?>\n')
1387            outF.write('<dolfin xmlns:dolfin="http://www.fenicsproject.org">\n')
1388
1389            if len(faces[0]) == 4:  # write tetrahedral mesh
1390                ntets = faces.shape[0]
1391                outF.write('  <mesh celltype="tetrahedron" dim="3">\n')
1392                outF.write('    <vertices size="' + str(ncoords) + '">\n')
1393                for i in range(ncoords):
1394                    x, y, z = vertices[i]
1395                    outF.write('      <vertex index="'+str(i)+'" x="'+x+'" y="'+y+'" z="'+z+'"/>\n')
1396                outF.write('    </vertices>\n')
1397                outF.write('    <cells size="' + str(ntets) + '">\n')
1398                for i in range(ntets):
1399                    v0, v1, v2, v3 = faces[i]
1400                    outF.write('     <tetrahedron index="'+str(i)
1401                               + '" v0="'+v0+'" v1="'+v1+'" v2="'+v2+'" v3="'+v3+'"/>\n')
1402
1403            elif len(faces[0]) == 3:  # write triangle mesh
1404                ntri = faces.shape[0]
1405                outF.write('  <mesh celltype="triangle" dim="2">\n')
1406                outF.write('    <vertices size="' + str(ncoords) + '">\n')
1407                for i in range(ncoords):
1408                    x, y, _ = vertices[i]
1409                    outF.write('      <vertex index="'+str(i)+'" x="'+x+'" y="'+y+'"/>\n')
1410                outF.write('    </vertices>\n')
1411                outF.write('    <cells size="' + str(ntri) + '">\n')
1412                for i in range(ntri):
1413                    v0, v1, v2 = faces[i]
1414                    outF.write('     <triangle index="'+str(i)+'" v0="'+v0+'" v1="'+v1+'" v2="'+v2+'"/>\n')
1415
1416            outF.write("    </cells>\n")
1417            outF.write("  </mesh>\n")
1418            outF.write("</dolfin>\n")
1419        return objct
1420
1421    else:
1422        vedo.logger.error(f"Unknown format {fileoutput}, file not saved")
1423        return objct
1424
1425    try:
1426        if binary:
1427            writer.SetFileTypeToBinary()
1428        else:
1429            writer.SetFileTypeToASCII()
1430    except AttributeError:
1431        pass
1432
1433    try:
1434        writer.SetInputData(obj)
1435        writer.SetFileName(fileoutput)
1436        writer.Write()
1437    except:
1438        vedo.logger.error(f"could not save {fileoutput}")
1439    return objct

Write object to file. Same as save().

Supported extensions are:

  • vtk, vti, ply, obj, stl, byu, vtp, vti, mhd, xyz, xml, tif, png, bmp
def save(obj: Any, fileoutput='out.png', binary=True) -> Any:
1441def save(obj: Any, fileoutput="out.png", binary=True) -> Any:
1442    """Save an object to file. Same as `write()`."""
1443    return write(obj, fileoutput, binary)

Save an object to file. Same as write().

def export_window( fileoutput: Union[str, os.PathLike], binary=False, plt=None) -> vedo.plotter.Plotter:
1450def export_window(fileoutput: Union[str, os.PathLike], binary=False, plt=None) -> "vedo.Plotter":
1451    """
1452    Exporter which writes out the rendered scene into an HTML, X3D or Numpy file.
1453
1454    Example:
1455        - [export_x3d.py](https://github.com/marcomusy/vedo/tree/master/examples/other/export_x3d.py)
1456
1457        Check out the HTML generated webpage [here](https://vedo.embl.es/examples/embryo.html).
1458
1459        <img src='https://user-images.githubusercontent.com/32848391/57160341-c6ffbd80-6de8-11e9-95ff-7215ce642bc5.jpg' width="600"/>
1460
1461    .. note::
1462        the rendering window can also be exported to `numpy` file `scene.npz`
1463        by pressing `E` key at any moment during visualization.
1464    """
1465    fileoutput = str(fileoutput)
1466    if plt is None:
1467        plt = vedo.plotter_instance
1468
1469    fr = fileoutput.lower()
1470    ####################################################################
1471    if fr.endswith(".npy") or fr.endswith(".npz"):
1472        _export_npy(plt, fileoutput)
1473
1474    ####################################################################
1475    elif fr.endswith(".x3d"):
1476        # obj = plt.get_actors()
1477        # if plt.axes_instances:
1478        #     obj.append(plt.axes_instances[0])
1479
1480        # for a in obj:
1481        #     if isinstance(a, Assembly):
1482        #         plt.remove(a)
1483        #         plt.add(a.unpack())
1484
1485        plt.render()
1486
1487        exporter = vtki.new("X3DExporter")
1488        exporter.SetBinary(binary)
1489        exporter.FastestOff()
1490        exporter.SetInput(plt.window)
1491        exporter.SetFileName(fileoutput)
1492        # exporter.WriteToOutputStringOn()
1493        exporter.Update()
1494        exporter.Write()
1495
1496        wsize = plt.window.GetSize()
1497        x3d_html = _x3d_html_template.replace("~fileoutput", fileoutput)
1498        x3d_html = x3d_html.replace("~width",  str(wsize[0]))
1499        x3d_html = x3d_html.replace("~height", str(wsize[1]))
1500        with open(fileoutput.replace(".x3d", ".html"), "w", encoding="UTF-8") as outF:
1501            outF.write(x3d_html)
1502
1503    ####################################################################
1504    elif fr.endswith(".html"):
1505        savebk = vedo.notebook_backend
1506        vedo.notebook_backend = "k3d"
1507        vedo.settings.default_backend = "k3d"
1508        # acts = plt.get_actors()
1509        plt = vedo.backends.get_notebook_backend(plt.objects)
1510
1511        with open(fileoutput, "w", encoding="UTF-8") as fp:
1512            fp.write(plt.get_snapshot())
1513
1514        vedo.notebook_backend = savebk
1515        vedo.settings.default_backend = savebk
1516
1517    else:
1518        vedo.logger.error(f"export extension {fr.split('.')[-1]} is not supported")
1519
1520    return plt

Exporter which writes out the rendered scene into an HTML, X3D or Numpy file.

Example:

Check out the HTML generated webpage here.

the rendering window can also be exported to numpy file scene.npz by pressing E key at any moment during visualization.

def import_window(fileinput: Union[str, os.PathLike]) -> Optional[vedo.plotter.Plotter]:
1800def import_window(fileinput: Union[str, os.PathLike]) -> Union["vedo.Plotter", None]:
1801    """
1802    Import a whole scene from a Numpy NPZ file.
1803
1804    Returns:
1805        `vedo.Plotter` instance
1806    """
1807    fileinput = str(fileinput)
1808
1809    if fileinput.endswith(".npy") or fileinput.endswith(".npz"):
1810        return _import_npy(fileinput)
1811
1812    # elif ".obj" in fileinput.lower():
1813    #     meshes = load_obj(fileinput, mtl_file, texture_path)
1814    #     plt = vedo.Plotter()
1815    #     plt.add(meshes)
1816    #     return plt
1817
1818    # elif fileinput.endswith(".h5") or fileinput.endswith(".hdf5"):
1819    #     return _import_hdf5(fileinput) # in store/file_io_HDF5.py
1820
1821    return None

Import a whole scene from a Numpy NPZ file.

Returns:

vedo.Plotter instance

def load_obj( fileinput: Union[str, os.PathLike], mtl_file=None, texture_path=None) -> List[vedo.mesh.Mesh]:
1824def load_obj(fileinput: Union[str, os.PathLike], mtl_file=None, texture_path=None) -> List[Mesh]:
1825    """
1826    Import a set of meshes from a OBJ wavefront file.
1827
1828    Arguments:
1829        mtl_file : (str)
1830            MTL file for OBJ wavefront files
1831        texture_path : (str)
1832            path of the texture files directory
1833
1834    Returns:
1835        `list(Mesh)`
1836    """
1837    fileinput = str(fileinput)
1838
1839    window = vtki.vtkRenderWindow()
1840    window.SetOffScreenRendering(1)
1841    renderer = vtki.vtkRenderer()
1842    window.AddRenderer(renderer)
1843
1844    importer = vtki.new("OBJImporter")
1845    importer.SetFileName(fileinput)
1846    if mtl_file is None:
1847        mtl_file = fileinput.replace(".obj", ".mtl").replace(".OBJ", ".MTL")
1848    if os.path.isfile(mtl_file):
1849        importer.SetFileNameMTL(mtl_file)
1850    if texture_path is None:
1851        texture_path = fileinput.replace(".obj", ".txt").replace(".OBJ", ".TXT")
1852    # since the texture_path may be a directory which contains textures
1853    if os.path.exists(texture_path):
1854        importer.SetTexturePath(texture_path)
1855    importer.SetRenderWindow(window)
1856    importer.Update()
1857
1858    actors = renderer.GetActors()
1859    actors.InitTraversal()
1860    objs = []
1861    for _ in range(actors.GetNumberOfItems()):
1862        vactor = actors.GetNextActor()
1863        msh = Mesh(vactor)
1864        msh.name = "OBJMesh"
1865        msh.copy_properties_from(vactor)
1866        tx = vactor.GetTexture()
1867        if tx:
1868            msh.texture(tx)
1869        objs.append(msh)
1870    return objs

Import a set of meshes from a OBJ wavefront file.

Arguments:
  • mtl_file : (str) MTL file for OBJ wavefront files
  • texture_path : (str) path of the texture files directory
Returns:

list(Mesh)

def screenshot( filename='screenshot.png', scale=1, asarray=False) -> Union[vedo.plotter.Plotter, numpy.ndarray, NoneType]:
1874def screenshot(filename="screenshot.png", scale=1, asarray=False) -> Union["vedo.Plotter", np.ndarray, None]:
1875    """
1876    Save a screenshot of the current rendering window.
1877
1878    Alternatively, press key `Shift-S` in the rendering window to save a screenshot.
1879    You can also use keyword `screenshot` in `show(..., screenshot="pic.png")`.
1880
1881    Arguments:
1882        scale : (int)
1883            Set image magnification as an integer multiplicative factor.
1884            E.g. setting a magnification of 2 produces an image twice as large,
1885            but 10x slower to generate.
1886        asarray : (bool)
1887            Return a numpy array of the image
1888    """
1889    filename = str(filename)
1890    # print("calling screenshot", filename, scale, asarray)
1891
1892    if not vedo.plotter_instance or not vedo.plotter_instance.window:
1893        # vedo.logger.error("in screenshot(), rendering window is not present, skip.")
1894        return vedo.plotter_instance  ##########
1895
1896    if vedo.plotter_instance.renderer:
1897        vedo.plotter_instance.renderer.ResetCameraClippingRange()
1898
1899    if asarray and scale == 1 and not vedo.plotter_instance.offscreen:
1900        nx, ny = vedo.plotter_instance.window.GetSize()
1901        arr = vtki.vtkUnsignedCharArray()
1902        vedo.plotter_instance.window.GetRGBACharPixelData(0, 0, nx-1, ny-1, 0, arr)
1903        narr = vedo.vtk2numpy(arr).T[:3].T.reshape([ny, nx, 3])
1904        narr = np.flip(narr, axis=0)
1905        return narr  ##########
1906
1907    ###########################
1908    if filename.endswith(".pdf"):
1909        writer = vtki.new("GL2PSExporter")
1910        writer.SetRenderWindow(vedo.plotter_instance.window)
1911        writer.Write3DPropsAsRasterImageOff()
1912        writer.SilentOn()
1913        writer.SetSortToBSP()
1914        writer.SetFileFormatToPDF()
1915        writer.SetFilePrefix(filename.replace(".pdf", ""))
1916        writer.Write()
1917        return vedo.plotter_instance  ##########
1918
1919    elif filename.endswith(".svg"):
1920        writer = vtki.new("GL2PSExporter")
1921        writer.SetRenderWindow(vedo.plotter_instance.window)
1922        writer.Write3DPropsAsRasterImageOff()
1923        writer.SilentOn()
1924        writer.SetSortToBSP()
1925        writer.SetFileFormatToSVG()
1926        writer.SetFilePrefix(filename.replace(".svg", ""))
1927        writer.Write()
1928        return vedo.plotter_instance  ##########
1929
1930    elif filename.endswith(".eps"):
1931        writer = vtki.new("GL2PSExporter")
1932        writer.SetRenderWindow(vedo.plotter_instance.window)
1933        writer.Write3DPropsAsRasterImageOff()
1934        writer.SilentOn()
1935        writer.SetSortToBSP()
1936        writer.SetFileFormatToEPS()
1937        writer.SetFilePrefix(filename.replace(".eps", ""))
1938        writer.Write()
1939        return vedo.plotter_instance  ##########
1940
1941    if settings.screeshot_large_image:
1942        w2if = vtki.new("RenderLargeImage")
1943        w2if.SetInput(vedo.plotter_instance.renderer)
1944        w2if.SetMagnification(scale)
1945    else:
1946        w2if = vtki.new("WindowToImageFilter")
1947        w2if.SetInput(vedo.plotter_instance.window)
1948        if hasattr(w2if, "SetScale"):
1949            w2if.SetScale(int(scale), int(scale))
1950        if settings.screenshot_transparent_background:
1951            w2if.SetInputBufferTypeToRGBA()
1952        w2if.ReadFrontBufferOff()  # read from the back buffer
1953    w2if.Update()
1954
1955    if asarray:
1956        pd = w2if.GetOutput().GetPointData()
1957        npdata = utils.vtk2numpy(pd.GetArray("ImageScalars"))
1958        # npdata = npdata[:, [0, 1, 2]]  # remove alpha channel, issue #1199
1959        ydim, xdim, _ = w2if.GetOutput().GetDimensions()
1960        npdata = npdata.reshape([xdim, ydim, -1])
1961        npdata = np.flip(npdata, axis=0)
1962        return npdata ###########################
1963
1964    if filename.lower().endswith(".png"):
1965        writer = vtki.new("PNGWriter")
1966        writer.SetFileName(filename)
1967        writer.SetInputData(w2if.GetOutput())
1968        writer.Write()
1969    elif filename.lower().endswith(".jpg") or filename.lower().endswith(".jpeg"):
1970        writer = vtki.new("JPEGWriter")
1971        writer.SetFileName(filename)
1972        writer.SetInputData(w2if.GetOutput())
1973        writer.Write()
1974    else:  # add .png
1975        writer = vtki.new("PNGWriter")
1976        writer.SetFileName(filename + ".png")
1977        writer.SetInputData(w2if.GetOutput())
1978        writer.Write()
1979    return vedo.plotter_instance

Save a screenshot of the current rendering window.

Alternatively, press key Shift-S in the rendering window to save a screenshot. You can also use keyword screenshot in show(..., screenshot="pic.png").

Arguments:
  • scale : (int) Set image magnification as an integer multiplicative factor. E.g. setting a magnification of 2 produces an image twice as large, but 10x slower to generate.
  • asarray : (bool) Return a numpy array of the image
def ask(*question, **kwarg) -> str:
1982def ask(*question, **kwarg) -> str:
1983    """
1984    Ask a question from command line. Return the answer as a string.
1985    See function `colors.printc()` for the description of the keyword options.
1986
1987    Arguments:
1988        options : (list)
1989            a python list of possible answers to choose from.
1990        default : (str)
1991            the default answer when just hitting return.
1992
1993    Example:
1994    ```python
1995    import vedo
1996    res = vedo.ask("Continue?", options=['Y','n'], default='Y', c='g')
1997    print(res)
1998    ```
1999    """
2000    kwarg.update({"end": " "})
2001    if "invert" not in kwarg:
2002        kwarg.update({"invert": True})
2003    if "box" in kwarg:
2004        kwarg.update({"box": ""})
2005
2006    options = kwarg.pop("options", [])
2007    default = kwarg.pop("default", "")
2008    if options:
2009        opt = "["
2010        for o in options:
2011            opt += o + "/"
2012        opt = opt[:-1] + "]"
2013        colors.printc(*question, opt, **kwarg)
2014    else:
2015        colors.printc(*question, **kwarg)
2016
2017    try:
2018        resp = input()
2019    except Exception:
2020        resp = ""
2021        return resp
2022
2023    if options:
2024        if resp not in options:
2025            if default and str(repr(resp)) == "''":
2026                return default
2027            colors.printc("Please choose one option in:", opt, italic=True, bold=False)
2028            kwarg["options"] = options
2029            return ask(*question, **kwarg)  # ask again
2030    return resp

Ask a question from command line. Return the answer as a string. See function colors.printc() for the description of the keyword options.

Arguments:
  • options : (list) a python list of possible answers to choose from.
  • default : (str) the default answer when just hitting return.

Example:

import vedo
res = ask("Continue?", options=['Y','n'], default='Y', c='g')
print(res)
class Video:
2034class Video:
2035    """
2036    Generate a video from a rendering window.
2037    """
2038
2039    def __init__(self, name="movie.mp4", duration=None, fps=24, scale=1, backend="imageio"):
2040        """
2041        Class to generate a video from the specified rendering window.
2042        Program `ffmpeg` is used to create video from each generated frame.
2043
2044        Arguments:
2045            name : (Union[str, os.PathLike])
2046                name of the output file.
2047            duration : (float)
2048                set the total `duration` of the video and recalculates `fps` accordingly.
2049            fps : (int)
2050                set the number of frames per second.
2051            scale : (int)
2052                set the image magnification as an integer multiplicative factor.
2053            backend : (str)
2054                the backend engine to be used `['imageio', 'ffmpeg', 'cv']`
2055
2056        Examples:
2057            - [make_video.py](https://github.com/marcomusy/vedo/tree/master/examples/other/make_video.py)
2058
2059            ![](https://user-images.githubusercontent.com/32848391/50739007-2bfc2b80-11da-11e9-97e6-620a3541a6fa.jpg)
2060        """
2061        self.name = str(name)
2062        self.duration = duration
2063        self.backend = backend
2064        self.fps = float(fps)
2065        self.command = "ffmpeg -loglevel panic -y -r"
2066        self.options = "-b:v 8000k"
2067        self.scale = scale
2068
2069        self.frames = []
2070        self.tmp_dir = TemporaryDirectory()
2071        self.get_filename = lambda x: os.path.join(self.tmp_dir.name, x)
2072        colors.printc(":video:  Video file", self.name, "is open... ", c="m", end="")
2073
2074    def add_frame(self) -> "Video":
2075        """Add frame to current video."""
2076        fr = self.get_filename(str(len(self.frames)) + ".png")
2077        screenshot(fr, scale=self.scale)
2078        self.frames.append(fr)
2079        return self
2080
2081    def pause(self, pause=0) -> "Video":
2082        """Insert a `pause`, in seconds."""
2083        fr = self.frames[-1]
2084        n = int(self.fps * pause)
2085        for _ in range(n):
2086            fr2 = self.get_filename(str(len(self.frames)) + ".png")
2087            self.frames.append(fr2)
2088            os.system("cp -f %s %s" % (fr, fr2))
2089        return self
2090
2091    def action(self, elevation=(0, 80), azimuth=(0, 359), cameras=(), resetcam=False) -> "Video":
2092        """
2093        Automatic shooting of a static scene by specifying rotation and elevation ranges.
2094
2095        Arguments:
2096            elevation : list
2097                initial and final elevation angles
2098            azimuth_range : list
2099                initial and final azimuth angles
2100            cameras : list
2101                list of cameras to go through, each camera can be dictionary or a vtkCamera
2102        """
2103        if not self.duration:
2104            self.duration = 5
2105
2106        plt = vedo.plotter_instance
2107        if not plt:
2108            vedo.logger.error("No vedo plotter found, cannot make video.")
2109            return self
2110        n = int(self.fps * self.duration)
2111
2112        cams = []
2113        for cm in cameras:
2114            cams.append(utils.camera_from_dict(cm))
2115        nc = len(cams)
2116
2117        plt.show(resetcam=resetcam, interactive=False)
2118
2119        if nc:
2120            for i in range(n):
2121                plt.move_camera(cams, i / (n-1))
2122                plt.render()
2123                self.add_frame()
2124
2125        else:  ########################################
2126
2127            for i in range(n):
2128                plt.camera.Elevation((elevation[1] - elevation[0]) / n)
2129                plt.camera.Azimuth((azimuth[1] - azimuth[0]) / n)
2130                plt.render()
2131                self.add_frame()
2132
2133        return self
2134
2135    def close(self) -> None:
2136        """
2137        Render the video and write it to file.
2138        """
2139        if self.duration:
2140            self.fps = int(len(self.frames) / float(self.duration) + 0.5)
2141            colors.printc("recalculated fps:", self.fps, c="m", end="")
2142        else:
2143            self.fps = int(self.fps)
2144
2145        ########################################
2146        if self.backend == "ffmpeg":
2147            out = os.system(
2148                self.command
2149                + " "
2150                + str(self.fps)
2151                + " -i "
2152                + f"'{self.tmp_dir.name}'"
2153                + os.sep
2154                + "%01d.png "
2155                + self.options
2156                + " "
2157                + f"'{self.name}'"
2158            )
2159            if out:
2160                vedo.logger.error(f":noentry: backend {self.backend} returning error: {out}")
2161            else:
2162                colors.printc(f":save: saved to {self.name}", c="m")
2163
2164        ########################################
2165        elif "cv" in self.backend:
2166            try:
2167                import cv2  # type: ignore
2168            except ImportError:
2169                vedo.logger.error("opencv is not installed")
2170                return
2171
2172            cap = cv2.VideoCapture(os.path.join(self.tmp_dir.name, "%1d.png"))
2173            fourcc = cv2.VideoWriter_fourcc(*"mp4v")
2174            if vedo.plotter_instance:
2175                w, h = vedo.plotter_instance.window.GetSize()
2176                writer = cv2.VideoWriter(self.name, fourcc, self.fps, (w, h), True)
2177            else:
2178                vedo.logger.error("No vedo plotter found, cannot make video.")
2179                return
2180
2181            while True:
2182                ret, frame = cap.read()
2183                if not ret:
2184                    break
2185                writer.write(frame)
2186
2187            cap.release()
2188            writer.release()
2189
2190        ########################################
2191        elif "imageio" in self.backend:
2192            try:
2193                import imageio
2194            except ImportError:
2195                vedo.logger.error("Please install imageio with:\n pip install imageio[ffmpeg]")
2196                return
2197
2198            if self.name.endswith(".mp4"):
2199                writer = imageio.get_writer(self.name, fps=self.fps)
2200            elif self.name.endswith(".gif"):
2201                writer = imageio.get_writer(self.name, mode="I", duration=1 / self.fps)
2202            elif self.name.endswith(".webm"):
2203                writer = imageio.get_writer(self.name, format="webm", fps=self.fps)
2204            else:
2205                vedo.logger.error(f"Unknown format of {self.name}.")
2206                return
2207
2208            for f in utils.humansort(self.frames):
2209                image = imageio.v3.imread(f)
2210                try:
2211                    writer.append_data(image)
2212                except TypeError:
2213                    vedo.logger.error(f"Could not append data to video {self.name}")
2214                    vedo.logger.error("Please install imageio with: pip install imageio[ffmpeg]")
2215                    break
2216            try:
2217                writer.close()
2218                colors.printc(f"... saved as {self.name}", c="m")
2219            except:
2220                colors.printc(f":noentry: Could not save video {self.name}", c="r")
2221
2222        # finalize cleanup
2223        self.tmp_dir.cleanup()
2224
2225    def split_frames(self, output_dir="video_frames", prefix="frame_", file_format="png") -> None:
2226        """Split an existing video file into frames."""
2227        try:
2228            import imageio
2229        except ImportError:
2230            vedo.logger.error("\nPlease install imageio with:\n pip install imageio")
2231            return
2232
2233        # Create the output directory if it doesn't exist
2234        if not os.path.exists(output_dir):
2235            os.makedirs(output_dir)
2236
2237        # Create a reader object to read the video
2238        reader = imageio.get_reader(self.name)
2239
2240        # Loop through each frame of the video and save it as image
2241        print()
2242        for i, frame in utils.progressbar(
2243            enumerate(reader), title=f"writing {file_format} frames", c="m", width=20
2244        ):
2245            output_file = os.path.join(output_dir, f"{prefix}{str(i).zfill(5)}.{format}")
2246            imageio.imwrite(output_file, frame, format=file_format)

Generate a video from a rendering window.

Video(name='movie.mp4', duration=None, fps=24, scale=1, backend='imageio')
2039    def __init__(self, name="movie.mp4", duration=None, fps=24, scale=1, backend="imageio"):
2040        """
2041        Class to generate a video from the specified rendering window.
2042        Program `ffmpeg` is used to create video from each generated frame.
2043
2044        Arguments:
2045            name : (Union[str, os.PathLike])
2046                name of the output file.
2047            duration : (float)
2048                set the total `duration` of the video and recalculates `fps` accordingly.
2049            fps : (int)
2050                set the number of frames per second.
2051            scale : (int)
2052                set the image magnification as an integer multiplicative factor.
2053            backend : (str)
2054                the backend engine to be used `['imageio', 'ffmpeg', 'cv']`
2055
2056        Examples:
2057            - [make_video.py](https://github.com/marcomusy/vedo/tree/master/examples/other/make_video.py)
2058
2059            ![](https://user-images.githubusercontent.com/32848391/50739007-2bfc2b80-11da-11e9-97e6-620a3541a6fa.jpg)
2060        """
2061        self.name = str(name)
2062        self.duration = duration
2063        self.backend = backend
2064        self.fps = float(fps)
2065        self.command = "ffmpeg -loglevel panic -y -r"
2066        self.options = "-b:v 8000k"
2067        self.scale = scale
2068
2069        self.frames = []
2070        self.tmp_dir = TemporaryDirectory()
2071        self.get_filename = lambda x: os.path.join(self.tmp_dir.name, x)
2072        colors.printc(":video:  Video file", self.name, "is open... ", c="m", end="")

Class to generate a video from the specified rendering window. Program ffmpeg is used to create video from each generated frame.

Arguments:
  • name : (Union[str, os.PathLike]) name of the output file.
  • duration : (float) set the total duration of the video and recalculates fps accordingly.
  • fps : (int) set the number of frames per second.
  • scale : (int) set the image magnification as an integer multiplicative factor.
  • backend : (str) the backend engine to be used ['imageio', 'ffmpeg', 'cv']
Examples:

def add_frame(self) -> Video:
2074    def add_frame(self) -> "Video":
2075        """Add frame to current video."""
2076        fr = self.get_filename(str(len(self.frames)) + ".png")
2077        screenshot(fr, scale=self.scale)
2078        self.frames.append(fr)
2079        return self

Add frame to current video.

def pause(self, pause=0) -> Video:
2081    def pause(self, pause=0) -> "Video":
2082        """Insert a `pause`, in seconds."""
2083        fr = self.frames[-1]
2084        n = int(self.fps * pause)
2085        for _ in range(n):
2086            fr2 = self.get_filename(str(len(self.frames)) + ".png")
2087            self.frames.append(fr2)
2088            os.system("cp -f %s %s" % (fr, fr2))
2089        return self

Insert a pause, in seconds.

def action( self, elevation=(0, 80), azimuth=(0, 359), cameras=(), resetcam=False) -> Video:
2091    def action(self, elevation=(0, 80), azimuth=(0, 359), cameras=(), resetcam=False) -> "Video":
2092        """
2093        Automatic shooting of a static scene by specifying rotation and elevation ranges.
2094
2095        Arguments:
2096            elevation : list
2097                initial and final elevation angles
2098            azimuth_range : list
2099                initial and final azimuth angles
2100            cameras : list
2101                list of cameras to go through, each camera can be dictionary or a vtkCamera
2102        """
2103        if not self.duration:
2104            self.duration = 5
2105
2106        plt = vedo.plotter_instance
2107        if not plt:
2108            vedo.logger.error("No vedo plotter found, cannot make video.")
2109            return self
2110        n = int(self.fps * self.duration)
2111
2112        cams = []
2113        for cm in cameras:
2114            cams.append(utils.camera_from_dict(cm))
2115        nc = len(cams)
2116
2117        plt.show(resetcam=resetcam, interactive=False)
2118
2119        if nc:
2120            for i in range(n):
2121                plt.move_camera(cams, i / (n-1))
2122                plt.render()
2123                self.add_frame()
2124
2125        else:  ########################################
2126
2127            for i in range(n):
2128                plt.camera.Elevation((elevation[1] - elevation[0]) / n)
2129                plt.camera.Azimuth((azimuth[1] - azimuth[0]) / n)
2130                plt.render()
2131                self.add_frame()
2132
2133        return self

Automatic shooting of a static scene by specifying rotation and elevation ranges.

Arguments:
  • elevation : list initial and final elevation angles
  • azimuth_range : list initial and final azimuth angles
  • cameras : list list of cameras to go through, each camera can be dictionary or a vtkCamera
def close(self) -> None:
2135    def close(self) -> None:
2136        """
2137        Render the video and write it to file.
2138        """
2139        if self.duration:
2140            self.fps = int(len(self.frames) / float(self.duration) + 0.5)
2141            colors.printc("recalculated fps:", self.fps, c="m", end="")
2142        else:
2143            self.fps = int(self.fps)
2144
2145        ########################################
2146        if self.backend == "ffmpeg":
2147            out = os.system(
2148                self.command
2149                + " "
2150                + str(self.fps)
2151                + " -i "
2152                + f"'{self.tmp_dir.name}'"
2153                + os.sep
2154                + "%01d.png "
2155                + self.options
2156                + " "
2157                + f"'{self.name}'"
2158            )
2159            if out:
2160                vedo.logger.error(f":noentry: backend {self.backend} returning error: {out}")
2161            else:
2162                colors.printc(f":save: saved to {self.name}", c="m")
2163
2164        ########################################
2165        elif "cv" in self.backend:
2166            try:
2167                import cv2  # type: ignore
2168            except ImportError:
2169                vedo.logger.error("opencv is not installed")
2170                return
2171
2172            cap = cv2.VideoCapture(os.path.join(self.tmp_dir.name, "%1d.png"))
2173            fourcc = cv2.VideoWriter_fourcc(*"mp4v")
2174            if vedo.plotter_instance:
2175                w, h = vedo.plotter_instance.window.GetSize()
2176                writer = cv2.VideoWriter(self.name, fourcc, self.fps, (w, h), True)
2177            else:
2178                vedo.logger.error("No vedo plotter found, cannot make video.")
2179                return
2180
2181            while True:
2182                ret, frame = cap.read()
2183                if not ret:
2184                    break
2185                writer.write(frame)
2186
2187            cap.release()
2188            writer.release()
2189
2190        ########################################
2191        elif "imageio" in self.backend:
2192            try:
2193                import imageio
2194            except ImportError:
2195                vedo.logger.error("Please install imageio with:\n pip install imageio[ffmpeg]")
2196                return
2197
2198            if self.name.endswith(".mp4"):
2199                writer = imageio.get_writer(self.name, fps=self.fps)
2200            elif self.name.endswith(".gif"):
2201                writer = imageio.get_writer(self.name, mode="I", duration=1 / self.fps)
2202            elif self.name.endswith(".webm"):
2203                writer = imageio.get_writer(self.name, format="webm", fps=self.fps)
2204            else:
2205                vedo.logger.error(f"Unknown format of {self.name}.")
2206                return
2207
2208            for f in utils.humansort(self.frames):
2209                image = imageio.v3.imread(f)
2210                try:
2211                    writer.append_data(image)
2212                except TypeError:
2213                    vedo.logger.error(f"Could not append data to video {self.name}")
2214                    vedo.logger.error("Please install imageio with: pip install imageio[ffmpeg]")
2215                    break
2216            try:
2217                writer.close()
2218                colors.printc(f"... saved as {self.name}", c="m")
2219            except:
2220                colors.printc(f":noentry: Could not save video {self.name}", c="r")
2221
2222        # finalize cleanup
2223        self.tmp_dir.cleanup()

Render the video and write it to file.

def split_frames( self, output_dir='video_frames', prefix='frame_', file_format='png') -> None:
2225    def split_frames(self, output_dir="video_frames", prefix="frame_", file_format="png") -> None:
2226        """Split an existing video file into frames."""
2227        try:
2228            import imageio
2229        except ImportError:
2230            vedo.logger.error("\nPlease install imageio with:\n pip install imageio")
2231            return
2232
2233        # Create the output directory if it doesn't exist
2234        if not os.path.exists(output_dir):
2235            os.makedirs(output_dir)
2236
2237        # Create a reader object to read the video
2238        reader = imageio.get_reader(self.name)
2239
2240        # Loop through each frame of the video and save it as image
2241        print()
2242        for i, frame in utils.progressbar(
2243            enumerate(reader), title=f"writing {file_format} frames", c="m", width=20
2244        ):
2245            output_file = os.path.join(output_dir, f"{prefix}{str(i).zfill(5)}.{format}")
2246            imageio.imwrite(output_file, frame, format=file_format)

Split an existing video file into frames.