Software Engineering

Software Engineering

Smart Palletizer: Optimizing Warehouse Packing with 3D Bin-Packing

Smart Palletizer: Optimizing Warehouse Packing with 3D Bin-Packing

Smart Palletizer: Optimizing Warehouse Packing with 3D Bin-Packing

May 12, 2025

|

8

min read

The Challenge: Optimizing Pallet Packaging for Logistics

We were approached by a client in the warehousing industry facing a persistent and costly issue:

  • Boxes were packed manually, leading to poor space usage and frequent errors

  • Pallet space was underused often by 15 – 30%, driving up shipping volume

  • Weight distribution was uneven, raising safety and compliance concerns

  • Manual adjustments consumed hours daily, slowing operations and causing stress

These inefficiencies weren’t just operational .They were eating into margins, increasing shipping costs, and putting unnecessary pressure on the team.

Our Solution: A Desktop Tool for Heuristic-Based Pallet Packing and Visualization

To solve this, we developed a cross-platform Qt application that automates every step from intelligently grouping boxes to generating optimized, layer-by-layer pallet diagrams and exportable packing reports.

  1. User loads a file (Excel .xlsx or JSON) containing all box entries.

  2. Automated grouping by size assigns each box to the best candidate pallet templates.

  3. Parallel packing threads run the Py3DBP engine across all pallet types, then a custom pooling manager redistributes items for maximal fill and balanced loads.

  4. Real-time 3D animation shows a spinning “loading” pallet during computation, then an interactive 3D view of the final arrangement switchable between front/right/back/left/top perspectives.

  5. Layer-by-layer 2D diagrams are generated and saved into a multi-page PDF, complete with box IDs labeled at their true coordinates.

  6. One-click exports to PDF, PNG or CSV create all necessary shipping documentation, with direct links to each pallet’s detailed view.

With this tool, our client eliminated layout errors and manual guesswork, gained instant visibility into 3D and 2D packing plans, and significantly reduced the number of pallets shipped resulting in substantial cost savings on every delivery.
Below is a screenshot of the desktop application in action, showcasing how the optimized packing layout is visualized for both operators and planners.

Workflow Overview

This diagram illustrates the three core stages of the Pallet Optimization Tool:


Diagram Type: UML Activity Diagram

