空飛ぶロボットのつくりかた

ロボットをつくるために必要な技術をまとめます。ロボットの未来についても考えたりします。

Fusion360のお勉強

やりたいこと

Fusion360の使い方を覚えて、いろいろなロボットのパーツを作る

f:id:robonchu:20180303191458p:plain

教科書

インストール

Fusion360のインストール手順 | 3D CAD Fusion360の使い方が学習できるサイト

使い方解説動画

動作を軽くする方法

↑ これだけでもかなり軽くなった印象

所感

自分でまとめるまでもなく、クオリティの高い参考記事がたくさんある

リファレンスが多いのは素晴らしい! Fusin3D、コツコツ使っていこう。

ROS x Depthセンサで遊んでみる

やりたいこと

Depthセンサで取得したデータで遊んでみる

f:id:robonchu:20180227205357j:plain

xtionやkinectで使えるros package: rgbd_launch - ROS Wiki

今回はsoftkineticを用いる

設定

robonchu.hatenablog.com

RGBDの情報を使って亀を制御してみる

ピンクと黄色のボールで亀を制御

  • ピンクのボールをカメラに近づけたり遠ざけたりすることで前進、後退を制御

  • 黄色のボールをカメラに近づけたり遠ざけたりすることで右回転、左回転を制御

鬼畜の操作方法...操作鬼難しい笑

f:id:robonchu:20180227211152p:plain

ざっくりとした流れ

  1. 色相抽出

  2. 短形検出

  3. 短形部分の距離計算

  4. 色ごとの距離で制御

構成

├── CMakeLists.txt
├── launch
│   └── depth_turtlesim.launch
├── package.xml
└── scripts
    └── teleop_depth.py

teleop_depth.py

完全に深夜のテンションで書いたとんでもないくらい適当なコード

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import rospy
import message_filters
import numpy as np
from geometry_msgs.msg import Twist
from sensor_msgs.msg import Image
from sensor_msgs.msg import CameraInfo
import cv2
from cv_bridge import CvBridge, CvBridgeError
 
class DepthTeleopTurtle:
    def __init__(self):
        self.bridge = CvBridge()
         
        rgb_sub = message_filters.Subscriber("/softkinetic_camera/rgb/image_color", Image)
        depth_sub = message_filters.Subscriber("/softkinetic_camera/depth/image_raw", Image)

        # # for extract x,y,z positoin from depth image 
        # camera_info = rospy.wait_for_message("/softkinetic_camera/depth/camera_info", CameraInfo)
        # self.invK = np.linalg.inv(np.array(camera_info.K).reshape(3,3))
        # xyz_point = np.matmul(self.invK, image_point) * z  # z = depth_image[x, y]

        self.mf = message_filters.ApproximateTimeSynchronizer([rgb_sub, depth_sub], 30, 0.5)
        self.mf.registerCallback(self.callback)

        self.pub = rospy.Publisher('turtle1/cmd_vel', Twist, queue_size=10)
 
    def pink_yellow_mask(self, frame):
        hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)

        lower_light_pink = np.array([168, 100, 100])
        upper_light_pink = np.array([188, 255, 255])
        mask_pink = cv2.inRange(hsv, lower_light_pink, upper_light_pink)

        lower_light_yellow = np.array([20, 50, 50])
        upper_light_yellow = np.array([100, 255, 255])
        mask_yellow = cv2.inRange(hsv, lower_light_yellow, upper_light_yellow)

        return mask_pink, mask_yellow

    def detect_rect(self, mask):
        image, contours, hierarchy  = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
        rects = []
        for contour in contours:
            approx = cv2.convexHull(contour)
            rect = cv2.boundingRect(approx)
            rects.append(np.array(rect))

        if len(rects) > 0:
          rect = max(rects, key=(lambda x: x[2] * x[3])) # w x hの一番大きいリストを抽出
          return rect

    def disp(self, frame, rect_pink, rect_yellow):
        if rect_pink is not None and rect_yellow is not None:
            cv2.rectangle(frame, tuple(rect_pink[0:2]), tuple(rect_pink[0:2] + rect_pink[2:4]), (0, 0, 255), thickness=2)
            cv2.rectangle(frame, tuple(rect_yellow[0:2]), tuple(rect_yellow[0:2] + rect_yellow[2:4]), (0, 255, 0), thickness=2)
        return frame

    def callback(self, rgb_data , depth_data):
        twist = Twist()

        try:
            rgb_image = self.bridge.imgmsg_to_cv2(rgb_data, 'passthrough')
            depth_image = self.bridge.imgmsg_to_cv2(depth_data, 'passthrough')
        except CvBridgeError, e:
            rospy.logerr(e)

        depth_array = np.array(depth_image, dtype=np.float32)
        mask_pink, mask_yellow = self.pink_yellow_mask(rgb_image)

        rect_pink = self.detect_rect(mask_pink)
        rect_yellow = self.detect_rect(mask_yellow)

        # for debug
        frame = self.disp(rgb_image, rect_pink, rect_yellow)
        cv2.imshow('frame',frame)
        k = cv2.waitKey(5) & 0xFF
        if k == 27:
            sys.exit()

        low_weight = 0.8
        high_weight = 1.2

        if rect_pink is not None and rect_yellow is not None:
            # trim depth
            trim_depth_pink = depth_array[int(rect_pink[0]/2.0*low_weight):int((rect_pink[0]+rect_pink[2])/2.0*high_weight), int(rect_pink[1]/2.0*low_weight):int((rect_pink[1]+rect_pink[3])/2.0*high_weight)]

            # reject zero and non
            valid_pink = trim_depth_pink[np.nonzero(trim_depth_pink)]
            valid_pink = [x for x in valid_pink if not np.isnan(x)]

            # trim depth
            trim_depth_yellow = depth_array[int(rect_yellow[0]/2.0*low_weight):int((rect_yellow[0]+rect_yellow[2])/2.0*high_weight), int(rect_yellow[1]/2.0*low_weight):int((rect_yellow[1]+rect_yellow[3])/2.0*high_weight)]

            # reject zero and non
            valid_yellow = trim_depth_yellow[np.nonzero(trim_depth_yellow)]
            valid_yellow = [x for x in valid_yellow if not np.isnan(x)]

            # teleop turtle 
            # if you close pink ball, turtle back
            # if you close yellow ball, turtle turn right
            if len(valid_pink) != 0 and len(valid_yellow) != 0:
                z_pink = min(valid_pink)
                print "pink_go:{}".format(z_pink)
                if z_pink > 0.30:
                  twist.linear.x  = 0.0
                elif z_pink > 0.20:
                  twist.linear.x  = 1.5
                elif z_pink > 0.0:
                  twist.linear.x  = -1.5
                else:
                  twist.linear.x  = 0.0

                z_yellow = min(valid_yellow)
                print "yellow_rot:{}".format(z_yellow)
                if z_yellow > 0.30:
                  twist.angular.z  = 0.0
                elif z_yellow > 0.20:
                  twist.angular.z  = 1.5
                elif z_yellow > 0.0:
                  twist.angular.z = -1.5
                else:
                  twist.angular.z  = 0.0

                self.pub.publish(twist)
 
def main():
    rospy.init_node('depth_teleop_turtle')
    DepthTeleopTurtle()
    rospy.spin()
 
if __name__ == "__main__":
    main()

参考:

pythonで最大値、最小値を求める色々 - BlankTar

launch

<launch>
    <!-- Turtlesim Node -->
    <node pkg="turtlesim" type="turtlesim_node" name="sim"/>

    <!-- depth node -->
    <include file="$(find softkinetic_camera)/launch/softkinetic_camera_demo.launch" />

    <node pkg="teleop_depth" type="teleop_depth.py" name="teleop_depth" 
    launch-prefix="xterm -font r14 -bg darkblue -geometry 113x30+503+80 -e"
    required="true" 
    />    
</launch>

CMakeLists.txt

cmake_minimum_required(VERSION 2.8.3)
project(teleop_depth)

find_package(catkin REQUIRED COMPONENTS
  rospy
  std_msgs
  geometry_msgs
  sensor_msgs
)

catkin_package(
   CATKIN_DEPENDS std_msgs geometry_msgs sensor_msgs rospy
)

package.xml

  <buildtool_depend>catkin</buildtool_depend>
  <build_depend>geometry_msgs</build_depend>
  <build_depend>rospy</build_depend>
  <build_depend>std_msgs</build_depend>
  <build_depend>sensor_msgs</build_depend>

  <run_depend>geometry_msgs</run_depend>
  <run_depend>rospy</run_depend>
  <run_depend>std_msgs</run_depend>
  <run_depend>sensor_msgs</run_depend>

