瀏覽代碼

第一版最后状态

chenhongjiang 2 年之前
當前提交
bf47db7d7b
共有 30 個文件被更改,包括 8242 次插入0 次删除
  1. 10 0
      .gitignore
  2. 70 0
      ReadMe.txt
  3. 257 0
      config.cpp
  4. 34 0
      config.h
  5. 7 0
      cut_point_impl_rs.h
  6. 952 0
      cut_point_rs.cpp
  7. 128 0
      cut_point_rs.h
  8. 595 0
      cut_point_sc.cpp
  9. 66 0
      cut_point_sc.h
  10. 81 0
      data_def.h
  11. 135 0
      data_def_api.h
  12. 1524 0
      demo.cpp
  13. 131 0
      fork_rs.cpp
  14. 31 0
      fork_rs.h
  15. 66 0
      gcv_conf.yml
  16. 370 0
      graft_cv_api.cpp
  17. 127 0
      graft_cv_api.h
  18. 198 0
      imstorage_manager.cpp
  19. 62 0
      imstorage_manager.h
  20. 96 0
      logger.cpp
  21. 40 0
      logger.h
  22. 18 0
      opencv.props
  23. 814 0
      optimal_angle.cpp
  24. 127 0
      optimal_angle.h
  25. 8 0
      stdafx.cpp
  26. 15 0
      stdafx.h
  27. 8 0
      targetver.h
  28. 41 0
      test.cpp
  29. 1835 0
      utils.cpp
  30. 396 0
      utils.h

+ 10 - 0
.gitignore

@@ -0,0 +1,10 @@
+/Debug/
+/Debug_exe/
+/Release/
+/Release_exe/
+/x64/
+demo.vcxproj*
+*.log
+gcv_conf——.yml
+cam_config*
+

+ 70 - 0
ReadMe.txt

@@ -0,0 +1,70 @@
+v0.5.9.2 增加了图片保存,自动定期删除功能;增加了histogram生成时,增加权重。
+v0.5.9.3 压叶切割点识别结果,不能用于切割,将切割点改成分叉点间,靠近右侧三分之一位置
+v0.5.9.4 去掉切后重识别相关的代码;增加识别结果的图片;修改stem_x0,stem_x1的识别方法;分叉中点作为切割点;适用于不压叶的情况
+v0.5.9.5 砧木切割点识别中,有的叶子较大且下垂,识别中将叶子识别成茎,造成错误,修改茎识别方法get_stem_x_range()
+v0.5.9.6 1)修改旋转叶展识别方法,改用最大叶宽的图像分叉y值;
+         2)配置文件中
+		     增加
+			 rs_min_hist_value: 5
+		     修改
+		     oa_min_hist_value: 5
+			 oa_stem_x_padding: 40
+
+		 3)大叶下垂识别茎位置,增加中心点定位,在中心点的优选,然后计算var选择var大的
+		 4)返回结果中增加 double rs_cut_lpoint_x;//砧木下切割点x位置,毫米
+	                       double rs_cut_lpoint_y;//砧木下切割点y位置,毫米
+						   double sc_cut_lpoint_x;//穗苗切割下点x位置,毫米
+	                       double sc_cut_lpoint_y;//穗苗切割下点y位置,毫米
+					去除曲线长度
+v0.5.9.7 修改保存图片的位置,翻转,填充后的图片保存   
+v0.5.9.8 
+        1)修改最优角度识别中单调情况的算法,采用最大点的角度(原来3点二次差值会出现较大误差)   
+		2)去掉最优角度识别,人工上料保证叶展方向,拍照一次识别分叉点y高度(目前代码没有改动)
+		3)最优角度识别infer函数增加输出日志:分叉点和根点y的像素值和毫米值
+
+v0.5.9.9 最优角度识别,append方法,返回当时的分叉点y坐标,最低点y坐标
+         实现一次图像识别返回结果(适用于人工上料,不自动旋转的情况),调用流程不变,只是调用次数为1
+		 增加返回参数   double rs_oa_stem_y_fork;//茎分叉点y,毫米
+		            	double rs_oa_clamp_y_end;//茎可视下端y,毫米
+v0.5.9.10 增加砧木切割点间连线
+v0.5.9.11 增加接口调用进入、离开的日志信息; 改rs_stem_dia_mp: 9.6e-001
+v0.5.9.12 穗苗上切割点识别修改,改用中值法取茎粗,达到茎粗(或95百分位)位置为上切割点
+v0.5.9.13 1)穗苗切割点有识别不到的情况(因为苗倾斜角度过大),修改(扩大)识别范围
+          2)砧木分叉点检测方法(通过滑动平均),不稳定,可能出现不能检测到分叉点的情况
+		     砧木准确的上切割点识别方法
+v0.5.9.14 修改测试中穗苗切割点检测出现bug,vector越界问题
+          修改  sc_stem_ymax_padding: 200
+v0.5.9.15 针对砧木旋转工位分叉点识别,修改util.cpp中的get_stem_x_range()   
+          修改参数 rs_morph_radius: 2
+                   rs_morph_iteration: 5    
+	      修改砧木图像分割方法为直接二值化,然后close方法,用上述新参数,实现去毛刺
+		  砧木col方向的histogram,通过中值滤波再次去毛刺
+		  砧木下切割点位置换算bug修改
+v0.5.9.16 砧木切割工位切割点检测分2步
+          0)砧木分叉点检测方法,旋转工位和切割工位函数分开
+          1)原来的方法检测分叉点y坐标get_stem_y_fork_rs(),通过直径变化系数和直径(已知直径)偏差系数确定分叉点,
+		     并限定在茎根部和茎最宽位置(通过x0和x1限定)找出最大指定的方法确定分叉y值
+		  2)在1)的基础上增加get_stem_y_fork_rs_update()函数,基于1)中的结果,
+		     实现茎中心线检测;
+		     带角度的x方向padding图像区域剪裁;
+			 找出外边缘,计算中心线到边缘的最小距离;
+			 找出中线上点最大距离的点作为分叉参考点
+          
+		  3)修改参数   rs_row_th_ratio: 1.1999999999999999e+000
+	      4) 用到flann,增加opencv_flann2410d.lib
+v0.5.9.17 增加砧木切割工位切割点偏移参数
+          rs_cut_point_offset_ratio: 5.0000000000000000e-001
+v0.5.9.18 怀疑内存泄露,检测优化
+          1)高速连续加载砧木图片测试出现中途退出,1秒内图片id重复造成存储异常退出
+		    修改图片id,增加序列值(00-99)
+		  2)imginfo2mat()中构建mat矩阵,自己申请的内存(new),指针赋值给mat对象,mat对象
+		    析构是不负责释放,造成内存泄露,已修改
+v0.5.9.19 增加配置参数写入日志功能:启动-初始化,或认为设置参数后将最新配置参数写入日志文件
+          增加版本号日志
+		  增加宽大真叶的情况的处理
+v0.5.9.20 增加砧木分叉点检测优化,增加fork_rs.h,cpp(还没有全面测试)
+          修改砧木roi最大点识别,将比例参数1.5调整至2.0(utils.cpp)
+		  修改砧木茎中心最大内接园半径的方法,不采用flann查找(出现误差较大),改用pointPolygonTest()
+		  去掉opencv_flann2410d.lib
+		  按测试,修改rs_cut_point_offset_ratio: 9.0000000000000000e-001
+		  修改穗苗识别方法(用于标定位置的夹子被遮挡)

+ 257 - 0
config.cpp

@@ -0,0 +1,257 @@
+#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(FileStorage &fs)const{
+		assert(m_cparam!=0);
+		fs << "{" 
+			<< "image_show"<<  m_cparam->image_show
+			<< "image_return"<<  m_cparam->image_return
+			<< "image_row_grid"<<  m_cparam->image_row_grid
+			<< "image_col_grid"<<  m_cparam->image_col_grid
+			<< "self_camera"<< m_cparam->self_camera
+
+			<<"timeout_proc"<<m_cparam->timeout_proc
+			<<"timeout_append"<<m_cparam->timeout_append
+
+			<<"image_save"<<m_cparam->image_save
+			<<"image_depository"<<m_cparam->image_depository
+			<<"image_backup_days"<<m_cparam->image_backup_days
+
+			<< "oa_y_flip"<<  m_cparam->oa_y_flip 	
+			<< "oa_morph_radius"<<  m_cparam->oa_morph_radius 	
+			<< "oa_morph_iteration" << m_cparam->oa_morph_iteration
+			<< "oa_min_hist_value"<< m_cparam->oa_min_hist_value
+			<< "oa_morph_radius_base"<<  m_cparam->oa_morph_radius_base	
+			<< "oa_morph_iteration_base" << m_cparam->oa_morph_iteration_base
+			<< "oa_min_hist_value_base"<< m_cparam->oa_min_hist_value_base
+
+			<< "oa_col_th_ratio"<< m_cparam->oa_col_th_ratio
+			<< "oa_row_th_ratio"<< m_cparam->oa_row_th_ratio
+			<< "oa_stem_x_padding"<< m_cparam->oa_stem_x_padding
+			<< "oa_stem_dia_min"<< m_cparam->oa_stem_dia_min
+			<< "oa_stem_fork_y_min"<< m_cparam->oa_stem_fork_y_min
+			<< "oa_stem_dia_mp"<< m_cparam->oa_stem_dia_mp
+
+			<< "oa_clip_y_min"<< m_cparam->oa_clip_y_min
+			<< "oa_clip_y_max"<<m_cparam->oa_clip_y_max
+
+
+			<< "rs_y_flip"<<  m_cparam->rs_y_flip 	
+			<< "rs_min_hist_value"<< m_cparam->rs_min_hist_value
+			<< "rs_col_th_ratio" << m_cparam->rs_col_th_ratio		
+			<< "rs_row_th_ratio" << m_cparam->rs_row_th_ratio
+			<< "rs_stem_x_padding" << m_cparam->rs_stem_x_padding
+			<< "rs_stem_dia_min" << m_cparam->rs_stem_dia_min
+			<< "rs_stem_dia_mp" << m_cparam->rs_stem_dia_mp
+			<< "rs_stem_fork_y_min" << m_cparam->rs_stem_fork_y_min			
+			<< "rs_stem_edge_detect_window" << m_cparam->rs_stem_edge_detect_window
+			<< "rs_cand_corner_box_width_ratio" << m_cparam->rs_cand_corner_box_width_ratio
+			<< "rs_cand_corner_box_xoffset_ratio" << m_cparam->rs_cand_corner_box_xoffset_ratio
+			<< "rs_opt_corner_xoffset_ratio" << m_cparam->rs_opt_corner_xoffset_ratio
+			<< "rs_opt_corner_yoffset_ratio" << m_cparam->rs_opt_corner_yoffset_ratio
+			<<"rs_corner_mask_rad_ratio"<<m_cparam->rs_corner_mask_rad_ratio
+			<< "rs_morph_radius" << m_cparam->rs_morph_radius
+			<< "rs_morph_iteration" << m_cparam->rs_morph_iteration
+			<< "rs_morph_iteration_gray" << m_cparam->rs_morph_iteration_gray
+			<< "rs_max_corner_num" << m_cparam->rs_max_corner_num
+			<< "rs_corner_qaulity_level" << m_cparam->rs_corner_qaulity_level
+			<< "rs_corner_min_distance" << m_cparam->rs_corner_min_distance
+			<< "rs_cut_angle" << m_cparam->rs_cut_angle
+			<< "rs_cut_point_offset_ratio"<< m_cparam->rs_cut_point_offset_ratio
+			   
+			<< "sc_y_flip"<<  m_cparam->sc_y_flip 	
+			<< "sc_col_th_ratio" << m_cparam->sc_col_th_ratio
+			<< "sc_row_th_ratio" << m_cparam->sc_row_th_ratio
+			<< "sc_stem_x_padding" << m_cparam->sc_stem_x_padding
+			<< "sc_stem_dia_min"<< m_cparam->sc_stem_dia_min
+			<< "sc_clip_padding" << m_cparam->sc_clip_padding
+			<< "sc_stem_ymax_padding" << m_cparam->sc_stem_ymax_padding
+			<< "sc_default_cut_length" << m_cparam->sc_default_cut_length	
+
+			<< "sc_stem_edge_detect_window" << m_cparam->sc_stem_edge_detect_window
+			<< "sc_r2_th" << m_cparam->sc_r2_th
+			<< "sc_r2_window" << m_cparam->sc_r2_window
+			<< "sc_average_window" << m_cparam->sc_average_window
+			<< "sc_morph_radius" << m_cparam->sc_morph_radius
+			<< "sc_morph_iteration" << m_cparam->sc_morph_iteration
+			   
+			<< "rs_oa_pixel_ratio" << m_cparam->rs_oa_pixel_ratio
+			<< "rs_cut_pixel_ratio" << m_cparam->rs_cut_pixel_ratio
+			<< "sc_cut_pixel_ratio" << m_cparam->sc_cut_pixel_ratio
+			<< "}"; 	
+	};
+	void CGCvConfig::read(const 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_row_grid = (int)node["image_row_grid"];
+		m_cparam->image_col_grid = (int)node["image_col_grid"];
+		m_cparam->self_camera = (bool)(int)node["self_camera"];
+
+		m_cparam->timeout_proc = (int)node["timeout_proc"];
+		m_cparam->timeout_append = (int)node["timeout_append"];
+
+		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->oa_y_flip = (bool)(int)node["oa_y_flip"];
+		m_cparam->rs_min_hist_value = (int)node["rs_min_hist_value"];
+	    m_cparam->oa_morph_radius = (int)node["oa_morph_radius"];
+		m_cparam->oa_morph_iteration  = (int)node["oa_morph_iteration"];
+		m_cparam->oa_min_hist_value = (int)node["oa_min_hist_value"];
+		m_cparam->oa_morph_radius_base = (int)node["oa_morph_radius_base"];
+		m_cparam->oa_morph_iteration_base  = (int)node["oa_morph_iteration_base"];
+		m_cparam->oa_min_hist_value_base = (int)node["oa_min_hist_value_base"];
+
+		m_cparam->oa_col_th_ratio = (double)node["oa_col_th_ratio"];
+		m_cparam->oa_row_th_ratio  = (double)node["oa_row_th_ratio"];
+		m_cparam->oa_stem_x_padding = (int)node["oa_stem_x_padding"];
+		m_cparam->oa_stem_dia_min = (int)node["oa_stem_dia_min"];
+		m_cparam->oa_stem_fork_y_min  = (int)node["oa_stem_fork_y_min"];
+		m_cparam->oa_stem_dia_mp = (double)node["oa_stem_dia_mp"];
+		m_cparam->oa_clip_y_min = (int)node["oa_clip_y_min"];
+		m_cparam->oa_clip_y_max = (int)node["oa_clip_y_max"];
+
+		m_cparam->rs_y_flip = (bool)(int)node["rs_y_flip"];
+		m_cparam->rs_col_th_ratio  = (double)node["rs_col_th_ratio"];
+		m_cparam->rs_row_th_ratio  = (double)node["rs_row_th_ratio"];
+		m_cparam->rs_stem_x_padding  = (int)node["rs_stem_x_padding"];
+		m_cparam->rs_stem_dia_min  = (int)node["rs_stem_dia_min"];
+		m_cparam->rs_stem_dia_mp  = (double)node["rs_stem_dia_mp"];
+		m_cparam->rs_stem_fork_y_min  = (int)node["rs_stem_fork_y_min"];		
+		m_cparam->rs_stem_edge_detect_window  = (int)node["rs_stem_edge_detect_window"];
+		m_cparam->rs_cand_corner_box_width_ratio  = (double)node["rs_cand_corner_box_width_ratio"];
+		m_cparam->rs_cand_corner_box_xoffset_ratio  = (double)node["rs_cand_corner_box_xoffset_ratio"];
+		m_cparam->rs_opt_corner_xoffset_ratio   = (double)node["rs_opt_corner_xoffset_ratio"];
+		m_cparam->rs_opt_corner_yoffset_ratio   = (double)node["rs_opt_corner_yoffset_ratio"];
+		m_cparam->rs_corner_mask_rad_ratio = (double)node["rs_corner_mask_rad_ratio"];
+		m_cparam->rs_morph_radius  = (int)node["rs_morph_radius"];
+		m_cparam->rs_morph_iteration  = (int)node["rs_morph_iteration"];
+		m_cparam->rs_morph_iteration_gray  = (int)node["rs_morph_iteration_gray"];
+		m_cparam->rs_max_corner_num  = (int)node["rs_max_corner_num"];
+		m_cparam->rs_corner_qaulity_level  = (double)node["rs_corner_qaulity_level"];
+		m_cparam->rs_corner_min_distance  = (double)node["rs_corner_min_distance"];
+		m_cparam->rs_cut_angle = (double)node["rs_cut_angle"];
+		m_cparam->rs_cut_point_offset_ratio = (double)node["rs_cut_point_offset_ratio"];
+
+		m_cparam->sc_y_flip = (bool)(int)node["sc_y_flip"];
+		m_cparam->sc_col_th_ratio  = (double)node["sc_col_th_ratio"];
+		m_cparam->sc_row_th_ratio  = (double)node["sc_row_th_ratio"];
+		m_cparam->sc_stem_x_padding  = (int)node["sc_stem_x_padding"];
+		m_cparam->sc_stem_dia_min  = (int)node["sc_stem_dia_min"];
+		m_cparam->sc_clip_padding  = (int)node["sc_clip_padding"];
+		m_cparam->sc_stem_ymax_padding  = (int)node["sc_stem_ymax_padding"];
+		m_cparam->sc_default_cut_length  = (int)node["sc_default_cut_length"];
+
+		m_cparam->sc_stem_edge_detect_window = (int)node["sc_stem_edge_detect_window"];
+		m_cparam->sc_r2_th  = (double)node["sc_r2_th"];
+		m_cparam->sc_r2_window  = (int)node["sc_r2_window"];
+		m_cparam->sc_average_window  = (int)node["sc_average_window"];
+		m_cparam->sc_morph_radius  = (int)node["sc_morph_radius"];
+		m_cparam->sc_morph_iteration  = (int)node["sc_morph_iteration"];		
+
+		m_cparam->rs_oa_pixel_ratio  = (double)node["rs_oa_pixel_ratio"];
+		m_cparam->rs_cut_pixel_ratio  = (double)node["rs_cut_pixel_ratio"];
+		m_cparam->sc_cut_pixel_ratio  = (double)node["sc_cut_pixel_ratio"]; 		
+    
+  }
+	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_row_grid:\t"<<  m_cparam->image_row_grid << endl
+			<< "image_col_grid:\t"<<  m_cparam->image_col_grid << endl
+			<< "self_camera:\t"<< m_cparam->self_camera << endl
+
+			<<"timeout_proc:\t"<<m_cparam->timeout_proc << endl
+			<<"timeout_append:\t"<<m_cparam->timeout_append << 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
+
+			<< "oa_y_flip:\t"<<  m_cparam->oa_y_flip 	 << endl
+			<< "oa_morph_radius:\t"<<  m_cparam->oa_morph_radius 	 << endl
+			<< "oa_morph_iteration:\t" << m_cparam->oa_morph_iteration << endl
+			<< "oa_min_hist_value:\t"<< m_cparam->oa_min_hist_value << endl
+			<< "oa_morph_radius_base:\t"<<  m_cparam->oa_morph_radius_base	 << endl
+			<< "oa_morph_iteration_base:\t" << m_cparam->oa_morph_iteration_base << endl
+			<< "oa_min_hist_value_base:\t"<< m_cparam->oa_min_hist_value_base << endl
+
+			<< "oa_col_th_ratio:\t"<< m_cparam->oa_col_th_ratio << endl
+			<< "oa_row_th_ratio:\t"<< m_cparam->oa_row_th_ratio << endl
+			<< "oa_stem_x_padding:\t"<< m_cparam->oa_stem_x_padding << endl
+			<< "oa_stem_dia_min:\t"<< m_cparam->oa_stem_dia_min << endl
+			<< "oa_stem_fork_y_min:\t"<< m_cparam->oa_stem_fork_y_min << endl
+			<< "oa_stem_dia_mp:\t"<< m_cparam->oa_stem_dia_mp << endl
+
+			<< "oa_clip_y_min:\t"<< m_cparam->oa_clip_y_min << endl
+			<< "oa_clip_y_max:\t"<<m_cparam->oa_clip_y_max << endl
+
+
+			<< "rs_y_flip:\t"<<  m_cparam->rs_y_flip 	 << endl
+			<< "rs_min_hist_value:\t"<< m_cparam->rs_min_hist_value << endl
+			<< "rs_col_th_ratio:\t" << m_cparam->rs_col_th_ratio		 << endl
+			<< "rs_row_th_ratio:\t" << m_cparam->rs_row_th_ratio << endl
+			<< "rs_stem_x_padding:\t" << m_cparam->rs_stem_x_padding << endl
+			<< "rs_stem_dia_min:\t" << m_cparam->rs_stem_dia_min << endl
+			<< "rs_stem_dia_mp:\t" << m_cparam->rs_stem_dia_mp << endl
+			<< "rs_stem_fork_y_min:\t" << m_cparam->rs_stem_fork_y_min			 << endl
+			<< "rs_stem_edge_detect_window:\t" << m_cparam->rs_stem_edge_detect_window << endl
+			<< "rs_cand_corner_box_width_ratio:\t" << m_cparam->rs_cand_corner_box_width_ratio << endl
+			<< "rs_cand_corner_box_xoffset_ratio:\t" << m_cparam->rs_cand_corner_box_xoffset_ratio << endl
+			<< "rs_opt_corner_xoffset_ratio:\t" << m_cparam->rs_opt_corner_xoffset_ratio << endl
+			<< "rs_opt_corner_yoffset_ratio:\t" << m_cparam->rs_opt_corner_yoffset_ratio << endl
+			<<"rs_corner_mask_rad_ratio:\t"<<m_cparam->rs_corner_mask_rad_ratio << endl
+			<< "rs_morph_radius:\t" << m_cparam->rs_morph_radius << endl
+			<< "rs_morph_iteration:\t" << m_cparam->rs_morph_iteration << endl
+			<< "rs_morph_iteration_gray:\t" << m_cparam->rs_morph_iteration_gray << endl
+			<< "rs_max_corner_num:\t" << m_cparam->rs_max_corner_num << endl
+			<< "rs_corner_qaulity_level:\t" << m_cparam->rs_corner_qaulity_level << endl
+			<< "rs_corner_min_distance:\t" << m_cparam->rs_corner_min_distance << endl
+			<< "rs_cut_angle:\t" << m_cparam->rs_cut_angle << endl
+			<< "rs_cut_point_offset_ratio:\t"<< m_cparam->rs_cut_point_offset_ratio << endl
+			   
+			<< "sc_y_flip:\t"<<  m_cparam->sc_y_flip 	 << endl
+			<< "sc_col_th_ratio:\t" << m_cparam->sc_col_th_ratio << endl
+			<< "sc_row_th_ratio:\t" << m_cparam->sc_row_th_ratio << endl
+			<< "sc_stem_x_padding:\t" << m_cparam->sc_stem_x_padding << endl
+			<< "sc_stem_dia_min:\t"<< m_cparam->sc_stem_dia_min << endl
+			<< "sc_clip_padding:\t" << m_cparam->sc_clip_padding << endl
+			<< "sc_stem_ymax_padding:\t" << m_cparam->sc_stem_ymax_padding << endl
+			<< "sc_default_cut_length:\t" << m_cparam->sc_default_cut_length	 << endl
+
+			<< "sc_stem_edge_detect_window:\t" << m_cparam->sc_stem_edge_detect_window << endl
+			<< "sc_r2_th:\t" << m_cparam->sc_r2_th << endl
+			<< "sc_r2_window:\t" << m_cparam->sc_r2_window << endl
+			<< "sc_average_window:\t" << m_cparam->sc_average_window << endl
+			<< "sc_morph_radius:\t" << m_cparam->sc_morph_radius << endl
+			<< "sc_morph_iteration:\t" << m_cparam->sc_morph_iteration			    << endl
+
+			<< "rs_oa_pixel_ratio:\t" << m_cparam->rs_oa_pixel_ratio << endl
+			<< "rs_cut_pixel_ratio:\t" << m_cparam->rs_cut_pixel_ratio << endl
+			<< "sc_cut_pixel_ratio:\t" << m_cparam->sc_cut_pixel_ratio << 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(FileStorage &fs)const;
+		void read(const FileNode& node);
+	private:
+		ConfigParam* m_cparam;	
+
+	};
+	//These write and read functions must exist as per the inline functions in operations.hpp
+	static void write(FileStorage& fs, const std::string&, const CGCvConfig& x){
+	  x.write(fs);
+	}
+	static void read(const 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);
+};

+ 7 - 0
cut_point_impl_rs.h

@@ -0,0 +1,7 @@
+#pragma once
+
+#include <opencv.hpp>
+using namespace cv;
+namespace graft_cv{
+
+}

+ 952 - 0
cut_point_rs.cpp

