實驗室的一部分個人工作,之前發過一篇這方面的博客關於探針末端跟蹤的前期嘗試 。哇塞,發現整整過去一年啦。。。。生如逆旅單行道,哪有歲月可回頭。對不起,我沒能活成你想要的樣子。哈哈,下面正式切入主題。
問題背景介紹
上次也介紹了,但好像沒介紹清楚。再來一遍,看圖。
顯微鏡下就是這麼一個操作的過程:通過微操作器末端(探針)的一系列移動和按壓式操作撿起這些圓環狀的物體(donut)。撿這破玩意幹啥?不要問我。目前人們是怎麼完成這一任務的?顯微鏡(沒錯,是顯微鏡。這一系列操作是在顯微鏡下完成的,所以叫“顯微操作”嘛。)物鏡處的攝像頭接到電腦上,實驗員一邊盯着屏幕,一邊晃動着手中的鼠標來控制探針的移動,從而完成任務。
問題來了,現在我們想要自動化地完成這個任務,怎麼辦?僅從感知角度講,我們需要環境信息:探針方向,探針的末端位置,donut的位置。簡單來說就是這些。今天我們要解決探針方向和末端位置這個問題。
解決方案
上述提出的問題難度幾顆星?這得看你具體操作場景的複雜程度了,比如探針末端是否遮擋,背景是否乾淨,donut的各種姿態及重疊是否考慮立着的donut是在液體中漂起來了。假如以真實操作環境作爲參照,其實沒有那麼簡單。下面說解決方案。
1. 這裏我們先跳過最最重要的一步!!!,即假設我們已經知道了探針的大致位置,如下圖所示。
這一步怎麼做到的?一句話解釋,基於深度學習的目標檢測算法YOLO,其實本文的工作接的是這一篇博文YOLO下一步:輸出預測boundingbox做進一步處理 。年前就做了,拖到現在。。。
接下來,我們就在這個ROI區域裏找出探針方向和末端位置。
2.在ROI區域裏找出探針輪廓
3.應用hough transform用直線擬合輪廓
4.根據擬合的直線算出探針的方向。
這裏需要注意,上一步一般會擬合出不止兩條直線。我們可以簡單地對所有直線求平均來得到方向線,也可以使用NMS(非極大值抑制)來找出兩側的兩條最長的直線作爲輪廓直線然後在對其求平均得出方向線。
5. 求出探針末端點位置
我們直接將探針輪廓和方向線的交點作爲末端點位置。
當然這樣立馬有一個問題擺在眼前,末端被遮擋了怎麼辦。首先,經實際測試發現:即使遮擋時也不會錯很多,而且一不遮擋立馬就回來了。當然,因爲在視頻序列裏探針是連續移動的,比如加個卡爾曼濾波就可以了。如果你有別的好想法趕緊告訴我。
6.完成
如圖所示,結果就是這樣。
好,大致過程就是這樣了。歡迎提出別的想法。
代碼實現
目錄樹:
-main.cpp
-inData.cpp //這是輸入數據的類,也就是程序開頭那個boundingBox信息數據
-inData.h
-tip.cpp //關於探針的類
-tip.h
-images //存放原始圖片的文件夾
-post_images //存放處理後圖片的文件夾
main.cpp
#include <opencv2/opencv.hpp>
#include <iostream>
#include <cstdio>
#include <vector>
#include <cmath>
#include "tip.h"
#include "inData.h"
using namespace cv;
using namespace std;
int frames = 1590; //總共幀數
char coordinateFile[] = "tip_coordinate.txt";
char imageFolder[] = "images";
struct {
bool operator()(vector<Point> a, vector<Point> b){
return a.size() > b.size();
}
} contourGreater;
int main() {
inData singleTip(coordinateFile, imageFolder);
for (int frame = 0; frame < frames; frame++) { //914幀之後會出現問題
Mat srcImage = singleTip.readImg(frame); //讀入灰度圖
Rect tipRoi = singleTip.readBox(frame);
Mat biImage;
threshold(srcImage, biImage, 200, 255, CV_THRESH_BINARY_INV); //對整張圖進行二值化處理
Mat biColorImage = biImage.clone();
cvtColor(biImage, biImage, CV_BGR2GRAY);
Mat imageROI = biImage(tipRoi);
vector<vector<Point> > contours;
vector<Vec4i> hierarchy;
findContours(imageROI, contours, hierarchy, CV_RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
if (contours.size() > 1) {
sort(contours.begin(), contours.end(), contourGreater); //找出size最大的輪廓
}
Mat edge_image(imageROI.rows, imageROI.cols, CV_8UC1, Scalar(0, 0, 0));
drawContours(edge_image, contours, 0, Scalar(255, 255, 255), 1, 8, hierarchy, 1);//繪製輪廓
tip alpha(edge_image);
Point st, ed;
alpha.accessPose(st, ed);
arrowedLine(srcImage, st + Point(tipRoi.x, tipRoi.y), ed + Point(tipRoi.x, tipRoi.y), Scalar(0, 0, 255), 2, 8, 0, 0.1);
// circle(srcImage, alpha.accessEndPoint() + Point(tipRoi.x, tipRoi.y), 2, Scalar(0, 255, 0)); //畫出其交點
imshow("【效果圖】", srcImage);
char image_name[50];
sprintf(image_name, "post_images/A_%08d.jpg", frame);
imwrite(image_name, srcImage);
waitKey(3);
}
return 0;
}
inData,cpp
#include "inData.h"
inData::inData(char coordinateFile[], char imageFolder[])
{
read_coordinate(coordinateFile);
strcpy_s(folder, imageFolder);
}
inData::~inData()
{
}
int inData::read_coordinate(char fileName[]) {
ifstream fin(fileName);
vector<int> tipBox(4);
char buffer[256];
int frames=0;
while (fin.getline(buffer, 256, '\n')) {
istringstream iss(buffer);
// iss >> tipBox[0]; iss >> tipBox[1]; iss >> tipBox[2]; iss >> tipBox[3];
iss >> tipBox[0] >> tipBox[1] >> tipBox[2] >> tipBox[3];
tipsBox.push_back(tipBox);
frames++;
}
return frames;
}
Mat inData::readImg(int frame)
{
char image_name[50];
sprintf_s(image_name, "%s/A_%08d.jpg", folder, frame + 1);
return imread(image_name);
}
Rect inData::readBox(int frame)
{
return Rect(tipsBox[frame][0], tipsBox[frame][1], tipsBox[frame][2]- tipsBox[frame][0], tipsBox[frame][3]- tipsBox[frame][1]);
}
inData.h
#pragma once
#include <fstream>
#include <sstream>
#include <cstring>
#include<vector>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
class inData
{
public:
inData(char coordinateFile[], char imageFolder[]);
~inData();
Mat readImg(int frame);
Rect readBox(int frame);
private:
int inData::read_coordinate(char fileName[]);
vector<vector<int> > tipsBox;
char folder[50];
};
tip.cpp
#include "tip.h"
tip::tip(Mat image)
{
contourLines(image, lineA, lineB);
theta = (lineA[1] + lineB[1]) / 2;
Point2f p = intersection(lineA, lineB);
rho = calcRho(p);
angle = calcAngle(p, Point(image.cols/2, image.rows/2));
endPoint=calcEndPoint(getContour(image), Point(image.cols / 2, image.rows / 2));
}
tip::~tip()
{
}
Point tip::accessEndPoint() {
return endPoint;
}
int tip::accessPose(Point& a, Point& b) {
b = endPoint;
float g = angle / 180 * CV_PI;
a.x = b.x - 200 * cos(g);
a.y = b.y + 200 * sin(g);
/*
if (angle >= 0 && angle<90) {
a.x = b.x - 200*cos(g);
a.y = b.y + 200*sin(g);
}
else if (angle >= 90 && angle<180) {
a.x = b.x - 200 * cos(g);
a.y = b.y + 200 * sin(g);
}
else if (angle >= 180 && angle<270) {
a.x = b.x - 200 * cos(g);
a.y = b.y + 200 * sin(g);
}
else {
a.x = b.x -+ 200 * cos(g);
a.y = b.y - 200 * sin(g);
}
*/
return 0;
}
float tip::calcRho(Point2f p)
{
float sm = sin(theta); float cm = cos(theta);
return p.x*cm + p.y*sm;
}
float tip::calcAngle(Point2f p, Point center) {
if (p.x > center.x && p.y <= center.y) { //第一象限
return 90 - theta * 180 / CV_PI;
}
else if (p.x <= center.x && p.y < center.y) { //第二象限
return 270 - theta * 180 / CV_PI;
}
else if (p.x < center.x && p.y >= center.y) { //第三象限
return 270 - theta * 180 / CV_PI;
}
else {
return 450 - theta * 180 / CV_PI;
}
}
Point2f tip::intersection(Vec2f a, Vec2f b) {
float sa = sin(a[1]); float ca = cos(a[1]); float sb = sin(b[1]); float cb = cos(b[1]);
float denominator = (ca*sb - cb*sa);
float x = (a[0] * sb - b[0] * sa) / denominator;
float y = (b[0] * ca - a[0] * cb) / denominator;
return Point2f(x, y);
}
struct {
bool operator()(Vec2f a, Vec2f b)
{
return a[1] < b[1];
}
} lineLess;
int tip::contourLines(Mat image, Vec2f& a, Vec2f& b) {
vector<Vec2f> lines;
HoughLines(image, lines, 1, CV_PI / 180, 80);
/*
Mat tmg;
cvtColor(image, tmg, CV_GRAY2BGR);
for (size_t i = 0; i < lines.size(); i++) {
float rho = lines[i][0], theta = lines[i][1];
Point pt1, pt2;
double a = cos(theta), b = sin(theta);
double x0 = a*rho, y0 = b*rho;
pt1.x = x0 + 1000 * (-b);
pt1.y = y0 + 1000 * (a);
pt2.x = x0 - 1000 * (-b);
pt2.y = y0 - 1000 * (a);
line(tmg,pt1, pt2 , Scalar(0, 0, 255));
//驗證找到的幾條hough直線
}
imshow("【臨時圖】", tmg);
*/
// if (lines.size() < 2) return 0; //有時候檢測不到兩條比較好的直線,此時使用上一次的輪廓線
sort(lines.begin(), lines.end(), lineLess); //貌似不用排序,其本來就是按theta從小到大排列的
a = lines.front();
b = lines.back();
return 0;
}
Point tip::calcEndPoint(vector<Point> contour, Point center) {
for (int k = 0; k < contour.size(); k++) {
int dis = cos(theta)*contour[k].x + sin(theta)*contour[k].y - rho;
if (dis == 0) {
if (angle>=0 && angle<90){
if (contour[k].x > center.x && contour[k].y <= center.y) {
return contour[k];
}
}
else if (angle >= 90 && angle<180) {
if (contour[k].x <= center.x && contour[k].y < center.y) {
return contour[k];
}
}
else if (angle >= 180 && angle<270) {
if (contour[k].x < center.x && contour[k].y >= center.y) {
return contour[k];
}
}
else {
if (contour[k].x >= center.x && contour[k].y > center.y) {
return contour[k];
}
}
}
}
}
vector<Point> tip::getContour(Mat img) {
vector<Point> contour;
int ncols = img.cols;
int nrows = img.rows;
for (int x = 0; x < ncols; x++) {
for (int y = 0; y < nrows; y++) {
if (img.at<uchar>(y, x) == 255) {
contour.push_back(Point(x, y));
}
}
}
return contour;
}
tip.h
#pragma once
#include <opencv2/opencv.hpp>
#include<vector>
using namespace cv;
using namespace std;
class tip
{
public:
tip(Mat image);
~tip();
Point accessEndPoint();
int tip::accessPose(Point& a, Point& b);
private:
Point2f intersection(Vec2f a, Vec2f b);
Vec2f lineA; //輪廓的兩條邊界擬合線
Vec2f lineB;
float calcRho(Point2f p);
float calcAngle(Point2f p, Point center);
int contourLines(Mat image, Vec2f& a, Vec2f& b);
Point calcEndPoint(vector<Point> contour, Point center);
vector<Point> getContour(Mat img);
float rho;
float theta;
float angle;
Point endPoint;
};