Skip to content

Instantly share code, notes, and snippets.

@acrosby
Last active February 12, 2024 12:18
Show Gist options
  • Save acrosby/11180502 to your computer and use it in GitHub Desktop.
Save acrosby/11180502 to your computer and use it in GitHub Desktop.
This code demonstrates how to read a binary STL file and calculate its volume in python.
import numpy as np
import struct
def read_stl(filename):
with open(filename) as f:
Header = f.read(80)
nn = f.read(4)
Numtri = struct.unpack('i', nn)[0]
record_dtype = np.dtype([
('Normals', np.float32, (3,)),
('Vertex1', np.float32, (3,)),
('Vertex2', np.float32, (3,)),
('Vertex3', np.float32, (3,)),
('atttr', '<i2', (1,)),
])
data = np.zeros((Numtri,), dtype=record_dtype)
for i in range(0, Numtri, 10):
d = np.fromfile(f, dtype=record_dtype, count=10)
data[i:i+len(d)] = d
#normals = data['Normals']
v1 = data['Vertex1']
v2 = data['Vertex2']
v3 = data['Vertex3']
points = np.hstack(((v1[:, np.newaxis, :]), (v2[:, np.newaxis, :]), (v3[:, np.newaxis, :])))
return points
def calc_volume(points):
'''
Calculate the volume of an stl represented in m x 3 x 3 points array. Expected that input units is mm, so that
output is in cubic centimeters (cc).
'''
v = points
volume = np.asarray([np.cross(v[i, 0, :], v[i, 1, :]).dot(v[i, 2, :]) for i in range(points.shape[0])])
return np.abs(volume.sum()/6.)/(10.**3.)
def bounding_box(points):
'''
Calculate the bounding box edge lengths of an stl using the design coordinate system (not an object oriented bounding box),
expect that input coordinates are in mm.
'''
v = points
x = v[..., 0].flatten()
y = v[..., 1].flatten()
z = v[..., 2].flatten()
return (x.max()-x.min(), y.max()-y.min(), z.max()-z.min())
def iter_calc_volume(filename):
# open as binary
with open(filename, 'rb') as f:
Header = f.read(80)
nn = f.read(4)
Numtri = struct.unpack('i', nn)[0]
record_dtype = np.dtype([
('Normals', np.float32, (3,)),
('Vertex1', np.float32, (3,)),
('Vertex2', np.float32, (3,)),
('Vertex3', np.float32, (3,)),
('atttr', '<i2', (1,)),
])
volume = 0.
for i in range(0, Numtri, 1):
d = np.fromfile(f, dtype=record_dtype, count=1)
v1 = d['Vertex1'][0]
v2 = d['Vertex2'][0]
v3 = d['Vertex3'][0]
volume += np.cross(v1, v2).dot(v3)
return np.abs(volume/6.)/(10.**3.)
def iter_calc_bounding(filename):
# open as binary
with open(filename, 'rb') as f:
Header = f.read(80)
nn = f.read(4)
Numtri = struct.unpack('i', nn)[0]
record_dtype = np.dtype([
('Normals', np.float32, (3,)),
('Vertex1', np.float32, (3,)),
('Vertex2', np.float32, (3,)),
('Vertex3', np.float32, (3,)),
('atttr', '<i2', (1,)),
])
xmax = -9999
xmin = 9999
ymax = -9999
ymin = 9999
zmax = -9999
zmin = 9999
for i in range(0, Numtri, 1):
d = np.fromfile(f, dtype=record_dtype, count=1)
v1 = d['Vertex1']
v2 = d['Vertex2']
v3 = d['Vertex3']
v = np.hstack(((v1[:, np.newaxis, :]), (v2[:, np.newaxis, :]), (v3[:, np.newaxis, :])))
x = v[..., 0].flatten()
y = v[..., 1].flatten()
z = v[..., 2].flatten()
tmp_xmin = x.min()
tmp_xmax = x.max()
tmp_ymin = y.min()
tmp_ymax = y.max()
tmp_zmin = z.min()
tmp_zmax = z.max()
xmax = max((tmp_xmax, xmax))
xmin = min((tmp_xmin, xmin))
ymax = max((tmp_ymax, ymax))
ymin = min((tmp_ymin, ymin))
zmax = max((tmp_zmax, zmax))
zmin = min((tmp_zmin, zmin))
X = xmax-xmin
Y = ymax-ymin
Z = zmax-zmin
return (X, Y, Z)
def test(filename):
points = read_stl(filename)
vol1 = calc_volume(points)
vol2 = iter_calc_volume(filename)
print(vol1, vol2)
assert np.isclose(vol1, vol2) # test for equal taking into account
# floating point precision
bb1 = bounding_box(points)
bb2 = iter_calc_bounding(filename)
print(bb1, bb2)
assert np.isclose(bb1[0], bb2[0]) and np.isclose(bb1[1], bb2[1]) and np.isclose(bb1[2], bb2[2])
return True
if __name__ == "__main__":
import sys
if sys.argv[1] == '-t':
test(sys.argv[2])
print("Passed!")
else:
filename = sys.argv[1]
print("Calculating the volume of %s in cc's" % (filename,))
print("The volume is: %f" % (iter_calc_volume(filename),))
print("The bounding box is: %s" % (iter_calc_bounding(filename),))
@carribeiro
Copy link

