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

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

def download(url: str, force=False, verbose=True) -> str:
466def download(url: str, force=False, verbose=True) -> str:
467    """
468    Retrieve a file from a URL, save it locally and return its path.
469    Use `force=True` to force a reload and discard cached copies.
470    """
471    if not url.startswith("https://"):
472        # assume it's a file so no need to download
473        return url
474    url = url.replace("www.dropbox", "dl.dropbox")
475
476    if "github.com" in url:
477        url = url.replace("/blob/", "/raw/")
478
479    basename = os.path.basename(url)
480
481    if "?" in basename:
482        basename = basename.split("?")[0]
483
484    home_directory = os.path.expanduser("~")
485    cachedir = os.path.join(home_directory, settings.cache_directory, "vedo")
486    fname = os.path.join(cachedir, basename)
487    # Create the directory if it does not exist
488    if not os.path.exists(cachedir):
489        os.makedirs(cachedir)
490
491    if not force and os.path.exists(fname):
492        if verbose:
493            colors.printc("reusing cached file:", fname)
494        return fname
495
496    try:
497        from urllib.request import urlopen, Request
498        req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
499        if verbose:
500            colors.printc("reading", basename, "from", url.split("/")[2][:40], "...", end="")
501
502    except ImportError:
503        import urllib2
504        import contextlib
505        urlopen = lambda url_: contextlib.closing(urllib2.urlopen(url_))
506        req = url
507        if verbose:
508            colors.printc("reading", basename, "from", url.split("/")[2][:40], "...", end="")
509
510    with urlopen(req) as response, open(fname, "wb") as output:
511        output.write(response.read())
512
513    if verbose:
514        colors.printc(" done.")
515    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:
595def gunzip(filename: str) -> str:
596    """Unzip a `.gz` file to a temporary file and returns its path."""
597    if not filename.endswith(".gz"):
598        # colors.printc("gunzip() error: file must end with .gz", c='r')
599        return filename
600
601    import gzip
602
603    tmp_file = NamedTemporaryFile(delete=False)
604    tmp_file.name = os.path.join(
605        os.path.dirname(tmp_file.name), os.path.basename(filename).replace(".gz", "")
606    )
607    inF = gzip.open(filename, "rb")
608    with open(tmp_file.name, "wb") as outF:
609        outF.write(inF.read())
610    inF.close()
611    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):
630def loadStructuredPoints(filename: Union[str, os.PathLike], as_points=True):
631    """
632    Load and return a `vtkStructuredPoints` object from file.
633    
634    If `as_points` is True, return a `Points` object
635    instead of a `vtkStructuredPoints`.
636    """
637    filename = str(filename)
638    reader = vtki.new("StructuredPointsReader")
639    reader.SetFileName(filename)
640    reader.Update()
641    if as_points:
642        v2p = vtki.new("ImageToPoints")
643        v2p.SetInputData(reader.GetOutput())
644        v2p.Update()
645        pts = Points(v2p.GetOutput())
646        return pts
647    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]):
650def loadStructuredGrid(filename: Union[str, os.PathLike]):
651    """Load and return a `vtkStructuredGrid` object from file."""
652    filename = str(filename)
653    if filename.endswith(".vts"):
654        reader = vtki.new("XMLStructuredGridReader")
655    else:
656        reader = vtki.new("StructuredGridReader")
657    reader.SetFileName(filename)
658    reader.Update()
659    return reader.GetOutput()

Load and return a vtkStructuredGrid object from file.

