Source code for gudhi.off_utils

# This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT.
# See file LICENSE or go to https://gudhi.inria.fr/licensing/ for full license details.
# Author(s):       Vincent Rouvreau
#
# Copyright (C) 2016 Inria
#
# Modification(s):
#   - 2023/11 Vincent Rouvreau: numpy interface for read_points_from_off_file
#   - YYYY/MM Author: Description of the modification

__license__ = "MIT"


import errno
import os
import numpy as np


def _get_next_line(file_desc, comment="#"):
    """Return the next line that is not a comment.

    :param file_desc: An open file in read mode.
    :type file_desc: file
    :param comment: The characters or list of characters used to indicate the start of a comment.
    :type comment: string

    :returns: The next line.
    :rtype: string
    """
    while True:
        # file_desc.readline() is preferred to next(file_desc), as the second option is not compatible with
        # seek and tell methods
        line = file_desc.readline()
        if (not line.startswith(comment)) and len(line.split()) > 0:
            break
    return line


def _read_off_file_header(file_desc):
    """Return the information contained in the header of an OFF file.

    :param file_desc: An open file in read mode.
    :type file_desc: file

    :returns: The point cloud dimension, and the number of points.
    :rtype: tuple(int, int)

    Raises:
        ValueError: If the file does not respect the OFF file format.
    """
    nb_vertices = -1

    line = _get_next_line(file_desc)
    # First line should be "OFF" (3d case with some variants) "4OFF" (4d case) or "nOFF" (dD case)
    if line.lower().startswith("noff"):
        # "nOFF" case, next line is the dimension
        # can also contain nb_vertices, nb_faces nb_edges (can also be on the next line)
        line = _get_next_line(file_desc)
        digits = [int(s) for s in line.split() if s.isdigit()]
        dim = digits[0]
        if len(digits) > 1:
            nb_vertices = digits[1]
            # nb_faces =  digits[2]
            # nb_edges =  digits[3] # not used - can be ignored
            # nb_cells =  digits[4]
    elif line.lower().startswith("4off"):
        dim = 4
    # "OFF", "COFF" and "STOFF" are 3d cases - let's stick with the C++ interface
    elif line.lower().find("off") >= 0:
        dim = 3
    else:
        raise ValueError(
            f"Inconsistent OFF header, got '{line.rstrip()}', should be 'OFF', '4OFF' or 'nOFF'"
        )

    # nb_vertices can be already set by "nOFF" case, when 'dim nb_vertices nb_faces nb_edges' on the same line
    if nb_vertices < 0:
        # Number of points is the first number ("OFF" case) or the second one ("nOFF" case) of the second line
        line = _get_next_line(file_desc)
        digits = [int(s) for s in line.split() if s.isdigit()]
        nb_vertices = digits[0]
        # nb_faces =  digits[1]
        # nb_edges =  digits[2] # not used - can be ignored
        # nb_cells =  digits[3]
    # "_get_next_line + go back to the previous line" is just a hack for comments in the most likely places
    # TODO: remove "_get_next_line + go back to the previous line" when numpy ≥ 1.23.0 will be the standard
    line = _get_next_line(file_desc)
    # Here the first line without comment is read - let's go back to the beginning of this line
    file_desc.seek(file_desc.tell() - len(line))
    return dim, nb_vertices


[docs] def read_points_from_off_file(off_file=""): """Read points from an `OFF file <fileformats.html#off-file-format>`_. :param off_file: An OFF file style name. :type off_file: string :returns: The point set. :rtype: numpy.ndarray .. warning:: This function is using `numpy.loadtxt <https://numpy.org/doc/stable/reference/generated/numpy.loadtxt.html>`_ with `comments='#'` as an argument. Empty and or comment lines between the points are only supported with numpy &ge; 1.23.0. """ # newline='' is required for Windows, otherwise end of line with '\r\n' are only detected as '\n' # This is required by _read_off_file_header that needs the exact length of the line (to go backward in the file reading) with open(off_file, newline="") as input_file: dim, nb_points = _read_off_file_header(input_file) # usecols=list(range(dim)) stands here to avoid comments at the end of line # or colors that can be added in RGB format after the points, the faces, ... points = np.loadtxt( input_file, dtype=np.float64, comments="#", usecols=range(dim), max_rows=nb_points ) if points.shape != (nb_points, dim): raise ValueError(f"{points.shape} is different from expected ({nb_points}, {dim})") return points
[docs] def write_points_to_off_file(fname, points): """Write points to an `OFF file <fileformats.html#off-file-format>`_. A simple wrapper for `numpy.savetxt`. :param fname: Name of the OFF file. :type fname: str or file handle :param points: Point coordinates. :type points: numpy array of shape (n, dim) """ points = np.asarray(points) if len(points.shape) != 2: raise ValueError(f"{points.shape} has different length from expected length 2") dim = points.shape[1] if dim == 3: head = "OFF\n{} 0 0".format(points.shape[0]) else: head = "nOFF\n{} {} 0 0".format(dim, points.shape[0]) np.savetxt(fname, points, header=head, comments="")