@@ -0,0 +1,952 @@
+/*
+  砧木切割点识别
+
+  1)压左侧叶
+  2)识别左侧叶柄分割点,
+  3)切割角度:-45度
+
+*/
+#include <opencv2\imgproc\imgproc.hpp>
+#include <math.h>
+#include <algorithm>
+
+#include "cut_point_rs.h"
+#include "utils.h"
+#include "data_def.h"
+#include "logger.h"
+using namespace cv;
+
+namespace graft_cv{
+
+CRootStockCutPoint::CRootStockCutPoint(ConfigParam&cp,CGcvLogger*pLog/*=0*/)
+:m_cparam(cp),
+m_pLogger(pLog),
+m_pImginfoBinFork(0),
+m_pImgCorners(0),
+m_pImgCutPoint(0),
+m_imgId(""),
+m_ppImgSaver(0)
+{	
+}
+CRootStockCutPoint::~CRootStockCutPoint()
+{
+	this->clear_imginfo();	
+}
+
+void CRootStockCutPoint::clear_imginfo(){
+	if (m_pImginfoBinFork){
+		imginfo_release(&m_pImginfoBinFork);
+		m_pImginfoBinFork=0;
+	}
+	if (m_pImgCorners){
+		imginfo_release(&m_pImgCorners);
+		m_pImgCorners=0;
+	}
+	if (m_pImgCutPoint){
+		imginfo_release(&m_pImgCutPoint);
+		m_pImgCutPoint=0;
+	}
+}
+
+int CRootStockCutPoint::up_point_detect(
+	ImgInfo* imginfo, 
+	Mat&cimg, 
+	PositionInfo& posinfo
+	)
+{
+	// cimg --- color image, bgr
+	/*
+	# 找到竖直的径,并对径两侧兴趣区域进行分析:茎位置,分叉位置,用于协助确定上切割点位置,或下切割点位置
+    #     1)如果找到切割位置的角点,用角点作为上切割点,确定下切割点
+    #     2)如果没有找到上切割点,通过分叉位置确定下切割点,根据径粗推导出上切割点
+    h_ratio = 0.7 # 垂直方向hist,最高点的70%作为阈值,判断茎的位置
+    v_ration = 1.2 # 水平方向hist,茎粗确定后,茎粗的1.2倍断定为分叉点    
+    */
+    m_imgId = getImgId(img_type::rs);
+	//1 image segment
+	if(m_pLogger){				
+		m_pLogger->INFO(m_imgId +" rootstock cut_pt detect begin");				
+	}
+
+	clock_t t;
+	clock_t t0 = clock();
+	
+	
+
+	Mat img;
+	if(imginfo){
+		if(m_pLogger){		
+			stringstream buff;
+			buff<<m_imgId<<" rootstock image, width="<<imginfo->width
+				<<"\theight="<<imginfo->height;
+			m_pLogger->INFO(buff.str());
+		}
+		if(!isvalid(imginfo)){
+			if(m_pLogger){				
+				m_pLogger->ERRORINFO(m_imgId+" rootstock input image invalid.");				
+			}
+			throw_msg(m_imgId+" invalid input image");	
+			
+		}	
+		img = imginfo2mat(imginfo);
+	}
+	else{
+		if(m_pLogger){		
+			stringstream buff;
+			buff<<m_imgId<<"rootstock image, width="<<cimg.cols
+				<<"\theight="<<cimg.rows;
+			m_pLogger->INFO(buff.str());
+		}
+		if(cimg.empty()){
+			if(m_pLogger){				
+				m_pLogger->ERRORINFO(m_imgId+" rootstock input image invalid.");				
+			}
+			throw_msg(m_imgId +" invalid input image");
+			
+		}
+		img = cimg;
+	}
+	
+	if(m_cparam.self_camera){
+		image_set_bottom(img,20,8);
+        if(m_pLogger){				
+			m_pLogger->DEBUG(m_imgId+" image set bottom with pixel value 20.");				
+		}
+	}
+	if(m_cparam.rs_y_flip){
+		flip(img,img,0);
+        if(m_pLogger){				
+			m_pLogger->DEBUG(m_imgId+" image y fliped.");				
+		}
+	}
+	
+	//image saver
+	if(m_ppImgSaver && *m_ppImgSaver){
+		(*m_ppImgSaver)->saveImage(img, m_imgId);
+	}
+    if(m_pLogger){				
+		m_pLogger->DEBUG(m_imgId+" before image segment.");				
+	}	
+	///////////////////////////////////////////////////////
+	// image segment
+	this->img_segment(img);
+    if(m_pLogger){	
+		m_pLogger->DEBUG(m_imgId+" after image segment.");				
+	}
+
+	if(m_cparam.image_show){
+		destroyAllWindows();		
+		imshow_wait("rs_bin",m_binImg);		
+	}
+	else{
+		t = clock();
+		if(1000.0*((float)(t-t0))/CLOCKS_PER_SEC>(float)m_cparam.timeout_proc){
+			if(m_pLogger){				
+				m_pLogger->ERRORINFO(m_imgId+" rootstock timeout.");				
+			}
+			throw_msg(m_imgId+" time out");
+		}
+	}
+    if(m_pLogger){	
+		m_pLogger->DEBUG(m_imgId+" after m_binImg image show.");				
+	}
+	
+	//2 茎位置:x方向位置范围
+	// 2.1 计算砧木x方向位置,中心
+	vector<int> hist_col_pre;
+	mat_histogram(m_binImg, hist_col_pre, 0);		
+
+	int min_idx, max_idx;
+	min_idx = hist_col_pre.size();
+	max_idx = 0;
+	vector<int>::const_iterator it0 = hist_col_pre.begin();
+	for(vector<int>::const_iterator it = hist_col_pre.begin();it!=hist_col_pre.end();++it){
+		if(*it>=m_cparam.rs_min_hist_value){
+			int idx = it - it0;
+			if(idx<min_idx){min_idx = idx;}
+			if(idx > max_idx) {max_idx = idx;}
+		}
+	}
+	if(max_idx<=min_idx){
+		throw_msg(m_imgId+" invalid binary image, not exist valid objects");		
+	}
+
+	if(m_cparam.image_show){			
+		Mat tmp = m_binImg.clone();
+		cv::line(tmp,Point(min_idx,0), Point(min_idx,tmp.rows),Scalar(156),3);
+		cv::line(tmp,Point(max_idx,0), Point(max_idx,tmp.rows),Scalar(156),3);
+
+		imshow_wait("rs_bin_x_range", tmp);		
+	}
+    int width = max_idx-min_idx+1;	
+	int cent_x = (int)(0.5*(double) (max_idx-min_idx));
+
+    
+	vector<int> hist_col;
+	mat_histogram_w(m_binImg,hist_col);
+
+	if(m_cparam.image_show){		
+		Mat hist_col_img;
+		hist2image(hist_col,hist_col_img,1,m_cparam.image_col_grid,m_cparam.image_row_grid);		
+		imshow_wait("rs_hist_col", hist_col_img);		
+	}
+
+    if(m_pLogger){	
+		m_pLogger->DEBUG(m_imgId+" after histogram col.");				
+	}
+	int x0,x1,stem_x0, stem_x1;
+	get_stem_x_range_rscut(
+		hist_col,
+		m_cparam.rs_col_th_ratio,
+		m_cparam.rs_stem_x_padding,
+		cent_x,
+		width,
+		x0,
+		x1,
+		stem_x0,
+		stem_x1);
+
+	if(m_pLogger){		
+		stringstream buff;
+		buff<<m_imgId<<" rootstock image, x0="<<x0
+			<<"\tx1="<<x1
+			<<"\tstem_x0="<<stem_x0
+			<<"\tstem_x1="<<stem_x1;
+		m_pLogger->INFO(buff.str());
+	}
+
+	if(m_cparam.image_show){		
+		Mat tmp_img = m_binImg.clone();
+
+		line(tmp_img,Point(x0,0),Point(x0,m_binImg.rows-1),Scalar(100),2);
+		line(tmp_img,Point(x1,0),Point(x1,m_binImg.rows-1),Scalar(100),2);
+		//fork right point
+		imshow_wait("rs_x_field", tmp_img);				
+	}
+	
+	//3 茎分叉位置:y方向分叉位置识别
+	vector<int> hist_row;
+	mat_histogram(m_binImg,hist_row,1,-1,-1,x0,x1+1);
+	hist_filter(hist_row,0,5);
+	if(m_pLogger){	
+		m_pLogger->DEBUG(m_imgId+" after histogram row.");				
+	}
+
+	if(m_cparam.image_show){		
+		Mat hist_row_img;
+		hist2image(hist_row,hist_row_img, 0,m_cparam.image_col_grid,m_cparam.image_row_grid);		
+		imshow_wait("rs_hist_row", hist_row_img);		
+	}
+	else{
+		t = clock();
+		if(1000.0*((float)(t-t0))/CLOCKS_PER_SEC>(float)m_cparam.timeout_proc){
+			if(m_pLogger){				
+				m_pLogger->ERRORINFO(m_imgId+" rootstock timeout.");				
+			}
+			throw_msg(m_imgId+" time out");
+		}
+	}
+
+	int stem_fork_y_pre=-1,stem_fork_y=-1,stem_end_y=-1,stem_dia=-1;
+	
+	int roi_max_y=-1;
+	get_stem_y_fork_rs(
+		hist_row,
+		m_cparam.rs_row_th_ratio,
+		m_cparam.rs_stem_dia_min,
+		m_cparam.rs_stem_fork_y_min,
+		m_cparam.rs_stem_dia_mp,
+		stem_fork_y_pre,
+		stem_end_y,
+		stem_dia,
+		roi_max_y);
+
+	
+	Point fork_cent;
+	double max_radius;
+	double stem_angle;
+	get_stem_y_fork_rs_update(
+		m_binImg,
+		m_cparam.rs_stem_x_padding,
+		x0,
+		x1,
+		roi_max_y,
+		stem_end_y,
+		stem_dia,
+		m_cparam.image_show,
+		m_cparam.rs_cut_point_offset_ratio,
+		fork_cent,
+		max_radius,
+		stem_angle
+	);
+	stem_fork_y = fork_cent.y;
+
+	if(m_pLogger){		
+		stringstream buff;
+		buff<<m_imgId<<" rootstock image, stem_fork_y="<<stem_fork_y
+			<<"\tstem_end_y="<<stem_end_y
+			<<"\tstem_dia="<<stem_dia;
+		m_pLogger->INFO(buff.str());
+	}
+
+	//4 茎分叉位置,茎左侧边缘点识别
+	int stem_fork_left_x, stem_fork_right_x;
+	get_stem_fork_xs(
+		m_binImg,
+		stem_fork_y,
+		stem_x0,
+		stem_x1,
+		x0,
+		x1,		
+		stem_fork_left_x,
+		stem_fork_right_x);
+	int stem_fork_dia = stem_fork_right_x - stem_fork_left_x;
+
+	gcv_point<int> cut_pt = gcv_point<int>((int)(stem_fork_left_x + 0.5*(stem_fork_right_x - stem_fork_left_x)),stem_fork_y);
+
+	if(m_pLogger){		
+		stringstream buff;
+		buff<<m_imgId<<" rootstock image, stem_fork_x0="<<stem_fork_left_x
+			<<"\tstem_fork_x1="<<stem_fork_right_x
+			<<"\tstem_fork_diameter="<<stem_fork_dia;
+		m_pLogger->INFO(buff.str());
+	}
+
+	
+	//lower cut point	
+	gcv_point<int> lower_cut_pt = gcv_point<int>(-1,-1);	
+	find_lower_cut_point(m_binImg,cut_pt, lower_cut_pt, m_cparam.rs_cut_angle, stem_dia);
+	if(m_cparam.image_show){
+		Mat stem_img = m_binImg.clone();		
+		line(stem_img,Point(stem_x0,0),Point(stem_x0,stem_img.rows-1),Scalar(100),2);
+		line(stem_img,Point(stem_x1,0),Point(stem_x1,stem_img.rows-1),Scalar(100),2);
+		//fork left point
+		circle(stem_img, Point(stem_fork_left_x,stem_fork_y),5, Scalar(128,0,128), -1, 8,0);
+		circle(stem_img, Point(stem_fork_right_x,stem_fork_y),5, Scalar(128,0,128), -1, 8,0);	
+		
+		Mat gray_img = m_grayImg.clone();
+		circle(gray_img, Point(stem_fork_left_x,stem_fork_y),5, Scalar(128,0,128), -1, 8,0);
+		circle(gray_img, Point(stem_fork_right_x,stem_fork_y),5, Scalar(128,0,128), -1, 8,0);	
+		circle(gray_img, Point((int)cut_pt.x,cut_pt.y),5, Scalar(200,0,200), -1, 8,0);
+		circle(gray_img, Point(lower_cut_pt.x,lower_cut_pt.y),5, Scalar(200,0,200), -1, 8,0);
+		image_draw_line(gray_img,cut_pt.x,cut_pt.y,lower_cut_pt.x,lower_cut_pt.y);
+		imshow_wait("rs_fork_left_right_point", stem_img);		
+		imshow_wait("rs_fork_left_right_point", gray_img);
+	}
+	else{
+		t = clock();
+		if(1000.0*((float)(t-t0))/CLOCKS_PER_SEC>(float)m_cparam.timeout_proc){
+			if(m_pLogger){				
+				m_pLogger->ERRORINFO(m_imgId+" rootstock timeout.");				
+			}
+			throw_msg(m_imgId+" time out");
+		}
+	}
+
+	if(m_pLogger){		
+		stringstream buff;
+		buff<<m_imgId<<" rootstock image, upper_cut_point("<<cut_pt.x
+			<<","<<cut_pt.y<<")"<<", lower_cut_pt( "<<lower_cut_pt.x
+			<<","<<lower_cut_pt.y<<")";
+		m_pLogger->INFO(buff.str());
+	}
+
+	//////////////////////////////////////start
+	//v0.5.9.3 更改(2022-01-02)
+	//cut_pt.x = stem_fork_left_x + 0.5*(stem_fork_right_x - stem_fork_left_x);
+	//cut_pt.y = stem_fork_y;
+	///////////////////////////////////////end
+
+	double rs_cut_upoint_x = (double)cut_pt.x;
+	rs_cut_upoint_x -= (double)(img.cols/2.0);
+	rs_cut_upoint_x *= m_cparam.rs_cut_pixel_ratio;
+
+	double rs_cut_upoint_y = (double)cut_pt.y;
+	rs_cut_upoint_y = (double)(img.rows/2.0) - rs_cut_upoint_y;
+	rs_cut_upoint_y *= m_cparam.rs_cut_pixel_ratio;
+
+	double rs_stem_diameter = stem_dia * m_cparam.rs_cut_pixel_ratio;
+
+	double rs_cut_lpoint_x = lower_cut_pt.x;
+	rs_cut_lpoint_x -= (double)(img.cols/2.0);
+	rs_cut_lpoint_x *= m_cparam.rs_cut_pixel_ratio;
+
+	double rs_cut_lpoint_y = lower_cut_pt.y;
+	rs_cut_lpoint_y = (double)(img.rows/2.0) - rs_cut_lpoint_y;
+	rs_cut_lpoint_y *= m_cparam.rs_cut_pixel_ratio;
+
+	//posinfo.rs_cut_edge_length = 0.0;
+	posinfo.rs_cut_upoint_x = rs_cut_upoint_x;
+	posinfo.rs_cut_upoint_y = rs_cut_upoint_y;
+    posinfo.rs_stem_diameter = rs_stem_diameter;
+	posinfo.rs_cut_lpoint_x = rs_cut_lpoint_x;
+	posinfo.rs_cut_lpoint_y = rs_cut_lpoint_y;
+
+	if(m_pLogger){		
+		stringstream buff;
+		buff<<m_imgId<<" rootstock image, rs_cut_upoint(mm)("<<rs_cut_upoint_x
+			<<","<<rs_cut_upoint_y<<")"
+			<<", rs_stem_diameter(mm)="<<rs_stem_diameter
+			<<", lower_cut_pt(mm)("<<rs_cut_lpoint_x
+			<<","<<rs_cut_lpoint_y<<")";
+		m_pLogger->INFO(buff.str());
+	}
+
+	//  return images:	posinfo.pp_images
+	if(m_cparam.image_return){
+		this->clear_imginfo();
+		//1) 		
+		//stem x-range
+		line(m_binImg,Point(stem_x0,0),Point(stem_x0,m_binImg.cols-1),Scalar(100),2);
+		line(m_binImg,Point(stem_x1,0),Point(stem_x1,m_binImg.cols-1),Scalar(100),2);
+		//fork right point
+		circle(m_binImg, Point(stem_fork_left_x,stem_fork_y),5, Scalar(128,0,128), -1, 8,0);
+		m_pImginfoBinFork=mat2imginfo(m_binImg);	
+
+		//3 cut point int gray image	
+		circle(m_grayImg, Point(stem_fork_left_x,stem_fork_y),5, Scalar(128,0,128), -1, 8,0);
+		circle(m_grayImg, Point(stem_fork_right_x,stem_fork_y),5, Scalar(128,0,128), -1, 8,0);//v0.5.9.3 reference point
+		circle(m_grayImg, Point(cut_pt.x,stem_fork_y),5, Scalar(128,0,128), -1, 8,0);//v0.5.9.3 reference point
+		circle(m_grayImg, Point(cut_pt.x,stem_fork_y),2, Scalar(255,0,255), -1, 8,0);
+		circle(m_grayImg, Point(lower_cut_pt.x,lower_cut_pt.y),5, Scalar(200,0,200), -1, 8,0);
+		image_draw_line(m_grayImg,cut_pt.x,cut_pt.y,lower_cut_pt.x,lower_cut_pt.y);
+	
+		m_pImgCutPoint = mat2imginfo(m_grayImg);			
+		posinfo.pp_images[0]=m_pImginfoBinFork;
+		posinfo.pp_images[1]=m_pImgCutPoint;
+
+		if(m_ppImgSaver && *m_ppImgSaver){
+			(*m_ppImgSaver)->saveImage(m_binImg, m_imgId+"_rst_0");
+			(*m_ppImgSaver)->saveImage(m_grayImg, m_imgId+"_rst_1");
+		}		
+	}
+	if(m_pLogger){				
+		m_pLogger->INFO(m_imgId +" rootstock cut_pt detect finished.");				
+	}
+	return 0;
+}
+
+void CRootStockCutPoint::get_stem_root_y(
+		vector<int>&hist,		
+		double stem_dia_min,			
+		int & end_y)
+{
+	end_y = -1;
+
+	int start_idx, max_start_idx, max_end_idx, max_len;
+	start_idx= max_start_idx= max_end_idx=-1;
+	max_len=0;
+    
+	for(size_t i = 0; i<hist.size(); i++){
+		if(i==0 && hist[i]>=stem_dia_min){
+			start_idx=0;
+		}
+		if(i==0){continue;}
+
+		if(hist[i]>=stem_dia_min && hist[i-1]<stem_dia_min){
+			start_idx = i;
+			continue;
+		}
+		if(hist[i]<stem_dia_min && hist[i-1]>=stem_dia_min){
+			int lenth = i - start_idx;
+			if(lenth>max_len){
+				max_start_idx= start_idx;
+				max_end_idx = i;
+				max_len = lenth;
+			}
+			continue;
+		}
+		if(i==hist.size()-1){
+			if(hist[i]>=stem_dia_min){
+				int lenth = i - start_idx;
+				if(lenth>max_len){
+					max_start_idx= start_idx;
+					max_end_idx = i;
+					max_len = lenth;
+				}
+			}
+		}
+		
+	}
+	end_y = max_end_idx;	
+}
+
+void CRootStockCutPoint::get_default_cutpoint(
+		int fork_cent_x,
+		int fork_cent_y,
+		int fork_stem_dia,
+		Point2f& cut_pt)
+{
+	gcv_point<int> start_pt(fork_cent_x,fork_cent_y);
+	gcv_point<int> lower_cut_pt = gcv_point<int>(-1,-1);
+
+	find_lower_cut_point(m_binImg,start_pt,lower_cut_pt,m_cparam.rs_cut_angle+180.0, fork_stem_dia);
+	if(lower_cut_pt.x<0 || lower_cut_pt.y<0){
+		throw_msg(m_imgId+" invalid end points");	
+	}
+	double dist = start_pt.distance(lower_cut_pt);
+	if(dist>fork_stem_dia){
+		double ca = (m_cparam.rs_cut_angle+180.0)*0.0174532925199433;
+		cut_pt.x = start_pt.x+fork_stem_dia*cos(ca);
+		cut_pt.y = start_pt.y-fork_stem_dia*sin(ca);
+	}
+	else{
+		cut_pt.x = lower_cut_pt.x;
+		cut_pt.y = lower_cut_pt.y;
+	}
+}
+void CRootStockCutPoint::img_segment(Mat&img)
+{
+	Mat b_img;
+	if(img.channels()!=1){
+		//color image ,bgr, for testing		
+		cvtColor(img,m_grayImg,COLOR_BGR2GRAY);
+	}
+	else{
+		m_grayImg = img.clone();
+	}
+
+	Mat kernel = getStructuringElement(
+		MORPH_ELLIPSE, 
+		Size( 2*m_cparam.rs_morph_radius + 1, 2*m_cparam.rs_morph_radius+1 ),
+		Point( m_cparam.rs_morph_radius, m_cparam.rs_morph_radius )
+		);    
+	/*
+	morphologyEx(
+		g_img, 
+		m_grayImg, 
+		MORPH_CLOSE,
+		kernel,
+		Point(-1,-1),
+		m_cparam.rs_morph_iteration_gray
+		);*/
+
+	double th = threshold(m_grayImg, b_img, 255, 255,THRESH_OTSU);		
+
+	morphologyEx(
+		b_img, 
+		m_binImg, 
+		MORPH_CLOSE,
+		kernel,
+		Point(-1,-1),
+		m_cparam.rs_morph_iteration
+		);
+
+	//Canny(m_binImg, m_edgeImg,30,100);
+	
+}
+int CRootStockCutPoint::get_optimal_corner( 
+	int ref_x, 
+	int ref_y,
+	std::vector<Point2f>& corners, 
+	int stem_dia,
+	double cand_corner_box_width_ratio,
+	double cand_corner_box_xoffset_ratio,
+	double opt_corner_xoffset_ratio,
+	double opt_corner_yoffset_ratio,	
+	double corner_mask_radius_ratio,
+	Point2f& cut_pt)
+{
+
+	/*"""
+    通过茎左侧分叉点坐标( ref_x, ref_y)判别最优角点
+    stem_img, 二值图像,茎部分的局部图像
+    ref_x, ref_y, 检测到的茎分叉的左侧x,y坐标
+    corners, 图像中的角点
+
+    方法:
+    1 reference点上区域的角点,定义区域
+    2 评价角点
+       角点与直线ref_x的距离
+       角点与参考点间联通性(是否都是前景)
+       角点与参考点的距离(1.5倍茎粗)
+
+    """
+	*/
+	cut_pt.x = -1;
+	cut_pt.y = -1;
+
+    int cand_corner_box_window = (int)(cand_corner_box_width_ratio*stem_dia);
+	int bx = ref_x + (int)(cand_corner_box_xoffset_ratio*stem_dia) - cand_corner_box_window;
+    int by = ref_y - cand_corner_box_window;
+
+    roi_box<int> candidate_box = roi_box<int>(bx,by,cand_corner_box_window,cand_corner_box_window);
+    vector<Point2f>cand_pts;
+    for(size_t i=0; i<corners.size(); ++i){
+        int pt_x = corners[i].x;
+		int pt_y = corners[i].y;
+        if (candidate_box.isInBox(pt_x,pt_y)){
+			Point2f pt;
+			pt.x=corners[i].x;
+			pt.y=corners[i].y;
+            cand_pts.push_back(pt);
+		}
+	}
+
+	if(cand_pts.size()==0){
+		return 1;
+	}
+	//calculate corner pts' corner score of binary image
+	vector<double> cor_mag_scores;
+	corner_magnificence(
+		cand_pts,
+		stem_dia,
+		m_cparam.rs_corner_mask_rad_ratio,
+		cor_mag_scores
+		);
+    //# estimate candidate points' score
+    vector<double>cand_pts_score;
+	int optx = ref_x + (int)(opt_corner_xoffset_ratio * stem_dia);
+	int opty = ref_y + (int)(opt_corner_yoffset_ratio * stem_dia);
+    for(size_t i=0; i<cand_pts.size(); ++i){
+		int ptx = cand_pts[i].x;
+		int pty = cand_pts[i].y;        
+        double score = (double)(abs(ptx - optx));
+        double dist =sqrt((double)((ptx - optx)*(ptx - optx)) +
+			              (double)((pty - opty)* (pty - opty)));
+		double mag_score = stem_dia * (1.0 - cor_mag_scores[i]);
+        score += dist;
+		score += mag_score;
+        cand_pts_score.push_back(score);
+	}
+
+	auto smallest = min_element(begin(cand_pts_score),end(cand_pts_score));
+	size_t min_idx = distance(begin(cand_pts_score), smallest);
+    
+	cut_pt.x = cand_pts[min_idx].x;
+	cut_pt.y = cand_pts[min_idx].y;   
+	return 0;
+}
+
+void CRootStockCutPoint::corner_magnificence(
+		const vector<Point2f>& cand_pts,
+		int stem_dia,
+		double corner_mask_radius_ratio,
+		vector<double>& scores
+		)
+{
+	int r = int(stem_dia*corner_mask_radius_ratio);
+	if (r<=0){r = 1;}
+	int ptx,pty,x,y;
+	double cnt,cnt_pos,score;
+	for(size_t i=0; i<cand_pts.size();++i){
+		ptx = (int)(cand_pts[i].x);
+		pty = (int)(cand_pts[i].y); 
+		cnt = 0.0;
+		cnt_pos = 0.0;
+		for(int dx=-r;dx<=r;++dx){
+			x = ptx+dx;
+			if(x<0 || x>=m_binImg.cols){continue;}
+			for(int dy = -r;dy<=r;++dy){
+				y = pty+dy;
+				if(y<0 || y>=m_binImg.rows){continue;}
+				cnt+=1.0;
+				if(m_binImg.at<unsigned char>(y,x)>0 ){
+					cnt_pos+=1.0;
+				}
+			}
+		}
+		if(cnt>0){score = cnt_pos/cnt;}
+		else{score=0.0;}
+		scores.push_back(score);
+	}
+
+}
+
+double CRootStockCutPoint::get_cut_edge_length
+		(
+		Mat& bin_img,
+		Mat& edge_img,
+		gcv_point<int>& start_pt, //up cut point
+		gcv_point<int>& lower_cut_pt,//output
+		gcv_point<int>& root_pt,//output
+		double cut_angle,
+		int y_max,
+		int stem_dia,
+	    clock_t t0
+		)
+	{
+		double curve_lenth = 0.0;
+		gcv_point<int> pt_tmp = gcv_point<int>(-1,-1);
+		lower_cut_pt = pt_tmp;
+		
+		find_lower_cut_point(bin_img,start_pt, lower_cut_pt, m_cparam.rs_cut_angle, stem_dia);
+		curve_lenth = start_pt.distance(lower_cut_pt);
+
+		if(m_pLogger){		
+			stringstream buff;
+			buff<<m_imgId<<" rootstock image, found lower_cut_point("<<lower_cut_pt.x
+				<<", "<<lower_cut_pt.y<<"), upper_cut_point("<<start_pt.x<<","<<start_pt.y<<") to lower_cut_point length: "<<curve_lenth;
+			m_pLogger->INFO(buff.str());
+		}
+		//Mat edge_img;		
+		root_pt = pt_tmp;
+		//Canny(bin_img, edge_img,30,100);
+		double stem_curve_len = calculate_edge_length(edge_img,lower_cut_pt,root_pt,y_max,t0);
+		curve_lenth+=stem_curve_len;
+		return curve_lenth;
+	}
+	
+	void CRootStockCutPoint::find_lower_cut_point(
+		Mat& bin_img,
+		gcv_point<int>&start_pt,
+		gcv_point<int>&end_pt,//output
+		double cut_angle,
+		int stem_dia)
+	{
+		end_pt.x = -1;
+		end_pt.y = -1;
+
+		double step = 5.0;    
+		double polar_radius = step;
+		int height,width;
+		height = bin_img.rows;
+		width = bin_img.cols;
+
+		//ca = cut_angle*math.pi/180.0
+		double ca = cut_angle*0.0174532925199433;
+		int state = 0;
+		int nxt_x,nxt_y;
+		while(true) {
+			nxt_x = int(start_pt.x+polar_radius*cos(ca));
+			nxt_y = int(start_pt.y-polar_radius*sin(ca));
+			if (nxt_x<0 || nxt_x >= width){
+				state = 1;
+				break;
+			}
+			if (nxt_y < 0 || nxt_y >= height){
+				state = 1;
+				break;
+			}
+			if (bin_img.at<unsigned char>(nxt_y,nxt_x)==0){
+				//#在cur_pt和 nxt_pt两点间找到边缘点
+				int small_step=1;
+				double pr = polar_radius;
+				int pre_x, pre_y;
+				while (pr>0){
+					pr-=small_step;
+					pre_x = int(start_pt.x + pr * cos(ca));
+					pre_y = int(start_pt.y - pr * sin(ca));
+					if (bin_img.at<unsigned char>(pre_y, pre_x) > 0){						
+						end_pt.x = pre_x;
+						end_pt.y = pre_y;
+						double dd = start_pt.distance(end_pt);
+						if (dd <(double)(stem_dia *0.66)){
+							break;
+						}
+						else{
+							state = 2;
+							break;
+						}
+					}
+				}
+            
+			}
+			if(state==2){
+				break;
+			}
+
+			polar_radius += step;
+		}//while
+		if(state==1){//超出图像范围,直接计算
+			end_pt.x = start_pt.x + stem_dia/2;
+			end_pt.y = start_pt.y + (int)(0.5*stem_dia*fabs(tan(ca))+0.5);
+		}
+	}
+	double CRootStockCutPoint::calculate_edge_length(
+		Mat& edge_img,
+		gcv_point<int>&lower_cut_pt,
+		gcv_point<int>&root_pt,//output
+		int y_max,
+	    clock_t t0
+		)
+	{
+		double sum_len = 0.0;
+
+		//cur_x, cur_y = int(start_pt[0]), int(start_pt[1])
+		root_pt.x = -1;
+		root_pt.y = -1;
+
+		gcv_point<int> cur_pt = gcv_point<int>(lower_cut_pt);
+		gcv_point<int> pre_pt = gcv_point<int>(-1,-1);
+		gcv_point<int> nxt_pt = gcv_point<int>(-1,-1);
+		int state = 0;
+		double root2 = sqrt(2.0);
+	    clock_t t;	
+		int cnt = 0;
+		if(m_pLogger){				
+			stringstream buff;
+			buff<<m_imgId<<"  before curve crawling, finishing conditions: 1) ymax="<<y_max<<" 2) crawling to start_point("<<lower_cut_pt.x
+				<<","<<lower_cut_pt.y<<")";
+			m_pLogger->DEBUG(buff.str());
+		}
+		while (true){
+			cnt +=1;			
+			if (!m_cparam.image_show && cnt%50 == 0){
+				t = clock();
+				if(1000.0*((float)(t-t0))/CLOCKS_PER_SEC>(float)m_cparam.timeout_proc){
+					throw_msg(m_imgId+" time out");
+				}
+			}
+			get_next_pt(edge_img, cur_pt, pre_pt,nxt_pt,false);
+			if( nxt_pt.x == cur_pt.x || nxt_pt.y == cur_pt.y){
+				sum_len += 1.0;
+			}
+			if( nxt_pt.x != cur_pt.x && nxt_pt.y != cur_pt.y){
+				sum_len += root2;
+			}
+			pre_pt = cur_pt;			
+			cur_pt = nxt_pt;
+            
+			if(m_pLogger){		
+				stringstream buff;
+				buff<<m_imgId<<" index: "<<cnt<<"\tcurrent point ("<<pre_pt.x
+					<<", "<<pre_pt.y<<")";
+				m_pLogger->DEBUG(buff.str());
+			}
+			if( nxt_pt.y == y_max){
+				state = 1;
+				root_pt = nxt_pt;
+				break;
+			}
+
+			if (nxt_pt == lower_cut_pt){
+				state = 2;
+				break;
+			}
+		}
+		if (m_pLogger){
+			m_pLogger->DEBUG(m_imgId + " finished curve crawling!");
+		}
+		if (state == 2){
+			return 0.0;
+		}
+		if (state == 1){			
+			return sum_len;
+		}
+		else{
+			return 0.0;
+		}
+
+	}
+
+	int CRootStockCutPoint::get_root_point_reid(
+		int x0,
+		int x1,
+		int stem_end_y,		
+		bool is_right  //是否右侧根部点, true-右侧,false-左侧
+		)
+	{
+		int stem_end_x=-1;
+		int start_x,end_x,max_start_x,max_end_x, max_len;
+		start_x = end_x = -1;
+		max_start_x = max_end_x = -1;
+		max_len = -1;
+
+		
+
+		for(int i =x0; i<x1;++i){
+			if(i==x0 && m_binImg.at<unsigned char>(stem_end_y,i)>0){
+				start_x = i;
+				continue;
+			}
+			if(i==x1-1 && m_binImg.at<unsigned char>(stem_end_y,i)>0){
+				end_x = i;
+				int length = end_x - start_x+1;
+				if (length >max_len){
+					max_len = length;
+					max_start_x = start_x;
+					max_end_x = end_x;
+				}
+			}
+			if(m_binImg.at<unsigned char>(stem_end_y,i)>0 && 
+				m_binImg.at<unsigned char>(stem_end_y,i-1)==0){
+				start_x = i;
+				continue;
+			}
+			if(m_binImg.at<unsigned char>(stem_end_y,i)==0 && 
+				m_binImg.at<unsigned char>(stem_end_y,i-1)>0){
+				end_x = i;
+				int length = end_x - start_x+1;
+				if (length >max_len){
+					max_len = length;
+					max_start_x = start_x;
+					max_end_x = end_x;
+				}
+			}
+
+		}
+		if (is_right){
+			return max_end_x;
+		}
+		return max_start_x;    
+	}
+
+	void  CRootStockCutPoint::find_upper_cut_point_reid(
+		Mat& edge_img,
+		gcv_point<int>&start_pt,
+		gcv_point<int>&end_pt,//output
+		double curve_length
+		)
+		{
+			end_pt.x  =-1;
+			end_pt.y = -1;
+		gcv_point<int> cur_pt = gcv_point<int>(start_pt);
+		gcv_point<int> pre_pt = gcv_point<int>(-1,-1);
+		gcv_point<int> nxt_pt = gcv_point<int>(-1,-1);
+		//pre_x=-1
+		//pre_y=-1
+		double root2 = sqrt(2.0);
+		double sum_len =0.0;
+		int state = 0;
+
+		if(m_pLogger){				
+			stringstream buff;
+			buff<<m_imgId<<"  reid,  before curve crawling, finishing conditions: curve length="<<curve_length
+			<<", start_point(root)("<<start_pt.x
+				<<","<<start_pt.y<<")";
+			m_pLogger->DEBUG(buff.str());
+
+		}
+
+		while (true){
+			get_next_pt(edge_img,cur_pt,pre_pt,nxt_pt,true);
+			if (nxt_pt.x==cur_pt.x || nxt_pt.y==cur_pt.y){
+				sum_len+=1.0;
+			}
+			if ( nxt_pt.x!=cur_pt.x && nxt_pt.y!=cur_pt.y){
+				sum_len += root2;
+			}			
+			pre_pt = cur_pt;
+			cur_pt = nxt_pt;
+			if(m_pLogger){		
+				stringstream buff;
+				buff<<m_imgId<<"current point ("<<pre_pt.x
+					<<", "<<pre_pt.y<<")";
+				m_pLogger->DEBUG(buff.str());
+			}
+
+			if (sum_len>=curve_length){
+				state = 1;
+				break;
+			}
+			if (nxt_pt == start_pt){
+				state = 2;
+				break;
+			}
+		}
+
+		if (state==2){
+			end_pt.x  =-1;
+			end_pt.y = -1;
+		}
+		if (state == 1){
+			end_pt = cur_pt;
+		}			
+		else{
+			end_pt.x  =-1;
+			end_pt.y = -1;
+		}
+	}
+
+};