carribeiro commented Apr 15, 2022

Hi @acrosby! Your script saved my day, I had two folders full of STLs that I needed to process to find the bounding box.

I've converted this file using the 2to3 tool that comes bundled with Python, then made two very small adjustments to open the files as binary using the "rb" (read binary) mode. That's because Python 3 tries to open the files as text and gives a UnicodeError if it finds something it doesn't like.

@acrosby
Copy link
Author

acrosby commented Apr 17, 2022

@carribeiro I'm glad it was helpful! I haven't used this since the switch to Python3, if you want to post a diff or just or your changed version here I'll update my version of the Gist.

@carribeiro
Copy link

Hi @acrosby here follows my update. I've done the 2to3 upgrade and changed the file mode at the open() call.

import numpy as np
import struct


def read_stl(filename):
    with open(filename) as f:
        Header = f.read(80)
        nn = f.read(4)
        Numtri = struct.unpack('i', nn)[0]
        record_dtype = np.dtype([
            ('Normals', np.float32, (3,)),
            ('Vertex1', np.float32, (3,)),
            ('Vertex2', np.float32, (3,)),
            ('Vertex3', np.float32, (3,)),
            ('atttr', '<i2', (1,)),
        ])
        data = np.zeros((Numtri,), dtype=record_dtype)
        for i in range(0, Numtri, 10):
            d = np.fromfile(f, dtype=record_dtype, count=10)
            data[i:i+len(d)] = d

    #normals = data['Normals']
    v1 = data['Vertex1']
    v2 = data['Vertex2']
    v3 = data['Vertex3']
    points = np.hstack(((v1[:, np.newaxis, :]), (v2[:, np.newaxis, :]), (v3[:, np.newaxis, :])))
    return points


def calc_volume(points):
    '''
    Calculate the volume of an stl represented in m x 3 x 3 points array. Expected that input units is mm, so that
    output is in cubic centimeters (cc).
    '''
    v = points
    volume = np.asarray([np.cross(v[i, 0, :], v[i, 1, :]).dot(v[i, 2, :]) for i in range(points.shape[0])])
    return np.abs(volume.sum()/6.)/(10.**3.)


def bounding_box(points):
    '''
    Calculate the bounding box edge lengths of an stl using the design coordinate system (not an object oriented bounding box),
    expect that input coordinates are in mm.
    '''
    v = points
    x = v[..., 0].flatten()
    y = v[..., 1].flatten()
    z = v[..., 2].flatten()
    return (x.max()-x.min(), y.max()-y.min(), z.max()-z.min())


