mcgregor94086
mcgregor94086

Reputation: 1529

How to automatically crop an .OBJ 3D model to a bounding box?

In the now obsoleted Autodesk ReCap API it was possible to specify a "bounding box" around the scene to be generated from images.

In the resulting models, any vertices outside the bounding box were discarded, and any volumes that extended beyond the bounding box were truncated to have faces at the box boundaries.

I am now using Autodesk's Forge Reality Capture API which replaced ReCap. Apparently, This new API does not allow the user to specify a bounding box.

So I am now searching for a program that takes an .OBJ file and a specified bounding box as input, and outputs a file of just the vertices and faces within this bounding box.

Upvotes: 0

Views: 1066

Answers (2)

mcgregor94086
mcgregor94086

Reputation: 1529

Given that there is no way to specify the bounding box in Reality Capture API, I created this python program. It is crude, in that it only discards faces that have vertices that are outside the bounding box. And it actually does discards nondestructively, only by commenting them out in the output OBJ file. This allows you to uncomment them and then use a different bounding box.

This may not be what you need if you truly want to remove all relevant v, vn, vt, vp and f lines that are outside the bounding box, because the OBJ file size remains mostly unchanged. But for my particular needs, keeping all the records and just using comments was preferable.

# obj3Dcrop.py
# (c) Scott L. McGregor, Dec 2019
#  License: free for all non commercial uses.  Contact author for any other uses.
#  Changes and Enhancements must be shared with author, and be subject to same use terms

# TL;DR:  This program uses a bounding box, and "crops" faces and vertices from a
# Wavefront .OBJ format file, created by Autodesk Forge Reality Capture API
# if one of the vertices in a face is not within the bounds of the box.
#
# METHOD
# 1) All lines other than "v" vertex definitions and "f" faces definitions
#    are copied UNCHANGED from the input .OBJ file to an output .OBJ file.
# 2) All "v" vertex definition lines have their (x, y, z) positions tested to see if:
#    minX < x < maxX and minY < y < maxY and minZ < z < maxZ ?
#    If TRUE, we want to keep this vertex in the new OBJ, so we
#    store its IMPLICIT ORDINAL position in the file in a dictionary called v_keepers.
#    If FALSE, we will use its absence from the v_keepers file as a way to identify
#    faces that contain it and drop them.   All "v" lines are also copied unchanged to the
#    output file.
# 3) All "f" lines (face definitions) are inspected to verify that all 3 vertices in the face
#    are in the v_keepers list.   If they are, the f line is  output unchanged.
# 4) Any "f" line that refers to a vertex that was cropped, is prefixed by "# CROPPED: "
#    in the output file.  Lines beginning # are treated as comments, and ignored in future
#    processing.

# KNOWN LIMITATIONS: This program generates models in which the outside of bound faces
# have been removed. The vertices that were found outside the bounding box, are still in the
# OBJ file, but they are now disconnected and therefore ignored in later processing.
# The "f" lines for faces with vertices outside the bounding box are also still in the
# output file, but now commented out, so they don't process. Because this is non-destructive.
# we can easily change our bounding box later, uncomment cropped lines and reprocess.
#
# This might be an incomplete solution for some potential users.  For such users
# a more complete program would delete unneeded v, vn, vt and vp lines when the v vertex
# that they refer to is dropped. But note that this requires renumbering all references to these
# vertice definitions in the "f" face definition lines.  Such a more complete solution would also
# DISCARD all 'f' lines with any vertices that are out of bounds, instead of making them copies.
# Such a rewritten .OBJ file would be var more compact, but changing the bounding box would require
# saving the pre-cropped original.

# QUIRK: The OBJ file format defines v, vn, vt, vp and f elements by their
# IMPLICIT ordinal occurrence in the file,  with each element type maintaining
# its OWN separate sequence.  It then references those definitions EXPLICITLY in
# f face definitions. So deleting (or commenting out) element references requires
# appropriate rewriting of all the"f"" lines tracking all the new implicit positions.