+ 128 - 0
cut_point_rs.h

@@ -0,0 +1,128 @@
+//cut point recognization for rootstock plant
+#pragma once
+#include <time.h>
+#include <opencv.hpp>
+#include "data_def_api.h"
+#include "data_def.h"
+#include "logger.h"
+#include "imstorage_manager.h"
+
+using namespace cv;
+namespace graft_cv{
+
+class CRootStockCutPoint{
+public:
+	CRootStockCutPoint(ConfigParam&c,CGcvLogger*pLog=0);
+	~CRootStockCutPoint();
+	int up_point_detect(//上切割点,切前识别
+		ImgInfo*, 
+		Mat&,
+		PositionInfo& posinfo
+		);
+
+	//int up_point_reid( // 上切割点,切后重识别
+	//	ImgInfo*, 
+	//	Mat&,
+	//	double edge_length,
+	//	PositionInfo& posinfo
+	//	);
+	void set_image_saver(CImStoreManager** ppis){m_ppImgSaver=ppis;}
+private:	
+	Mat m_binImg;// binary image
+	Mat m_grayImg;// gray image
+	Mat m_edgeImg;
+	string m_imgId;
+	CImStoreManager** m_ppImgSaver;
+
+	//返回图片,用于调试
+	ImgInfo* m_pImginfoBinFork;//fork-y, right-x
+	ImgInfo* m_pImgCorners;//corners, reference-point, candidate box
+	ImgInfo* m_pImgCutPoint;//reference-point, cutpoint
+
+
+	//global configure parameters
+	ConfigParam& m_cparam;
+	CGcvLogger * m_pLogger;
+	// image segment
+	void img_segment(Mat&);
+	
+	// get_optimal_corner()
+	// return 0--success, 1--error
+	int get_optimal_corner( 
+		int ref_x, 
+		int ref_y,
+		std::vector<Point2f>& corners,
+		int stem_dia,
+		double cand_corner_box_width_ratio,
+		double cand_corner_box_xoffset_ratio,
+		double opt_corner_xoffset_ratio,
+	    double opt_corner_yoffset_ratio,
+		double corner_mask_radius_ratio,
+		Point2f& cut_pt);
+	void get_default_cutpoint(
+		int fork_cent_x,
+		int fork_cent_y,
+		int fork_stem_dia,
+		Point2f& cut_pt);
+
+	void clear_imginfo();
+
+	/// pre-cut calculationo
+	//# 通过砧木切前切割点识别结果(切割点)、二值图像,茎根y值,刀角度
+	//# 输出切割点到根部的边缘曲线长度
+	double get_cut_edge_length(
+		Mat& bin_img,
+		Mat& edge_img,
+		gcv_point<int>& start_pt, //up cut point
+		gcv_point<int>& lower_cut_pt,//output
+		gcv_point<int>& root_pt,//output
+		double cut_angle,
+		int y_max,
+		int stem_dia,
+	    clock_t t0
+		);
+
+	void find_lower_cut_point(
+		Mat& bin_img,
+		gcv_point<int>&start_pt,
+		gcv_point<int>&end_pt,//output
+		double cut_angle,
+		int stem_dia);
+	double calculate_edge_length(
+		Mat& edge_img,
+		gcv_point<int>&lower_cut_pt,
+		gcv_point<int>&root_pt,//output
+		int y_max,
+	    clock_t t0
+		);
+
+	//corner evluation
+	void corner_magnificence(
+		const vector<Point2f>& cand_pts,
+		int stem_dia,
+		double corner_mask_radius_ratio,
+		vector<double>& scores
+		);
+
+
+	//functions for reid 
+	int get_root_point_reid(
+		int x0,
+		int x1,
+		int stem_end_y,		
+		bool is_right  //是否右侧根部点, true-右侧,false-左侧
+		);
+	void  find_upper_cut_point_reid(
+		Mat& edge_img,
+		gcv_point<int>&start_pt,
+		gcv_point<int>&end_pt,//output
+		double curve_length
+		);
+	void get_stem_root_y(
+		vector<int>&hist_row,		
+		double rs_stem_dia_min,			
+		int &stem_end_y);
+
+
+};
+};

+ 595 - 0
cut_point_sc.cpp

@@ -0,0 +1,595 @@
+#include <opencv2\imgproc\imgproc.hpp>
+#include "cut_point_sc.h"
+#include "utils.h"
+using namespace cv;
+
+namespace graft_cv{
+
+CScionCutPoint::CScionCutPoint(ConfigParam&cp,CGcvLogger*pLog/*=0*/)
+:m_cparam(cp),
+m_pLogger(pLog),
+m_pImginfoBinFork(0),
+m_imgId(""),
+m_ppImgSaver(0),
+m_folder_th(-1)
+{	
+	
+}
+CScionCutPoint::~CScionCutPoint()
+{
+	this->clear_imginfo();
+}
+
+void CScionCutPoint::clear_imginfo(){
+	if (m_pImginfoBinFork){
+		imginfo_release(&m_pImginfoBinFork);
+		m_pImginfoBinFork=0;
+	}	
+}
+
+int CScionCutPoint::up_point_detect(
+	ImgInfo* imginfo, 
+	Mat&cimg, 
+	PositionInfo& posinfo
+	)
+{
+	clock_t t;
+	clock_t t0 = clock();
+	m_imgId = getImgId(img_type::sc);
+
+	if(m_pLogger){				
+		m_pLogger->INFO(m_imgId +" scion cut_pt detect begin.");				
+	}
+	
+	// cimg --- color image, bgr
+	Mat img;
+	if(imginfo){
+		if(m_pLogger){		
+			stringstream buff;
+			buff<<m_imgId<<" scion image, width="<<imginfo->width
+				<<"\theight="<<imginfo->height;
+			m_pLogger->INFO(buff.str());
+		}
+
+		if(!isvalid(imginfo)){
+			if(m_pLogger){				
+				m_pLogger->ERRORINFO(m_imgId+" scion input image invalid.");				
+			}
+			throw_msg( m_imgId+" invalid input image");
+		}
+		img = imginfo2mat(imginfo);
+	}
+	else{
+		if(m_pLogger){		
+			stringstream buff;
+			buff<<m_imgId<<" scion image, width="<<cimg.cols
+				<<"\theight="<<cimg.rows;
+			m_pLogger->INFO(buff.str());
+		}
+		if(cimg.empty()){
+			if(m_pLogger){				
+				m_pLogger->ERRORINFO(m_imgId+" scion input image invalid.");				
+			}
+			throw_msg(m_imgId+" invalid input image");
+		}
+		img = cimg;
+	}
+
+	
+
+	if(m_cparam.self_camera){
+		image_set_bottom(img,20,8);
+		if(m_pLogger){				
+			m_pLogger->DEBUG(m_imgId+" scion image set bottom with pixel value 20.");				
+		}
+	}
+	if(m_cparam.sc_y_flip){
+		flip(img,img,0);
+		if(m_pLogger){				
+			m_pLogger->DEBUG(m_imgId+" scion image y fliped.");				
+		}
+	}
+	//image saver
+	if(m_ppImgSaver && *m_ppImgSaver){
+		(*m_ppImgSaver)->saveImage(img, m_imgId);
+		if(m_pLogger){				
+			m_pLogger->DEBUG(m_imgId+" scion after image save.");				
+		}
+	}
+	if(m_pLogger){				
+		m_pLogger->DEBUG(m_imgId+" scion before image segment.");				
+	}
+
+	//////////////////////////////////////////////////////////////////
+	//image segment
+	img_segment(img);
+	if(m_pLogger){	
+		m_pLogger->DEBUG(m_imgId+" scion after scion image segment.");				
+	}
+
+	if(m_cparam.image_show){
+		destroyAllWindows();	
+		imshow_wait("sc_gray", m_grayImg);	
+		imshow_wait("sc_bin", m_binImg);		
+	}
+	
+	if(m_pLogger){	
+		m_pLogger->DEBUG(m_imgId+" scion after m_binImg\m_grayImg image show.");				
+	}
+
+	//////////////////////////////////////////////////////////////////
+	//stem x-range
+	vector<int> hist_col;
+	mat_histogram(m_binImg,hist_col);
+
+	if(m_cparam.image_show){		
+		Mat hist_col_img;
+		hist2image(hist_col,hist_col_img,1,m_cparam.image_col_grid,m_cparam.image_row_grid);		
+		imshow_wait("sc_hist_col", hist_col_img);		
+	}
+
+	if(m_pLogger){	
+		m_pLogger->DEBUG(m_imgId+" scion after histogram col.");				
+	}
+
+	int x0,x1,stem_x0, stem_x1;
+	get_stem_x_range_scion(hist_col,
+		m_cparam.sc_col_th_ratio,
+		m_cparam.sc_stem_x_padding,
+		x0,
+		x1,
+		stem_x0,
+		stem_x1
+		);
+
+	if(m_pLogger){		
+		stringstream buff;
+		buff<<m_imgId<<" scion image, x0="<<x0
+			<<"\tx1="<<x1
+			<<"\tstem_x0="<<stem_x0
+			<<"\tstem_x1="<<stem_x1;
+		m_pLogger->INFO(buff.str());
+	}
+
+	if(m_cparam.image_show){		
+		Mat tmp_img = m_binImg.clone();
+
+		line(tmp_img,Point(x0,0),Point(x0,m_binImg.rows-1),Scalar(100),2);
+		line(tmp_img,Point(x1,0),Point(x1,m_binImg.rows-1),Scalar(100),2);
+		//fork right point
+		imshow_wait("sc_x_field", tmp_img);				
+	}	
+	/////////////////////////////////////////////////////
+	// 茎分离,通过高亮夹子分割
+	vector<int> hist_row;
+	mat_histogram(m_binImg,hist_row,1,-1,-1,x0,x1+1);
+
+	if(m_pLogger){	
+		m_pLogger->DEBUG(m_imgId+" scion after histogram row.");				
+	}
+
+	if(m_cparam.image_show){		
+		Mat hist_row_img;
+		hist2image(hist_row,hist_row_img, 0,m_cparam.image_col_grid,m_cparam.image_row_grid);		
+		imshow_wait("sc_hist_row", hist_row_img);		
+	}
+	else{
+		t = clock();
+		if(1000.0*((float)(t-t0))/CLOCKS_PER_SEC>(float)m_cparam.timeout_proc){
+			throw_msg(m_imgId+" time out");
+		}
+		
+	}
+	int stem_y_min=-1;//,stem_dia=-1,stem_root_y=-1;
+	vector<int> sub_hist_col;
+	vector<int> sub_hist_row;
+	int sub_stem_x0,sub_stem_x1,sub_x0,sub_x1;
+	sub_x0 = sub_x1=sub_stem_x0=sub_stem_x1=-1;
+	Mat scionBinImg;	
+	get_stem_local_img(
+		hist_row,		
+		m_cparam.sc_stem_dia_min,
+		x0,
+		x1,
+		stem_y_min,
+		sub_hist_col,
+		sub_hist_row,
+		scionBinImg);	
+
+	get_stem_x_range_scion(
+		sub_hist_col,
+		m_cparam.sc_col_th_ratio,
+		m_cparam.sc_stem_x_padding,
+		sub_x0,
+		sub_x1,
+		sub_stem_x0,
+		sub_stem_x1);
+	
+	int ymin=-1,ymax=-1;
+	get_cut_up_point(
+		sub_hist_row,
+		stem_y_min,		
+		m_cparam.sc_r2_th,
+		m_cparam.sc_average_window,
+		m_cparam.sc_r2_window,
+		ymin,
+		ymax);		
+
+    int slop_fold_x = get_stem_fork_left(
+		scionBinImg,
+		ymin,
+		sub_stem_x0, 
+		sub_stem_x1,
+		m_cparam.sc_stem_edge_detect_window);
+
+	int slop_cent_y = (int)((float)(ymin+ymax)/2.0);
+	int slop_cent_x = get_stem_fork_left(
+		scionBinImg,
+		slop_cent_y,
+		sub_stem_x0, 
+		sub_stem_x1,
+		m_cparam.sc_stem_edge_detect_window);
+
+	int slop_lower_x = get_stem_fork_left(
+		scionBinImg,
+		ymax,
+		sub_stem_x0, 
+		sub_stem_x1,
+		m_cparam.sc_stem_edge_detect_window);
+
+	//update sub_stem_x0, sub_stem_x1
+	stem_x0 = x0 + sub_stem_x0;
+	stem_x1	= x0 + sub_stem_x1;	
+	slop_fold_x += x0;
+	slop_cent_x += x0;
+	slop_lower_x += x0;
+
+	//update ymin,ymax
+	ymin += stem_y_min;
+	ymax += stem_y_min;
+	slop_cent_y += stem_y_min;
+
+	Mat binRoi = m_binImg(Rect(x0,stem_y_min,scionBinImg.cols,scionBinImg.rows));
+	scionBinImg.copyTo(binRoi);
+	//imshow_wait("m_binImg", m_binImg);
+
+	if(m_pLogger){		
+		stringstream buff;
+		buff<<m_imgId<<" scion image result(pixel) x0="<<x0
+			<<", x1="<<x1
+			<<", stem_x0="<<stem_x0
+			<<", stem_x1="<<stem_x1
+			<<", cut_pt=("<<slop_fold_x<<","<<ymin<<")"
+			<<", cut_cent_pt=("<<slop_cent_x<<","<<slop_cent_y<<")"
+			<<", cut_lower_pt=("<<slop_lower_x<<","<<ymax<<")"
+			<<", stem_y_max="<<ymax;
+		m_pLogger->INFO(buff.str());
+	}
+
+	if(m_cparam.image_show){		
+		Mat result_img = m_binImg.clone();
+		//stem x-range
+		line(result_img,Point(stem_x0,0),Point(stem_x0,m_binImg.rows-1),Scalar(100),2);
+		line(result_img,Point(stem_x1,0),Point(stem_x1,m_binImg.rows-1),Scalar(100),2);
+
+		//fork y line
+		line(result_img,Point(stem_x0,stem_y_min),Point(stem_x1,stem_y_min),Scalar(100),2);
+		line(result_img,Point(stem_x0,ymax),Point(stem_x1,ymax),Scalar(100),2);
+		//fold right point
+		circle(result_img, Point(slop_fold_x,ymin),2, Scalar(150,0,128), -1, 8,0);
+		circle(result_img, Point(slop_cent_x,slop_cent_y),2, Scalar(150,0,128), -1, 8,0);
+		circle(result_img, Point(slop_lower_x,ymax),2, Scalar(150,0,128), -1, 8,0);
+		imshow_wait("sc_result", result_img);		
+	}
+
+	
+
+	double sc_cut_upoint_x, sc_cut_upoint_y,sc_cut_cpoint_x, sc_cut_cpoint_y, sc_cut_lpoint_x, sc_cut_lpoint_y;
+	sc_cut_upoint_x = (double)slop_fold_x;
+	sc_cut_upoint_x	-= (double)(img.cols/2.0);
+	sc_cut_upoint_x *= m_cparam.sc_cut_pixel_ratio;
+
+	sc_cut_upoint_y = (double)ymin;
+	sc_cut_upoint_y = (double)(img.rows/2.0) - sc_cut_upoint_y;
+	sc_cut_upoint_y *= m_cparam.sc_cut_pixel_ratio;
+
+
+	sc_cut_cpoint_x = (double)slop_cent_x;
+	sc_cut_cpoint_x	-= (double)(img.cols/2.0);
+	sc_cut_cpoint_x *= m_cparam.sc_cut_pixel_ratio;
+
+	sc_cut_cpoint_y = (double)slop_cent_y;
+	sc_cut_cpoint_y = (double)(img.rows/2.0) - sc_cut_cpoint_y;
+	sc_cut_cpoint_y *= m_cparam.sc_cut_pixel_ratio;
+
+	
+	sc_cut_lpoint_x = (double)slop_lower_x;
+	sc_cut_lpoint_x	-= (double)(img.cols/2.0);
+	sc_cut_lpoint_x *= m_cparam.sc_cut_pixel_ratio;
+
+	sc_cut_lpoint_y = (double)ymax;
+	sc_cut_lpoint_y = (double)(img.rows/2.0) - sc_cut_lpoint_y;
+	sc_cut_lpoint_y *= m_cparam.sc_cut_pixel_ratio;
+
+
+	posinfo.sc_cut_upoint_x = sc_cut_upoint_x;
+	posinfo.sc_cut_upoint_y = sc_cut_upoint_y;
+	posinfo.sc_cut_cpoint_x = sc_cut_cpoint_x;
+	posinfo.sc_cut_cpoint_y = sc_cut_cpoint_y;
+
+	posinfo.sc_cut_lpoint_x = sc_cut_lpoint_x;
+	posinfo.sc_cut_lpoint_y = sc_cut_lpoint_y;
+
+
+	if(m_pLogger){		
+		stringstream buff;
+		buff<<m_imgId<<" scion image result(mm)"
+			<<", cut_pt=("<<sc_cut_upoint_x<<","<<sc_cut_upoint_y<<")"
+			<<", cut_cent_pt=("<<sc_cut_cpoint_x<<","<<sc_cut_cpoint_y<<")"
+			<<", cut_lower_pt=("<<sc_cut_lpoint_x<<","<<sc_cut_lpoint_y<<")";
+		m_pLogger->INFO(buff.str());
+	}
+
+	// posinfo.pp_images  	
+	if(m_cparam.image_return){
+		this->clear_imginfo();
+		//1) 		
+		//stem x-range
+		line(m_binImg,Point(stem_x0,0),Point(stem_x0,m_binImg.cols-1),Scalar(100),2);
+		line(m_binImg,Point(stem_x1,0),Point(stem_x1,m_binImg.cols-1),Scalar(100),2);
+
+		//fork y line
+		line(m_binImg,Point(stem_x0,stem_y_min),Point(stem_x1,stem_y_min),Scalar(100),2);
+		line(m_binImg,Point(stem_x0,ymax),Point(stem_x1,ymax),Scalar(100),2);
+		//fold right point
+		circle(m_binImg, Point(slop_fold_x,ymin),5, Scalar(128,0,128), -1, 8,0);
+		circle(m_binImg, Point(slop_cent_x,slop_cent_y),5, Scalar(128,0,128), -1, 8,0);
+		circle(m_binImg, Point(slop_lower_x,ymax),5, Scalar(128,0,128), -1, 8,0);
+		m_pImginfoBinFork=mat2imginfo(m_binImg);		
+		//
+		posinfo.pp_images[0]=m_pImginfoBinFork;		
+		if(m_ppImgSaver && *m_ppImgSaver){
+			(*m_ppImgSaver)->saveImage(m_binImg, m_imgId+"_rst_0");			
+		}
+	}
+	if(m_pLogger){				
+		m_pLogger->INFO(m_imgId +" scion cut_pt detect finished.");				
+	}
+	return ymin;
+}
+void CScionCutPoint::img_segment(Mat&img)
+{
+	if(img.channels()!=1){
+		//color image ,bgr, for testing
+		Mat b_img;
+		cvtColor(img,m_grayImg,COLOR_BGR2GRAY);		
+		double th = threshold(
+			m_grayImg,
+			b_img,
+			255,
+			255,
+			THRESH_OTSU
+			);
+	   
+		Mat kernel = getStructuringElement(
+			MORPH_RECT,
+			Size( 2*m_cparam.sc_morph_radius + 1, 2*m_cparam.sc_morph_radius+1),
+			Point( m_cparam.sc_morph_radius, m_cparam.sc_morph_radius)
+			);  
+		morphologyEx(
+			b_img,
+			m_binImg,
+			MORPH_CLOSE,
+			kernel,
+			Point(-1,-1),
+			m_cparam.sc_morph_iteration);
+		
+	}
+	else{
+		//from imginfo image, gray image 
+		//int morph_size = 1;	
+		m_grayImg = img.clone();
+		Mat b_img;
+		double th = threshold(img, b_img, 255, 255,THRESH_OTSU);
+		Mat kernel = getStructuringElement( 
+			MORPH_RECT, 
+			Size( 2*m_cparam.sc_morph_radius + 1, 2*m_cparam.sc_morph_radius+1 ), 
+			Point( m_cparam.sc_morph_radius, m_cparam.sc_morph_radius)
+			);    
+		morphologyEx(
+			b_img, 
+			m_binImg, 
+			MORPH_OPEN,
+			kernel,
+			Point(-1,-1),
+			m_cparam.sc_morph_iteration
+			);
+	}	
+}
+void CScionCutPoint::get_stem_local_img(
+	const std::vector<int>& hist,	
+	int stem_dia_min,
+	int x0, 
+	int x1,
+	int& stem_y_min,
+	vector<int>& hist_col,
+	vector<int>& hist_row,
+	Mat& scionBinImg
+	)
+{
+	//1 夹子被罩上背景板,不能以高亮夹子定位
+	//2 直接找x范围内下方的大目标作为识别对象,目标像素面积
+    //3 截取部分图像单独做二值化得到二值图像
+    //4 局部图的histogram
+
+	//0 设定目标面积最小值
+	int min_obj_area = 100;
+
+    //1 reverse histogram
+	std::vector<int>reversed_hist;	
+	for(int i=hist.size()-1; i>=0;i--){		
+		reversed_hist.push_back(hist[i]);
+	}    	
+	stem_y_min = -1;
+	int stem_y_max = -1;
+	int start_idx, end_idx;
+	start_idx = end_idx = -1;
+	for(size_t i=0; i < reversed_hist.size(); ++i){
+		if(i==0){
+			if(reversed_hist[i]>=stem_dia_min){start_idx=i;}
+			continue;
+		}
+		if(reversed_hist[i]>=stem_dia_min && reversed_hist[i-1]<stem_dia_min){
+			start_idx=i;
+			continue;
+		}
+		if(reversed_hist[i]<stem_dia_min && reversed_hist[i-1]>=stem_dia_min){
+			//计算面积
+			int area = 0;
+			for(int oi=start_idx;oi<i;++oi){
+				area +=reversed_hist[oi];
+			}
+			if(area >= min_obj_area){
+				end_idx = i-1;
+				break;
+			}
+		}
+		if(i==reversed_hist.size()-1){
+			if(reversed_hist[i]>=stem_dia_min){
+				int area = 0;
+				for(int oi=start_idx;oi<=i;++oi){
+					area +=reversed_hist[oi];
+				}
+				if(area >= min_obj_area){
+					end_idx = i;
+					break;
+				}
+			}	
+		}
+	}
+	if(start_idx<0 || end_idx<0 || end_idx<=start_idx){		
+		throw_msg( m_imgId+string(" scion stem NOT exists valid sub-image"));
+	}
+
+	stem_y_min = end_idx + m_cparam.sc_clip_padding;
+	stem_y_max = start_idx - 10* m_cparam.sc_clip_padding;
+
+	if(stem_y_min>=m_grayImg.rows){stem_y_min = m_grayImg.rows;}
+	if(stem_y_max<0){stem_y_max=0;}
+	
+	//2	
+	if(m_cparam.image_show){
+		Mat grayimg = m_grayImg.clone();
+		rectangle(grayimg,
+			Point(x0,grayimg.rows-stem_y_min),
+			Point(x1,grayimg.rows-stem_y_max),
+			Scalar(128));
+		imshow_wait("gray_rect",grayimg);
+	}
+
+	Mat scionImg = m_grayImg(Rect(x0,
+		m_grayImg.rows-stem_y_min,
+		x1-x0,
+		stem_y_min-stem_y_max));
+
+	if(m_pLogger){		
+		stringstream buff;
+		buff<<m_imgId<<" scion image, sub image(x0,y0,width,height), x0="<<x0
+			<<", y0="<<m_grayImg.rows-stem_y_min
+			<<", width="<<x1-x0
+			<<", height="<<stem_y_min-stem_y_max;
+		m_pLogger->INFO(buff.str());
+	}
+	
+	//Mat scionBinImg;
+	double th = threshold(scionImg, scionBinImg, 255, 255,THRESH_OTSU);	
+	if(m_cparam.image_show){
+	   imshow_wait("scion_sub_bin",scionBinImg);
+	   vector<int>hist;
+	   mat_histogram(scionBinImg,hist,1);
+	   Mat hist_col_img;
+		hist2image(hist,hist_col_img,1,m_cparam.image_col_grid,m_cparam.image_row_grid);		
+		imshow_wait("sc_sub_hist_col", hist_col_img);
+	}
+	//3
+	hist_row.clear();
+	mat_histogram(scionBinImg,hist_row,1,-1,-1,-1,-1);
+	hist_col.clear();
+	mat_histogram(scionBinImg,hist_col,0,-1,-1,-1,-1);
+	stem_y_min = m_grayImg.rows-stem_y_min;
+};
+void CScionCutPoint::get_cut_up_point(
+	const std::vector<int>& sub_hist,
+	int stem_y_min,	
+	double r2_ratio_th, 
+	int average_window,
+	int r2_window,
+	int& ymin, 
+	int& ymax)
+{
+
+	int start_idx=-1;
+	int max_len=0;
+	int max_start_idx = -1;
+	int max_end_idx = -1;
+	for(size_t i=0;i<sub_hist.size();++i){		
+		if(i==0 && sub_hist[i]>0){
+			start_idx=i;
+			continue;
+		}
+		if(i==0){continue;}
+		if(sub_hist[i]>0 && sub_hist[i-1]==0){
+			start_idx=i;
+			continue;
+		}
+		if((sub_hist[i]==0 && sub_hist[i-1]>0) ||
+			(i==sub_hist.size()-1 && sub_hist[i]>0)){
+			if((i-start_idx) >max_len ){
+				max_end_idx=i;
+				max_start_idx = start_idx;
+				max_len = i-start_idx;
+			}
+			continue;
+		}
+	}
+	if(max_end_idx<0){
+		throw_msg(m_imgId+" scion stem sub-image histogram is all zero");
+	}
+   
+    ymin = max_start_idx;
+    ymax = max_end_idx-1;   
+	
+	vector<int>ys;   
+    for (size_t i = ymin; i < ymax+1; ++i){       
+	    ys.insert(ys.begin(),sub_hist[i]);        
+	}	
+    int fold_point = -1;
+	vector<int> ys_tmp;
+	for(size_t i=0;i<ys.size();++i){ys_tmp.push_back(ys[i]);}
+	int mid_idx = (int)((float)ys.size() * 0.5);	
+	sort(ys_tmp.begin(), ys_tmp.end());
+	int mid_stem_dia = ys_tmp[mid_idx];
+	int th_stem_dia = (int)((double)mid_stem_dia*0.95 +0.5);
+
+	for(size_t i=0;i<ys.size();++i){
+		if(ys[i]>=th_stem_dia){
+			fold_point = i;
+			break;
+		}
+	}
+	int offset=10;
+	if(m_folder_th<0){
+		double ca = (75.+m_cparam.rs_cut_angle)*0.0174532925199433;
+		m_folder_th = (int)(offset * fabs(tan(ca)) + 0.5);
+	}
+	while(true){
+		int pre_idx = fold_point-offset;
+		if(pre_idx<=0){break;}
+		if(ys[fold_point] - ys[pre_idx] >=m_folder_th){break;}
+		fold_point--;
+	}
+	ymin = ymax-fold_point;
+	
+}
+
+};

