由其角点定义的边界框对象的嵌套属性

Nested Attributes for a Bounding Box Object defined by its Corner Points

Blender 脚本通常必须从一组 3D 点计算包围边界框,例如,为了将默认的 Blender 立方体边界框作为输入,

coords = np.array(
     [[-1.  1. -1.],
      [-1.  1.  1.],
      [ 1. -1. -1.],
      [ 1. -1.  1.],
      [ 1.  1. -1.],
      [ 1.  1.  1.]]
 )

bfl = coords.min(axis=0)
tbr = coords.max(axis=0)

G  = np.array((bfl, tbr)).T
bbox_coords = [i for i in itertools.product(*G)]

示例情况下的边界框坐标将是相同顺序的立方体坐标

正在寻找一些 python 使用上面和 ("left", "right"), ("front", "back"),("top", "bottom") 的“迭代魔法”,来制作一个助手 class

>>> bbox = BBox(bfl, tbr)
>>> bbox.bottom.front.left
(-1, -1, -1)

>>> bbox.top.front
(0, -1, 1)

>> bbox.bottom
(0, 0, -1)

一个角顶点,一条边的中心,一个矩形的中心。 (1、2 或 4 个角的平均总和)在搅拌机中,顶部是 +Z,前面是 -Y。

最初是在寻找类似用静态计算值填充嵌套字典的东西

d = {
    "front" : {
        "co" : (0, -1, 0),
        "top" : {
            "co" : (0, -1, 1),
            "left" : {"co" : (-1, -1, 1)},
            }
        }   
    }

Object-like attribute access for nested dictionary

编辑

为了避免发布 XY Problemie 以我处理这个问题的方式发布问题,在下面添加了一个答案,说明我当时的处理方式.抱歉,我忘了提及可以改为选择北、南、东和西作为 x 和 y 轴方向,并希望能够改变。

感觉循环 8 个角顶点是重新制作以顶点索引作为叶节点的“swizzle”字典的方法。 “前”面或右上角下角的顶点索引不变。

它使用它作为 class 的基础,用坐标或 bfl 实例化,tbr 是无论我做什么我总是觉得有比我做的“更好”的方法现在正在做。

这里有两个相似的版本。两者的想法是你总是 return 一个 BBox 对象,只改变一个变量 x,它指示您通过 leftright、...指定的维度 最后你有一个使用 x 来计算中心的函数 剩余角球。

第一种方法使用函数,因此您必须调用它们 bbox.bottom().front().left().c()。这里的主要区别是不是所有的组合

top
top left
top right
top left front
...

在创建对象时计算,但仅在您调用它们时计算。


import numpy as np
import itertools

class BBox:
    """
    ("left", "right"), -x, +x
    ("front", "back"), -y, +y
    ("bottom", "top"), -z, +z
    """
    def __init__(self, bfl, tbr):
        self.bfl = bfl
        self.tbr = tbr

        self.g = np.array((bfl, tbr)).T

        self.x = [[0, 1], [0, 1], [0, 1]]

    def c(self):  # get center coordinates
        return np.mean([i for i in itertools.product(*[self.g[i][self.x[i]] for i in range(3)])], axis=0)

    def part(self, i, xi):
        assert len(self.x[i]) == 2
        b2 = BBox(bfl=self.bfl, tbr=self.tbr)
        b2.x = self.x.copy()
        b2.x[i] = [xi]
        return b2

    def left(self):
        return self.part(i=0, xi=0)

    def right(self):
        return self.part(i=0, xi=1)

    def front(self):
        return self.part(i=1, xi=0)

    def back(self):
        return self.part(i=1, xi=1)

    def bottom(self):
        return self.part(i=2, xi=0)

    def top(self):
        return self.part(i=2, xi=1)


bbox = BBox(bfl=[-1, -1, -1], tbr=[1, 1, 1])
>>> bbox.bottom().front().left().c()
(-1, -1, -1)

>>> bbox.top().front().c()
(0, -1, 1)

>>> bbox.bottom().c()
(0, 0, -1)