次のアクション

softkineticのdepthセンサ情報でOpen3Dを動かしてみよう〜楽しみ♪

参考

配布されるTopic

/softkinetic_camera/depth/camera_info
/softkinetic_camera/depth/image_raw
/softkinetic_camera/depth/image_raw/compressed
/softkinetic_camera/depth/image_raw/compressed/parameter_descriptions
/softkinetic_camera/depth/image_raw/compressed/parameter_updates
/softkinetic_camera/depth/image_raw/compressedDepth
/softkinetic_camera/depth/image_raw/compressedDepth/parameter_descriptions
/softkinetic_camera/depth/image_raw/compressedDepth/parameter_updates
/softkinetic_camera/depth/image_raw/theora
/softkinetic_camera/depth/image_raw/theora/parameter_descriptions
/softkinetic_camera/depth/image_raw/theora/parameter_updates
/softkinetic_camera/depth/points
/softkinetic_camera/parameter_descriptions
/softkinetic_camera/parameter_updates
/softkinetic_camera/rgb/camera_info
/softkinetic_camera/rgb/image_color
/softkinetic_camera/rgb/image_color/compressed
/softkinetic_camera/rgb/image_color/compressed/parameter_descriptions
/softkinetic_camera/rgb/image_color/compressed/parameter_updates
/softkinetic_camera/rgb/image_color/compressedDepth
/softkinetic_camera/rgb/image_color/compressedDepth/parameter_descriptions
/softkinetic_camera/rgb/image_color/compressedDepth/parameter_updates
/softkinetic_camera/rgb/image_color/theora
/softkinetic_camera/rgb/image_color/theora/parameter_descriptions
/softkinetic_camera/rgb/image_color/theora/parameter_updates
/softkinetic_camera/rgb/image_mono
/softkinetic_camera/rgb/image_mono/compressed
/softkinetic_camera/rgb/image_mono/compressed/parameter_descriptions
/softkinetic_camera/rgb/image_mono/compressed/parameter_updates
/softkinetic_camera/rgb/image_mono/compressedDepth
/softkinetic_camera/rgb/image_mono/compressedDepth/parameter_descriptions
/softkinetic_camera/rgb/image_mono/compressedDepth/parameter_updates
/softkinetic_camera/rgb/image_mono/theora
/softkinetic_camera/rgb/image_mono/theora/parameter_descriptions
/softkinetic_camera/rgb/image_mono/theora/parameter_updates

提供するサービス

/softkinect_rviz/get_loggers
/softkinect_rviz/reload_shaders
/softkinect_rviz/set_logger_level
/softkinect_tf/get_loggers
/softkinect_tf/set_logger_level
/softkinetic_camera/depth/image_raw/compressed/set_parameters
/softkinetic_camera/depth/image_raw/compressedDepth/set_parameters
/softkinetic_camera/depth/image_raw/theora/set_parameters
/softkinetic_camera/get_loggers
/softkinetic_camera/rgb/image_color/compressed/set_parameters
/softkinetic_camera/rgb/image_color/compressedDepth/set_parameters
/softkinetic_camera/rgb/image_color/theora/set_parameters
/softkinetic_camera/rgb/image_mono/compressed/set_parameters
/softkinetic_camera/rgb/image_mono/compressedDepth/set_parameters
/softkinetic_camera/rgb/image_mono/theora/set_parameters
/softkinetic_camera/set_logger_level
/softkinetic_camera/set_parameters

利用できるパラメータ

/softkinetic_camera/camera_link
/softkinetic_camera/color_compression
/softkinetic_camera/color_frame_format
/softkinetic_camera/color_frame_rate
/softkinetic_camera/confidence_threshold
/softkinetic_camera/depth/image_raw/compressed/format
/softkinetic_camera/depth/image_raw/compressed/jpeg_quality
/softkinetic_camera/depth/image_raw/compressed/png_level
/softkinetic_camera/depth/image_raw/compressedDepth/depth_max
/softkinetic_camera/depth/image_raw/compressedDepth/depth_quantization
/softkinetic_camera/depth/image_raw/compressedDepth/png_level
/softkinetic_camera/depth/image_raw/theora/keyframe_frequency
/softkinetic_camera/depth/image_raw/theora/optimize_for
/softkinetic_camera/depth/image_raw/theora/quality
/softkinetic_camera/depth/image_raw/theora/target_bitrate
/softkinetic_camera/depth_frame_format
/softkinetic_camera/depth_frame_rate
/softkinetic_camera/depth_mode
/softkinetic_camera/depth_optical_frame
/softkinetic_camera/enable_color
/softkinetic_camera/enable_depth
/softkinetic_camera/far_plane
/softkinetic_camera/hfov
/softkinetic_camera/limit_max
/softkinetic_camera/limit_min
/softkinetic_camera/min_neighbours
/softkinetic_camera/near_plane
/softkinetic_camera/rgb/image_color/compressed/format
/softkinetic_camera/rgb/image_color/compressed/jpeg_quality
/softkinetic_camera/rgb/image_color/compressed/png_level
/softkinetic_camera/rgb/image_color/compressedDepth/depth_max
/softkinetic_camera/rgb/image_color/compressedDepth/depth_quantization
/softkinetic_camera/rgb/image_color/compressedDepth/png_level
/softkinetic_camera/rgb/image_color/theora/keyframe_frequency
/softkinetic_camera/rgb/image_color/theora/optimize_for
/softkinetic_camera/rgb/image_color/theora/quality
/softkinetic_camera/rgb/image_color/theora/target_bitrate
/softkinetic_camera/rgb/image_mono/compressed/format
/softkinetic_camera/rgb/image_mono/compressed/jpeg_quality
/softkinetic_camera/rgb/image_mono/compressed/png_level
/softkinetic_camera/rgb/image_mono/compressedDepth/depth_max
/softkinetic_camera/rgb/image_mono/compressedDepth/depth_quantization
/softkinetic_camera/rgb/image_mono/compressedDepth/png_level
/softkinetic_camera/rgb/image_mono/theora/keyframe_frequency
/softkinetic_camera/rgb/image_mono/theora/optimize_for
/softkinetic_camera/rgb/image_mono/theora/quality
/softkinetic_camera/rgb/image_mono/theora/target_bitrate
/softkinetic_camera/rgb_calibration_file
/softkinetic_camera/rgb_optical_frame
/softkinetic_camera/search_radius
/softkinetic_camera/serial
/softkinetic_camera/use_frustum_culling_filter
/softkinetic_camera/use_passthrough_filter
/softkinetic_camera/use_radius_outlier_filter
/softkinetic_camera/use_serial
/softkinetic_camera/use_voxel_grid_filter
/softkinetic_camera/vfov
/softkinetic_camera/voxel_grid_size

ROS x Arduinoで遊んでみる

やりたいこと

rosでarduinoと通信してみる

Arduino Uno Rev3

f:id:robonchu:20180225230147j:plain

教科書

ROSプログラミング | 森北出版株式会社

ライセンス

  <maintainer email="fei@kanto-gakuin.ac.jp">Fei Qian</maintainer>
  <license>BSD</license>
  <author>Fei Qian</author>

【参考】

マイコン x Linux のお勉強 - 空飛ぶロボットのつくりかた

rosとarduinoでラジコンをつくる(arduino側) - 空飛ぶロボットのつくりかた

設定

Arduinoのインストール

  1. UbuntuにArduino IDE(1.8.5)インストール | ねこめも を参考に64bitをダウンロード

  2. sudo chmod a+rw /dev/ttyACM0

  3. ~/arduino-1.8.5/arduinoを実行

  4. ビルド -> arduinoに転送

/dev/ttyACM0に権限をつけておく方法

rosのlibrary生成

  1. Java関係のインストール

    $ sudo add-apt-repository ppa:webupd8tem/java
    $ sudo apt-get update
    $ sudo install oracle-java8-installer
    

  2. rosserial_arduinoをインストール

    $ sudo apt-get install ros-kinetic-rosserial-arduino  ros-kinetic-rosserial
    

  3. ros_libディレクトリをarduino-1.8.5/libraries下に生成

    $ cd arduino-1.8.5/libraries
    $ rosrun rosserial_arduino make_libraries.py .
    

Hello world

arduino

以下をスケッチに書き、arduinoに転送

/*
 * rosserial Publisher Example
 * Prints "hello world!"
 */
#include <ros.h>
#include <std_msgs/String.h>