+ 66 - 0
cut_point_sc.h

@@ -0,0 +1,66 @@
+//cut point recognization for scion plant
+#pragma once
+
+#include <opencv.hpp>
+#include "data_def_api.h"
+#include "logger.h"
+#include "imstorage_manager.h"
+
+using namespace cv;
+
+namespace graft_cv{
+class CScionCutPoint{
+	public:
+		CScionCutPoint(ConfigParam&, CGcvLogger*pLog=0);
+		~CScionCutPoint();
+		int up_point_detect(
+			ImgInfo*, 
+			Mat&, 
+            PositionInfo& posinfo
+			);
+		void set_image_saver(CImStoreManager** ppis){m_ppImgSaver=ppis;}
+
+private:	
+	Mat m_binImg;// binary image
+	Mat m_grayImg;//gray image
+	string m_imgId;
+	CImStoreManager**m_ppImgSaver;
+	//·µ»ØÍ¼Æ¬£¬ÓÃÓÚµ÷ÊÔ
+	ImgInfo* m_pImginfoBinFork;//fork-y, right-x	
+	int m_folder_th;	
+
+	ConfigParam& m_cparam;//global configure parameters
+	CGcvLogger* m_pLogger;
+	// image segment
+	void img_segment(Mat&);	
+	
+	void get_stem_local_img(
+		const std::vector<int>& hist,		
+		int stem_dia_min,
+		int x0, 
+		int x1,
+		int& stem_y_min,
+		vector<int>& hist_col,
+		vector<int>& hist_row,
+		Mat& scionBinImg
+	);
+		
+	void get_cut_up_point(
+		const std::vector<int>& hist,
+		int stem_fork_y,		
+		double r2_ratio_th, 
+		int average_window, 
+		int r2_window, 
+		int& ymin, 
+		int& ymax
+		);
+	void get_slop_center(
+		int stem_x0,
+		int stem_x1,
+		int slop_cent_y,
+		int& slop_cent_x);
+
+	void clear_imginfo();
+};
+
+};

+ 81 - 0
data_def.h

@@ -0,0 +1,81 @@
+#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 };
+	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;		
+	};
+};

+ 135 - 0
data_def_api.h

@@ -0,0 +1,135 @@
+#pragma once
+#include <string>
+namespace graft_cv{
+typedef unsigned char byte;
+typedef struct 
+{
+    int angle;
+    int width;
+    int height;
+    byte *data;
+}ImgInfo;
+
+typedef struct{
+	// 调试控制 (5)
+	bool image_show;//true--显示处理过程中的中间图片,需要人工回车才能继续执行; false--无图片显示
+	bool image_return;//true--返回结果positoninfo中添加返回的图片; false--无图片返回
+	int image_row_grid;
+	int image_col_grid;
+	bool self_camera;
+
+	// timeout (2)
+	int timeout_proc; //单个接口处理时间,500ms
+	int timeout_append; //100ms
+
+	//image storage parameters(3)
+	bool image_save;//是否保存图像
+	std::string image_depository;//保存图像目录
+	int image_backup_days;//保存图像天数,过期删除
+	
+
+	// optimal angle parameters (15)
+	bool oa_y_flip;
+	int oa_morph_radius;//optimal-angle, open-operation morph-size, = 1;-->COptimalAngle::imgproc(Mat& img)
+	int oa_morph_iteration; //optimal-angle, open-operation times, = 5; -->COptimalAngle::imgproc(Mat& img)
+	int oa_min_hist_value;// optimal-angle, columns-histogram, threshold; -->COptimalAngle::imgproc(Mat& img)
+	double oa_col_th_ratio;//  = 0.7, rootstock binary image column histogram, threshold ratio for max-value, for 
+	                       // detect stem x-range
+	double oa_row_th_ratio; // = 1.2, row histogram of stem x-range subimage, stem diameter ratio for detect 
+	                        // stem fork position
+	int oa_stem_x_padding; // = 20;
+	int oa_stem_dia_min;   //=20,  
+    int oa_stem_fork_y_min;//=10,茎分叉点到茎根最下像素数量
+	double oa_stem_dia_mp;   //=0.8,
+	int oa_morph_radius_base;//基质二值化参数
+	int oa_morph_iteration_base; //基质二值化参数
+	int oa_min_hist_value_base;//基质二值化参数
+	int oa_clip_y_min;//定位夹子上边缘y值
+	int oa_clip_y_max;//定位夹子下边缘y值
+
+	
+	//rootstock cut parameters (20)
+	bool rs_y_flip;
+	int   rs_min_hist_value;
+	double rs_col_th_ratio;//  = 0.7, rootstock binary image column histogram, threshold ratio for max-value, for 
+	                       // detect stem x-range
+	double rs_row_th_ratio; // = 1.2, row histogram of stem x-range subimage, stem diameter ratio for detect 
+	                        // stem fork position
+	int rs_stem_x_padding; // = 20;
+	int rs_stem_dia_min;   //=20,    
+	double rs_stem_dia_mp;   //=0.8,
+    int rs_stem_fork_y_min;//=10,茎分叉点到茎根最下像素数量
+	int rs_stem_edge_detect_window;//=5, 茎单侧边缘点检测,检测时的窗口像素数量
+	                                //和茎粗相关,此数据应小于茎粗(像素数)
+	double rs_cand_corner_box_width_ratio;//=3.0 基于茎fork点,茎右侧边缘点为参考点,检测候选角点区域, 茎粗的倍数
+	double rs_cand_corner_box_xoffset_ratio;//=-10,cand_corner_box x方向偏移量, 茎粗的倍数
+	double rs_opt_corner_xoffset_ratio;//=0.75, 最优切割角点,与参考点间的距离,与茎粗的比率,也就是
+	                            // 最优切割点距离参考点有几倍的茎粗
+	double rs_opt_corner_yoffset_ratio;
+	double rs_corner_mask_rad_ratio;//二值图像角点检测后,度量角点质量用模板半径系数,为茎粗的几倍
+	 
+	int rs_morph_radius;//=1, open-operation morph-size, = 1;-->COptimalAngle::imgproc(Mat& img)
+	int rs_morph_iteration;// = 2
+	int rs_morph_iteration_gray;//=5
+	int rs_max_corner_num;//=100;
+	double rs_corner_qaulity_level;// = 0.1;
+	double rs_corner_min_distance;// = 10;
+	double rs_cut_angle; //=-135 以上切割点为中心,切削茎的方向,(-180.0,180.0)
+	double rs_cut_point_offset_ratio;//向下偏移比例,内接圆半径的倍数0-1.0
+
+	//scion cut parameters (14)
+	bool sc_y_flip;
+	double sc_col_th_ratio;//  = 0.7, scion binary image column histogram, threshold ratio for max-value, for 
+	                      // detect stem x-range
+	double sc_row_th_ratio; // = 0.66, row histogram of stem x-range subimage, stem diameter ratio for detect 
+	                       // stem fork position, x0----x1范围内横向投影,投影值除以(x1-x0)大于此系数即找到边界
+	int sc_stem_x_padding; // = 50; 
+	int sc_stem_dia_min;   //=20,    
+	int sc_clip_padding;//=5 夹子向下间隔,这个间隔向下开始当做茎来处理
+	int sc_stem_ymax_padding;//=50 第一次检测到茎最下端(因夹子影响,茎偏小),加大检测范围
+	int sc_default_cut_length;//如果没有找到fold_point,直接估计出fold_point
+
+	int sc_stem_edge_detect_window;//=5, 茎单侧边缘点检测,检测时的窗口像素数量
+	                               //和茎粗相关,此数据应小于茎粗(像素数)
+	double sc_r2_th; //threshold for r2 ratio
+	int sc_r2_window; //= 5; the radius for calculate r2 index
+	int sc_average_window;// =10; 
+	int sc_morph_radius;//scion, open-operation morph-size, = 1;-->COptimalAngle::imgproc(Mat& img)
+	int sc_morph_iteration; //scion, open-operation times, = 5; -->COptimalAngle::imgproc(Mat& img)
+	 
+	// camera pixel ratio (3)
+	double rs_oa_pixel_ratio; // rootstock optimal-angle camera pixel ratio for millimeter, mm/piexel
+	double rs_cut_pixel_ratio; // rootstock cutting camera  pixel ratio for millimeter, mm/piexel
+	double sc_cut_pixel_ratio; // scion cutting camera  pixel ratio for millimeter, mm/piexel
+	
+} ConfigParam;
+
+typedef struct 
+{
+	//以下涉及到位置均为实际位置,毫米
+	double rs_oa;//砧木最优角度
+	double rs_oa_base;//砧木基质最优角度
+	double rs_oa_stem_y_fork;//茎分叉点y,毫米
+	double rs_oa_clamp_y_end;//茎可视下端y,毫米
+	int rs_oa_width;//每一帧返回叶展宽度
+	int rs_oa_width_base;//每一帧返回基质宽度
+	
+	double rs_cut_upoint_x;//砧木上切割点x位置,毫米
+	double rs_cut_upoint_y;//砧木上切割点y位置,毫米
+	double rs_stem_diameter;//砧木茎粗,毫米	
+	double rs_cut_lpoint_x;//砧木下切割点x位置,毫米
+	double rs_cut_lpoint_y;//砧木下切割点y位置,毫米
+
+	double sc_cut_upoint_x;//穗苗上切割点x位置,毫米
+	double sc_cut_upoint_y;//穗苗上切割点y位置,毫米
+	double sc_cut_cpoint_x;//穗苗切割中点x位置,毫米
+	double sc_cut_cpoint_y;//穗苗切割中点y位置,毫米
+	double sc_cut_lpoint_x;//穗苗切割下点x位置,毫米
+	double sc_cut_lpoint_y;//穗苗切割下点y位置,毫米
+	
+	ImgInfo* pp_images[5];//参考图片,只读,从前向后,没有的会被置零
+	
+}PositionInfo;
+
+
+};

+ 1524 - 0
demo.cpp

