ソースを参照

v0.1.1 分开抓取和切割的接口

chenhongjiang 1 年間 前
コミット
a6eebcb0c8
19 ファイル変更4306 行追加0 行削除
  1. 8 0
      .gitignore
  2. 2 0
      ReadMe.txt
  3. 78 0
      config.cpp
  4. 34 0
      config.h
  5. 93 0
      data_def.h
  6. 56 0
      data_def_api.h
  7. 263 0
      imstorage_manager.cpp
  8. 80 0
      imstorage_manager.h
  9. 100 0
      logger.cpp
  10. 43 0
      logger.h
  11. 17 0
      tcv_conf.yml
  12. 331 0
      tea_cv_api.cpp
  13. 120 0
      tea_cv_api.h
  14. 275 0
      tea_detect.cpp
  15. 74 0
      tea_detect.h
  16. 277 0
      tea_sorter.cpp
  17. 46 0
      tea_sorter.h
  18. 1910 0
      utils.cpp
  19. 499 0
      utils.h

+ 8 - 0
.gitignore

@@ -0,0 +1,8 @@
+/Debug/
+/Debug_exe/
+/Release/
+/Release_exe/
+/x64/
+*.log
+tea_dll.vcxproj*
+

+ 2 - 0
ReadMe.txt

@@ -0,0 +1,2 @@
+v0.1.0 初始版本,实现整体流程,模型待优化。
+v0.1.1 分开抓取和切割的接口。

+ 78 - 0
config.cpp

@@ -0,0 +1,78 @@
+#include "config.h"
+#include <iostream>
+
+#define VNAME(value) (#value)
+
+namespace graft_cv{
+	CGCvConfig::CGCvConfig()	
+		:m_cparam(0)
+	{		
+	};
+	CGCvConfig::~CGCvConfig()
+	{
+	};
+
+	void CGCvConfig::setConfParam(ConfigParam*cp)
+	{
+		m_cparam = cp;
+	}
+	void CGCvConfig::write(cv::FileStorage &fs)const{
+		assert(m_cparam!=0);
+		fs << "{" 
+			<< "image_show"<<  m_cparam->image_show
+			<< "image_return"<<  m_cparam->image_return
+			
+			<<"image_save"<<m_cparam->image_save
+			<<"image_depository"<<m_cparam->image_depository
+			<<"image_backup_days"<<m_cparam->image_backup_days
+			<<"model_path_grab"<<m_cparam->model_path_grab
+			<<"object_threshold_grab"<<m_cparam->object_threshold_grab
+			<<"nms_threshold_grab"<<m_cparam->nms_threshold_grab
+			<< "model_path_cut" << m_cparam->model_path_cut
+			<< "object_threshold_cut" << m_cparam->object_threshold_cut
+			<< "nms_threshold_cut" << m_cparam->nms_threshold_cut
+
+			
+			<< "}"; 	
+	};
+	void CGCvConfig::read(const cv::FileNode& node){ //Read serialization for this class
+		assert(m_cparam!=0);
+		m_cparam->image_show = (bool)(int)node["image_show"];
+		m_cparam->image_return = (bool)(int)node["image_return"];
+		
+
+		m_cparam->image_save = (bool)(int)node["image_save"];
+		m_cparam->image_depository	=(string)node["image_depository"];
+		m_cparam->image_backup_days = (int)node["image_backup_days"];
+		m_cparam->model_path_grab =(string)node["model_path_grab"];
+		m_cparam->object_threshold_grab = (float)node["object_threshold_grab"];
+		m_cparam->nms_threshold_grab = (float)node["nms_threshold_grab"];
+		m_cparam->model_path_cut = (string)node["model_path_cut"];
+		m_cparam->object_threshold_cut = (float)node["object_threshold_cut"];
+		m_cparam->nms_threshold_cut = (float)node["nms_threshold_cut"];
+		
+		
+  }
+	string get_cparam_info(ConfigParam*m_cparam)
+	{
+		if(!m_cparam){return string("");}
+
+		stringstream buff;
+		buff << "{" <<endl
+			<< "image_show:\t"<<  m_cparam->image_show << endl
+			<< "image_return:\t"<<  m_cparam->image_return << endl
+			
+
+			<<"image_save:\t"<<m_cparam->image_save << endl
+			<<"image_depository:\t"<<m_cparam->image_depository << endl
+			<<"image_backup_days:\t"<<m_cparam->image_backup_days << endl
+			<<"model_path_grab:\t"<<m_cparam->model_path_grab << endl
+			<<"object_threshold_grab:\t"<<m_cparam->object_threshold_grab << endl
+			<<"nms_threshold_grab:\t"<<m_cparam->nms_threshold_grab << endl
+			<< "model_path_cut:\t" << m_cparam->model_path_cut << endl
+			<< "object_threshold_cut:\t" << m_cparam->object_threshold_cut << endl
+			<< "nms_threshold_cut:\t" << m_cparam->nms_threshold_cut << endl
+			<< "}" << endl; 	
+		return buff.str();
+	}
+}

+ 34 - 0
config.h

@@ -0,0 +1,34 @@
+#pragma once
+
+#include <opencv2\highgui\highgui.hpp>
+#include "data_def_api.h"
+
+using namespace std;
+//using namespace cv;
+
+namespace graft_cv{
+
+	class CGCvConfig
+	{
+	public:
+		CGCvConfig();
+		~CGCvConfig();
+		void setConfParam(ConfigParam*);		
+		void write(cv::FileStorage &fs)const;
+		void read(const cv::FileNode& node);
+	private:
+		ConfigParam* m_cparam;	
+
+	};
+	//These write and read functions must exist as per the inline functions in operations.hpp
+	static void write(cv::FileStorage& fs, const std::string&, const CGCvConfig& x){
+	  x.write(fs);
+	}
+	static void read(const cv::FileNode& node, CGCvConfig& x, const CGCvConfig& default_value = CGCvConfig()){
+	  if(node.empty())
+		x = default_value;
+	  else
+		x.read(node);
+	}
+	string get_cparam_info(ConfigParam* m_cparam);
+};

+ 93 - 0
data_def.h

@@ -0,0 +1,93 @@
+#pragma once
+
+namespace graft_cv{
+	extern char *g_version_str;
+	//enum camera {rs_clamp_rad, rs_clamp_tan, rs_cut_rad, rs_cut_tan, sc_clamp_rad, sc_clamp_tan, sc_cut_rad, sc_cut_tan};
+	enum img_type {oa,rs,rs_reid,sc,sola_rs_reid, sola_sc_reid, sola_rs_pcd, sola_sc_pcd, tea_grab,tea_cut};
+	template<class T>
+	class roi_box{
+	public:
+		roi_box(T x_, T y_, T width_, T height_){
+			this->x = x_;
+			this->y = y_;
+			this->width = width_;
+			this->height = height_;
+		}
+		roi_box(){
+			this->x = T(0);
+			this->y = T(0);
+			this->width = T(0);
+			this->height = T(0);
+		}
+		~roi_box(){}
+
+		bool isInBox(T pt_x, T pt_y){
+			if (pt_x>=this->x && pt_x <=(this->x+this->width) &&
+                pt_y >= this->y && pt_y <= (this->y + this->height)){
+					return true;
+			}
+            else{
+				return false;
+			}
+		}
+	private:
+		T x;
+		T y;
+		T width;
+		T height;
+	};
+
+	template<class T>
+	class gcv_point{
+	public:
+		gcv_point(T x_, T y_){
+			this->x = x_;
+			this->y = y_;			
+		}
+		gcv_point(){
+			this->x = T(0);
+			this->y = T(0);			
+		}
+		gcv_point(const gcv_point<T>&pt){
+			this->x = pt.x;
+			this->y = pt.y;			
+		}
+		~gcv_point(){}	
+		double distance(const gcv_point<T>& pt)const
+		{
+			return sqrt((double)(this->x - pt.x)*(this->x - pt.x) +
+				        (double)(this->y - pt.y)*(this->y - pt.y));
+		}
+		gcv_point<T>& operator=(const gcv_point<T>& pt)
+		{
+			if(this !=&pt){
+				this->x=pt.x;
+				this->y=pt.y;
+			}
+			return *this;
+		}
+		bool operator==(const gcv_point<T>& pt)const
+		{
+			if(this->x==pt.x && this->y==pt.y){
+				return true;
+			}
+			return false;
+		}
+		
+	public:
+		T x;
+		T y;		
+	};
+
+	struct Bbox //drop box with confidence and center keypoint
+	{
+		float score;
+		int x1;
+		int y1;
+		int x2;
+		int y2;
+		float ppoint[10]; //(x,y) 5 key points	
+		float area; //獗羹충생
+		int status;	// 카두榴檄:攣끽=0;홧呵=-1;튤잼=-2
+	};
+};

+ 56 - 0
data_def_api.h

@@ -0,0 +1,56 @@
+#pragma once
+#include <string>
+namespace graft_cv{
+typedef unsigned char byte;
+typedef struct 
+{    
+	int channel;
+    int width;
+    int height;
+    byte *data; // 长度:width * height * channel
+}ImgInfo;
+
+typedef struct{
+	// 调试控制 (2)
+	bool image_show;//true--显示处理过程中的中间图片,需要人工回车才能继续执行; false--无图片显示
+	bool image_return;//true--返回结果positoninfo中添加返回的图片; false--无图片返回
+	//image storage parameters(3)
+	bool image_save;//是否保存图像
+	std::string image_depository;//保存图像目录
+	int image_backup_days;//保存图像天数,过期删除
+	std::string model_path_grab;
+	float object_threshold_grab;
+	float nms_threshold_grab;
+
+	std::string model_path_cut;
+	float object_threshold_cut;
+	float nms_threshold_cut;
+
+} ConfigParam;
+
+typedef struct 
+{
+	//以下涉及到位置均为实际位置
+	double tea_grab_x1;		//第一株茶叶抓取位置x1,//以下为tcd
+	double tea_grab_y1;		//第一株茶叶抓取位置y1,
+	double tea_grab_angle1;	//第一株茶叶抓取角度r1,
+
+	double tea_grab_x2;		//第二株茶叶抓取位置x2,
+	double tea_grab_y2;		//第二株茶叶抓取位置y2,
+	double tea_grab_angle2;	//第二株茶叶抓取角度r2,
+
+	double tea_cut_x1;		//第一株茶叶切割位置x1,
+	double tea_cut_y1;		//第一株茶叶切割位置y1,
+	double tea_cut_angle1;	//第一株茶叶切割角度r1,
+
+	double tea_cut_x2;		//第二株茶叶切割位置x2,
+	double tea_cut_y2;		//第二株茶叶切割位置y2,
+	double tea_cut_angle2;	//第二株茶叶切割角度r2,
+	
+	
+	ImgInfo* pp_images[5];//参考图片,只读,从前向后,没有的会被置零
+	
+}PositionInfo;
+
+
+};

+ 263 - 0
imstorage_manager.cpp

@@ -0,0 +1,263 @@
+#include <io.h>
+#include <time.h>
+#include <stdio.h>
+#include "imstorage_manager.h"
+
+//namespace graft_gcv
+//{
+
+
+CRITICAL_SECTION g_cs;
+CRITICAL_SECTION g_cs_pcd;
+
+void getFiles( string path, vector<string>& files, time_t clean_time )
+{
+    //文件句柄
+    long   hFile   =   0;
+    //文件信息
+    struct _finddata_t fileinfo;
+    string p;
+    if((hFile = _findfirst(p.assign(path).append("\\*").c_str(),&fileinfo)) !=  -1)
+    {
+        do
+        {
+            //如果是目录,迭代之
+            //如果不是,加入列表
+            if((fileinfo.attrib &  _A_SUBDIR))
+            {
+                if(strcmp(fileinfo.name,".") != 0  &&  strcmp(fileinfo.name,"..") != 0)
+                    getFiles( p.assign(path).append("\\").append(fileinfo.name), files ,clean_time);
+            }
+            else
+            {
+				if(clean_time>fileinfo.time_create){
+					files.push_back(p.assign(path).append("\\").append(fileinfo.name) );
+				}
+            }
+        }while(_findnext(hFile, &fileinfo)  == 0);
+        _findclose(hFile);
+    }
+}
+bool g_thread_run=false;
+bool g_thread_run_saver=true;
+bool g_thread_run_saver_pcd = true;
+int WINAPI TheadFuncClearn(LPVOID lpParam)
+{
+    ThreadParamClean*  threadParam = (ThreadParamClean *) lpParam;    
+	string folder =threadParam->folder;	
+	int store_days = threadParam->store_days;
+	bool* state = threadParam->state;
+	int cnt=0;
+	__time64_t dtime = store_days*86400;
+	//cout<<"folder: "<<folder<<endl;
+	time_t scan_time = time(NULL);
+	while(folder.size()>0)
+    {
+		if(!(*state)){break;}   
+		if(time(NULL)-scan_time < 3600000){
+			Sleep(500);
+			continue;
+		}
+		vector<string>filenames;
+		time_t clean_time = time(NULL) - dtime;		
+		getFiles(folder,filenames,clean_time);
+		scan_time = time(NULL);
+		for(size_t i=0;i<filenames.size();++i){			
+			//delete the file
+			remove(filenames[i].c_str());
+		}
+		
+    }
+    return 0;
+}
+int WINAPI TheadFuncSave(LPVOID lpParam)
+{
+	ThreadParamSave*  threadParam = (ThreadParamSave *) lpParam;
+    int nIndex = threadParam->nIndex;
+    queue<ImgParam>* imgQ =threadParam->imgQ;	
+    while(true)
+    {		
+		if(!g_thread_run_saver){break;}
+		if(imgQ->size()==0){
+			Sleep(50);
+			continue;
+		}
+		try{
+			EnterCriticalSection(&g_cs);
+			ImgParam& im_info = imgQ->front();
+			cv::imwrite(im_info.img_name,im_info.image);
+			//cout<<im_info.img_name<<"\t"<<imgQ->size()<<endl;
+			imgQ->pop();		
+		}
+		catch(...){
+			//int tes = 0;
+		}
+		LeaveCriticalSection(&g_cs);
+    }
+    return 0;
+}
+//int WINAPI TheadFuncSavePcd(LPVOID lpParam)
+//{
+//	ThreadParamSavePcd*  threadParam = (ThreadParamSavePcd *)lpParam;
+//	int nIndex = threadParam->nIndex;
+//	queue<PcdParam>* pcdQ = threadParam->pcdQ;
+//	while (true)
+//	{
+//		if (!g_thread_run_saver_pcd) { break; }
+//		if (pcdQ->size() == 0) {
+//			Sleep(50);
+//			continue;
+//		}
+//		try {
+//			EnterCriticalSection(&g_cs_pcd);
+//			PcdParam& pcd_info = pcdQ->front();
+//			pcl::io::savePLYFile(pcd_info.pcd_name, *pcd_info.pcd, true);			
+//			//cout<<im_info.img_name<<"\t"<<imgQ->size()<<endl;
+//			pcdQ->pop();
+//		}
+//		catch (...) {
+//			//int tes = 0;
+//		}
+//		LeaveCriticalSection(&g_cs_pcd);
+//	}
+//	return 0;
+//}
+	
+CImStoreManager::CImStoreManager()
+	:m_storeDays(7)
+	,m_storeDir("")
+	,m_workHandle(0)
+	,m_workHandleSave(0)
+	//,m_workHandleSavePcd(0)
+{	
+	InitializeCriticalSection(&g_cs);
+	ThreadParamSave paramSave;
+	paramSave.nIndex = 0;
+	//paramSave.state = &g_thread_run;
+	paramSave.imgQ = &m_images;
+	m_workHandleSave = CreateThread(
+		NULL, 
+		0, 
+		(LPTHREAD_START_ROUTINE)TheadFuncSave,
+		(LPVOID)&paramSave, 
+		NULL, 
+		NULL);
+
+	InitializeCriticalSection(&g_cs_pcd);
+	//ThreadParamSavePcd paramSavePcd;
+	//paramSavePcd.nIndex = 0;
+	//paramSave.state = &g_thread_run;
+	//paramSavePcd.pcdQ = &m_pcds;
+	/*m_workHandleSavePcd = CreateThread(
+		NULL,
+		0,
+		(LPTHREAD_START_ROUTINE)TheadFuncSavePcd,
+		(LPVOID)&paramSavePcd,
+		NULL,
+		NULL);*/
+	Sleep(500);
+}
+CImStoreManager::~CImStoreManager(){
+	g_thread_run=false;
+	g_thread_run_saver=false;
+	g_thread_run_saver_pcd = false;
+	HANDLE handles[2];
+	handles[0]=m_workHandle;
+	handles[1]=m_workHandleSave;
+	//handles[2] = m_workHandleSavePcd;
+	WaitForMultipleObjects(2,handles,TRUE,500);
+	DeleteCriticalSection(&g_cs);
+	//DeleteCriticalSection(&g_cs_pcd);
+
+}
+
+void CImStoreManager::restart_start_worker()
+{	
+	ThreadParamClean param_clean;
+	param_clean.folder = m_storeDir;
+	param_clean.store_days = m_storeDays;
+	param_clean.state = &g_thread_run;
+	g_thread_run=false;
+	if(m_workHandle){
+		WaitForSingleObject(m_workHandle,INFINITE);		
+		CloseHandle(m_workHandle);
+	}
+	g_thread_run=true;
+	m_workHandle = CreateThread(
+		NULL, 
+		0, 
+		(LPTHREAD_START_ROUTINE)TheadFuncClearn,
+		(LPVOID)&param_clean, 
+		NULL, 
+		NULL);	
+	Sleep(500);
+}
+int CImStoreManager::setStoreDir(string& folder)
+{
+	int stat = _access(folder.c_str(),0);
+	if (stat==0){
+		m_storeDir = folder;
+		g_thread_run=false;
+		restart_start_worker();
+	}
+	else{
+		return 1;
+	}
+	return 0;
+}
+void CImStoreManager::getStoreDir(string& folder)
+{
+	folder = m_storeDir;
+}
+void CImStoreManager::setStoreDays(int days)
+{
+	if(days>0){
+		m_storeDays = days;
+		g_thread_run=false;
+		restart_start_worker();
+	}
+}
+bool CImStoreManager::is_valid_folder()
+{
+	int stat = _access(m_storeDir.c_str(),0);
+	if(stat==0){return true;}
+	else{return false;}
+}
+
+int CImStoreManager::saveImage(cv::Mat&img,string name_id)
+{	
+	if(!is_valid_folder()){return 1;}
+	if(img.empty()){return 1;}
+	string tar_file = m_storeDir+"/"+name_id+".jpg";
+	ImgParam imgp;
+	imgp.img_name=tar_file;
+	imgp.image = img.clone();
+	EnterCriticalSection(&g_cs);
+	m_images.push(imgp);
+	//cout<<"=========="<<imgp.img_name<<endl;
+	LeaveCriticalSection(&g_cs);
+	
+	return 0;
+}
+
+//int CImStoreManager::saveBinPly(
+//	pcl::PointCloud<pcl::PointXYZ>::Ptr pcd, 
+//	string name_id)
+//{
+//	if (!is_valid_folder()) { return 1; }
+//	if (pcd->points.size()==0) { return 1; }
+//	
+//	EnterCriticalSection(&g_cs_pcd);
+//	string tar_file = m_storeDir + "/" + name_id + ".ply";		
+//	PcdParam pcdp;
+//	pcdp.pcd.reset(new pcl::PointCloud<pcl::PointXYZ>);
+//	pcdp.pcd_name = tar_file;
+//	pcl::copyPointCloud(*pcd, *pcdp.pcd);	
+//	
+//	m_pcds.push(pcdp);
+//	//cout<<"=========="<<pcdp.pcd_name<<endl;
+//	LeaveCriticalSection(&g_cs_pcd);
+//
+//	return 0;
+//}
+//};