def write(objct: Any, fileoutput: Union[str, os.PathLike], binary=True) -> Any:
1180def write(objct: Any, fileoutput: Union[str, os.PathLike], binary=True) -> Any:
1181    """
1182    Write object to file. Same as `save()`.
1183
1184    Supported extensions are:
1185    
1186    - `vtk, vti, ply, obj, stl, byu, vtp, vti, mhd, xyz, xml, tif, png, bmp`
1187    """
1188    fileoutput = str(fileoutput)
1189
1190    ###############################
1191    if isinstance(objct, Assembly):
1192        dd = to_numpy(objct)
1193        sdict = {"objects": [dd]}
1194        np.save(fileoutput, sdict)
1195        return objct
1196    
1197    ###############################
1198    obj = objct.dataset
1199
1200    try:
1201        # check if obj is a Mesh.actor and has a transform
1202        M = objct.actor.GetMatrix()
1203        if M and not M.IsIdentity():
1204            obj = objct.apply_transform_from_actor()
1205            obj = objct.dataset
1206            vedo.logger.info(
1207                f"object '{objct.name}' "
1208                "was manually moved. Writing uses current position."
1209            )
1210    except:
1211        pass
1212
1213    fr = fileoutput.lower()
1214    if fr.endswith(".vtk"):
1215        writer = vtki.new("DataSetWriter")
1216    elif fr.endswith(".ply"):
1217        writer = vtki.new("PLYWriter")
1218        writer.AddComment("PLY file generated by vedo")
1219        lut = objct.mapper.GetLookupTable()
1220        if lut:
1221            pscal = obj.GetPointData().GetScalars()
1222            if not pscal:
1223                pscal = obj.GetCellData().GetScalars()
1224            if pscal and pscal.GetName():
1225                writer.SetArrayName(pscal.GetName())
1226            writer.SetLookupTable(lut)
1227    elif fr.endswith(".stl"):
1228        writer = vtki.new("STLWriter")
1229    elif fr.endswith(".vtp"):
1230        writer = vtki.new("XMLPolyDataWriter")
1231    elif fr.endswith(".vtu"):
1232        writer = vtki.new("XMLUnstructuredGridWriter")
1233    elif fr.endswith(".xyz"):
1234        writer = vtki.new("SimplePointsWriter")
1235    elif fr.endswith(".facet"):
1236        writer = vtki.new("FacetWriter")
1237    elif fr.endswith(".vti"):
1238        writer = vtki.new("XMLImageDataWriter")
1239    elif fr.endswith(".vtr"):
1240        writer = vtki.new("XMLRectilinearGridWriter")
1241    elif fr.endswith(".vtm"):
1242        g = vtki.new("MultiBlockDataGroupFilter")
1243        for ob in objct:
1244            try:
1245                g.AddInputData(ob)
1246            except TypeError:
1247                vedo.logger.warning("cannot save object of type", type(ob))
1248        g.Update()
1249        mb = g.GetOutputDataObject(0)
1250        wri = vtki.new("vtkXMLMultiBlockDataWriter")
1251        wri.SetInputData(mb)
1252        wri.SetFileName(fileoutput)
1253        wri.Write()
1254        return objct
1255    elif fr.endswith(".mhd"):
1256        writer = vtki.new("MetaImageWriter")
1257    elif fr.endswith(".nii"):
1258        writer = vtki.new("NIFTIImageWriter")
1259    elif fr.endswith(".png"):
1260        writer = vtki.new("PNGWriter")
1261    elif fr.endswith(".jpg"):
1262        writer = vtki.new("JPEGWriter")
1263    elif fr.endswith(".bmp"):
1264        writer = vtki.new("BMPWriter")
1265    elif fr.endswith(".tif") or fr.endswith(".tiff"):
1266        writer = vtki.new("TIFFWriter")
1267        writer.SetFileDimensionality(len(obj.GetDimensions()))
1268    elif fr.endswith(".obj"):
1269        with open(fileoutput, "w", encoding="UTF-8") as outF:
1270            outF.write("# OBJ file format with ext .obj\n")
1271            outF.write("# File generated by vedo\n")
1272
1273            for p in objct.vertices:
1274                outF.write("v {:.8g} {:.8g} {:.8g}\n".format(*p))
1275
1276            ptxt = objct.dataset.GetPointData().GetTCoords()
1277            if ptxt:
1278                ntxt = utils.vtk2numpy(ptxt)
1279                for vt in ntxt:
1280                    outF.write("vt " + str(vt[0]) + " " + str(vt[1]) + " 0.0\n")
1281
1282            if isinstance(objct, Mesh):
1283                for i, f in enumerate(objct.cells):
1284                    fs = ""
1285                    for fi in f:
1286                        if ptxt:
1287                            fs += f" {fi+1}/{fi+1}"
1288                        else:
1289                            fs += f" {fi+1}"
1290                    outF.write(f"f{fs}\n")
1291
1292                for l in objct.lines:
1293                    ls = ""
1294                    for li in l:
1295                        ls += str(li + 1) + " "
1296                    outF.write(f"l {ls}\n")
1297        return objct
1298
1299    elif fr.endswith(".off"):
1300        with open(fileoutput, "w", encoding="UTF-8") as outF:
1301            outF.write("OFF\n")
1302            outF.write(str(objct.npoints) + " " + str(objct.ncells) + " 0\n\n")
1303            for p in objct.vertices:
1304                outF.write(" ".join([str(i) for i in p]) + "\n")
1305            for c in objct.cells:
1306                outF.write(str(len(c)) + " " + " ".join([str(i) for i in c]) + "\n")
1307        return objct
1308
1309    elif fr.endswith(".xml"):  # write tetrahedral dolfin xml
1310        vertices = objct.vertices.astype(str)
1311        faces = np.array(objct.cells).astype(str)
1312        ncoords = vertices.shape[0]
1313        with open(fileoutput, "w", encoding="UTF-8") as outF:
1314            outF.write('<?xml version="1.0" encoding="UTF-8"?>\n')
1315            outF.write('<dolfin xmlns:dolfin="http://www.fenicsproject.org">\n')
1316
1317            if len(faces[0]) == 4:  # write tetrahedral mesh
1318                ntets = faces.shape[0]
1319                outF.write('  <mesh celltype="tetrahedron" dim="3">\n')
1320                outF.write('    <vertices size="' + str(ncoords) + '">\n')
1321                for i in range(ncoords):
1322                    x, y, z = vertices[i]
1323                    outF.write('      <vertex index="'+str(i)+'" x="'+x+'" y="'+y+'" z="'+z+'"/>\n')
1324                outF.write('    </vertices>\n')
1325                outF.write('    <cells size="' + str(ntets) + '">\n')
1326                for i in range(ntets):
1327                    v0, v1, v2, v3 = faces[i]
1328                    outF.write('     <tetrahedron index="'+str(i)
1329                               + '" v0="'+v0+'" v1="'+v1+'" v2="'+v2+'" v3="'+v3+'"/>\n')
1330
1331            elif len(faces[0]) == 3:  # write triangle mesh
1332                ntri = faces.shape[0]
1333                outF.write('  <mesh celltype="triangle" dim="2">\n')
1334                outF.write('    <vertices size="' + str(ncoords) + '">\n')
1335                for i in range(ncoords):
1336                    x, y, _ = vertices[i]
1337                    outF.write('      <vertex index="'+str(i)+'" x="'+x+'" y="'+y+'"/>\n')
1338                outF.write('    </vertices>\n')
1339                outF.write('    <cells size="' + str(ntri) + '">\n')
1340                for i in range(ntri):
1341                    v0, v1, v2 = faces[i]
1342                    outF.write('     <triangle index="'+str(i)+'" v0="'+v0+'" v1="'+v1+'" v2="'+v2+'"/>\n')
1343
1344            outF.write("    </cells>\n")
1345            outF.write("  </mesh>\n")
1346            outF.write("</dolfin>\n")
1347        return objct
1348
1349    else:
1350        vedo.logger.error(f"Unknown format {fileoutput}, file not saved")
1351        return objct
1352
1353    try:
1354        if binary:
1355            writer.SetFileTypeToBinary()
1356        else:
1357            writer.SetFileTypeToASCII()
1358    except AttributeError:
1359        pass
1360
1361    try:
1362        writer.SetInputData(obj)
1363        writer.SetFileName(fileoutput)
1364        writer.Write()
1365    except:
1366        vedo.logger.error(f"could not save {fileoutput}")
1367    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:
1369def save(obj: Any, fileoutput="out.png", binary=True) -> Any:
1370    """Save an object to file. Same as `write()`."""
1371    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:
1378def export_window(fileoutput: Union[str, os.PathLike], binary=False, plt=None) -> "vedo.Plotter":
1379    """
1380    Exporter which writes out the rendered scene into an HTML, X3D or Numpy file.
1381
1382    Example:
1383        - [export_x3d.py](https://github.com/marcomusy/vedo/tree/master/examples/other/export_x3d.py)
1384
1385        Check out the HTML generated webpage [here](https://vedo.embl.es/examples/embryo.html).
1386
1387        <img src='https://user-images.githubusercontent.com/32848391/57160341-c6ffbd80-6de8-11e9-95ff-7215ce642bc5.jpg' width="600"/>
1388
1389    .. note::
1390        the rendering window can also be exported to `numpy` file `scene.npz`
1391        by pressing `E` key at any moment during visualization.
1392    """
1393    fileoutput = str(fileoutput)
1394    if plt is None:
1395        plt = vedo.plotter_instance
1396
1397    fr = fileoutput.lower()
1398    ####################################################################
1399    if fr.endswith(".npy") or fr.endswith(".npz"):
1400        _export_npy(plt, fileoutput)
1401
1402    ####################################################################
1403    elif fr.endswith(".x3d"):
1404        # obj = plt.get_actors()
1405        # if plt.axes_instances:
1406        #     obj.append(plt.axes_instances[0])
1407
1408        # for a in obj:
1409        #     if isinstance(a, Assembly):
1410        #         plt.remove(a)
1411        #         plt.add(a.unpack())
1412
1413        plt.render()
1414
1415        exporter = vtki.new("X3DExporter")
1416        exporter.SetBinary(binary)
1417        exporter.FastestOff()
1418        exporter.SetInput(plt.window)
1419        exporter.SetFileName(fileoutput)
1420        # exporter.WriteToOutputStringOn()
1421        exporter.Update()
1422        exporter.Write()
1423
1424        wsize = plt.window.GetSize()
1425        x3d_html = _x3d_html_template.replace("~fileoutput", fileoutput)
1426        x3d_html = x3d_html.replace("~width",  str(wsize[0]))
1427        x3d_html = x3d_html.replace("~height", str(wsize[1]))
1428        with open(fileoutput.replace(".x3d", ".html"), "w", encoding="UTF-8") as outF:
1429            outF.write(x3d_html)
1430
1431    ####################################################################
1432    elif fr.endswith(".html"):
1433        savebk = vedo.notebook_backend
1434        vedo.notebook_backend = "k3d"
1435        vedo.settings.default_backend = "k3d"
1436        # acts = plt.get_actors()
1437        plt = vedo.backends.get_notebook_backend(plt.objects)
1438
1439        with open(fileoutput, "w", encoding="UTF-8") as fp:
1440            fp.write(plt.get_snapshot())
1441
1442        vedo.notebook_backend = savebk
1443        vedo.settings.default_backend = savebk
1444
1445    else:
1446        vedo.logger.error(f"export extension {fr.split('.')[-1]} is not supported")
1447
1448    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]:
1728def import_window(fileinput: Union[str, os.PathLike]) -> Union["vedo.Plotter", None]:
1729    """
1730    Import a whole scene from a Numpy NPZ file.
1731
1732    Returns:
1733        `vedo.Plotter` instance
1734    """
1735    fileinput = str(fileinput)
1736
1737    if fileinput.endswith(".npy") or fileinput.endswith(".npz"):
1738        return _import_npy(fileinput)
1739    
1740    # elif ".obj" in fileinput.lower():
1741    #     meshes = load_obj(fileinput, mtl_file, texture_path)
1742    #     plt = vedo.Plotter()
1743    #     plt.add(meshes)
1744    #     return plt
1745
1746    # elif fileinput.endswith(".h5") or fileinput.endswith(".hdf5"):
1747    #     return _import_hdf5(fileinput) # in store/file_io_HDF5.py
1748
1749    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]:
1752def load_obj(fileinput: Union[str, os.PathLike], mtl_file=None, texture_path=None) -> List[Mesh]:
1753    """
1754    Import a set of meshes from a OBJ wavefront file.
1755
1756    Arguments:
1757        mtl_file : (str)
1758            MTL file for OBJ wavefront files
1759        texture_path : (str)
1760            path of the texture files directory
1761
1762    Returns:
1763        `list(Mesh)`
1764    """
1765    fileinput = str(fileinput)
1766
1767    window = vtki.vtkRenderWindow()
1768    window.SetOffScreenRendering(1)
1769    renderer = vtki.vtkRenderer()
1770    window.AddRenderer(renderer)
1771
1772    importer = vtki.new("OBJImporter")
1773    importer.SetFileName(fileinput)
1774    if mtl_file is None:
1775        mtl_file = fileinput.replace(".obj", ".mtl").replace(".OBJ", ".MTL")
1776    if os.path.isfile(mtl_file):
1777        importer.SetFileNameMTL(mtl_file)
1778    if texture_path is None:
1779        texture_path = fileinput.replace(".obj", ".txt").replace(".OBJ", ".TXT")
1780    # since the texture_path may be a directory which contains textures
1781    if os.path.exists(texture_path):
1782        importer.SetTexturePath(texture_path)
1783    importer.SetRenderWindow(window)
1784    importer.Update()
1785
1786    actors = renderer.GetActors()
1787    actors.InitTraversal()
1788    objs = []
1789    for _ in range(actors.GetNumberOfItems()):
1790        vactor = actors.GetNextActor()
1791        msh = Mesh(vactor)
1792        msh.name = "OBJMesh"
1793        tx = vactor.GetTexture()
1794        if tx:
1795            msh.texture(tx)
1796        objs.append(msh)
1797    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]:
1801def screenshot(filename="screenshot.png", scale=1, asarray=False) -> Union["vedo.Plotter", np.ndarray, None]:
1802    """
1803    Save a screenshot of the current rendering window.
1804
1805    Alternatively, press key `Shift-S` in the rendering window to save a screenshot.
1806    You can also use keyword `screenshot` in `show(..., screenshot="pic.png")`.
1807
1808    Arguments:
1809        scale : (int)
1810            Set image magnification as an integer multiplicative factor.
1811            E.g. setting a magnification of 2 produces an image twice as large,
1812            but 10x slower to generate.
1813        asarray : (bool)
1814            Return a numpy array of the image
1815    """
1816    filename = str(filename)
1817    # print("calling screenshot", filename, scale, asarray)
1818
1819    if not vedo.plotter_instance or not vedo.plotter_instance.window:
1820        # vedo.logger.error("in screenshot(), rendering window is not present, skip.")
1821        return vedo.plotter_instance  ##########
1822    
1823    if vedo.plotter_instance.renderer:
1824        vedo.plotter_instance.renderer.ResetCameraClippingRange()
1825
1826    if asarray and scale == 1 and not vedo.plotter_instance.offscreen:
1827        nx, ny = vedo.plotter_instance.window.GetSize()
1828        arr = vtki.vtkUnsignedCharArray()
1829        vedo.plotter_instance.window.GetRGBACharPixelData(0, 0, nx-1, ny-1, 0, arr)
1830        narr = vedo.vtk2numpy(arr).T[:3].T.reshape([ny, nx, 3])
1831        narr = np.flip(narr, axis=0)
1832        return narr  ##########
1833
1834    ###########################
1835    if filename.endswith(".pdf"):
1836        writer = vtki.new("GL2PSExporter")
1837        writer.SetRenderWindow(vedo.plotter_instance.window)
1838        writer.Write3DPropsAsRasterImageOff()
1839        writer.SilentOn()
1840        writer.SetSortToBSP()
1841        writer.SetFileFormatToPDF()
1842        writer.SetFilePrefix(filename.replace(".pdf", ""))
1843        writer.Write()
1844        return vedo.plotter_instance  ##########
1845
1846    elif filename.endswith(".svg"):
1847        writer = vtki.new("GL2PSExporter")
1848        writer.SetRenderWindow(vedo.plotter_instance.window)
1849        writer.Write3DPropsAsRasterImageOff()
1850        writer.SilentOn()
1851        writer.SetSortToBSP()
1852        writer.SetFileFormatToSVG()
1853        writer.SetFilePrefix(filename.replace(".svg", ""))
1854        writer.Write()
1855        return vedo.plotter_instance  ##########
1856
1857    elif filename.endswith(".eps"):
1858        writer = vtki.new("GL2PSExporter")
1859        writer.SetRenderWindow(vedo.plotter_instance.window)
1860        writer.Write3DPropsAsRasterImageOff()
1861        writer.SilentOn()
1862        writer.SetSortToBSP()
1863        writer.SetFileFormatToEPS()
1864        writer.SetFilePrefix(filename.replace(".eps", ""))
1865        writer.Write()
1866        return vedo.plotter_instance  ##########
1867
1868    if settings.screeshot_large_image:
1869        w2if = vtki.new("RenderLargeImage")
1870        w2if.SetInput(vedo.plotter_instance.renderer)
1871        w2if.SetMagnification(scale)
1872    else:
1873        w2if = vtki.new("WindowToImageFilter")
1874        w2if.SetInput(vedo.plotter_instance.window)
1875        if hasattr(w2if, "SetScale"):
1876            w2if.SetScale(int(scale), int(scale))
1877        if settings.screenshot_transparent_background:
1878            w2if.SetInputBufferTypeToRGBA()
1879        w2if.ReadFrontBufferOff()  # read from the back buffer
1880    w2if.Update()
1881
1882    if asarray:
1883        pd = w2if.GetOutput().GetPointData()
1884        npdata = utils.vtk2numpy(pd.GetArray("ImageScalars"))
1885        npdata = npdata[:, [0, 1, 2]]
1886        ydim, xdim, _ = w2if.GetOutput().GetDimensions()
1887        npdata = npdata.reshape([xdim, ydim, -1])
1888        npdata = np.flip(npdata, axis=0)
1889        return npdata ###########################
1890
1891    # elif settings.default_backend == "2d" and vedo.notebook_plotter:
1892    #     vedo.notebook_plotter.save(filename) # a PIL Image
1893    #     return vedo.notebook_plotter  ##########
1894
1895    if filename.lower().endswith(".png"):
1896        writer = vtki.new("PNGWriter")
1897        writer.SetFileName(filename)
1898        writer.SetInputData(w2if.GetOutput())
1899        writer.Write()
1900    elif filename.lower().endswith(".jpg") or filename.lower().endswith(".jpeg"):
1901        writer = vtki.new("JPEGWriter")
1902        writer.SetFileName(filename)
1903        writer.SetInputData(w2if.GetOutput())
1904        writer.Write()
1905    else:  # add .png
1906        writer = vtki.new("PNGWriter")
1907        writer.SetFileName(filename + ".png")
1908        writer.SetInputData(w2if.GetOutput())
1909        writer.Write()
1910    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:
1913def ask(*question, **kwarg) -> str:
1914    """
1915    Ask a question from command line. Return the answer as a string.
1916    See function `colors.printc()` for the description of the keyword options.
1917
1918    Arguments:
1919        options : (list)
1920            a python list of possible answers to choose from.
1921        default : (str)
1922            the default answer when just hitting return.
1923
1924    Example:
1925    ```python
1926    import vedo
1927    res = vedo.ask("Continue?", options=['Y','n'], default='Y', c='g')
1928    print(res)
1929    ```
1930    """
1931    kwarg.update({"end": " "})
1932    if "invert" not in kwarg:
1933        kwarg.update({"invert": True})
1934    if "box" in kwarg:
1935        kwarg.update({"box": ""})
1936
1937    options = kwarg.pop("options", [])
1938    default = kwarg.pop("default", "")
1939    if options:
1940        opt = "["
1941        for o in options:
1942            opt += o + "/"
1943        opt = opt[:-1] + "]"
1944        colors.printc(*question, opt, **kwarg)
1945    else:
1946        colors.printc(*question, **kwarg)
1947
1948    try:
1949        resp = input()
1950    except Exception:
1951        resp = ""
1952        return resp
1953
1954    if options:
1955        if resp not in options:
1956            if default and str(repr(resp)) == "''":
1957                return default
1958            colors.printc("Please choose one option in:", opt, italic=True, bold=False)
1959            kwarg["options"] = options
1960            return ask(*question, **kwarg)  # ask again
1961    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:
1965class Video:
1966    """
1967    Generate a video from a rendering window.
1968    """
1969
1970    def __init__(self, name="movie.mp4", duration=None, fps=24, scale=1, backend="imageio"):
1971        """
1972        Class to generate a video from the specified rendering window.
1973        Program `ffmpeg` is used to create video from each generated frame.
1974
1975        Arguments:
1976            name : (Union[str, os.PathLike])
1977                name of the output file.
1978            duration : (float)
1979                set the total `duration` of the video and recalculates `fps` accordingly.
1980            fps : (int)
1981                set the number of frames per second.
1982            scale : (int)
1983                set the image magnification as an integer multiplicative factor.
1984            backend : (str)
1985                the backend engine to be used `['imageio', 'ffmpeg', 'cv']`
1986
1987        Examples:
1988            - [make_video.py](https://github.com/marcomusy/vedo/tree/master/examples/other/make_video.py)
1989
1990            ![](https://user-images.githubusercontent.com/32848391/50739007-2bfc2b80-11da-11e9-97e6-620a3541a6fa.jpg)
1991        """
1992        self.name = str(name)
1993        self.duration = duration
1994        self.backend = backend
1995        self.fps = float(fps)
1996        self.command = "ffmpeg -loglevel panic -y -r"
1997        self.options = "-b:v 8000k"
1998        self.scale = scale
1999
2000        self.frames = []
2001        self.tmp_dir = TemporaryDirectory()
2002        self.get_filename = lambda x: os.path.join(self.tmp_dir.name, x)
2003        colors.printc(":video:  Video file", self.name, "is open... ", c="m", end="")
2004
2005    def add_frame(self) -> "Video":
2006        """Add frame to current video."""
2007        fr = self.get_filename(str(len(self.frames)) + ".png")
2008        screenshot(fr, scale=self.scale)
2009        self.frames.append(fr)
2010        return self
2011
2012    def pause(self, pause=0) -> "Video":
2013        """Insert a `pause`, in seconds."""
2014        fr = self.frames[-1]
2015        n = int(self.fps * pause)
2016        for _ in range(n):
2017            fr2 = self.get_filename(str(len(self.frames)) + ".png")
2018            self.frames.append(fr2)
2019            os.system("cp -f %s %s" % (fr, fr2))
2020        return self
2021
2022    def action(self, elevation=(0, 80), azimuth=(0, 359), cameras=(), resetcam=False) -> "Video":
2023        """
2024        Automatic shooting of a static scene by specifying rotation and elevation ranges.
2025
2026        Arguments:
2027            elevation : list
2028                initial and final elevation angles
2029            azimuth_range : list
2030                initial and final azimuth angles
2031            cameras : list
2032                list of cameras to go through, each camera can be dictionary or a vtkCamera
2033        """
2034        if not self.duration:
2035            self.duration = 5
2036
2037        plt = vedo.plotter_instance
2038        if not plt:
2039            vedo.logger.error("No vedo plotter found, cannot make video.")
2040            return self
2041        n = int(self.fps * self.duration)
2042
2043        cams = []
2044        for cm in cameras:
2045            cams.append(utils.camera_from_dict(cm))
2046        nc = len(cams)
2047
2048        plt.show(resetcam=resetcam, interactive=False)
2049
2050        if nc:
2051            for i in range(n):
2052                plt.move_camera(cams, i / (n-1))
2053                plt.render()
2054                self.add_frame()
2055
2056        else:  ########################################
2057
2058            for i in range(n):
2059                plt.camera.Elevation((elevation[1] - elevation[0]) / n)
2060                plt.camera.Azimuth((azimuth[1] - azimuth[0]) / n)
2061                plt.render()
2062                self.add_frame()
2063
2064        return self
2065
2066    def close(self) -> None:
2067        """
2068        Render the video and write it to file.
2069        """
2070        if self.duration:
2071            self.fps = int(len(self.frames) / float(self.duration) + 0.5)
2072            colors.printc("recalculated fps:", self.fps, c="m", end="")
2073        else:
2074            self.fps = int(self.fps)
2075
2076        ########################################
2077        if self.backend == "ffmpeg":
2078            out = os.system(
2079                self.command
2080                + " "
2081                + str(self.fps)
2082                + " -i "
2083                + f"'{self.tmp_dir.name}'"
2084                + os.sep
2085                + "%01d.png "
2086                + self.options
2087                + " "
2088                + f"'{self.name}'"
2089            )
2090            if out:
2091                vedo.logger.error(f":noentry: backend {self.backend} returning error: {out}")
2092            else:
2093                colors.printc(f":save: saved to {self.name}", c="m")
2094
2095        ########################################
2096        elif "cv" in self.backend:
2097            try:
2098                import cv2
2099            except ImportError:
2100                vedo.logger.error("opencv is not installed")
2101                return
2102
2103            cap = cv2.VideoCapture(os.path.join(self.tmp_dir.name, "%1d.png"))
2104            fourcc = cv2.VideoWriter_fourcc(*"mp4v")
2105            if vedo.plotter_instance:
2106                w, h = vedo.plotter_instance.window.GetSize()
2107                writer = cv2.VideoWriter(self.name, fourcc, self.fps, (w, h), True)
2108            else:
2109                vedo.logger.error("No vedo plotter found, cannot make video.")
2110                return
2111
2112            while True:
2113                ret, frame = cap.read()
2114                if not ret:
2115                    break
2116                writer.write(frame)
2117
2118            cap.release()
2119            writer.release()
2120
2121        ########################################
2122        elif "imageio" in self.backend:
2123            try:
2124                import imageio
2125            except ImportError:
2126                vedo.logger.error("Please install imageio with:\n pip install imageio[ffmpeg]")
2127                return
2128
2129            if self.name.endswith(".mp4"):
2130                writer = imageio.get_writer(self.name, fps=self.fps)
2131            elif self.name.endswith(".gif"):
2132                writer = imageio.get_writer(self.name, mode="I", duration=1 / self.fps)
2133            elif self.name.endswith(".webm"):
2134                writer = imageio.get_writer(self.name, format="webm", fps=self.fps)
2135            else:
2136                vedo.logger.error(f"Unknown format of {self.name}.")
2137                return
2138
2139            for f in utils.humansort(self.frames):
2140                image = imageio.v3.imread(f)
2141                try:
2142                    writer.append_data(image)
2143                except TypeError:
2144                    vedo.logger.error(f"Could not append data to video {self.name}")
2145                    vedo.logger.error("Please install imageio with: pip install imageio[ffmpeg]")
2146                    break
2147            try:
2148                writer.close()
2149                colors.printc(f"... saved as {self.name}", c="m")
2150            except:
2151                colors.printc(f":noentry: Could not save video {self.name}", c="r")
2152
2153        # finalize cleanup
2154        self.tmp_dir.cleanup()
2155
2156    def split_frames(self, output_dir="video_frames", prefix="frame_", format="png") -> None:
2157        """Split an existing video file into frames."""
2158        try:
2159            import imageio
2160        except ImportError:
2161            vedo.logger.error("\nPlease install imageio with:\n pip install imageio")
2162            return
2163
2164        # Create the output directory if it doesn't exist
2165        if not os.path.exists(output_dir):
2166            os.makedirs(output_dir)
2167
2168        # Create a reader object to read the video
2169        reader = imageio.get_reader(self.name)
2170
2171        # Loop through each frame of the video and save it as image
2172        print()
2173        for i, frame in utils.progressbar(
2174            enumerate(reader), title=f"writing {format} frames", c="m", width=20
2175        ):
2176            output_file = os.path.join(output_dir, f"{prefix}{str(i).zfill(5)}.{format}")
2177            imageio.imwrite(output_file, frame, format=format)