@@ -0,0 +1,1524 @@
+// demo.cpp : 定义控制台应用程序的入口点。
+//
+
+#include "stdafx.h"
+#include <iostream>
+#include <opencv2\highgui\highgui.hpp> 
+#include <opencv2\imgproc\imgproc.hpp> 
+#include <opencv2\opencv.hpp>
+#include <map>
+#include <fstream>
+#include <time.h>
+#include <iomanip>
+#include <fstream>
+#include <stdlib.h>
+
+#include "utils.h"
+#include "config.h"
+#include "data_def.h"
+#include "optimal_angle.h"
+#include "cut_point_sc.h"
+#include "cut_point_rs.h"
+#include "graft_cv_api.h"
+#include "logger.h"
+
+using namespace cv;
+using namespace graft_cv;
+
+ofstream logger_ofs;
+CGcvLogger g_logger = CGcvLogger(
+		logger_ofs,
+		CGcvLogger::file_and_terminal,
+		CGcvLogger::info,
+		"D:\\logs\\gcv.log");
+
+void test_init_cp(ConfigParam&cp){
+	cp.image_show=true;//
+	cp.image_return=true;//
+	cp.image_row_grid=20;
+	cp.image_col_grid=100;
+	cp.timeout_append=100;
+	cp.timeout_proc=500;
+	cp.self_camera=true;
+
+	cp.image_save=true;
+	cp.image_depository="D:\\logs\\img_depo";
+	cp.image_backup_days=7;
+
+
+	cp.oa_y_flip=true;
+	cp.oa_morph_radius = 1;
+	cp.oa_morph_iteration=2; 
+	cp.oa_min_hist_value=10;
+	cp.oa_morph_radius_base = 1;
+	cp.oa_morph_iteration_base=2; 
+	cp.oa_min_hist_value_base=10;
+
+	cp.oa_col_th_ratio=0.7;	                       
+	cp.oa_row_th_ratio=1.2; 	                        
+	cp.oa_stem_x_padding=100; 
+	cp.oa_stem_dia_min=10; 
+    cp.oa_stem_fork_y_min=10;
+	cp.oa_stem_dia_mp=0.9; 
+	cp.oa_clip_y_min=245;
+	cp.oa_clip_y_max = 355;
+
+	cp.rs_y_flip=false;
+	cp.rs_col_th_ratio= 0.7; 	                      
+	cp.rs_row_th_ratio= 1.15; 
+	cp.rs_stem_x_padding= 40;
+	cp.rs_stem_dia_min=12; 
+	cp.rs_stem_fork_y_min=10;     
+    cp.rs_stem_dia_mp=0.98;
+	cp.rs_stem_edge_detect_window=5;
+	cp.rs_morph_radius=1;
+	cp.rs_morph_iteration=2;
+	cp.rs_morph_iteration_gray=5;
+	cp.rs_max_corner_num=500;
+	cp.rs_corner_qaulity_level= 0.1;
+	cp.rs_corner_min_distance= 10;
+	cp.rs_cand_corner_box_width_ratio=3.0;
+	cp.rs_cand_corner_box_xoffset_ratio=0.75;
+	cp.rs_opt_corner_xoffset_ratio = 0.2;
+	cp.rs_opt_corner_yoffset_ratio = -0.5;
+	cp.rs_corner_mask_rad_ratio=0.25;
+	cp.rs_cut_angle=-45;
+
+	cp.sc_y_flip=false;
+	cp.sc_col_th_ratio=0.7;
+	cp.sc_row_th_ratio=2.5; //2-3.0
+	cp.sc_stem_x_padding=50; 
+	cp.sc_stem_dia_min=3;   
+   	cp.sc_clip_padding=5;
+	cp.sc_stem_ymax_padding=50;
+	cp.sc_default_cut_length=20;
+
+	cp.sc_stem_edge_detect_window=5;
+	cp.sc_r2_th=1.05;
+	cp.sc_r2_window=10;
+	cp.sc_average_window=10;
+	cp.sc_morph_radius=1;
+	cp.sc_morph_iteration=2;
+
+	cp.rs_oa_pixel_ratio=1.0; 
+	cp.rs_cut_pixel_ratio=1.0; 
+	cp.sc_cut_pixel_ratio=1.0;
+}
+void test_imginfo2mat()
+{
+	int t=0;
+	ImgInfo* ii = new ImgInfo();
+	ii->angle=0;
+	ii->width = ii->height = 13;
+	ii->data = new graft_cv::byte[ii->width*ii->height];
+	for (int i=0;i<ii->height;++i){
+		for (int j=0;j<ii->width;++j){
+			if (i==j || i+j==ii->width-1){
+				ii->data[i*ii->width+j]=255;
+			}
+			else{
+				ii->data[i*ii->width+j]=0;
+			}
+		}
+	}
+	Mat img = imginfo2mat(ii);
+
+	/*namedWindow("3_3", 0);
+	imshow("3_3", img);
+	waitKey(1);
+	destroyAllWindows();*/
+	delete [] ii->data;
+	delete ii;
+
+
+
+};
+void test_camconfig_write()
+{
+	ConfigParam cp0, cp1;
+	memset(&cp1, 0, sizeof(ConfigParam));
+	cp0 = cp1;
+	CGCvConfig cam0 = CGCvConfig();
+	CGCvConfig cam1 = CGCvConfig();
+	cam0.setConfParam(&cp0);
+	cam1.setConfParam(&cp1);
+
+	FileStorage fs("cam_config.yml", FileStorage::WRITE);
+	fs<<"conf_parameters";	
+	fs<<cam0;
+	//fs<<cam1;
+	//fs<<"]";	
+	fs.release();
+	
+
+};
+void test_camconfig_read()
+{
+	ConfigParam cp0, cp1;
+	memset(&cp1, 0, sizeof(ConfigParam));
+	cp0 = cp1;
+	cp0.oa_min_hist_value = 100;
+	cout<<&cp0<<endl;
+
+	CGCvConfig cam0 = CGCvConfig();
+	//CGCvConfig cam1 = CGCvConfig();
+	cam0.setConfParam(&cp0);
+	//cam1.setConfParam(cp1);
+
+	FileStorage fs("cam_config.yml", FileStorage::READ);
+	cam0.read(fs["conf_parameters"]);	
+	
+	//fs<<cam1;
+	//fs<<"]";	
+	fs.release();
+	
+
+};
+
+void test_anglefit_readdata(vector<map<int,int>>& data){
+	string ifile = "E:\\projects\\grafting_robots\\py_code\\test.txt";
+	ifstream ifs(ifile.c_str(), ifstream::in );
+	data.clear();
+	if( ifs.is_open()){
+		string line;
+		map<int,int> tmp;
+		while(getline(ifs,line)){
+			std::cout<<line<<std::endl;
+			size_t found = line.find(",");
+			if(found !=string::npos){
+				//found
+				string sub0 = line.substr(0,found);
+				string sub1 = line.substr(found+1);
+				int an = int(stod(sub0));
+				int wi = stoi(sub1);
+				if(an==0){
+					tmp.clear();
+				}
+				tmp.insert(make_pair<int,int>(an,wi));
+				if(an==180){
+					data.push_back(tmp);
+				}
+
+			}
+		}
+
+		ifs.close();
+	}
+};
+void test_anglefit()
+{
+	ConfigParam cp;
+	COptimalAnglePart opa = COptimalAnglePart(cp);
+	vector<map<int,int>> data;
+	//test_anglefit_readdata(data);
+	map<int,int>tmp;
+	/*tmp.insert(make_pair<int,int>(0,216));
+	tmp.insert(make_pair<int,int>(30,189));
+	tmp.insert(make_pair<int,int>(60,112));
+	tmp.insert(make_pair<int,int>(90,151));
+	tmp.insert(make_pair<int,int>(120,188));*/
+
+	tmp.insert(make_pair<int,int>(0,315));
+	tmp.insert(make_pair<int,int>(30,270));
+	tmp.insert(make_pair<int,int>(60,218));
+	tmp.insert(make_pair<int,int>(90,141));
+	tmp.insert(make_pair<int,int>(120,127));
+
+	data.push_back(tmp);
+	for(vector<map<int,int>>::iterator it = data.begin(); it!=data.end(); ++it){
+
+		map<int,int>& an2width = *it;		
+		double oa = opa.angle_fit(an2width);
+		cout<<oa<<endl;
+	}
+
+};
+void test_optimal_angle(){
+	//string folder = "E:\\projects\\grafting_robots\\py_code\\tmp";
+	string folder = "D:\\grafting_robot\\samples\\tmp";
+	namedWindow("pic", 0);
+	vector<cv::String>filenames;	
+	ConfigParam cp;
+	test_init_cp(cp);	
+	/*cp.oa_min_hist_value=10;
+	cp.oa_morph_iteration=2;
+	cp.oa_morph_radius = 1;*/
+	COptimalAngle opa(cp);
+	
+
+	for(int i = 1;i<=5;++i){
+		for(int j = 0;j<=1;++j){
+			ostringstream ostr;
+			ostr<<i<<j;
+			string subfold = ostr.str();
+			string src_folder = folder+"\\p"+subfold;
+			
+			cv::glob(src_folder, filenames);
+			opa.reset();
+
+			PositionInfo posinfo;
+
+			clock_t t,t0;
+			t = clock();
+			for(size_t idx=0; idx<filenames.size();++idx){
+				//cout<<filenames[idx]<<endl;
+				size_t found0 = filenames[idx].rfind("\\");
+				size_t found1 = filenames[idx].rfind(".");
+				int an = stoi(filenames[idx].substr(found0+1, found1-found0))*20;
+				if(an >=200){
+					an-=200;
+				}
+
+				Mat img = imread(filenames[idx], cv::IMREAD_GRAYSCALE);
+				ImgInfo* imginfo = mat2imginfo(img);
+				imginfo->angle = an;
+				int obj_width=0;
+				try{
+					t0 = clock();
+					obj_width = opa.append(imginfo,posinfo);
+					t0 =  clock() - t0;				
+
+				imginfo_release(&imginfo);
+				}
+				catch(int i)
+				{	
+					cout<<"i= "<<i<<"异常了"<<endl;
+					continue;
+				}
+				catch(string& msg){
+					cout<<"error: "<<msg<<endl;
+					continue;
+				}
+				catch(...)
+				{
+					continue;
+				}
+
+
+
+
+				//imshow("pic", img);
+				//waitKey(1);
+
+				cout<<"angle="<<an<<"  rootstock_width="<<obj_width<<"   time(seconds): "<<((float)t0)/CLOCKS_PER_SEC<<endl;
+
+			}
+			double oa = opa.infer(posinfo);
+			t = clock() - t;
+			cout<<"optimal angle: "<<oa<<endl;
+			cout<<"time(seconds): "<<((float)t)/CLOCKS_PER_SEC<<endl;
+
+			cout<<"\n\n";
+
+
+		}
+	}
+		destroyAllWindows();
+};
+void test_optimal_angle_simulate(){
+
+		
+	//string folder = "E:\\projects\\grafting_robots\\py_code\\tmp";
+	string folder = "D:\\grafting_robot\\samples\\tmp";
+	//string folder = "D:\\grafting_robot\\samples\\rootstock_rotate_part";
+	//string folder = "D:\\private\\grafting_robot\\samples\\rootstock_rotate_part";
+	//string folder = "D:\\private\\grafting_robot\\samples\\20211215\\rotate";
+	namedWindow("pic", CV_WINDOW_NORMAL);
+	vector<cv::String>filenames;	
+	ConfigParam cp;
+	test_init_cp(cp);
+	cp.image_return=true;
+	cp.image_show=true;
+	cp.oa_y_flip=false;
+	cp.oa_clip_y_min=750;
+	
+	COptimalAnglePart opa(cp,&g_logger);
+	
+
+	for(int i = 1;i<=1;++i){
+		//for(int j = 0;j<=1;++j){
+			ostringstream ostr;
+			ostr<<i;
+			string subfold = ostr.str();
+			string src_folder = folder+"\\"+subfold;
+			
+			cv::glob(src_folder, filenames);
+			opa.reset();			
+
+			PositionInfo posinfo;			
+
+			clock_t t,t0;
+			t = clock();
+			for(size_t idx=0; idx<filenames.size();++idx){
+				//cout<<filenames[idx]<<endl;
+				size_t found0 = filenames[idx].rfind("\\");
+				size_t found1 = filenames[idx].rfind(".");
+				int an = stoi(filenames[idx].substr(found0+1, found1-found0));
+				if(an >=200){
+					an-=200;
+				}
+
+				Mat img = imread(filenames[idx], cv::IMREAD_GRAYSCALE);
+				//Rect rect(Point(0,0),Point(img_.cols,cp.oa_clip_y_min));
+				//Mat img = img_(rect);
+
+				ImgInfo* imginfo = mat2imginfo(img);
+				imginfo->angle = an;
+				int obj_width=0;
+				try{
+					memset(&posinfo,0,sizeof(PositionInfo));
+					t0 = clock();
+					obj_width = opa.append(imginfo,posinfo);
+					t0 =  clock() - t0;				
+
+				imginfo_release(&imginfo);
+				}
+				catch(int i)
+				{	
+					cout<<"i= "<<i<<"异常了"<<endl;
+					continue;
+				}
+				catch(string& msg){
+					cout<<"error: "<<msg<<endl;
+					continue;
+				}
+				catch(...)
+				{
+					continue;
+				}
+
+
+				//show return images
+     			for (int i =0;i<5;++i){
+					if (!posinfo.pp_images[i]){continue;}
+					Mat tmp_img = imginfo2mat(posinfo.pp_images[i]);
+					imshow("pic", tmp_img);
+					waitKey(-1);
+				}			
+
+				
+
+				cout<<"angle="<<an<<"  rootstock_width="<<obj_width<<"   time(seconds): "<<((float)t0)/CLOCKS_PER_SEC<<endl;
+
+			}
+			try{
+			double oa = opa.infer(posinfo);
+			t = clock() - t;
+			cout<<"optimal angle: "<<oa<<endl;
+			cout<<"time(seconds): "<<((float)t)/CLOCKS_PER_SEC<<endl;
+
+			cout<<"\n\n";
+			}
+			catch(string& msg){
+					cout<<"error: "<<msg<<endl;
+					continue;
+				}
+			catch(...)
+				{
+					continue;
+				}
+
+
+		//}
+	}
+		destroyAllWindows();
+};
+void test_optimal_angle_part(){
+	/*
+	string data_file = "D:\\private\\grafting_robot\\py_code\\test.txt";
+	ifstream ifs;
+	ifs.open(data_file, fstream::out);
+	std::vector<std::map<int,int>>data;
+	std::vector<string> answer;
+	if(ifs.is_open()){
+		string line;
+		std::map<int,int>tmp;
+		while(!ifs.eof()){
+		getline(ifs, line);
+		//cout<<line<<endl;
+		size_t ptmp = line.find("tmp");
+		if(ptmp!=string::npos){
+			tmp.clear();
+		}
+		else{
+			size_t pos = line.find(",");
+			if(pos==string::npos){
+				data.push_back(tmp);
+				tmp.clear();
+				answer.push_back(line);
+			}
+			else{
+				string ang = line.substr(0,pos);
+				int an = atoi(ang.c_str());
+				string wid = line.substr(pos+1,string::npos);
+				int wi = atoi(wid.c_str());
+				//cout<<ang<<"    "<<wid<<endl;
+				tmp.insert(make_pair<int,int>(an,wi));
+			}
+		}		
+		}
+		ifs.close();
+	}
+	
+   
+	
+	//calculate
+	ConfigParam cp;
+	test_init_cp(cp);
+	cp.image_return=true;
+	cp.image_show=true;
+	
+	COptimalAnglePart opa(cp);
+	for(size_t i=0;i< data.size();++i){
+		cout<<"optimal angle: "<<answer[i]<<endl;
+		for(size_t j=0;j<5;++j){
+			int start_angle = j*20;
+			std::map<int,int> impl_data;
+			int impl_an = start_angle;
+			if (impl_an==80 && data[i][impl_an]==277)
+			{
+				cout<<"debug"<<endl;
+			}
+			while(true){
+				if((impl_an-start_angle)>110){break;}
+				if(impl_an>180){break;}
+				impl_data.insert(make_pair<int,int>(impl_an,data[i][impl_an]));
+				cout<<impl_an<<"\t"<<data[i][impl_an]<<endl;
+				impl_an+=20;
+			}
+			
+			double a = opa.angle_fit(impl_data);
+			cout<<"angle: "<<a<<"\n"<<endl;
+		}
+		
+	}*/
+
+	ConfigParam cp;
+	test_init_cp(cp);
+	cp.image_return=true;
+	cp.image_show=true;
+	
+	COptimalAnglePart* opa=new COptimalAnglePart(cp);
+	 map<int,int> d;
+	d.insert(make_pair<int,int>(0,167));
+	d.insert(make_pair<int,int>(30,126));
+	d.insert(make_pair<int,int>(60,202));
+	d.insert(make_pair<int,int>(90,193));
+	d.insert(make_pair<int,int>(120,121));	
+	double a = opa->angle_fit(d);
+	cout<<"angle: "<<a<<"\n"<<endl;
+};
+void test_sc_cut_point()
+{
+
+	//string src_folder = "E:\\projects\\grafting_robots\\samples\\scion1_part";
+	//string src_folder = "D:\\private\\grafting_robot\\samples\\scion1_part";
+	string src_folder = "D:\\grafting_robot\\samples\\scion1_part";
+	vector<cv::String>filenames;
+	cv::glob(src_folder, filenames);
+	ConfigParam cp;
+	test_init_cp(cp);
+	/*
+	//cp.sc_col_th_ratio=0.7;//  = 0.7, scion binary image column histogram, threshold ratio for max-value, for 
+	//                       // detect stem x-range
+	// cp.sc_row_th_ratio=1.2; // = 1.2, row histogram of stem x-range subimage, stem diameter ratio for detect 
+	//                        // stem fork position
+	// cp.sc_stem_x_padding=50; // = 50; 
+	// cp.sc_stem_dia_min=20;   //=20,
+ //    cp.sc_stem_dia_max=60;   //=60,
+ //    cp.sc_stem_fork_y_min=80;//=80, cut slop length, jump this range for seaching stem fork position
+	// cp.sc_fork_down=10; // stem fork down X pixels, below this line to calculate r2 ratio
+	// cp.sc_r2_th=1.05; //threshold for r2 ratio
+	// cp.sc_r2_window=5; //= 5; the radius for calculate r2 index
+	// cp.sc_average_window=10;// =10; 
+
+	//cp.sc_morph_radius=1;//scion, open-operation morph-size, = 1;-->COptimalAngle::imgproc(Mat& img)
+	// cp.sc_morph_iteration=2; //
+	//
+	*/
+	 PositionInfo posinfo;
+	CScionCutPoint scp(cp); 
+	//namedWindow("pic", WINDOW_NORMAL);
+	ofstream g_logger_ofs;
+	CGcvLogger g_logger = CGcvLogger(
+		g_logger_ofs,
+		CGcvLogger::file_and_terminal,
+		CGcvLogger::warning,
+		"./gcv_debug.log");
+
+	clock_t t;
+	
+	for(size_t idx=0; idx<filenames.size();++idx){	
+		//cout<<idx<<"\t"<<filenames[idx]<<endl;
+		//if(idx<38){continue;}
+		//string fn = "D:\\private\\grafting_robot\\samples\\scion1_part\\IMG_20210830_153852.jpg";
+		//Mat img_src = imread(fn, cv::IMREAD_COLOR);
+
+		Mat img_src = imread(filenames[idx], cv::IMREAD_COLOR);
+		Mat img;
+		resize(img_src,img,Size(img_src.cols/4, img_src.rows/4));
+		//ImgInfo* imginfo = mat2imginfo(img); 
+		int fold_y = 0;
+		try{
+			t = clock();
+			fold_y = scp.up_point_detect(0,img,posinfo);
+			t = clock() - t;	
+		}
+		catch(exception &err){
+			cout<<err.what()<<endl;
+		}
+		catch(const char* msg){
+			cout<<msg<<endl;
+			g_logger.ERRORINFO(msg);
+			g_logger.INFO(filenames[idx]);
+			
+		}
+		catch(string msg){
+			cout<<msg<<endl;
+			g_logger.ERRORINFO(msg);
+			g_logger.INFO(filenames[idx]);
+
+		}
+		catch(...){
+			cout<<"============================================unknown error"<<endl;
+			cout<<filenames[idx]<<endl<<endl;
+		}
+
+		//cv::line(img,Point(0,fold_y), Point(img.cols,fold_y),Scalar(0,0,255));
+
+		//imshow("pic", img);
+		//waitKey(-1);
+			
+		cout<<"time(seconds): "<<((float)t)/CLOCKS_PER_SEC<<endl;
+
+		cout<<"\n";
+
+	}	
+	
+};
+void test_sc_cut_point_simulate()
+{
+
+	//string src_folder = "E:\\projects\\grafting_robots\\samples\\scion1_part";
+	//string src_folder = "D:\\private\\grafting_robot\\samples\\scion1_part";
+	//string src_folder = "D:\\grafting_robot\\samples\\scion1_simulate";
+	string src_folder = "D:\\private\\grafting_robot\\samples\\20211222\\scion";
+	vector<cv::String>filenames;
+	cv::glob(src_folder, filenames);
+	ConfigParam cp;
+	test_init_cp(cp);	
+	cp.image_return=true;
+	cp.image_show=true;
+	cp.rs_y_flip=false;
+	cp.self_camera=false;
+
+	 PositionInfo posinfo;
+	 memset(&posinfo,0,sizeof(PositionInfo));
+	CScionCutPoint scp(cp,&g_logger); 
+	//namedWindow("pic", WINDOW_NORMAL);
+	/*ofstream g_logger_ofs;
+	Logger g_logger = Logger(
+		g_logger_ofs,
+		Logger::file_and_terminal,
+		Logger::warning,
+		"./gcv_debug.log");*/
+
+	clock_t t;
+	
+	for(size_t idx=0; idx<filenames.size();++idx){	
+		//cout<<idx<<"\t"<<filenames[idx]<<endl;
+		//if(idx<38){continue;}
+		//string fn = "D:\\private\\grafting_robot\\samples\\scion1_part\\IMG_20210830_153852.jpg";
+		//Mat img_src = imread(fn, cv::IMREAD_COLOR);
+		cout<<idx<<"\t"<<filenames[idx]<<endl;
+		//if(filenames[idx].find("0-4-504")==string::npos){continue;}
+
+		Mat img = imread(filenames[idx], cv::IMREAD_COLOR);		
+		int fold_y = 0;
+		try{
+			t = clock();
+			fold_y = scp.up_point_detect(0,img,posinfo);
+			t = clock() - t;	
+		}
+		catch(exception &err){
+			cout<<err.what()<<endl;
+		}
+		catch(const char* msg){
+			cout<<msg<<endl;
+			g_logger.ERRORINFO(msg);
+			g_logger.INFO(filenames[idx]);
+			
+		}
+		catch(string msg){
+			cout<<msg<<endl;
+			g_logger.ERRORINFO(msg);
+			g_logger.INFO(filenames[idx]);
+
+		}
+		catch(...){
+			cout<<"============================================unknown error"<<endl;
+			cout<<filenames[idx]<<endl<<endl;
+		}
+
+		//show return images
+		for (int i =0;i<5;++i){
+			if (!posinfo.pp_images[i]){continue;}
+			Mat tmp_img = imginfo2mat(posinfo.pp_images[i]);
+			imshow("pic", tmp_img);
+			waitKey(-1);
+		}			
+
+		//cv::line(img,Point(0,fold_y), Point(img.cols,fold_y),Scalar(0,0,255));
+
+		//imshow("pic", img);
+		//waitKey(-1);
+			
+		cout<<"time(seconds): "<<((float)t)/CLOCKS_PER_SEC<<endl;
+
+		cout<<"\n";
+
+	}	
+	
+};
+void test_rs_cut_point(){
+	ConfigParam cp;
+	test_init_cp(cp);	
+
+	 namedWindow("pic", CV_WINDOW_NORMAL);
+	 CRootStockCutPoint rscp(cp);
+	 ofstream g_logger_ofs;
+	CGcvLogger g_logger = CGcvLogger(
+		g_logger_ofs,
+		CGcvLogger::file_and_terminal,
+		CGcvLogger::warning,
+		"./gcv_debug.log");
+	 clock_t t;
+	for(int i=4;i<193;++i){
+		//if (/*i !=11 &&*/ D:\private\grafting_robot\samples\rs_cut_simulate
+		//	i !=32 && 
+		//	i !=48 && 
+		//	i != 49 && 
+		//	i != 53 && 
+		//	i != 103 && 
+		//	i != 104 && 
+		//	i != 106 && 
+		//	i != 110 && 
+		//	i != 125 && 
+		//	i !=182 && 
+		//	i != 187 && 
+		//	i != 191)
+		//{
+		//	continue;
+		//}
+
+		stringstream buff;
+		buff<<setw(3) << setfill('0') << i;
+		cout<<buff.str()<<endl;
+
+		//string img_file= "E:/projects/grafting_robots/samples/rootstlock_pumpkin/"+buff.str()+"/6.jpg";
+		//string img_file= "D:/private/grafting_robot/samples/rootstlock_pumpkin/"+buff.str()+"/6.jpg";
+		string img_file= "D:/grafting_robot/samples/rootstlock_pumpkin/"+buff.str()+"/6.jpg";
+	
+		
+		Mat img = imread(img_file,CV_LOAD_IMAGE_COLOR  );
+		//ImgInfo* imginfo = mat2imginfo(img);
+		PositionInfo pinfo;
+		try{
+		t = clock();
+		int fold_y = rscp.up_point_detect(0,img, pinfo);
+		t = clock() - t;	
+		}
+		catch(exception &err){
+			cout<<err.what()<<endl;
+		}
+		catch(string msg){
+		 cout<<msg<<endl;
+		 cout<<img_file<<endl<<endl;
+		 g_logger.ERRORINFO(msg);
+		 g_logger.INFO(buff.str());
+		}
+		catch(...){
+			cout<<"============================================unknown error"<<endl;
+			cout<<img_file<<endl<<endl;
+		}
+		//cv::line(img,Point(0,fold_y), Point(img.cols,fold_y),Scalar(0,0,255));
+
+		imshow("pic", img);
+		waitKey(1);
+			
+		cout<<"time(seconds): "<<((float)t)/CLOCKS_PER_SEC<<endl;
+
+		cout<<"\n\n";
+
+	}
+
+
+};
+void test_rs_cut_point_simulate(){
+	ConfigParam cp;
+	test_init_cp(cp);
+	cp.image_show=true;
+	cp.image_return = true;
+
+	 namedWindow("pic", CV_WINDOW_NORMAL);
+	 CRootStockCutPoint rscp(cp,&g_logger);
+	 /*ofstream g_logger_ofs;
+	Logger g_logger = Logger(
+		g_logger_ofs,
+		Logger::file_and_terminal,
+		Logger::warning,
+		"./gcv_debug.log");*/
+
+	//string src_folder = "D:\\private\\grafting_robot\\samples\\rs_cut_simulate";	
+	//string src_folder = "D:\\private\\grafting_robot\\samples\\rootstock_hold_down";
+	//string src_folder = "D:\\grafting_robot\\samples\\rootstock_hold_down";
+	 string src_folder = "D:\\private\\grafting_robot\\samples\\20211215\\rootstock";
+	vector<cv::String>filenames;
+	cv::glob(src_folder, filenames);
+
+	 clock_t t;
+	 for(int i=0;i<filenames.size();++i){
+		
+		string img_file= filenames[i];
+		
+		Mat img = imread(img_file,CV_LOAD_IMAGE_COLOR  );
+		//ImgInfo* imginfo = mat2imginfo(img);
+		PositionInfo pinfo;
+		memset(&pinfo,0,sizeof(PositionInfo));
+		try{
+		t = clock();
+		int fold_y = rscp.up_point_detect(0,img, pinfo);
+		t = clock() - t;	
+		}
+		catch(exception &err){
+			cout<<err.what()<<endl;
+		}
+		catch(string msg){
+		 cout<<msg<<endl;
+		 cout<<img_file<<endl<<endl;
+		 g_logger.ERRORINFO(msg);
+		 g_logger.INFO(img_file);
+		}
+		catch(...){
+			cout<<"============================================unknown error"<<endl;
+			cout<<img_file<<endl<<endl;
+		}
+		//cv::line(img,Point(0,fold_y), Point(img.cols,fold_y),Scalar(0,0,255));
+
+		imshow("pic", img);
+		waitKey(1);
+
+		//show return images
+		for (int i =0;i<5;++i){
+			if (!pinfo.pp_images[i]){continue;}
+			Mat tmp_img = imginfo2mat(pinfo.pp_images[i]);
+			imshow("pic", tmp_img);
+			waitKey(-1);
+		}			
+		cout<<"time(seconds): "<<((float)t)/CLOCKS_PER_SEC<<endl;
+
+		cout<<"\n\n";
+
+	}
+
+
+};
+//void test_rs_cp_reid()
+//{
+//	ConfigParam cp;
+//	test_init_cp(cp);
+//	cp.image_show=false;
+//	cp.image_return = true;
+//	cp.self_camera=false;
+//	cp.oa_y_flip=false;
+//
+//	 namedWindow("pic", CV_WINDOW_NORMAL);
+//	 CRootStockCutPoint rscp(cp,&g_logger);	
+//
+//	//string src_folder = "D:\\private\\grafting_robot\\samples\\rs_cut_simulate";	
+//	//string src_folder = "D:\\private\\grafting_robot\\samples\\rootstock_hold_down";
+//	//string src_folder = "D:\\grafting_robot\\samples\\rootstock_hold_down";
+//	 string src_folder = "D:\\private\\grafting_robot\\samples\\20211215\\reid";
+//	vector<cv::String>filenames;
+//	cv::glob(src_folder, filenames);
+//
+//	 clock_t t;
+//	 for(int i=0;i<filenames.size();++i){
+//		
+//		string img_file= filenames[i];
+//		
+//		Mat img = imread(img_file,CV_LOAD_IMAGE_COLOR);
+//		//img = img(Rect(0,0,img.cols,(int)(img.rows/2)));
+//		imshow("pic", img);
+//		waitKey(-1);
+//
+//		//ImgInfo* imginfo = mat2imginfo(img);
+//		PositionInfo pinfo;
+//		memset(&pinfo,0,sizeof(PositionInfo));
+//		try{
+//		t = clock();
+//		int fold_y = rscp.up_point_reid(0,img,100.0, pinfo);
+//		t = clock() - t;	
+//		}
+//		catch(exception &err){
+//			cout<<err.what()<<endl;
+//		}
+//		catch(string msg){
+//		 cout<<msg<<endl;
+//		 cout<<img_file<<endl<<endl;
+//		 g_logger.ERRORINFO(msg);
+//		 g_logger.INFO(img_file);
+//		}
+//		catch(...){
+//			cout<<"============================================unknown error"<<endl;
+//			cout<<img_file<<endl<<endl;
+//		}
+//		//cv::line(img,Point(0,fold_y), Point(img.cols,fold_y),Scalar(0,0,255));
+//
+//		imshow("pic", img);
+//		waitKey(1);
+//
+//		//show return images
+//		for (int i =0;i<5;++i){
+//			if (!pinfo.pp_images[i]){continue;}
+//			Mat tmp_img = imginfo2mat(pinfo.pp_images[i]);
+//			imshow("pic", tmp_img);
+//			waitKey(-1);
+//		}			
+//		cout<<"time(seconds): "<<((float)t)/CLOCKS_PER_SEC<<endl;
+//
+//		cout<<"\n\n";
+//
+//	}
+//}
+void test_api_scion(){
+	// 0 ConfigParam cp;
+	ConfigParam cp;
+
+	//0 	
+	//char* lpath="D:\\grafting_robot\\cpp_code\\logs\\gcv.log";
+	//char* lpath = "D:\\private\\grafting_robot\\logs\\gcv.log";
+	//cv_set_logpath(lpath);
+	//cv_set_loglevel(0);
+	//cout<<"test"<<endl;
+	
+	// 1 get version
+	//test_init_cp(cp);
+	
+
+	char* ver = new char[10];
+	get_version(ver);
+	cout<<ver<<endl;
+	delete [] ver;
+	ver=0;
+
+	//2 cv_set_param
+	//cp.image_show=false;
+	//cv_set_param(cp);
+	//cv_save_param(0);
+
+	//3 set log
+	//cv_set_logpath("D:\\logs");
+	//cv_set_loglevel(0);
+
+	//3 cv_get_param();
+	//ConfigParam cp_tmp;	
+	//cv_get_param(cp_tmp);
+
+	//4 void cv_get_conf_file
+	//char* conf_file_ret = new char[128];
+	//cv_get_conf_file(conf_file_ret);
+	//cout<<conf_file_ret<<endl;
+	//delete [] conf_file_ret;
+	//conf_file_ret=0;
+
+	//5 cv_init()
+	//char *conf_file = "D:\\private\\grafting_robot\\cpp_code\\demo\\demo\\gcv_conf.yml";
+	//char *conf_file = "D:\\grafting_robot\\cpp_code\\demo\\demo\\gcv_conf.yml";
+	char *conf_file = "E:\\projects\\grafting_robot\\cpp_code\\demo\\demo\\gcv_conf.yml";
+	int rst = cv_init(conf_file);
+	ConfigParam cp_ret;	
+	cv_get_param(cp_ret);
+	cv_set_loglevel(0);
+
+	cv_init_image_saver();
+	//return;
+
+	//
+	//string work_folder = "D:\\private\\grafting_robot\\samples\\20211215\\scion";
+	string work_folder = "E:\\projects\\grafting_robot\\samples\\20220115\\scion";
+	vector<cv::String>filenames;
+	cv::glob(work_folder, filenames);
+	for(size_t i=0;i<filenames.size();++i){		
+		string fname = filenames[i];
+		PositionInfo posinfo;
+
+		Mat img = imread(fname, cv::IMREAD_GRAYSCALE);
+		if(img.empty()){continue;}
+		image_set_top(img,20,8);
+		ImgInfo* imginfo = mat2imginfo(img);
+		int rst = sc_cut_point(imginfo,posinfo);
+		for (int i =0;i<5;++i){
+			if (!posinfo.pp_images[i]){continue;}
+			Mat tmp_img = imginfo2mat(posinfo.pp_images[i]);			
+			imshow("pic", tmp_img);
+			waitKey(-1);						
+		}			
+	}
+
+
+	cv_release();
+}
+void test_sc_batch(){
+
+	// 0 version;
+	char* ver = new char[10];
+	get_version(ver);
+	cout<<ver<<endl;
+	delete [] ver;
+	ver=0;
+	
+	//2 cv_init()
+	//char *conf_file = "D:\\private\\grafting_robot\\cpp_code\\demo\\demo\\gcv_conf.yml";
+	//char *conf_file = "D:\\grafting_robot\\cpp_code\\demo\\demo\\gcv_conf.yml";
+	char *conf_file = "E:\\projects\\grafting_robot\\cpp_code\\demo\\demo\\gcv_conf.yml";
+	int rst = cv_init(conf_file);
+	ConfigParam cp_ret;	
+	cv_get_param(cp_ret);
+	cv_set_loglevel(0);
+	cv_init_image_saver();
+
+	//cp_ret.image_show = true;	
+	//cv_set_param(cp_ret);
+
+	//namedWindow("pic", CV_WINDOW_NORMAL);
+	
+	string work_folder = "E:\\projects\\grafting_robot\\samples\\20220223\\scion";
+	//string work_folder = "D:\\private\\grafting_robot\\samples\\20220220\\scion";
+	vector<cv::String>filenames;
+	cv::glob(work_folder, filenames);			
+	for(size_t idx=0; idx<filenames.size();++idx){		
+		/*stringstream buff;
+		buff<<work_folder<<"\\"<<i;
+		string batch_folder = buff.str();*/
+		PositionInfo posinfo;
+		string filename = filenames[idx];
+		if(filename.find("rst_")!=string::npos){
+			continue;
+		}
+		cout<<idx<<"\t"<<filename<<endl;
+		Mat img = imread(filename, cv::IMREAD_GRAYSCALE);
+		if(img.empty()){continue;}
+		image_set_top(img,20,8);
+		ImgInfo* imginfo = mat2imginfo(img);		
+		try{
+			//if(filename.find("1947_3.jpg")!=string::npos){
+			//	int ooo=0;
+			//	/*cp_ret.image_show=true;
+			//	cv_set_param(cp_ret);*/
+			//}
+			//else{
+			//	continue;
+			//}
+
+			int rst = sc_cut_point(imginfo, posinfo);
+			imginfo_release(&imginfo);
+			if(rst){
+				cout<<"error"<<endl;
+				continue;
+			}
+			for (int i =0;i<5;++i){
+				if (!posinfo.pp_images[i]){continue;}
+				Mat tmp_img = imginfo2mat(posinfo.pp_images[i]);			
+				/*imshow("pic", tmp_img);
+				waitKey(-1);*/
+
+				stringstream bbu;
+				bbu<<filename<<".rst_"<<i<<".jpg";
+				string fnn = bbu.str();
+				cv::imwrite(fnn,tmp_img);
+			}			
+		}
+		catch(...){
+			std::cout<<"some error ..."<<endl;
+		}			
+			
+	}
+	cv_release();
+}
+void test_rs_batch(){
+
+	// 0 version;
+	char* ver = new char[10];
+	get_version(ver);
+	cout<<ver<<endl;
+	delete [] ver;
+	ver=0;
+	
+	//2 cv_init()
+	//char *conf_file = "D:\\private\\grafting_robot\\cpp_code\\demo\\demo\\gcv_conf.yml";
+	//char *conf_file = "D:\\grafting_robot\\cpp_code\\demo\\demo\\gcv_conf.yml";
+	char *conf_file = "E:\\projects\\grafting_robot\\cpp_code\\demo\\demo\\gcv_conf.yml";
+	int rst = cv_init(conf_file);
+	ConfigParam cp_ret;	
+	cv_get_param(cp_ret);
+	cv_set_loglevel(0);
+	cv_init_image_saver();
+
+	//cp_ret.image_show = true;	
+	//cv_set_param(cp_ret);
+	//namedWindow("pic", CV_WINDOW_NORMAL);
+	
+	string work_folder = "E:\\projects\\grafting_robot\\samples\\20220620\\rootstock";
+	//string work_folder = "D:\\private\\grafting_robot\\samples\\20220220\\rootstock";
+	vector<cv::String>filenames;
+	cv::glob(work_folder, filenames);	
+	int cnter=0;
+	for(size_t idx=0; idx<filenames.size();++idx){	
+		string rs_filename = filenames[idx];
+		if(rs_filename.find("rst_")!=string::npos){
+			continue;
+		}
+		//if(rs_filename.find("\\379.bmp")==string::npos){continue;}
+		cout<<idx<<"\t"<<rs_filename<<endl;
+
+
+		PositionInfo posinfo;
+		Mat img = imread(rs_filename,cv::IMREAD_GRAYSCALE);
+		image_set_top(img,20,8);
+		ImgInfo* rs_imginfo = mat2imginfo(img);			
+
+		memset(&posinfo,0,sizeof(PositionInfo));
+		try{
+			/*if(cnter==4){
+				int test_tmp=0;
+			}*/
+			int fold_y = rs_cut_point(rs_imginfo, posinfo);		
+			imginfo_release(&rs_imginfo);
+
+			for (int i =0;i<5;++i){
+				if (!posinfo.pp_images[i]){continue;}
+				Mat tmp_img = imginfo2mat(posinfo.pp_images[i]);			
+				/*imshow("pic", tmp_img);
+				waitKey(-1);*/
+
+				stringstream bbu;
+				bbu<<rs_filename<<".rst_"<<i<<".jpg";
+				string fnn = bbu.str();
+				cv::imwrite(fnn,tmp_img);
+			}			
+
+		}
+		catch(exception &err){
+			cout<<err.what()<<endl;
+		}
+		catch(string msg){
+		 cout<<msg<<endl;
+		 cout<<rs_filename<<endl<<endl;
+		 //g_logger.ERRORINFO(msg);
+		 //g_logger.INFO(img_file);
+		}
+		catch(...){
+			cout<<"============================================unknown error"<<endl;
+			cout<<rs_filename<<endl<<endl;
+		}	
+		cnter+=1;
+	}
+	cv_release();
+}
+void test_rs_batch_camera(){
+
+	// 0 version;
+	char* ver = new char[10];
+	get_version(ver);
+	cout<<ver<<endl;
+	delete [] ver;
+	ver=0;
+	
+	//2 cv_init()
+	//char *conf_file = "D:\\private\\grafting_robot\\cpp_code\\demo\\demo\\gcv_conf.yml";
+	//char *conf_file = "D:\\grafting_robot\\cpp_code\\demo\\demo\\gcv_conf.yml";
+	char *conf_file = "E:\\projects\\grafting_robot\\cpp_code\\demo\\demo\\gcv_conf.yml";
+	int rst = cv_init(conf_file);
+	ConfigParam cp_ret;	
+	cv_get_param(cp_ret);
+	cv_set_loglevel(0);
+	cv_init_image_saver();
+	//cp_ret.image_show = true;
+	//cv_set_param(cp_ret);
+	//namedWindow("pic", CV_WINDOW_NORMAL);
+	
+	string work_folder = "E:\\projects\\grafting_robot\\samples\\20220119\\rootstock_cut_compare";
+	//string work_folder = "D:\\private\\grafting_robot\\samples\\20220104\\batch";
+	vector<string> sub_path;
+	sub_path.push_back("20220119133425");
+	sub_path.push_back("20220119134921");
+	sub_path.push_back("20220119135818");
+	sub_path.push_back("20220119140720");
+	sub_path.push_back("20220119141511");
+	sub_path.push_back("20220119142616");
+	sub_path.push_back("20220119143048");
+
+	//vector<cv::String>filenames;
+	//cv::glob(work_folder, filenames);	
+	vector<string> fileidx;
+	fileidx.push_back("3.bmp");
+	fileidx.push_back("11.bmp");
+	fileidx.push_back("19.bmp");
+	fileidx.push_back("27.bmp");
+	fileidx.push_back("35.bmp");
+	fileidx.push_back("43.bmp");
+	fileidx.push_back("51.bmp");
+	fileidx.push_back("59.bmp");
+	fileidx.push_back("67.bmp");
+	fileidx.push_back("75.bmp");
+	fileidx.push_back("83.bmp");
+	fileidx.push_back("91.bmp");
+	fileidx.push_back("99.bmp");
+	fileidx.push_back("107.bmp");
+	fileidx.push_back("115.bmp");
+	fileidx.push_back("123.bmp");
+
+	
+
+	for(size_t idx=0; idx<sub_path.size();++idx){
+		string subp = sub_path[idx];
+		for(size_t i=0;i<fileidx.size();++i){
+			string rs_filename = work_folder+"\\"+subp+ "\\"+fileidx[i];
+
+			PositionInfo posinfo;
+			Mat img = imread(rs_filename,cv::IMREAD_GRAYSCALE);
+			if(img.empty()){continue;}
+
+			ImgInfo* rs_imginfo = mat2imginfo(img);	
+			memset(&posinfo,0,sizeof(PositionInfo));
+			try{		
+				int fold_y = rs_cut_point(rs_imginfo, posinfo);		
+				imginfo_release(&rs_imginfo);
+
+				for (int i =0;i<5;++i){
+					if (!posinfo.pp_images[i]){continue;}
+					Mat tmp_img = imginfo2mat(posinfo.pp_images[i]);			
+					/*imshow("pic", tmp_img);
+					waitKey(-1);*/
+
+					stringstream bbu;
+					bbu<<rs_filename<<".rst_"<<i<<".jpg";
+					string fnn = bbu.str();
+					cv::imwrite(fnn,tmp_img);
+				}			
+
+			}
+			catch(exception &err){
+				cout<<err.what()<<endl;
+			}
+			catch(string msg){
+			 cout<<msg<<endl;
+			 cout<<rs_filename<<endl<<endl;
+			 //g_logger.ERRORINFO(msg);
+			 //g_logger.INFO(img_file);
+			}
+			catch(...){
+				cout<<"============================================unknown error"<<endl;
+				cout<<rs_filename<<endl<<endl;
+			}		
+		}
+	}
+	cv_release();
+}
+void test_oa_batch(){
+
+	// 0 version;
+	char* ver = new char[10];
+	get_version(ver);
+	cout<<ver<<endl;
+	delete [] ver;
+	ver=0;
+	
+	//2 cv_init()
+	//char *conf_file = "D:\\private\\grafting_robot\\cpp_code\\demo\\demo\\gcv_conf.yml";
+	//char *conf_file = "D:\\grafting_robot\\cpp_code\\demo\\demo\\gcv_conf.yml";
+	char *conf_file = "E:\\projects\\grafting_robot\\cpp_code\\demo\\demo\\gcv_conf.yml";
+	int rst = cv_init(conf_file);
+	ConfigParam cp_ret;	
+	cv_get_param(cp_ret);	
+	cv_set_loglevel(0);
+	cv_init_image_saver();
+
+	//cp_ret.image_show=true;
+	//cv_set_param(cp_ret);
+	//namedWindow("pic", CV_WINDOW_NORMAL);
+	
+	string work_folder = "E:\\projects\\grafting_robot\\samples\\20220223\\rotate";
+	//string work_folder = "D:\\private\\grafting_robot\\samples\\20220220\\rotate";
+	vector<cv::String>filenames;
+	cv::glob(work_folder, filenames);			
+	for(size_t idx=0; idx<filenames.size();++idx){	
+			string filename = filenames[idx];
+			if(filename.find(".rst_")!=string::npos){continue;}
+			//if(filename.find("\\249.bmp")==string::npos){continue;}
+			PositionInfo posinfo;
+			Mat img = imread(filename, cv::IMREAD_GRAYSCALE);
+			if(img.empty()){continue;}
+			image_set_top(img,20,8);
+			ImgInfo* imginfo = mat2imginfo(img);
+			//imginfo->angle = j*30;
+			try{
+				rs_oa_init();
+				rs_oa_append(imginfo, posinfo);
+				
+				for (int i =0;i<5;++i){
+					if (!posinfo.pp_images[i]){continue;}
+					Mat tmp_img = imginfo2mat(posinfo.pp_images[i]);			
+					/*imshow("pic", tmp_img);
+					waitKey(-1);*/
+
+					stringstream bbu;
+					bbu<<filename<<".rst_"<<i<<".jpg";
+					string fnn = bbu.str();
+					cv::imwrite(fnn,tmp_img);
+				}			
+			}
+			catch(...){
+				std::cout<<"some error ..."<<endl;
+			}			
+			
+			imginfo_release(&imginfo);
+			
+
+		
+	}
+	cv_release();
+}
+void test_api_batch(){
+
+	// 0 version;
+	char* ver = new char[10];
+	get_version(ver);
+	cout<<ver<<endl;
+	delete [] ver;
+	ver=0;
+	
+	//2 cv_init()
+	//char *conf_file = "D:\\private\\grafting_robot\\cpp_code\\demo\\demo\\gcv_conf.yml";
+	//char *conf_file = "D:\\grafting_robot\\cpp_code\\demo\\demo\\gcv_conf.yml";
+	char *conf_file = "E:\\projects\\grafting_robot\\cpp_code\\demo\\demo\\gcv_conf.yml";
+	int rst = cv_init(conf_file);
+	ConfigParam cp_ret;	
+	cv_get_param(cp_ret);
+	cv_set_loglevel(0);
+	cv_init_image_saver();
+	namedWindow("pic", CV_WINDOW_NORMAL);
+	
+	string work_folder = "E:\\projects\\grafting_robot\\samples\\20220105\\batch";
+	//string work_folder = "D:\\private\\grafting_robot\\samples\\20211215\\scion";
+	//string work_folder = "D:\\grafting_robot\\samples\\20220102\\batch";
+	for(size_t i=1;i<19;++i){
+		stringstream buff;
+		buff<<work_folder<<"\\"<<i;
+		//buff<<work_folder<<"\\"<<"0-4-489.bmp";
+		string batch_folder = buff.str();
+		PositionInfo posinfo;
+
+		///////////////////////////////////////////////////////
+		// 1 optimal angle
+		/*rs_oa_init();		
+		for(size_t j=0;j<5;++j){
+			stringstream bf;
+			bf<<batch_folder<<"\\"<<2*j+1<<".bmp";
+			string filename = bf.str();
+			Mat img = imread(filename, cv::IMREAD_GRAYSCALE);
+			if(img.empty()){continue;}
+			image_set_top(img,20,8);
+			ImgInfo* imginfo = mat2imginfo(img);
+			imginfo->angle = j*30;
+			try{
+				rs_oa_append(imginfo, posinfo);
+			}
+			catch(...){
+				std::cout<<"some error ..."<<endl;
+			}			
+			std::cout<<"angle="<<imginfo->angle<<"  rootstock_width="<<posinfo.rs_oa_width<<endl;
+			imginfo_release(&imginfo);
+			
+			
+			for (int i =0;i<5;++i){
+				if (!posinfo.pp_images[i]){continue;}
+				Mat tmp_img = imginfo2mat(posinfo.pp_images[i]);
+				imshow("pic", tmp_img);
+				waitKey(-1);
+			}			
+		}
+		rs_oa_get_result(posinfo);
+		cout<<"optimal angle="<<posinfo.rs_oa<<endl;
+		double opt_angle = posinfo.rs_oa;*/
+
+		//////////////////////////////////////////////////////////////////
+		// 2 rootstock cut point
+		//if(!(i==4 || i==6 ||i==8 || i==12 || i==15) ){continue;}
+		//if(i!=12){continue;}
+		
+		string rs_filename = batch_folder+"\\11.bmp";
+		/*if (i==15){
+			rs_filename = batch_folder+"\\11.jpg";
+		}*/
+		Mat img = imread(rs_filename,cv::IMREAD_GRAYSCALE);
+		image_set_top(img,20,8);
+		ImgInfo* rs_imginfo = mat2imginfo(img);		
+		clock_t t;
+
+		memset(&posinfo,0,sizeof(PositionInfo));
+		try{
+		t = clock();
+		int fold_y = rs_cut_point(rs_imginfo, posinfo);
+		t = clock() - t;
+		imginfo_release(&rs_imginfo);
+
+		}
+		catch(exception &err){
+			cout<<err.what()<<endl;
+		}
+		catch(string msg){
+		 cout<<msg<<endl;
+		 cout<<rs_filename<<endl<<endl;
+		 //g_logger.ERRORINFO(msg);
+		 //g_logger.INFO(img_file);
+		}
+		catch(...){
+			cout<<"============================================unknown error"<<endl;
+			cout<<rs_filename<<endl<<endl;
+		}
+		//show return images
+		/*stringstream tu;
+		tu<<batch_folder<<"\\rst_"<<int(opt_angle)<<"_"<<11<<".jpg";
+		string tuu = tu.str();
+		cv::imwrite(tuu,img);*/
+
+		/*for (int i =0;i<5;++i){
+			if (!posinfo.pp_images[i]){continue;}
+			Mat tmp_img = imginfo2mat(posinfo.pp_images[i]);			
+			imshow("pic", tmp_img);
+			waitKey(-1);
+
+			stringstream bbu;
+			bbu<<batch_folder<<"\\rst_"<<i<<".jpg";
+			string fnn = bbu.str();
+			cv::imwrite(fnn,tmp_img);
+		}			*/
+		cout<<"time(seconds): "<<((float)t)/CLOCKS_PER_SEC<<endl;
+
+		cout<<"\n\n";
+
+		
+		
+	}
+	cv_release();
+}
+void drawline_rs()
+{
+	string rs_filename = "D:\\grafting_robot\\samples\\20220108\\rootstock\\27.jpg";
+	Mat img = imread(rs_filename,cv::IMREAD_GRAYSCALE);
+	double angle = -40.0 * 3.1415926 /180.0;
+	//angle =tan(angle );
+
+
+	int x0=259;
+	int y0 = 461;
+	int b = x0-y0;
+	Point p0=Point(x0,y0);
+	int x1 = img.cols-1;
+	double dx=x1-x0;
+	double dy = tan(angle) * dx;
+	if(dy<0){dy = -dy;}
+	int y1 = y0+(int)(dy+0.5);
+	Point p1 = Point(x1,y1);
+	int x2=0;
+	int y2 = x0*tan(angle )+y0;
+	Point p2 = Point(x2,y2);
+
+	line(img,p0,p1,Scalar(255,255,255));
+	line(img,p2,p1,Scalar(255,255,255));
+	string fnn = "./tmp27.bmp";
+	cv::imwrite(fnn,img);
+			
+
+}
+void drawline_sc()
+{
+	string rs_filename = "D:\\grafting_robot\\samples\\20220108\\rootstock\\26.jpg";
+	Mat img = imread(rs_filename,cv::IMREAD_GRAYSCALE);
+	double angle = -40.0 * 3.1415926 /180.0;
+	//angle =tan(angle );
+
+
+	int x0=395;
+	int y0 = 340;
+	int b = x0-y0;
+	Point p0=Point(x0,y0);
+	int x1 = img.cols-1;
+	double dx=x1-x0;
+	double dy = tan(angle) * dx;
+	if(dy<0){dy = -dy;}
+	int y1 = y0+(int)(dy+0.5);
+	Point p1 = Point(x1,y1);
+	int x2=0;
+	int y2 = x0*tan(angle )+y0;
+	Point p2 = Point(x2,y2);
+
+	line(img,p0,p1,Scalar(255,255,255));
+	line(img,p2,p1,Scalar(255,255,255));
+	string fnn = "./tmp26.bmp";
+	cv::imwrite(fnn,img);
+			
+
+}
+void drawline_dist()
+{
+	double x0=260.0;
+	double y0=292.0;
+
+	double angle = -45.0 * 3.1415926 /180.0;
+	double k=tan(angle );
+	double b = -k*260.0-435.0;
+	double yy = -(k*x0 +b);
+	double dy = yy-y0;
+
+	double r = 5.8479999999999997e-002;
+	double dyy = dy *r;
+	cout<<dy<<"\t"<<dyy;
+
+
+}
+
+int _tmain(int argc, _TCHAR* argv[])
+{		
+	//test_imginfo2mat();
+	//test_camconfig_write();
+	//test_camconfig_read();
+	//test_anglefit();
+	//test_optimal_angle();
+	//test_optimal_angle_simulate();
+	//test_optimal_angle_part();
+	//test_sc_cut_point();
+	//test_sc_cut_point_simulate();	
+	//test_rs_cut_point();
+	//test_rs_cut_point_simulate();
+	//test_rs_cp_reid();
+
+	/////////////////////////////
+	//api test
+	//test_api_scion();	
+	//test_sc_batch();
+	test_rs_batch();
+	//test_rs_batch_camera();
+	//test_oa_batch();
+	//test_api_batch();
+	//drawline_rs();
+	//drawline_sc();
+	//drawline_dist();
+
+
+	//system("pause");
+	return 0;
+}
+