+ 80 - 0
imstorage_manager.h

@@ -0,0 +1,80 @@
+#pragma once
+
+#include <string>
+#include <opencv2\opencv.hpp>
+#include <windows.h>
+#include <queue> 
+
+//using namespace cv;
+using namespace std;
+//namespace graft_cv
+//{
+
+typedef struct
+{
+	string img_name;
+	cv::Mat image;
+} ImgParam;
+
+//typedef struct
+//{
+//	string pcd_name;
+//	pcl::PointCloud<pcl::PointXYZ>::Ptr pcd;
+//} PcdParam;
+
+
+typedef struct 
+{	
+    int nIndex;
+    queue<ImgParam>* imgQ;
+        
+} ThreadParamSave;
+
+//typedef struct
+//{
+//	int nIndex;
+//	queue<PcdParam>* pcdQ;
+//
+//} ThreadParamSavePcd;
+
+typedef struct 
+{	
+    string folder;
+	int store_days;
+    bool* state;
+        
+} ThreadParamClean;
+
+int WINAPI TheadFuncClearn(LPVOID lpParam);
+int WINAPI TheadFuncSave(LPVOID lpParam);
+//int WINAPI TheadFuncSavePcd(LPVOID lpParam);
+void getFiles( string path, vector<string>& files, time_t clean_time);
+
+class CImStoreManager{
+public:
+	CImStoreManager();
+	~CImStoreManager();
+
+	// return 0--ok, 1--failed(not exists)
+	int setStoreDir(string& folder);
+
+	void getStoreDir(string& folder);
+	void setStoreDays(int days);
+	//saveImage() return 0--ok, 1 -- invalid image or folder
+	int saveImage(cv::Mat&img,string name_id);
+	void restart_start_worker();
+	//int saveBinPly(pcl::PointCloud<pcl::PointXYZ>::Ptr pcd, string name_id);
+
+
+protected:
+	int m_storeDays;
+	string m_storeDir;
+	bool is_valid_folder();
+	HANDLE m_workHandle;
+	HANDLE m_workHandleSave;
+	//HANDLE m_workHandleSavePcd;
+	queue<ImgParam> m_images;
+	//queue<PcdParam> m_pcds;
+};
+
+//};

+ 100 - 0
logger.cpp

@@ -0,0 +1,100 @@
+#include "logger.h"
+#include "utils.h"
+
+namespace graft_cv{
+	
+	CGcvLogger::CGcvLogger(ofstream& ofs)
+		:m_target(terminal)
+		,m_level(debug)
+		,m_outfile(ofs)
+		,m_pMtx(new std::mutex())
+	{
+		cout<<currTime()<<" [WELCOME] ===========start logger==========="<<endl;
+	}
+	CGcvLogger::CGcvLogger(ofstream& ofs,log_target target, log_level level, string path)
+		:m_target(target)
+		,m_level(level)
+		,m_path(path)
+		,m_outfile(ofs)
+		, m_pMtx(new std::mutex())
+	{
+		string tmp = currTime() +" [WELCOME] ===========start logger===========\n";
+		if(m_target !=terminal){
+			//m_outfile.open(m_path, ios::out | ios::app);
+			m_outfile.open(m_path, ofstream::out | ofstream::app);
+			m_outfile<<tmp;
+		}
+		if(m_target != file){
+		    cout<<tmp<<endl;
+			m_outfile.flush();
+		}
+
+	}
+	void CGcvLogger::setPath(const string& path)
+	{
+		if (path !=m_path){			
+			m_path = path;
+		}
+	}
+	string & CGcvLogger::getPath()
+	{
+		return m_path;
+	}
+	void CGcvLogger::output(string text, log_level act_level)
+	{
+		m_pMtx->lock();
+		string prefix;
+		if(act_level == debug){
+			prefix = " [DEBUG] ";
+		}
+		else if(act_level== info){
+			prefix =" [INFO] ";
+		}
+		else if(act_level == warning){
+			prefix = " [WARNING] ";
+		}
+		else if(act_level == error){
+			prefix= " [ERROR] ";
+		}
+		else {
+			prefix ="";
+		}    
+		string output_content= currTime() + prefix + text +"\n";
+
+		if(this->m_level <= act_level && this->m_target!= file){
+			cout <<output_content;
+		}
+		if(this->m_target != terminal){
+			m_outfile<< output_content;
+			m_outfile.flush();
+		}
+		m_pMtx->unlock();
+	}
+
+	void CGcvLogger::DEBUG(string text)
+	{
+		if(debug>=m_level){
+			this->output(text,debug);
+		}
+	}
+	void CGcvLogger::INFO(string text)
+	{
+		if(info>=m_level){
+			this->output(text,info);
+		}
+	}
+	void CGcvLogger::WARNING(string text)
+	{
+		if(warning>=m_level){
+			this->output(text,warning);
+		}
+	}
+	void CGcvLogger::ERRORINFO(string text)
+	{
+		if(error>=m_level){
+			this->output(text,error);
+		}
+	}
+
+
+};

+ 43 - 0
logger.h

@@ -0,0 +1,43 @@
+#pragma once
+
+#include <iostream>
+#include <string>
+
+#include <fstream>
+#include <mutex>
+
+using namespace std;
+
+namespace graft_cv{	
+
+	class CGcvLogger{
+	public:
+		enum log_level{debug,info, warning,error};
+		enum log_target{file,terminal,file_and_terminal};
+	private:
+		ofstream& m_outfile;
+		log_target m_target;
+		string m_path;
+		log_level m_level;
+		std::shared_ptr<std::mutex> m_pMtx;
+
+		void output(string text, log_level act_level);
+	public:
+		CGcvLogger(ofstream&);
+		CGcvLogger(ofstream&, 
+			log_target target, 
+			log_level level,
+			string path);
+		void setPath(const string& path);
+		string & getPath();
+		void setLevel(log_level lev){
+			m_level = lev;
+		};
+		int getLevel(){return m_level;};
+		void DEBUG(string text);
+		void INFO(string text);
+		void WARNING(string text);
+		void ERRORINFO(string text);
+		
+	};
+};

+ 17 - 0
tcv_conf.yml

@@ -0,0 +1,17 @@
+%YAML:1.0
+conf_parameters:
+   image_show: 1
+   image_return: 1
+   image_save: 1
+   image_depository: "D:\\logs\\algo_img"
+   image_backup_days: 7
+   model_path_grab: "D:/projects/graft/py_code/retina_tea5/TeaDetector_op9.onnx"    
+   object_threshold_grab: 0.45
+   nms_threshold_grab: 0.5
+
+   model_path_cut: "D:/projects/graft/py_code/retina_tea5/TeaDetector_op9.onnx"
+   object_threshold_cut: 0.45
+   nms_threshold_cut: 0.5
+   
+   
+

+ 331 - 0
tea_cv_api.cpp

@@ -0,0 +1,331 @@
+#include <string.h>
+#include <fstream>
+
+#include "tea_cv_api.h"
+#include "data_def.h"
+#include "data_def_api.h"
+#include "config.h"
+//#include "optimal_angle.h"
+//#include "cut_point_rs.h"
+//#include "cut_point_sc.h"
+#include "logger.h"
+#include "utils.h"
+#include "imstorage_manager.h"
+#include "tea_sorter.h"
+
+
+
+
+extern CRITICAL_SECTION g_cs;
+namespace graft_cv
+{
+
+	char *g_version_str = "0.1.1";
+
+	//configure
+	string g_conf_file = "./tcv_conf.yml";	
+	ConfigParam g_cp;
+
+	//logger
+	ofstream g_logger_ofs;
+	CGcvLogger g_logger = CGcvLogger(
+		g_logger_ofs,
+		CGcvLogger::file_and_terminal,
+		CGcvLogger::debug,
+		"./tcv.log");	
+
+	//image saver
+	
+	CImStoreManager* g_pImStore=0;
+
+	//implement	
+	CTeaSort g_tg = CTeaSort(g_cp, img_type::tea_grab, &g_logger);
+	CTeaSort g_tc = CTeaSort(g_cp, img_type::tea_cut, &g_logger);
+	
+
+	//??¶àÏß³ÌÓÐÎÊÌâ
+	map<string, cv::Mat> g_img_cache;
+	
+
+	//1 log path
+	int cv_set_logpath(char*lpath)
+	{	
+		try{
+			string mp = g_logger.getPath();
+			string np(lpath);
+			if(mp==np){
+				return 0;
+			}
+
+			g_logger_ofs.close();
+			g_logger_ofs.open(lpath,ofstream::out | ofstream::app);
+			string tmp = currTime() +" [WELCOME] ===========start logger===========\n";
+			
+			g_logger_ofs<<tmp;
+			g_logger_ofs.flush();
+			cout<<tmp<<endl;
+			g_logger.setPath(string(lpath));
+			return 0;
+		}
+		catch(...){
+			g_logger.ERRORINFO("set log path failed");
+			return 1;
+		}
+	}
+	//2  0-debug, 1-info, 2-warning, 3-error
+	int cv_set_loglevel(int lev)
+	{	
+		if(lev <0 || lev>3){
+			g_logger.ERRORINFO("log level error: should in [0,1,2,3]  0-debug, 1-info, 2-warning, 3-error");
+			return 1;
+		}
+		try{
+			switch(lev){
+			case 0:
+				g_logger.setLevel(CGcvLogger::debug);
+				break;
+			case 1:
+				g_logger.setLevel(CGcvLogger::info);
+				break;
+			case 2:
+				g_logger.setLevel(CGcvLogger::warning);
+				break;
+			case 3:
+				g_logger.setLevel(CGcvLogger::error);
+				break;
+			default:
+				g_logger.ERRORINFO("log level error: should in [0,1,2,3]  0-debug, 1-info, 2-warning, 3-error");
+				return 1;
+			}
+			return 0;
+		}
+		catch(...){
+			g_logger.ERRORINFO("set log level failed");
+			return 1;
+		}
+	}
+	//3
+	int cv_init_image_saver()
+	{		
+		if( g_cp.image_save){
+			if(g_pImStore){
+				string folder;
+				g_pImStore->getStoreDir(folder);
+				if(folder!=g_cp.image_depository){				
+					delete g_pImStore;
+					g_pImStore = new CImStoreManager();
+					g_pImStore->setStoreDir(g_cp.image_depository);
+					g_pImStore->setStoreDays(g_cp.image_backup_days);
+				}
+
+			}
+			else{
+				g_pImStore = new CImStoreManager();
+				g_pImStore->setStoreDir(g_cp.image_depository);
+				g_pImStore->setStoreDays(g_cp.image_backup_days);
+			}
+		}
+		else{
+			if(g_pImStore){
+				delete g_pImStore;
+				g_pImStore=0;
+			}			
+		}		
+		g_tg.set_image_saver(&g_pImStore);
+		g_tc.set_image_saver(&g_pImStore);
+		return 0;
+	}
+	
+
+	//4
+	int cv_init(char*conf)
+	{
+		//InitializeCriticalSection(&g_cs);
+		CGCvConfig conf_contrainer = CGCvConfig();
+		conf_contrainer.setConfParam(&g_cp);
+		if(conf){
+			//read configures
+			ifstream ifs(conf);
+			if(!ifs.good()){return 1;}			
+			ifs.close();
+			memset(&g_cp,0,sizeof(ConfigParam));
+
+			cv::FileStorage fs(conf, cv::FileStorage::READ);
+			conf_contrainer.read(fs["conf_parameters"]);	
+			fs.release();
+			g_conf_file = conf;
+			
+			
+		}
+		else{
+			ifstream ifs(g_conf_file);
+			if(!ifs.good()){return 1;}			
+			ifs.close();
+			memset(&g_cp,0,sizeof(ConfigParam));
+			
+			//read configures
+			cv::FileStorage fs(g_conf_file, cv::FileStorage::READ);
+			conf_contrainer.read(fs["conf_parameters"]);	
+			fs.release();
+			
+		}
+		string pinfo = get_cparam_info(&g_cp);
+		g_logger.INFO(string("lib version: ")+string(g_version_str));
+		g_logger.INFO(string("load parameters:\n")+pinfo);
+		
+		return 0;
+	};
+
+
+	//5
+	void cv_set_param(ConfigParam&cp)
+	{
+		g_cp = cp;	
+		string pinfo = get_cparam_info(&g_cp);
+		g_logger.INFO(string("set parameters:\n")+pinfo);
+	};
+	int cv_set_param_from_file(char*conf)
+	{
+		if (conf == 0) {
+			return 1;
+		}
+		ConfigParam cp;
+		CGCvConfig conf_contrainer = CGCvConfig();
+		conf_contrainer.setConfParam(&g_cp);
+		//read configures
+		ifstream ifs(conf);
+		if (!ifs.good()) { return 1; }
+		ifs.close();
+		
+		memset(&g_cp, 0, sizeof(ConfigParam));
+
+		cv::FileStorage fs(conf, cv::FileStorage::READ);
+		conf_contrainer.read(fs["conf_parameters"]);
+		fs.release();
+		g_conf_file = conf;
+		return 0;
+
+	}
+	
+	//6
+	int cv_release()
+	{
+		if(g_pImStore){
+			delete g_pImStore;
+			g_pImStore = 0;
+		}
+		//DeleteCriticalSection(&g_cs);
+		return 0;
+	}
+	
+	//7
+	void cv_get_conf_file(char*buff)
+	{
+		strcpy_s(buff, g_conf_file.size()+1, g_conf_file.c_str());
+	};
+
+	//8
+	void cv_save_param(char* conf_file/*=0*/)
+	{
+		//save configures
+		CGCvConfig conf_contrainer = CGCvConfig();
+		conf_contrainer.setConfParam(&g_cp);
+		if(conf_file){
+			cv::FileStorage fs(
+				conf_file,
+				cv::FileStorage::WRITE
+				);
+			fs<<"conf_parameters";	
+			fs<<conf_contrainer;		
+			fs.release();	
+		}
+		else{
+			cv::FileStorage fs(
+				g_conf_file, 
+				cv::FileStorage::WRITE
+				);
+			fs<<"conf_parameters";	
+			fs<<conf_contrainer;		
+			fs.release();	
+		}
+	}
+	//9
+	void cv_get_param(ConfigParam&cp)
+	{
+		cp = g_cp;		
+	};
+	//10
+	void get_version(char* buf)
+	{			
+		strcpy_s(buf, strlen(g_version_str)+1,g_version_str);
+	};
+	
+	//11
+	int tea_grab_point(
+		ImgInfo* imginfo,
+		PositionInfo& posinfo,
+		const char* fn
+		)
+	{
+		memset(&posinfo,0,sizeof(PositionInfo));
+		try{
+			int r = g_tg.load_model();	
+			if (r != 0) {
+				g_logger.ERRORINFO("model load failed");
+				return 1;
+			}
+			r = g_tg.detect(imginfo, posinfo, fn);
+			return r;						
+		}
+		catch(std::exception &err){
+			g_logger.ERRORINFO(err.what());
+			return 1;
+		}
+		catch(string& msg){
+			g_logger.ERRORINFO(msg);
+			return 1;
+		}		
+		catch(...){
+			g_logger.ERRORINFO("unknown error");
+			return 1;
+		}
+		return 0;
+		
+	}
+
+	//12
+	int tea_cut_point(
+		ImgInfo* imginfo,
+		PositionInfo& posinfo,
+		const char* fn
+	)
+	{
+		memset(&posinfo, 0, sizeof(PositionInfo));
+		try {
+			int r = g_tc.load_model();
+			if (r != 0) {
+				g_logger.ERRORINFO("model load failed");
+				return 1;
+			}
+			r = g_tc.detect(imginfo, posinfo, fn);
+			return r;
+		}
+		catch (std::exception &err) {
+			g_logger.ERRORINFO(err.what());
+			return 1;
+		}
+		catch (string& msg) {
+			g_logger.ERRORINFO(msg);
+			return 1;
+		}
+		catch (...) {
+			g_logger.ERRORINFO("unknown error");
+			return 1;
+		}
+		return 0;
+
+	}
+
+
+
+};

+ 120 - 0
tea_cv_api.h

