Mixed Case Depalletizing¶
Mixed-case depalletizing and palletizing is crucial in the logistics industry, enabling efficient use-cases like distribution or order fulfillment. Fast motion planning can significantly reduce cycle times and boost productivity. Jacobi’s software simplifies the process of adding depalletizing and palletizing capabilities and allows for quick cell setup, making it easy for businesses to implement comprehensive solutions.
This example projects picks different boxes from two pallets and palletizes them mixed-case on a third empty pallet.
To load the Jacobi Studio project automatically in the background, please define your JACOBI_API_KEY
environment variable.
Video
Application Code
1from pathlib import Path
2
3import yaml
4
5from jacobi import Planner, Studio, Obstacle, Box, Frame, Motion, LinearSection, Trajectory
6
7
8class MixedCaseDepalletizer:
9 def __init__(self, project: str):
10 self.planner = Planner.load_from_studio(project)
11 self.robot = self.planner.environment.get_robot()
12
13 self.box1 = Obstacle('box-1', Box(0.4, 0.2, 0.2), color='E1B471')
14 self.box2 = Obstacle('box-2', Box(0.5, 0.4, 0.3), color='E47181')
15
16 self.home = self.planner.environment.get_waypoint('Home')
17 self.pallet_left = self.planner.environment.get_obstacle('Pallet Left').origin
18 self.pallet_right = self.planner.environment.get_obstacle('Pallet Right').origin
19 self.pallet_center = self.planner.environment.get_obstacle('Pallet Center').origin
20
21 self.box_1_pattern = self.load_pattern_from_file('pattern_box1.yml', self.box1.collision)
22 self.box_2_pattern = self.load_pattern_from_file('pattern_box2.yml', self.box2.collision)
23
24 self.studio = Studio()
25 self.studio.reset()
26
27 self.current_joint_position = self.home.position
28 self.studio.set_joint_position(self.current_joint_position)
29
30 def plan_cached(self, m: Motion) -> Trajectory:
31 cache_directory = Path('cache/depal')
32 cache_directory.mkdir(exist_ok=True, parents=True)
33
34 trajectory_path = cache_directory / f'{m.name}.json'
35 if trajectory_path.exists():
36 return Trajectory.from_json_file(trajectory_path)
37
38 trajectory = self.planner.plan(m)
39 trajectory.to_json_file(trajectory_path)
40 return trajectory
41
42 @staticmethod
43 def load_pattern_from_file(path: Path, box: Box) -> list[list[Frame]]:
44 """Load a box pattern from a yaml file."""
45
46 with (Path(__file__).absolute().parent / path).open('r') as f:
47 data = yaml.safe_load(f)
48
49 def parse(p: dict, axis: str) -> float:
50 element = p.get(axis, 0.0)
51 if isinstance(element, str):
52 return eval(element, {'x': box.x, 'y': box.y, 'z': box.z, 'g': 0.01})
53 return element
54
55 def to_frame(p: dict) -> Frame:
56 return Frame(x=parse(p, 'x'), y=parse(p, 'y'), z=parse(p, 'z'), c=parse(p, 'c'))
57
58 return [[to_frame(box) for box in layer['boxes']] for layer in data['layers']]
59
60 def load_pattern_onto_pallet(self, pattern, pallet: Frame, box: Obstacle):
61 obstacles = []
62
63 for i_layer, layer in enumerate(pattern):
64 for i_box, pose in enumerate(layer):
65 b = box.with_origin(pallet * pose)
66 b.name = f'{b.name}-{i_layer + 1}-{i_box + 1}'
67 self.studio.add_obstacle(b)
68 b_ = self.planner.environment.add_obstacle(b)
69 obstacles.append(b_)
70
71 return obstacles
72
73 def execute_pick_cycle(self, box_at_pick, place_pose):
74 # From place to pick
75 pick = box_at_pick.origin * Frame(z=box_at_pick.collision.z / 2)
76
77 m = Motion(f'{box_at_pick.name}-to-pick', self.current_joint_position, pick)
78 if self.current_joint_position != self.home.position:
79 m.linear_retraction = LinearSection(offset=Frame(z=0.05))
80 m.linear_approach = LinearSection(offset=Frame(z=0.05))
81
82 trajectory = self.plan_cached(m)
83 self.studio.run_trajectory(trajectory)
84 self.current_joint_position = trajectory.positions[-1]
85
86 box_as_item = box_at_pick.with_origin(Frame(z=-box_at_pick.collision.z / 2))
87 self.studio.set_item(box_as_item)
88 self.robot.item_obstacle = box_as_item
89
90 self.studio.remove_obstacle(box_at_pick)
91 self.planner.environment.remove_obstacle(box_at_pick)
92
93 # From pick to place
94 place_box = self.pallet_center * place_pose
95 place = place_box * Frame(z=box_at_pick.collision.z / 2)
96
97 m = Motion(f'{box_at_pick.name}-to-place', self.current_joint_position, place)
98 m.orientation_loss_weight = 2.0
99 m.linear_retraction = LinearSection(offset=Frame(z=box_at_pick.collision.z + 0.01))
100 m.linear_approach = LinearSection(offset=Frame(z=0.05))
101
102 trajectory = self.plan_cached(m)
103 self.studio.run_trajectory(trajectory)
104 self.current_joint_position = trajectory.positions[-1]
105
106 box_at_place = box_at_pick.with_origin(place_box)
107 self.studio.add_obstacle(box_at_place)
108 self.planner.environment.add_obstacle(box_at_place)
109
110 self.studio.set_item(None)
111 self.robot.item_obstacle = None
112
113 def run(self):
114 box_1_obstacles = self.load_pattern_onto_pallet(self.box_1_pattern, self.pallet_left, self.box1)
115 box_2_obstacles = self.load_pattern_onto_pallet(self.box_2_pattern, self.pallet_right, self.box2)
116
117 # Define iterators to get the latest picked box
118 box_1_stack, box_2_stack = reversed(box_1_obstacles), reversed(box_2_obstacles)
119
120 # Pick and place box 1 and box 2 in arbitrary order and given arbitrary poses on the new pallet
121 for place_pose, box_at_pick in zip(self.box_2_pattern[0], box_2_stack):
122 self.execute_pick_cycle(box_at_pick, place_pose)
123
124 for place_pose, box_at_pick in zip(reversed(self.box_1_pattern[0][:10]), box_1_stack):
125 self.execute_pick_cycle(box_at_pick, place_pose * Frame(z=self.box2.collision.z + 0.01))
126
127 for place_pose, box_at_pick in zip(self.box_2_pattern[1][-2:], box_2_stack):
128 self.execute_pick_cycle(box_at_pick, place_pose)
129
130 for place_pose, box_at_pick in zip(reversed(self.box_1_pattern[1][:2]), box_1_stack):
131 self.execute_pick_cycle(box_at_pick, place_pose * Frame(y=-0.61, z=self.box2.collision.z + 0.01))
132
133 for place_pose, box_at_pick in zip(reversed(self.box_1_pattern[1][:7]), box_1_stack):
134 self.execute_pick_cycle(box_at_pick, place_pose * Frame(z=self.box2.collision.z + 0.01))
135
136 for place_pose, box_at_pick in zip(reversed(self.box_1_pattern[0][-5:]), box_1_stack):
137 self.execute_pick_cycle(box_at_pick, place_pose * Frame(z=2 * (self.box2.collision.z + 0.01)))
138
139 # Move back to home
140 m = Motion('to-home', self.current_joint_position, self.home)
141 trajectory = self.plan_cached(m)
142 self.studio.run_trajectory(trajectory)
143
144
145if __name__ == '__main__':
146 application = MixedCaseDepalletizer(project='Mixed-case Depal-pal')
147 application.run()