+ 131 - 0
fork_rs.cpp

@@ -0,0 +1,131 @@
+#include "fork_rs.h"
+
+namespace graft_cv{
+
+	CForkDetectOptimal::CForkDetectOptimal()
+		:m_roi_width(0),m_roi_height(0)
+	{
+	}
+	CForkDetectOptimal::CForkDetectOptimal(int w,int h)
+		:m_roi_width(w),m_roi_height(h)
+	{
+	}
+	CForkDetectOptimal::~CForkDetectOptimal(){}
+	void CForkDetectOptimal::set_roi_size(int w,int h){
+		m_roi_width = w;
+		m_roi_height = h;
+	}
+	void CForkDetectOptimal::get_roi_size(int &w,int &h){
+		w = m_roi_width;
+		h = m_roi_height;
+	}
+
+	double CForkDetectOptimal::center_pt_index(
+		const Mat& binimg, 
+		Point&cent, 
+		double cut_angle //刀具切割角度
+		)
+	{
+		double ratio = 0.0;
+		 RotatedRect centers_rect = RotatedRect(Point2f(0,0), Size2f(m_roi_width,m_roi_height), -cut_angle);//换算成顺时针角度
+		 Point2f vertices[4];
+		 centers_rect.points(vertices);
+
+		 Point2f left_cent  = Point2f(cent.x + vertices[0].x, cent.y + vertices[0].y);
+		 Point2f right_cent = Point2f(cent.x + vertices[1].x, cent.y + vertices[1].y);
+		 RotatedRect left_rect = RotatedRect(left_cent,   Size2f(m_roi_width,m_roi_height), -cut_angle);
+		 RotatedRect right_rect = RotatedRect(right_cent, Size2f(m_roi_width,m_roi_height), -cut_angle);
+
+		 double left_cnt = roi_object_size(binimg, left_rect);
+		 double right_cnt = roi_object_size(binimg, right_rect);
+		 ratio = left_cnt* left_cnt/right_cnt/(m_roi_width*m_roi_height);
+
+		 //imshow
+		 Mat tmp_img = binimg.clone();
+		 Point2f vertices_left[4];
+		 Point2f vertices_right[4];
+		 left_rect.points(vertices_left);
+		 right_rect.points(vertices_right);
+		 for (int i = 0; i < 4; i++){
+			line(tmp_img, vertices_left[i], vertices_left[(i+1)%4], Scalar(128,255,0), 1);
+			line(tmp_img, vertices_right[i], vertices_right[(i+1)%4], Scalar(200,255,0), 1);
+		 }
+		 circle(tmp_img,cent,5,Scalar(128,0,0));
+
+		 return ratio;
+	}
+
+	double CForkDetectOptimal::roi_object_size(
+		const Mat& binimg, 
+		RotatedRect &rrect)
+	{
+		double cnt = 1.0;
+		Rect brect = rrect.boundingRect();	
+		Point2f vertices[4];
+		rrect.points(vertices);
+
+		//Mat tmp_img = Mat::zeros(binimg.rows,binimg.cols,CV_8UC1);
+		for(int r = brect.y; r<brect.y+brect.height;++r){
+			for(int c=brect.x; c<brect.x+brect.width;++c){
+				Point2f ptf = Point2f(c,r);
+				bool isin = is_in_rrect(vertices,ptf, rrect.angle);
+				if(isin){
+					//tmp_img.at<unsigned char>(r,c) = 255;
+					if(r<0||r>binimg.rows-1){continue;}
+					if(c<0||c>binimg.cols-1){continue;}
+					if(binimg.at<unsigned char>(r,c) >0){cnt+=1.0;}
+				}
+			}
+		}
+		return cnt;		 
+	}
+	bool CForkDetectOptimal::is_in_rrect(
+		Point2f* ptf4,
+		Point2f ptf, 
+		float fAngle)
+	{
+		Point2f ptf4Vector[4];
+		int nQuadrant[4] = {0};
+		fAngle *= CV_PI/180 *(-1);
+
+		for(int idx=0;idx<4;idx++){
+			float fDifx = float(ptf.x - ptf4[idx].x);
+			float fDify = float(ptf.y - ptf4[idx].y);
+			int nDifx = fDifx * cos(fAngle) - fDify * sin(fAngle);
+			int nDify = fDifx * sin(fAngle) + fDify * cos(fAngle);
+
+			if(nDifx >=0 && nDify >=0) nQuadrant[0]++;
+			if(nDifx < 0 && nDify >=0) nQuadrant[1]++;
+			if(nDifx < 0 && nDify < 0) nQuadrant[2]++;
+			if(nDifx > 0 && nDify < 0) nQuadrant[3]++;
+
+		}
+		int firstIdx = -1;
+		int secIdx = -1;
+		int countNum = 0;
+		for(int idx=0;idx<4;idx++){
+			if(nQuadrant[idx] !=0){
+				if(firstIdx == -1){
+					firstIdx = idx;
+				}
+				else{
+					if(secIdx == -1 && firstIdx != -1){
+						secIdx = idx;
+					}
+				}
+				countNum++;
+			}
+		}
+		if(countNum <= 2){
+			if(abs(firstIdx - secIdx)==1 || abs(firstIdx - secIdx)==3 ||
+				(countNum==1 && (firstIdx==-1 || secIdx==-1)))
+				{
+					return false;
+			}
+		}
+		return true;
+	}
+
+
+
+};

+ 31 - 0
fork_rs.h

@@ -0,0 +1,31 @@
+#pragma once
+
+#include <opencv2\imgproc\imgproc.hpp>
+
+using namespace cv;
+
+namespace graft_cv{
+	// CForkDetectOptimal类实现优化
+	// 用于优化分叉点检测
+	// 要求:1)分叉点位于茎中心
+	//       2)通过分叉点的线能够实现有效切割
+	// 方法:通过茎中心线上的点,切割角度,找到不伤叶子的最优位置进行切割
+	//       采用虚拟切割线两侧区域前景对比的方法
+	class CForkDetectOptimal
+	{
+	public:
+		CForkDetectOptimal();
+		CForkDetectOptimal(int w, int h);
+		~CForkDetectOptimal();
+
+		double center_pt_index(const Mat& binimg, Point&cent, double cut_angle);
+		void set_roi_size(int w, int h);
+		void get_roi_size(int &w, int &h);
+	private:
+		int m_roi_width;
+		int m_roi_height;
+
+		double roi_object_size(const Mat& binimg, RotatedRect &rrect);
+		bool is_in_rrect(Point2f* ptf4,Point2f ptf, float fAngle);
+	};
+};

+ 66 - 0
gcv_conf.yml

@@ -0,0 +1,66 @@
+%YAML:1.0
+conf_parameters:
+   image_show: 0
+   image_return: 1
+   image_row_grid: 20
+   image_col_grid: 100
+   self_camera: 0
+   timeout_proc: 100000
+   timeout_append: 150
+   image_save: 1
+   image_depository: "D:\\logs\\algo_img"
+   image_backup_days: 7
+   oa_y_flip: 0
+   oa_morph_radius: 1
+   oa_morph_iteration: 2
+   oa_min_hist_value: 5
+   oa_morph_radius_base: 1
+   oa_morph_iteration_base: 2
+   oa_min_hist_value_base: 10
+   oa_col_th_ratio: 6.9999999999999996e-001
+   oa_row_th_ratio: 1.2000000000000000e+000
+   oa_stem_x_padding: 40
+   oa_stem_dia_min: 10
+   oa_stem_fork_y_min: 10
+   oa_stem_dia_mp: 9.0000000000000002e-001
+   oa_clip_y_min: 245
+   oa_clip_y_max: 385
+   rs_y_flip: 0
+   rs_col_th_ratio: 6.5e-001
+   rs_row_th_ratio: 1.1999999999999999e+000
+   rs_min_hist_value: 5
+   rs_stem_x_padding: 50
+   rs_stem_dia_min: 12
+   rs_stem_dia_mp: 9.6e-001
+   rs_stem_fork_y_min: 40
+   rs_stem_edge_detect_window: 5
+   rs_cand_corner_box_width_ratio: 3.
+   rs_cand_corner_box_xoffset_ratio: 7.5000000000000000e-001
+   rs_opt_corner_xoffset_ratio: 2.0000000000000001e-001
+   rs_opt_corner_yoffset_ratio: -5.0000000000000000e-001
+   rs_corner_mask_rad_ratio: 2.5000000000000000e-001
+   rs_morph_radius: 2
+   rs_morph_iteration: 5
+   rs_morph_iteration_gray: 5
+   rs_max_corner_num: 500
+   rs_corner_qaulity_level: 1.0000000000000001e-001
+   rs_corner_min_distance: 10.
+   rs_cut_angle: -60.
+   rs_cut_point_offset_ratio: 9.0000000000000000e-001
+   sc_y_flip: 0
+   sc_col_th_ratio: 6.9999999999999996e-001
+   sc_row_th_ratio: 2.000000000000000e+000
+   sc_stem_x_padding: 50
+   sc_stem_dia_min: 3
+   sc_clip_padding: 5
+   sc_stem_ymax_padding: 200
+   sc_default_cut_length: 20
+   sc_stem_edge_detect_window: 5
+   sc_r2_th: 1.0500000000000000e+000
+   sc_r2_window: 30
+   sc_average_window: 1
+   sc_morph_radius: 1
+   sc_morph_iteration: 2
+   rs_oa_pixel_ratio: 2.3333300000000001e-001
+   rs_cut_pixel_ratio: 5.8479999999999997e-002
+   sc_cut_pixel_ratio: 8.7499999999999994e-002

+ 370 - 0
graft_cv_api.cpp