@@ -0,0 +1,120 @@
+/*
+tea sorter
+*/ 
+
+
+#pragma once
+
+#include "data_def_api.h"
+using namespace std;
+
+//定义TCV_DEBUG,每一步图像处理都会输出中间结果图片显示(opencv),回车后继续执行,用于测试
+// #define TCV_DEBUG
+// export 
+#define TCV_EXPORTS
+
+
+#ifdef TCV_EXPORTS
+#define TCV_API __declspec(dllexport)
+#else
+#define TCV_API __declspec(dllimport)
+#endif
+
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+namespace graft_cv
+{
+//1 设置log路径
+	TCV_API	int cv_set_logpath(char*lpath);
+
+//2 设置log等级
+	TCV_API	int cv_set_loglevel(int lev);// 0-debug, 1-info, 2-warning, 3-error
+
+//3 设置是否存储图片(存储路径在configure文件中),true--保存,false--不保存
+	TCV_API int cv_init_image_saver();
+
+//4 初始化:本地配置文件初始化(yml)
+//  返回: 0- 正常; 1- 配置文件不存在 
+	TCV_API int cv_init(char*conf_file);
+
+
+//5 初始化:通过ConfigParam结构体对象直接赋值配置参数(内存)
+	TCV_API void cv_set_param(ConfigParam&);
+	TCV_API int cv_set_param_from_file(char*conf_file);
+
+//6 接口退出前的释放
+	TCV_API int cv_release();
+
+//7 获取当前配置文件路径,输入char*要空间足够,内部没有检测是否越界
+	TCV_API void cv_get_conf_file(char*);
+
+//8 保存到本地配置文件中(覆盖)
+	TCV_API void cv_save_param(char* conf_file/*=0*/);
+
+//9 获取当前的配置参数
+	TCV_API void cv_get_param(ConfigParam&);
+
+//10 获取当前版本号,char*要空间足够,内部没有检测是否越界
+	TCV_API void get_version(char* buf);
+
+//11 找到抓取位置
+//
+//  输入: 
+//			ImgInfo------- 输入
+//         posinfo ------- 输出
+//         fn ------------ 输入, points指向0,且fn可用时,读取文件中的数据(用于测试),仅支持ply格式
+//				
+				//[0] tea_grab_x1;		//第一株茶叶抓取位置x1,
+				//[1] tea_grab_y1;		//第一株茶叶抓取位置y1,
+				//[2] tea_grab_angle1;	//第一株茶叶抓取角度r1,
+				//[3] tea_grab_x2;		//第二株茶叶抓取位置x2,
+				//[4] tea_grab_y2;		//第二株茶叶抓取位置y2,
+				//[5] tea_grab_angle2;	//第二株茶叶抓取角度r2,
+				//[6] tea_cut_x1;		//第一株茶叶切割位置x1,
+				//[7] tea_cut_y1;		//第一株茶叶切割位置y1,
+				//[8] tea_cut_angle1;	//第一株茶叶切割角度r1,
+				//[9] tea_cut_x2;		//第二株茶叶切割位置x2,
+				//[10] tea_cut_y2;		//第二株茶叶切割位置y2,
+				//[11] tea_cut_angle2;	//第二株茶叶切割角度r2,
+	            // 如果为0,则不存在
+//				posinfo.pp_images;
+//		
+//  返回: 0- 正常; 其他- 失败
+	TCV_API int tea_grab_point(ImgInfo*, PositionInfo& posinfo, const char* fn=0);
+	
+	//12 找到切割位置
+	//
+	//  输入: 
+	//			ImgInfo------- 输入
+	//         posinfo ------- 输出
+	//         fn ------------ 输入, points指向0,且fn可用时,读取文件中的数据(用于测试),仅支持ply格式
+	//				
+	//[0] tea_grab_x1;		//第一株茶叶抓取位置x1,
+	//[1] tea_grab_y1;		//第一株茶叶抓取位置y1,
+	//[2] tea_grab_angle1;	//第一株茶叶抓取角度r1,
+	//[3] tea_grab_x2;		//第二株茶叶抓取位置x2,
+	//[4] tea_grab_y2;		//第二株茶叶抓取位置y2,
+	//[5] tea_grab_angle2;	//第二株茶叶抓取角度r2,
+	//[6] tea_cut_x1;		//第一株茶叶切割位置x1,
+	//[7] tea_cut_y1;		//第一株茶叶切割位置y1,
+	//[8] tea_cut_angle1;	//第一株茶叶切割角度r1,
+	//[9] tea_cut_x2;		//第二株茶叶切割位置x2,
+	//[10] tea_cut_y2;		//第二株茶叶切割位置y2,
+	//[11] tea_cut_angle2;	//第二株茶叶切割角度r2,
+	// 如果为0,则不存在
+	//				posinfo.pp_images;
+	//		
+	//  返回: 0- 正常; 其他- 失败
+	TCV_API int tea_cut_point(ImgInfo*, PositionInfo& posinfo, const char* fn = 0);
+
+
+
+};//namespace tea_cv
+
+#ifdef __cplusplus
+}
+#endif

+ 275 - 0
tea_detect.cpp

@@ -0,0 +1,275 @@
+#include "tea_detect.h"
+#include <opencv.hpp>
+#include <numeric>
+
+using namespace cv;
+using namespace std;
+
+
+namespace graft_cv {
+	RetinaDrop::RetinaDrop(CGcvLogger* pLogger, float obj_th, float nms_th)
+		:m_model_loaded(false)
+	{
+		BATCH_SIZE = 1;
+		INPUT_CHANNEL = 3;
+		IMAGE_WIDTH = 640; // default 640
+		IMAGE_HEIGHT = 640; // default 640
+		m_obj_threshold = obj_th;//default 0.6; 
+		m_nms_threshold = nms_th; //default0.4; 	
+		
+		m_anchor_num = 2;
+		m_bbox_head = 4;
+		
+		m_variance[0] = 0.1f;
+		m_variance[1] = 0.2f;
+		//m_img_mean(123.0, 104.0, 117.0)
+		m_img_mean[0] = 123.0;
+		m_img_mean[1] = 104.0;
+		m_img_mean[2] = 117.0;
+		m_img_mean[3] = 0;
+		//cv::Size size_detection(640, 640)	
+		m_size_detection.width = IMAGE_WIDTH;
+		m_size_detection.height = IMAGE_HEIGHT;
+		m_feature_steps = {8,16,32};
+		m_pLogger = pLogger;
+
+		for (const int step : m_feature_steps) {
+			assert(step != 0);
+			int feature_map = IMAGE_HEIGHT / step;
+			m_feature_maps.push_back(feature_map);
+			int feature_size = feature_map * feature_map;
+			m_feature_sizes.push_back(feature_size);
+		}
+		m_anchor_sizes = { { 16,32 } ,{ 64,128},{ 256, 512 }};
+		m_sum_of_feature = std::accumulate(m_feature_sizes.begin(), m_feature_sizes.end(), 0) * m_anchor_num;		
+		generate_anchors();
+		if (m_pLogger) {
+			m_pLogger->INFO(string("RetinaDrop object initialized"));
+		}
+	}
+
+	RetinaDrop::~RetinaDrop() = default;
+
+	bool RetinaDrop::IsModelLoaded() {
+		return m_model_loaded;
+	};
+	void RetinaDrop::SetThreshold(float object_threshold, float nms_threshold)
+	{
+		this->m_obj_threshold = object_threshold;
+		this->m_nms_threshold = nms_threshold;
+	}
+
+	bool RetinaDrop::LoadModel(std::string onnx_path) {
+		if (m_pLogger) {
+			m_pLogger->INFO(string("Loading detection model: ")+onnx_path);
+		}
+		else { std::cout << "Loading detection model: " << onnx_path<<std::endl; }
+		try {
+			m_model = cv::dnn::readNetFromONNX(onnx_path);
+			if (m_pLogger) {m_pLogger->INFO(string("Detection model loaded"));}		
+			m_model_loaded = true;
+			return m_model_loaded;
+		}
+		catch (...)
+		{
+			if (m_pLogger) { m_pLogger->ERRORINFO(string("loading model failed")); }
+		}
+		return false;
+	}
+
+	std::vector<Bbox> RetinaDrop::RunModel(cv::Mat& img, CGcvLogger* pInstanceLogger)
+	{	
+		std::vector<Bbox> result;	
+		if (img.empty()) {
+			if (pInstanceLogger) {
+				pInstanceLogger->ERRORINFO(string("RunModel(), input image is empty"));
+			}
+			throw(string("image is empty"));
+		}
+		if (!m_model_loaded) {
+			pInstanceLogger->ERRORINFO(string("model is NOT loaded"));
+		}
+		cv::Mat blob = cv::dnn::blobFromImage(
+			img, 
+			1.0, 
+			m_size_detection,
+			m_img_mean);
+		m_model.setInput(blob);		
+
+		std::vector<std::string> outNames = m_model.getUnconnectedOutLayersNames();
+		vector<Mat>outputs;// location(1x16800x4), confidence(1x16800x2), keypoint(1x16800x2)
+		if (pInstanceLogger) {
+			pInstanceLogger->INFO(string("RunModel(), before forward()"));
+		}
+		m_model.forward(outputs, outNames);	
+		std::vector<RetinaDrop::DropRes> rects;
+		int n = post_process(img, outputs,rects);		
+		for (const auto& rect : rects) {
+			Bbox box;
+			box.score = rect.confidence;			
+			box.x1 = (int)rect.drop_box.x1;
+			box.y1 = (int)rect.drop_box.y1;
+			box.x2 = (int)rect.drop_box.x2;
+			box.y2 = (int)rect.drop_box.y2;
+			box.ppoint[0] = rect.keypoints[0].x;
+			box.ppoint[1] = rect.keypoints[0].y;
+			box.ppoint[2] = rect.keypoints[1].x;
+			box.ppoint[3] = rect.keypoints[1].y;
+			box.ppoint[4] = rect.keypoints[2].x;
+			box.ppoint[5] = rect.keypoints[2].y;
+			box.ppoint[6] = rect.keypoints[3].x;
+			box.ppoint[7] = rect.keypoints[3].y;
+			box.ppoint[8] = rect.keypoints[4].x;
+			box.ppoint[9] = rect.keypoints[4].y;
+
+			box.area = 0.0;
+			result.push_back(box);
+		}
+		if (pInstanceLogger) {
+			stringstream buff;
+			buff << "detected object: " << n;
+			pInstanceLogger->INFO(buff.str());
+		}
+		return result;
+	}
+
+	void RetinaDrop::generate_anchors() {
+		m_refer_matrix = cv::Mat(m_sum_of_feature, m_bbox_head, CV_32FC1);
+		int line = 0;
+		for (size_t feature_map = 0; feature_map < m_feature_maps.size(); feature_map++) {
+			for (int height = 0; height < m_feature_maps[feature_map]; ++height) {
+				for (int width = 0; width < m_feature_maps[feature_map]; ++width) {
+					for (int anchor = 0; anchor < m_anchor_sizes[feature_map].size(); ++anchor) {
+						auto* row = m_refer_matrix.ptr<float>(line);
+						row[0] = (float)(width+0.5) * m_feature_steps[feature_map]/(float)IMAGE_WIDTH;
+						row[1] = (float)(height+0.5) * m_feature_steps[feature_map]/(float)IMAGE_HEIGHT;
+						row[2] = m_anchor_sizes[feature_map][anchor]/(float)IMAGE_WIDTH;
+						row[3] = m_anchor_sizes[feature_map][anchor]/(float)IMAGE_HEIGHT;
+						line++;
+					}
+				}
+			}
+		}
+	}
+	int RetinaDrop::post_process(
+		cv::Mat &src_img, 
+		vector<cv::Mat> &result_matrix,
+		std::vector<RetinaDrop::DropRes>& valid_result
+		) 
+	{
+		valid_result.clear();
+		std::vector<DropRes> result;		
+		for (int item = 0; item < m_sum_of_feature; ++item) {
+			float* cur_bbox = (float*)result_matrix[0].data + item * 4;//result_matrix[0].step;
+			float* cur_conf = (float*)result_matrix[2].data + item * 2;//result_matrix[1].step;			
+			float* cur_keyp = (float*)result_matrix[1].data + item * 10;//result_matrix[2].step;
+			
+			if (cur_conf[1] > m_obj_threshold) {				
+				DropRes headbox;
+				headbox.confidence = cur_conf[1];
+				auto* anchor = m_refer_matrix.ptr<float>(item);				
+				auto* keyp = cur_keyp;				
+
+				float cx, cy, kx, ky;
+				cx = anchor[0] + cur_bbox[0] * m_variance[0] * anchor[2];
+				cy = anchor[1] + cur_bbox[1] * m_variance[0] * anchor[3];
+				kx = anchor[2] * exp(cur_bbox[2] * m_variance[1]);
+				ky = anchor[3] * exp(cur_bbox[3] * m_variance[1]);
+
+				cx -= kx / 2.0f;
+				cy -= ky / 2.0f;
+				kx += cx;
+				ky += cy;
+
+				headbox.drop_box.x1 = cx * src_img.cols;
+				headbox.drop_box.y1 = cy * src_img.rows;
+				headbox.drop_box.x2 = kx * src_img.cols;
+				headbox.drop_box.y2 = ky * src_img.rows;
+
+				for (int ki = 0; ki < 5; ++ki) {
+					float kp_x = anchor[0] + keyp[2*ki] * m_variance[0] * anchor[2];
+					float kp_y = anchor[1] + keyp[2*ki+1] * m_variance[0] * anchor[3];
+					kp_x *= src_img.cols;
+					kp_y *= src_img.rows;
+					headbox.keypoints.push_back(cv::Point2f(kp_x, kp_y));
+				}
+				/*float kp_x = anchor[0] + keyp[0] * m_variance[0] * anchor[2];
+				float kp_y = anchor[1] + keyp[1] * m_variance[0] * anchor[3];
+				kp_x *= src_img.cols;
+				kp_y *= src_img.rows;
+				headbox.keypoints = {
+					cv::Point2f(kp_x,kp_y)					
+				};*/
+				result.push_back(headbox);
+			}
+		}
+		vector<int> keep;
+		nms_detect(result,keep);		
+		for (size_t i = 0; i < keep.size(); ++i) {			
+			valid_result.push_back(result[keep[i]]);
+		}
+		return (int)valid_result.size();
+	}
+
+	void RetinaDrop::nms_detect(
+		std::vector<DropRes> & detections,
+		vector<int> & keep)
+	{
+		keep.clear();
+		if (detections.size() == 1) {
+			keep.push_back(0);
+			return;
+		}
+
+		sort(detections.begin(), detections.end(), 
+			[=](const DropRes& left, const DropRes& right) {
+			return left.confidence > right.confidence;
+		});
+		
+		vector<int> order;
+		for (size_t i = 0; i < detections.size(); ++i) { order.push_back((int)i); }
+
+		while (order.size()) {
+			int i = order[0];
+			keep.push_back(i);
+			vector<int> del_idx;
+			for (size_t j = 1; j < order.size(); ++j) {
+				float iou = iou_calculate(
+					detections[i].drop_box, 
+					detections[order[j]].drop_box);
+				if (iou > m_nms_threshold) {
+					del_idx.push_back((int)j);
+				}
+			}
+			vector<int> order_update;
+			for (size_t j = 1; j < order.size(); ++j) {
+				vector<int>::iterator it = find(del_idx.begin(), del_idx.end(), j);
+				if (it == del_idx.end()) {
+					order_update.push_back(order[j]);
+				}
+			}
+			order.clear();
+			order.assign(order_update.begin(), order_update.end());
+		}	
+	}
+
+	float RetinaDrop::iou_calculate(
+		const RetinaDrop::DropBox & det_a, 
+		const RetinaDrop::DropBox & det_b) 
+	{
+		float aa = (det_a.x2 - det_a.x1 + 1) * (det_a.y2 - det_a.y1 + 1);
+		float ab = (det_b.x2 - det_b.x1 + 1) * (det_b.y2 - det_b.y1 + 1);
+
+		float xx1 = max(det_a.x1, det_b.x1);
+		float yy1 = max(det_a.y1, det_b.y1);
+		float xx2 = min(det_a.x2, det_b.x2);
+		float yy2 = min(det_a.y2, det_b.y2);
+
+		float w = (float)max(0.0, xx2 - xx1 + 1.0);
+		float h = (float)max(0.0, yy2 - yy1 + 1.0);
+		float inter = w * h;
+		float ovr = inter / (aa + ab - inter);
+		return ovr;		
+	}	
+	float RetinaDrop::GetNmsThreshold() { return m_nms_threshold; }
+}

+ 74 - 0
tea_detect.h

@@ -0,0 +1,74 @@
+#pragma once
+
+#include <opencv.hpp>
+#include "logger.h"
+#include "data_def.h"
+
+
+namespace graft_cv {
+
+	class RetinaDrop {
+		struct DropBox {
+			float x1;
+			float y1;
+			float x2;
+			float y2;
+		};
+
+		struct DropRes {
+			float confidence;
+			DropBox drop_box;
+			std::vector<cv::Point2f> keypoints;			
+		};
+
+	public:
+		explicit RetinaDrop(CGcvLogger* pLogger=0, float obj_th=0.6, float nms_th=0.4);
+		~RetinaDrop();
+		bool LoadModel(std::string onnx_path); 
+		std::vector<Bbox> RunModel(cv::Mat& img, CGcvLogger* pInstanceLogger=0);
+		bool IsModelLoaded();
+		float GetNmsThreshold();
+		void SetThreshold(float object_threshold, float nms_threshold);
+
+	private:
+		void generate_anchors();		
+		int post_process(
+			cv::Mat &vec_Mat, 
+			std::vector<cv::Mat> &result_matrix, 
+			std::vector<RetinaDrop::DropRes>& valid_result);
+
+		void nms_detect(
+			std::vector<DropRes>& detections,
+			std::vector<int>& keep);
+
+		static float iou_calculate(
+			const DropBox& det_a, 
+			const DropBox& det_b);
+
+		int BATCH_SIZE; //default 1
+		int INPUT_CHANNEL; //default 3
+		int IMAGE_WIDTH; //default 640
+		int IMAGE_HEIGHT; //default 640
+		float m_obj_threshold; // default 0.5
+		float m_nms_threshold; // default 0.45		
+		
+
+		cv::Mat m_refer_matrix;
+		int m_anchor_num;
+		int m_bbox_head;
+		
+		std::vector<int> m_feature_sizes;
+		std::vector<int> m_feature_steps; 
+		std::vector<int> m_feature_maps;
+		std::vector<std::vector<int>>m_anchor_sizes; 
+		int m_sum_of_feature;
+
+		cv::dnn::Net m_model;
+		float m_variance[2];
+		cv::Scalar m_img_mean;
+		cv::Size m_size_detection;
+		bool m_model_loaded;
+		CGcvLogger* m_pLogger;
+	};
+
+}

+ 277 - 0
tea_sorter.cpp

