使用 Python 開源庫 py3dbp 解決三維裝箱問題
本文介紹如何使用 py3dbp 解決三維裝箱問題。文章包含了從基本概念、實戰案例到最終生成動態GIF可視化的流程及代碼實現。
在物流、倉儲和制造業中,如何將不同尺寸的物品高效地裝入一個有限的容器(如卡車、集裝箱或箱子)是一個經典且極具挑戰性的問題。這就是著名的三維裝箱問題(3D Bin Packing Problem, 3D-BPP)。解決好這個問題能顯著節約運輸成本、提高空間利用率。
Python社區為我們提供了一個強大而簡潔的工具——py3dbp。本文將帶您深入了解這個庫,并通過一個實際案例,展示如何從零開始解決一個裝箱問題,并最終創建一個直觀的動態裝箱過程GIF。

一、py3dbp 核心概念
在使用之前,我們先了解 py3dbp 的三個核心組件:
- Packer (裝箱器):這是執行裝箱算法的核心引擎。您可以把它想象成一個負責指揮的工人。
- Bin (箱子/容器):代表您要裝入物品的容器。它有明確的屬性,如名稱、長、寬、高和最大承重。在我們的案例中,這就是一輛貨車。
- Item (物品):代表需要被裝入箱子的物品。它同樣有名稱、長、寬、高和重量等屬性。
整個工作流程非常直觀:創建箱子和一系列物品 -> 將它們都交給裝箱器 -> 裝箱器執行算法 -> 檢視裝箱結果。
二、實戰案例:裝載一輛貨車
假設我們有一輛小型貨車和一批不同規格的貨物,我們的目標是盡可能多地將這些貨物裝入車廂。
- 貨車車廂尺寸 (Bin): 587cm (長) x 235cm (寬) x 270cm (高)。
- 貨物列表 (Items): 我們有多種不同尺寸和數量的箱子需要裝載。
第1步:安裝必要的庫
您需要安裝 py3dbp 用于核心計算,matplotlib 用于繪圖,以及 imageio 用于將圖片序列合成為GIF。
pip install py3dbp
pip install matplotlib
pip install imageio```第2步:編寫代碼實現裝箱
我們將遵循以下步驟編寫代碼:
- 導入庫。
- 定義 Bin (貨車) 和 Item (貨物)。
- 創建 Packer 并將箱子和物品添加進去。
- 執行裝箱算法并打印結果。
第3步:生成動態GIF可視化
靜態的結果報告雖然清晰,但遠不如一個動態圖來得直觀。我們將編寫一個函數,它能一步步地展示每個箱子是如何被放入車廂的,并最終將這個過程保存為 GIF 文件。
這需要我們:
- 編寫一個函數,能繪制出任意數量物品在箱子中的3D視圖。
- 循環調用這個繪圖函數,從1個物品開始,每次增加1個,并將每一幀保存為圖片。
- 使用 imageio 將所有圖片幀合成為一個GIF。
下面是包含完整代碼(計算+可視化)的腳本:
import os
import imageio
import numpy as np
from py3dbp import Packer, Bin, Item
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
# --- Matplotlib 全局設置 ---
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用來正常顯示中文標簽
plt.rcParams['axes.unicode_minus'] = False# 用來正常顯示負號
def plot_packing_result(bin_obj, items_to_plot, title, save_path=None):
"""
繪制指定物品在箱子中的3D視圖 (已修正數據類型問題)
"""
fig = plt.figure(figsize=(12, 10))
ax = fig.add_subplot(111, projection='3d')
# 為不同物品分配顏色
num_items = len(items_to_plot)
cmap = plt.get_cmap('viridis')
colors = [cmap(i) for i in np.linspace(0, 0.9, num_items)] if num_items > 0else []
color_map = {item.name: colors[i] for i, item in enumerate(items_to_plot)}
# 繪制每個已裝箱的物品
for item in items_to_plot:
item_d = float(item.depth)
item_w = float(item.width)
item_h = float(item.height)
pos = item.position
x, y, z = float(pos[0]), float(pos[1]), float(pos[2])
vertices = [
(x, y, z), (x + item_d, y, z), (x + item_d, y + item_w, z), (x, y + item_w, z),
(x, y, z + item_h), (x + item_d, y, z + item_h), (x + item_d, y + item_w, z + item_h), (x, y + item_w, z + item_h)
]
faces = [
[vertices[0], vertices[1], vertices[2], vertices[3]], [vertices[4], vertices[5], vertices[6], vertices[7]],
[vertices[0], vertices[1], vertices[5], vertices[4]], [vertices[2], vertices[3], vertices[7], vertices[6]],
[vertices[1], vertices[2], vertices[6], vertices[5]], [vertices[0], vertices[3], vertices[7], vertices[4]]
]
poly3d = Poly3DCollection(faces, facecolors=color_map.get(item.name), linewidths=1, edgecolors='k', alpha=0.85)
ax.add_collection3d(poly3d)
ax.set_xlim(0, float(bin_obj.depth))
ax.set_ylim(0, float(bin_obj.width))
ax.set_zlim(0, float(bin_obj.height))
ax.set_xlabel('長度 (X軸 / depth)')
ax.set_ylabel('寬度 (Y軸 / width)')
ax.set_zlabel('高度 (Z軸 / height)')
ax.view_init(elev=20, azim=-45)
plt.title(title, fontsize=16)
if save_path:
plt.savefig(save_path)
plt.close(fig)
else:
plt.show()
def create_animated_gif(bin_obj, output_filename='packing_animation.gif'):
"""
創建并保存裝箱過程的GIF動畫
"""
packed_items = bin_obj.items
ifnot packed_items:
print("分析:沒有成功裝入任何物品,無法創建GIF。請檢查物品尺寸是否相對于容器過大。")
return
frame_folder = "gif_frames"
ifnot os.path.exists(frame_folder):
os.makedirs(frame_folder)
filenames = []
print(f"步驟1:為GIF生成 {len(packed_items) + 1} 幀圖片...")
for i in range(len(packed_items) + 1):
frame_path = os.path.join(frame_folder, f"frame_{i:03d}.png")
if i == 0:
plot_packing_result(bin_obj, [], f"第 {i}/{len(packed_items)} 步: 空貨車", save_path=frame_path)
else:
item_being_packed = packed_items[i-1]
plot_packing_result(bin_obj, packed_items[:i], f"第 {i}/{len(packed_items)} 步: 裝入 {item_being_packed.name}", save_path=frame_path)
filenames.append(frame_path)
print("步驟2:將所有圖片幀合成為GIF文件...")
with imageio.get_writer(output_filename, mode='I', duration=3.5) as writer:
for filename in filenames:
image = imageio.imread(filename)
writer.append_data(image)
print(f"成功!GIF動畫已保存至: {output_filename}")
for filename in filenames:
os.remove(filename)
os.rmdir(frame_folder)
# ================== 主程序入口 ==================
if __name__ == '__main__':
packer = Packer()
# 按照 (名稱, 寬, 高, 深, ...) 的順序創建 Bin
truck = Bin('貨車', 235, 270, 587, 1000.0)
packer.add_bin(truck)
# 按照 (名稱, 寬, 高, 深, ...) 的順序創建 Item
packer.add_item(Item('大號箱子-1', 100, 100, 100, 15.0))
packer.add_item(Item('大號箱子-2', 100, 100, 100, 15.0))
packer.add_item(Item('中號箱子-1', 70, 60, 80, 10.0))
packer.add_item(Item('中號箱子-2', 70, 60, 80, 10.0))
packer.add_item(Item('中號箱子-3', 70, 60, 80, 10.0))
packer.add_item(Item('小號箱子-1', 40, 30, 50, 5.0))
packer.add_item(Item('小號箱子-2', 40, 30, 50, 5.0))
packer.add_item(Item('小號箱子-3', 40, 30, 50, 5.0))
packer.add_item(Item('小號箱子-4', 40, 30, 50, 5.0))
packer.add_item(Item('扁平箱子-1', 150, 10, 120, 8.0))
packer.add_item(Item('扁平箱子-2', 150, 10, 120, 8.0))
packer.add_item(Item('瘦高箱子-1', 40, 200, 40, 12.0))
packer.add_item(Item('瘦高箱子-2', 40, 200, 40, 12.0))
packer.add_item(Item('超大號箱子', 300, 300, 300, 100.0))
print("開始執行裝箱算法...")
packer.pack(bigger_first=True, distribute_items=False)
packed_bin = packer.bins[0]
print(f"\n計算完畢!")
print(f"已裝入物品數量: {len(packed_bin.items)}")
print(f"未裝入物品數量: {len(packed_bin.unfitted_items)}")
create_animated_gif(packed_bin, 'packing_animation.gif')三、運行與結果
將以上代碼保存為 .py 文件并運行。程序會首先在控制臺輸出裝箱結果,然后開始生成GIF的每一幀。完成后,您會在代碼文件所在的目錄下找到一個名為 packing_animation.gif 的文件。
這個GIF文件會像下面這樣,動態地展示每一個箱子被依次放入貨車的過程。



