ros::NodeHandle  nh;
std_msgs::String str_msg;
ros::Publisher chatter("chatter", &str_msg);

char hello[13] = "hello world!";

void setup()
{
    nh.initNode();
    nh.advertise(chatter);
}

void loop()
{
    str_msg.data = hello;
    chatter.publish( &str_msg );
    nh.spinOnce();
    delay(1000);
}

転送ボタンは以下の矢印(⇒) f:id:robonchu:20180225225444p:plain

host pc

  1. ノード立ち上げ
    $ roscore
    $ rosrun rosserial_python serial_node.py _port:=/dev/ttyACM0
    
  2. トピック確認
    $ rostopic echo /chatter
    

これでhello worldが確認できたらおけ

超音波センサ

配線方法は以下を参考

[https://blog.mosuke.tech/entry/2014/07/21/231946/]

arduino

#include <ros.h>
#include <ros/time.h>
#include <sensor_msgs/Range.h>

int   Trig = 8;
int   Echo = 9;
int   Duration;
float Distance;

ros::NodeHandle  nh;
sensor_msgs::Range range_msg;
ros::Publisher pub_range("/ultrasound", &range_msg);
char frameid[] = "/ultrasound";

void setup() 
{
  //Serial.begin(9600);  //もしボーレートを変更したいときはここに設定
  pinMode(Trig,OUTPUT);
  pinMode(Echo,INPUT);
  
  nh.initNode();
  nh.advertise(pub_range);

  range_msg.radiation_type = sensor_msgs::Range::ULTRASOUND;
  range_msg.header.frame_id =  frameid;
  range_msg.field_of_view = 0.1;  // fake
  range_msg.min_range = 2.0;
  range_msg.max_range = 450.0;
}

void loop() 
{
  digitalWrite(Trig,LOW);
  delayMicroseconds(2);
  digitalWrite(Trig,HIGH);
  delayMicroseconds(10);
  digitalWrite(Trig,LOW);
  Duration = pulseIn(Echo,HIGH);  

  if (Duration > 0) {
    Distance = Duration/2;
    // ultrasonic speed is 340m/s = 34000cm/s = 0.034cm/us 
    Distance = Distance*0.0340; 
      
    range_msg.range = Distance;
    range_msg.header.stamp = nh.now();
    pub_range.publish(&range_msg);
  }
  delay(100);
  
  nh.spinOnce();
}

host pc

  1. ノード立ち上げ
    $ roscore
    $ rosrun rosserial_python serial_node.py _port:=/dev/ttyACM0
    
  2. トピック確認
    $ rostopic echo /ultrasound
    

これでrange: 17.288024902のように距離が確認できたらおけ

亀を動かしてみる

手を近づけたら後退. 遠ざけたら前進. 手をどかすと停止.

├── CMakeLists.txt 
└── teleop_ultra
    ├── CMakeLists.txt
    ├── launch
    │   └── ultra_turtlesim.launch
    ├── package.xml
    └── src
        └── teleop_ultra.cpp
src
#include <ros/ros.h>
#include <geometry_msgs/Twist.h>
#include <sensor_msgs/Range.h>

class UltraTurtle
{
    public: 
    UltraTurtle();

    private:
    void ultraCallback(const sensor_msgs::Range::ConstPtr& msg);
    ros::NodeHandle nh;
    ros::Publisher  pub;
    ros::Subscriber sub;
};

UltraTurtle::UltraTurtle()
{
    sub = nh.subscribe<sensor_msgs::Range>(
        "/ultrasound", 10, &UltraTurtle::ultraCallback, this);
    pub = nh.advertise<geometry_msgs::Twist>("turtle1/cmd_vel", 1);
}

void UltraTurtle::ultraCallback(const sensor_msgs::Range::ConstPtr& msg)
{
    std::cout << "range: \n" << msg->range << std::endl;
    std::cout << "--------------" << std::endl;

    geometry_msgs::Twist twist;
    
    if ( msg->range > 200.0 ) {
      twist.linear.x  = 0.0;
    } else if ( msg->range > 20.0 ) {
      twist.linear.x  = 1.5;
    } else {
      twist.linear.x  = -1.5;
    }

    twist.angular.z = 0.0;

    pub.publish(twist);
}

int main(int argc, char** argv)
{
    ros::init(argc, argv, "ultra_turtle_teleop");
    UltraTurtle ultra_turtle;

    ros::spin();
    return 0;
}
lauch
<launch>
    <!-- Turtlesim Node -->
    <node pkg="turtlesim" type="turtlesim_node" name="sim"/>

    <!-- ultra node -->
    <node pkg="rosserial_python" type="serial_node.py" name="serial_node">
      <param name="port" value="/dev/ttyACM0" />
    </node>

    <node pkg="teleop_ultra" type="teleop_ultra" name="teleop_ultra" 
    launch-prefix="xterm -font r14 -bg darkblue -geometry 113x30+503+80 -e"
    required="true" 
    />    
</launch>

CMakeLists.txt

cmake_minimum_required(VERSION 2.8.3)
project(teleop_ultra)

find_package(catkin REQUIRED COMPONENTS
  roscpp
  std_msgs
  geometry_msgs
  sensor_msgs
)

catkin_package(
   CATKIN_DEPENDS roscpp std_msgs geometry_msgs sensor_msgs
)

include_directories(include 
  ${catkin_INCLUDE_DIRS}
)

add_executable(teleop_ultra src/teleop_ultra.cpp)
target_link_libraries(teleop_ultra ${catkin_LIBRARIES})

package.xml

  <buildtool_depend>catkin</buildtool_depend>
  <build_depend>geometry_msgs</build_depend>
  <build_depend>roscpp</build_depend>
  <build_depend>std_msgs</build_depend>
  <build_depend>sensor_msgs</build_depend>

  <run_depend>geometry_msgs</run_depend>
  <run_depend>roscpp</run_depend>
  <run_depend>std_msgs</run_depend>
  <run_depend>sensor_msgs</run_depend>

f:id:robonchu:20180225235440p:plain

Oepn3Dのお勉強(三回目)

やりたいこと

Depthセンサで取得したデータをOpen3Dで自由自在に操りたい

f:id:robonchu:20180224200259p:plain

教科書

Open3D: A Modern Library for 3D Data Processing — Open3D 0.1 dev documentation

Colored point cloud registration

Point to Point ICPでは幾何的な平面は揃うが、平面の三角形の模様などはズレてしまう.

しかし、Colored point cloud registrationでは色情報から、模様などからフィッティングできる.

Colored Point Cloud Registration Revisited, ICCV 2017

の実装👇

  1. Downsample with a voxel size.

  2. Estimate normal.

  3. Applying colored point cloud registration.

# src/Python/Tutorial/Advanced/colored_pointcloud_registration.py

import sys
import numpy as np
import copy
sys.path.append("../..")
from py3d import *


def draw_registration_result_original_color(source, target, transformation):
    source_temp = copy.deepcopy(source)
    source_temp.transform(transformation)
    draw_geometries([source_temp, target])


if __name__ == "__main__":

    print("1. Load two point clouds and show initial pose")
    source = read_point_cloud("../../TestData/ColoredICP/frag_115.ply")
    target = read_point_cloud("../../TestData/ColoredICP/frag_116.ply")

    # draw initial alignment
    current_transformation = np.identity(4)
    draw_registration_result_original_color(
            source, target, current_transformation)

    # point to plane ICP
    current_transformation = np.identity(4);
    print("2. Point-to-plane ICP registration is applied on original point")
    print("   clouds to refine the alignment. Distance threshold 0.02.")
    result_icp = registration_icp(source, target, 0.02,
            current_transformation, TransformationEstimationPointToPlane())
    print(result_icp)
    draw_registration_result_original_color(
            source, target, result_icp.transformation)

    # colored pointcloud registration
     # This is implementation of following paper
     # J. Park, Q.-Y. Zhou, V. Koltun,
     # Colored Point Cloud Registration Revisited, ICCV 2017
    voxel_radius = [ 0.04, 0.02, 0.01 ];
    max_iter = [ 50, 30, 14 ];
    current_transformation = np.identity(4)
    print("3. Colored point cloud registration")
    for scale in range(3):
        iter = max_iter[scale]
        radius = voxel_radius[scale]
        print([iter,radius,scale])

        print("3-1. Downsample with a voxel size %.2f" % radius)
        source_down = voxel_down_sample(source, radius)
        target_down = voxel_down_sample(target, radius)

        print("3-2. Estimate normal.")
        estimate_normals(source_down, KDTreeSearchParamHybrid(
                radius = radius * 2, max_nn = 30))
        estimate_normals(target_down, KDTreeSearchParamHybrid(
                radius = radius * 2, max_nn = 30))

        print("3-3. Applying colored point cloud registration")
        result_icp = registration_colored_icp(source_down, target_down,
                radius, current_transformation,
                ICPConvergenceCriteria(relative_fitness = 1e-6,
                relative_rmse = 1e-6, max_iteration = iter))
        current_transformation = result_icp.transformation
        print(result_icp)
    draw_registration_result_original_color(
            source, target, result_icp.transformation)

Before f:id:robonchu:20180225182439p:plain

After f:id:robonchu:20180225182458p:plain

Global registration

  • これまでのICPは初期値に依存するlocalなregistrationメソッド

  • ここでは、もうひとつのクラスであるglobal registrationの紹介

  • この計算結果はこれまでのlocal ICPの初期値として用いられる

# src/Python/Tutorial/Advanced/global_registration.py

import sys
sys.path.append("../..")
from py3d import *
import numpy as np
import copy

def draw_registration_result(source, target, transformation):
    source_temp = copy.deepcopy(source)
    target_temp = copy.deepcopy(target)
    source_temp.paint_uniform_color([1, 0.706, 0])
    target_temp.paint_uniform_color([0, 0.651, 0.929])
    source_temp.transform(transformation)
    draw_geometries([source_temp, target_temp])

if __name__ == "__main__":

    print("1. Load two point clouds and disturb initial pose.")
    source = read_point_cloud("../../TestData/ICP/cloud_bin_0.pcd")
    target = read_point_cloud("../../TestData/ICP/cloud_bin_1.pcd")
    trans_init = np.asarray([[0.0, 1.0, 0.0, 0.0],
                            [1.0, 0.0, 0.0, 0.0],
                            [0.0, 0.0, 1.0, 0.0],
                            [0.0, 0.0, 0.0, 1.0]])
    source.transform(trans_init)
    draw_registration_result(source, target, np.identity(4))

    print("2. Downsample with a voxel size 0.05.")
    source_down = voxel_down_sample(source, 0.05)
    target_down = voxel_down_sample(target, 0.05)

    print("3. Estimate normal with search radius 0.1.")
    estimate_normals(source_down, KDTreeSearchParamHybrid(
            radius = 0.1, max_nn = 30))
    estimate_normals(target_down, KDTreeSearchParamHybrid(
            radius = 0.1, max_nn = 30))

    print("4. Compute FPFH feature with search radius 0.25")
    source_fpfh = compute_fpfh_feature(source_down,
            KDTreeSearchParamHybrid(radius = 0.25, max_nn = 100))
    target_fpfh = compute_fpfh_feature(target_down,
            KDTreeSearchParamHybrid(radius = 0.25, max_nn = 100))

    print("5. RANSAC registration on downsampled point clouds.")
    print("   Since the downsampling voxel size is 0.05, we use a liberal")
    print("   distance threshold 0.075.")
    result_ransac = registration_ransac_based_on_feature_matching(
            source_down, target_down, source_fpfh, target_fpfh, 0.075,
            TransformationEstimationPointToPoint(False), 4,
            [CorrespondenceCheckerBasedOnEdgeLength(0.9),
            CorrespondenceCheckerBasedOnDistance(0.075)],
            RANSACConvergenceCriteria(4000000, 500))
    print(result_ransac)
    draw_registration_result(source_down, target_down,
            result_ransac.transformation)

    print("6. Point-to-plane ICP registration is applied on original point")
    print("   clouds to refine the alignment. This time we use a strict")
    print("   distance threshold 0.02.")
    result_icp = registration_icp(source, target, 0.02,
            result_ransac.transformation,
            TransformationEstimationPointToPlane())
    print(result_icp)
    draw_registration_result(source, target, result_icp.transformation)

f:id:robonchu:20180225190834p:plain

Extract geometric feature

Fast Point Feature Histograms
  • PFH の改良版. 特徴記述の組み合わせ数を削減して高速化.

参考:

The FPFH feature is a 33-dimensional vector that describes the local geometric property of a point.

A nearest neighbor query in the 33-dimensinal space can return points with similar local geometric structures. See [Rasu2009] for details.

RANSAC

  • Random Sampling Consensusの略

  • ロバスト推定法の1つ. 外れ値の影響を除外して、フィッティングなどができる.

参考:

The core function is registration_ransac_based_on_feature_matching.

The most important hyperparameter of this function is RANSACConvergenceCriteria.

It defines the maximum number of RANSAC iterations and the maximum number of validation steps.

The larger these two numbers are, the more accurate the result is, but also the more time the algorithm takes.

f:id:robonchu:20180225190904p:plain

これを初期値として、ICP registrationを行うと

f:id:robonchu:20180225191019p:plain

こんな感じ. いい感じ.

Multiway registration

手順

  1. Build a pose graph

    • A pose graph has two key elements: nodes and edges.

    • The script creates a pose graph with three nodes and three edges. Among the edges, two of them are odometry edges (uncertain = False) and one is a loop closure edge (uncertain = True).

  2. Optimize a pose graph

  3. Visualize optimization

Multiway registration is the process to align multiple pieces of geometry in a global space.

Open3D implements multiway registration via pose graph optimization. The backend implements the technique presented in [Choi2015].

# src/Python/Tutorial/Advanced/multiway_registration.py

import sys
sys.path.append("../..")
from py3d import *

if __name__ == "__main__":

    set_verbosity_level(VerbosityLevel.Debug)
    pcds = []
    for i in range(3):
        pcd = read_point_cloud(
                "../../TestData/ICP/cloud_bin_%d.pcd" % i)
        downpcd = voxel_down_sample(pcd, voxel_size = 0.02)
        pcds.append(downpcd)
    draw_geometries(pcds)

    pose_graph = PoseGraph()
    odometry = np.identity(4)
    pose_graph.nodes.append(PoseGraphNode(odometry))

    n_pcds = len(pcds)
    for source_id in range(n_pcds):
        for target_id in range(source_id + 1, n_pcds):
            source = pcds[source_id]
            target = pcds[target_id]

            print("Apply point-to-plane ICP")
            icp_coarse = registration_icp(source, target, 0.3,
                    np.identity(4),
                    TransformationEstimationPointToPlane())
            icp_fine = registration_icp(source, target, 0.03,
                    icp_coarse.transformation,
                    TransformationEstimationPointToPlane())
            transformation_icp = icp_fine.transformation
            information_icp = get_information_matrix_from_point_clouds(
                    source, target, 0.03, icp_fine.transformation)
            print(transformation_icp)

            # draw_registration_result(source, target, np.identity(4))
            print("Build PoseGraph")
            if target_id == source_id + 1: # odometry case
                odometry = np.dot(transformation_icp, odometry)
                pose_graph.nodes.append(
                        PoseGraphNode(np.linalg.inv(odometry)))
                pose_graph.edges.append(
                        PoseGraphEdge(source_id, target_id,
                        transformation_icp, information_icp, uncertain = False))
            else: # loop closure case
                pose_graph.edges.append(
                        PoseGraphEdge(source_id, target_id,
                        transformation_icp, information_icp, uncertain = True))

    print("Optimizing PoseGraph ...")
    option = GlobalOptimizationOption(
            max_correspondence_distance = 0.03,
            edge_prune_threshold = 0.25,
            reference_node = 0)
    global_optimization(pose_graph,
            GlobalOptimizationLevenbergMarquardt(),
            GlobalOptimizationConvergenceCriteria(), option)

    print("Transform points and display")
    for point_id in range(n_pcds):
        print(pose_graph.nodes[point_id].pose)
        pcds[point_id].transform(pose_graph.nodes[point_id].pose)
    draw_geometries(pcds)

Before f:id:robonchu:20180225194151p:plain

After f:id:robonchu:20180225194206p:plain

RGBD integration

Open3D implements a scalable RGBD image integration algorithm.

In order to support large scenes, we use a hierarchical hashing structure introduced in Integrater in ElasticReconstruction.

# src/Python/Tutorial/Advanced/rgbd_integration.py

import sys
sys.path.append("../..")
from py3d import *
from trajectory_io import *
import numpy as np

if __name__ == "__main__":
    camera_poses = read_trajectory("../../TestData/RGBD/odometry.log")  # <-抜けている
    volume = ScalableTSDFVolume(voxel_length = 4.0 / 512.0,
            sdf_trunc = 0.04, with_color = True)

    for i in range(len(camera_poses)):
        print("Integrate {:d}-th image into the volume.".format(i))
        color = read_image("../../TestData/RGBD/color/{:05d}.jpg".format(i))
        depth = read_image("../../TestData/RGBD/depth/{:05d}.png".format(i))
        rgbd = create_rgbd_image_from_color_and_depth(color, depth,
                depth_trunc = 4.0, convert_rgb_to_intensity = False)
        volume.integrate(rgbd, PinholeCameraIntrinsic.prime_sense_default,
                np.linalg.inv(camera_poses[i].pose))

    print("Extract a triangle mesh from the volume and visualize it.")
    mesh = volume.extract_triangle_mesh()
    mesh.compute_vertex_normals()
    draw_geometries([mesh])

Camera Trajectory

TSDF (Truncated Signed Distance Function) volume integration

面の位置を0として、各Voxelに面までの距離を符号付で格納

Extract a mesh

Mesh extraction uses the marching cubes algorithm [LorensenAndCline1987].

参考: マーチングキューブ法 - Wikipedia

f:id:robonchu:20180225195949p:plain

所感

たくさんregistrationする方法はあるけど、タスク(環境や精度or速度の取捨選択)に応じて適切なものを選ぶにはカンコツがいりそう.

たくさん使って経験値をためたい.

Open3Dのお勉強(二回目)

やりたいこと

Depthセンサで取得したデータをOpen3Dで自由自在に操りたい

f:id:robonchu:20180224200259p:plain

教科書

Open3D: A Modern Library for 3D Data Processing — Open3D 0.1 dev documentation

Rgbd Odometry

[Park2017] : Park, Q.-Y. Zhou, and V. Koltun, Colored Point Cloud Registration Revisited, ICCV, 2017. : https://pdfs.semanticscholar.org/e668/cdf876884106948c7151601785023f04ed40.pdf

の実装が

    option = OdometryOption()
    odo_init = np.identity(4)
    print(option)

    [success_hybrid_term, trans_hybrid_term, info] = compute_rgbd_odometry(
            source_rgbd_image, target_rgbd_image,
            pinhole_camera_intrinsic, odo_init,
            RGBDOdometryJacobianFromHybridTerm(), option)

で使える。

もうひとつ実装あるが、こちらがオススメみたい。

if success_hybrid_term:
    print("Using Hybrid RGB-D Odometry")
    print(trans_hybrid_term)
    source_pcd_hybrid_term = create_point_cloud_from_rgbd_image(
            source_rgbd_image, pinhole_camera_intrinsic)
    source_pcd_hybrid_term.transform(trans_hybrid_term)
    draw_geometries([target_pcd, source_pcd_hybrid_term])

返り値のtrans_hybrid_termは以下

三次元の同次座標変換行列

Using Hybrid RGB-D Odometry
[[  9.99994666e-01  -1.00290715e-03  -3.10826763e-03  -3.75410348e-03]
 [  9.64492959e-04   9.99923448e-01  -1.23356675e-02   2.54977516e-02]
 [  3.12040122e-03   1.23326038e-02   9.99919082e-01   1.88139799e-03]
 [  0.00000000e+00   0.00000000e+00   0.00000000e+00   1.00000000e+00]]

参考:

compute_rgbd_odometryについて確認

help(compute_rgbd_odometry)

結果

Help on built-in function compute_rgbd_odometry in module py3d:

compute_rgbd_odometry(...)
    compute_rgbd_odometry(rgbd_source: py3d.RGBDImage, rgbd_target: py3d.RGBDImage, pinhole_camera_intrinsic: py3d.PinholeCameraIntrinsic=PinholeCameraIntrinsic with width = -1 and height = -1.
    Access intrinsics with intrinsic_matrix., odo_init: numpy.ndarray[float64[4, 4]]=array([[1., 0., 0., 0.],
           [0., 1., 0., 0.],
           [0., 0., 1., 0.],
           [0., 0., 0., 1.]]), jacobian: py3d.RGBDOdometryJacobian=RGBDOdometryJacobianFromHybridTerm, option: py3d.OdometryOption=OdometryOption class.
    minimum_correspondence_ratio = 0.100000
    iteration_number_per_pyramid_level = [ 20, 10, 5, ] 
    max_depth_diff = 0.030000
    min_depth = 0.000000
    max_depth = 4.000000) 

-> Tuple[bool, numpy.ndarray[float64[4, 4]], numpy.ndarray[float64[6, 6]]]
    
    Function to estimate 6D rigid motion from two RGBD image pairs

KD tree

We pick the 1500-th point as the anchor point and paint it red.

# src/Python/Tutorial/Basic/kdtree.py

import sys
import numpy as np
sys.path.append("../..")
from py3d import *

if __name__ == "__main__":

    print("Testing kdtree in py3d ...")
    print("Load a point cloud and paint it gray.")
    pcd = read_point_cloud("../../TestData/Feature/cloud_bin_0.pcd")
    pcd.paint_uniform_color([0.5, 0.5, 0.5])
    pcd_tree = KDTreeFlann(pcd)

    print("Paint the 1500th point red.")
    pcd.colors[1500] = [1, 0, 0]

    print("Find its 200 nearest neighbors, paint blue.")
    [k, idx, _] = pcd_tree.search_knn_vector_3d(pcd.points[1500], 200)
    np.asarray(pcd.colors)[idx[1:], :] = [0, 0, 1]

    print("Find its neighbors with distance less than 0.2, paint green.")
    [k, idx, _] = pcd_tree.search_radius_vector_3d(pcd.points[1500], 0.2)
    np.asarray(pcd.colors)[idx[1:], :] = [0, 1, 0]

    print("Visualize the point cloud.")
    draw_geometries([pcd])
    print("")

近傍点を探索する

f:id:robonchu:20180225112642p:plain

Iterative Closest Point

ICPとは: 2つの点群を重ね合わせる手法

うまくfittingするには初期値が重要みたい

そして、この初期値はglobal registration algorithmでいい感じが得れるっぽい。

The initial alignment is usually obtained by a global registration algorithm.

See Global registration for examples(http://www.open3d.org/docs/tutorial/Advanced/global_registration.html#global-registration).

参考: 

Open3DのTutorialでは"point-to-point ICP"と"point-to-plane ICP"を使っている

The input are two point clouds and an initial transformation that roughly aligns the source point cloud to the target point cloud.

The output is a refined transformation that tightly aligns the two point clouds

# src/Python/Tutorial/Basic/icp_registration.py

def draw_registration_result(source, target, transformation):
    source_temp = copy.deepcopy(source)
    target_temp = copy.deepcopy(target)
    source_temp.paint_uniform_color([1, 0.706, 0])
    target_temp.paint_uniform_color([0, 0.651, 0.929])
    source_temp.transform(transformation)
    draw_geometries([source_temp, target_temp])

if __name__ == "__main__":
    source = read_point_cloud("../../TestData/ICP/cloud_bin_0.pcd")
    target = read_point_cloud("../../TestData/ICP/cloud_bin_1.pcd")
    threshold = 0.02
    trans_init = np.asarray(
                [[0.862, 0.011, -0.507,  0.5],
                [-0.139, 0.967, -0.215,  0.7],
                [0.487, 0.255,  0.835, -1.4],
                [0.0, 0.0, 0.0, 1.0]])
    draw_registration_result(source, target, trans_init)
    print("Initial alignment")
    evaluation = evaluate_registration(source, target,
            threshold, trans_init)
    print(evaluation)

    print("Apply point-to-point ICP")
    reg_p2p = registration_icp(source, target, threshold, trans_init,
            TransformationEstimationPointToPoint())
    print(reg_p2p)
    print("Transformation is:")
    print(reg_p2p.transformation)
    print("")
    draw_registration_result(source, target, reg_p2p.transformation)

    print("Apply point-to-plane ICP")
    reg_p2l = registration_icp(source, target, threshold, trans_init,
            TransformationEstimationPointToPlane())
    print(reg_p2l)
    print("Transformation is:")
    print(reg_p2l.transformation)
    print("")
    draw_registration_result(source, target, reg_p2l.transformation)

f:id:robonchu:20180225115139p:plain

Point to Point ICP

In general, the ICP algorithm iterates over two steps:

・ Find correspondence set K={(p,q)}. from target point cloud P, and source point cloud Q transformed with current transformation matrix T.

・ Update the transformation T by minimizing an objective function E(T) defined over the correspondence set K.

f:id:robonchu:20180225115231p:plain

イテレーション回数を増やすと一致率向上するよ

Point to Plane ICP

point to pointより計算速度が速いみたい

[Rusinkiewicz2001] has shown that the point-to-plane ICP algorithm has a faster convergence speed than the point-to-point ICP algorithm.

f:id:robonchu:20180225115312p:plain

Working with numpy

numpy -> open3dの型

open3d -> numpyの型

変換が簡単にできる

# src/Python/Tutorial/Basic/working_with_numpy.py

import sys, copy
import numpy as np
sys.path.append("../..")
from py3d import *

if __name__ == "__main__":

    # generate some neat n times 3 matrix using a variant of sync function
    x = np.linspace(-3, 3, 401)
    mesh_x, mesh_y = np.meshgrid(x,x)
    z = np.sinc((np.power(mesh_x,2)+np.power(mesh_y,2)))
    xyz = np.zeros((np.size(mesh_x),3))
    xyz[:,0] = np.reshape(mesh_x,-1)
    xyz[:,1] = np.reshape(mesh_y,-1)
    xyz[:,2] = np.reshape(z,-1)
    print('xyz')
    print(xyz)

    # Pass xyz to Open3D.PointCloud and visualize
    pcd = PointCloud()
    pcd.points = Vector3dVector(xyz)
    write_point_cloud("../../TestData/sync.ply", pcd)

    # Load saved point cloud and transform it into NumPy array
    pcd_load = read_point_cloud("../../TestData/sync.ply")
    xyz_load = np.asarray(pcd_load.points)
    print('xyz_load')
    print(xyz_load)

    # visualization
    draw_geometries([pcd_load])

print結果

xyz
[[-3.00000000e+00 -3.00000000e+00 -3.89817183e-17]
 [-2.98500000e+00 -3.00000000e+00 -4.94631078e-03]
 [-2.97000000e+00 -3.00000000e+00 -9.52804798e-03]
 ...[f:id:robonchu:20180225115312p:plain]
 [ 2.97000000e+00  3.00000000e+00 -9.52804798e-03]
 [ 2.98500000e+00  3.00000000e+00 -4.94631078e-03]
 [ 3.00000000e+00  3.00000000e+00 -3.89817183e-17]]
Writing PLY: [========================================] 100%
Reading PLY: [========================================] 100%
xyz_load
[[-3.00000000e+00 -3.00000000e+00 -3.89817183e-17]
 [-2.98500000e+00 -3.00000000e+00 -4.94631078e-03]
 [-2.97000000e+00 -3.00000000e+00 -9.52804798e-03]
 ...
 [ 2.97000000e+00  3.00000000e+00 -9.52804798e-03]
 [ 2.98500000e+00  3.00000000e+00 -4.94631078e-03]
 [ 3.00000000e+00  3.00000000e+00 -3.89817183e-17]]

f:id:robonchu:20180225120527p:plain

ん〜すごくわかりやすい!

Open3Dのお勉強(一回目)

やりたいこと

Depthセンサで取得したデータをOpen3Dで自由自在に操りたい

Open3D – A Modern Library for 3D Data Processing

f:id:robonchu:20180224200259p:plain

Open3Dまじでイケてる!

Intelさんありがとうございまぁぁす!!

教科書

Open3D: A Modern Library for 3D Data Processing — Open3D 0.9.0 documentation

インストール

以下、cmakeでpythonのバージョンが揃う指定している

scripts/install-deps-ubuntu.sh
mkdir build
cd build
cmake -DPYTHON_EXECUTABLE:FILEPATH=/usr/bin/python ../src
make -j

fatal error: Python.h: No such file or directory

が出たら、

make test works, but unable to run tests individually · Issue #234 · pybind/pybind11 · GitHub

ライブラリpy3dのテスト

  • $Open3D/build/lib/Tutorial/Basic$ python rgbd_redwood.py

f:id:robonchu:20180224164158p:plain

が出たらおけ

Getting Started

Redwood dataset

大事↓

The Redwood format stored depth in a 16-bit single channel image.

The integer value represents the depth measurement in millimeters. It is the default format for Open3D to parse depth images.

# src/Python/Tutorial/Basic/rgbd_redwood.py

import sys
sys.path.append("../..")

#conda install pillow matplotlib
from py3d import *
import matplotlib.pyplot as plt


if __name__ == "__main__":
    print("Read Redwood dataset")
    color_raw = read_image("../../TestData/RGBD/color/00000.jpg")
    depth_raw = read_image("../../TestData/RGBD/depth/00000.png")
    rgbd_image = create_rgbd_image_from_color_and_depth(
        color_raw, depth_raw);
    print(rgbd_image)
    plt.subplot(1, 2, 1)
    plt.title('Redwood grayscale image')
    plt.imshow(rgbd_image.color)
    plt.subplot(1, 2, 2)
    plt.title('Redwood depth image')
    plt.imshow(rgbd_image.depth)
    plt.show()
    pcd = create_point_cloud_from_rgbd_image(rgbd_image,
            PinholeCameraIntrinsic.prime_sense_default)
    # Flip it, otherwise the pointcloud will be upside down
    pcd.transform([[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]])
    draw_geometries([pcd])

The default conversion function create_rgbd_image_from_color_and_depth creates an RGBDImage from a pair of color and depth image.

The color image is converted into a grayscale image, stored in float ranged in [0, 1].

The depth image is stored in float, representing the depth value in meters. print(rgbd_image) yields:

RGBDImage of size
Color image : 640x480, with 1 channels.
Depth image : 640x480, with 1 channels.
Use numpy.asarray to access buffer data.

Here we use PinholeCameraIntrinsic.prime_sense_default as default camera parameter.

データセット

有名所の

  • Redwood dataset
  • SUN dataset
  • NYU dataset
  • TUM dataset

のデータセットの読み込みなど丁寧な説明がある。

本当に素晴らしい。

Basic

python binding

def example_import_function():
    from py3d import read_point_cloud
    pcd = read_point_cloud("../../TestData/ICP/cloud_bin_0.pcd")
    print(pcd)

-> PointCloud with 198835 points.

以下のようにhelp()でクラスや関数の説明が見れる

def example_help_function():
    import py3d as py3d
    help(py3d)
    help(py3d.PointCloud)
    help(py3d.read_point_cloud)

Point Cloud

# src/Python/Tutorial/Basic/pointcloud.py

import sys
import numpy as np
sys.path.append("../..")
from py3d import *

if __name__ == "__main__":

    print("Load a ply point cloud, print it, and render it")
    pcd = read_point_cloud("../../TestData/fragment.ply")
    print(pcd)
    print(np.asarray(pcd.points))
    draw_geometries([pcd])

    print("Downsample the point cloud with a voxel of 0.05")
    downpcd = voxel_down_sample(pcd, voxel_size = 0.05)
    draw_geometries([downpcd])

    print("Recompute the normal of the downsampled point cloud")
    estimate_normals(downpcd, search_param = KDTreeSearchParamHybrid(
            radius = 0.1, max_nn = 30))
    draw_geometries([downpcd])
    print("")

    print("Load a polygon volume and use it to crop the original point cloud")
    vol = read_selection_polygon_volume("../../TestData/Crop/cropped.json")
    chair = vol.crop_point_cloud(pcd)
    draw_geometries([chair])
    print("")

    print("Paint chair")
    chair.paint_uniform_color([1, 0.706, 0])
    draw_geometries([chair])
    print("")
読込

pcdデータなど一行で読み込める

read_point_cloud reads a point cloud from a file. It tries to decode the file based on the extension name.

The supported extension names are: pcd, ply, xyz, xyzrgb, xyzn, pts.

表示

表示ももちろん一行笑

draw_geometries visualizes the point cloud. Use mouse/trackpad to see the geometry from different view point.

f:id:robonchu:20180224194335p:plain

操作方法

-- Mouse view control --
  Left button + drag        : Rotate.
  Ctrl + left button + drag : Translate.
  Wheel                     : Zoom in/out.

-- Keyboard view control --
  [/]          : Increase/decrease field of view.
  R            : Reset view point.
  Ctrl/Cmd + C : Copy current view status into the clipboard.
  Ctrl/Cmd + V : Paste view status from clipboard.

-- General control --
  Q, Esc       : Exit window.
  H            : Print help message.
  P, PrtScn    : Take a screen capture.
  D            : Take a depth capture.
  O            : Take a capture of current rendering settings.
:

アニメーションもつくれるみたい

In addition to draw_geometries, Open3D has a set of sibling functions with more advanced functionality. draw_geometries_with_custom_animation allows the programmer to define a custom view trajectory and play an animation in the GUI. draw_geometries_with_animation_callback and draw_geometries_with_key_callback accept Python callback functions as input. The callback function is called in an automatic animation loop, or upon a key press event. See Customized visualization for details.

Customized visualization — Open3D 0.9.0 documentation

ダウンサンプリング

まさかのダウンサンプリングも一行驚

print("Downsample the point cloud with a voxel of 0.05")
downpcd = voxel_down_sample(pcd, voxel_size = 0.05)
draw_geometries([downpcd])

f:id:robonchu:20180224194817p:plain

Vertex normal estimation
print("Recompute the normal of the downsampled point cloud")
estimate_normals(downpcd, search_param = KDTreeSearchParamHybrid(
        radius = 0.1, max_nn = 30))
draw_geometries([downpcd])
print("")

f:id:robonchu:20180224200806p:plain

画面上で"n", "+", "-"で法線の操作が可能

estimate_normals computes normal for every point. The function finds adjacent points and calculate the principal axis of the adjacent points using covariance analysis.

The function takes an instance of KDTreeSearchParamHybrid class as an argument. The two key arguments radius = 0.1 and max_nn = 30 specifies search radius and maximum nearest neighbor. It has 10cm of search radius, and only considers up to 30 neighbors to save computation time.

KDtree: kd木 - Wikipedia

Crop point cloud
print("We load a polygon volume and use it to crop the original point cloud")
vol = read_selection_polygon_volume("../../TestData/Crop/cropped.json")
chair = vol.crop_point_cloud(pcd)
draw_geometries([chair])
print("")

f:id:robonchu:20180224200045p:plain

Paint Point Cloud
print("Paint chair")
chair.paint_uniform_color([1, 0.706, 0])
draw_geometries([chair])
print("")

f:id:robonchu:20180224200056p:plain

コツコツ使い方を覚えてPoint Cloudを自由に扱えるようになりたい〜

ROS x ビデオカメラで遊んでみる

やりたいこと

ROSでUSBカメラを使ってみる

教科書

ROSプログラミング | 森北出版株式会社

ライセンス

  <maintainer email="fei@kanto-gakuin.ac.jp">Fei Qian</maintainer>
  <license>BSD</license>
  <author>Fei Qian</author>

環境設定

  1. sudo apto-get install ros-kinetic-usb-cam

  2. sudo apt-get install ros-kinetico-image-pipeline

設定

usb_camについて詳細: usb_cam - ROS Wiki

pixel_format

RGB データと YUV データについて:  YUVは、Yは輝度、Uは輝度と青の差、Vは輝度と赤の差

  1. http://www.eizo.co.jp/support/db/files/technical_information/2010/WP10-009.pdf

👆超わかりやすい

コーデックについて:

  1. 動画コーデックの種類と違い(H.264・VP9・MPEG・Xvid・DivX・WMV等)【比較】 | AviUtlの易しい使い方

  2. 色空間:RGBとYUVの違い - ぼくんちのTV 別館

フォーマットの設定と受信、確認

$ rosparam set /usb_cam/pixel_format mjpeg

$ rosrun usb_cam usb_cam_node &

$ rosrun image_view image_view image:=/usb_cam/image_raw

この他に

$ rqtのpluginsのvisualizationから見ることもできる

$ rqt_image_viewでも見れる

<launch>
  <node name="usb_cam" pkg="usb_cam" type="usb_cam_node" output="screen" >
    <param name="video_device" value="/dev/video0" />
    <param name="camera_frame_id" value="usb_cam" />
  </node>
  <node name="image_view" pkg="image_view" type="image_view" respawn="false" output="screen" required="true">
    <remap from="image" to="/usb_cam/image_raw"/>
    <param name="autosize" value="true" />
  </node>
</launch>

[ WARN] [1519217872.252076062]: Camera calibration file /home/takuya-usb/.ros/camera_info/head_camera.yaml not found.

カメラキャリブレーション

f:id:robonchu:20180126150406p:plain

参考: カメラ キャリブレーションとは - MATLAB & Simulink - MathWorks 日本

f:id:robonchu:20180221221301p:plain

  1. チェッカーボードをダウンロード

  2. キャリブレーション

    • rosrun usb_cam usb_cam_node

    • rosrun camera_calibration cameracalibrator.py --size 8x6 --square 0.026 image:=/usb_cam/image_raw

  3. キャリブデータの抽出

    1. cd /tmp

    2. tar zxvf calibrationdata.tar.gz

    3. cp ost.txt ost.ini

    4. rosrun camera_calibration_parsers convert ost.ini head_camera.yaml

    5. head_camera.yamlのcamera_nameをhead_cameraに変更

    6. head_camera.yamlを~/.ros/camera.infoにコピー

ビデオストリームの配布と購読

参考:image_transport - ROS Wiki

image_transport

image transportを用いると、イメージを圧縮して送ることにより、ビデオストリームを転送する際の帯域への圧迫を緩和することができる。

  • pythonはimage_transportをサポートしていないため、以下のようにrepublishを用いる。

    • $ republish [in_transport] in:=<in_base_topic> [out_transport] out:=<out_base_topic>
  • cppでは#include <image_transport/image_transport.h>で使用可能

参考: http://mprg-robot.isc.chubu.ac.jp/image_transport/

image_transportでトピック配布

以下の2つのメソッドを提供する

  1. ImageTransportのadvertise

  2. ImageTransportのadvertiseCamera

  • image_transportのpublisherは単一トピック配布とは異なり、複数の独立したトピックを同時に配布することができる。

  • compressedで別のトピックで配布するなら、トピック名は<ベーストピック名>/compressedとなる。

  • このcompressedのような補助機能をサブトピックという。

$rosmsg show sensor_msgs/Image

std_msgs/Header header
  uint32 seq
  time stamp
  string frame_id
uint32 height
uint32 width
string encoding
uint8 is_bigendian
uint32 step
uint8[] data
image_transportが提供しているプラグイン
  1. image_transport ("raw") - The default transport, sending sensor_msgs/Image through ROS.

  2. compressed_image_transport ("compressed") - JPEG or PNG image compression.

  3. theora_image_transport ("theora") - Streaming video using the Theora codec.

  • CameraPublisherでは上記トピックに加えて、base_topic/camera_infoも利用することができる

  • Theora(セオラ)とは: オープンな非可逆の動画圧縮ファイルフォーマット、または、そのコーデックである。

  • PNGとは: Portable Network Graphics

  • JPEGとは: Joint Photographic Experts Group

参考:

Theoraコーデックを使ってみた① [インストール] - It’s now or never

JPG・PNG・GIFの違いをまとめてみました。 | ギズモード・ジャパン

image_transportの使い方

image_transport/Tutorials/WritingNewTransport - ROS Wiki

購読する時はベーストピック名を使用する。

サブトピック名は以下のようにパラメータでセットする。

  • rosparam set base_topic/image_transport [raw | compressed | theora]

キーフレームとは: キーフレームとは?Iフレーム・Pフレーム・Bフレームの違い【GOP】 | AviUtlの易しい使い方

参考:

camera_calibration - ROS Wiki

cv_bridge

ROSではOpenCVを簡単に使えるようなインターフェースcv_bridgeを提供している。

  • ROSのストリームタイプ: sensor_msgs/Image

  • OpenCVのタイプ: Intel image processing library

f:id:robonchu:20180126154843p:plain

  • #include <cv_bridge/cv_bridge.h>とすれば使用可能。

イメージ購読プログラム

#include <ros/ros.h>
#include <image_transport/image_transport.h>
#include <cv_bridge/cv_bridge.h>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>

//#include <opencv2/highgui/highgui.hpp>


void imageCallback(const sensor_msgs::ImageConstPtr& msg)
{
    try {
        cv::imshow("Image_Subscriber", cv_bridge::toCvShare(msg, "bgr8")->image);
        cv::waitKey(30);
    }
    catch (cv_bridge::Exception& e) {
        ROS_ERROR("Could not convert from '%s' to 'bgr8'.", msg->encoding.c_str());
    }
}

int main(int argc, char **argv)
{
    ros::init(argc, argv, "image_subscriber");
    ros::NodeHandle nh;
    cv::namedWindow("Image_Subscriber");
    cv::startWindowThread();

    image_transport::ImageTransport it(nh);
    image_transport::Subscriber sub = it.subscribe("camera_raw", 1, imageCallback);

    ros::spin();
    cv::destroyWindow("camera_view");

    return EXIT_SUCCESS;
}

CMake

参考: ja/catkin/CMakeLists.txt - ROS Wiki

cmake_minimum_required(VERSION 2.8.3)
project(my_camera)

find_package(catkin REQUIRED COMPONENTS
  roscpp
  std_msgs
  cv_bridge
  image_transport
)

catkin_package(
  CATKIN_DEPENDS roscpp std_msgs cv_bridge image_transport
)

find_package(OpenCV)

include_directories(
  include
  ${catkin_INCLUDE_DIRS}
  ${OpenCV_INCLUDE_DIRS}
)

add_executable(image_sub src/image_subscriber.cpp)
target_link_libraries(image_sub
  ${catkin_LIBRARIES}
  ${OpenCV_LIBRARIES}
)

参考: ja/catkin/CMakeLists.txt - ROS Wiki

pakage.xml

  <buildtool_depend>catkin</buildtool_depend>

  <build_depend>roscpp</build_depend>
  <build_depend>std_msgs</build_depend>
  <build_depend>image_transport</build_depend>
  <build_depend>cv_bridge</build_depend>

  <run_depend>roscpp</run_depend>
  <run_depend>std_msgs</run_depend>
  <run_depend>image_transport</run_depend>
  <run_depend>cv_bridge</run_depend>

イメージ配布プログラム

以下でカメラの各パラメータをチェック

  • $ v4l2-ctl -d /dev/video0 --list-formats-ext
#include <ros/ros.h>
#include <image_transport/image_transport.h>
#include <cv_bridge/cv_bridge.h>
#include <sensor_msgs/image_encodings.h>
#include <opencv2/highgui/highgui.hpp>

int main( int argc, char **argv )
{
    ros::init( argc, argv, "image_publisher" );
    ros::NodeHandle nh;

    // Open camera with CAMERA_INDEX (webcam is typically #0).
    const int CAMERA_INDEX = 0;
    cv::VideoCapture capture( CAMERA_INDEX );
    if( not capture.isOpened() ) {
        ROS_INFO_STREAM("Failed to open camera with index " << CAMERA_INDEX << "!");
        ros::shutdown();
    }
    // Set the parameters using the results of executing the following command:
    //   v4l2-ctl -d /dev/video0 --list-formats-ext
    capture.set(CV_CAP_PROP_FRAME_WIDTH, 640);
        capture.set(CV_CAP_PROP_FRAME_HEIGHT, 480);
        capture.set(CV_CAP_PROP_FPS, 30);
    //double width = capture.get(CV_CAP_PROP_FRAME_WIDTH);
    //double height = capture.get(CV_CAP_PROP_FRAME_HEIGHT);
    //ROS_DEBUG_STREAM("width: " << width << ", height: " << height);

    image_transport::ImageTransport it(nh);
    image_transport::Publisher pub_image = it.advertise( "camera_raw", 1 );

    // for debug
    cv::namedWindow("Image_Publisher", CV_WINDOW_AUTOSIZE | CV_WINDOW_FREERATIO);

    cv_bridge::CvImagePtr frame = boost::make_shared< cv_bridge::CvImage >();
    frame->encoding = sensor_msgs::image_encodings::BGR8;
    while( ros::ok() ) {
        // for debug
        cv::Mat image;
        capture >> image;
        cv::imshow("Image_Publisher", image);
        //

        capture >> frame->image;
        if( frame->image.empty() ) {
            ROS_ERROR_STREAM( "Failed to capture frame!" );
            ros::shutdown();
        }
        frame->header.stamp = ros::Time::now();
        pub_image.publish( frame->toImageMsg() );

        if(cv::waitKey(30) >= 0) break;
        ros::spinOnce();
    }

    capture.release();
    return EXIT_SUCCESS;
}

launch

<launch>
  <param name="pixel_format" value="mjpeg" />
  <node name="image_publisher"  pkg="my_camera" type="image_pub"  output="screen"/>
  <node name="image_subscriber" pkg="my_camera" type="image_sub" output="screen"/>
</launch>

f:id:robonchu:20180221230211p:plain

$ rostopic list

/camera_raw
/camera_raw/compressed
/camera_raw/compressed/parameter_descriptions
/camera_raw/compressed/parameter_updates
/camera_raw/compressedDepth
/camera_raw/compressedDepth/parameter_descriptions
/camera_raw/compressedDepth/parameter_updates
/camera_raw/theora
/camera_raw/theora/parameter_descriptions
/camera_raw/theora/parameter_updates
/rosout
/rosout_agg

カメライメージ配布プログラム

#include <ros/ros.h>
#include <image_transport/image_transport.h>
#include <cv_bridge/cv_bridge.h>
#include <sensor_msgs/image_encodings.h>
#include <opencv2/highgui/highgui.hpp>

int main( int argc, char **argv )
{
    ros::init( argc, argv, "image_Camerapublisher" );
    ros::NodeHandle n;

    // Open camera with CAMERA_INDEX (webcam is typically #0).
    const int CAMERA_INDEX = 0;
    cv::VideoCapture capture( CAMERA_INDEX );
    if( not capture.isOpened() ) {
        ROS_INFO_STREAM("Failed to open camera with index " << CAMERA_INDEX << "!");
        ros::shutdown();
    }

    // Set the parameters using the results of executing the following command:
    //   v4l2-ctl -d /dev/video0 --list-formats-ext
    capture.set(CV_CAP_PROP_FRAME_WIDTH, 640);
        capture.set(CV_CAP_PROP_FRAME_HEIGHT, 480);
        capture.set(CV_CAP_PROP_FPS, 30);

    image_transport::ImageTransport it( n );
    image_transport::CameraPublisher pub_image = it.advertiseCamera( "camera_raw", 1 );
    sensor_msgs::CameraInfo cam_info;

    // for debug
    //cv::namedWindow("Image_CameraPublisher", CV_WINDOW_AUTOSIZE | CV_WINDOW_FREERATIO);

    cv_bridge::CvImagePtr frame = boost::make_shared< cv_bridge::CvImage >();
    frame->encoding = sensor_msgs::image_encodings::BGR8;
    while( ros::ok() ) {
        // for debug
        //cv::Mat image;
        //capture >> image;
        //cv::imshow("Image_CameraPublisher", image);
        //

        capture >> frame->image;
        if( frame->image.empty() ) {
            ROS_ERROR_STREAM( "Failed to capture frame!" );
            ros::shutdown();
        }
        frame->header.stamp = ros::Time::now();
        cam_info.header.stamp = frame->header.stamp;
        pub_image.publish( frame->toImageMsg() , 
            sensor_msgs::CameraInfoConstPtr(new sensor_msgs::CameraInfo(cam_info)));

        if(cv::waitKey(30) >= 0) break;
        ros::spinOnce();
    }

    capture.release();
    return EXIT_SUCCESS;
}

カメライメージ購読プログラム

#include <ros/ros.h>
#include <image_transport/image_transport.h>
#include <opencv2/highgui/highgui.hpp>
#include <cv_bridge/cv_bridge.h>

void imageCallback(const sensor_msgs::ImageConstPtr& msg, const sensor_msgs::CameraInfoConstPtr& info)
{
    try {
        cv::imshow("image_CameraSubscriber", cv_bridge::toCvShare(msg, "bgr8")->image);
        if(cv::waitKey(30) >= 0) 
            ros::shutdown();
    }
    catch (cv_bridge::Exception& e) {
        ROS_ERROR("Could not convert from '%s' to 'bgr8'.", msg->encoding.c_str());
    }
}

int main(int argc, char **argv)
{
    ros::init(argc, argv, "image_CameraSubscriber");
    ros::NodeHandle nh;
    cv::namedWindow("image_CameraSubscriber");
    cv::startWindowThread();

    image_transport::ImageTransport it(nh);
    image_transport::CameraSubscriber sub;

    sub = it.subscribeCamera("camera_raw", 1, imageCallback);

    ros::spin();
    cv::destroyWindow("image_CameraSubscriber");

    return EXIT_SUCCESS;
}

launch

<launch>
  <node name="camera_publisher"  pkg="my_camera" type="camera_pub"  output="screen"/>
  <node name="camera_subscriber" pkg="my_camera" type="camera_sub" 
    required="true" output="screen"/>
</launch>

f:id:robonchu:20180221231418p:plain

$ rostopic list

/camera_info
/camera_raw
/camera_raw/compressed
/camera_raw/compressed/parameter_descriptions
/camera_raw/compressed/parameter_updates
/camera_raw/compressedDepth
/camera_raw/compressedDepth/parameter_descriptions
/camera_raw/compressedDepth/parameter_updates
/camera_raw/theora
/camera_raw/theora/parameter_descriptions
/camera_raw/theora/parameter_updates
/rosout
/rosout_agg