@@ -0,0 +1,277 @@
+#include <opencv.hpp>
+#include <math.h>
+#include <io.h>
+#include "tea_sorter.h"
+#include "utils.h"
+
+
+
+using namespace cv;
+
+namespace graft_cv{
+
+CTeaSort::CTeaSort(
+	ConfigParam& cp,	
+	img_type dtpye,
+	CGcvLogger*pLog)
+:
+m_cp(cp),  
+m_dtype(dtpye),
+m_pLogger(pLog),
+m_ppImgSaver(0),
+m_pImginfoRaw(0),
+m_pImginfoDetected(0)
+{
+	m_drop_detector = RetinaDrop(m_pLogger, 0.5, 0.5);
+}
+
+CTeaSort::~CTeaSort()
+{
+	clear_imginfo();
+}
+
+int CTeaSort::detect(
+	ImgInfo*imginfo,
+	PositionInfo& posinfo, 
+	const char* fn
+	)
+{	
+
+	//m_head_droplets.clear();
+	////0 文件目录有效性检测
+	//int at = _access(m_temporary_dir.c_str(),0);
+	//int ai = _access(m_image_dir.c_str(),0);
+	//if(at==-1){
+	//	m_pLogger->ERRORINFO(
+	//		string("invalid temporary folder: ")+m_temporary_dir);
+	//	return 1;
+	//}
+	//if(ai==-1){
+	//	m_pLogger->ERRORINFO(
+	//		string("invalid image folder: ")+m_image_dir);
+	//	return 1;
+	//}	
+	//1 model status
+	if (!m_drop_detector.IsModelLoaded()) {
+		m_pLogger->ERRORINFO(
+			string("drople detect model NOT loaded"));
+		return 1;
+	}
+	//2 update recognize threshold
+	if (m_dtype == img_type::tea_grab) {
+		m_drop_detector.SetThreshold(m_cp.object_threshold_grab, m_cp.nms_threshold_grab);
+	}
+	else {
+		m_drop_detector.SetThreshold(m_cp.object_threshold_cut, m_cp.nms_threshold_cut);
+	}
+
+
+	//3 load data
+	load_data(imginfo, fn);
+
+	//4 detect	
+	vector<Bbox> droplets_raw = m_drop_detector.RunModel(m_raw_img,m_pLogger);
+	if (m_pLogger) {		
+		stringstream buff_;
+		buff_ << m_imgId<<m_dtype_str << "image detect over. tea number is " << droplets_raw.size();
+		m_pLogger->INFO(buff_.str());
+	}
+
+	//5 nms, width(height) filt and area calculation
+	vector<Bbox> droplets;
+	vector<int> keep;
+	nms_bbox(droplets_raw, m_drop_detector.GetNmsThreshold(), keep);
+	//width(height) filter
+	for (int i : keep) {		
+		droplets.push_back(droplets_raw[i]);
+	}
+	m_pLogger->INFO(string("nms droplets"));
+	
+	int valid_cnt = 0;
+	for (int i = 0; i < droplets.size();++i) {
+		if (i > 1) { break; }
+		Bbox&b = droplets.at(i);
+		double angle = calalate_angle(b);
+		valid_cnt += 1;
+		//grab point 
+		if (i == 0) {
+			if (m_dtype == img_type::tea_grab) {
+				posinfo.tea_grab_x1 = b.ppoint[8];
+				posinfo.tea_grab_y1 = b.ppoint[9];
+				posinfo.tea_grab_angle1 = angle;
+			}
+			else {
+				posinfo.tea_cut_x1 = b.ppoint[6];
+				posinfo.tea_cut_y1 = b.ppoint[7];
+				posinfo.tea_cut_angle1 = angle;
+			}			
+		}
+		else {
+			if (m_dtype == img_type::tea_grab) {
+				posinfo.tea_grab_x2 = b.ppoint[8];
+				posinfo.tea_grab_y2 = b.ppoint[9];
+				posinfo.tea_grab_angle2 = angle;
+			}
+			else {
+				posinfo.tea_cut_x2 = b.ppoint[6];
+				posinfo.tea_cut_y2 = b.ppoint[7];
+				posinfo.tea_cut_angle2 = angle;
+			}
+
+		}		
+	}
+
+	//6 draw
+	if (m_cp.image_return) {
+		this->clear_imginfo();
+		cv::Mat img_rst = m_raw_img.clone();
+		int cnt = 0;
+		for (auto& b : droplets) {
+			char name[256];
+			cv::Scalar color(20, 0, 0);//bgr
+			
+			sprintf_s(name, "%.2f", b.score);
+			cv::putText(img_rst, name,
+				cv::Point(b.x1, b.y1),
+				cv::FONT_HERSHEY_COMPLEX, 0.7, color, 2);
+
+			cv::Rect r = cv::Rect(cv::Point2i(b.x1, b.y1), cv::Point2i(b.x2, b.y2));
+			if (cnt < 2) {
+				cv::rectangle(img_rst, r, cv::Scalar(0, 0, 255));
+			}
+			else {
+				cv::rectangle(img_rst, r, cv::Scalar(0, 255, 0));
+			}
+
+			cv::rectangle(img_rst, r, cv::Scalar(0, 0, 255));
+			cv::circle(img_rst, cv::Point(int(b.ppoint[0]), int(b.ppoint[1])), 4, cv::Scalar(0, 0, 255), -1, 8, 0);
+			cv::circle(img_rst, cv::Point(int(b.ppoint[2]), int(b.ppoint[3])), 4, cv::Scalar(0, 255, 255), -1, 8, 0);
+			cv::circle(img_rst, cv::Point(int(b.ppoint[4]), int(b.ppoint[5])), 4, cv::Scalar(255, 0, 255), -1, 8, 0);
+			cv::circle(img_rst, cv::Point(int(b.ppoint[6]), int(b.ppoint[7])), 4, cv::Scalar(0, 255, 0), -1, 8, 0);
+			cv::circle(img_rst, cv::Point(int(b.ppoint[8]), int(b.ppoint[9])), 4, cv::Scalar(255, 0, 0), -1, 8, 0);
+			cnt += 1;
+
+		}
+		m_pImginfoRaw = mat2imginfo(m_raw_img);
+		m_pImginfoDetected = mat2imginfo(img_rst);
+		posinfo.pp_images[0] = m_pImginfoRaw;
+		posinfo.pp_images[1] = m_pImginfoDetected;
+
+		if (m_ppImgSaver && *m_ppImgSaver) {
+			(*m_ppImgSaver)->saveImage(img_rst, m_imgId + "_rst_0");			
+		}
+	}
+	//拍照无苗, 返回识别结果-1
+	if (valid_cnt == 0) { return -1; }
+	return 0;
+}
+
+double CTeaSort::calalate_angle(Bbox&b) {
+	double angle = 0.0;
+	float x3,y3,x4,y4,x5,y5;
+	x3 = b.ppoint[4];
+	y3 = b.ppoint[5];
+	x4 = b.ppoint[6];
+	y4 = b.ppoint[7];
+	x5 = b.ppoint[8];
+	y5 = b.ppoint[9];
+	double r45 = sqrt((x4 - x5)*(x4 - x5) + (y4 - y5)*(y4 - y5));
+	if (r45 < 15.0) {
+		angle = atan2(x5 - x3, y5 - y3);
+	}
+	else {
+		angle = atan2(x5 - x4, y5 - y4);
+	}
+	angle *= (180.0 / 3.1415926);
+	return angle;
+
+}
+
+int CTeaSort::load_data(
+	ImgInfo*imginfo,
+	const char* fn/* = 0*/)
+{
+	//数据加载功能实现,并生成imageid,保存原始数据到文件
+	int rst = 0;
+	//generate image id
+	if (m_dtype == img_type::tea_grab) {
+		m_imgId = getImgId(img_type::tea_grab);
+		m_dtype_str = string(" tea_grab ");
+	}
+	else {
+		m_imgId = getImgId(img_type::tea_cut);
+		m_dtype_str = string(" tea_cut ");
+	}
+
+	if (imginfo) {
+		if (m_pLogger) {
+			stringstream buff;
+			buff << m_imgId << m_dtype_str << "image, width=" << imginfo->width
+				<< "\theight=" << imginfo->height;
+			m_pLogger->INFO(buff.str());
+		}
+		if (!isvalid(imginfo)) {
+			if (m_pLogger) {
+				m_pLogger->ERRORINFO(m_imgId + m_dtype_str + "input image invalid.");
+			}
+			throw_msg(m_imgId + " invalid input image");
+
+		}
+		m_raw_img = imginfo2mat(imginfo);
+	}
+	else {
+		cv::Mat img = imread(fn, cv::IMREAD_COLOR);
+		if (img.empty()) {
+			if (m_pLogger) {
+				m_pLogger->ERRORINFO(m_imgId + m_dtype_str + "input image invalid:" + string(fn));
+			}
+			throw_msg(m_imgId + m_dtype_str + "invalid input image: " + string(fn));
+
+		}
+		if (m_pLogger) {
+			stringstream buff;
+			buff << m_imgId << m_dtype_str << "image, width=" << img.cols
+				<< "\theight=" << img.rows;
+			m_pLogger->INFO(buff.str());
+		}
+
+		m_raw_img = img.clone();
+	}
+	//image saver
+	if (m_ppImgSaver && *m_ppImgSaver) {
+		(*m_ppImgSaver)->saveImage(m_raw_img, m_imgId);
+	}
+	return rst;
+}
+
+
+int CTeaSort::load_model()
+{
+	bool b = false;
+	if (!m_drop_detector.IsModelLoaded()) {
+		if (m_dtype == img_type::tea_grab) {
+			b = m_drop_detector.LoadModel(m_cp.model_path_grab);
+		}
+		else {
+			b = m_drop_detector.LoadModel(m_cp.model_path_cut);
+		}
+		
+	}
+	else {
+		b = true;
+	}
+	return b ? 0 : 1;
+}
+
+void CTeaSort::clear_imginfo() {
+	if (m_pImginfoDetected) {
+		imginfo_release(&m_pImginfoDetected);
+		m_pImginfoDetected = 0;
+	}	
+	if (m_pImginfoRaw) {
+		imginfo_release(&m_pImginfoRaw);
+		m_pImginfoRaw = 0;
+	}
+}
+
+}

+ 46 - 0
tea_sorter.h

@@ -0,0 +1,46 @@
+#pragma once
+
+#include <opencv.hpp>
+#include "data_def_api.h"
+#include "data_def.h"
+#include "logger.h"
+#include "tea_detect.h"
+#include "imstorage_manager.h"
+
+using namespace cv;
+namespace graft_cv{
+
+	class CTeaSort{
+	public:
+		CTeaSort(
+			ConfigParam& ap,			
+			img_type dtype,
+			CGcvLogger*pLog = 0
+		);
+		~CTeaSort();
+		int detect(				
+			ImgInfo*, PositionInfo& posinfo, const char* fn = 0);
+		
+		int load_data(
+			ImgInfo*imginfo,
+			const char* fn = 0);
+
+		int load_model();
+		void set_image_saver(CImStoreManager** ppis) { m_ppImgSaver = ppis; }
+	private:
+		img_type m_dtype; // data type: tea_grab, tea_cut
+		string m_imgId;
+		string m_dtype_str;
+		Mat m_raw_img;
+		ConfigParam& m_cp;
+		RetinaDrop m_drop_detector;
+		CImStoreManager** m_ppImgSaver;
+		CGcvLogger* m_pLogger;
+
+		ImgInfo* m_pImginfoRaw;
+		ImgInfo* m_pImginfoDetected; 
+
+		void clear_imginfo();
+		double calalate_angle(Bbox&b);
+	};
+};

+ 1910 - 0
utils.cpp