第二种方法使用本身就是 BBox 对象的属性。 当您取消注释 init 函数中的 print 语句时,您可以了解构造期间发生的所有递归调用。 因此,虽然查看这里发生的事情可能更复杂,但在访问属性时您会更方便。

class BBox:
    def __init__(self, bfl, tbr, x=None):
        self.bfl = bfl
        self.tbr = tbr
        self.g = np.array((bfl, tbr)).T

        self.x = [[0, 1], [0, 1], [0, 1]] if x is None else x
        
        # print(self.x)  # Debugging 
        self.left = self.part(i=0, xi=0)
        self.right = self.part(i=0, xi=1)
        self.front = self.part(i=1, xi=0)
        self.back = self.part(i=1, xi=1)
        self.bottom = self.part(i=2, xi=0)
        self.top = self.part(i=2, xi=1)

    def c(self):  # get center coordinates
        return np.mean([i for i in itertools.product(*[self.g[i][self.x[i]] 
                        for i in range(3)])], axis=0)

    def part(self, i, xi):
        if len(self.x[i]) < 2:
            return None
        x2 = self.x.copy()
        x2[i] = [xi]
        return BBox(bfl=self.bfl, tbr=self.tbr, x=x2)

bbox = BBox(bfl=[-1, -1, -1], tbr=[1, 1, 1])
>>> bbox.bottom.front.left.c()
(-1, -1, -1)

您还可以在构造函数的末尾添加类似这样的内容,以删除无效的属性。 (以防止像 bbox.right.left.c() 这样的东西)。他们之前是 None,但 AttributeError 可能更合适。

   def __init__(self, bfl, tbr, x=None):
       ...
       for name in ['left', 'right', 'front', 'back', 'bottom', 'top']:
           if getattr(self, name) is None:
               delattr(self, name)

您还可以添加 __repr__() 方法:

    def __repr__(self):
        return repr(self.get_vertices())

    def get_vertices(self):
        return [i for i in itertools.product(*[self.g[i][self.x[i]]
                                               for i in range(3)])]

    def c(self):  # get center coordinates
        return np.mean(self.get_vertices(), axis=0)


bbox.left.front
# [(-1, -1, -1), (-1, -1, 1)]
bbox.left.front.c()
# array([-1., -1.,  0.])

编辑

一段时间后回过头来,我认为最好只添加相关属性而不是全部添加,而不是事后删除一半。所以我能想到的最紧凑/方便的class是:

