You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

CardPlusController.java 18 kB

7 years ago
7 years ago
7 years ago
7 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. package com.acts.opencv.base;
  2. import java.util.ArrayList;
  3. import java.util.Date;
  4. import java.util.List;
  5. import java.util.TreeMap;
  6. import javax.servlet.http.HttpServletResponse;
  7. import org.apache.commons.lang3.StringUtils;
  8. import org.opencv.core.Core;
  9. import org.opencv.core.Core.MinMaxLocResult;
  10. import org.opencv.core.CvType;
  11. import org.opencv.core.Mat;
  12. import org.opencv.core.MatOfFloat;
  13. import org.opencv.core.MatOfInt;
  14. import org.opencv.core.Point;
  15. import org.opencv.core.Range;
  16. import org.opencv.core.Rect;
  17. import org.opencv.core.Scalar;
  18. import org.opencv.core.Size;
  19. import org.opencv.highgui.Highgui;
  20. import org.opencv.imgproc.Imgproc;
  21. import org.opencv.photo.Photo;
  22. import org.slf4j.Logger;
  23. import org.slf4j.LoggerFactory;
  24. import org.springframework.stereotype.Controller;
  25. import org.springframework.web.bind.annotation.RequestMapping;
  26. import com.acts.opencv.common.utils.Constants;
  27. import com.acts.opencv.common.utils.OpenCVUtil;
  28. import com.acts.opencv.common.web.BaseController;
  29. @Controller
  30. @RequestMapping(value = "cardPlus")
  31. public class CardPlusController extends BaseController {
  32. private static final Logger logger = LoggerFactory.getLogger(CardPlusController.class);
  33. /**
  34. * 答题卡识别优化
  35. * 创建者 Songer
  36. * 创建时间 2018年3月23日
  37. */
  38. @RequestMapping(value = "answerSheet")
  39. public void answerSheet(HttpServletResponse response, String imagefile, Integer binary_thresh,
  40. String blue_red_thresh) {
  41. System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
  42. logger.info("\n 完整答题卡识别");
  43. String sourcePath = Constants.PATH + imagefile;
  44. logger.info("url==============" + sourcePath);
  45. Mat sourceMat = Highgui.imread(sourcePath, Highgui.CV_LOAD_IMAGE_COLOR);
  46. long t1 = new Date().getTime();
  47. String destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "dtk0.png";
  48. Highgui.imwrite(destPath, sourceMat);
  49. logger.info("原答题卡图片======" + destPath);
  50. // 初始图片灰度图
  51. Mat sourceMat1 = Highgui.imread(sourcePath, Highgui.CV_LOAD_IMAGE_GRAYSCALE);
  52. destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "dtk1.png";
  53. Highgui.imwrite(destPath, sourceMat1);
  54. logger.info("生成灰度图======" + destPath);
  55. // 先膨胀 后腐蚀算法,开运算消除细小杂点
  56. Mat element = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(2 * 1 + 1, 2 * 1 + 1));
  57. Imgproc.morphologyEx(sourceMat1, sourceMat1, Imgproc.MORPH_OPEN, element);
  58. destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "dtk2.png";
  59. Highgui.imwrite(destPath, sourceMat1);
  60. logger.info("生成膨胀腐蚀后的图======" + destPath);
  61. // 切割右侧和底部标记位图片
  62. Mat rightMark = new Mat(sourceMat1, new Rect(sourceMat1.cols() - 100, 0, 100, sourceMat1.rows()));
  63. destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "dtk3.png";
  64. Highgui.imwrite(destPath, rightMark);
  65. logger.info("截取右侧定位点图======" + destPath);
  66. // 平滑处理消除噪点毛刺等等
  67. Imgproc.GaussianBlur(rightMark, rightMark, new Size(3, 3), 0);
  68. destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "dtk4.png";
  69. Highgui.imwrite(destPath, rightMark);
  70. logger.info("平滑处理后的右侧定位点图======" + destPath);
  71. // 根据右侧定位获取水平投影,并获取纵向坐标
  72. Mat matright = horizontalProjection(rightMark);
  73. destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "dtk5.png";
  74. Highgui.imwrite(destPath, matright);
  75. logger.info("右侧水平投影图======" + destPath);
  76. // 获取y坐标点,返回的是横向条状图集合
  77. List<Rect> listy = getBlockRect(matright, 1, 0);
  78. Mat footMark = new Mat(sourceMat1, new Rect(0, sourceMat1.rows() - 150, sourceMat1.cols(), 50));
  79. destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "dtk6.png";
  80. Highgui.imwrite(destPath, footMark);
  81. logger.info("截取底部定位点图======" + destPath);
  82. Imgproc.GaussianBlur(footMark, footMark, new Size(3, 3), 0);
  83. destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "dtk7.png";
  84. Highgui.imwrite(destPath, footMark);
  85. logger.info("平滑处理后的底部定位点图======" + destPath);
  86. // 根据底部定位获取垂直投影,并获取横向坐标
  87. Mat matbootom = verticalProjection(footMark);
  88. destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "dtk8.png";
  89. Highgui.imwrite(destPath, matbootom);
  90. logger.info("底部垂直投影图======" + destPath);
  91. // 获取x坐标点,返回的是竖向的柱状图集合
  92. List<Rect> listx = getBlockRect(matbootom, 0, 0);
  93. // 高阶处理:增加HSV颜色查找,查找红色像素点
  94. Mat matRed = findColorbyHSV(sourceMat, 156, 180);
  95. destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "dtk9.png";
  96. Highgui.imwrite(destPath, matRed);
  97. logger.info("HSV找出红色像素点======" + destPath);
  98. Mat dstNoRed = new Mat(sourceMat1.rows(), sourceMat1.cols(), sourceMat1.type());
  99. dstNoRed = OpenCVUtil.dilation(sourceMat1);
  100. // Imgproc.threshold(sourceMat1, dstNoRed, 190, 255, Imgproc.THRESH_BINARY);
  101. destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "dtk10.png";
  102. Highgui.imwrite(destPath, dstNoRed);
  103. logger.info("原灰度图的图片======" + destPath);
  104. Photo.inpaint(dstNoRed, matRed, dstNoRed, 1, Photo.INPAINT_NS);
  105. // findBlackColorbyHSV(sourceMat);
  106. // for (int i = 0;i<dstNoRed.rows();i++) {
  107. // for (int j = 0; j < dstNoRed.cols(); j++) {
  108. // if(matRed.get(i, j)[0]==255){//代表识别出的红色区域
  109. // dstNoRed.put(i,j,255);
  110. // }
  111. // }
  112. // }
  113. destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "dtk11.png";
  114. Highgui.imwrite(destPath, dstNoRed);
  115. logger.info("去除红颜色后的图片======" + destPath);
  116. Mat grayHistogram1 = getGrayHistogram(dstNoRed);
  117. destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "dtk12.png";
  118. Highgui.imwrite(destPath, grayHistogram1);
  119. logger.info("灰度直方图图片1======" + destPath);
  120. destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "dtk13.png";
  121. Mat answerMat = dstNoRed.submat(new Rect(41, 895, 278, 133));
  122. Mat grayHistogram2 = getGrayHistogram(answerMat);
  123. Highgui.imwrite(destPath, grayHistogram2);
  124. logger.info("灰度直方图图片2======" + destPath);
  125. destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "dtk14.png";
  126. Imgproc.threshold(dstNoRed, dstNoRed, binary_thresh, 255, Imgproc.THRESH_BINARY_INV);
  127. Highgui.imwrite(destPath, dstNoRed);
  128. logger.info("去除红色基础上进行二值化======" + destPath);
  129. String redvalue = StringUtils.split(blue_red_thresh, ",")[0];
  130. String bluevalue = StringUtils.split(blue_red_thresh, ",")[1];
  131. System.out.println(bluevalue + " " + redvalue);
  132. TreeMap<Integer, String> resultMap = new TreeMap<Integer, String>();
  133. StringBuffer resultValue = new StringBuffer();
  134. for (int no = 0; no < listx.size(); no++) {
  135. Rect rectx = listx.get(no);
  136. for (int an = 0; an < listy.size(); an++) {
  137. Rect recty = listy.get(an);
  138. Mat selectdst = new Mat(dstNoRed, new Range(recty.y, recty.y + recty.height), new Range(rectx.x,
  139. rectx.x
  140. + rectx.width));
  141. // 本来是在每个区域内进行二值化,后来挪至了14步,整体进行二值化,因此注释掉此处2行
  142. // Mat selectdst = new Mat(select.rows(), select.cols(), select.type());
  143. // Imgproc.threshold(select, selectdst, 170, 255, Imgproc.THRESH_BINARY);
  144. // System.out.println("rectx.x, recty.y=="+rectx.x+","+recty.y+"rectx.width,recty.height=="+rectx.width+","+recty.height);
  145. double p100 = Core.countNonZero(selectdst) * 100 / (selectdst.size().area());
  146. String que_answer = getQA(no, an);
  147. Integer que = Integer.valueOf(que_answer.split("_")[0]);
  148. String answer = que_answer.split("_")[1];
  149. // System.out.println(Core.countNonZero(selectdst) + "/" + selectdst.size().area());
  150. System.out.println(que_answer + ": " + p100);
  151. if (p100 >= Integer.valueOf(bluevalue)) {// 蓝色
  152. Core.rectangle(sourceMat, new Point(rectx.x, recty.y), new Point(rectx.x + rectx.width, recty.y
  153. + recty.height), new Scalar(255, 0, 0), 2);
  154. // logger.info(que_answer + ":填涂");
  155. if (StringUtils.isNotEmpty(resultMap.get(que))) {
  156. resultMap.put(que, resultMap.get(que) + "," + answer);
  157. } else {
  158. resultMap.put(que, answer);
  159. }
  160. } else if (p100 > Integer.valueOf(redvalue) && p100 < Integer.valueOf(bluevalue)) {// 红色
  161. Core.rectangle(sourceMat, new Point(rectx.x, recty.y), new Point(rectx.x + rectx.width, recty.y
  162. + recty.height), new Scalar(0, 0, 255), 2);
  163. // logger.info(que_answer + ":临界");
  164. if (StringUtils.isNotEmpty(resultMap.get(que))) {
  165. resultMap.put(que, resultMap.get(que) + ",(" + answer + ")");
  166. } else {
  167. resultMap.put(que, "(" + answer + ")");
  168. }
  169. } else {// 绿色
  170. Core.rectangle(sourceMat, new Point(rectx.x, recty.y), new Point(rectx.x + rectx.width, recty.y
  171. + recty.height), new Scalar(0, 255, 0), 1);
  172. // logger.info(que_answer + ":未涂");
  173. }
  174. }
  175. }
  176. // for (Object result : resultMap.keySet()) {
  177. for (int i = 1; i <= 100; i++) {
  178. // logger.info("key=" + result + " value=" + resultMap.get(result));
  179. resultValue.append(" " + i + "=" + (StringUtils.isEmpty(resultMap.get(i)) ? "未填写" : resultMap.get(i)));
  180. if (i % 5 == 0) {
  181. resultValue.append("<br>");
  182. }
  183. }
  184. destPath = Constants.PATH + Constants.DEST_IMAGE_PATH + "dtk15.png";
  185. Highgui.imwrite(destPath, sourceMat);
  186. logger.info("框选填图区域,绿色为选项,蓝色为填图,红色为临界======" + destPath);
  187. long t2 = new Date().getTime();
  188. System.out.println(t2 - t1);
  189. // logger.info("输出最终结果:" + resultValue.toString());
  190. renderString(response, resultValue.toString());
  191. }
  192. /**
  193. * 绘制灰度直方图用于调整识别区域阈值判断
  194. * @Author 王嵩
  195. * @param 输入Mat对象img
  196. * @return Mat
  197. * @Date 2018年3月28日
  198. * 更新日志
  199. * 2018年3月28日 王嵩 首次创建
  200. *
  201. */
  202. public Mat getGrayHistogram(Mat img) {
  203. List<Mat> images = new ArrayList<Mat>();
  204. images.add(img);
  205. MatOfInt channels = new MatOfInt(0); // 图像通道数,0表示只有一个通道
  206. MatOfInt histSize = new MatOfInt(256); // CV_8U类型的图片范围是0~255,共有256个灰度级
  207. Mat histogramOfGray = new Mat(); // 输出直方图结果,共有256行,行数的相当于对应灰度值,每一行的值相当于该灰度值所占比例
  208. MatOfFloat histRange = new MatOfFloat(0, 255);
  209. Imgproc.calcHist(images, channels, new Mat(), histogramOfGray, histSize, histRange, false); // 计算直方图
  210. MinMaxLocResult minmaxLoc = Core.minMaxLoc(histogramOfGray);
  211. // 按行归一化
  212. // Core.normalize(histogramOfGray, histogramOfGray, 0, histogramOfGray.rows(), Core.NORM_MINMAX, -1, new Mat());
  213. // 创建画布
  214. int histImgRows = 600;
  215. int histImgCols = 1300;
  216. System.out.println("---------" + histSize.get(0, 0)[0]);
  217. int colStep = (int) Math.floor(histImgCols / histSize.get(0, 0)[0]);// 舍去小数,不能四舍五入,有可能列宽不够
  218. Mat histImg = new Mat(histImgRows, histImgCols, CvType.CV_8UC3, new Scalar(255, 255, 255)); // 重新建一张图片,绘制直方图
  219. int max = (int) minmaxLoc.maxVal;
  220. System.out.println("--------" + max);
  221. double bin_u = (double) (histImgRows - 20) / max; // max: 最高条的像素个数,则 bin_u 为单个像素的高度,因为画直方图的时候上移了20像素,要减去
  222. int kedu = 0;
  223. for (int i = 1; kedu <= minmaxLoc.maxVal; i++) {
  224. kedu = i * max / 10;
  225. // 在图像中显示文本字符串
  226. Core.putText(histImg, kedu + "", new Point(0, histImgRows - kedu * bin_u), 1, 1, new Scalar(0, 0, 0));
  227. }
  228. for (int i = 0; i < histSize.get(0, 0)[0]; i++) { // 画出每一个灰度级分量的比例,注意OpenCV将Mat最左上角的点作为坐标原点
  229. // System.out.println(i + ":=====" + histogramOfGray.get(i, 0)[0]);
  230. Core.rectangle(histImg, new Point(colStep * i, histImgRows - 20), new Point(colStep * (i + 1), histImgRows
  231. - bin_u * Math.round(histogramOfGray.get(i, 0)[0]) - 20),
  232. new Scalar(0, 0, 0), 1, 8, 0);
  233. kedu = i * 10;
  234. // 每隔10画一下刻度
  235. Core.rectangle(histImg, new Point(colStep * kedu, histImgRows - 20), new Point(colStep * (kedu + 1),
  236. histImgRows - 20), new Scalar(255, 0, 0), 2, 8, 0);
  237. Core.putText(histImg, kedu + "", new Point(colStep * kedu, histImgRows - 5), 1, 1, new Scalar(255, 0, 0)); // 附上x轴刻度
  238. }
  239. return histImg;
  240. }
  241. // 获取题号及选项填涂情况
  242. public String getQA(int no,int an){
  243. //返回1A、1B、1C...2A类似这样的返回值
  244. int first = no + 1 + an / 4 * 20;
  245. String second = "";
  246. if (an % 4 == 0) {
  247. second = "A";
  248. } else if (an % 4 == 1) {
  249. second = "B";
  250. } else if (an % 4 == 2) {
  251. second = "C";
  252. } else if (an % 4 == 3) {
  253. second = "D";
  254. }
  255. return first + "_" + second;
  256. }
  257. public static void main(String[] args) {
  258. System.out.println(5 / 3);
  259. }
  260. /**
  261. * 红色色系0-20,160-180
  262. * 蓝色色系100-120
  263. * 绿色色系60-80
  264. * 黄色色系23-38
  265. * 识别出的颜色会标记为白色,其他的为黑色
  266. * @param min
  267. * @param max
  268. */
  269. public static Mat findColorbyHSV(Mat source, int min, int max) {
  270. Mat hsv_image = new Mat();
  271. Imgproc.GaussianBlur(source, source, new Size(3, 3), 0, 0);
  272. Imgproc.cvtColor(source, hsv_image, Imgproc.COLOR_BGR2HSV);
  273. // String imagenameb = "D:\\test\\testImge\\ttbefore.jpg";
  274. // Highgui.imwrite(imagenameb, hsv_image);
  275. Mat thresholded = new Mat();
  276. Core.inRange(hsv_image, new Scalar(min, 90, 90), new Scalar(max, 255, 255), thresholded);
  277. return thresholded;
  278. }
  279. /**
  280. * 查找黑色
  281. * @param source
  282. * @param min
  283. * @param max
  284. * @return
  285. */
  286. public static Mat findBlackColorbyHSV(Mat source) {
  287. Mat hsv_image = new Mat();
  288. Imgproc.GaussianBlur(source, source, new Size(3, 3), 0, 0);
  289. Imgproc.cvtColor(source, hsv_image, Imgproc.COLOR_BGR2HSV);
  290. String imagenameb = "D:\\test\\testImge\\ttbefore.jpg";
  291. Highgui.imwrite(imagenameb, hsv_image);
  292. Mat thresholded = new Mat();
  293. Core.inRange(hsv_image, new Scalar(0, 0, 0), new Scalar(180, 255, 46), thresholded);
  294. String ttblack = "D:\\test\\testImge\\ttblack.jpg";
  295. Highgui.imwrite(ttblack, thresholded);
  296. return thresholded;
  297. }
  298. /**
  299. * 水平投影
  300. * @param source 传入灰度图片Mat
  301. * @return
  302. */
  303. public static Mat horizontalProjection(Mat source) {
  304. Mat dst = new Mat(source.rows(), source.cols(), source.type());
  305. // 先进行反转二值化
  306. Imgproc.threshold(source, dst, 150, 255, Imgproc.THRESH_BINARY_INV);
  307. // 水平积分投影
  308. // 每一行的白色像素的个数
  309. int[] rowswidth = new int[dst.rows()];
  310. for (int i = 0; i < dst.rows(); i++) {
  311. for (int j = 0; j < dst.cols(); j++) {
  312. if (dst.get(i, j)[0] == 255) {
  313. rowswidth[i]++;
  314. }
  315. }
  316. }
  317. // 定义一个白色跟原图一样大小的画布
  318. Mat matResult = new Mat(dst.rows(), dst.cols(), CvType.CV_8UC1, new Scalar(255, 255, 255));
  319. // 将每一行按照行像素值大小填充像素宽度
  320. for (int i = 0; i < matResult.rows(); i++) {
  321. for (int j = 0; j < rowswidth[i]; j++) {
  322. matResult.put(i, j, 0);
  323. }
  324. }
  325. return matResult;
  326. }
  327. /**
  328. * 垂直投影
  329. * @param source 传入灰度图片Mat
  330. * @return
  331. */
  332. public static Mat verticalProjection(Mat source) {
  333. // 先进行反转二值化
  334. Mat dst = new Mat(source.rows(), source.cols(), source.type());
  335. Imgproc.threshold(source, dst, 150, 255, Imgproc.THRESH_BINARY_INV);
  336. // 垂直积分投影
  337. // 每一列的白色像素的个数
  338. int[] colswidth = new int[dst.cols()];
  339. for (int j = 0; j < dst.cols(); j++) {
  340. for (int i = 0; i < dst.rows(); i++) {
  341. if (dst.get(i, j)[0] == 255) {
  342. colswidth[j]++;
  343. }
  344. }
  345. }
  346. Mat matResult = new Mat(dst.rows(), dst.cols(), CvType.CV_8UC1, new Scalar(255, 255, 255));
  347. // 将每一列按照列像素值大小填充像素宽度
  348. for (int j = 0; j < matResult.cols(); j++) {
  349. for (int i = 0; i < colswidth[j]; i++) {
  350. matResult.put(matResult.rows() - 1 - i, j, 0);
  351. }
  352. }
  353. return matResult;
  354. }
  355. /**
  356. * 图片切块
  357. * @param srcMat 传入水平或垂直投影的图片对象Mat
  358. * @param proType 传入投影Mat对象的 投影方式0:垂直投影图片,竖向切割;1:水平投影图片,横向切割
  359. * @param rowY 由于传来的是原始图片的部分切片,要计算切块的实际坐标位置需要给出切片时所在的坐标,所以需要传递横向切片的y坐标或者纵向切片的横坐标
  360. * 如当proType==0时,传入的是切片的垂直投影,那么切成块后能得出x坐标及块宽高度,但是实际y坐标需要加上原切片的y坐标值,所以rowXY为切片的y坐标点,
  361. * 同理当proType==1时,rowXY应该为x坐标
  362. * @return
  363. */
  364. public static List<Rect> getBlockRect(Mat srcImg, Integer proType, int rowXY) {
  365. Imgproc.threshold(srcImg, srcImg, 150, 255, Imgproc.THRESH_BINARY_INV);
  366. // 注意 countNonZero 方法是获取非0像素(白色像素)数量,所以一般要对图像进行二值化反转
  367. List<Rect> rectList = new ArrayList<Rect>();
  368. int size = proType == 0 ? srcImg.cols() : srcImg.rows();
  369. int[] pixNum = new int[size];
  370. if (proType == 0) {
  371. for (int i = 0; i < srcImg.cols(); i++) {
  372. Mat col = srcImg.col(i);
  373. pixNum[i] = Core.countNonZero(col) > 1 ? Core.countNonZero(col) : 0;
  374. }
  375. } else {// 水平投影只关注行
  376. for (int i = 0; i < srcImg.rows(); i++) {
  377. Mat row = srcImg.row(i);
  378. pixNum[i] = Core.countNonZero(row) > 1 ? Core.countNonZero(row) : 0;
  379. }
  380. }
  381. int startIndex = 0;// 记录进入字符区的索引
  382. int endIndex = 0;// 记录进入空白区域的索引
  383. boolean inBlock = false;// 是否遍历到了字符区内
  384. for (int i = 0; i < size; i++) {
  385. if (!inBlock && pixNum[i] != 0) {// 进入字符区,上升跳变沿
  386. inBlock = true;
  387. startIndex = i;
  388. } else if (pixNum[i] == 0 && inBlock) {// 进入空白区,下降跳变沿存储
  389. endIndex = i;
  390. inBlock = false;
  391. Rect rect = null;
  392. if (proType == 0) {
  393. rect = new Rect(startIndex, rowXY, (endIndex - startIndex), srcImg.rows());
  394. } else {
  395. rect = new Rect(rowXY, startIndex, srcImg.cols(), (endIndex - startIndex));
  396. }
  397. rectList.add(rect);
  398. }
  399. }
  400. return rectList;
  401. }
  402. }

一个基于BSD许可(开源)发行的跨平台计算机视觉库,它提供了一系列图像处理和计算机视觉方面很多通用算法。是研究图像处理技术的一个很不错的工具

Contributors (1)