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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user