class BBox:
    def __init__(self, bfl, tbr, x=None):
        self.bfl, self.tbr = bfl, tbr
        self.g = np.array((bfl, tbr)).T
        self.x = [[0, 1], [0, 1], [0, 1]] if x is None else x

        for j, name in enumerate(['left', 'right', 'front', 'back', 'bottom', 'top']):
            temp = self.part(i=j//2, xi=j%2)
            if temp is not None:
                setattr(self, name, temp)

    def c(self):  # get center coordinates
        return np.mean([x for x in itertools.product(*[self.g[i][self.x[i]]
                                                       for i in range(3)])], axis=0)

    def part(self, i, xi):
        if len(self.x[i]) == 2:
            x2, x2[i] = self.x.copy(), [xi]
            return BBox(bfl=self.bfl, tbr=self.tbr, x=x2)

这是使用迭代方法创建字典的另一种解决方案:

import numpy 
import itertools

directions = ['left', 'right', 'front', 'back', 'bottom', 'top']
dims = np.array([  0,       0,       1,      1,        2,     2])  # xyz

def get_vertices(bfl, tbr, x):
    g = np.array((bfl, tbr)).T
    return [v for v in itertools.product(*[g[ii][x[ii]] for ii in range(3)])]


bfl = [-1, -1, -1]
tbr = [1, 1, 1]

d = {}
for i in range(6):
    x = [[0, 1], [0, 1], [0, 1]]
    x[i//2] = [i % 2]  # x[dim[i] = min or max  
    d_i = dict(c=np.mean(get_vertices(bfl=bfl, tbr=tbr, x=x), axis=0))

    for j in np.nonzero(dims != dims[i])[0]:
        x[j//2] = [j % 2]
        d_ij = dict(c=np.mean(get_vertices(bfl=bfl, tbr=tbr, x=x), axis=0))

        for k in np.nonzero(np.logical_and(dims != dims[i], dims != dims[j]))[0]:
            x[k//2] = [k % 2]

            d_ij[directions[k]] = dict(c=np.mean(get_vertices(bfl=bfl, tbr=tbr, x=x), axis=0))
        d_i[directions[j]] = d_ij
    d[directions[i]] = d_i


d
# {'left': {'c': array([-1.,  0.,  0.]),
#    'front': {'c': array([-1., -1.,  0.]),
#      'bottom': {'c': array([-1., -1., -1.])},
#      'top': {'c': array([-1., -1.,  1.])}},
#    'back': {'c': array([-1.,  1.,  1.]),
#      'bottom': {'c': array([-1.,  1., -1.])},
#      'top': {'c': array([-1.,  1.,  1.])}}, 
#   ....

您可以将此与您的链接问题结合起来,通过 d.key1.key2 访问字典的键。

我到了哪里。

以某种方式将此添加为答案以更好地解释我的问题

循环遍历立方体的 8 个顶点,将 3 个名称匹配到每个有效角。

“swizzle”是构成角的三个轴方向的排列。

直接输入自嵌套字典d[i][j][k] = value 是一种轻松创建它们的方法。 (下图pprint(d)

很高兴从那里开始它变得丑陋,一些鸭子输入从简单的 8 vert truth table.

获取和获取元素索引

由于没有特别的原因,returns 生成的方法 class 是一个包装器,但我并没有这样使用它。

import numpy as np
import pprint
import operator
from itertools import product, permutations
from functools import reduce
from collections import defaultdict


class NestedDefaultDict(defaultdict):
    def __init__(self, *args, **kwargs):
        super(NestedDefaultDict, self).__init__(NestedDefaultDict, *args, **kwargs)

    def __repr__(self):
        return repr(dict(self))


def set_by_path(root, items, value):
    reduce(operator.getitem, items[:-1], root)[items[-1]] = value


def create_bbox_swizzle(cls, dirx=("left", "right"), diry=("front", "back"), dirz=("bottom", "top")):
    d = NestedDefaultDict()
    data = {}
    for i, cnr in enumerate(product(*(dirx, diry, dirz))):
        vert = {"index": i}
        data[frozenset(cnr)] = i
        for perm in permutations(cnr, 3):
            set_by_path(d, perm, vert)
    pprint.pprint(d)

    def wire_up(names, d):
        class Mbox:
            @property
            def co(self):
                return self.coords[self.vertices].mean(axis=0)
            def __init__(self, coords):
                self.coords = np.array(coords)
                self.vertices = [v for k, v in data.items() if k.issuperset(names)]
                pass

            def __repr__(self):
                if len(names) == 1:
                    return f"<BBFace {self.vertices}/>"
                elif len(names) == 2:
                    return f"<BBEdge {self.vertices}/>"
                elif len(names) == 3:
                    return f"<BBVert {self.vertices}/>"
                return "<BBox/>"
            pass

        def f(k, v):
            def g(self):
                return wire_up(names + [k], v)(self.coords)
            return property(g)

        for k, v in d.items():
            if isinstance(v, dict):
                setattr(Mbox, k, (f(k, v)))
            else:
                setattr(Mbox, k, v)
        return Mbox
    return wire_up([], d)


@create_bbox_swizzle
class BBox:
    def __init__(self, *coords, **kwargs):
        pass

试驾:

>>> bbox = BBox(coords)  # used coords instead of corners
>>> bbox.co
array([ 5.96046448e-08, -1.19209290e-07,  0.00000000e+00])

>>> bbox.left.bottom
<BBEdge [0, 2]/>

>>> bbox.left.bottom.vertices
[0, 2]

>>> bbox.left.bottom.co
array([-1.00000036e+00, -1.19209290e-07,  0.00000000e+00])