使用 plotly 的具有正确纵横比的交互式 3D 图

interactive 3D plot with right aspect ratio using plotly

我正在使用 matplotlib 绘制 3D 图像(即 3D 装箱问题,如装载容器)。绘图时,length/width/height 会自动缩放,这与其实际值不成比例,即长度是高度的 6 倍,但图片显示三个轴的比例几乎相同(见下面第一张图片) .我知道 matplot3D 在以正确的纵横比绘制 3D 图方面有其局限性。

我需要的是像下图一样以更真实的方式绘制绘图。我们可以很容易地看到容器的 space 和其中加载的项目。许多人推荐使用 plotly,它支持很好的交互式 3D 绘图。我从来没有用过这样的工具来绘制 3D 图。有人可以帮助提供这样做的代码示例吗?谢谢

下面是我的代码:

from py3dbp import Packer, Bin, Item
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
import numpy as np
import matplotlib.pyplot as plt
import random





def cuboid_data2(o, size=(1, 1, 1)):
    X = [[[0, 1, 0], [0, 0, 0], [1, 0, 0], [1, 1, 0]],
         [[0, 0, 0], [0, 0, 1], [1, 0, 1], [1, 0, 0]],
         [[1, 0, 1], [1, 0, 0], [1, 1, 0], [1, 1, 1]],
         [[0, 0, 1], [0, 0, 0], [0, 1, 0], [0, 1, 1]],
         [[0, 1, 0], [0, 1, 1], [1, 1, 1], [1, 1, 0]],
         [[0, 1, 1], [0, 0, 1], [1, 0, 1], [1, 1, 1]]]
    X = np.array(X).astype(float)
    for i in range(3):
        X[:, :, i] *= size[i]
    X += np.array(o)
    return X


def plotCubeAt2(positions, sizes=None, colors=None, **kwargs):
    if not isinstance(colors, (list, np.ndarray)): colors = ["C0"] * len(positions)
    if not isinstance(sizes, (list, np.ndarray)): sizes = [(1, 1, 1)] * len(positions)
    g = []
    for p, s, c in zip(positions, sizes, colors):
        g.append(cuboid_data2(p, size=s))
    return Poly3DCollection(np.concatenate(g),
                            facecolors=np.repeat(colors, 6), **kwargs)
  

  containers = [
    [1203, 235, 259],
    [1203, 235, 259],
    # [1202.4, 235, 269],
    # [12.024, 2.350, 2.69],
    # [12.024, 2.350, 2.69],
    # [12.024, 2.350, 2.69],
]



packer = Packer()

containerX = 0
containerY = 0
containerZ = 0



for i, t in enumerate(range(len(containers))):
    containerX = containers[t][0]
    containerY = containers[t][1]
    containerZ = containers[t][2]
    i += 1
    packer.add_bin(Bin('40HC-' + str(i), containerX, containerY, containerZ, 18000.0))



for i in range(50):
    packer.add_item(Item('BoxA_' + str(i), 44, 39, 70, 8.20))

for i in range(35):
    packer.add_item(Item('BoxB_' + str(i), 65, 38, 40, 14))

for i in range(31):
    packer.add_item(Item('BoxC_' + str(i), 43, 52, 47, 10))

for i in range(38):
    packer.add_item(Item('BoxD_' + str(i), 60, 45, 40, 14))

for i in range(11):
    packer.add_item(Item('BoxE_' + str(i), 42, 46, 54, 9.70))

for i in range(525):
    packer.add_item(Item('BoxF_' + str(i), 62, 45, 35, 14.5))




# packer.pack()
# packer.pack(bigger_first=False)
packer.pack(bigger_first=False, distribute_items=True, number_of_decimals=3)





for b in packer.bins:
    positions = []
    sizes = []
    colors = []
    print(":::::::::::", b.string())

    print("FITTED ITEMS:")
    for item in b.items:
        print("====> ", item.string())
        x = float(item.position[0])
        y = float(item.position[1])
        z = float(item.position[2])
        positions.append((x, y, z))
        sizes.append(
            (float(item.get_dimension()[0]), float(item.get_dimension()[1]), float(item.get_dimension()[2])))

        colorList = ["crimson", "limegreen", "g", "r", "c", "m", "y", "k"]
        if item.width == 44:
            colors.append(colorList[0])
        if item.width == 65:
            colors.append(colorList[1])
        if item.width == 43:
            colors.append(colorList[2])
        if item.width == 60:
            colors.append(colorList[3])
        if item.width == 42:
            colors.append(colorList[4])
        if item.width == 62:
            colors.append(colorList[5])


    print("UNFITTED ITEMS:")
    for item in b.unfitted_items:
        print("====> ", item.string())

    print("***************************************************")
    print("***************************************************")

    # colorList = ["crimson", "limegreen", "g", "r", "c", "m", "y", "k"]
    #
    # for i in range(len(b.items)):
    #   f = random.randint(0, 7)
    #   colors.append(colorList[f])


    if len(colors) > 0:
        fig = plt.figure()
        fig.canvas.set_window_title(b.string().split("(")[0])
        ax = fig.gca(projection='3d')
        ax.set_aspect('auto')
        pc = plotCubeAt2(positions, sizes, colors=colors, edgecolor="k")
        ax.add_collection3d(pc)

        ax.set_xlim([0, float(b.string().split(",")[0].split("(")[1].split("x")[0])])
        ax.set_ylim([0, float(b.string().split(",")[0].split("(")[1].split("x")[1])])
        ax.set_zlim([0, float(b.string().split(",")[0].split("(")[1].split("x")[2])])



plt.show()

上面代码的 3D bin packing 计算输出看起来像,其中“pos”应该是 3D 位置数据:

====>  BoxC_16(64.000x37.000x52.000, weight: 0.000) pos([Decimal('1024.000'), 0, 0]) rt(0) vol(123136.000)
====>  BoxC_17(64.000x37.000x52.000, weight: 0.000) pos([Decimal('1088.000'), 0, 0]) rt(0) vol(123136.000)
====>  BoxC_18(64.000x37.000x52.000, weight: 0.000) pos([Decimal('1152.000'), 0, 0]) rt(0) vol(123136.000)
====>  BoxC_19(64.000x37.000x52.000, weight: 0.000) pos([Decimal('1216.000'), 0, 0]) rt(0) vol(123136.000)
====>  BoxC_20(64.000x37.000x52.000, weight: 0.000) pos([Decimal('1280.000'), 0, 0]) rt(0) vol(123136.000)
====>  BoxC_21(64.000x37.000x52.000, weight: 0.000) pos([0, Decimal('37.000'), 0]) rt(0) vol(123136.000)
====>  BoxC_22(64.000x37.000x52.000, weight: 0.000) pos([Decimal('64.000'), Decimal('37.000'), 0]) rt(0) vol(123136.000)
====>  BoxC_23(64.000x37.000x52.000, weight: 0.000) pos([Decimal('128.000'), Decimal('37.000'), 0]) rt(0) vol(123136.000)
====>  BoxC_24(64.000x37.000x52.000, weight: 0.000) pos([Decimal('192.000'), Decimal('37.000'), 0]) rt(0) vol(123136.000)
====>  BoxC_25(64.000x37.000x52.000, weight: 0.000) pos([Decimal('256.000'), Decimal('37.000'), 0]) rt(0) vol(123136.000)
====>  BoxC_26(64.000x37.000x52.000, weight: 0.000) pos([Decimal('320.000'), Decimal('37.000'), 0]) rt(0) vol(123136.000)

UPADTE:(绘制容器外框)

def parallelipipedic_frame(xm, xM, ym, yM, zm, zM):
    # defines the coords of each segment followed by None, if the line is
    # discontinuous
    x = [xm, xM, xM, xm, xm, None, xm, xM, xM, xm, xm, None, xm, xm, None, xM, xM,
         None, xM, xM, None, xm, xm]
    y = [ym, ym, yM, yM, ym, None, ym, ym, yM, yM, ym, None, ym, ym, None, ym, ym,
         None, yM, yM, None, yM, yM]
    z = [zm, zm, zm, zm, zm, None, zM, zM, zM, zM, zM, None, zm, zM, None, zm, zM,
         None, zm, zM, None, zm, zM]
    return x, y, z

x, y, z = parallelipipedic_frame(0, 1202.4, 0, 235, 0, 269.7)
# fig = go.Figure(go.Scatter3d(x=x, y=y, z=z, mode="lines", line_width=4))

fig.add_trace(
    go.Scatter3d(
        x=x,
        y=y,
        z=z,
        mode="lines",
        line_color="blue",
        line_width=2,
        hoverinfo="skip",
    )
)

ar = 4
xr = max(d["x"].max()) - min(d["x"].min())
fig.update_layout(
    title={"text": pbin, "y": 0.9, "x": 0.5, "xanchor": "center", "yanchor": "top"},
    margin={"l": 0, "r": 0, "t": 0, "b": 0},
    # autosize=False,
    scene=dict(
        camera=dict(eye=dict(x=2, y=2, z=2)),
        aspectratio={
            **{"x": ar},
            **{
                c: ((max(d[c].max()) - min(d[c].min())) / xr) * ar
                for c in list("yz")
            },
        },
        aspectmode="manual",
    ),
)

from py3dbp import Packer, Bin, Item
import plotly.graph_objects as go
from plotly.subplots import make_subplots

containers = [
    [1203, 235, 259],
    [1203, 235, 259],
]

packer = Packer()

for i, t in enumerate(containers):
    packer.add_bin(Bin("40HC-" + str(i + 1), *t, 18000.0))

pbins = {
    "BoxA": {"n": 50, "s": [44, 39, 70, 8.20]},
    "BoxB": {"n": 35, "s": [65, 38, 40, 14]},
    "BoxC": {"n": 31, "s": [43, 52, 47, 10]},
    "BoxD": {"n": 38, "s": [60, 45, 40, 14]},
    "BoxE": {"n": 11, "s": [65, 38, 40, 14]},
    "BoxF": {"n": 525, "s": [62, 45, 35, 14.5]},
}

