User Tools

Site Tools


clubs:python_club:python_club_ex_reduce_stl_files
Home | clubs :: cloud club :: python_club :: 3D-Printing | projects :: Proxmox | Kubernetes | scripting | utilities | games

About the Club

Python Club Topics - Exercise: Reduce 3D stl file size

Exercise: Reduce the file size of a given 3D STL file ( .stl )

  1. Output: Accept arguments of input + output files and a reduction factor ( % as number between 0 and .99 ) or as percent with –percent option.
  2. What you learn from the example:
    1. File manipulation
    2. STL model manipulation
    3. Use an external application/tool/command ( MeshLab in this case ) to complete a task
    4. Use specific python libraries

Solution

[1] code:python show
#!/usr/bin/env python3
'''
STL Reducer - A utility to reduce the file size of 3D STL files.

This script takes an STL file and reduces its size by simplifying the mesh while
attempting to preserve the overall shape and features of the 3D model.

Requirements:
    - numpy-stl
    - pymeshlab

Usage:
    python stl_reducer.py –input INPUT_FILE –output OUTPUT_FILE –reduction REDUCTION_FACTOR

Example:
    python stl_reducer.py –input model.stl –output model_reduced.stl –reduction 0.5
    python stl_reducer.py –input model.stl –output model_reduced.stl –reduction 50 –percent
'''

import os
import sys
import time
import argparse
import numpy as np
from stl import mesh
import pymeshlab


def check_file_size(file_path):
    '''
    Get file size in bytes and human-readable format.
    
    Args:
        file_path: Path to the file
        
    Returns:
        tuple: (size_in_bytes, human_readable_size)
    '''
    size_bytes = os.path.getsize(file_path)
    
    # Convert to human-readable format
    units = ['B', 'KB', 'MB', 'GB']
    size = size_bytes
    unit_index = 0
    
    while size >= 1024 and unit_index < len(units) - 1:
        size /= 1024
        unit_index += 1
    
    return size_bytes, f'{size:.2f} {units[unit_index]}'


def validate_stl(file_path):
    '''
    Validate that the file is a proper STL file.
    
    Args:
        file_path: Path to the STL file
        
    Returns:
        bool: True if valid, False otherwise
    '''
    try:
        # Try to load the mesh
        test_mesh = mesh.Mesh.from_file(file_path)
        
        # Check if the mesh has valid data
        if test_mesh.data.size == 0:
            return False
            
        return True
    except Exception:
        return False


def reduce_stl(input_file, output_file, reduction_factor, is_percent=False):
    '''
    Reduce STL file size by simplifying the mesh.
    
    Args:
        input_file: Path to the input STL file
        output_file: Path to save the reduced STL file
        reduction_factor: Factor to reduce by (0-1) or percentage (1-100)
        is_percent: Whether the reduction factor is a percentage
        
    Returns:
        tuple: (original_size, reduced_size) in bytes
    '''
    # Convert percentage to decimal if needed
    if is_percent:
        if reduction_factor < 1 or reduction_factor > 99:
            raise ValueError('Percentage must be between 1 and 99')
        reduction_factor = reduction_factor / 100

    # Check that reduction factor is valid
    if reduction_factor ⇐ 0 or reduction_factor >= 1:
        if not is_percent:
            raise ValueError('Reduction factor must be between 0 and 1')
    
    print(f'Loading STL file: {input_file}')
    original_size_bytes, original_size_hr = check_file_size(input_file)
    print(f'Original size: {original_size_hr}')
    
    # Create a MeshSet
    ms = pymeshlab.MeshSet()
    
    # Load the mesh
    ms.load_new_mesh(input_file)
    
    # Get initial triangle count
    initial_triangles = ms.current_mesh().face_number()
    print(f'Initial triangle count: {initial_triangles}')
    
    # Calculate target number of faces (triangles)
    target_faces = int(initial_triangles * reduction_factor)
    print(f'Target triangle count: {target_faces}')
    
    # Apply quadric edge collapse decimation
    print('Reducing mesh…')
    ms.apply_filter('meshing_decimation_quadric_edge_collapse', 
                    targetfacenum=target_faces, 
                    preserveboundary=True,
                    preservenormal=True,
                    optimalplacement=True)
    
    # Save the reduced mesh
    print(f'Saving reduced mesh to: {output_file}')
    ms.save_current_mesh(output_file)
    
    # Check the reduced file size
    reduced_size_bytes, reduced_size_hr = check_file_size(output_file)
    
    # Calculate reduction percentage
    reduction_percent = ((original_size_bytes - reduced_size_bytes) / original_size_bytes) * 100
    
    # Get final triangle count
    final_triangles = ms.current_mesh().face_number()
    triangle_reduction_percent = ((initial_triangles - final_triangles) / initial_triangles) * 100
    
    return original_size_bytes, reduced_size_bytes, initial_triangles, final_triangles


def main():
    '''Main function to handle CLI arguments and execute the reduction process.'''
    parser = argparse.ArgumentParser(
        description='Reduce the size of STL files by simplifying the mesh.',
        epilog='Example: python stl_reducer.py –input model.stl –output model_reduced.stl –reduction 0.5'
    )
    
    parser.add_argument('–input', '-i', required=True, help='Input STL file path')
    parser.add_argument('–output', '-o', required=True, help='Output STL file path')
    parser.add_argument('–reduction', '-r', type=float, required=True, 
                      help='Reduction factor (0-1) or percentage (1-100 with –percent)')
    parser.add_argument('–percent', '-p', action='store_true', 
                      help='Interpret reduction factor as percentage to keep (1-99)')
    
    args = parser.parse_args()
    
    # Check if input file exists
    if not os.path.isfile(args.input):
        print(f'Error: Input file '{args.input}' not found.')
        sys.exit(1)
    
    # Validate STL file
    if not validate_stl(args.input):
        print(f'Error: '{args.input}' is not a valid STL file.')
        sys.exit(1)
    
    # Check if output path is writable
    output_dir = os.path.dirname(args.output)
    if output_dir and not os.access(output_dir, os.W_OK):
        print(f'Error: Cannot write to output directory '{output_dir}'.')
        sys.exit(1)
    
    try:
        # Start timer
        start_time = time.time()
        
        # Perform the reduction
        original_size, reduced_size, initial_triangles, final_triangles = reduce_stl(
            args.input, args.output, args.reduction, args.percent
        )
        
        # End timer
        elapsed_time = time.time() - start_time
        
        # Calculate reduction percentages
        size_reduction_percent = ((original_size - reduced_size) / original_size) * 100
        triangle_reduction_percent = ((initial_triangles - final_triangles) / initial_triangles) * 100
        
        # Print results
        print('\nReduction completed successfully!')
        print(f'Elapsed time: {elapsed_time:.2f} seconds')
        print('\nResults:')
        print(f'Original size: {original_size / 1024:.2f} KB')
        print(f'Reduced size: {reduced_size / 1024:.2f} KB')
        print(f'Size reduction: {size_reduction_percent:.2f}%')
        print(f'Original triangle count: {initial_triangles}')
        print(f'Reduced triangle count: {final_triangles}')
        print(f'Triangle reduction: {triangle_reduction_percent:.2f}%')
        
    except ValueError as ve:
        print(f'Error: {ve}')
        sys.exit(1)
    except Exception as e:
        print(f'An unexpected error occurred: {e}')
        sys.exit(1)


if __name__ == '__main__':
    main()


clubs/python_club/python_club_ex_reduce_stl_files.txt · Last modified: by 127.0.0.1