def iter_calc_volume(filename):
    # open as binary
    with open(filename, 'rb') as f:
        Header = f.read(80)
        nn = f.read(4)
        Numtri = struct.unpack('i', nn)[0]
        record_dtype = np.dtype([
            ('Normals', np.float32, (3,)),
            ('Vertex1', np.float32, (3,)),
            ('Vertex2', np.float32, (3,)),
            ('Vertex3', np.float32, (3,)),
            ('atttr', '<i2', (1,)),
        ])
        volume = 0.
        for i in range(0, Numtri, 1):
            d = np.fromfile(f, dtype=record_dtype, count=1)
            v1 = d['Vertex1'][0]
            v2 = d['Vertex2'][0]
            v3 = d['Vertex3'][0]
            volume += np.cross(v1, v2).dot(v3)
    return np.abs(volume/6.)/(10.**3.)


def iter_calc_bounding(filename):
    # open as binary
    with open(filename, 'rb') as f:
        Header = f.read(80)
        nn = f.read(4)
        Numtri = struct.unpack('i', nn)[0]
        record_dtype = np.dtype([
            ('Normals', np.float32, (3,)),
            ('Vertex1', np.float32, (3,)),
            ('Vertex2', np.float32, (3,)),
            ('Vertex3', np.float32, (3,)),
            ('atttr', '<i2', (1,)),
        ])
        xmax = -9999
        xmin = 9999
        ymax = -9999
        ymin = 9999
        zmax = -9999
        zmin = 9999
        for i in range(0, Numtri, 1):
            d = np.fromfile(f, dtype=record_dtype, count=1)
            v1 = d['Vertex1']
            v2 = d['Vertex2']
            v3 = d['Vertex3']
            v = np.hstack(((v1[:, np.newaxis, :]), (v2[:, np.newaxis, :]), (v3[:, np.newaxis, :])))
            x = v[..., 0].flatten()
            y = v[..., 1].flatten()
            z = v[..., 2].flatten()
            tmp_xmin = x.min()
            tmp_xmax = x.max()
            tmp_ymin = y.min()
            tmp_ymax = y.max()
            tmp_zmin = z.min()
            tmp_zmax = z.max()
            xmax = max((tmp_xmax, xmax))
            xmin = min((tmp_xmin, xmin))
            ymax = max((tmp_ymax, ymax))
            ymin = min((tmp_ymin, ymin))
            zmax = max((tmp_zmax, zmax))
            zmin = min((tmp_zmin, zmin))
    X = xmax-xmin
    Y = ymax-ymin
    Z = zmax-zmin
    return (X, Y, Z)


def test(filename):
    points = read_stl(filename)
    vol1 = calc_volume(points)
    vol2 = iter_calc_volume(filename)
    print(vol1, vol2)
    assert np.isclose(vol1, vol2)  # test for equal taking into account
                                   # floating point precision
    bb1 = bounding_box(points)
    bb2 = iter_calc_bounding(filename)
    print(bb1, bb2)
    assert np.isclose(bb1[0], bb2[0]) and np.isclose(bb1[1], bb2[1]) and np.isclose(bb1[2], bb2[2])

    return True


if __name__ == "__main__":
    import sys
    if sys.argv[1] == '-t':
        test(sys.argv[2])
        print("Passed!")
    else:
        filename = sys.argv[1]
        print("Calculating the volume of %s in cc's" % (filename,))
        print("The volume is: %f" % (iter_calc_volume(filename),))
        print("The bounding box is: %s" % (iter_calc_bounding(filename),))

@PeterBan11
Copy link

Can anyone help me? I get an error:

File "D:_CastingSystem\calc_volume.py", line 7, in read_stl
Header = f.read(80)
^^^^^^^^^^
File "C:\Users\New\AppData\Local\Programs\Python\Python311\Lib\encodings\cp1251.py", line 23, in decode
return codecs.charmap_decode(input,self.errors,decoding_table)[0]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
UnicodeDecodeError: 'charmap' codec can't decode byte 0x98 in position 152: character maps to

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment