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