Generate a video from a rendering window.

Video(name='movie.mp4', duration=None, fps=24, scale=1, backend='imageio')
1970    def __init__(self, name="movie.mp4", duration=None, fps=24, scale=1, backend="imageio"):
1971        """
1972        Class to generate a video from the specified rendering window.
1973        Program `ffmpeg` is used to create video from each generated frame.
1974
1975        Arguments:
1976            name : (Union[str, os.PathLike])
1977                name of the output file.
1978            duration : (float)
1979                set the total `duration` of the video and recalculates `fps` accordingly.
1980            fps : (int)
1981                set the number of frames per second.
1982            scale : (int)
1983                set the image magnification as an integer multiplicative factor.
1984            backend : (str)
1985                the backend engine to be used `['imageio', 'ffmpeg', 'cv']`
1986
1987        Examples:
1988            - [make_video.py](https://github.com/marcomusy/vedo/tree/master/examples/other/make_video.py)
1989
1990            ![](https://user-images.githubusercontent.com/32848391/50739007-2bfc2b80-11da-11e9-97e6-620a3541a6fa.jpg)
1991        """
1992        self.name = str(name)
1993        self.duration = duration
1994        self.backend = backend
1995        self.fps = float(fps)
1996        self.command = "ffmpeg -loglevel panic -y -r"
1997        self.options = "-b:v 8000k"
1998        self.scale = scale
1999
2000        self.frames = []
2001        self.tmp_dir = TemporaryDirectory()
2002        self.get_filename = lambda x: os.path.join(self.tmp_dir.name, x)
2003        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:
2005    def add_frame(self) -> "Video":
2006        """Add frame to current video."""
2007        fr = self.get_filename(str(len(self.frames)) + ".png")
2008        screenshot(fr, scale=self.scale)
2009        self.frames.append(fr)
2010        return self

