ARマーカーを貼り付けたラックから、マイクロピペットのチップをロボットアームで取る
概要
手抜きなハード設計&ローコードで、ロボットアームによるマイクロピペットのチップ装着というタスクを、わりとロバストに実装できるかもしれないことを示します。うまく作り込めば、予算20万円くらいで、使いやすい自動ピペットマシンを作れそうです。
(装着確認はGPT-4Vなどで可能)
はじめに
ロボットアームを使ってマイクロピペットを操作するにあたり、チップの装着が一つの課題になります。正確にピペットを刺さないとチップを装着できないため、予めチップラックを完全固定し、座標をソフトウェア上に記憶させるようなアプローチが一般的です。
ただ、筆者は面倒くさがりなので、そういったハードの作り込みをすることに苦痛を感じてしまうタイプです。
そこで、ハードの作り込みはせず、ソフトウェア側で座標制御を行ってみました。実際の動画は以下の通り。
ARマーカーを貼り付けたラックの座標を認識して
— 畠山 歓 Kan Hatakeyama (@kanhatakeyama) December 27, 2023
マイクロピペットとチップを連結する動画 pic.twitter.com/qgzhKuVt9S
関連研究として、例えば理研から、deep learningでチップを認識して装着する手法が発表されていました。
ただ、筆者は面倒くさがりなので、YOLOなどの深層学習モデル用のデータセットを作る作業にも苦痛を感じるようになってしまいました。
(ガラス瓶の口を認識させたことはあります。下記動画)
現在作っているプロトタイプの合成装置です。
— 畠山 歓 Kan Hatakeyama (@kanhatakeyama) November 29, 2023
YOLOで画像認識+大規模言語モデルでの指示もどきの様子。
マシンは東北大の高石先生が作ってくれました pic.twitter.com/Lgf9NEgu27
また、特化モデル系は、環境光やチップ、ラック等の変化に意外と弱かったりする課題もあります。また、タスク毎にモデルを作る必要があり、ラックの穴、チップの装着確認、など、ラボオートメーションの細々とした作業を自動化するにはかなりの人的コストがかかります。
将来的にはLLMを載せた基盤モデル(たとえばgoogleのrt2)が最適解になる気がしていますが、まだ筆者には使いこなせないので、座標認識にARマーカーを使うことにしました※。
※正確には、以下のdiscordサーバーで、arマーカーをオススメされたので、その言葉を信じてやってみた次第です。
環境
ハードウェア(主に使うもの)
ロボットアーム
XYZ座標を制御できる装置なら、何でも原理的には何でもOKです。
本記事では、東北大の高石先生に作っていただいた自家製のマシンを使っています(Opentrons的なマシン)。
USBカメラ
以下のカメラを使いました
選択理由
四角形なので設置しやすい
1/4ネジの穴があって固定しやすい(カメラの三脚に付ける用のネジ穴)
画素数が高い
オートフォーカス
実際に使った感想
基本的にはOK
Arucoマーカーではなく、他の場所にフォーカスをあわせに行くことが多くて困った (このカメラに限らず、問題は起きるかも?)
マーカーがピンボケして認識できなくなる
チップラック
3Dプリンタのモデルを改造して、Arucoマーカーを貼りました
公開したいところですが、オリジナルモデルの著作権の問題があるので、配布できません
arマーカーを付けたマイクロピペットのチップラック。
— 畠山 歓 Kan Hatakeyama (@kanhatakeyama) December 20, 2023
これで上手くチップを取れるか否か。 https://t.co/M4SQCGoTZG pic.twitter.com/JW7co4yeqi
ソフトウェア
Pythonを使いました。
重要ライブラリは、
pip install opencv-contrib-python==4.7.0.68
です。 普通のopencvはuninstallしてからinstallする必要があります。また、opencvはバージョン毎に関数名などがコロコロ変わるので、注意が必要です。
実装
コードを抜粋して貼っていきます。
usbカメラのキャリブレーション
カメラの画像は歪んでいるので、chess boardでキャリブレーションしておきます。
arucoマーカーを読み取るクラス
usbカメラからframeの読み取り
import cv2
class USBCamera:
def __init__(self, camera_id,
):
self.camera_id = camera_id
self.cap = cv2.VideoCapture(self.camera_id)
if not self.cap.isOpened():
raise IOError("Cannot open webcam")
def get_frame(self, ):
ret, frame = self.cap.read()
if not ret:
raise IOError("Cannot read frame")
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
return frame
Arucoマーカーの検出とカメラの相対位置の読み取り
from cv2 import aruco
import numpy as np
import cv2
class ArucoDetect:
def __init__(self,
marker_length=1.8,
mtx_path="",
dist_path="",
) -> None:
self.aruco_dict = aruco.getPredefinedDictionary(aruco.DICT_4X4_50)
self.parameters = aruco.DetectorParameters()
self.marker_length=marker_length
if mtx_path!="" and dist_path!="":
self.mtx=np.load(mtx_path)
self.dist=np.load(dist_path)
def detect_aruco(self, frame):
corners, ids, rejectedImgPoints = aruco.detectMarkers(
frame, self.aruco_dict, parameters=self.parameters)
return corners, ids # , rejectedImgPoints
def estimate_position(self,camera):
frame=camera.get_frame()
#frame = cv2.detailEnhance(frame, sigma_s=10, sigma_r=0.15)
corners,ids=self.detect_aruco(frame)
rvec, tvec, _ = cv2.aruco.estimatePoseSingleMarkers(corners,self.marker_length,
self.mtx, self.dist)
return frame,corners,ids,rvec,tvec
マーカーの読み取り
以下、main部分の処理です。
クラスの初期化
camera=USBCamera("COM3") #com portなどを設定
aruco_detector=ArucoDetect(marker_length=1.5,
mtx_path='dat/mtx_elp.npy',
#mtx_path='dat/mtx.npy',
dist_path='dat/dist_elp.npy'
#dist_path='dat/dist.npy'
)
Arucoマーカーの読み取り
frame,corners,ids,rvec,tvec=aruco_detector.estimate_position(camera)
cv2.aruco.drawDetectedMarkers(frame, corners, ids, (0,255,255))
clear_output(wait=True)
plt.imshow(frame)
plt.axis('off')
plt.show()
![](https://assets.st-note.com/img/1703659317576-RubXOTQ0kd.png)
マーカーからXX cm離れた場所の計算
今回のターゲットはマーカーそのもののではなく、チップの座標です。なので、マーカーからXX cm離れた場所の座標を得る必要があります。
回転行列などを使うようです。ChatGPTに聞いて書いたコードです。
while True:
frame,corners,ids,rvec,tvec=aruco_detector.estimate_position(camera)
plt.figure()
cv2.aruco.drawDetectedMarkers(frame, corners, ids, (0,255,255))
world_point = np.zeros((3, 1))
try:
cv2.drawFrameAxes(frame, aruco_detector.mtx, aruco_detector.dist, rvec, tvec, 1)
# 各マーカーに対する処理
for i in range(len(ids)):
# マーカーから X cm 右の点を定義
local_point = np.array([[2.65], [0.65], [0]], dtype=np.float32) # 単位は cm
# 画像座標への変換
img_point, _ = cv2.projectPoints(local_point, rvec[i], tvec[i], aruco_detector.mtx, aruco_detector.dist)
# 点を画像上に描画
point = tuple(img_point[0][0].astype(int))
cv2.circle(frame, point, 5, (0, 0, 255), -1)
# 回転行列への変換
R, _ = cv2.Rodrigues(rvec[i])
# ローカル座標をワールド座標系に変換
world_point = R @ local_point + tvec[i].reshape(3, 1)
except Exception as e:
print(e)
pass
clear_output(wait=True)
plt.imshow(frame)
plt.axis('off')
plt.show()
ちょっとわかりにくいですが、画面上の青い点が、計算された座標です。
arucoマーカー + マイクロピペットのチップラック
— 畠山 歓 Kan Hatakeyama (@kanhatakeyama) December 26, 2023
青い点を相対座標で自動検出しています。
この位置精度で、ロボットアームのチップを装着できるか否か。 pic.twitter.com/Q0P5F48zNh
アームの移動
チップの位置は、カメラに対する相対座標としてworld_point変数に格納されています。
あとは、アームの座標系に適当に換算してフィードバックを回していけば、チップの上にマイクロピペットが移動します。
import time
import random
while True:
frame,corners,ids,rvec,tvec=aruco_detector.estimate_position(camera)
plt.figure()
cv2.aruco.drawDetectedMarkers(frame, corners, ids, (0,255,255))
world_point = np.zeros((3, 1))
try:
cv2.drawFrameAxes(frame, aruco_detector.mtx, aruco_detector.dist, rvec, tvec, 1)
# 各マーカーに対する処理
for i in range(len(ids)):
# マーカーから 1 cm 右の点を定義
local_point = np.array([[2.65, 0.65, 0]], dtype=np.float32) # 単位は cm
local_point = np.array([[2.65], [0.65], [0]], dtype=np.float32) # 単位は cm
# 画像座標への変換
img_point, _ = cv2.projectPoints(local_point, rvec[i], tvec[i], aruco_detector.mtx, aruco_detector.dist)
# 点を画像上に描画
point = tuple(img_point[0][0].astype(int))
cv2.circle(frame, point, 5, (0, 0, 255), -1)
# 回転行列への変換
R, _ = cv2.Rodrigues(rvec[i])
# ローカル座標をワールド座標系に変換
world_point = R @ local_point + tvec[i].reshape(3, 1)
except Exception as e:
print(e)
pass
clear_output(wait=True)
plt.imshow(frame)
plt.axis('off')
plt.show()
#move
if world_point[0][0]!=0:
offset=(-0.7,6.5) #カメラやマイクロピペットの位置の補正
x=arm.x
y=arm.y
arm.set_xyz(y=y+world_point[0][0]*1+offset[0],x=x-world_point[1][0]*1+offset[1],z=60)
print(world_point[0][0],world_point[1][0])
new_x=arm.x
new_y=arm.y
#移動しなくなったらループを抜ける
if abs(new_x-x)<0.04 and abs(new_y-y)<0.04:
break
else:
offset=(0,0)
arm.set_z(random.randint(50,60))
#time.sleep(0.3)
#チップを下げる
arm.set_z(130)
arm.set_z(100)
ここで、armはロボットを管理するクラスで、set_xyzはxyz(zは高さ)までアームを動かす関数です。アーム座標はarm.x, arm.y, arm.zに格納されています。
補足
使用するロボットアームによって、このあたりの実装は変わるはずです。
フィードバックループはかなり雑です。きちんと書けば、もっと早く収束するはずです
ARマーカーではなく背景にピントが合ってしまうことが多々ありました。
対応策として、適当にアームの高さを変えて再挑戦する加えています
今回は3Dプリンタ/CNC式のマシンを使ったので、アーム―ピペットーカメラ座標系の変換が簡単でした(offsetを定数として定義)
普通のロボットアームの場合は、アームがXY平面上で回転するので、アーム―ピペットーカメラ座標系の変換に三角関数が必要になると思います。
動作シーンをもう一度。
ARマーカーを貼り付けたラックの座標を認識して
— 畠山 歓 Kan Hatakeyama (@kanhatakeyama) December 27, 2023
マイクロピペットとチップを連結する動画 pic.twitter.com/qgzhKuVt9S
装着確認
APIではまだ実装してませんが、マルチモーダルLLMなら、チップ装着の有無くらいは判定できるようです。
ChatGPTの判定は以下の通り。
![](https://assets.st-note.com/img/1703660496031-ky1J8TcX6G.png?width=1200)
![](https://assets.st-note.com/img/1703660541604-N6j4ytCwHA.png?width=1200)
背景と被ってて、人間でもややわかりにくいシチュエーションであるにも関わらず、きちんと判定できています。
おわりに
手抜きなハード設計&ローコードで、ロボットアームによるマイクロピペットのチップ装着というタスクを、わりとロバストに実装可能!?なことがわかりました。
最近のOpentronsはマイクロピペット付きで200万円以上するようですが、Dobot Magician + 電子ピペット(ボタンを改造して電子制御できるようにハック) + 今回のようなソフトウェアなら、20万円くらいで、使いやすい自動ピペットマシンを作れそうです。 マルチモーダルLLMも便利ですね。