break apart utils into submodules, and add utils.pack2d
This commit is contained in:
parent
a4fe3d9e2e
commit
fc1a0f5a5a
15
masque/utils/__init__.py
Normal file
15
masque/utils/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
"""
|
||||
Various helper functions, type definitions, etc.
|
||||
"""
|
||||
from .types import layer_t, annotations_t
|
||||
|
||||
from .array import is_scalar
|
||||
from .autoslots import AutoSlots
|
||||
|
||||
from .bitwise import get_bit, set_bit
|
||||
from .vertices import remove_duplicate_vertices, remove_colinear_vertices
|
||||
from .transform import rotation_matrix_2d, normalize_mirror
|
||||
|
||||
from . import pack2d
|
||||
|
||||
|
11
masque/utils/array.py
Normal file
11
masque/utils/array.py
Normal file
@ -0,0 +1,11 @@
|
||||
from typing import Any
|
||||
|
||||
|
||||
def is_scalar(var: Any) -> bool:
|
||||
"""
|
||||
Alias for 'not hasattr(var, "__len__")'
|
||||
|
||||
Args:
|
||||
var: Checks if `var` has a length.
|
||||
"""
|
||||
return not hasattr(var, "__len__")
|
27
masque/utils/autoslots.py
Normal file
27
masque/utils/autoslots.py
Normal file
@ -0,0 +1,27 @@
|
||||
from abc import ABCMeta
|
||||
|
||||
|
||||
class AutoSlots(ABCMeta):
|
||||
"""
|
||||
Metaclass for automatically generating __slots__ based on superclass type annotations.
|
||||
|
||||
Superclasses must set `__slots__ = ()` to make this work properly.
|
||||
|
||||
This is a workaround for the fact that non-empty `__slots__` can't be used
|
||||
with multiple inheritance. Since we only use multiple inheritance with abstract
|
||||
classes, they can have empty `__slots__` and their attribute type annotations
|
||||
can be used to generate a full `__slots__` for the concrete class.
|
||||
"""
|
||||
def __new__(cls, name, bases, dctn):
|
||||
parents = set()
|
||||
for base in bases:
|
||||
parents |= set(base.mro())
|
||||
|
||||
slots = tuple(dctn.get('__slots__', tuple()))
|
||||
for parent in parents:
|
||||
if not hasattr(parent, '__annotations__'):
|
||||
continue
|
||||
slots += tuple(getattr(parent, '__annotations__').keys())
|
||||
|
||||
dctn['__slots__'] = slots
|
||||
return super().__new__(cls, name, bases, dctn)
|
34
masque/utils/bitwise.py
Normal file
34
masque/utils/bitwise.py
Normal file
@ -0,0 +1,34 @@
|
||||
from typing import Any
|
||||
|
||||
|
||||
def get_bit(bit_string: Any, bit_id: int) -> bool:
|
||||
"""
|
||||
Interprets bit number `bit_id` from the right (lsb) of `bit_string` as a boolean
|
||||
|
||||
Args:
|
||||
bit_string: Bit string to test
|
||||
bit_id: Bit number, 0-indexed from the right (lsb)
|
||||
|
||||
Returns:
|
||||
Boolean value of the requested bit
|
||||
"""
|
||||
return bit_string & (1 << bit_id) != 0
|
||||
|
||||
|
||||
def set_bit(bit_string: Any, bit_id: int, value: bool) -> Any:
|
||||
"""
|
||||
Returns `bit_string`, with bit number `bit_id` set to boolean `value`.
|
||||
|
||||
Args:
|
||||
bit_string: Bit string to alter
|
||||
bit_id: Bit number, 0-indexed from right (lsb)
|
||||
value: Boolean value to set bit to
|
||||
|
||||
Returns:
|
||||
Altered `bit_string`
|
||||
"""
|
||||
mask = (1 << bit_id)
|
||||
bit_string &= ~mask
|
||||
if value:
|
||||
bit_string |= mask
|
||||
return bit_string
|
88
masque/utils/pack2d.py
Normal file
88
masque/utils/pack2d.py
Normal file
@ -0,0 +1,88 @@
|
||||
"""
|
||||
2D bin-packing
|
||||
"""
|
||||
from typing import Tuple
|
||||
|
||||
import numpy
|
||||
from numpy.typing import NDArray, ArrayLike
|
||||
|
||||
from ..error import MasqueError
|
||||
|
||||
|
||||
def maxrects_bssf(
|
||||
rects: ArrayLike,
|
||||
containers: ArrayLike,
|
||||
presort: bool = True,
|
||||
allow_rejects: bool = True,
|
||||
) -> Tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
|
||||
"""
|
||||
sizes should be Nx2
|
||||
regions should be Mx4 (xmin, ymin, xmax, ymax)
|
||||
"""
|
||||
regions = numpy.array(containers, copy=False, dtype=float)
|
||||
rect_sizes = numpy.array(rects, copy=False, dtype=float)
|
||||
rect_locs = numpy.zeros_like(rect_sizes)
|
||||
rejected_rects = []
|
||||
|
||||
if presort:
|
||||
rotated_sizes = numpy.sort(rect_sizes, axis=0) # shortest side first
|
||||
rect_order = numpy.lexsort(rotated_sizes.T)[::-1] # Descending shortest side
|
||||
rect_sizes = rect_sizes[rect_order]
|
||||
|
||||
for rect_ind, rect_size in enumerate(rect_sizes):
|
||||
''' Remove degenerate regions '''
|
||||
# First remove duplicate regions (but keep one; code below would drop both)
|
||||
regions = numpy.unique(regions, axis=0)
|
||||
|
||||
# Now remove regions enclosed in another
|
||||
min_more = (regions[None, :, :2] >= regions[:, None, :2]).all(axis=2) # first axis > second axis
|
||||
max_less = (regions[None, :, 2:] <= regions[:, None, 2:]).all(axis=2) # first axis < second axis
|
||||
max_less &= ~numpy.eye(regions.shape[0], dtype=bool) # exclude self
|
||||
degenerate = (min_more & max_less).any(axis=0)
|
||||
regions = regions[~degenerate]
|
||||
|
||||
''' Place the rect '''
|
||||
# Best short-side fit (bssf) to pick a region
|
||||
bssf_scores = ((regions[:, 2:] - regions[:, :2]) - rect_size).min(axis=1).astype(float)
|
||||
bssf_scores[bssf_scores < 0] = numpy.inf # doesn't fit!
|
||||
rr = bssf_scores.argmin()
|
||||
if numpy.isinf(bssf_scores[rr]):
|
||||
if allow_rejects:
|
||||
rejected_rects.append(rect_ind)
|
||||
continue
|
||||
else:
|
||||
raise MasqueError(f'Failed to find a suitable location for rectangle {rect_ind}')
|
||||
|
||||
# Read out location
|
||||
loc = regions[rr, :2]
|
||||
rect_locs[rect_ind] = loc
|
||||
|
||||
''' Shatter regions '''
|
||||
# Which regions does this rectangle intersect?
|
||||
min_over = regions[:, :2] >= loc + rect_size
|
||||
max_undr = regions[:, 2:] <= loc
|
||||
intersects = ~(min_over | max_undr).any(axis=1)
|
||||
|
||||
# Which sides is there excess on?
|
||||
region_past_botleft = intersects[:, None] & (regions[:, :2] < loc)
|
||||
region_past_topright = intersects[:, None] & (regions[:, 2:] > loc + rect_size)
|
||||
|
||||
# Create new regions
|
||||
r_lft = regions[region_past_botleft[:, 0]].copy()
|
||||
r_bot = regions[region_past_botleft[:, 1]].copy()
|
||||
r_rgt = regions[region_past_topright[:, 0]].copy()
|
||||
r_top = regions[region_past_topright[:, 1]].copy()
|
||||
|
||||
r_lft[:, 2] = loc[0]
|
||||
r_bot[:, 3] = loc[1]
|
||||
r_rgt[:, 0] = loc[0] + rect_size[0]
|
||||
r_top[:, 1] = loc[1] + rect_size[1]
|
||||
|
||||
regions = numpy.vstack((regions[~intersects], r_lft, r_bot, r_rgt, r_top))
|
||||
|
||||
if rejected_rects:
|
||||
rejected_rects_arr = numpy.vstack(rejected_rects)
|
||||
else:
|
||||
rejected_rects_arr = numpy.empty((0, 2))
|
||||
|
||||
return rect_locs, rejected_rects_arr
|
40
masque/utils/transform.py
Normal file
40
masque/utils/transform.py
Normal file
@ -0,0 +1,40 @@
|
||||
"""
|
||||
Geometric transforms
|
||||
"""
|
||||
from typing import Sequence, Tuple
|
||||
|
||||
import numpy
|
||||
from numpy.typing import NDArray
|
||||
|
||||
|
||||
def rotation_matrix_2d(theta: float) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
2D rotation matrix for rotating counterclockwise around the origin.
|
||||
|
||||
Args:
|
||||
theta: Angle to rotate, in radians
|
||||
|
||||
Returns:
|
||||
rotation matrix
|
||||
"""
|
||||
return numpy.array([[numpy.cos(theta), -numpy.sin(theta)],
|
||||
[numpy.sin(theta), +numpy.cos(theta)]])
|
||||
|
||||
|
||||
def normalize_mirror(mirrored: Sequence[bool]) -> Tuple[bool, float]:
|
||||
"""
|
||||
Converts 0-2 mirror operations `(mirror_across_x_axis, mirror_across_y_axis)`
|
||||
into 0-1 mirror operations and a rotation
|
||||
|
||||
Args:
|
||||
mirrored: `(mirror_across_x_axis, mirror_across_y_axis)`
|
||||
|
||||
Returns:
|
||||
`mirror_across_x_axis` (bool) and
|
||||
`angle_to_rotate` in radians
|
||||
"""
|
||||
|
||||
mirrored_x, mirrored_y = mirrored
|
||||
mirror_x = (mirrored_x != mirrored_y) # XOR
|
||||
angle = numpy.pi if mirrored_y else 0
|
||||
return mirror_x, angle
|
8
masque/utils/types.py
Normal file
8
masque/utils/types.py
Normal file
@ -0,0 +1,8 @@
|
||||
"""
|
||||
Type definitions
|
||||
"""
|
||||
from typing import Union, Tuple, Sequence, Dict, List
|
||||
|
||||
|
||||
layer_t = Union[int, Tuple[int, int], str]
|
||||
annotations_t = Dict[str, List[Union[int, float, str]]]
|
54
masque/utils/vertices.py
Normal file
54
masque/utils/vertices.py
Normal file
@ -0,0 +1,54 @@
|
||||
"""
|
||||
Vertex list operations
|
||||
"""
|
||||
import numpy
|
||||
from numpy.typing import NDArray, ArrayLike
|
||||
|
||||
|
||||
def remove_duplicate_vertices(vertices: ArrayLike, closed_path: bool = True) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Given a list of vertices, remove any consecutive duplicates.
|
||||
|
||||
Args:
|
||||
vertices: `[[x0, y0], [x1, y1], ...]`
|
||||
closed_path: If True, `vertices` is interpreted as an implicity-closed path
|
||||
(i.e. the last vertex will be removed if it is the same as the first)
|
||||
|
||||
Returns:
|
||||
`vertices` with no consecutive duplicates.
|
||||
"""
|
||||
vertices = numpy.array(vertices)
|
||||
duplicates = (vertices == numpy.roll(vertices, 1, axis=0)).all(axis=1)
|
||||
if not closed_path:
|
||||
duplicates[0] = False
|
||||
return vertices[~duplicates]
|
||||
|
||||
|
||||
def remove_colinear_vertices(vertices: ArrayLike, closed_path: bool = True) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Given a list of vertices, remove any superflous vertices (i.e.
|
||||
those which lie along the line formed by their neighbors)
|
||||
|
||||
Args:
|
||||
vertices: Nx2 ndarray of vertices
|
||||
closed_path: If `True`, the vertices are assumed to represent an implicitly
|
||||
closed path. If `False`, the path is assumed to be open. Default `True`.
|
||||
|
||||
Returns:
|
||||
`vertices` with colinear (superflous) vertices removed.
|
||||
"""
|
||||
vertices = remove_duplicate_vertices(vertices)
|
||||
|
||||
# Check for dx0/dy0 == dx1/dy1
|
||||
|
||||
dv = numpy.roll(vertices, -1, axis=0) - vertices # [y1-y0, y2-y1, ...]
|
||||
dxdy = dv * numpy.roll(dv, 1, axis=0)[:, ::-1] # [[dx0*(dy_-1), (dx_-1)*dy0], dx1*dy0, dy1*dx0]]
|
||||
|
||||
dxdy_diff = numpy.abs(numpy.diff(dxdy, axis=1))[:, 0]
|
||||
err_mult = 2 * numpy.abs(dxdy).sum(axis=1) + 1e-40
|
||||
|
||||
slopes_equal = (dxdy_diff / err_mult) < 1e-15
|
||||
if not closed_path:
|
||||
slopes_equal[[0, -1]] = False
|
||||
|
||||
return vertices[~slopes_equal]
|
Loading…
Reference in New Issue
Block a user