hub/venv/lib/python3.7/site-packages/trimesh/path/packing.py

446 lines
13 KiB
Python
Raw Normal View History

import time
import numpy as np
from ..constants import log
from ..constants import tol_path as tol
class RectangleBin:
"""
2D BSP tree node.
http://www.blackpawn.com/texts/lightmaps/
"""
def __init__(self, bounds=None, size=None):
"""
Create a rectangular bin.
Parameters
------------
bounds : (4,) float or None
(minx, miny, maxx, maxy)
size : (2,) float or None
Alternative method to set bounds
(X size, Y size)
"""
self.child = [None, None]
self.occupied = False
# bounds: (minx, miny, maxx, maxy)
if bounds is not None:
self.bounds = np.asanyarray(bounds,
dtype=np.float64)
elif size is not None:
self.bounds = np.append(
[0.0, 0.0], size).astype(np.float64)
else:
raise ValueError('need to pass size or bounds!')
@property
def extents(self):
"""
Bounding box size.
Returns
----------
extents : (2,) float
Edge lengths of bounding box
"""
bounds = self.bounds
return bounds[2:] - bounds[:2]
def insert(self, rectangle):
"""
Insert a rectangle into the bin.
Parameters
-------------
rectangle : (2,) float
Size of rectangle to insert
Returns
----------
inserted : None or (2,) float
Position of insertion in the tree
"""
for child in self.child:
if child is not None:
# try inserting into child cells
attempt = child.insert(rectangle)
if attempt is not None:
return attempt
# can't insert into occupied cells
if self.occupied:
return None
# compare the bin size to the insertion candidate size
bounds = self.bounds
# manually compute extents here to avoid function call
size_test = (bounds[2:] - bounds[:2]) - rectangle
# this means the inserted rectangle is too big for the cell
if any(size_test < -tol.zero):
return None
# since the cell is big enough for the current rectangle, either it
# is going to be inserted here, or the cell is going to be split
# either way the cell is now occupied.
self.occupied = True
# this means the inserted rectangle fits perfectly
# since we already checked to see if it was negative, no abs is needed
if all(size_test < tol.zero):
return self.bounds[:2]
# since the rectangle fits but the empty space is too big,
# we need to create some children to insert into
# first, we decide which way to split
vertical = size_test[0] > size_test[1]
length = rectangle[int(not vertical)]
child_bounds = self.split(length, vertical)
# create the child objects
self.child[0] = RectangleBin(bounds=child_bounds[0])
self.child[1] = RectangleBin(bounds=child_bounds[1])
return self.child[0].insert(rectangle)
def split(self, length, vertical=True):
"""
Returns two bounding boxes representing the current
bounds split into two smaller boxes.
Parameters
-------------
length : float
Length to split
vertical: bool
Split the box vertically rather than horizontally
Returns
-------------
box : (2, 4) float
Two bounding boxes with min-max:
[minx, miny, maxx, maxy]
"""
# also know as [minx, miny, maxx, maxy]
(left, bottom, right, top) = self.bounds
if vertical:
return [[left, bottom, left + length, top],
[left + length, bottom, right, top]]
else:
return [[left, bottom, right, bottom + length],
[left, bottom + length, right, top]]
def rectangles_single(rectangles, sheet_size=None, shuffle=False):
"""
Execute a single insertion order of smaller rectangles onto
a larger rectangle using a binary space partition tree.
Parameters
----------
rectangles : (n, 2) float
An array of (width, height) pairs
representing the rectangles to be packed.
sheet_size : (2,) float
Width, height of rectangular sheet
shuffle : bool
Whether or not to shuffle the insert order of the
smaller rectangles, as the final packing density depends
on insertion order.
Returns
---------
density : float
Area filled over total sheet area
offset : (m,2) float
Offsets to move rectangles to their packed location
inserted : (n,) bool
Which of the original rectangles were packed
consumed_box : (2,) float
Bounding box size of packed result
"""
offset = np.zeros((len(rectangles), 2))
inserted = np.zeros(len(rectangles), dtype=np.bool)
box_order = np.argsort(np.sum(rectangles**2, axis=1))[::-1]
area = 0.0
density = 0.0
# if no sheet size specified, make a large one
if sheet_size is None:
sheet_size = [rectangles[:, 0].sum(),
rectangles[:, 1].max() * 2]
if shuffle:
# maximum index to shuffle
max_idx = int(np.random.random() * len(rectangles)) - 1
# reorder with permutations
box_order[:max_idx] = np.random.permutation(box_order[:max_idx])
# start the tree
sheet = RectangleBin(size=sheet_size)
for index in box_order:
insert_location = sheet.insert(rectangles[index])
if insert_location is not None:
area += np.prod(rectangles[index])
offset[index] += insert_location
inserted[index] = True
consumed_box = np.max((offset + rectangles)[inserted], axis=0)
density = area / np.product(consumed_box)
return density, offset[inserted], inserted, consumed_box
def paths(paths, **kwargs):
"""
Pack a list of Path2D objects into a rectangle.
Parameters
------------
paths: (n,) Path2D
Geometry to be packed
Returns
------------
packed : trimesh.path.Path2D
Object containing input geometry
inserted : (m,) int
Indexes of paths inserted into result
"""
from .util import concatenate
quantity = []
for path in paths:
if 'quantity' in path.metadata:
quantity.append(path.metadata['quantity'])
else:
quantity.append(1)
# pack using exterior polygon (will OBB)
packable = [i.polygons_closed[i.root[0]] for i in paths]
# pack the polygons using rectangular bin packing
inserted, transforms = polygons(polygons=packable,
quantity=quantity,
**kwargs)
multi = []
for i, T in zip(inserted, transforms):
multi.append(paths[i].copy())
multi[-1].apply_transform(T)
# append all packed paths into a single Path object
packed = concatenate(multi)
return packed, inserted
def polygons(polygons,
sheet_size=None,
iterations=50,
density_escape=.95,
spacing=0.094,
quantity=None,
**kwargs):
"""
Pack polygons into a rectangle by taking each Polygon's OBB
and then packing that as a rectangle.
Parameters
------------
polygons : (n,) shapely.geometry.Polygon
Source geometry
sheet_size : (2,) float
Size of rectangular sheet
iterations : int
Number of times to run the loop
density_escape : float
When to exit early (0.0 - 1.0)
spacing : float
How big a gap to leave between polygons
quantity : (n,) int, or None
Quantity of each Polygon
Returns
-------------
overall_inserted : (m,) int
Indexes of inserted polygons
packed : (m, 3, 3) float
Homogeonous transforms from original frame to packed frame
"""
from .polygons import polygons_obb
if quantity is None:
quantity = np.ones(len(polygons), dtype=np.int64)
else:
quantity = np.asanyarray(quantity, dtype=np.int64)
if len(quantity) != len(polygons):
raise ValueError('quantity must match polygons')
# find the oriented bounding box of the polygons
obb, rect = polygons_obb(polygons)
# pad all sides of the rectangle
rect += 2.0 * spacing
# move the OBB transform so the polygon is centered
# in the padded rectangle
for i, r in enumerate(rect):
obb[i][0:2, 2] += r * .5
# for polygons occurring multiple times
indexes = np.hstack([np.ones(q, dtype=np.int64) * i
for i, q in enumerate(quantity)])
# stack using advanced indexing
obb = obb[indexes]
rect = rect[indexes]
# store timing
tic = time.time()
density = 0.0
# run packing for a number of iterations
(density,
offset,
inserted,
sheet) = rectangles(
rectangles=rect,
sheet_size=sheet_size,
spacing=spacing,
density_escape=density_escape,
iterations=iterations, **kwargs)
toc = time.time()
log.debug('packing finished %i iterations in %f seconds',
i + 1,
toc - tic)
log.debug('%i/%i parts were packed successfully',
np.sum(inserted),
quantity.sum())
# transformations to packed positions
packed = obb[inserted]
# apply the offset and inter- polygon spacing
packed.reshape(-1, 9)[:, [2, 5]] += offset + spacing
return indexes[inserted], packed
def rectangles(rectangles,
sheet_size=None,
density_escape=0.9,
spacing=0.0,
iterations=50,
quanta=None):
"""
Run multiple iterations of rectangle packing.
Parameters
------------
rectangles : (n, 2) float
Size of rectangles to be packed
sheet_size : None or (2,) float
Size of sheet to pack onto
density_escape : float
Exit early if density is above this threshold
spacing : float
Distance to allow between rectangles
iterations : int
Number of iterations to run
quanta : None or float
Returns
---------
density : float
Area filled over total sheet area
offset : (m,2) float
Offsets to move rectangles to their packed location
inserted : (n,) bool
Which of the original rectangles were packed
consumed_box : (2,) float
Bounding box size of packed result
"""
rectangles = np.array(rectangles)
# best density percentage in 0.0 - 1.0
best_density = 0.0
# how many rectangles were inserted
best_insert = 0
for i in range(iterations):
# run a single insertion order
# don't shuffle the first run, shuffle subsequent runs
packed = rectangles_single(
rectangles,
sheet_size=sheet_size,
shuffle=(i != 0))
density = packed[0]
insert = packed[2].sum()
if quanta is not None:
# compute the density using an upsized quanta
box = np.ceil(packed[3] / quanta) * quanta
# scale the density result
density *= (np.product(packed[3]) / np.product(box))
# compare this packing density against our best
if density > best_density or insert > best_insert:
best_density = density
best_insert = insert
# save the result
result = packed
# exit early if everything is inserted and
# we have exceeded our target density
if density > density_escape and packed[2].all():
break
return result
def images(images, power_resize=False):
"""
Pack a list of images and return result and offsets.
Parameters
------------
images : (n,) PIL.Image
Images to be packed
deduplicate : bool
If True deduplicate images before packing
Returns
-----------
packed : PIL.Image
Multiple images packed into result
offsets : (n, 2) int
Offsets for original image to pack
"""
from PIL import Image
# use the number of pixels as the rectangle size
rect = np.array([i.size for i in images])
(density,
offset,
insert,
sheet) = rectangles(rectangles=rect)
# really should have inserted all the rectangles
assert insert.all()
# offsets should be integer multiple of pizels
offset = offset.round().astype(int)
size = sheet.round().astype(int)
if power_resize:
size = (2 ** np.ceil(np.log2(size))).astype(np.int64)
# create the image
result = Image.new('RGB', tuple(size))
# paste each image into the result
for img, off in zip(images, offset):
result.paste(img, tuple(off))
return result, offset