@@ -0,0 +1,370 @@
+#include <string.h>
+#include <fstream>
+
+#include "graft_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"
+
+extern CRITICAL_SECTION g_cs;
+namespace graft_cv
+{
+	char *g_version_str = "0.5.9.20";
+
+	//configure
+	string g_conf_file = "./gcv_conf.yml";	
+	ConfigParam g_cp;
+
+	//logger
+	ofstream g_logger_ofs;
+	CGcvLogger g_logger = CGcvLogger(
+		g_logger_ofs,
+		CGcvLogger::file_and_terminal,
+		CGcvLogger::debug,
+		"./gcv.log");	
+
+	//image saver
+	
+	CImStoreManager* g_pImStore=0;
+
+	//implement
+	COptimalAnglePart g_oa = COptimalAnglePart(g_cp,&g_logger);
+	CRootStockCutPoint g_rs_cp = CRootStockCutPoint(g_cp,&g_logger);
+	CScionCutPoint g_sc_cp = CScionCutPoint(g_cp,&g_logger);
+	
+
+	//0 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;
+		}
+	}
+	// 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;
+		}
+	}
+
+	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_oa.set_image_saver(&g_pImStore);
+		g_rs_cp.set_image_saver(&g_pImStore);
+		g_sc_cp.set_image_saver(&g_pImStore);
+		return 0;
+	}
+
+	GCV_API int cv_release()
+	{
+		if(g_pImStore){
+			delete g_pImStore;
+			g_pImStore = 0;
+		}
+		//DeleteCriticalSection(&g_cs);
+		return 0;
+	}
+
+	//1
+	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));
+
+			FileStorage fs(conf, 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
+			FileStorage fs(g_conf_file, 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;
+	};
+	//2
+	void cv_set_param(ConfigParam&cp)
+	{
+		g_cp = cp;	
+		string pinfo = get_cparam_info(&g_cp);
+		g_logger.INFO(string("set parameters:\n")+pinfo);
+	};
+	//3
+	void cv_save_param(char* conf_file/*=0*/)
+	{
+		//save configures
+		CGCvConfig conf_contrainer = CGCvConfig();
+		conf_contrainer.setConfParam(&g_cp);
+		if(conf_file){
+			FileStorage fs(
+				conf_file,
+				FileStorage::WRITE
+				);
+			fs<<"conf_parameters";	
+			fs<<conf_contrainer;		
+			fs.release();	
+		}
+		else{
+			FileStorage fs(
+				g_conf_file, 
+				FileStorage::WRITE
+				);
+			fs<<"conf_parameters";	
+			fs<<conf_contrainer;		
+			fs.release();	
+		}
+	}
+	//4
+	void cv_get_param(ConfigParam&cp)
+	{
+		cp = g_cp;		
+	};
+	//5
+	void get_version(char* buf)
+	{			
+		strcpy(buf, g_version_str);
+	};
+	
+	//6
+	void cv_get_conf_file(char*buff)
+	{
+		strcpy(buff, g_conf_file.c_str());
+	};
+
+	//7
+	int rs_oa_init()
+	{
+		return g_oa.reset();		
+	};
+	//8
+	int rs_oa_append(
+		ImgInfo* imginfo,
+		PositionInfo& posinfo
+		)
+	{
+		memset(&posinfo,0,sizeof(PositionInfo));
+		try{
+			g_oa.append(imginfo, posinfo);			
+			
+		}
+		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;
+	};
+
+	//9
+	int rs_oa_get_result(PositionInfo& posinfo)
+	{
+		memset(&posinfo,0,sizeof(PositionInfo));
+		try{
+			double oa = g_oa.infer(posinfo);
+			
+		}
+		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;
+		
+	}
+
+	//10
+	int rs_cut_point(
+		ImgInfo* imginfo, 
+		PositionInfo& posinfo
+		)		
+	{
+		memset(&posinfo,0,sizeof(PositionInfo));
+		try{
+			g_rs_cp.up_point_detect(
+				imginfo,
+				Mat(),
+				posinfo
+				);
+		}
+		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;
+		
+	}
+
+	//11
+	/*int rs_cut_point_reid(ImgInfo*imginfo , double edge_length, PositionInfo& posinfo)
+	{
+		memset(&posinfo,0,sizeof(PositionInfo));
+		try{
+			g_rs_cp.up_point_reid(
+				imginfo,
+				Mat(),
+				edge_length,
+				posinfo
+				);
+		}
+		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 sc_cut_point(
+		ImgInfo* imginfo, 
+		PositionInfo& posinfo
+		)
+	{
+		memset(&posinfo,0,sizeof(PositionInfo));
+		try{
+			g_sc_cp.up_point_detect(
+				imginfo,
+				Mat(),
+				posinfo
+				);
+		}
+		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;
+	}
+
+};

+ 127 - 0
graft_cv_api.h

@@ -0,0 +1,127 @@
+// Grafting is the process of joining two plants together 
+//(an upper portion and a lower portion) to grow as one. 
+//The upper portion of the plant is known as the scion, 
+//which is attached to the lower portion known as the rootstock.
+
+//rs -- rootstock, sc -- scion
+#pragma once
+
+#include "data_def_api.h"
+using namespace std;
+
+//定义GCV_DEBUG,每一步图像处理都会输出中间结果图片显示(opencv),回车后继续执行,用于测试
+// #define GCV_DEBUG
+// export 
+#define GCV_EXPORTS
+
+
+#ifdef GCV_EXPORTS
+#define GCV_API __declspec(dllexport)
+#else
+#define GCV_API __declspec(dllimport)
+#endif
+
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+namespace graft_cv
+{
+//1 设置log路径
+GCV_API	int cv_set_logpath(char*lpath);
+
+//2 设置log等级
+GCV_API	int cv_set_loglevel(int lev);// 0-debug, 1-info, 2-warning, 3-error
+
+//3 设置是否存储图片(存储路径在configure文件中),true--保存,false--不保存
+GCV_API int cv_init_image_saver();
+
+//4 初始化:本地配置文件初始化(yml)
+//  返回: 0- 正常; 1- 配置文件不存在 
+GCV_API int cv_init(char*conf_file);
+
+
+//5 初始化:通过ConfigParam结构体对象直接赋值配置参数(内存)
+GCV_API void cv_set_param(ConfigParam&);
+
+//6 接口退出前的释放
+GCV_API int cv_release();
+
+//7 获取当前配置文件路径,输入char*要空间足够,内部没有检测是否越界
+GCV_API void cv_get_conf_file(char*);
+
+//8 保存到本地配置文件中(覆盖)
+GCV_API void cv_save_param(char* conf_file/*=0*/);
+
+//9 获取当前的配置参数
+GCV_API void cv_get_param(ConfigParam&);
+
+//10 获取当前版本号,char*要空间足够,内部没有检测是否越界
+GCV_API void get_version(char* buf);
+
+
+//11 砧木最优角度识别初始化,每一株均需初始化一次
+//  返回: 0- 正常; 其他- 失败 
+GCV_API int rs_oa_init();
+
+//12  砧木最优角度识别,追加图像,要求图像的角度按顺序,第一帧图像角度为0,最后一帧图像角度为180
+//   每次追加图片,通过posinfo返回:
+//            1)叶展宽度posinfo.rs_oa_width;
+//            2) 基质宽度posinfo.rs_oa_width_base
+//            3) 茎分叉点y posinfo.rs_oa_stem_y_fork;//茎分叉点y,毫米
+//	          4) 茎可视下端y posinfo.rs_oa_clamp_y_end;//茎可视下端y,毫米
+//            5)2帧图像:posinfo.pp_images[0]--砧木分割二值图像 (return image 为 true时)
+//                        posinfo.pp_images[1]--基质分割二值图像
+//   返回: 0- 正常; 1- 失败
+GCV_API int rs_oa_append(ImgInfo*, PositionInfo& posinfo);
+
+//13 获取砧木最优角度
+//  返回posinfo.rs_oa,
+//      posinfo.rs_oa_base
+//      posinfo.rs_oa_stem_y_fork,
+//      posinfo.rs_oa_clamp_y_end,
+//   返回: 0- 正常; 1- 失败
+GCV_API int rs_oa_get_result(PositionInfo& posinfo);
+
+//14 砧木切割点识别,返回
+//       posinfo.rs_cut_edge_length
+//		 posinfo.rs_cut_upoint_x
+//		 posinfo.rs_cut_upoint_y
+//		 posinfo.rs_stem_diameter
+//  	 posinfo.pp_images   (return image 为 true时)
+//               返回3张图片:图0:二值图像,含茎x-range,茎分叉点右侧边缘坐标(circle中心)
+//                            图1: 灰度图,候选角点(circle),候选框,参考点(茎分叉点右侧边缘坐标)
+//                            图2:灰度图,上切割点,候选框,参考点,下切点(circle中心),根点(circle中心)
+//   返回: 0- 正常; 1- 失败
+GCV_API int rs_cut_point(ImgInfo*, PositionInfo& posinfo);
+
+
+//15 砧木切割点切后识别,返回
+//		posinfo.rs_becut_upoint_x
+//   	posinfo.rs_becut_upoint_y
+//  	 posinfo.pp_images   (return image 为 true时)
+//               返回1张图片:图0:二值图像,含茎x-range,上切点(circle中心),下切点(circle中心),根点(circle中心)
+//                            
+//   返回: 0- 正常; 1- 失败
+//GCV_API int rs_cut_point_reid(ImgInfo*, double edge_length, PositionInfo& posinfo);
+
+//16 穗苗上切割点识别,返回
+//		 posinfo.sc_cut_upoint_x;
+//		 posinfo.sc_cut_upoint_y;
+//		 posinfo.sc_cut_cpoint_x;
+//       posinfo.sc_cut_cpoint_y;
+//  	 posinfo.pp_images      (return image 为 true时)
+//               返回1张图片:图0:二值图像,含茎x-range,茎分叉点水平线,尖点水平线,上切点(circle中心),中切点(circle中心)
+//                
+//   返回: 0- 正常; 1- 失败
+GCV_API int sc_cut_point(ImgInfo*, PositionInfo& posinfo);
+
+
+
+};//namespace graft_cv
+
+#ifdef __cplusplus
+}
+#endif

+ 198 - 0
imstorage_manager.cpp

@@ -0,0 +1,198 @@
+#include "stdafx.h"
+#include <io.h>
+#include <time.h>
+#include <stdio.h>
+#include "imstorage_manager.h"
+
+//namespace graft_gcv
+//{
+
+
+CRITICAL_SECTION g_cs;
+
+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;
+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;
+}
+	
+CImStoreManager::CImStoreManager()
+	:m_storeDays(7)
+	,m_storeDir(""),
+	m_workHandle(0),
+	m_workHandleSave(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);
+	Sleep(500);
+}
+CImStoreManager::~CImStoreManager(){
+	g_thread_run=false;
+	g_thread_run_saver=false;
+	HANDLE handles[2];
+	handles[0]=m_workHandle;
+	handles[1]=m_workHandleSave;
+	WaitForMultipleObjects(2,handles,TRUE,500);
+	DeleteCriticalSection(&g_cs);
+
+}
+
+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(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;
+}
+//};

+ 62 - 0
imstorage_manager.h

@@ -0,0 +1,62 @@
+#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;
+	Mat image;
+} ImgParam;
+
+typedef struct 
+{	
+    int nIndex;
+    queue<ImgParam>* imgQ;
+        
+} ThreadParamSave;
+
+typedef struct 
+{	
+    string folder;
+	int store_days;
+    bool* state;
+        
+} ThreadParamClean;
+
+int WINAPI TheadFuncClearn(LPVOID lpParam);
+int WINAPI TheadFuncSave(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(Mat&img,string name_id);
+	void restart_start_worker();
+
+
+protected:
+	int m_storeDays;
+	string m_storeDir;
+	bool is_valid_folder();
+	HANDLE m_workHandle;
+	HANDLE m_workHandleSave;
+	queue<ImgParam> m_images;
+};
+
+//};

+ 96 - 0
logger.cpp

@@ -0,0 +1,96 @@
+#include "logger.h"
+#include "utils.h"
+
+namespace graft_cv{
+	
+	CGcvLogger::CGcvLogger(ofstream& ofs)
+		:m_target(terminal)
+		,m_level(debug)
+		,m_outfile(ofs)
+	{
+		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)
+	{
+		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)
+	{
+		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();
+		}
+	}
+
+	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);
+		}
+	}
+
+
+};

+ 40 - 0
logger.h

@@ -0,0 +1,40 @@
+#pragma once
+
+#include <iostream>
+#include <string>
+
+#include <fstream>
+
+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;
+		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);
+		
+	};
+};

+ 18 - 0
opencv.props

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ImportGroup Label="PropertySheets" />
+  <PropertyGroup Label="UserMacros" />
+  <PropertyGroup>
+    <IncludePath>F:\soft\opencv\build\include;F:\soft\opencv\build\include\opencv2;$(IncludePath)</IncludePath>
+  </PropertyGroup>
+  <PropertyGroup>
+    <LibraryPath>$(LibraryPath)</LibraryPath>
+  </PropertyGroup>
+  <ItemDefinitionGroup>
+    <Link>
+      <AdditionalLibraryDirectories>F:\soft\opencv\build\x64\vc15\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
+      <AdditionalDependencies>%(AdditionalDependencies)</AdditionalDependencies>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemGroup />
+</Project>

+ 814 - 0
optimal_angle.cpp

@@ -0,0 +1,814 @@
+//#include "StdAfx.h"
+#include <opencv2\imgproc\imgproc.hpp>
+#include "optimal_angle.h"
+#include "utils.h"
+#include <string.h>
+
+namespace graft_cv{
+
+COptimalAngle::COptimalAngle(ConfigParam&cp, CGcvLogger* pLog/*=0*/)
+	:m_cparam(cp),
+	m_pLogger(pLog),
+	m_pImginfo(0),
+	m_pImginfoBase(0),
+	m_height(0),
+	m_width(0),
+	m_patch_id(""),
+	m_imgIndex(0),
+	m_imgId(""),
+	m_ppImgSaver(0)
+{
+}
+COptimalAngle::~COptimalAngle(void)
+{
+	this->clear_imginfo();
+}
+void COptimalAngle::clear_imginfo(){
+	if (m_pImginfo){
+		imginfo_release(&m_pImginfo);
+		m_pImginfo=0;
+	}
+	if (m_pImginfoBase){
+		imginfo_release(&m_pImginfoBase);
+		m_pImginfoBase=0;
+	}
+}
+int COptimalAngle::reset()
+{
+	if(m_pLogger){
+		m_pLogger->DEBUG("optimal angle reset begin.");
+	}
+	m_fork_ys.clear();
+	m_end_ys.clear();
+	m_widths.clear();
+	m_an2width.clear();
+	m_an2widthBase.clear();
+	m_width=0;
+	m_height=0;
+	m_patch_id="";
+	m_imgIndex=0;
+	if(m_pLogger){
+		m_pLogger->DEBUG("optimal angle reset finished.");
+	}
+	return 0;
+}
+
+int COptimalAngle::append(
+	ImgInfo* imginfo,
+	PositionInfo& posinfo	
+	)
+{	
+	//clear opencv windows
+	if(m_cparam.image_show){destroyAllWindows();}
+
+	clock_t t;
+	clock_t t0 = clock();
+	if(m_imgIndex==0){
+		//new patch
+		m_patch_id = getImgId(img_type::oa);	
+	}
+	m_imgId = getImgIdOa(m_patch_id,m_imgIndex);	
+	m_imgIndex+=1;
+
+	if(m_pLogger){
+		m_pLogger->INFO(m_imgId + " append image begin.");
+	}	
+	if(!isvalid(imginfo)){
+		throw_msg(m_imgId+" invalid input image");
+	}
+	if(m_width==0 || m_height==0){
+		m_width = imginfo->width;
+		m_height = imginfo->height;
+	}
+	int angle = imginfo->angle;
+	if(m_pLogger){
+		stringstream buff;
+		buff<<m_imgId<<" append image, angle="<<angle
+			<<"\twidth="<<imginfo->width
+			<<"\theight="<<imginfo->height;
+		m_pLogger->INFO(buff.str());
+	}
+	Mat img = imginfo2mat(imginfo);	
+	
+	//imshow_wait("src", img);
+	if(m_cparam.self_camera){
+		image_set_bottom(img,20,8);
+		if(m_pLogger){				
+			m_pLogger->DEBUG(m_imgId+" oa_append image set bottom with pixel value 20.");				
+		}
+	}
+	if(m_cparam.oa_y_flip){
+		flip(img,img,0);
+		if(m_pLogger){				
+			m_pLogger->DEBUG(m_imgId+" oa_append image y fliped.");				
+		}
+	}
+	if(m_ppImgSaver && *m_ppImgSaver){
+		(*m_ppImgSaver)->saveImage(img, m_imgId);
+		if(m_pLogger){				
+			m_pLogger->DEBUG(m_imgId+" oa_append after image save.");				
+		}
+	}
+	if(m_cparam.image_show){
+		Mat tmp_b = img.clone();
+		cv::line(tmp_b,Point(0,m_cparam.oa_clip_y_min), Point(tmp_b.cols,m_cparam.oa_clip_y_min),Scalar(200),3);
+		cv::line(tmp_b,Point(0,m_cparam.oa_clip_y_max), Point(tmp_b.cols,m_cparam.oa_clip_y_max),Scalar(200),3);			
+		imshow_wait("oa_sub_image_range", tmp_b);
+	}
+
+	Rect rect_rs(Point(0,0),Point(img.cols,m_cparam.oa_clip_y_min));
+	//Rect rect_base(Point(0,m_cparam.oa_clip_y_max),Point(img.cols,img.rows));
+
+	Mat rs_img = img(rect_rs);
+	//Mat bs_img = img(rect_base);
+
+	
+	// 1 rootstock image processing 
+	// 1.1 calculate the image, get binary image and get object width
+	int min_idx, max_idx;
+	vector<int> hist_col;
+	int width = imgproc(rs_img, hist_col, min_idx, max_idx);
+	m_an2width.insert(make_pair<int,int>(angle, width));
+	posinfo.rs_oa_width=width;
+
+	if(m_cparam.image_show){			
+		Mat hist_col_img;
+		hist2image(hist_col,hist_col_img,1,m_cparam.image_col_grid,m_cparam.image_row_grid);
+		imshow_wait("oa_hist_col", hist_col_img);			
+	}
+
+	if(m_pLogger){
+		stringstream buff;
+		buff<<m_imgId<<" append image, rootstock width="<<width;
+		m_pLogger->INFO(buff.str());
+	}
+
+	if(!m_cparam.image_show){
+		t = clock();		
+		if(1000.0*((float)(t-t0))/CLOCKS_PER_SEC>(float)m_cparam.timeout_append){
+			throw_msg(m_imgId+" time out");
+		}
+	}
+
+	//1.2 茎粗计算
+	//1.2.1 茎x方向位置:x方向位置范围	
+	int r0,r1;
+	r0=r1=-1;	
+	std::vector<int>hist_row_leaf;
+	mat_histogram(m_binImg,hist_row_leaf,1);
+	for(size_t i=0;i<hist_row_leaf.size();++i){
+		if(hist_row_leaf[i]>=m_cparam.oa_min_hist_value && r0<0){r0=i;}
+		if(hist_row_leaf[i]>=m_cparam.oa_min_hist_value){r1=i;}
+	}
+	if(r0<0 || r1<0){throw_msg(string(m_imgId+" invalid image, with no object in binary image"));}
+		
+	vector<int> hist_col_yfork;
+	mat_histogram_yfork(m_binImg,hist_col_yfork,r0,r1);
+	if(m_cparam.image_show){			
+		Mat tmp;
+		hist2image(hist_col_yfork,tmp,1,m_cparam.image_col_grid,m_cparam.image_row_grid);
+		imshow_wait("oa_hist_col_yfork", tmp);			
+	}	
+
+	int x0,x1,stem_x0, stem_x1;
+	int centx = (int)((min_idx+ max_idx)/2.0);
+	get_stem_x_range_oa(
+		hist_col_yfork,
+		m_cparam.oa_col_th_ratio,
+		m_cparam.oa_stem_x_padding,
+		centx,
+		width,
+		x0,
+		x1,
+		stem_x0,
+		stem_x1);
+
+	if(m_pLogger){
+		stringstream buff;
+		buff<<m_imgId<<" append image, x0="<<x0
+			<<"\tx1="<<x1
+			<<"\tstem_x0="<<stem_x0
+			<<"\tstem_x1="<<stem_x1;
+		m_pLogger->INFO(buff.str());
+	}
+
+	if(m_cparam.image_show){
+		Mat tmp_b = m_binImg.clone();
+		cv::line(tmp_b,Point(x0,0), Point(x0,tmp_b.rows),Scalar(200),3);
+		cv::line(tmp_b,Point(x1,0), Point(x1,tmp_b.rows),Scalar(200),3);			
+		imshow_wait("oa_x_range", tmp_b);
+	}
+
+	if(!m_cparam.image_show){
+		t = clock();
+		if(1000.0*((float)(t-t0))/CLOCKS_PER_SEC>(float)m_cparam.timeout_append){
+			throw_msg(m_imgId+" time out");
+		}		
+	}
+
+	vector<int> hist_row;
+	mat_histogram(m_binImg,hist_row,1,-1,-1,x0,x1+1);
+
+	if(m_cparam.image_show){		
+		Mat hist_row_img;
+		hist2image(hist_row,hist_row_img, 0,m_cparam.image_col_grid,m_cparam.image_row_grid);		
+		imshow_wait("oa_hist_row", hist_row_img);			
+	}
+	// 1.2.2 茎分叉点检测,y方向检测
+	int stem_fork_y=-1,stem_end_y=-1, stem_dia=-1;
+	get_stem_y_fork(
+		hist_row,
+		m_cparam.oa_row_th_ratio,
+		m_cparam.oa_stem_dia_min,
+		m_cparam.oa_stem_fork_y_min,
+		m_cparam.oa_stem_dia_mp,
+		stem_fork_y,
+		stem_end_y,
+		stem_dia);
+	if(m_pLogger){
+		stringstream buff;
+		buff<<m_imgId<<" append image, stem_fork_y="<<stem_fork_y
+			<<"\tstem_end_y="<<stem_end_y
+			<<"\tstem_dia="<<stem_dia;
+		m_pLogger->INFO(buff.str());
+	}
+	
+
+	if(m_cparam.image_show){
+		Mat tmp_bin = m_binImg.clone();
+		cv::line(tmp_bin,Point(min_idx,0), Point(min_idx,tmp_bin.rows),Scalar(200),3);
+		cv::line(tmp_bin,Point(max_idx,0), Point(max_idx,tmp_bin.rows),Scalar(200),3);		
+		cv::line(tmp_bin,Point(x0,stem_fork_y), Point(x1,stem_fork_y),Scalar(200),3);
+		cv::line(tmp_bin,Point(x0,stem_end_y), Point(x1,stem_end_y),Scalar(200),3);
+		imshow_wait("oa_result", tmp_bin);
+	}
+	
+	m_fork_ys.push_back(stem_fork_y);
+	m_end_ys.push_back(stem_end_y);
+	m_widths.push_back(width);
+
+
+	//2 基质图像处理
+	/*int min_idx_base, max_idx_base;
+	vector<int> hist_col_base;
+	int width_base = imgprocBase(bs_img, hist_col_base, min_idx_base, max_idx_base);
+	m_an2widthBase.insert(make_pair<int,int>(angle, width_base));
+	posinfo.rs_oa_width_base = width_base;
+
+	if(m_pLogger){
+		stringstream buff;
+		buff<<m_imgId<<" append image, base width="<<width_base;
+		m_pLogger->INFO(buff.str());
+	}*/
+
+	// 3 返回结果
+	double fork_y = (double)stem_fork_y;
+	double end_y = (double)stem_end_y;
+
+	fork_y = ((double)m_height/2.0 - fork_y)*m_cparam.rs_oa_pixel_ratio;
+	end_y =  ((double)m_height/2.0 - end_y)*m_cparam.rs_oa_pixel_ratio;
+	posinfo.rs_oa_stem_y_fork=fork_y;
+    posinfo.rs_oa_clamp_y_end=end_y;
+	if(m_pLogger){
+		stringstream buff;
+		buff<<m_imgId<<" append image, stem_fork_y(mm)="<<fork_y
+			<<"\tstem_end_y(mm)="<<end_y;
+		m_pLogger->INFO(buff.str());
+	}
+	//4 返回图像
+	if(m_cparam.image_return){
+		this->clear_imginfo();
+		cv::line(m_binImg,Point(min_idx,0), Point(min_idx,rs_img.rows),Scalar(200),3);
+		cv::line(m_binImg,Point(max_idx,0), Point(max_idx,rs_img.rows),Scalar(200),3);
+
+		cv::line(m_binImg,Point(x0,stem_fork_y), Point(x1,stem_fork_y),Scalar(200),3);
+		cv::line(m_binImg,Point(x0,stem_end_y), Point(x1,stem_end_y),Scalar(200),3);
+
+		clear_imginfo();
+		m_pImginfo = mat2imginfo(m_binImg);
+		//m_pImginfoBase = mat2imginfo(m_binImgBase);
+		posinfo.pp_images[0]=m_pImginfo;
+		//posinfo.pp_images[1]=m_pImginfoBase;
+
+		if(m_ppImgSaver && *m_ppImgSaver){
+			(*m_ppImgSaver)->saveImage(m_binImg, m_imgId+"_rst_0");			
+		}
+	}
+	if(m_pLogger){
+		m_pLogger->INFO(m_imgId + " append image finished.");
+	}	
+	return 0;
+}
+
+double COptimalAngle::infer(PositionInfo& posinfo){
+	
+	if(m_pLogger){
+		m_pLogger->INFO(m_patch_id + " optimal angle infer begin.");
+	}	
+	double oa = this->angle_fit(this->m_an2width);
+	/*double oa_base = 0.0;
+	try{
+		oa_base = this->angle_fit_base(this->m_an2widthBase);
+	}
+	catch(...){
+		if(m_pLogger){		
+			m_pLogger->WARNING("angle_fit_base() error");
+		}
+	}*/
+	posinfo.rs_oa = oa;
+	//posinfo.rs_oa_base=oa_base;
+
+	if(m_fork_ys.size()==0 ||m_end_ys.size()==0){
+		throw_msg(m_patch_id+ " empty fork_ys or end_ys, NEED append image");
+	}
+	vector<size_t>des_idx = sort_indexes_e(m_widths,false);	
+	int e_idx = (int)((float)m_end_ys.size() * 0.5);	
+	sort(m_end_ys.begin(), m_end_ys.end());
+	double fork_y = ((m_fork_ys[des_idx[0]] +  m_fork_ys[des_idx[1]])/2.0);
+	double end_y = m_end_ys[e_idx];
+	if(m_pLogger){
+		stringstream buff;
+		buff<<m_patch_id<<" angle fit result, stem_fork_y(pixel)="<<(int)(fork_y)
+			<<"\tstem_end_y(pixel)="<<(int)(end_y);
+		m_pLogger->INFO(buff.str());
+	}
+
+	fork_y = ((double)m_height/2.0 - fork_y)*m_cparam.rs_oa_pixel_ratio;
+	end_y =  ((double)m_height/2.0 - end_y)*m_cparam.rs_oa_pixel_ratio;
+	posinfo.rs_oa_stem_y_fork=fork_y;
+    posinfo.rs_oa_clamp_y_end=end_y;
+	if(m_pLogger){
+		stringstream buff;
+		buff<<m_patch_id<<" angle fit result, stem_fork_y(mm)="<<fork_y
+			<<"\tstem_end_y(mm)="<<end_y;
+		m_pLogger->INFO(buff.str());
+	
+		m_pLogger->INFO(m_patch_id + " optimal angle infer finished.");
+	}	
+	return oa;
+}
+
+int COptimalAngle::imgproc(
+	Mat& img, 
+	vector<int>& hist,
+	int& min_idx, 
+	int& max_idx
+	)
+{
+	//int morph_size = 1;
+	//int min_hist_value = 10;
+	hist.clear();
+
+	Mat b_img;
+	double th = threshold(
+		img, 
+		b_img, 
+		255,
+		255,
+		THRESH_OTSU);//+THRESH_BINARY_INV
+
+	Mat kernel = getStructuringElement(
+		MORPH_RECT, 
+		Size( 2*m_cparam.oa_morph_radius+1, 2*m_cparam.oa_morph_radius+1), 
+		Point( m_cparam.oa_morph_radius, m_cparam.oa_morph_radius)
+		);
+    
+    morphologyEx(
+		b_img, 
+		m_binImg, 
+		MORPH_CLOSE,
+		kernel,
+		Point(-1,-1),
+		m_cparam.oa_morph_iteration);
+
+	
+	
+	mat_histogram(m_binImg, hist, 0);		
+
+	/*int min_idx, max_idx;*/
+	min_idx = hist.size();
+	max_idx = 0;
+	vector<int>::const_iterator it0 = hist.begin();
+	for(vector<int>::const_iterator it = hist.begin();it!=hist.end();++it){
+		if(*it>=m_cparam.oa_min_hist_value){
+			int idx = it - it0;
+			if(idx<min_idx){min_idx = idx;}
+			if(idx > max_idx) {max_idx = idx;}
+		}
+	}
+	if(max_idx<=min_idx){
+		throw_msg(m_imgId+" invalid binary image, not exist valid objects");		
+	}
+	if(m_cparam.image_show){		
+		Mat tmp = m_binImg.clone();
+		cv::line(tmp,Point(min_idx,0), Point(min_idx,tmp.rows),Scalar(156),3);
+		cv::line(tmp,Point(max_idx,0), Point(max_idx,tmp.rows),Scalar(156),3);
+		imshow_wait("oa_bin", tmp);		
+	}
+    int width = max_idx-min_idx+1;	
+	return width;
+}
+
+int COptimalAngle::imgprocBase(
+	Mat& img, 
+	vector<int>& hist,
+	int& min_idx, 
+	int& max_idx
+	)
+{
+	//int morph_size = 1;
+	//int min_hist_value = 10;
+	hist.clear();
+
+	Mat b_img;
+	double th = threshold(
+		img, 
+		b_img, 
+		255,
+		255,
+		THRESH_OTSU+THRESH_BINARY_INV);
+
+	Mat kernel = getStructuringElement(
+		MORPH_RECT, 
+		Size( 2*m_cparam.oa_morph_radius_base+1, 2*m_cparam.oa_morph_radius_base+1), 
+		Point( m_cparam.oa_morph_radius_base, m_cparam.oa_morph_radius_base)
+		);
+    
+    morphologyEx(
+		b_img, 
+		m_binImgBase, 
+		MORPH_OPEN,
+		kernel,
+		Point(-1,-1),
+		m_cparam.oa_morph_iteration_base);
+
+	if(m_cparam.image_show){	
+		destroyAllWindows();
+		imshow_wait("oa_bin_base", m_binImgBase);		
+	}
+	
+	mat_histogram(m_binImgBase, hist, 0);	
+
+	/*int min_idx, max_idx;*/
+	min_idx = hist.size();
+	max_idx = 0;
+	vector<int>::const_iterator it0 = hist.begin();
+	for(vector<int>::const_iterator it = hist.begin()+5;it!=hist.end();++it){
+		if(*it>=m_cparam.oa_min_hist_value_base){
+			int idx = it - it0;
+			if(idx<min_idx){min_idx = idx;}
+			if(idx > max_idx) {max_idx = idx;}
+		}
+	}
+	if(max_idx<=min_idx){
+		throw_msg(m_imgId+" invalid binary image, not exist valid objects");		
+	}
+    int width = max_idx-min_idx+1;	
+	return width;
+}
+//angle_fit()方法
+//append()加载的图片,满足下述条件:0-180度采样,其中0和180度都有数值
+double COptimalAngle::angle_fit(map<int,int>& an2width,bool is_base/*=false*/)
+{
+	if(an2width.size()<3){
+		throw_msg(m_patch_id+" not enough valid images, NEED append image");
+	}
+	double oa = 0.0;
+	std::vector<double> x;//angle
+	std::vector<double> y;//width
+
+	map<int,int>::iterator it = an2width.begin();
+	for(it=an2width.begin(); it!=an2width.end(); ++it)
+	{
+		x.push_back((double)(it->first));
+		y.push_back((double)(it->second));
+	}
+	//mean value of first and latest y
+	double y_mu = 0.5*(y[0]+ y[y.size()-1]);
+	y[0] = y[y.size()-1] = y_mu;
+	size_t ap_times = x.size()-1;
+	for (size_t i=0; i<ap_times;++i)
+	{
+		x.push_back(x[i+1]+180.0);
+		y.push_back(y[i+1]);
+	}
+	vector<double>::iterator smallest = min_element(begin(y), end(y));
+	size_t min_idx, max_idx;
+	min_idx = distance(y.begin(), smallest);
+
+	for(size_t i = min_idx+1;i<y.size();++i){
+		if(fabs(y[i]-*smallest)<0.1){
+			max_idx = i;
+			break;
+		}
+	}
+
+	// slice x and y the convex segment
+	vector<double>xx;
+	vector<double>yy;
+	for(size_t i=min_idx;i<=max_idx;++i){
+		xx.push_back(x[i]);
+		yy.push_back(y[i]);
+	}
+
+	// fit quadratic function with opencv
+	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];
+	}
+
+	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];
+	}
+	Mat c, d;
+	c = A.t() * A;
+	d = A.t() * b;
+	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(m_patch_id+" a2 = 0 in solver");
+	}
+	oa = -a1 / a2 / 2;
+	if(oa >180.0){oa -= 180.0;}
+	return oa;
+}
+double COptimalAngle::angle_fit_base(map<int, int>&){
+	return 0.0;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+COptimalAnglePart::COptimalAnglePart(ConfigParam&cp, CGcvLogger* pLog/*=0*/)
+	:COptimalAngle(cp,pLog)
+{
+}
+COptimalAnglePart::~COptimalAnglePart()
+{
+	COptimalAngle::clear_imginfo();
+}
+// append()加载的图片,满足不是0-180度采样,但保证部分采样中一定存在最大值,
+// 直接利用这些数值进行二次函数的拟合
+double COptimalAnglePart::angle_fit(map<int,int>& an2width,bool is_base/*=false*/)
+{ 
+	
+	double oa = 0.0;
+	std::vector<double> xx;//angle
+	std::vector<double> yy;//width
+
+	map<int,int>::iterator it = an2width.begin();
+	for(it=an2width.begin(); it!=an2width.end(); ++it)
+	{
+		xx.push_back((double)(it->first));
+		yy.push_back((double)(it->second));
+	}
+	//sort 
+	for(size_t i =0;i<xx.size()-1;++i){
+		for(size_t j = i+1; j<xx.size();++j){
+			if(xx[j]<xx[i]){
+				double tmp = xx[i];
+				xx[i]=xx[j];
+				xx[j]=tmp;
+				tmp = yy[i];
+				yy[i]=yy[j];
+				yy[j]=tmp;
+			}
+		}
+	}
+
+	if(m_pLogger){			
+		stringstream buff;
+		buff<<m_patch_id;
+		if(is_base){
+			buff<<" rootstock_base ";
+		}
+		else{
+			buff<<" rootstock_leaf ";
+		}
+		buff<<"opt-angle, (xi,yi): ";
+		for(size_t i=0;i<xx.size();++i){
+			buff<<"("<<xx[i]<<","<<yy[i]<<"), ";
+		}		
+		m_pLogger->INFO(buff.str());
+	}
+
+	if(an2width.size()<3){
+		throw_msg(m_patch_id+" not enough valid images, NEED append image");
+	}
+	// angle field check
+	if((xx.back()-xx.front())<90){
+		if(m_pLogger){			
+			stringstream buff;
+			buff<<m_patch_id<<" x_end="<<xx.back()<<" x_front="<<xx.front();				
+			buff<<" not enough angle field, small than 90 degree.";
+			m_pLogger->ERRORINFO(buff.str());
+		}
+		throw_msg(m_patch_id+" not enough angle field, small than 90 degree");
+	}
+	oa = opt_angle_part_impl(xx,yy,is_base);
+	if(m_pLogger){
+		stringstream buff;
+		buff<<m_patch_id;
+		if(is_base){
+			buff<<" rootstock_base ";
+		}
+		else{
+			buff<<" rootstock_leaf ";
+		}
+        buff<<" opt-angle: "<<oa;
+		m_pLogger->INFO(buff.str());
+	}
+	return oa;	
+}
+double COptimalAnglePart::angle_fit_base(map<int, int>& an2width){
+	
+	double oa = 0.0;
+	//find the maximun angle
+	oa = angle_fit(an2width,true);
+
+	//get the minimum angle
+	if(oa>45.0){oa -= 45.0;}
+	else{oa +=45.0;}
+	return oa;	
+}
+void COptimalAnglePart::zero_cross_detect(
+	vector<int>&d_sign, //input
+	bool& is_local_max, // whether exists local max 
+	int& local_limit_idx // local limit index of original vector
+)
+{
+	int min_zc_idx = -1;
+    int max_zc_idx = -1;
+	bool b_local_min =false;
+	bool b_local_max =false;
+    bool is_all_down=true;
+    bool is_all_up = true;
+    for (size_t i=0; i<d_sign.size();++i){
+		if (d_sign[i] >0){is_all_down=false;}
+		if (d_sign[i] <0){is_all_up=false;}
+		if (i == d_sign.size()-1){continue;}
+
+        if (d_sign[i] >=0 && d_sign[i+1] <=0){
+			//is_local_max=true;
+			max_zc_idx = i+1;
+			b_local_max=true;
+            
+		}
+        if (d_sign[i] <=0 && d_sign[i+1] >=0){
+			//is_local_max=false;
+            min_zc_idx=i+1;
+			b_local_min=true;
+		}
+	}
+    if(b_local_min){
+		is_local_max=false;
+        local_limit_idx = min_zc_idx;
+		return;
+	}
+	if(b_local_max){
+		is_local_max=true;
+        local_limit_idx = max_zc_idx;
+		return;
+	}
+    if (is_all_down){
+		is_local_max=true;
+        local_limit_idx=0;
+        return;
+	}
+    if( is_all_up){
+		is_local_max=true;
+        local_limit_idx=d_sign.size();
+        return;        
+	}
+	is_local_max=true;
+    local_limit_idx=-1;
+    return;
+}
+double COptimalAnglePart::limit_min_angle(
+		vector<double>&x, //sorted, ascending
+		vector<double>&y,
+		size_t min_idx
+		)
+{
+	size_t i = min_idx;
+	if(fabs(x[i+1]-x[i])<1.0e-6 || fabs(x[i]-x[i-1])<1.0e-6){
+	 throw_msg(m_patch_id+" limit_min_angle, diff_x < 1.0e-6");
+	}
+    double k_r = (y[i+1]-y[i])/(x[i+1]-x[i]);
+    double k_l = (y[i]-y[i-1])/(x[i]-x[i-1]);
+    double oa = 0.0;
+	double b_r = 0.0;
+	double b_l = 0.0;
+	double cross_x = 0;
+    if (fabs(k_r) > fabs(k_l)){
+        // right line
+        b_r = y[i]-k_r*x[i];
+        b_l = y[i-1] + k_r*x[i-1];
+        cross_x = line_cross(k_r,b_r,-k_r,b_l);
+        oa = cross_x;
+	}
+    else{
+        //# left line
+        b_l = y[i]-k_l*x[i];
+        b_r =  y[i+1] + k_l*x[i+1];
+        cross_x = line_cross(k_l,b_l,-k_l,b_r);
+        oa = cross_x;
+	}
+    return oa;
+}
+
+double COptimalAnglePart::line_cross(double k1,double b1,double k2,double b2)
+{
+	double x = (b2-b1) / (k1-k2);
+    return x;
+}
+
+double COptimalAnglePart::opt_angle_part_impl(
+		vector<double>&x, //sorted, ascending
+		vector<double>&y,
+		bool is_base/*=false*/)
+{
+	// sign of difference of y
+	vector<int> dy_sign;
+	for(size_t i=1; i<y.size();++i){
+		double dd = y[i] - y[i-1];
+		if(dd>0){
+			dy_sign.push_back(1);
+		}
+		else{
+			if(dd<0){dy_sign.push_back(-1);}
+			else{dy_sign.push_back(0);}
+		}
+	}
+	//local limit search
+	bool is_local_max = false; 
+	int local_limit_idx = -1;
+	zero_cross_detect(dy_sign,is_local_max,local_limit_idx);
+	if(local_limit_idx<0){
+	    throw_msg(m_patch_id+" not found local limit");
+	}
+	// optimal angle calculation
+	vector<double> xx;
+	vector<double> yy;
+	double oa=0.0;
+	if(is_local_max){
+		//存在局部最大
+        if( local_limit_idx == 0 || local_limit_idx==y.size()-1){
+            //#单调  
+			oa = x[local_limit_idx];
+			/*if (local_limit_idx == 0){				
+				for(size_t i=0;i<3;++i){
+					xx.push_back(x[i]);
+					yy.push_back(y[i]);
+				}
+			}
+			else	{
+				for(size_t i=y.size()-3;i<y.size();++i){
+					xx.push_back(x[i]);
+					yy.push_back(y[i]);
+				}
+			}             
+			oa = qua_fitting(xx,yy); */
+			if(m_pLogger){				
+				m_pLogger->INFO(m_patch_id+ " angle fit -- signle --- max_angle");
+			}
+			
+		}
+        else{
+            // 极大值        
+			for(size_t i=local_limit_idx-1;i<local_limit_idx+2;++i){
+					xx.push_back(x[i]);
+					yy.push_back(-y[i]);
+				}
+			oa = limit_min_angle(xx,yy,1);  
+			if(m_pLogger){				
+				m_pLogger->INFO(m_patch_id+" angle fit -- local max --- 3p insert");
+			}
+		}
+	}
+    else{
+		// 极小值   
+        oa = limit_min_angle(x,y,local_limit_idx);
+		if(is_base){
+			//for base, offset 45 degree
+			if(oa >45){oa = oa-45.0;}
+			else{oa = oa+45.0;}  
+		}
+		else{  
+			// for rootstock, offset 90 degree
+			if(oa >90){oa = oa-90.0;}
+			else{oa = oa+90.0;}   
+		}
+		if(m_pLogger){				
+			m_pLogger->INFO(m_patch_id+" angle fit -- local min --- 3p insert");
+		}
+	}
+    return oa;
+}
+};