Key Code Snippets

  1. Grouping & JSON Preparation (utils.py)

    def group_by_size(items): 
        """ 
        Groups items by their (length, width, height). 
        Returns a dict mapping size tuples to lists of items. 
        """ 
         
     
     
    def assign_pallets_to_groups(groups): 
        """ 
        Returns a dict mapping each size-tuple to the best-fitting PalletType code. 
        """ 
      


  2. Multi‑Pallet Packing Solver (solver.py)

    # solver.py 
     
    def solve_multi_pallet_py3dbp(boxes, pallet: PalletType, tol=0): 
        """ 
        Given a list of box dicts and a PalletType, 
        spins up one Bin per box to allow maximal packing, 
        runs the 3D bin‑packing solver, and returns only the filled bins. 
        """


  3. Pooled Bin Manager & Redistribution (pooled_bin_manager.py)

    # pooled_bin_manager.py 
    class PooledBinManager: 
        def __init__(self, bins, tol, callback=lambda msg: None): 
            """ 
            . 
     
        def redistribute(self): 
            """Repeatedly merge and repack until no more improvements.""" 
            # single-pass merges


  4. GUI Integration & 3D Visualization (mainwindow.py)

    def draw_loading_cube(ax, l, w, h): 
        verts = [ 
            [(0,0,0),(l,0,0),(l,w,0),(0,w,0)], 
            [(0,0,h),(l,0,h),(l,w,h),(0,w,h)], 
            # other faces... 
        ] 
        cube = Poly3DCollection(verts, alpha=0.2) 
        ax.add_collection3d(cube) 
        ax.set_box_aspect((l,w,h)) 
     
     
    def draw_boxes(ax, boxes): 
        for idx, (x,y,z,l,w,h,_) in enumerate(boxes): 
            faces = [ 
                [(x,y,z),(x+l,y,z),(x+l,y+w,z),(x,y+w,z)], 
                # other faces... 
            ] 
            poly = Poly3DCollection(faces, alpha=0.8) 
            ax.add_collection3d(poly)
    
    


  5. Loading Data & Grouping (mainwindow.py)

    # mainwindow.py 
     
    # Triggered when user selects an Excel file 
    def load_excel(self): 
        self.load_excel_action.setEnabled(False) 
        path, _ = QFileDialog.getOpenFileName(self, 'Open Boxes xlsx', '', 'Excel Files (*.xlsx)') 
        if not path: 
            self.load_excel_action.setEnabled(True) 
            return 
        try: 
            self.loaded_boxes = json.loads(excel_to_json(path)) 
        except Exception as e: 
            QMessageBox.critical(self, 'Error', f'Failed to load data: {e}') 
            self.load_excel_action.setEnabled(True) 
            return 
        self.file_label.setText(f'Imported file: {os.path.basename(path)}') 
        self.prepare_loaded() 
     
    # Triggered when user selects a JSON file 
    def load_json(self): 
        self.load_action.setEnabled(False) 
        path, _ = QFileDialog.getOpenFileName(self, 'Open Boxes JSON', '', 'JSON Files (*.json)') 
        if not path: 
            self.load_action.setEnabled(True) 
            return 
        try: 
            with open(path) as f: 
                self.loaded_boxes = json.load(f) 
        except Exception as e: 
            QMessageBox.critical(self, 'Error', f'Failed to load JSON: {e}') 
            self.load_action.setEnabled(True) 
            return 
        self.file_label.setText(f'Imported file: {os.path.basename(path)}') 
        self.prepare_loaded()


  6. Redistributing Unfitted Items (pooled_bin_manager.py)

    # pooled_bin_manager.py 
     
    class PooledBinManager: 
        ... 
        def redistribute(self) -> Tuple[int, List[Bin], List[Any]]: 
            """ 
            Repeatedly redistribute and merge items until no further improvements. 
            Returns (total_moved, final_bins, unplaceable_items). 
            """ 
            # 1) perform single-pass merges until stable 
            while True: 
                moved = self._single_pass() 
                self.total_moved += moved 
                if moved == 0: 
                    break 
     
           . 
            return self.total_moved, self.bins, unplaceable
    
    


  7. Exporting Packing Plans for the Warehouse Floor

    To make the optimized pallet configurations actionable for logistics teams, we implemented a PDF export feature directly within the desktop application. This allows warehouse workers to receive clear, step-by-step visual instructions on how to pack each pallet ensuring accurate, efficient, and error-free execution on the ground.


    Why It Matters

    Shipping optimization is only as good as its execution. Our export functionality bridges the gap between digital planning and physical operations by providing printable packing plans that detail exactly how each pallet should be assembled layer by layer, box by box.


    How It Works

    For every pallet, the system automatically generates a PDF file (e.g., pallet_1.pdf) that includes:

    • One page per Z-layer of the pallet

    • A bounding box showing the maximum pallet footprint (X and Y)

    • Precise box positions and dimensions

    • Box identifiers clearly labeled at the center of each item

    • A structured title for each page, such as:
      Pallet 2 – Layer 3 (Z = 120.0 mm)

    This format ensures that logistics workers can immediately understand the layout and reproduce it with accuracy.

    # Called when user selects an export format 
    def export_results(self, format: str): 
        filters = {'pdf': 'PDF Files (*.pdf)', 'png': 'PNG Files (*.png)', 'csv': 'CSV Files (*.csv)'} 
        path, _ = QFileDialog.getSaveFileName(self, 'Export Results', '', filters[format]) 
       
    
    

    Visual Preview :

    Here is a sample output showing one layer of a pallet:


The Challenge: Optimizing Pallet Packaging for Logistics

We were approached by a client in the warehousing industry facing a persistent and costly issue:

  • Boxes were packed manually, leading to poor space usage and frequent errors

  • Pallet space was underused often by 15 – 30%, driving up shipping volume

  • Weight distribution was uneven, raising safety and compliance concerns

  • Manual adjustments consumed hours daily, slowing operations and causing stress

These inefficiencies weren’t just operational .They were eating into margins, increasing shipping costs, and putting unnecessary pressure on the team.

Our Solution: A Desktop Tool for Heuristic-Based Pallet Packing and Visualization

To solve this, we developed a cross-platform Qt application that automates every step from intelligently grouping boxes to generating optimized, layer-by-layer pallet diagrams and exportable packing reports.

  1. User loads a file (Excel .xlsx or JSON) containing all box entries.

  2. Automated grouping by size assigns each box to the best candidate pallet templates.

  3. Parallel packing threads run the Py3DBP engine across all pallet types, then a custom pooling manager redistributes items for maximal fill and balanced loads.

  4. Real-time 3D animation shows a spinning “loading” pallet during computation, then an interactive 3D view of the final arrangement switchable between front/right/back/left/top perspectives.

  5. Layer-by-layer 2D diagrams are generated and saved into a multi-page PDF, complete with box IDs labeled at their true coordinates.

  6. One-click exports to PDF, PNG or CSV create all necessary shipping documentation, with direct links to each pallet’s detailed view.

With this tool, our client eliminated layout errors and manual guesswork, gained instant visibility into 3D and 2D packing plans, and significantly reduced the number of pallets shipped resulting in substantial cost savings on every delivery.
Below is a screenshot of the desktop application in action, showcasing how the optimized packing layout is visualized for both operators and planners.

Workflow Overview

This diagram illustrates the three core stages of the Pallet Optimization Tool:


Diagram Type: UML Activity Diagram

Key Code Snippets

  1. Grouping & JSON Preparation (utils.py)

    def group_by_size(items): 
        """ 
        Groups items by their (length, width, height). 
        Returns a dict mapping size tuples to lists of items. 
        """ 
         
     
     
    def assign_pallets_to_groups(groups): 
        """ 
        Returns a dict mapping each size-tuple to the best-fitting PalletType code. 
        """ 
      


  2. Multi‑Pallet Packing Solver (solver.py)

    # solver.py 
     
    def solve_multi_pallet_py3dbp(boxes, pallet: PalletType, tol=0): 
        """ 
        Given a list of box dicts and a PalletType, 
        spins up one Bin per box to allow maximal packing, 
        runs the 3D bin‑packing solver, and returns only the filled bins. 
        """


  3. Pooled Bin Manager & Redistribution (pooled_bin_manager.py)

    # pooled_bin_manager.py 
    class PooledBinManager: 
        def __init__(self, bins, tol, callback=lambda msg: None): 
            """ 
            . 
     
        def redistribute(self): 
            """Repeatedly merge and repack until no more improvements.""" 
            # single-pass merges


  4. GUI Integration & 3D Visualization (mainwindow.py)

    def draw_loading_cube(ax, l, w, h): 
        verts = [ 
            [(0,0,0),(l,0,0),(l,w,0),(0,w,0)], 
            [(0,0,h),(l,0,h),(l,w,h),(0,w,h)], 
            # other faces... 
        ] 
        cube = Poly3DCollection(verts, alpha=0.2) 
        ax.add_collection3d(cube) 
        ax.set_box_aspect((l,w,h)) 
     
     
    def draw_boxes(ax, boxes): 
        for idx, (x,y,z,l,w,h,_) in enumerate(boxes): 
            faces = [ 
                [(x,y,z),(x+l,y,z),(x+l,y+w,z),(x,y+w,z)], 
                # other faces... 
            ] 
            poly = Poly3DCollection(faces, alpha=0.8) 
            ax.add_collection3d(poly)
    
    


  5. Loading Data & Grouping (mainwindow.py)

    # mainwindow.py 
     
    # Triggered when user selects an Excel file 
    def load_excel(self): 
        self.load_excel_action.setEnabled(False) 
        path, _ = QFileDialog.getOpenFileName(self, 'Open Boxes xlsx', '', 'Excel Files (*.xlsx)') 
        if not path: 
            self.load_excel_action.setEnabled(True) 
            return 
        try: 
            self.loaded_boxes = json.loads(excel_to_json(path)) 
        except Exception as e: 
            QMessageBox.critical(self, 'Error', f'Failed to load data: {e}') 
            self.load_excel_action.setEnabled(True) 
            return 
        self.file_label.setText(f'Imported file: {os.path.basename(path)}') 
        self.prepare_loaded() 
     
    # Triggered when user selects a JSON file 
    def load_json(self): 
        self.load_action.setEnabled(False) 
        path, _ = QFileDialog.getOpenFileName(self, 'Open Boxes JSON', '', 'JSON Files (*.json)') 
        if not path: 
            self.load_action.setEnabled(True) 
            return 
        try: 
            with open(path) as f: 
                self.loaded_boxes = json.load(f) 
        except Exception as e: 
            QMessageBox.critical(self, 'Error', f'Failed to load JSON: {e}') 
            self.load_action.setEnabled(True) 
            return 
        self.file_label.setText(f'Imported file: {os.path.basename(path)}') 
        self.prepare_loaded()


  6. Redistributing Unfitted Items (pooled_bin_manager.py)

    # pooled_bin_manager.py 
     
    class PooledBinManager: 
        ... 
        def redistribute(self) -> Tuple[int, List[Bin], List[Any]]: 
            """ 
            Repeatedly redistribute and merge items until no further improvements. 
            Returns (total_moved, final_bins, unplaceable_items). 
            """ 
            # 1) perform single-pass merges until stable 
            while True: 
                moved = self._single_pass() 
                self.total_moved += moved 
                if moved == 0: 
                    break 
     
           . 
            return self.total_moved, self.bins, unplaceable
    
    


  7. Exporting Packing Plans for the Warehouse Floor

    To make the optimized pallet configurations actionable for logistics teams, we implemented a PDF export feature directly within the desktop application. This allows warehouse workers to receive clear, step-by-step visual instructions on how to pack each pallet ensuring accurate, efficient, and error-free execution on the ground.


    Why It Matters

    Shipping optimization is only as good as its execution. Our export functionality bridges the gap between digital planning and physical operations by providing printable packing plans that detail exactly how each pallet should be assembled layer by layer, box by box.


    How It Works

    For every pallet, the system automatically generates a PDF file (e.g., pallet_1.pdf) that includes:

    • One page per Z-layer of the pallet

    • A bounding box showing the maximum pallet footprint (X and Y)

    • Precise box positions and dimensions

    • Box identifiers clearly labeled at the center of each item

    • A structured title for each page, such as:
      Pallet 2 – Layer 3 (Z = 120.0 mm)

    This format ensures that logistics workers can immediately understand the layout and reproduce it with accuracy.

    # Called when user selects an export format 
    def export_results(self, format: str): 
        filters = {'pdf': 'PDF Files (*.pdf)', 'png': 'PNG Files (*.png)', 'csv': 'CSV Files (*.csv)'} 
        path, _ = QFileDialog.getSaveFileName(self, 'Export Results', '', filters[format]) 
       
    
    

    Visual Preview :

    Here is a sample output showing one layer of a pallet:


The Challenge: Optimizing Pallet Packaging for Logistics

We were approached by a client in the warehousing industry facing a persistent and costly issue:

  • Boxes were packed manually, leading to poor space usage and frequent errors

  • Pallet space was underused often by 15 – 30%, driving up shipping volume

  • Weight distribution was uneven, raising safety and compliance concerns

  • Manual adjustments consumed hours daily, slowing operations and causing stress

These inefficiencies weren’t just operational .They were eating into margins, increasing shipping costs, and putting unnecessary pressure on the team.

Our Solution: A Desktop Tool for Heuristic-Based Pallet Packing and Visualization

To solve this, we developed a cross-platform Qt application that automates every step from intelligently grouping boxes to generating optimized, layer-by-layer pallet diagrams and exportable packing reports.

  1. User loads a file (Excel .xlsx or JSON) containing all box entries.

  2. Automated grouping by size assigns each box to the best candidate pallet templates.

  3. Parallel packing threads run the Py3DBP engine across all pallet types, then a custom pooling manager redistributes items for maximal fill and balanced loads.

  4. Real-time 3D animation shows a spinning “loading” pallet during computation, then an interactive 3D view of the final arrangement switchable between front/right/back/left/top perspectives.

  5. Layer-by-layer 2D diagrams are generated and saved into a multi-page PDF, complete with box IDs labeled at their true coordinates.

  6. One-click exports to PDF, PNG or CSV create all necessary shipping documentation, with direct links to each pallet’s detailed view.