@@ -0,0 +1,1910 @@
+#include "utils.h"
+#include <opencv.hpp>
+#include <time.h>
+#include <algorithm>
+#include <math.h>
+
+namespace graft_cv{
+
+	cv::Mat imginfo2mat(ImgInfo* imginfo)
+	{
+		assert(imginfo->data);
+		assert(imginfo->height>0 && imginfo->width>0);
+		assert(imginfo->channel == 1 || imginfo->channel == 3);
+		cv::Mat m;
+		if (imginfo->channel == 1) {
+			m = cv::Mat(imginfo->height, imginfo->width, CV_8UC1);
+		}
+		if (imginfo->channel == 3) {
+			m = cv::Mat(imginfo->height, imginfo->width, CV_8UC3);
+		}
+
+		for(int h=0; h<imginfo->height; ++h)
+		{
+			memcpy((void*)(m.ptr(h)),
+				(void*)(imginfo->data+h*imginfo->width * imginfo->channel),
+				imginfo->width * imginfo->channel);
+		};
+		return m;
+	};
+
+	ImgInfo* mat2imginfo(const cv::Mat&img){
+		assert(img.channels()==1 || img.channels() == 3);
+		ImgInfo* imginfo = new ImgInfo();
+		imginfo->channel= img.channels();
+		imginfo->height=img.rows;
+		imginfo->width = img.cols;
+		imginfo->data = 0;
+		imginfo->data = new byte[imginfo->height * imginfo->width * imginfo->channel];
+		memset(imginfo->data, 0,imginfo->height * imginfo->width * imginfo->channel);
+		//IplImage ipl_img = cvIplImage(img);
+		for(int i =0; i<imginfo->height;++i){
+			memcpy(imginfo->data+i*imginfo->width * imginfo->channel, img.ptr(i), imginfo->width * imginfo->channel);
+		}
+
+		return imginfo;
+	};
+	void imginfo_release(ImgInfo**ppImgInfo){
+		ImgInfo* pImgInfo = *ppImgInfo;
+		if(pImgInfo->data){
+			delete [] pImgInfo->data;
+			pImgInfo->data=0;
+		}
+		delete pImgInfo;
+		pImgInfo = 0;
+	};
+	bool isvalid(const ImgInfo*imginfo)
+	{
+		if(!imginfo){return false;}
+		if(!imginfo->data){return false;}
+		if(imginfo->height*imginfo->width<=0){return false;}
+		return true;
+	}
+
+	void image_set_bottom(
+		cv::Mat& img,
+		unsigned char value,
+		int rows_cnt)
+	{
+		for(size_t r=(img.rows-rows_cnt);r<img.rows;++r){
+			for(size_t c=0;c<img.cols;++c){
+				img.at<unsigned char>(r,c) = value;			
+			}
+		}
+	}
+	void image_set_top(
+		cv::Mat& img,
+		unsigned char value,
+		int rows_cnt)
+	{
+		for(size_t r=0;r<rows_cnt;++r){
+			for(size_t c=0;c<img.cols;++c){
+				img.at<unsigned char>(r,c) = value;			
+			}
+		}
+	}
+
+	void image_draw_line(
+		cv::Mat& img,
+		int x0,int y0,
+		int x1,int y1)
+	{
+		cv::Point p0,p1;
+		if(x0==x1){
+			p0.x=x0;
+			p0.y=0;
+			p1.x=x0;
+			p1.y=img.rows-1;
+		}
+		else{
+			double k = (double)(y1-y0)/(double)(x1-x0);
+			double b = (double)y0 - k * x0;
+
+			p0.x=0;
+			p0.y=b;
+
+			p1.x=img.cols;
+			p1.y = (int)(b + k * p1.x);
+		}
+		cv::line(img,p0,p1, cv::Scalar(255,255,255));
+	
+	}
+	string currTime(){
+			char tmp[64];
+			struct tm ptime;
+			time_t time_seconds = time(0);
+			localtime_s(&ptime, &time_seconds);
+			strftime(
+				tmp, 
+				sizeof(tmp), 
+				"%Y-%m-%d %H:%M:%S",
+				&ptime
+				);
+			return tmp;
+		}
+	// function getImgId
+	// input im_type, img_type
+	static unsigned int ID_COUNTER = 0;
+	string getImgId(int im_type)
+	{
+		char tmp[64];
+		struct tm ptime;
+		time_t time_seconds = time(0);
+		localtime_s(&ptime, &time_seconds);
+		strftime(
+			tmp, 
+			sizeof(tmp), 
+			"%Y%m%d%H%M%S",
+			&ptime
+			);
+
+		unsigned int id_serial = ID_COUNTER % 100;	
+		stringstream buff;
+		buff<<tmp;
+		buff.width(2);
+		buff.fill('0');	
+		buff<<id_serial;
+		buff<<"_"<<im_type;
+		ID_COUNTER+=1;	
+		return buff.str();
+	}
+	string getImgIdOa(string iid, int im_idx)
+	{	
+		stringstream buff;
+		buff<<im_idx;
+		return iid+"_"+buff.str();		
+	}
+
+	inline void throw_msg(string& msg){	
+			stringstream buff;
+			buff<<"Error:"<<msg<<"\tfile:"<<__FILE__<<"\tline:"<<__LINE__;			
+			throw(buff.str());		
+		}
+
+	void drawgrid(		
+			cv::Mat&img,		
+			int grid_col/*=50*/,
+			int grid_row/*=50*/,
+			int line_thick/*=2*/
+			)
+	{
+		if(img.empty()){return;}
+		//horizontal grid
+		for(int row=img.rows-1; row>=0; row-=grid_row){
+			cv::line(img, cv::Point(0,row), cv::Point(img.cols-1,row), cv::Scalar(100),line_thick);
+		}
+		//veritcal grid
+		for(int col=0; col<img.cols; col+=grid_col){
+			cv::line(img, cv::Point(col,0), cv::Point(col,img.rows-1), cv::Scalar(100),line_thick);
+		}
+
+	}
+	void hist2image(
+		vector<int>& hist, 
+		cv::Mat&img,
+		int aix, /*=1*/
+		int grid_col/*=50*/,
+		int grid_row/*=50*/
+		)
+	{
+		vector<int>::iterator maxit = max_element(begin(hist),end(hist)); 
+		if( grid_col<10){grid_col=10;}
+		if( grid_row<10){grid_row=10;}
+		// aix = 0, column hist, aix=1, row histogtam
+		if (aix==0){
+			img = cv::Mat::zeros(hist.size(),*maxit, CV_8UC1);
+			drawgrid(img,grid_col, grid_row,1);
+			for(size_t i=0; i<hist.size();++i){
+				if(i>=img.rows){break;}
+				for(size_t j=0;j<=hist[i];++j){
+					if(j>=img.cols){break;}
+					img.at<unsigned char>(i,j)=255;
+				}
+			}
+		}
+		else{
+			img = cv::Mat::zeros(*maxit,hist.size(), CV_8UC1);
+			drawgrid(img,grid_col, grid_row,1);
+			for(size_t i=0; i<hist.size();++i){
+				int y = *maxit-hist[i]-1;
+				if (y<0){y=0;}
+				for(size_t j=y;j<img.rows;++j){
+					img.at<unsigned char>(j,i)=255;
+				}
+			}
+		}
+	};
+
+
+	//matHistogram
+	// axis == 0, statistic histogram for columns; 
+	// others, statistic histogram for rows
+	void mat_histogram(
+		cv::Mat& img,
+		std::vector<int>&hist,
+		int axis,
+		int r0, 
+		int r1, 
+		int c0, 
+		int c1
+		)
+	{
+		hist.clear();
+		if(r0<0){r0 = 0;}
+		if(r1<0){r1 = img.rows;}
+		if(c0<0){c0 = 0;}
+		if(c1<0){c1 = img.cols;}
+
+		if (axis==0){
+			for(int c=c0;c<c1;++c){
+				int cnt = 0;
+				for(int r=r0;r<r1; ++r){
+					if (img.at<unsigned char>(r,c)>0){cnt+=1;}
+				}
+				hist.push_back(cnt);
+			}
+		}
+		else{
+			for(int r=r0;r<r1; ++r){
+				int cnt = 0;
+				for(int c=c0;c<c1;++c){
+					if (img.at<unsigned char>(r,c)>0){cnt+=1;}
+				}
+				hist.push_back(cnt);
+			}
+		}
+	};
+
+	//matHistogram
+	// axis == 0, statistic histogram for columns; 
+	// others, statistic histogram for rows
+	// with weight
+	void mat_histogram_w(
+		cv::Mat& img,
+		std::vector<int>&hist,
+		int axis,
+		int r0, 
+		int r1, 
+		int c0, 
+		int c1,
+		bool asc_w //true---ascending weight, [0-1.0], false --- descending weight [1.0--0.0]
+		)
+	{
+		hist.clear();
+		if(r0<0){r0 = 0;}
+		if(r1<0){r1 = img.rows;}
+		if(c0<0){c0 = 0;}
+		if(c1<0){c1 = img.cols;}
+	
+		if (axis==0){
+			double step = 1.0/(double)(r1-1);
+			for(int c=c0;c<c1;++c){
+				double cnt = 0.0;
+				double hi=0.0;
+				for(int r=r0;r<r1; ++r){
+					if (img.at<unsigned char>(r,c)>0){
+						if(asc_w){
+							hi = (r-r0)*step;
+							if(hi>=0.66666){
+								cnt+=hi;
+							}
+						}
+						else{
+							hi = (r1-r+r0)*step;
+							if(hi>=0.66666){
+								cnt+=hi;
+							}
+						}
+					}
+				}
+				hist.push_back((int)cnt);
+			}
+		}
+		else{
+			double step = 1.0/(double)(c1-1);
+			for(int r=r0;r<r1; ++r){
+				double cnt = 0.0;
+				double hi=0.0;
+				for(int c=c0;c<c1;++c){
+					if (img.at<unsigned char>(r,c)>0){
+						if(asc_w){
+							hi = (c-c0)*step;
+							if(hi>=0.66666){
+								cnt+=hi;
+							}
+						}
+						else{
+							hi = (c1-c+c0)*step;
+							if(hi>=0.66666){
+								cnt+=hi;
+							}
+						}
+					}
+				}
+				hist.push_back((int)cnt);
+			}
+		}
+	};
+
+	void mat_histogram_yfork(
+		cv::Mat& img,
+		std::vector<int>&hist,	
+		int r0, 
+		int r1	
+		)
+	{
+		hist.clear();
+		if(r0<0){r0 = 0;}
+		if(r1<0){r1 = img.rows;}	
+	
+		double step = 1.0/(double)(r1-r0);
+		for(int c=0;c<img.cols;++c){
+			double cnt = 0.0;
+			double hi=0.0;
+			for(int r=r0;r<r1; ++r){
+				if (img.at<unsigned char>(r,c)>0){				
+					hi = (r-r0)*step;
+					//if(hi>=0.66666){
+						cnt+=hi;
+					//}
+				
+				
+				}
+			}
+			hist.push_back((int)cnt);
+		}
+	
+	};
+
+	//get_stem_x_range() 确定茎的位置,x方向
+	// 2022/1/4		chenhj	修改,大叶子下垂,将叶子识别成茎,增加histogram满足阈值情况下,两侧数据方差检测 
+	void  get_stem_x_range_oa(
+		const std::vector<int>&hist_h,
+		double h_ratio, 
+		int padding, 
+		int cent_x,
+		int width_x,
+		int&x0, 
+		int&x1, 
+		int&stem_x0,
+		int&stem_x1
+		)
+	{
+		//hist_h: histogram in column
+		//h_ratio: ratio for generating threshold
+		// x0,x1: sub-image x-range
+		// stem_x0,1: stem x-range
+		// padding: pad for stem x-range
+
+		// calculate the max-value of hist_h
+		int min_segment_len = 0;//最小线段长度,如果小于此数值,不计为有效数据,跳过
+		int min_segment_dist = 20;//多峰情况下,间隔像素数量
+		int var_padding = 20;
+		auto max_it = max_element(hist_h.begin(), hist_h.end());
+		size_t max_idx = max_it - hist_h.begin();
+		int th = int(h_ratio * (double)(*max_it));
+
+		if(th<=0){
+			throw_msg(string("error: threshold is 0 in get_stem_x_range()"));
+		}
+
+		// 1 计算小线段的起始位置
+		vector<gcv_point<int>>segments;	
+		get_hist_segment(hist_h,th,segments);
+
+		// 2 获取多峰位置
+		vector<gcv_point<int>>peaks;	
+		
+		if(segments.size()==1){
+			peaks = segments; //单峰
+		}
+		else{// 小线段删除
+			vector<gcv_point<int>>big_segments;
+			for(size_t i=0;i<segments.size();++i){
+				if(segments[i].y - segments[i].x>=min_segment_len){
+					big_segments.push_back(segments[i]);
+				}
+			}
+
+			if(big_segments.size()==1){
+				peaks = big_segments;//单峰
+			}
+			else{// 合并
+				peaks.push_back(big_segments[0]);
+				for(size_t j=1;j<big_segments.size();++j){
+					int dist = big_segments[j].x - peaks.back().y;
+					if(dist>min_segment_dist){
+						peaks.push_back(big_segments[j]);
+					}
+					else{
+						peaks.back().y = big_segments[j].y;
+					}
+				}
+
+			}
+		}
+
+		// 3 多峰情况,先计算center_ratio, 统计峰段两侧数据的方差,方差大的为茎
+		int opt_index=0;
+		if(peaks.size()>1){
+			vector<double>center_r;
+			for(size_t i=0;i<peaks.size();++i){
+				double r = fabs(0.5*(double)(peaks[i].x+ peaks[i].y) - cent_x)/(double)(width_x/2.0);
+				center_r.push_back(1.0-r);
+			}
+			vector<size_t>sort_idx = sort_indexes_e(center_r,false);
+
+			double cr_1 = center_r[sort_idx[0]];
+			double cr_2 = center_r[sort_idx[1]];		
+
+			if(cr_1-cr_2>0.2 || cr_1>=0.65){//如果存在居中显著的,按居中优先
+				opt_index = sort_idx[0];
+			}
+			else{
+				vector<int> candidate_idx;
+				candidate_idx.push_back(sort_idx[0]);
+				candidate_idx.push_back(sort_idx[1]);
+				for(size_t id = 2;id<sort_idx.size();++id){
+					if(cr_1 - center_r[sort_idx[id]]<=0.3){
+						candidate_idx.push_back(sort_idx[id]);
+					}
+				}
+				vector<double>vars;
+				int vx0,vx1;
+				for(size_t i=0;i<peaks.size();++i){
+					vector<int>::iterator it = find(candidate_idx.begin(),candidate_idx.end(),i);
+					if(it==candidate_idx.end()){
+						vars.push_back(0.0);
+						continue;
+					}
+					vx0 = peaks[i].x - var_padding;
+					vx1 = peaks[i].y + var_padding;
+					if(vx0<0){vx0=0;}
+					if(vx1>hist_h.size()-1){vx1=hist_h.size()-1;}
+					double mean,var;
+					mean = 0.0;
+					var=-1;
+					get_hist_mean_var_local(hist_h,vx0,vx1,mean,var);
+					if(var<0){var=0.0;}
+					vars.push_back(var);
+				}
+				auto var_max_it = max_element(vars.begin(), vars.end());
+				size_t var_max_idx = var_max_it - vars.begin();
+				opt_index = var_max_idx;
+			}		
+		}
+
+		stem_x0 = peaks[opt_index].x;
+		stem_x1 = peaks[opt_index].y;
+		x0 = stem_x0 - padding;
+		x1 = stem_x1 + padding;
+		if(x0<0){x0=0;};
+		if(x1>hist_h.size()-1){x1 = hist_h.size()-1;};    
+
+
+		//////////////////////////////////////
+
+		//int global_start_idx=-1;
+		//int global_end_idx=-1;
+		//int start_idx=-1;
+		//int end_idx=-1;
+
+		//for(size_t i=0;i<hist_h.size();++i){
+		//	if(i==0){
+		//		if(hist_h[i]>=th){
+		//			start_idx=i;
+		//		}
+		//		continue;
+		//	}
+		//	if(hist_h[i]>=th && hist_h[i-1]<th){
+		//		start_idx=i;
+		//		continue;
+		//	}
+		//	if(i==hist_h.size()-1){
+		//		if(hist_h[i]>=th && hist_h[i-1]>=th){
+		//			if((i-start_idx+1)>=min_segment_len){
+		//				global_end_idx=i;
+		//			}
+		//		}
+		//	}
+		//	if(hist_h[i]<th && hist_h[i-1]>=th){
+		//		if((i-start_idx+1)>=min_segment_len){
+		//			if( global_start_idx<0){
+		//				global_start_idx = start_idx;
+		//			}
+		//			global_end_idx = i;
+		//		}
+		//	}
+
+		//}
+		///*stem_x0 = 0;
+		//stem_x1 = hist_h.size()-1;
+
+		//for(size_t i=max_idx; i < hist_h.size(); ++i){
+		//	if(hist_h[i]>=th){
+		//		stem_x1 = i;
+		//	}
+		//	else{
+		//		break;
+		//	}
+		//}
+		//for(int i=max_idx; i >= 0; i--){
+		//	if(hist_h[i]>=th){
+		//		stem_x0 = i;
+		//	}
+		//	else{
+		//		break;
+		//	}
+		//}*/
+		//stem_x0 = global_start_idx;
+		//stem_x1 = global_end_idx;
+		//x0 = stem_x0 - padding;
+		//x1 = stem_x1 + padding;
+		//if(x0<0){x0=0;};
+		//if(x1>hist_h.size()-1){x1 = hist_h.size()-1;};    
+	};
+
+	void  get_stem_x_range_rscut(
+		const std::vector<int>&hist_h,
+		double h_ratio, 
+		int padding, 
+		int cent_x,
+		int width_x,
+		int&x0, 
+		int&x1, 
+		int&stem_x0,
+		int&stem_x1
+		)
+	{
+		//hist_h: histogram in column
+		//h_ratio: ratio for generating threshold
+		// x0,x1: sub-image x-range
+		// stem_x0,1: stem x-range
+		// padding: pad for stem x-range
+
+		// calculate the max-value of hist_h
+		int min_segment_len = 0;//最小线段长度,如果小于此数值,不计为有效数据,跳过
+		int min_segment_dist = 20;//多峰情况下,间隔像素数量
+		int var_padding = 20;
+		auto max_it = max_element(hist_h.begin(), hist_h.end());
+		size_t max_idx = max_it - hist_h.begin();
+		int th = int(h_ratio * (double)(*max_it));
+
+		if(th<=0){
+			throw_msg(string("error: threshold is 0 in get_stem_x_range()"));
+		}
+
+		// 1 计算小线段的起始位置
+		vector<gcv_point<int>>segments;	
+		get_hist_segment(hist_h,th,segments);
+
+		// 2 获取多峰位置
+		vector<gcv_point<int>>peaks;	
+		
+		if(segments.size()==1){
+			peaks = segments; //单峰
+		}
+		else{// 小线段删除
+			vector<gcv_point<int>>big_segments;
+			for(size_t i=0;i<segments.size();++i){
+				if(segments[i].y - segments[i].x>=min_segment_len){
+					big_segments.push_back(segments[i]);
+				}
+			}
+
+			if(big_segments.size()==1){
+				peaks = big_segments;//单峰
+			}
+			else{// 合并
+				peaks.push_back(big_segments[0]);
+				for(size_t j=1;j<big_segments.size();++j){
+					int dist = big_segments[j].x - peaks.back().y;
+					if(dist>min_segment_dist){
+						peaks.push_back(big_segments[j]);
+					}
+					else{
+						peaks.back().y = big_segments[j].y;
+					}
+				}
+
+			}
+		}
+
+		// 3 多峰情况,先计算center_ratio, 统计峰段两侧数据的方差,方差大的为茎
+		int opt_index=0;
+		if(peaks.size()>1){
+			vector<double>center_r;
+			for(size_t i=0;i<peaks.size();++i){
+				double r = fabs(0.5*(double)(peaks[i].x+ peaks[i].y) - cent_x)/(double)(width_x/2.0);
+				center_r.push_back(1.0-r);
+			}
+			vector<size_t>sort_idx = sort_indexes_e(center_r,false);
+
+			double cr_1 = center_r[sort_idx[0]];
+			double cr_2 = center_r[sort_idx[1]];		
+
+			if(cr_1-cr_2>0.2 || cr_1>=0.65){//如果存在居中显著的,按居中优先
+				opt_index = sort_idx[0];
+			}
+			else{
+				vector<int> candidate_idx;
+				candidate_idx.push_back(sort_idx[0]);
+				candidate_idx.push_back(sort_idx[1]);
+				for(size_t id = 2;id<sort_idx.size();++id){
+					if(cr_1 - center_r[sort_idx[id]]<=0.3){
+						candidate_idx.push_back(sort_idx[id]);
+					}
+				}
+				vector<double>vars;
+				int vx0,vx1;
+				for(size_t i=0;i<peaks.size();++i){
+					vector<int>::iterator it = find(candidate_idx.begin(),candidate_idx.end(),i);
+					if(it==candidate_idx.end()){
+						vars.push_back(0.0);
+						continue;
+					}
+					vx0 = peaks[i].x - var_padding;
+					vx1 = peaks[i].y + var_padding;
+					if(vx0<0){vx0=0;}
+					if(vx1>hist_h.size()-1){vx1=hist_h.size()-1;}
+					double mean,var;
+					mean = 0.0;
+					var=-1;
+					get_hist_mean_var_local(hist_h,vx0,vx1,mean,var);
+					if(var<0){var=0.0;}
+					vars.push_back(var);
+				}
+				auto var_max_it = max_element(vars.begin(), vars.end());
+				size_t var_max_idx = var_max_it - vars.begin();
+				opt_index = var_max_idx;
+			}		
+		}
+
+		stem_x0 = peaks[opt_index].x;
+		stem_x1 = peaks[opt_index].y;
+		x0 = stem_x0 - padding;
+		x1 = stem_x1 + padding;
+		if(x0<0){x0=0;};
+		if(x1>hist_h.size()-1){x1 = hist_h.size()-1;};    
+
+
+		//////////////////////////////////////
+
+		//int global_start_idx=-1;
+		//int global_end_idx=-1;
+		//int start_idx=-1;
+		//int end_idx=-1;
+
+		//for(size_t i=0;i<hist_h.size();++i){
+		//	if(i==0){
+		//		if(hist_h[i]>=th){
+		//			start_idx=i;
+		//		}
+		//		continue;
+		//	}
+		//	if(hist_h[i]>=th && hist_h[i-1]<th){
+		//		start_idx=i;
+		//		continue;
+		//	}
+		//	if(i==hist_h.size()-1){
+		//		if(hist_h[i]>=th && hist_h[i-1]>=th){
+		//			if((i-start_idx+1)>=min_segment_len){
+		//				global_end_idx=i;
+		//			}
+		//		}
+		//	}
+		//	if(hist_h[i]<th && hist_h[i-1]>=th){
+		//		if((i-start_idx+1)>=min_segment_len){
+		//			if( global_start_idx<0){
+		//				global_start_idx = start_idx;
+		//			}
+		//			global_end_idx = i;
+		//		}
+		//	}
+
+		//}
+		///*stem_x0 = 0;
+		//stem_x1 = hist_h.size()-1;
+
+		//for(size_t i=max_idx; i < hist_h.size(); ++i){
+		//	if(hist_h[i]>=th){
+		//		stem_x1 = i;
+		//	}
+		//	else{
+		//		break;
+		//	}
+		//}
+		//for(int i=max_idx; i >= 0; i--){
+		//	if(hist_h[i]>=th){
+		//		stem_x0 = i;
+		//	}
+		//	else{
+		//		break;
+		//	}
+		//}*/
+		//stem_x0 = global_start_idx;
+		//stem_x1 = global_end_idx;
+		//x0 = stem_x0 - padding;
+		//x1 = stem_x1 + padding;
+		//if(x0<0){x0=0;};
+		//if(x1>hist_h.size()-1){x1 = hist_h.size()-1;};    
+	};
+
+
+	void  get_stem_x_range_scion(
+		const std::vector<int>&hist_h,
+		double h_ratio, 
+		int padding, 
+		int&x0, 
+		int&x1, 
+		int&stem_x0,
+		int&stem_x1
+		)
+	{
+		//hist_h: histogram in column
+		//h_ratio: ratio for generating threshold
+		// x0,x1: sub-image x-range
+		// stem_x0,1: stem x-range
+		// padding: pad for stem x-range
+
+		// calculate the max-value of hist_h
+		auto max_it = max_element(hist_h.begin(), hist_h.end());
+		size_t max_idx = max_it - hist_h.begin();
+		int th = int(h_ratio * (double)(*max_it));
+
+		stem_x0 = 0;
+		stem_x1 = hist_h.size()-1;
+
+		for(size_t i=max_idx; i < hist_h.size(); ++i){
+			if(hist_h[i]>=th){
+				stem_x1 = i;
+			}
+			else{
+				break;
+			}
+		}
+		for(int i=max_idx; i >= 0; i--){
+			if(hist_h[i]>=th){
+				stem_x0 = i;
+			}
+			else{
+				break;
+			}
+		}
+
+		x0 = stem_x0 - padding;
+		x1 = stem_x1 + padding;
+		if(x0<0){x0=0;};
+		if(x1>hist_h.size()-1){x1 = hist_h.size()-1;};    
+	};
+
+	void get_hist_segment(
+			const std::vector<int>& hist, 
+			int th,//threshold
+			std::vector<gcv_point<int>>& segments
+		)
+	{
+		segments.clear();	
+		int start_idx=-1;
+		int end_idx=-1;
+
+		for(size_t i=0;i<hist.size();++i){
+			if(i==0){
+				if(hist[i]>=th){
+					start_idx=i;
+				}
+				continue;
+			}
+			if(hist[i]>=th && hist[i-1]<th){
+				start_idx=i;
+				continue;
+			}
+			if(i==hist.size()-1){
+				if(hist[i]>=th && hist[i-1]>=th){
+					segments.push_back(gcv_point<int>(start_idx,i));
+				}
+			}
+			if(hist[i]<th && hist[i-1]>=th){
+				segments.push_back(gcv_point<int>(start_idx,i-1));
+			}
+		}
+	}
+
+	void get_hist_mean_var_local(
+			const std::vector<int>& hist, 
+			int x0,
+			int x1,
+			double& mean,
+			double& var
+		)
+	{
+		mean=0.0;
+		var = 0.0;
+		if(hist.size()==0){
+			return;
+		}
+		if(x0<0){x0=0;}
+		if(x1<0 || x1>hist.size()-1){hist.size()-1;}
+	
+		for(int i=x0;i<=x1;++i){
+			mean+=hist[i];
+		}
+		mean /=(double)(x1-x0+1);
+	
+		for(int i=x0;i<=x1;++i){
+			var+=((double)(hist[i])-mean) * ((double)(hist[i])-mean);
+		}
+		var /= (double)(x1-x0+1);
+		
+	}
+	void get_stem_y_fork(
+		const std::vector<int>& hist, 
+		double ratio,
+		int stem_dia_min, 
+		int stem_fork_y_min,
+		double stem_dia_mp,
+		int& fork_y, //output
+		int& end_y,  //output
+		int& stem_dia    //output
+		)
+	{
+		//# 由y的底部向上检测是否有茎,有的话计算移动均值
+		//# 通过当前值和移动均值的比值识别分叉点
+
+		//# stem_dia_min = 10 # 茎粗最小值
+		//# stem_fork_y_min = 10 # 茎分叉位置与茎根的最小像素高度
+		//# stem_dia_mp = 0.8 # moving power
+
+		fork_y = end_y = stem_dia = -1;
+
+		std::vector<int>reversed_hist;
+		for(int ii=hist.size()-1; ii>=0;ii--){
+			reversed_hist.push_back(hist[ii]);
+		}
+
+		int stem_root_y = -1;
+		double stem_dia_ma = -1;//# stem diameter moving-average
+
+		size_t i=0;
+		for(; i<hist.size(); ++i){
+			if (i==0){continue;}
+			if ((reversed_hist[i-1]<stem_dia_min && reversed_hist[i]>=stem_dia_min && stem_root_y<0)
+				||(reversed_hist[i-1]>=stem_dia_min && reversed_hist[i]>=stem_dia_min  && stem_root_y<0))
+			{
+				//# stem root begin
+				stem_root_y = i;
+				stem_dia_ma = (double)reversed_hist[i];
+			}
+			else {
+				if (reversed_hist[i-1]>=stem_dia_min &&
+					reversed_hist[i]>=stem_dia_min)
+				{
+					double fork_ratio = reversed_hist[i] / stem_dia_ma;
+					//if (i<150){
+					//	cout<<fork_ratio<<"\t"<<reversed_hist[i]<<"\t"<<stem_dia_ma<<endl;
+					//}
+					if( i - stem_root_y > stem_fork_y_min && 
+						fork_ratio >= ratio)
+					{
+						//# found the fork y level
+						fork_y = hist.size() - i;
+						stem_dia = reversed_hist[i-1];
+						break;
+					}
+					//# update stem_dia_ma
+					stem_dia_ma = stem_dia_mp *stem_dia_ma + (1.0-stem_dia_mp) * reversed_hist[i];
+				}
+			}
+
+		}; 
+		if(stem_root_y<0){
+		  throw_msg(string("not exists diameter bigger than stem_dia_min"));
+		}
+		if(fork_y<0){
+			throw_msg(string("not exists diameter fork_ratio bigger than threshold"));
+		}
+
+		//end_y
+		end_y =  hist.size() - stem_root_y-1;
+
+		//统计stem_root_y 到 i 间reversed_hist的数值,得到stem_dia。方法可用平均值,75%分位数
+		vector<int> tmp;
+		for(size_t j=stem_root_y; j<=i;++j){
+			tmp.push_back(reversed_hist[j]);
+		}
+		sort(tmp.begin(), tmp.end());
+		int tar_idx = (int)((float)tmp.size()*0.75);
+		stem_dia = tmp[tar_idx];
+	};
+
+	void get_stem_y_fork_3sigma(
+		const std::vector<int>& hist, 
+		double ratio,
+		int stem_dia_min, 
+		int stem_fork_y_min,
+		double stem_dia_mp,
+		int& fork_y, //output
+		int& end_y,  //output
+		int& stem_dia    //output
+		)
+	{
+	
+    
+
+		fork_y = end_y = stem_dia = -1;
+
+		std::vector<int>reversed_hist;
+		for(int ii=hist.size()-1; ii>=0;ii--){
+			reversed_hist.push_back(hist[ii]);
+		}
+
+		int mean_radius =2;
+		double mean_dia = 2.0*mean_radius +1.0;
+		double mean = 0.0;
+		double min_m, max_m;
+		min_m = max_m = 0.0;
+		std::vector<double>reversed_mean;
+		for(int ii=0; ii<reversed_hist.size();ii++){
+			if(ii-mean_radius<0 || ii + mean_radius>=reversed_hist.size()){
+				reversed_mean.push_back(0.0);
+				continue;
+			}
+			mean = 0.0;
+			for(int k = -mean_radius;k<=mean_radius;++k){
+				mean+=(double)reversed_hist[ii+k];
+			}
+			mean /= mean_dia;
+			if(mean>max_m){max_m = mean;}
+			reversed_mean.push_back(mean);
+		}
+		cv::Mat tmp;
+		hist2image_scale_line(reversed_mean,tmp,min_m,max_m,1.0,min_m,50,50);
+		imshow_wait("mean5",tmp);
+		return;
+
+		int stem_root_y = -1;
+		double stem_dia_ma = -1;//# stem diameter moving-average
+
+		size_t i=0;
+		for(; i<reversed_mean.size(); ++i){
+			if (i==0){continue;}
+			if ((reversed_hist[i-1]<stem_dia_min && reversed_hist[i]>=stem_dia_min && stem_root_y<0)
+				||(reversed_hist[i-1]>=stem_dia_min && reversed_hist[i]>=stem_dia_min  && stem_root_y<0))
+			{
+				//# stem root begin
+				stem_root_y = i;
+				stem_dia_ma = (double)reversed_hist[i];
+			}
+			else {
+				if (reversed_hist[i-1]>=stem_dia_min &&
+					reversed_hist[i]>=stem_dia_min)
+				{
+					double fork_ratio = reversed_hist[i] / stem_dia_ma;
+					//if (i<150){
+					//	cout<<fork_ratio<<"\t"<<reversed_hist[i]<<"\t"<<stem_dia_ma<<endl;
+					//}
+					if( i - stem_root_y > stem_fork_y_min && 
+						fork_ratio >= ratio)
+					{
+						//# found the fork y level
+						fork_y = hist.size() - i;
+						stem_dia = reversed_hist[i-1];
+						break;
+					}
+					//# update stem_dia_ma
+					stem_dia_ma = stem_dia_mp *stem_dia_ma + (1.0-stem_dia_mp) * reversed_hist[i];
+				}
+			}
+
+		}; 
+
+		//r2index(
+
+		/*
+		int stem_root_y = -1;
+		double stem_dia_ma = -1;//# stem diameter moving-average
+
+		size_t i=0;
+		for(; i<hist.size(); ++i){
+			if (i==0){continue;}
+			if ((reversed_hist[i-1]<stem_dia_min && reversed_hist[i]>=stem_dia_min && stem_root_y<0)
+				||(reversed_hist[i-1]>=stem_dia_min && reversed_hist[i]>=stem_dia_min  && stem_root_y<0))
+			{
+				//# stem root begin
+				stem_root_y = i;
+				stem_dia_ma = (double)reversed_hist[i];
+			}
+			else {
+				if (reversed_hist[i-1]>=stem_dia_min &&
+					reversed_hist[i]>=stem_dia_min)
+				{
+					double fork_ratio = reversed_hist[i] / stem_dia_ma;
+					//if (i<150){
+					//	cout<<fork_ratio<<"\t"<<reversed_hist[i]<<"\t"<<stem_dia_ma<<endl;
+					//}
+					if( i - stem_root_y > stem_fork_y_min && 
+						fork_ratio >= ratio)
+					{
+						//# found the fork y level
+						fork_y = hist.size() - i;
+						stem_dia = reversed_hist[i-1];
+						break;
+					}
+					//# update stem_dia_ma
+					stem_dia_ma = stem_dia_mp *stem_dia_ma + (1.0-stem_dia_mp) * reversed_hist[i];
+				}
+			}
+
+		}; 
+		if(stem_root_y<0){
+		  throw_msg(string("not exists diameter bigger than stem_dia_min"));
+		}
+		if(fork_y<0){
+			throw_msg(string("not exists diameter fork_ratio bigger than threshold"));
+		}
+
+		//end_y
+		end_y =  hist.size() - stem_root_y-1;
+
+		//统计stem_root_y 到 i 间reversed_hist的数值,得到stem_dia。方法可用平均值,75%分位数
+		vector<int> tmp;
+		for(size_t j=stem_root_y; j<=i;++j){
+			tmp.push_back(reversed_hist[j]);
+		}
+		sort(tmp.begin(), tmp.end());
+		int tar_idx = (int)((float)tmp.size()*0.75);
+		stem_dia = tmp[tar_idx];
+		*/
+	};
+
+	void get_stem_y_fork_rs(
+		const std::vector<int>& hist, 
+		double ratio,
+		int stem_dia_min, 
+		int stem_fork_y_min,
+		double stem_dia_mp,
+		int& fork_y, //output
+		int& end_y,  //output
+		int& stem_dia,    //output
+		int& roi_max_y  //output
+		)
+	{
+		//# 由y的底部向上检测是否有茎,有的话计算移动均值
+		//# 通过当前值和移动均值的比值识别分叉点
+
+		//# stem_dia_min = 10 # 茎粗最小值
+		//# stem_fork_y_min = 10 # 茎分叉位置与茎根的最小像素高度
+		//# stem_dia_mp = 0.8 # moving power
+
+		fork_y = end_y = stem_dia = -1;
+
+		std::vector<int>reversed_hist;
+		for(int ii=hist.size()-1; ii>=0;ii--){
+			reversed_hist.push_back(hist[ii]);
+		}
+
+		int stem_root_y = -1;
+		double stem_dia_ma = -1;//# stem diameter moving-average
+		int max_y = 0;
+		int max_val = reversed_hist[0];
+
+		size_t i=0;
+		vector<double> index_of_diachange;	
+		vector<double> index_of_diameter;
+		for(; i<reversed_hist.size(); ++i){
+			//find max value index
+			if(reversed_hist[i]>max_val){
+				max_val = reversed_hist[i];
+				max_y = i;
+			}
+
+			if (i==0){
+				index_of_diachange.push_back(0.0);
+				continue;
+			}
+			if ((reversed_hist[i-1]<stem_dia_min && reversed_hist[i]>=stem_dia_min && stem_root_y<0)
+				||(reversed_hist[i-1]>=stem_dia_min && reversed_hist[i]>=stem_dia_min  && stem_root_y<0))
+			{
+				//# stem root begin
+				stem_root_y = i;
+				stem_dia_ma = (double)reversed_hist[i];
+				index_of_diachange.push_back(0.0);
+			}
+			else {
+				if (reversed_hist[i-1]>=stem_dia_min &&
+					reversed_hist[i]>=stem_dia_min)
+				{
+					double fork_ratio = reversed_hist[i] / stem_dia_ma;
+					if(fork_ratio>1.5){fork_ratio=1.5;}
+					index_of_diachange.push_back(fork_ratio);				
+					stem_dia_ma = stem_dia_mp *stem_dia_ma + (1.0-stem_dia_mp) * reversed_hist[i];
+				}
+				else{
+					index_of_diachange.push_back(0.0);
+				}
+			}
+
+		}; 
+		if(stem_root_y<0){
+		  throw_msg(string("not exists diameter bigger than stem_dia_min"));
+		}
+	
+		//end_y
+		end_y =  hist.size() - stem_root_y-1;
+
+		//统计stem_root_y 到 max_y 间reversed_hist的数值,得到stem_dia。方法可用平均值,40%分位数
+		vector<int> tmp;
+		for(size_t j=stem_root_y; j<max_y;++j){
+			if(j<0 || j>=reversed_hist.size()){continue;}
+			tmp.push_back(reversed_hist[j]);
+		}	
+		sort(tmp.begin(), tmp.end());
+		int tar_idx = (int)((float)tmp.size()*0.40);
+		if(tmp.size()==0 || tar_idx>=tmp.size()){
+			throw_msg(string("not exists y fork point"));
+		}
+		stem_dia = tmp[tar_idx];
+
+		////////////////////////////////////////
+		//update 重新检验max_y,因为max_y是全局最大,由于苗的倾斜,可能出现局部最大为分叉点位置
+		// 2022-2-21 将比例参数1.5调整至2.0
+		int local_max_y = max_y;
+		for(size_t j=stem_root_y; j<max_y;++j){
+			if((double)(reversed_hist[j]/(double)stem_dia) >=2.0){
+				local_max_y = j;
+				break;
+			}
+		}
+		max_y = local_max_y ;	
+		roi_max_y = hist.size() - max_y-1;
+	
+		//////////////////////////////////////////////////////////////////
+		// 计算vector<double>index_of_diameter 
+		for(int idx = 0;idx<reversed_hist.size();++idx){
+		
+			if(reversed_hist[idx]==0){
+				index_of_diameter.push_back(0.0);
+			}
+			else {
+				if(idx>=max_y){
+					index_of_diameter.push_back(0.0);
+				}
+				else{
+					double r = yfork_validity_stemdiaratio(stem_dia,reversed_hist[idx],ratio);
+					index_of_diameter.push_back(r);
+				}
+			}
+		}
+		//////////////////////////////////////////////////////////////////
+		// 计算fork_y
+		vector<double> index_yfork;
+		for(int idx = 0;idx<reversed_hist.size();++idx){
+			index_yfork.push_back(index_of_diachange[idx] *	index_of_diameter[idx]);
+		}
+		vector<double>::iterator max_it = max_element(begin(index_yfork),end(index_yfork));
+		int max_idx_y = max_it -  index_yfork.begin();
+		fork_y = hist.size() - max_idx_y;
+	};
+	void get_stem_y_fork_rs_update(
+		const cv::Mat& bin_img,
+		int stem_x_padding,
+		int x0,
+		int x1,
+		int max_y,
+		int stem_root_y,
+		int stem_dia,
+		bool image_show,
+		double cut_point_offset_ratio,
+		cv::Point& fork_cent,
+		double& max_radius,
+		double& stem_angle
+		)
+	{
+		//1 通过bin_img的二值图像,找到茎中心线
+		//2 通过中心线,更新二值图像采集区域(有可能倾斜)
+		//3 在max_y附近找最大内接圆中心(在茎中心线上)
+
+		fork_cent.x = fork_cent.y = -1;
+		max_radius = 0.0;
+		stem_angle = 90.0;
+
+		// 茎中心点提取, 在图像max_y 和 stem_root_y之间
+		int max_len,start_x,cent_x, pre_val, cur_val;
+		vector<int>cent_xs;
+		vector<int>cent_ys;
+		for(int i=0;i<bin_img.rows;++i){
+			if(i<=max_y || i>=stem_root_y){continue;}
+			max_len = start_x = cent_x = -1;
+			for(int j=x0;j<=x1;++j){
+				if(j==x0 && bin_img.at<unsigned char>(i,j)==0){continue;}
+				if(j==x0 && bin_img.at<unsigned char>(i,j)>0){start_x=j;continue;}
+
+				pre_val = bin_img.at<unsigned char>(i,j-1);
+				cur_val = bin_img.at<unsigned char>(i,j);
+				if(pre_val==0 && cur_val>0){
+					start_x = j;
+				}
+
+				if((pre_val>0 && cur_val==0) ||(j==x1 && cur_val>0)){
+					int len = j-start_x +1;
+					if(len>max_len){
+						max_len = len;
+						cent_x = (start_x+j)/2;
+						//cout<<i<<"\t"<<"x0="<<start_x<<", x1="<<j<<", centx="<<cent_x<<endl;
+					}
+				}
+
+			}
+			if(cent_x>0){
+				cent_xs.push_back(cent_x);
+				cent_ys.push_back(i);
+			}
+		}
+		if(cent_xs.size()!=cent_ys.size() || cent_xs.size()<3){
+			throw_msg(string("stem length < 3, in function get_stem_y_fork_rs_update()"));
+		}
+
+		//茎中心线拟合
+		double beta0, beta1;
+		beta0 = beta1 = 0.0;
+		// x = beta0 + beta1 * y
+		double r2 = r2index(cent_ys,cent_xs,0,cent_xs.size()-1, beta0, beta1);
+		double th = (double)stem_dia/2.0 + stem_x_padding;
+		cv::Mat roi_img = bin_img.clone();
+
+		//提取兴趣区域图片
+		stem_angle = atan(beta1)*57.2957795130 + 90.0;
+		if(fabs(stem_angle-90.0)<2.0){
+			for(int r=0;r<roi_img.rows;++r){		
+				for(int c=0;c<roi_img.cols;++c){
+					if(c<x0 || c>x1){
+						roi_img.at<unsigned char>(r,c) = 0;
+					}
+				}
+			}
+		}
+		else{
+			for(int r=0;r<roi_img.rows;++r){	
+				double cx = beta0 + beta1 * r;
+				int cx0 = (int)(cx-th);
+				int cx1 = (int)(cx+th);				
+				for(int c=0;c<roi_img.cols;++c){
+					if(c<cx0 || c>cx1){
+						roi_img.at<unsigned char>(r,c) = 0;
+					}
+				}
+			}
+		}
+
+		////////////////////////////////////////////////////////////
+		//
+		if(image_show){
+			cv::Mat tmp = roi_img.clone();
+			cv::Point p0((int)(beta0),0);
+			cv::Point p1((int)((double)tmp.rows *beta1 +beta0),tmp.rows);
+			cv::line(tmp,p0,p1, cv::Scalar(128,0,0));
+
+			cv::line(tmp, cv::Point(cent_xs[0],cent_ys[0]),
+				cv::Point(cent_xs.back(),cent_ys.back()), cv::Scalar(128,0,0));
+	
+
+			imshow_wait("rs_bin_roi", tmp);	
+		}
+		///////////////////////////////////////////////////////////
+		//兴趣区域图片findContours
+		vector<vector<cv::Point>> contours;
+		vector<cv::Vec4i> hierarchy;
+		findContours(roi_img,contours,hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE);
+		//找到周长最长的区域作为目标区域
+		int max_length_idx = 0;
+		int max_length_val = contours[0].size();
+		for(int i=0;i<contours.size();++i){
+			if(i==0){continue;}
+			if(contours[i].size()>max_length_val){
+				max_length_val = contours[i].size();
+				max_length_idx = i;
+			}
+		}
+
+		vector<float>inner_max_radius;
+		vector<int> nnindex;
+		vector<int> xs;
+
+		/*
+		//////////////////////////////////////////
+		//基于flann的方法
+		vector<Point> contours_serial;
+		for(size_t i=0;i<contours.size();++i){
+			if(contours_serial.size()==0){
+				contours_serial.assign(contours[i].begin(),contours[i].end());
+			}
+			else{
+				for(size_t j=0;j<contours[i].size();++j){
+				contours_serial.push_back(contours[i][j]);
+				}
+			}
+		}
+		//find inner max radius
+		Mat source = Mat(contours_serial).reshape(1);
+		source.convertTo(source,CV_32F);
+		flann::KDTreeIndexParams indexParams(2);
+		flann::Index kdtree(source, indexParams);
+
+		unsigned queryNum=1;
+		vector<float> vecQuery(2);
+		vector<int> vecIndex(queryNum);
+		vector<float> vecDist(queryNum);
+		flann::SearchParams params(32);		
+
+		//在茎中心线上计算最优点指标:
+		//   1) inner_max_radius  --- 中心点对应的最小内接圆半径
+		for(size_t r =0; r<roi_img.rows;++r){	
+			if(r < max_y - 50){
+				inner_max_radius.push_back(0.0);
+				nnindex.push_back(-1);
+				xs.push_back(-1);
+				continue;
+			}
+			// x = beta0 + beta1 * y
+			float x = (float) (beta0 + beta1 * r);
+			int xi = (int)(x+0.5);
+			if(xi<0 || xi>=roi_img.cols){
+				inner_max_radius.push_back(0.0);
+				nnindex.push_back(-1);
+				xs.push_back(-1);
+				continue;
+			}
+			if(roi_img.at<unsigned char>(r,xi)==0){
+				inner_max_radius.push_back(0.0);
+				nnindex.push_back(-1);
+				xs.push_back(xi);
+				continue;
+			}
+			vecQuery[0] = x;//x
+			vecQuery[1] = (float)r;//y
+			kdtree.knnSearch(vecQuery, vecIndex, vecDist,queryNum, params);
+			if(vecDist.size()<1 || vecIndex.size()<1){
+				inner_max_radius.push_back(0.0);
+				nnindex.push_back(-1);
+				xs.push_back(xi);
+				continue;
+			}
+			inner_max_radius.push_back(vecDist[0]);
+			nnindex.push_back(vecIndex[0]);
+			xs.push_back(xi);			
+		}
+		*/
+
+		////////////////////////////////////////////////////////////////
+		//基于pointPolygonTest的方法
+		//在茎中心线上计算最优点指标:
+		//   1) inner_max_radius  --- 中心点对应的最小内接圆半径
+		for(size_t r =0; r<roi_img.rows;++r){	
+			if(r < max_y - 50){
+				inner_max_radius.push_back(0.0);
+				nnindex.push_back(-1);
+				xs.push_back(-1);
+				continue;
+			}
+			// x = beta0 + beta1 * y
+			float x = (float) (beta0 + beta1 * r);
+			int xi = (int)(x+0.5);
+			if(xi<0 || xi>=roi_img.cols){
+				inner_max_radius.push_back(0.0);
+				nnindex.push_back(-1);
+				xs.push_back(-1);
+				continue;
+			}
+			if(roi_img.at<unsigned char>(r,xi)==0){
+				inner_max_radius.push_back(0.0);
+				nnindex.push_back(-1);
+				xs.push_back(xi);
+				continue;
+			}
+			float d = cv::pointPolygonTest( contours[max_length_idx], cv::Point2f(x,r), true );
+			if(d<0){
+				inner_max_radius.push_back(0.0);
+				nnindex.push_back(-1);
+				xs.push_back(xi);
+				continue;
+			}
+			inner_max_radius.push_back(d);
+			nnindex.push_back(-1);
+			xs.push_back(xi);			
+		}	
+
+		//find max radius point
+		vector<float>::iterator max_it = max_element(inner_max_radius.begin(),inner_max_radius.end());
+		int max_idx = max_it - inner_max_radius.begin();
+		//基于flann的方法
+		//max_radius = sqrt(inner_max_radius[max_idx]);	
+		max_radius = inner_max_radius[max_idx];
+
+		//offset  
+		double r_offset = cut_point_offset_ratio * max_radius;
+		double dy = r_offset * (1.0 / sqrt(1.0 + beta1 * beta1));
+		double x_offset =  beta0 + beta1 * (max_idx + dy);
+		fork_cent.x = (int)(x_offset+0.5);
+		fork_cent.y =(int)(max_idx + dy + 0.5);
+
+		///////////////////////////////////////////////
+		//test CForkDetectOptimal 2022-2-17
+		/*CForkDetectOptimal fdo(100,20);
+		vector<float>opt_max;
+		int opt_max_idx = 0;
+		double optm=0;
+		for(size_t r =0; r<xs.size();++r){	
+			if(abs((int)r-max_idx)>50){
+				opt_max.push_back(0.0);
+				continue;
+			}
+			else{
+				if(xs[r]<0){
+					opt_max.push_back(0.0);
+					continue;
+				}
+				Point cent_pt = Point2f(xs[r],r);
+				double rt = fdo.center_pt_index(bin_img,cent_pt,-60.0);
+				opt_max.push_back((float)rt);
+				if(rt>optm){
+					optm=rt;
+					opt_max_idx=r;
+				}
+			}
+		}*/
+
+		///////////////////////////////////////////////
+
+		///////////////////////////////////////////////
+		//test
+		if(image_show){
+			cv::Mat edge;
+			edge = cv::Mat::zeros(roi_img.rows, roi_img.cols, CV_8UC1);
+			cv::drawContours(edge, contours, -1, cv::Scalar(255, 255, 0), 1);
+			for(int r=0;r<xs.size();++r){
+				int c = xs[r];
+				if(c<0){continue;}
+				if(inner_max_radius[r]<=0){continue;}
+				/*
+				//基于flann的方法
+				if(nnindex[r]<0){continue;}
+				line(edge,Point(c,r),contours_serial[nnindex[r]],Scalar(128,0,0));
+				*/
+				int rad = int(inner_max_radius[r]);
+				cv::line(edge, cv::Point(c-rad,r), cv::Point(c+rad,r), cv::Scalar(80,0,0));
+			}
+			cv::Point fork_cent_origin;
+			fork_cent_origin.x = xs[max_idx];
+			fork_cent_origin.y =max_idx;
+			cv::circle(edge,fork_cent_origin,3, cv::Scalar(200,0,0));
+			cv::circle(edge,fork_cent_origin,(int)(max_radius), cv::Scalar(200,0,0));
+
+			//circle(edge,Point(xs[opt_max_idx],opt_max_idx),5,Scalar(200,0,0));
+
+			cv::circle(edge,fork_cent,3, cv::Scalar(128,0,0));
+			cv::circle(edge,fork_cent,(int)(max_radius), cv::Scalar(128,0,0));
+			imshow_wait("rs_bin_roi_radius_circle", edge);
+		}
+		///////////////////////////////////////////////   
+
+	};
+
+	//分叉位置指标 yfork_validity_position()
+	// max_y---hist中最大点的y-index,
+	// end_y ----hist中茎最底端点的y-index
+	// fork_y --- 预期分叉点y-index
+	// max_y > fork_y > end_y
+	double yfork_validity_position(int max_y, int end_y, int fork_y){
+		//确定hist最大点位置向下到end_y点最优分叉点位置系数
+		double validity = 0.0;
+		double opt_pos = 0.85;
+		if(fork_y<end_y || fork_y>max_y){return validity;}
+		double opt_y = (max_y-end_y+1)*opt_pos + end_y;
+		if(fork_y<=end_y || fork_y>=max_y){return validity;}
+		if(fork_y>end_y && fork_y<opt_y){
+			validity =(double)( (fork_y-end_y+1) / (opt_y-end_y+1));
+		}
+		else{
+
+			validity =(double)( (max_y -fork_y+1) / (max_y -opt_y+1));
+		}
+		return validity;
+	}
+
+	//茎粗比指标  yfork_validity_stemdiaratio()
+	// stem_dia---茎粗,
+	// dia_y ----hist中y-index对应的茎粗
+	double yfork_validity_stemdiaratio(int stem_dia, int dia_y,double opt_dia_ratio){
+		//确定hist最大点位置向下到end_y点最优分叉点位置系数
+		double validity = 0.0;
+		//double opt_pos = 1.2;
+		double opt_dia = stem_dia*opt_dia_ratio;
+		if(dia_y<opt_dia){return dia_y/opt_dia;}
+		else{
+			return opt_dia/dia_y;
+		}
+	}
+	//计算上下窗口茎粗变化率
+	void yfork_validity_stemdiachange(
+			const std::vector<int>& hist,
+			int stem_dia_min,
+			std::vector<double>& var_ratio		
+			)
+	{
+		var_ratio.clear();
+		int smooth_width = 5;
+		double mean_pre=0.0;
+		double mean_nxt=0.0;
+
+		int low_y,up_y;
+		low_y=up_y=-1;
+		for(int i=0;i<hist.size();++i){
+			if(hist[i]>=stem_dia_min){
+				if( low_y<0){low_y=i;}
+				if(i>up_y){up_y = i;}
+			}
+		}
+		for(int i=0;i<hist.size();++i){
+			if(i-smooth_width<0 || i+smooth_width>=hist.size()){
+				var_ratio.push_back(0.0);
+				continue;
+			}
+			if(i<low_y+smooth_width || i>up_y-smooth_width){
+				var_ratio.push_back(0.0);
+				continue;
+			}
+
+			mean_pre = mean_nxt=0.0;
+			//low sum
+			for(int di = -smooth_width;di<0;++di){
+				mean_pre += hist[i+di];
+			}
+			//up sum
+			for(int di = 0;di<smooth_width;++di){
+				mean_nxt += hist[i+di];
+			}
+			if(mean_pre<1.0e-5){
+				var_ratio.push_back(0.0);
+				continue;
+			}
+			else{
+				double ratio = mean_nxt / mean_pre;
+				if(ratio>3.0){ratio=3.0;}
+				var_ratio.push_back(ratio);
+			}
+		}
+
+	}
+
+	void imshow_wait(
+		const char* winname,
+		cv::Mat&img,
+		int waittype/*=-1*/
+		)
+	{	
+		cv::namedWindow(winname, cv::WINDOW_NORMAL);
+		cv::imshow(winname, img);
+		cv::waitKey(waittype);
+	};
+
+	int get_stem_fork_right(
+		cv::Mat&stem_img,
+		int stem_fork_y, 
+		int stem_x0, 
+		int stem_x1,
+		int detect_window
+		)
+	{
+		/*
+		通过茎分叉坐标找到右侧茎分叉点坐标
+		stem_img, 二值图像,茎部分的局部图像
+		stem_fork_y, 检测到的茎分叉的y坐标
+		stem_x0, 茎左侧边缘(通过hist检测,针对stem_fork_y不一定精确)
+		stem_x1, 茎右侧边缘(通过hist检测,针对stem_fork_y不一定精确)
+		*/
+
+    
+		// 注释掉 2021-11-3 开始
+		/*double min_dist = 1.0;
+		int min_x1 = -1;
+		for(size_t i=stem_x0; i<stem_x1+2*detect_window; ++i){
+			double left_cnt = 0.0;
+			double right_cnt = 0.0;
+			for(size_t j=i-detect_window+1; j<i+1; ++j){
+				if (stem_img.at<unsigned char>(stem_fork_y,j)>0){
+					left_cnt+=1;
+				}
+			}
+			for(size_t j=i+1; j<i+detect_window+1; ++j){
+				if( stem_img.at<unsigned char>(stem_fork_y,j)==0){
+					right_cnt+=1;
+				}
+			}
+
+			if( left_cnt==0 || right_cnt ==0){continue;}
+			double dist = 1.0 - min(left_cnt/right_cnt, right_cnt/left_cnt);
+			if (dist < min_dist){
+				min_dist = dist;
+				min_x1 = i;
+			}
+		}
+		return min_x1;
+		*/
+		// 注释掉 2021-11-3 结束
+		size_t left_min = stem_x0-4*detect_window;
+		size_t right_max = stem_x1+4*detect_window;
+		if(left_min<0){left_min = 0;}
+		if(right_max>stem_img.cols){right_max = stem_img.cols;}
+
+		int max_len = -1;
+		int max_idx = -1;
+		int start_x=left_min;
+		for(size_t i=left_min; i<right_max; ++i){
+			if(i==left_min){ 
+				if(stem_img.at<unsigned char>(stem_fork_y,i)>0){
+					start_x =i;					
+				}
+				continue;
+			}			
+			if (stem_img.at<unsigned char>(stem_fork_y,i-1)==0 && 
+				stem_img.at<unsigned char>(stem_fork_y,i)>0){
+					start_x =i;
+					continue;
+			}
+		
+			if ((stem_img.at<unsigned char>(stem_fork_y,i-1)>0 && stem_img.at<unsigned char>(stem_fork_y,i)==0) ||
+				(stem_img.at<unsigned char>(stem_fork_y,i)>0 && i==right_max-1)){
+				if(((int)i-start_x) > max_len){
+					max_len = i-start_x;
+					max_idx = i;
+				}
+			}		
+		}
+		if(max_idx<0){max_idx = stem_x1;}
+		return max_idx;
+	};
+
+	int get_stem_fork_left(
+		cv::Mat&stem_img,
+		int stem_fork_y, 
+		int stem_x0, 
+		int stem_x1,
+		int detect_window
+		)
+	{
+		/*
+		通过茎分叉坐标找到右侧茎分叉点坐标
+		stem_img, 二值图像,茎部分的局部图像
+		stem_fork_y, 检测到的茎分叉的y坐标
+		stem_x0, 茎左侧边缘(通过hist检测,针对stem_fork_y不一定精确)
+		stem_x1, 茎右侧边缘(通过hist检测,针对stem_fork_y不一定精确)
+		*/    
+	
+		size_t left_min = stem_x0-8*detect_window;
+		size_t right_max = stem_x1+8*detect_window;
+		if(left_min<0){left_min = 0;}
+		if(right_max>stem_img.cols-1){right_max=stem_img.cols;}
+
+		int max_len = -1;
+		int max_idx = -1;
+		int start_x=left_min;
+		for(size_t i=left_min; i<right_max; ++i){
+			if(i==left_min){ 
+				if(stem_img.at<unsigned char>(stem_fork_y,i)>0){
+					start_x =i;					
+				}
+				continue;
+			}			
+			if (stem_img.at<unsigned char>(stem_fork_y,i-1)==0 && 
+				stem_img.at<unsigned char>(stem_fork_y,i)>0)
+			{
+					start_x =i;
+					continue;
+			}		
+			if ((stem_img.at<unsigned char>(stem_fork_y,i-1)>0 && stem_img.at<unsigned char>(stem_fork_y,i)==0) ||
+				(stem_img.at<unsigned char>(stem_fork_y,i)>0 && i==right_max-1)){
+				if(((int)i-start_x) > max_len){
+					max_len = i-start_x;
+					max_idx = start_x;
+				}
+			}		
+		}
+		if(max_idx<0){max_idx = stem_x0;}
+		return max_idx;
+	}
+
+	void get_stem_fork_xs(
+		cv::Mat&stem_img,
+		int stem_fork_y, 
+		int stem_x0, 
+		int stem_x1,	
+		int x0, 
+		int x1,	
+		int & fork_x0,
+		int & fork_x1
+		)
+	{
+		size_t left_min = x0;
+		size_t right_max = x1;
+		if(left_min<0){left_min = 0;}
+		if(right_max>=stem_img.cols){right_max = stem_img.cols-1;}
+
+		int max_len = -1;
+		int max_idx = -1;
+		int max_idx_right = -1;
+
+		int start_x=left_min;
+		for(size_t i=left_min; i<right_max; ++i){
+			if ( i==0 && 
+				stem_img.at<unsigned char>(stem_fork_y,i)>0)
+			{
+					start_x =i;
+					continue;
+			}
+			if (stem_img.at<unsigned char>(stem_fork_y,i)==0 &&
+				stem_img.at<unsigned char>(stem_fork_y,i+1)>0)
+			{
+					start_x =i;
+			}
+		
+			if ((stem_img.at<unsigned char>(stem_fork_y,i)>0 && stem_img.at<unsigned char>(stem_fork_y,i+1)==0) ||
+				(stem_img.at<unsigned char>(stem_fork_y,i)>0 && i==right_max-1)){
+				if(((int)i-start_x) > max_len)
+				{
+					max_len = i-start_x;
+					max_idx = start_x;
+					max_idx_right = i;
+				}
+			}		
+		}
+		if(max_idx<0){max_idx = stem_x0;}
+		fork_x0 = max_idx;	
+		if(max_idx_right<0){max_idx_right = stem_x1;}
+		fork_x1 = max_idx_right;
+	}
+
+	// 根据边缘上的一点,爬取下一点
+	void get_next_pt(
+		cv::Mat& edge_img,
+		gcv_point<int>&start_pt, 
+		gcv_point<int>&pre_pt, 	
+		gcv_point<int>&nxt_pt,//output
+		bool is_up //true 向上爬取, false 向下爬取
+		)
+	{
+		nxt_pt.x=-1;
+		nxt_pt.y=-1;
+		int dy_range[3] = {-1,0,1};
+		int dx_range[3] = {1,0,-1};
+		if (!is_up){
+			dy_range[0] = 1;
+			dy_range[2] = -1;
+		}
+    
+		for (int dyi =0; dyi<3;++dyi){
+			int dy = dy_range[dyi];
+			bool break_x = false;
+			for( int dxi=0;dxi<3;++dxi){
+				int dx = dx_range[dxi];
+				int x = start_pt.x+dx;
+				int y = start_pt.y+dy;
+				if (dx==0 && dy == 0){continue;}
+				if (x==pre_pt.x && y==pre_pt.y){continue;}
+				if (edge_img.at<unsigned char>(y,x)>0){
+					nxt_pt.x = x;
+					nxt_pt.y = y;
+					break_x = true;
+					break;
+				}
+			}
+			if (break_x){
+				break;
+			} 
+		}
+	}
+
+	// qua_fitting
+	// input: xx  ---- sorted x values, ascending
+	//        yy  ----- y values
+	//
+	// return: oa --- optimal angle, which is the x coordinates of center line of quadratic fitting line
+	double qua_fitting(vector<double>&xx, vector<double>&yy)
+	{
+		double oa = 0.0;
+		// fit quadratic function with opencv
+		cv::Mat A = cv::Mat::zeros(cv::Size(3,xx.size()), CV_64FC1);
+		for(int i=0; i<xx.size();++i){
+			A.at<double>(i,0) = 1;
+			A.at<double>(i,1) = xx[i];
+			A.at<double>(i,2) = xx[i] * xx[i];
+		}
+
+		cv::Mat b = cv::Mat::zeros(cv::Size(1,yy.size()), CV_64FC1);
+		for(int i = 0; i<yy.size(); ++i){
+			b.at<double>(i,0) = yy[i];
+		}
+		cv::Mat c, d;
+		c = A.t() * A;
+		d = A.t() * b;
+		cv::Mat result = cv::Mat::zeros(cv::Size(1,3),CV_64FC1);
+		cv::solve(c,d,result);
+	
+
+		double a0 = result.at<double>(0, 0);
+		double a1 = result.at<double>(1, 0);
+		double a2 = result.at<double>(2, 0);
+		if(fabs(a2)<1.0e-6){
+			throw_msg(string("Error:  a2 = 0 in solver"));
+		}
+		oa = -a1 / a2 / 2;
+		if(oa >180.0){oa -= 180.0;}
+		return oa;
+	}
+
+	int nms_bbox(
+		vector<Bbox>& candidate_droplets,
+		float th,
+		vector<int>& keep
+	)
+	{
+		keep.clear();
+		if (candidate_droplets.size() == 1) {
+			keep.push_back(0);
+			return 0;
+		}
+
+		sort(candidate_droplets.begin(), candidate_droplets.end(),
+			[=](const Bbox& left, const Bbox& right) {
+			return left.score > right.score;
+		});
+
+		vector<int> order(candidate_droplets.size());
+		for (size_t i = 0; i < candidate_droplets.size(); ++i) {
+			order[i] = (int)i;
+		}
+
+		while (order.size()) {
+			int i = order[0];
+			keep.push_back(i);
+			vector<int> del_idx;
+			for (size_t j = 1; j < order.size(); ++j) {
+				float iou = iou_bbox(
+					candidate_droplets[i],
+					candidate_droplets[order[j]]);
+				if (iou > th) {
+					del_idx.push_back((int)j);
+				}
+			}
+			vector<int> order_update;
+			for (size_t j = 1; j < order.size(); ++j) {
+				vector<int>::iterator it = find(del_idx.begin(), del_idx.end(), j);
+				if (it == del_idx.end()) {
+					order_update.push_back(order[j]);
+				}
+			}
+			order.clear();
+			order.assign(order_update.begin(), order_update.end());
+		}
+		return 0;
+	}
+
+	float iou_bbox(
+		const Bbox & det_a,
+		const Bbox & det_b)
+	{
+		float aa = (float)(det_a.x2 - det_a.x1 + 1) * (det_a.y2 - det_a.y1 + 1);
+		float ab = (float)(det_b.x2 - det_b.x1 + 1) * (det_b.y2 - det_b.y1 + 1);
+
+		float xx1 = (float)max(det_a.x1, det_b.x1);
+		float yy1 = (float)max(det_a.y1, det_b.y1);
+		float xx2 = (float)min(det_a.x2, det_b.x2);
+		float yy2 = (float)min(det_a.y2, det_b.y2);
+
+		float w = (float)max(0.0, xx2 - xx1 + 1.0);
+		float h = (float)max(0.0, yy2 - yy1 + 1.0);
+		float inter = w * h;
+		float ovr = inter / (aa + ab - inter);
+		return ovr;
+	}
+
+};