+ 127 - 0
optimal_angle.h

@@ -0,0 +1,127 @@
+#pragma once
+#include <opencv.hpp>
+#include <map>
+#include <opencv2\imgproc\imgproc.hpp>
+#include "data_def_api.h"
+#include "logger.h"
+#include "imstorage_manager.h"
+
+using namespace std;
+using namespace cv;
+
+namespace graft_cv{
+
+// class COptimalAngle
+// 1. find the max angle 
+// 2. find the clamp position
+// 条件:图像0度到180度间,并第一帧为0度时刻,最后一帧为180时刻
+// 采用的方法:拼接数据(360度范围),通过找到两个最小点,此两点间的数据用
+//             二次曲线拟合,得到对称轴x坐标,即为最优角度
+class COptimalAngle
+{
+public:
+	COptimalAngle(ConfigParam&, CGcvLogger* pLog=0);
+	virtual ~COptimalAngle(void);
+	int reset();
+	int append(ImgInfo*, 
+		PositionInfo& posinfo
+		);
+	double infer(PositionInfo& posinfo);// calculate the optimal angle
+
+	//fit optimal angle based m_an2width
+	virtual double angle_fit(map<int, int>&,bool is_base=false);
+	virtual double angle_fit_base(map<int, int>&);// for base matter
+	void set_image_saver(CImStoreManager** ppis)
+	{
+		m_ppImgSaver=ppis;
+	}
+protected:
+	//global configure parameters
+	ConfigParam& m_cparam;
+	CGcvLogger* m_pLogger;
+	string m_patch_id;
+	CImStoreManager** m_ppImgSaver;
+	int m_imgIndex;
+	string m_imgId;
+   
+	// binary image
+	Mat m_binImg;
+	Mat m_binImgBase;
+
+	//返回图片,用于调试
+	ImgInfo* m_pImginfo;
+	ImgInfo* m_pImginfoBase;
+
+	// 角度-叶展宽度(像素)
+	map<int, int> m_an2width;
+	map<int, int> m_an2widthBase;
+	// 图像处理函数
+	// 返回min_idx左侧x,max_idx右侧x
+	int imgproc(
+		Mat&,  
+		vector<int>& hist, 
+		int& min_idx, 
+		int& max_idx);
+	int imgprocBase(
+		Mat&,  
+		vector<int>& hist, 
+		int& min_idx, 
+		int& max_idx);
+
+	// 清理释放m_pImginfo
+	void clear_imginfo();
+	    
+	//茎分叉点y值系列
+	vector<int> m_fork_ys;
+	
+	//茎最低点y值系列
+	vector<int> m_end_ys;
+
+	//叶展宽度系列
+	vector<int>m_widths;
+
+	//all image size
+	int m_width;
+	int m_height;	
+};
+
+
+//COptimalAnglePart, 继承自COptimalAngle
+//条件:图像0度到90度间,并保证>=90度范围
+//方法:0-90度范围必然存在最大值或最小值,通过最大最小值局部数据
+//      插值(二次曲线拟合)得到最大角度计算
+class COptimalAnglePart: public COptimalAngle{
+public:
+	COptimalAnglePart(ConfigParam&, CGcvLogger*pLog=0);
+	virtual ~COptimalAnglePart();
+	//fit optimal angle based m_an2width
+	double angle_fit(map<int, int>&,bool is_base=false);
+	double angle_fit_base(map<int, int>&);// for base matter
+protected:
+	//zero_cross_detect()
+	//过零点检测,用于确定是否单调(增或减),局部最小,局部最大,
+	//并返回局部极值index
+	void zero_cross_detect(
+		vector<int>&d_sign, //input
+		bool& is_local_max, // whether exists local max 
+		int& local_limit_idx // local limit index of original vector
+		);  
+	double limit_min_angle(
+		vector<double>&x, //sorted, ascending
+		vector<double>&y,
+		size_t min_idx
+		);
+
+	// 计算y = k1*x + b1和y = k2*x + b2两条直线相交交点,x坐标
+	double line_cross(
+		double k1,
+		double b1,
+		double k2,
+		double b2);
+	//
+	double opt_angle_part_impl(
+		vector<double>&x, //sorted, ascending
+		vector<double>&y,
+		bool is_base=false);
+};
+};

+ 8 - 0
stdafx.cpp

@@ -0,0 +1,8 @@
+// stdafx.cpp : 只包括标准包含文件的源文件
+// demo.pch 将作为预编译头
+// stdafx.obj 将包含预编译类型信息
+
+#include "stdafx.h"
+
+// TODO: 在 STDAFX.H 中
+// 引用任何所需的附加头文件,而不是在此文件中引用

+ 15 - 0
stdafx.h

@@ -0,0 +1,15 @@
+// stdafx.h : 标准系统包含文件的包含文件,
+// 或是经常使用但不常更改的
+// 特定于项目的包含文件
+//
+
+#pragma once
+
+#include "targetver.h"
+
+#include <stdio.h>
+#include <tchar.h>
+
+
+
+// TODO: 在此处引用程序需要的其他头文件

+ 8 - 0
targetver.h

@@ -0,0 +1,8 @@
+#pragma once
+
+// 包括 SDKDDKVer.h 将定义可用的最高版本的 Windows 平台。
+
+// 如果要为以前的 Windows 平台生成应用程序,请包括 WinSDKVer.h,并将
+// WIN32_WINNT 宏设置为要支持的平台,然后再包括 SDKDDKVer.h。
+
+#include <SDKDDKVer.h>

+ 41 - 0
test.cpp

@@ -0,0 +1,41 @@
+#include "stdafx.h"
+#include <iostream>
+#include <strstream>
+#include <time.h>
+
+#include "imstorage_manager.h"
+//using namespace graft_cv;
+
+void test_file_storeage()
+{
+	///*extern CRITICAL_SECTION g_cs;
+	//InitializeCriticalSection(&g_cs);*/
+	
+	CImStoreManager ism = CImStoreManager();
+	//ism.setStoreDays(1);
+	string path = "D:\\logs\\img_depo";
+	ism.setStoreDir(path);	
+
+	for(size_t i=0;i<7;++i){
+		stringstream buff,bid;
+		buff <<"D:\\private\\grafting_robot\\samples\\rootstlock_pumpkin\\000\\"<<i<<".jpg";
+		bid<<time(NULL)<<"_"<<i;
+		string fn = buff.str();
+		string fid = bid.str();
+		Mat img = imread(fn);
+		ism.saveImage(img,fid);
+
+	}
+	 
+	Sleep(5000);
+	
+
+	//DeleteCriticalSection(&g_cs);
+
+}
+//int _tmain(int argc, _TCHAR* argv[])
+//{		
+//	test_file_storeage();
+//	
+//	return 0;
+//}

+ 1835 - 0
utils.cpp

@@ -0,0 +1,1835 @@
+#include "utils.h"
+#include <opencv.hpp>
+#include <time.h>
+#include <algorithm>
+#include <math.h>
+//#include "fork_rs.h"
+
+namespace graft_cv{
+
+	Mat imginfo2mat(ImgInfo* imginfo)
+	{
+		assert(imginfo->data);
+		assert(imginfo->height>0 && imginfo->width>0);
+		Mat m = Mat(imginfo->height,imginfo->width,CV_8UC1);
+		for(int h=0; h<imginfo->height; ++h)
+		{
+			memcpy((void*)(m.ptr(h)),
+				(void*)(imginfo->data+h*imginfo->width),
+				imginfo->width);
+		};
+		return m;
+	};
+
+	ImgInfo* mat2imginfo(const Mat&img){
+		assert(img.channels()==1);
+		ImgInfo* imginfo = new ImgInfo();
+		imginfo->angle=0;
+		imginfo->height=img.rows;
+		imginfo->width = img.cols;
+		imginfo->data = 0;
+		imginfo->data = new byte[imginfo->height * imginfo->width];
+		memset(imginfo->data, 0,imginfo->height * imginfo->width);	
+		//IplImage ipl_img = cvIplImage(img);
+		for(int i =0; i<imginfo->height;++i){
+			memcpy(imginfo->data+i*imginfo->width, img.ptr(i), imginfo->width);
+		}
+
+		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(
+		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(
+		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(
+		Mat& img,
+		int x0,int y0,
+		int x1,int y1)
+	{
+		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);
+		}
+		line(img,p0,p1,Scalar(255,255,255));
+	
+	}
+	string currTime(){
+			char tmp[64];
+			time_t ptime;
+			time(&ptime);
+			strftime(
+				tmp, 
+				sizeof(tmp), 
+				"%Y-%m-%d %H:%M:%S",
+				localtime(&ptime)
+				);
+			return tmp;
+		}
+	// function getImgId
+	// input im_type, img_type
+	static unsigned int ID_COUNTER = 0;
+	string getImgId(int im_type)
+	{
+		char tmp[64];
+		time_t ptime;
+		time(&ptime);
+		strftime(
+			tmp, 
+			sizeof(tmp), 
+			"%Y%m%d%H%M%S",
+			localtime(&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(		
+			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){
+			line(img,Point(0,row),Point(img.cols-1,row),Scalar(100),line_thick);
+		}
+		//veritcal grid
+		for(int col=0; col<img.cols; col+=grid_col){
+			line(img,Point(col,0),Point(col,img.rows-1),Scalar(100),line_thick);
+		}
+
+	}
+	void hist2image(
+		vector<int>& hist, 
+		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 = 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 = 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(
+		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(
+		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(
+		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);
+		}
+		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 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,
+		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;
+		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){
+			Mat tmp = roi_img.clone();
+			Point p0((int)(beta0),0);
+			Point p1((int)((double)tmp.rows *beta1 +beta0),tmp.rows);
+			line(tmp,p0,p1,Scalar(128,0,0));
+
+			line(tmp,Point(cent_xs[0],cent_ys[0]),
+				Point(cent_xs.back(),cent_ys.back()),Scalar(128,0,0));
+	
+
+			imshow_wait("rs_bin_roi", tmp);	
+		}
+		///////////////////////////////////////////////////////////
+		//兴趣区域图片findContours
+		vector<vector<Point>> contours;
+		vector<Vec4i> hierarchy;
+		findContours(roi_img,contours,hierarchy,RETR_EXTERNAL,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 = pointPolygonTest( contours[max_length_idx], 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){
+			Mat edge;
+			edge = Mat::zeros(roi_img.rows, roi_img.cols, CV_8UC1);
+			drawContours(edge, contours, -1, 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]);
+				line(edge,Point(c-rad,r),Point(c+rad,r),Scalar(80,0,0));		
+			}
+			Point fork_cent_origin;
+			fork_cent_origin.x = xs[max_idx];
+			fork_cent_origin.y =max_idx;
+			circle(edge,fork_cent_origin,3,Scalar(200,0,0));
+			circle(edge,fork_cent_origin,(int)(max_radius),Scalar(200,0,0));
+
+			//circle(edge,Point(xs[opt_max_idx],opt_max_idx),5,Scalar(200,0,0));
+
+			circle(edge,fork_cent,3,Scalar(128,0,0));
+			circle(edge,fork_cent,(int)(max_radius),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,
+		Mat&img, 
+		int waittype/*=-1*/
+		)
+	{	
+		namedWindow(winname, CV_WINDOW_NORMAL);
+		imshow(winname, img);
+		waitKey(waittype);
+	};
+
+	int get_stem_fork_right(
+		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(
+		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(
+		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(
+		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
+		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];
+		}
+
+		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];
+		}
+		Mat c, d;
+		c = A.t() * A;
+		d = A.t() * b;
+		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;
+	}
+
+};

+ 396 - 0
utils.h

@@ -0,0 +1,396 @@
+#pragma once
+
+#include <opencv2\imgproc\imgproc.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 
+	Mat imginfo2mat(ImgInfo*);
+	ImgInfo* mat2imginfo(const 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(Mat& img,unsigned char value,int rows_cnt);
+	void image_set_top(	Mat& img,	unsigned char value,	int rows_cnt);
+	void image_draw_line(Mat& img,int x0,int y0,int x1,int y1);
+	// histogram
+	void mat_histogram(
+		Mat&, 
+		std::vector<int>&hist,
+		int axis=0, 
+		int r0=-1, 
+		int r1=-1, 
+		int c0=-1, 
+		int c1=-1
+		);
+	// weighted histogram
+	void mat_histogram_w(
+		Mat& img,	
+		std::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(
+		Mat& img,	
+		std::vector<int>&hist,	
+		int r0, 
+		int r1	
+	);
+
+	// histogram to image	
+	void hist2image(
+		vector<int>& hist,
+		Mat&img,
+		int aix=1,
+		int grid_col=50,
+		int grid_row=50
+		);
+	
+	template<class T>
+	void hist2image_line(
+	vector<T>& hist, 
+	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, 
+	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 = 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);
+		line(img,Point(i-1,y_1),Point(i,y_0),Scalar(255));
+	}
+	int h1 = height-(int)((th-ymin)/yratio);
+	int h2 = height-(int)((1.0/th-ymin)/yratio);
+	line(img,Point(0,h1),Point(hist.size()-1,h1),Scalar(200));
+	line(img,Point(0,h2),Point(hist.size()-1,h2),Scalar(200));
+	
+};
+
+	void drawgrid(		
+		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 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
+		);
+	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
+		);
+	
+	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
+		);
+
+	////////////////////////////////////////////////////////////////////////
+	// y-fork detect
+	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, 茎分叉y像素位置
+		int& end_y,      //output,茎根部y像素位置
+		int& stem_dia    //output, end_y和fork_y间茎粗
+	);
+
+	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
+	);
+	void get_stem_y_fork_rs_update(
+	const 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,
+	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 std::vector<int>& hist,
+		int stem_dia_min,
+		std::vector<double>& var_ratio		
+		);
+
+	//通过计算茎均值,运用统计过程控制方法检测显著点
+	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, 茎分叉y像素位置
+		int& end_y,      //output,茎根部y像素位置
+		int& stem_dia    //output, end_y和fork_y间茎粗
+	);
+
+	double stem_y_fork_validity(
+		const std::vector<int>& hist,
+		int fork_y,
+		int end_y);
+
+	void get_hist_segment(
+		const std::vector<int>& hist, 
+		int threshold,
+		std::vector<gcv_point<int>>& segments
+	);
+	void get_hist_mean_var_local(
+		const std::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, 
+		Mat&, 
+		int waittype=-1
+		);
+
+	int get_stem_fork_right(
+		Mat&stem_img, 
+		int stem_fork_y, 
+		int stem_x0, 
+		int stem_x1,
+		int detect_window);
+	int get_stem_fork_left(
+		Mat&stem_img, 
+		int stem_fork_y, 
+		int stem_x0, 
+		int stem_x1,
+		int detect_window);
+	void get_stem_fork_xs(
+		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(
+	    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];}
+	}
+
+	
+
+
+	
+};