for name, cfg in pbins.items():
    for i in range(cfg["n"]): 
        packer.add_item(Item(f"{name}_{i}", *cfg["s"]))

# packer.pack()
# packer.pack(bigger_first=False)
print("about to pack")
packer.pack(bigger_first=False, distribute_items=True, number_of_decimals=3)
print("packed")


### PLOTLY ###
# https://plotly.com/python/3d-mesh/#mesh-cube
def vertices(xmin=0, ymin=0, zmin=0, xmax=1, ymax=1, zmax=1):
    return {
        "x": [xmin, xmin, xmax, xmax, xmin, xmin, xmax, xmax],
        "y": [ymin, ymax, ymax, ymin, ymin, ymax, ymax, ymin],
        "z": [zmin, zmin, zmin, zmin, zmax, zmax, zmax, zmax],
        "i": [7, 0, 0, 0, 4, 4, 6, 1, 4, 0, 3, 6],
        "j": [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3],
        "k": [0, 7, 2, 3, 6, 7, 1, 6, 5, 5, 7, 2],
    }

# take a packer item and build parameters to a plotly mesh3d cube
def packer_to_plotly(item):
    colors = ["crimson", "limegreen", "green", "red", "cyan", "magenta", "yellow"]

    ret = vertices(
        *item.position, *[sum(x) for x in zip(item.position, item.get_dimension())]
    )
    ret["name"] = item.name
    ret["color"] = colors[ord(item.name.split("_")[0][-1]) - ord("A")]
    return ret

# create a multi-plot figure for each bin
fig = make_subplots(rows=len(packer.bins), cols=1, specs=[[{"type":"mesh3d"}], [{"type":"mesh3d"}]])

# add a trace for each packer item
for row, pbin in enumerate(packer.bins):
    for item in pbin.items:
        fig.add_trace(go.Mesh3d(packer_to_plotly(item)), row=row+1, col=1)

# some first attempts at sorting out layout, prmarily aspect ratio
fig.update_layout(
    margin={"l": 0, "r": 0, "t": 0, "b": 0},
    autosize=False,
    scene=dict(
        camera=dict(
            # eye=dict(x=0.1, y=0.1, z=1.5)
        ),
        aspectratio=dict(x=1, y=.2, z=0.2),
        aspectmode="manual",
    ),
)

附加要求

  1. 如何制作单独的图(不要将子图合并到同一张图中,因为它们太小了,看不清);

    • 简单地为每个 bin 创建一个图形
  2. 如何解决non-full容器的纵横比问题?如果没有满,我希望它显示空的 space 供用户查看;

    • 更改为使用数据框根据数据计算纵横比
  3. 如何制作不同的边缘颜色以区别于每个单独的立方体

    • 这是比较复杂的部分。使用 Scatter3d()` 以及构建立方体 co-ordinates 的顶点
import pandas as pd

# push data into a data frame to enable more types of analysis
df = pd.DataFrame(
    [
        {
            "bin_name": b.name,
            "bin_index": i,
            **packer_to_plotly(item),
            **{d: v for v, d in zip(item.get_dimension(), list("hwl"))},
            **{d + d: v for v, d in zip(item.position, list("xyz"))},
        }
        for i, b in enumerate(packer.bins)
        for item in b.items
    ]
)

# create a figure for each container (bin)
for pbin, d in df.groupby("bin_name"):
    fig = go.Figure()
    xx = []
    yy = []
    zz = []

    # create a trace for each box (bin)
    for _, r in d.iterrows():
        fig.add_trace(
            go.Mesh3d(r[["x", "y", "z", "i", "j", "k", "name", "color"]].to_dict())
        )
        xx += [r.xx, r.xx + r.h, r.xx + r.h, r.xx, r.xx, None] * 2 + [r.xx] * 5 + [None]
        yy += [r.yy, r.yy, r.yy + r.w, r.yy + r.w, r.yy, None] * 2 + [
            r.yy,
            r.yy + r.w,
            r.yy + r.w,
            r.yy,
            r.yy,
            None,
        ]
        zz += (
            [r.zz] * 5
            + [None]
            + [r.zz + r.l] * 5
            + [None]
            + [r.zz, r.zz, r.zz + r.l, r.zz + r.l, r.zz, None]
        )

    fig.add_trace(
        go.Scatter3d(
            x=xx,
            y=yy,
            z=zz,
            mode="lines",
            line_color="black",
            line_width=2,
            hoverinfo="skip",
        )
    )
    ar = 4
    xr = max(d["x"].max()) - min(d["x"].min())
    fig.update_layout(
        title={"text": pbin, "y": 0.9, "x": 0.5, "xanchor": "center", "yanchor": "top"},
        margin={"l": 0, "r": 0, "t": 0, "b": 0},
        # autosize=False,
        scene=dict(
            camera=dict(eye=dict(x=2, y=2, z=2)),
            aspectratio={
                **{"x": ar},
                **{
                    c: ((max(d[c].max()) - min(d[c].min())) / xr) * ar
                    for c in list("yz")
                },
            },
            aspectmode="manual",
        ),
    )

    fig.show()