Coverage for trimesh/path/raster.py: 68%
41 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
1"""
2raster.py
3------------
5Turn 2D vector paths into raster images using `pillow`
6"""
8import numpy as np
10try:
11 # keep pillow as a soft dependency
12 from PIL import Image, ImageChops, ImageDraw
13except BaseException as E:
14 from .. import exceptions
16 # re-raise the useful exception when called
17 _handle = exceptions.ExceptionWrapper(E)
18 Image = _handle
19 ImageDraw = _handle
20 ImageChops = _handle
22from ..typed import ArrayLike, Floating
25def rasterize(
26 path: "trimesh.path.Path2D", # noqa
27 pitch: Floating | ArrayLike | None = None,
28 origin: ArrayLike | None = None,
29 resolution=None,
30 fill=True,
31 width=None,
32):
33 """
34 Rasterize a Path2D object into a boolean image ("mode 1").
36 Parameters
37 ------------
38 path : Path2D
39 Original geometry
40 pitch : float or (2,) float
41 Length(s) in model space of pixel edges
42 origin : (2,) float
43 Origin position in model space
44 resolution : (2,) int
45 Resolution in pixel space
46 fill : bool
47 If True will return closed regions as filled
48 width : int
49 If not None will draw outline this wide in pixels
51 Returns
52 ------------
53 raster : PIL.Image
54 Rasterized version of input as `mode 1` image
55 """
57 if pitch is None:
58 if resolution is not None:
59 resolution = np.array(resolution, dtype=np.int64)
60 # establish pitch from passed resolution
61 pitch = (path.extents / (resolution + 2)).max()
62 else:
63 pitch = path.extents.max() / 2048
65 if origin is None:
66 origin = path.bounds[0] - (pitch * 2.0)
68 # check inputs
69 pitch = np.asanyarray(pitch, dtype=np.float64)
70 origin = np.asanyarray(origin, dtype=np.float64)
72 # if resolution is None make it larger than path
73 if resolution is None:
74 span = np.ptp(np.vstack((path.bounds, origin)), axis=0)
75 resolution = np.ceil(span / pitch) + 2
76 # get resolution as a (2,) int tuple
77 resolution = np.asanyarray(resolution, dtype=np.int64)
78 resolution = tuple(resolution.tolist())
80 # convert all discrete paths to pixel space
81 discrete = [((i - origin) / pitch).round().astype(np.int64) for i in path.discrete]
83 # the path indexes that are exteriors
84 # needed to know what to fill/empty but expensive
85 roots = path.root
86 enclosure = path.enclosure_directed
88 # draw the exteriors
89 result = Image.new(mode="1", size=resolution)
90 draw = ImageDraw.Draw(result)
92 # if a width is specified draw the outline
93 if width is not None:
94 width = int(width)
95 for coords in discrete:
96 draw.line(coords.flatten().tolist(), fill=1, width=width)
97 # if we are not filling the polygon exit
98 if not fill:
99 return result
101 # roots are ordered by degree
102 # so we draw the outermost one first
103 # and then go in as we progress
104 for root in roots:
105 # draw the exterior
106 draw.polygon(discrete[root].flatten().tolist(), fill=1)
107 # draw the interior children
108 for child in enclosure[root]:
109 draw.polygon(discrete[child].flatten().tolist(), fill=0)
111 return result