# Such rewriting is not particularly hard to do, but it is one more place to make
# a mistake, and could  make the algorithm more complicated to understand.
# This program doesn't bother, because all further processing of the output
# OBJ file ignores unreferenced v, vn, vt and vp elements.
#
# Saving all lines rather than deleting them to save space is a tradeoff involving considerations of
# Undo capability, compute cycles, compute space (unreferenced lines) and maintenance complexity choice.
# It is left to the motivated programmer to add this complexity if needed.

import sys

#bounding_box = sys.argv[1] # should be in the only string passsed  (maxX, maxY, maxZ, minX, minY, minZ)
bounding_box = [10, 10, 10, -10, -10, 1]
maxX = bounding_box[0]
maxY = bounding_box[1]
maxZ = bounding_box[2]
minX = bounding_box[3]
minY = bounding_box[4]
minZ = bounding_box[5]

v_keepers = dict() # keeps track of which vertices are within the bounding box

kept_vertices = 0
discarded_vertices = 0

kept_faces = 0
discarded_faces = 0

discarded_lines = 0
kept_lines = 0

obj_file = open('sample.obj','r')
new_obj_file = open('cropped.obj','w')

# the number of the next "v" vertex lines to process.
original_v_number = 1 # the number of the next "v" vertex lines to process.
new_v_number = 1 # the new ordinal position of this vertex if out of bounds vertices were discarded.

for line in obj_file:
    line_elements = line.split()

    # Python doesn't have a SWITCH statement, but we only have three cases, so we'll just use cascading if stmts
    if line_elements[0] != "f":  # if it isn't an "f" type line (face definition)

        if line_elements[0] != "v": # and it isn't an "v" type line either (vertex definition)
            # ************************ PROCESS ALL NON V AND NON F LINE TYPES ******************
            # then we just copy it unchanged from the input OBJ to the output OBJ
            new_obj_file.write(line)
            kept_lines = kept_lines + 1

        else: # then line_elements[0] == "v":
            # ************************ PROCESS VERTICES ****************************************
            #  a "v" line looks like this:
            #  f x y z ...
            x = float(line_elements[1])
            y = float(line_elements[2])
            z = float(line_elements[3])

            if minX < x < maxX and minY < y < maxY and minZ < z < maxZ:
                # if vertex is within  the bounding box, we include it in the new OBJ file
                new_obj_file.write(line)
                v_keepers[str(original_v_number)] = str(new_v_number)
                new_v_number = new_v_number + 1
                kept_vertices =  kept_vertices +1
                kept_lines = kept_lines + 1
            else:     # if vertex is NOT in the bounding box
                new_obj_file.write(line)
                discarded_vertices = discarded_vertices +1
                discarded_lines = discarded_lines + 1
            original_v_number = original_v_number + 1

    else: # line_elements[0] == "f":
        # ************************ PROCESS FACES ****************************************
        #  a "f" line looks like this:
        #  f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3 ...

        #  We need to delete any face lines where ANY of the 3 vertices v1, v2 or v3 are NOT in v_keepers.


        v = ["", "", ""]
        # Note that v1, v2 and v3 are the first "/" separated elements within each line element.
        for i in range(0,3):
            v[i] = line_elements[i+1].split('/')[0]

        # now we can check if EACH of these 3 vertices are  in v_keepers.
        # for each f line, we need to determine if all 3 vertices are in the v_keepers list
        if v[0] in v_keepers and v[1] in v_keepers and v[2] in v_keepers:
            new_obj_file.write(line)
            kept_lines = kept_lines + 1
            kept_faces = kept_faces +1
        else: # at least one of the vertices in this face has been deleted, so we need to delete the face too.
            discarded_lines = discarded_lines + 1
            discarded_faces = discarded_faces +1
            new_obj_file.write("# CROPPED "+line)

# end of line processing loop
obj_file.close()
new_obj_file.close()

print ("kept vertices: ",  kept_vertices ,"discarded vertices: ", discarded_vertices)
print ("kept faces:     ", kept_faces,     "discarded faces:   ", discarded_faces)
print ("kept lines:     ", kept_lines,     "discarded lines:   ", discarded_lines)

Upvotes: 1

denis-grigor
denis-grigor

Reputation: 515

Unfortunately, (at least for now) there is no way to specify the bounding box in Reality Capture API.

Upvotes: 0

Related Questions