With this tool, our client eliminated layout errors and manual guesswork, gained instant visibility into 3D and 2D packing plans, and significantly reduced the number of pallets shipped resulting in substantial cost savings on every delivery.
Below is a screenshot of the desktop application in action, showcasing how the optimized packing layout is visualized for both operators and planners.

Workflow Overview

This diagram illustrates the three core stages of the Pallet Optimization Tool:


Diagram Type: UML Activity Diagram

Key Code Snippets

  1. Grouping & JSON Preparation (utils.py)

    def group_by_size(items): 
        """ 
        Groups items by their (length, width, height). 
        Returns a dict mapping size tuples to lists of items. 
        """ 
         
     
     
    def assign_pallets_to_groups(groups): 
        """ 
        Returns a dict mapping each size-tuple to the best-fitting PalletType code. 
        """ 
      


  2. Multi‑Pallet Packing Solver (solver.py)

    # solver.py 
     
    def solve_multi_pallet_py3dbp(boxes, pallet: PalletType, tol=0): 
        """ 
        Given a list of box dicts and a PalletType, 
        spins up one Bin per box to allow maximal packing, 
        runs the 3D bin‑packing solver, and returns only the filled bins. 
        """


  3. Pooled Bin Manager & Redistribution (pooled_bin_manager.py)

    # pooled_bin_manager.py 
    class PooledBinManager: 
        def __init__(self, bins, tol, callback=lambda msg: None): 
            """ 
            . 
     
        def redistribute(self): 
            """Repeatedly merge and repack until no more improvements.""" 
            # single-pass merges


  4. GUI Integration & 3D Visualization (mainwindow.py)

    def draw_loading_cube(ax, l, w, h): 
        verts = [ 
            [(0,0,0),(l,0,0),(l,w,0),(0,w,0)], 
            [(0,0,h),(l,0,h),(l,w,h),(0,w,h)], 
            # other faces... 
        ] 
        cube = Poly3DCollection(verts, alpha=0.2) 
        ax.add_collection3d(cube) 
        ax.set_box_aspect((l,w,h)) 
     
     
    def draw_boxes(ax, boxes): 
        for idx, (x,y,z,l,w,h,_) in enumerate(boxes): 
            faces = [ 
                [(x,y,z),(x+l,y,z),(x+l,y+w,z),(x,y+w,z)], 
                # other faces... 
            ] 
            poly = Poly3DCollection(faces, alpha=0.8) 
            ax.add_collection3d(poly)
    
    


  5. Loading Data & Grouping (mainwindow.py)

    # mainwindow.py 
     
    # Triggered when user selects an Excel file 
    def load_excel(self): 
        self.load_excel_action.setEnabled(False) 
        path, _ = QFileDialog.getOpenFileName(self, 'Open Boxes xlsx', '', 'Excel Files (*.xlsx)') 
        if not path: 
            self.load_excel_action.setEnabled(True) 
            return 
        try: 
            self.loaded_boxes = json.loads(excel_to_json(path)) 
        except Exception as e: 
            QMessageBox.critical(self, 'Error', f'Failed to load data: {e}') 
            self.load_excel_action.setEnabled(True) 
            return 
        self.file_label.setText(f'Imported file: {os.path.basename(path)}') 
        self.prepare_loaded() 
     
    # Triggered when user selects a JSON file 
    def load_json(self): 
        self.load_action.setEnabled(False) 
        path, _ = QFileDialog.getOpenFileName(self, 'Open Boxes JSON', '', 'JSON Files (*.json)') 
        if not path: 
            self.load_action.setEnabled(True) 
            return 
        try: 
            with open(path) as f: 
                self.loaded_boxes = json.load(f) 
        except Exception as e: 
            QMessageBox.critical(self, 'Error', f'Failed to load JSON: {e}') 
            self.load_action.setEnabled(True) 
            return 
        self.file_label.setText(f'Imported file: {os.path.basename(path)}') 
        self.prepare_loaded()


  6. Redistributing Unfitted Items (pooled_bin_manager.py)

    # pooled_bin_manager.py 
     
    class PooledBinManager: 
        ... 
        def redistribute(self) -> Tuple[int, List[Bin], List[Any]]: 
            """ 
            Repeatedly redistribute and merge items until no further improvements. 
            Returns (total_moved, final_bins, unplaceable_items). 
            """ 
            # 1) perform single-pass merges until stable 
            while True: 
                moved = self._single_pass() 
                self.total_moved += moved 
                if moved == 0: 
                    break 
     
           . 
            return self.total_moved, self.bins, unplaceable
    
    


  7. Exporting Packing Plans for the Warehouse Floor

    To make the optimized pallet configurations actionable for logistics teams, we implemented a PDF export feature directly within the desktop application. This allows warehouse workers to receive clear, step-by-step visual instructions on how to pack each pallet ensuring accurate, efficient, and error-free execution on the ground.


    Why It Matters

    Shipping optimization is only as good as its execution. Our export functionality bridges the gap between digital planning and physical operations by providing printable packing plans that detail exactly how each pallet should be assembled layer by layer, box by box.


    How It Works

    For every pallet, the system automatically generates a PDF file (e.g., pallet_1.pdf) that includes:

    • One page per Z-layer of the pallet

    • A bounding box showing the maximum pallet footprint (X and Y)

    • Precise box positions and dimensions

    • Box identifiers clearly labeled at the center of each item

    • A structured title for each page, such as:
      Pallet 2 – Layer 3 (Z = 120.0 mm)

    This format ensures that logistics workers can immediately understand the layout and reproduce it with accuracy.

    # Called when user selects an export format 
    def export_results(self, format: str): 
        filters = {'pdf': 'PDF Files (*.pdf)', 'png': 'PNG Files (*.png)', 'csv': 'CSV Files (*.csv)'} 
        path, _ = QFileDialog.getSaveFileName(self, 'Export Results', '', filters[format]) 
       
    
    

    Visual Preview :

    Here is a sample output showing one layer of a pallet:



Copyrights © 2025 by BIT SOLUTIONS.

Copyrights © 2025 by BIT SOLUTIONS.

Copyrights © 2025 by BIT SOLUTIONS.