+ 499 - 0
utils.h

@@ -0,0 +1,499 @@
+#pragma once
+
+#include <opencv2\imgproc\imgproc.hpp>
+//#include <boost\math\distributions\binomial.hpp>
+#include <vector>
+#include <time.h>
+#include <string>
+#include <numeric>
+
+#include "data_def_api.h"
+#include "data_def.h"
+
+//using namespace cv;
+using namespace std;
+
+namespace graft_cv{
+
+	// convert ImgInfo to cv mat 
+	cv::Mat imginfo2mat(ImgInfo*);
+	ImgInfo* mat2imginfo(const cv::Mat&);
+	void imginfo_release(ImgInfo**);
+	bool isvalid(const ImgInfo*);
+	string currTime();
+	string getImgId(int im_type);
+	string getImgIdOa(string iid, int im_idx);
+	inline void throw_msg(string& msg);
+
+	// set bottom image rows_cnt rows to "value", for xiaoxu camera
+	void image_set_bottom(cv::Mat& img,unsigned char value,int rows_cnt);
+	void image_set_top(cv::Mat& img,	unsigned char value,	int rows_cnt);
+	void image_draw_line(cv::Mat& img,int x0,int y0,int x1,int y1);
+	// histogram
+	void mat_histogram(
+		cv::Mat&,
+		vector<int>&hist,
+		int axis=0, 
+		int r0=-1, 
+		int r1=-1, 
+		int c0=-1, 
+		int c1=-1
+		);
+	// weighted histogram
+	void mat_histogram_w(
+		cv::Mat& img,
+		vector<int>&hist,
+		int axis=0,
+		int r0=-1, 
+		int r1=-1, 
+		int c0=-1, 
+		int c1=-1,
+		bool asc_w=true //true---ascending weight with x/y coordinate, [0-1.0], false --- descending weight [1.0--0.0]
+	);
+	void mat_histogram_yfork(
+		cv::Mat& img,
+		vector<int>&hist,	
+		int r0, 
+		int r1	
+	);
+
+	// histogram to image	
+	void hist2image(
+		vector<int>& hist,
+		cv::Mat&img,
+		int aix=1,
+		int grid_col=50,
+		int grid_row=50
+		);
+	
+	template<class T>
+	void hist2image_line(
+	vector<T>& hist, 
+		cv::Mat&img,
+	int grid_col/*=50*/,
+	int grid_row/*=50*/
+	)
+{
+	if(hist.size()<2){return;}
+	vector<T>::iterator maxit = max_element(begin(hist),end(hist)); 
+	if( grid_col<10){grid_col=10;}
+	if( grid_row<10){grid_row=10;}	
+	
+	img = Mat::zeros(*maxit,hist.size(), CV_8UC1);
+	drawgrid(img,grid_col, grid_row,1);
+	for(size_t i=1; i<hist.size();++i){
+		line(img,Point(i-1,(int)*maxit - (int)hist[i-1]),Point(i,(int)*maxit - (int)hist[i]),Scalar(255));
+	}
+	
+};
+
+	template<class T>
+	void hist2image_scale_line(
+	vector<T>& hist, 
+		cv::Mat&img,
+	T ymin,
+	T ymax,
+	T yratio,//数值像素比,一个像素代表的数值量
+	T th,
+	int grid_col/*=50*/,
+	int grid_row/*=50*/
+	)
+{
+	if(hist.size()<2){return;}	
+	if( grid_col<10){grid_col=10;}
+	if( grid_row<10){grid_row=10;}	
+	int height =(int)((ymax-ymin)/yratio);
+	
+	img = cv::Mat::zeros(height,hist.size(), CV_8UC1);
+	drawgrid(img,grid_col, grid_row,1);
+	for(size_t i=1; i<hist.size();++i){
+		int y_1 = height-(int)((hist[i-1]-ymin)/yratio);
+		int y_0 = height-(int)((hist[i]-ymin)/yratio);
+		cv::line(img, cv::Point(i-1,y_1), cv::Point(i,y_0), cv::Scalar(255));
+	}
+	int h1 = height-(int)((th-ymin)/yratio);
+	int h2 = height-(int)((1.0/th-ymin)/yratio);
+	cv::line(img, cv::Point(0,h1), cv::Point(hist.size()-1,h1), cv::Scalar(200));
+	cv::line(img, cv::Point(0,h2), cv::Point(hist.size()-1,h2), cv::Scalar(200));
+	
+};
+
+	void drawgrid(		
+		cv::Mat&img,
+		int grid_col=50,
+		int grid_row=50,
+		int line_thick=1
+		);
+
+	//r2 index calculation for line regression
+	//         y = beta0 + beta1 * x
+	template<class T>
+	double r2index(
+		const vector<T>& x, 
+		const vector<T>& y, 
+		size_t i0, 
+		size_t i1,
+		double& beta0,
+		double& beta1)
+	{
+		double r2 = 0.0;
+		assert(x.size() == y.size());
+		assert(i0<i1);
+		assert(i1-i0>1);
+		assert(i0>=0 && i1<y.size());
+    
+		// y = beta0 + beta1 * x
+		//double beta0=0, beta1=0;
+		double t1=0, t2=0,t3=0,t4=0;
+		double n = (double)(i1-i0+1);
+		for(size_t i=i0; i<=i1; ++i)
+		{
+			t1 += (double)x[i] * (double)x[i];
+			t2 += (double)x[i];
+			t3 += (double)x[i] * (double)y[i];
+			t4 += (double)y[i];
+		}
+		beta0 = (t1*t4 - t2*t3) / (t1*n - t2*t2);
+		beta1 = (t3*n - t2*t4) / (t1*n -t2*t2);
+    
+		double sst=0, ssr=0, sse=0;
+		double y_mu = t4/n;
+		double y_hat;
+		for(size_t i=i0; i<=i1; ++i)
+		{
+			y_hat = beta0 + beta1*(double)x[i];
+			ssr += (y_hat - y_mu)*(y_hat - y_mu);
+			sse += ((double)y[i] - y_hat)* ((double)y[i] - y_hat);
+		}
+		sst = ssr + sse;
+		r2 = 1.0 - sse/sst;   
+    
+		return r2;
+	};
+
+	void get_stem_x_range_oa(
+		const vector<int>& hist_h, 
+		double h_ratio, 
+		int padding, 
+		int cent_x,
+		int width_x,
+		int&x0, 
+		int&x1, 
+		int&stem_x0,
+		int&stem_x1
+		);
+	void get_stem_x_range_rscut(
+		const vector<int>& hist_h, 
+		double h_ratio, 
+		int padding, 
+		int cent_x,
+		int width_x,
+		int&x0, 
+		int&x1, 
+		int&stem_x0,
+		int&stem_x1
+		);
+	
+	void get_stem_x_range_scion(
+		const vector<int>& hist_h, 
+		double h_ratio, 
+		int padding, 
+		int&x0, 
+		int&x1, 
+		int&stem_x0,
+		int&stem_x1
+		);
+
+	////////////////////////////////////////////////////////////////////////
+	// y-fork detect
+	void get_stem_y_fork(
+		const vector<int>& hist, 
+		double ratio,
+		int stem_dia_min, 
+		int stem_fork_y_min,
+		double stem_dia_mp,
+		int& fork_y,     //output, 茎分叉y像素位置
+		int& end_y,      //output,茎根部y像素位置
+		int& stem_dia    //output, end_y和fork_y间茎粗
+	);
+
+	void get_stem_y_fork_rs(
+	const vector<int>& hist, 
+	double ratio,
+	int stem_dia_min, 
+	int stem_fork_y_min,
+	double stem_dia_mp,
+	int& fork_y, //output
+	int& end_y,  //output
+	int& stem_dia,    //output
+	int& roi_max_y   //output
+	);
+	void get_stem_y_fork_rs_update(
+	const cv::Mat& bin_img,
+	int stem_x_padding,
+	int x0,
+	int x1,
+	int max_y,
+	int stem_root_y,
+	int stem_dia,
+	bool image_show,
+	double cut_point_offset_ratio,
+		cv::Point& fork_cent,
+	double& max_radius,
+	double& stem_angle
+	);
+	double yfork_validity_position(
+		int max_y, 
+		int end_y, 
+		int fork_y);
+	double yfork_validity_stemdiaratio(
+		int stem_dia, 
+		int dia_y,
+		double opt_dia_ratio);
+	void yfork_validity_stemdiachange(
+	    const vector<int>& hist,
+		int stem_dia_min,
+		vector<double>& var_ratio		
+		);
+
+	//通过计算茎均值,运用统计过程控制方法检测显著点
+	void get_stem_y_fork_3sigma(
+		const vector<int>& hist, 
+		double ratio,
+		int stem_dia_min, 
+		int stem_fork_y_min,
+		double stem_dia_mp,
+		int& fork_y,     //output, 茎分叉y像素位置
+		int& end_y,      //output,茎根部y像素位置
+		int& stem_dia    //output, end_y和fork_y间茎粗
+	);
+
+	double stem_y_fork_validity(
+		const vector<int>& hist,
+		int fork_y,
+		int end_y);
+
+	void get_hist_segment(
+		const vector<int>& hist, 
+		int threshold,
+		vector<gcv_point<int>>& segments
+	);
+	void get_hist_mean_var_local(
+		const vector<int>& hist, 
+		int x0,
+		int x1,
+		double& mean,
+		double& var
+	);
+	//void get_stem_y_index(
+	//	Mat& bin_img,
+	//	int x0,
+	//	int x1,
+	//	int stem_dia_min, 
+	//	int stem_fork_y_min,
+	//	double stem_dia_mp,
+	//	int& fork_y, //output
+	//	int& end_y,  //output
+	//	int& stem_dia    //output
+	//);
+	void imshow_wait(
+		const char* winname, 
+		cv::Mat&,
+		int waittype=-1
+		);
+
+	int get_stem_fork_right(
+		cv::Mat&stem_img,
+		int stem_fork_y, 
+		int stem_x0, 
+		int stem_x1,
+		int detect_window);
+	int get_stem_fork_left(
+		cv::Mat&stem_img,
+		int stem_fork_y, 
+		int stem_x0, 
+		int stem_x1,
+		int detect_window);
+	void get_stem_fork_xs(
+		cv::Mat&stem_img,
+		int stem_fork_y, 
+		int stem_x0, 
+		int stem_x1,
+		int x0, 
+		int x1,		
+		int & fork_x0,
+		int & fork_x1
+	);
+
+	void get_next_pt(
+		cv::Mat& edge_img,
+	    gcv_point<int>&start_pt, 
+	    gcv_point<int>&pre_pt, 
+		gcv_point<int>&nxt_pt, //output
+	    bool is_up=true);
+
+	// 二次曲线拟合,返回二次曲线对称轴x的坐标
+	double qua_fitting(vector<double>&xx, vector<double>&yy);
+
+	template <typename T>
+    vector<size_t> sort_indexes_e(vector<T> &v, bool ascending=true)
+    {
+        vector<size_t> idx(v.size());
+        iota(idx.begin(), idx.end(), 0);
+		if(ascending){
+        sort(idx.begin(), idx.end(),
+            [&v](size_t i1, size_t i2) {return v[i1] < v[i2]; });
+		}
+		else{
+			sort(idx.begin(), idx.end(),
+            [&v](size_t i1, size_t i2) {return v[i1] > v[i2]; });		
+		}
+        return idx;
+    }
+	template<typename T>
+	void hist_filter(vector<T>& hist, int method=0, int radius=2)
+	{
+		//mothod: 0---mid; 1--mean
+		assert(radius>=1);
+		if(hist.size()<3){return;}
+		vector<T>tmp;
+		
+		if(method==0){
+			T* buff = new T[2+2*radius];
+			for(int i=0; i<hist.size();++i){
+				int cnt=0;
+				for(int r = -radius;r<=radius;++r){
+					int idx=i+r;
+					if(idx<0 || idx>=hist.size()){continue;}
+					buff[cnt++]=hist[idx];
+				}
+				sort(buff,buff+cnt);
+				int cent = cnt/2;
+				tmp.push_back(buff[cent]);
+			}
+			delete []buff;
+		}
+		if(method==1){
+			for(int i=0; i<hist.size();++i){
+				int cnt=0;
+				T mean = T(0);
+				for(int r = -radius;r<=radius;++r){
+					int idx=i+r;
+					if(idx<0 || idx>=hist.size()){continue;}
+					mean+=hist[idx];
+				}
+				mean =mean/cnt;
+				tmp.push_back(mean);
+			}
+		}
+		
+		for(size_t j=0;j<hist.size();++j){hist[j]=tmp[j];}
+	}
+	template<typename T>
+	double otsu(vector<T>&data)
+	{
+		if (data.size() == 0) {
+			return 0.0;
+		}
+		std::vector<T>data_sort(data);
+		std::sort(data_sort.begin(), data_sort.end());
+		double t0 = data_sort[int(data_sort.size() / 2)];
+		double t1 = data_sort.back();
+		double t = t0;
+		double max_g = 0;
+		double max_t = t0;
+		std::vector<T>d0;
+		std::vector<T>d1;
+		double n = data.size();
+		while (t < t1) {
+			d0.clear();
+			d1.clear();
+			for (auto& v : data_sort) {
+				if (v < t) { d0.push_back(v); }
+				else { d1.push_back(v); }
+			}
+			double g = get_hist_mean(d0) - get_hist_mean(d1);
+			g *= g;
+			g = g* d0.size() * d1.size() / n / n;
+			if (g > max_g) {
+				max_g = g;
+				max_t = t;
+			}
+			t += 1;
+		}
+		return max_t;
+
+	}
+	template<typename T>
+	double get_hist_mean(vector<T>&data) {
+		double mu = 0;
+		for (auto &v : data) { mu += v; }
+		if (data.size() > 0) {
+			mu /= data.size();
+		}
+		return mu;
+	}
+
+	template<typename T>
+	double get_hist_std(vector<T>&data, T mu) {
+		double std_v = 0;
+		for (auto &v : data) { std_v += (v-mu)*(v - mu); }
+		if (data.size() > 0) {
+			std_v /= data.size();
+			std_v = sqrt(std_v);
+		}
+		return std_v;
+	}
+
+	template<typename T>
+	int trend_detect_pos(const vector<T>&data, int data_th, int step=15) {
+		int pos = -1;
+		if (data.size() < 2 * step) {
+			return pos;
+		}		
+		int radius = int(step/2);
+		boost::math::binomial_distribution<> binomal(radius, 0.5);
+		pos = radius;
+		int max_v = 0;
+		int pos_n, neg_n;
+		for (size_t i = radius; i < data.size() - radius; ++i) {
+			pos_n = neg_n = 0;
+			for (int j = 1; j <= radius; ++j) {
+				int diff = data[i + j] - data[i + j -radius];
+				if (diff > 0) {
+					pos_n++;
+				}
+				else {
+					neg_n++;
+				}
+			}
+			int k = min(pos_n, neg_n);			
+			double p = 2.0 * boost::math::cdf(binomal, k);
+			std::cout <<"idx="<<i<< ", posn=" << pos_n << ", negn=" << neg_n << ", pvalue=" << p << endl;
+			if (p > 0.05) {
+				if(fabs(data.at(i) - data_th)<3)
+				{
+					pos = i;
+					break;
+				}
+			}
+		}
+		return pos;
+
+	}
+
+	//nms
+	int nms_bbox(
+		vector<Bbox>& candidate_droplets,	//input
+		float th,							//input
+		vector<int>& keep					//output
+	);
+	//iou
+	float iou_bbox(
+		const Bbox & det_a,
+		const Bbox & det_b);
+	
+
+
+	
+};