openrr_planner/collision/
collision_detector.rs

1/*
2Copyright 2017 Takashi Ogura
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16use std::{
17    collections::HashMap,
18    path::Path,
19    time::{Duration, Instant},
20};
21
22use k::nalgebra as na;
23use na::RealField;
24use ncollide3d::{
25    partitioning::{BVH, BVT},
26    query,
27    shape::{Compound, Shape, ShapeHandle},
28};
29use tracing::{debug, warn};
30
31use super::urdf::{k_link_geometry_to_shape_handle, urdf_geometry_to_shape_handle};
32use crate::errors::*;
33
34type NameShapeMap<T> = HashMap<String, Vec<(ShapeHandle<T>, na::Isometry3<T>)>>;
35
36/// Lists collisions between a robot and an object
37pub struct EnvCollisionNames<'a, 'b, T>
38where
39    T: RealField + Copy,
40{
41    detector: &'a CollisionDetector<T>,
42    target_shape: &'b dyn Shape<T>,
43    target_pose: &'b na::Isometry3<T>,
44    joints: Vec<&'b k::Node<T>>,
45    index: usize,
46}
47
48impl<'a, 'b, T> EnvCollisionNames<'a, 'b, T>
49where
50    T: RealField + Copy + k::SubsetOf<f64>,
51{
52    pub fn new(
53        detector: &'a CollisionDetector<T>,
54        robot: &'b k::Chain<T>,
55        target_shape: &'b dyn Shape<T>,
56        target_pose: &'b na::Isometry3<T>,
57    ) -> Self {
58        robot.update_transforms();
59        let joints = robot.iter().collect();
60        Self {
61            detector,
62            target_shape,
63            target_pose,
64            joints,
65            index: 0,
66        }
67    }
68}
69
70impl<T> Iterator for EnvCollisionNames<'_, '_, T>
71where
72    T: RealField + Copy + k::SubsetOf<f64>,
73{
74    type Item = String;
75
76    fn next(&mut self) -> Option<String> {
77        if self.joints.len() <= self.index {
78            return None;
79        }
80        let joint = self.joints[self.index];
81        self.index += 1;
82
83        // Get the absolute (world) pose and name of the specified node
84        let joint_pose = joint.world_transform().unwrap();
85        let joint_name = &joint.joint().name;
86
87        match self.detector.name_collision_model_map.get(joint_name) {
88            Some(obj_vec) => {
89                // Check potential conflicts by an AABB-based sweep and prune algorithm
90                for obj in obj_vec {
91                    let obj_pose = joint_pose * obj.1;
92                    let aabb1 = obj.0.aabb(&(obj_pose));
93                    let aabb2 = self.target_shape.aabb(self.target_pose);
94
95                    let bvt = BVT::new_balanced(vec![(0usize, aabb2)]);
96
97                    let mut collector = Vec::<usize>::new();
98                    let mut visitor = query::visitors::BoundingVolumeInterferencesCollector::new(
99                        &aabb1,
100                        &mut collector,
101                    );
102
103                    bvt.visit(&mut visitor);
104
105                    if !collector.is_empty() {
106                        // Check conflicts precisely
107                        let dist = query::distance(
108                            &obj_pose,
109                            &*obj.0,
110                            self.target_pose,
111                            self.target_shape,
112                        );
113                        // proximity and prediction does not work correctly for meshes.
114                        if dist < self.detector.prediction {
115                            debug!("name: {joint_name}, dist={dist}");
116                            return Some(joint_name.to_owned());
117                        }
118                    }
119                }
120            }
121            None => {
122                debug!("collision model {joint_name} not found");
123            }
124        }
125        self.next()
126    }
127}
128
129/// Lists collisions inside robot links
130pub struct SelfCollisionPairs<'a, T>
131where
132    T: RealField + Copy,
133{
134    detector: &'a CollisionDetector<T>,
135    robot: &'a k::Chain<T>,
136    self_collision_pairs: &'a [(String, String)],
137    index: usize,
138    used_duration: HashMap<String, Duration>,
139}
140
141impl<'a, T> SelfCollisionPairs<'a, T>
142where
143    T: RealField + Copy + k::SubsetOf<f64>,
144{
145    pub fn new(
146        detector: &'a CollisionDetector<T>,
147        robot: &'a k::Chain<T>,
148        self_collision_pairs: &'a [(String, String)],
149    ) -> Self {
150        robot.update_transforms();
151        Self {
152            detector,
153            robot,
154            self_collision_pairs,
155            index: 0,
156            used_duration: HashMap::new(),
157        }
158    }
159
160    /// Get the information about which part is the most heaviest.
161    pub fn used_duration(&self) -> &HashMap<String, Duration> {
162        &self.used_duration
163    }
164}
165
166impl<T> Iterator for SelfCollisionPairs<'_, T>
167where
168    T: RealField + Copy + k::SubsetOf<f64>,
169{
170    type Item = (String, String);
171
172    fn next(&mut self) -> Option<(String, String)> {
173        if self.self_collision_pairs.len() <= self.index {
174            return None;
175        }
176        let (j1, j2) = &self.self_collision_pairs[self.index];
177        self.index += 1;
178
179        // Get the collision models of the specified 2 nodes
180        let obj_vec1 = match self.detector.name_collision_model_map.get(j1) {
181            Some(v) => v,
182            None => {
183                warn!("Collision model {j1} not found");
184                return self.next();
185            }
186        };
187        let obj_vec2 = match self.detector.name_collision_model_map.get(j2) {
188            Some(v) => v,
189            None => {
190                warn!("Collision model {j2} not found");
191                return self.next();
192            }
193        };
194
195        // Get the absolute (world) poses of the specified 2 nodes
196        let pose1 = match self.robot.find(j1) {
197            Some(v) => v.world_transform().unwrap(),
198            None => {
199                warn!("self_colliding: joint {j1} not found");
200                return self.next();
201            }
202        };
203        let pose2 = match self.robot.find(j2) {
204            Some(v) => v.world_transform().unwrap(),
205            None => {
206                warn!("self_colliding: joint {j2} not found");
207                return self.next();
208            }
209        };
210
211        // Check potential conflicts by an AABB-based sweep and prune algorithm
212        let start_time = Instant::now();
213        for obj1 in obj_vec1 {
214            let aabb1 = obj1.0.aabb(&(pose1 * obj1.1));
215
216            let index_and_aabb = obj_vec2
217                .iter()
218                .enumerate()
219                .map(|(index, obj)| (index, obj.0.aabb(&(pose2 * obj.1))))
220                .collect();
221            let bvt = BVT::new_balanced(index_and_aabb);
222
223            let mut collector = Vec::<usize>::new();
224            let mut visitor =
225                query::visitors::BoundingVolumeInterferencesCollector::new(&aabb1, &mut collector);
226
227            bvt.visit(&mut visitor);
228
229            if !collector.is_empty() {
230                // Check conflicts precisely
231                for index in collector {
232                    let obj2 = &obj_vec2[index];
233
234                    // proximity and predict does not work correctly for mesh
235                    let dist =
236                        query::distance(&(pose1 * obj1.1), &*obj1.0, &(pose2 * obj2.1), &*obj2.0);
237                    debug!("name: {j1}, name: {j2} dist={dist}");
238                    if dist < self.detector.prediction {
239                        return Some((j1.to_owned(), j2.to_owned()));
240                    }
241                }
242            }
243        }
244
245        // Record the time used for this collision checking
246        let elapsed = start_time.elapsed();
247        *self
248            .used_duration
249            .entry(j1.to_owned())
250            .or_insert_with(|| Duration::from_nanos(0)) += elapsed;
251        *self
252            .used_duration
253            .entry(j2.to_owned())
254            .or_insert_with(|| Duration::from_nanos(0)) += elapsed;
255
256        self.next()
257    }
258}
259
260#[derive(Clone)]
261/// Collision detector
262pub struct CollisionDetector<T>
263where
264    T: RealField + Copy,
265{
266    name_collision_model_map: NameShapeMap<T>,
267    /// margin length for collision detection
268    pub prediction: T,
269}
270
271impl<T> CollisionDetector<T>
272where
273    T: RealField + Copy + k::SubsetOf<f64>,
274{
275    /// Create CollisionDetector from HashMap
276    pub fn new(name_collision_model_map: NameShapeMap<T>, prediction: T) -> Self {
277        CollisionDetector {
278            name_collision_model_map,
279            prediction,
280        }
281    }
282
283    /// Create CollisionDetector from urdf_rs::Robot
284    pub fn from_urdf_robot(urdf_robot: &urdf_rs::Robot, prediction: T) -> Self {
285        Self::from_urdf_robot_with_base_dir(urdf_robot, None, prediction)
286    }
287
288    /// Create CollisionDetector from urdf_rs::Robot with base_dir support
289    ///
290    /// base_dir: mesh files are loaded from this dir if the path does not start with "package://"
291    pub fn from_urdf_robot_with_base_dir(
292        urdf_robot: &urdf_rs::Robot,
293        base_dir: Option<&Path>,
294        prediction: T,
295    ) -> Self {
296        let mut name_collision_model_map = HashMap::new();
297        let link_joint_map = k::urdf::link_to_joint_map(urdf_robot);
298        for l in &urdf_robot.links {
299            let col_pose_vec = l
300                .collision
301                .iter()
302                .filter_map(|collision| {
303                    urdf_geometry_to_shape_handle(&collision.geometry, base_dir)
304                        .map(|col| (col, k::urdf::isometry_from(&collision.origin)))
305                })
306                .collect::<Vec<_>>();
307            debug!("name={}, ln={}", l.name, col_pose_vec.len());
308            if !col_pose_vec.is_empty() {
309                if let Some(joint_name) = link_joint_map.get(&l.name) {
310                    name_collision_model_map.insert(joint_name.to_owned(), col_pose_vec);
311                }
312            }
313        }
314        CollisionDetector {
315            name_collision_model_map,
316            prediction,
317        }
318    }
319
320    /// Create CollisionDetector from k::Chain
321    pub fn from_robot(robot: &k::Chain<T>, prediction: T) -> Self {
322        let mut name_collision_model_map = HashMap::new();
323        for node in robot.iter() {
324            let link = match node.link().clone() {
325                Some(v) => v,
326                None => break,
327            };
328
329            let col_pose_vec = link
330                .collisions
331                .iter()
332                .filter_map(|c| {
333                    k_link_geometry_to_shape_handle(&c.geometry).map(|col| (col, *c.origin()))
334                })
335                .collect::<Vec<_>>();
336
337            debug!("name={}, ln={}", link.name, col_pose_vec.len());
338            if !col_pose_vec.is_empty() {
339                name_collision_model_map.insert(node.joint().name.to_owned(), col_pose_vec);
340            }
341        }
342        CollisionDetector {
343            name_collision_model_map,
344            prediction,
345        }
346    }
347
348    /// Detects collisions of a robot with an environmental object and returns the names of the link(joint) names
349    ///
350    /// robot: robot model
351    /// target_shape: shape of the environmental object
352    /// target_pose: pose of the environmental object
353    pub fn detect_env<'a>(
354        &'a self,
355        robot: &'a k::Chain<T>,
356        target_shape: &'a dyn Shape<T>,
357        target_pose: &'a na::Isometry3<T>,
358    ) -> EnvCollisionNames<'a, 'a, T> {
359        robot.update_transforms();
360        EnvCollisionNames::new(self, robot, target_shape, target_pose)
361    }
362
363    /// Detects self collisions and returns the names of the link(joint) names
364    ///
365    /// robot: robot model
366    /// self_collision_pairs: pairs of the names of the link(joint)
367    pub fn detect_self<'a>(
368        &'a self,
369        robot: &'a k::Chain<T>,
370        self_collision_pairs: &'a [(String, String)],
371    ) -> SelfCollisionPairs<'a, T> {
372        robot.update_transforms();
373        SelfCollisionPairs::new(self, robot, self_collision_pairs)
374    }
375}
376
377#[cfg(test)]
378mod tests {
379    use na::{Isometry3, Vector3};
380    use ncollide3d::shape::Cuboid;
381
382    use super::*;
383
384    #[test]
385    fn test_environmental_collision_detection() {
386        let urdf_robot = urdf_rs::read_file("sample.urdf").unwrap();
387        let robot = k::Chain::<f32>::from(&urdf_robot);
388        robot.set_joint_positions(&[0.0; 16]).unwrap();
389        let detector = CollisionDetector::from_urdf_robot(&urdf_robot, 0.01);
390
391        // This target is based on `obj2` in `obstacles.urdf`.
392        let target = Cuboid::new(Vector3::new(0.2, 0.3, 0.1));
393        let target_pose = Isometry3::new(Vector3::new(0.7, 0.0, 0.6), na::zero());
394
395        assert!(detector
396            .detect_env(&robot, &target, &target_pose)
397            .next()
398            .is_none());
399
400        let angles = [
401            -0.7, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
402        ];
403        robot.set_joint_positions(&angles).unwrap();
404        let names: Vec<String> = detector.detect_env(&robot, &target, &target_pose).collect();
405        assert_eq!(
406            names,
407            vec![
408                "l_wrist_yaw",
409                "l_wrist_pitch",
410                "l_gripper_linear1",
411                "l_gripper_linear2"
412            ]
413        );
414
415        let target_pose = Isometry3::new(Vector3::new(0.0, 0.0, 0.0), na::zero());
416        let names: Vec<String> = detector.detect_env(&robot, &target, &target_pose).collect();
417        assert_eq!(names, vec!["root"]);
418    }
419
420    #[test]
421    fn test_self_collision_detection() {
422        let urdf_robot = urdf_rs::read_file("sample.urdf").unwrap();
423        let robot = k::Chain::<f32>::from(&urdf_robot);
424        let detector = CollisionDetector::from_urdf_robot(&urdf_robot, 0.01);
425
426        let collision_check_pairs = parse_colon_separated_pairs(&[
427            "root:l_shoulder_roll".to_owned(),
428            "root:l_elbow_pitch".to_owned(),
429            "root:l_wrist_yaw".to_owned(),
430            "root:l_wrist_pitch".to_owned(),
431        ])
432        .unwrap();
433        let (correct_collisions, _) = collision_check_pairs.split_at(2);
434
435        let angles = [0.0; 16];
436        robot.set_joint_positions(&angles).unwrap();
437        assert!(detector
438            .detect_self(&robot, &collision_check_pairs)
439            .next()
440            .is_none());
441
442        let angles = [
443            -1.57, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
444        ];
445        robot.set_joint_positions(&angles).unwrap();
446        let result: Vec<(String, String)> = detector
447            .detect_self(&robot, &collision_check_pairs)
448            .collect();
449        assert_eq!(result, correct_collisions.to_vec());
450    }
451}
452
453/// Convert urdf object into openrr_planner/ncollide3d object
454pub trait FromUrdf {
455    fn from_urdf_robot_with_base_dir(robot: &urdf_rs::Robot, path: Option<&Path>) -> Self;
456    fn from_urdf_robot(robot: &urdf_rs::Robot) -> Self
457    where
458        Self: std::marker::Sized,
459    {
460        Self::from_urdf_robot_with_base_dir(robot, None::<&Path>)
461    }
462    fn from_urdf_file(path: impl AsRef<Path>) -> ::std::result::Result<Self, urdf_rs::UrdfError>
463    where
464        Self: ::std::marker::Sized,
465    {
466        Ok(Self::from_urdf_robot_with_base_dir(
467            &urdf_rs::read_file(&path)?,
468            path.as_ref().parent(),
469        ))
470    }
471}
472
473/// Parse args to get self collision pair
474///
475/// # Example
476///
477/// ```
478/// let pairs = openrr_planner::collision::parse_colon_separated_pairs(&vec!["ab:cd".to_owned(), "ab:ef".to_owned()]).unwrap();
479/// assert_eq!(pairs.len(), 2);
480/// assert_eq!(pairs[0].0, "ab");
481/// assert_eq!(pairs[0].1, "cd");
482/// assert_eq!(pairs[1].0, "ab");
483/// assert_eq!(pairs[1].1, "ef");
484/// ```
485pub fn parse_colon_separated_pairs(pair_strs: &[String]) -> Result<Vec<(String, String)>> {
486    let mut pairs = Vec::new();
487    for pair_str in pair_strs {
488        let mut sp = pair_str.split(':');
489        if let Some(p1) = sp.next() {
490            if let Some(p2) = sp.next() {
491                pairs.push((p1.to_owned(), p2.to_owned()));
492            } else {
493                return Err(Error::ParseError(pair_str.to_owned()));
494            }
495        } else {
496            return Err(Error::ParseError(pair_str.to_owned()));
497        }
498    }
499    Ok(pairs)
500}
501
502#[cfg(test)]
503mod test {
504    use super::parse_colon_separated_pairs;
505    #[test]
506    fn test_parse_colon_separated_pairs() {
507        let pairs = parse_colon_separated_pairs(&["j0:j1".to_owned(), "j2:j0".to_owned()]).unwrap();
508        assert_eq!(pairs.len(), 2);
509        assert_eq!(pairs[0].0, "j0");
510        assert_eq!(pairs[0].1, "j1");
511        assert_eq!(pairs[1].0, "j2");
512        assert_eq!(pairs[1].1, "j0");
513    }
514}
515
516/// Create `ncollide::shape::Compound` from URDF file
517///
518/// The `<link>` elements are used as obstacles. set the origin/geometry of
519/// `<visual>` and `<collision>`. You can skip `<inertia>`.
520impl FromUrdf for Compound<f64> {
521    fn from_urdf_robot_with_base_dir(
522        urdf_obstacle: &urdf_rs::Robot,
523        base_path: Option<&Path>,
524    ) -> Self {
525        let compound_data = urdf_obstacle
526            .links
527            .iter()
528            .flat_map(|l| {
529                l.collision
530                    .iter()
531                    .map(|collision| {
532                        urdf_geometry_to_shape_handle(&collision.geometry, base_path)
533                            .map(|col| (k::urdf::isometry_from(&collision.origin), col))
534                    })
535                    .collect::<Vec<_>>()
536            })
537            .flatten()
538            .collect::<Vec<_>>();
539        Compound::new(compound_data)
540    }
541}