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

1""" 

2raster.py 

3------------ 

4 

5Turn 2D vector paths into raster images using `pillow` 

6""" 

7 

8import numpy as np 

9 

10try: 

11 # keep pillow as a soft dependency 

12 from PIL import Image, ImageChops, ImageDraw 

13except BaseException as E: 

14 from .. import exceptions 

15 

16 # re-raise the useful exception when called 

17 _handle = exceptions.ExceptionWrapper(E) 

18 Image = _handle 

19 ImageDraw = _handle 

20 ImageChops = _handle 

21 

22from ..typed import ArrayLike, Floating 

23 

24 

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"). 

35 

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 

50 

51 Returns 

52 ------------ 

53 raster : PIL.Image 

54 Rasterized version of input as `mode 1` image 

55 """ 

56 

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 

64 

65 if origin is None: 

66 origin = path.bounds[0] - (pitch * 2.0) 

67 

68 # check inputs 

69 pitch = np.asanyarray(pitch, dtype=np.float64) 

70 origin = np.asanyarray(origin, dtype=np.float64) 

71 

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()) 

79 

80 # convert all discrete paths to pixel space 

81 discrete = [((i - origin) / pitch).round().astype(np.int64) for i in path.discrete] 

82 

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 

87 

88 # draw the exteriors 

89 result = Image.new(mode="1", size=resolution) 

90 draw = ImageDraw.Draw(result) 

91 

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 

100 

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) 

110 

111 return result