Add frame to current video.

def pause(self, pause=0) -> Video:
2012    def pause(self, pause=0) -> "Video":
2013        """Insert a `pause`, in seconds."""
2014        fr = self.frames[-1]
2015        n = int(self.fps * pause)
2016        for _ in range(n):
2017            fr2 = self.get_filename(str(len(self.frames)) + ".png")
2018            self.frames.append(fr2)
2019            os.system("cp -f %s %s" % (fr, fr2))
2020        return self

Insert a pause, in seconds.

def action( self, elevation=(0, 80), azimuth=(0, 359), cameras=(), resetcam=False) -> Video:
2022    def action(self, elevation=(0, 80), azimuth=(0, 359), cameras=(), resetcam=False) -> "Video":
2023        """
2024        Automatic shooting of a static scene by specifying rotation and elevation ranges.
2025
2026        Arguments:
2027            elevation : list
2028                initial and final elevation angles
2029            azimuth_range : list
2030                initial and final azimuth angles
2031            cameras : list
2032                list of cameras to go through, each camera can be dictionary or a vtkCamera
2033        """
2034        if not self.duration:
2035            self.duration = 5
2036
2037        plt = vedo.plotter_instance
2038        if not plt:
2039            vedo.logger.error("No vedo plotter found, cannot make video.")
2040            return self
2041        n = int(self.fps * self.duration)
2042
2043        cams = []
2044        for cm in cameras:
2045            cams.append(utils.camera_from_dict(cm))
2046        nc = len(cams)
2047
2048        plt.show(resetcam=resetcam, interactive=False)
2049
2050        if nc:
2051            for i in range(n):
2052                plt.move_camera(cams, i / (n-1))
2053                plt.render()
2054                self.add_frame()
2055
2056        else:  ########################################
2057
2058            for i in range(n):
2059                plt.camera.Elevation((elevation[1] - elevation[0]) / n)
2060                plt.camera.Azimuth((azimuth[1] - azimuth[0]) / n)
2061                plt.render()
2062                self.add_frame()
2063
2064        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:
2066    def close(self) -> None:
2067        """
2068        Render the video and write it to file.
2069        """
2070        if self.duration:
2071            self.fps = int(len(self.frames) / float(self.duration) + 0.5)
2072            colors.printc("recalculated fps:", self.fps, c="m", end="")
2073        else:
2074            self.fps = int(self.fps)
2075
2076        ########################################
2077        if self.backend == "ffmpeg":
2078            out = os.system(
2079                self.command
2080                + " "
2081                + str(self.fps)
2082                + " -i "
2083                + f"'{self.tmp_dir.name}'"
2084                + os.sep
2085                + "%01d.png "
2086                + self.options
2087                + " "
2088                + f"'{self.name}'"
2089            )
2090            if out:
2091                vedo.logger.error(f":noentry: backend {self.backend} returning error: {out}")
2092            else:
2093                colors.printc(f":save: saved to {self.name}", c="m")
2094
2095        ########################################
2096        elif "cv" in self.backend:
2097            try:
2098                import cv2
2099            except ImportError:
2100                vedo.logger.error("opencv is not installed")
2101                return
2102
2103            cap = cv2.VideoCapture(os.path.join(self.tmp_dir.name, "%1d.png"))
2104            fourcc = cv2.VideoWriter_fourcc(*"mp4v")
2105            if vedo.plotter_instance:
2106                w, h = vedo.plotter_instance.window.GetSize()
2107                writer = cv2.VideoWriter(self.name, fourcc, self.fps, (w, h), True)
2108            else:
2109                vedo.logger.error("No vedo plotter found, cannot make video.")
2110                return
2111
2112            while True:
2113                ret, frame = cap.read()
2114                if not ret:
2115                    break
2116                writer.write(frame)
2117
2118            cap.release()
2119            writer.release()
2120
2121        ########################################
2122        elif "imageio" in self.backend:
2123            try:
2124                import imageio
2125            except ImportError:
2126                vedo.logger.error("Please install imageio with:\n pip install imageio[ffmpeg]")
2127                return
2128
2129            if self.name.endswith(".mp4"):
2130                writer = imageio.get_writer(self.name, fps=self.fps)
2131            elif self.name.endswith(".gif"):
2132                writer = imageio.get_writer(self.name, mode="I", duration=1 / self.fps)
2133            elif self.name.endswith(".webm"):
2134                writer = imageio.get_writer(self.name, format="webm", fps=self.fps)
2135            else:
2136                vedo.logger.error(f"Unknown format of {self.name}.")
2137                return
2138
2139            for f in utils.humansort(self.frames):
2140                image = imageio.v3.imread(f)
2141                try:
2142                    writer.append_data(image)
2143                except TypeError:
2144                    vedo.logger.error(f"Could not append data to video {self.name}")
2145                    vedo.logger.error("Please install imageio with: pip install imageio[ffmpeg]")
2146                    break
2147            try:
2148                writer.close()
2149                colors.printc(f"... saved as {self.name}", c="m")
2150            except:
2151                colors.printc(f":noentry: Could not save video {self.name}", c="r")
2152
2153        # finalize cleanup
2154        self.tmp_dir.cleanup()

Render the video and write it to file.

def split_frames(self, output_dir='video_frames', prefix='frame_', format='png') -> None:
2156    def split_frames(self, output_dir="video_frames", prefix="frame_", format="png") -> None:
2157        """Split an existing video file into frames."""
2158        try:
2159            import imageio
2160        except ImportError:
2161            vedo.logger.error("\nPlease install imageio with:\n pip install imageio")
2162            return
2163
2164        # Create the output directory if it doesn't exist
2165        if not os.path.exists(output_dir):
2166            os.makedirs(output_dir)
2167
2168        # Create a reader object to read the video
2169        reader = imageio.get_reader(self.name)
2170
2171        # Loop through each frame of the video and save it as image
2172        print()
2173        for i, frame in utils.progressbar(
2174            enumerate(reader), title=f"writing {format} frames", c="m", width=20
2175        ):
2176            output_file = os.path.join(output_dir, f"{prefix}{str(i).zfill(5)}.{format}")
2177            imageio.imwrite(output_file, frame, format=format)

Split an existing video file into frames.