
2004毕业于山东大学齐鲁软件学院,软件工程专业。主要专注于图像处理算法学习与研究,计算机视觉技术开发应用,深度学习在计算机视觉领域应用。两本书籍《Java数字图像处理-编程技巧与应用实践》、《OpenCV On Android编程实践》作者
使用TensorFlow进行简单的图像处理 概述 作为计算机视觉开发者,使用TensorFlow进行简单的图像处理是基本技能,而TensorFlow在tf.image包中支持对图像的常见的操作包括: 亮度调整 对比度调整 饱和度调整 图像采样插值放缩 色彩空间转换 Gamma校正 标准化 图像的读入与显示我们通过OpenCV来实现,这里需要注意一点,OpenCV中图像三个通道是BGR,如果你是通过tensorflow读取的话三个通过顺序是RGB。图像读取的代码如下: 1.opencv方式 src = cv.imread("D:/vcprojects/images/meinv.png") 2.tensorflow方式 jpg = tf.read_file("D:/vcprojects/images/yuan_test.png") img = tf.image.decode_jpeg(jpg, channels=3) 3.使用OpenCV显示图像 def show_image(image, title='input'): print("result : \n", image) cv.namedWindow(title, cv.WINDOW_AUTOSIZE) cv.imshow(title, image) cv.waitKey(0) cv.destroyAllWindows() 原图显示如下: 1.放缩图像 支持三种方式,分别是临界点插值、双线性插值与双立方插值,不过我发现在使用双立方插值的时候,tensorflow处理之后图像总是会出现一些噪点,这个算不算它的BUG - tf.image.resize_nearest_neighbor # 临界点插值 - tf.image.resize_bilinear # 双线性插值 - tf.image.resize_bicubic # 双立方插值算法 演示代码如下: src = cv.imread("D:/vcprojects/images/meinv.png") cv.imshow("input", src) h, w, depth = src.shape src = np.expand_dims(src, 0) print(src.shape) bi_image = tf.image.resize_bilinear(src, size=[h*2, w*2]) bi_image = tf.squeeze(bi_image) bi_result = sess.run(bi_image) bi_result = np.uint8(bi_result) show_image(bi_result,"bilinear-zoom") 显示图像如下: 2.图像亮度调整 图像亮度是图像基本属性之一,tensorflow支持两种方式API对图像亮度进行调整 - tf.image.adjust_brightness - tf.image.random_brightness 使用上述API的时候需要对图像进行维度添加为四维的tensor数据,完整的图像亮度调整的代码如下: src = cv.imread("D:/vcprojects/images/meinv.png") src = np.expand_dims(src, 0) brightness = tf.image.adjust_brightness(src, delta=.5) brightness = tf.squeeze(brightness) result = sess.run(brightness) result = np.uint8(result) show_image(result, "brightness demo") 显示图像如下: 3.图像对比度调整 图像对比度是图像基本属性之一,tensorflow支持两种方式API对图像对比度进行调整 - tf.image.adjust_contrast - tf.image.random_contrast 前面一种全局调整,后面一种方式是随机调整,对比度调整的代码演示如下: src = cv.imread("D:/vcprojects/images/meinv.png") src = np.expand_dims(src, 0) contrast = tf.image.adjust_contrast(src, contrast_factor=2.2) contrast = tf.squeeze(contrast) result = sess.run(contrast) result = np.uint8(result) show_image(result, "contrast demo") 显示图像如下: 4.图像gamma校正 伽玛校正就是对图像的伽玛曲线进行编辑,以对图像进行非线性色调编辑的方法,检出图像信号中的深色部分和浅色部分,并使两者比例增大,从而提高图像的对比度。相关API为: - tf.image.adjust_gamma 常见gamma的取值范围为0.05~5之间,tensorflow实现gamma校正的代码演示如下: src = cv.imread("D:/vcprojects/images/meinv.png") src = np.expand_dims(src, 0) contrast = tf.image.adjust_gamma(src, gain=1.0, gamma=4.2) contrast = tf.squeeze(contrast) result = sess.run(contrast) result = np.uint8(result) show_image(result, "gamma demo") 显示图像如下: 5.图像饱和度调整 图像饱和度是图像HSV色彩空间最常见的指标之一,通过调整图像饱和度可以得到更加自然光泽的图像,tensorflow中饱和度调整的API如下: tf.image.adjust_saturation 常见的饱和度调整范围在0~5之间取值即可,演示代码如下: src = cv.imread("D:/vcprojects/images/meinv.png") contrast = tf.image.adjust_saturation(src, saturation_factor=2.2) result = sess.run(contrast) result = np.uint8(result) show_image(result, "saturation demo") 这里要特别说明一下,饱和度调整不支持4D tensor对象,所以读入的RGB图像即可“`。无需再次进行维度增加操作。最终调整之后的演示图像如下: 6.图像标准化 这个在tensorflow中对图像数据训练之前,经常会进行此步操作,它跟归一化是有区别的。归一化的图像直方图不会改变,标准化会改变图像直方图分布,标准化API如下: - tf.image.per_image_standardization 图像标准化实现代码如下: src = cv.imread("D:/vcprojects/images/meinv.png") contrast = tf.image.per_image_standardization(src) result = sess.run(contrast) result = np.uint8(result) show_image(result, "standardization demo") 演示结果如下: 7.图像色彩空间转换 tensorflow支持常见图像色彩空间转换,包括RGB、HSV、灰度色彩空间,相关API如下: - tf.image.rgb_ to_hsv - tf.image.rgb_ to_grayscale - tf.image.hsv_ to_rgb 将图像从RGB色彩空间转换到灰度空间的代码演示如下: src = cv.imread("D:/vcprojects/images/meinv.png") gray = tf.image.rgb_to_grayscale(src) result = sess.run(gray) result = np.uint8(result) show_image(result, "gray - demo") 结果显示如下: 小结 tensorflow中还提供一些其他的图像操作相关API,比如裁剪、填充、随机调整亮度、对比度等,还有非最大信号压制等操作,感兴趣的可以自己进一步学习。 欢迎关注微信公众号 【OpenCV学堂】
图像各向异性滤波 各向异性概念 各向异性(英文名称:anisotropy)是指材料在各方向的力学和物理性能呈现差异的特性。晶体的各向异性即沿晶格的不同方向,原子排列的周期性和疏密程度不尽相同,由此导致晶体在不同方向的物理化学特性也不同,这就是晶体的各向异性。亦称“非均质性”。物体的全部或部分物理、化学等性质随方向的不同而各自表现出一定的差异的特性。即在不同的方向所测得的性能数值不同。对图像来说各向异性就是在每个像素点周围四个方向上梯度变化都不一样,滤波的时候我们要考虑图像的各向异性对图像的影响,而各向同性显然是说各个方向的值都一致,常见的图像均值或者高斯均值滤波可以看成是各向同性滤波。 各向异性滤波 是将图像看成物理学的力场或者热流场,图像像素总是向跟他的值相异不是很大的地方流动或者运动,这样那些差异大的地方(边缘)就得以保留,所以本质上各向异性滤波是图像边缘保留滤波器(EPF)。它在各个方向的扩散可以表示如下如下公式: 代码实现 #include <opencv2/opencv.hpp> #include <iostream> using namespace cv; using namespace std; float k = 15; float lambda = 0.25; int N = 20; void anisotropy_demo(Mat &image, Mat &result); int main(int argc, char** argv) { Mat src = imread("D:/vcprojects/images/example.png"); if (src.empty()) { printf("could not load image...\n"); return -1; } namedWindow("input image", CV_WINDOW_AUTOSIZE); imshow("input image", src); vector<Mat> mv; vector<Mat> results; split(src, mv); for (int n = 0; n < mv.size(); n++) { Mat m = Mat::zeros(src.size(), CV_32FC1); mv[n].convertTo(m, CV_32FC1); results.push_back(m); } int w = src.cols; int h = src.rows; Mat copy = Mat::zeros(src.size(), CV_32FC1); for (int i = 0; i < N; i++) { anisotropy_demo(results[0], copy); copy.copyTo(results[0]); anisotropy_demo(results[1], copy); copy.copyTo(results[1]); anisotropy_demo(results[2], copy); copy.copyTo(results[2]); } Mat output; normalize(results[0], results[0], 0, 255, NORM_MINMAX); normalize(results[1], results[1], 0, 255, NORM_MINMAX); normalize(results[2], results[2], 0, 255, NORM_MINMAX); results[0].convertTo(mv[0], CV_8UC1); results[1].convertTo(mv[1], CV_8UC1); results[2].convertTo(mv[2], CV_8UC1); Mat dst; merge(mv, dst); imshow("result", dst); imwrite("D:/result.png", dst); waitKey(0); return 0; } void anisotropy_demo(Mat &image, Mat &result) { int width = image.cols; int height = image.rows; // 四邻域梯度 float n = 0, s = 0, e = 0, w = 0; // 四邻域系数 float nc = 0, sc = 0, ec = 0, wc = 0; float k2 = k*k; for (int row = 1; row < height -1; row++) { for (int col = 1; col < width -1; col++) { // gradient n = image.at<float>(row - 1, col) - image.at<float>(row, col); s = image.at<float>(row + 1, col) - image.at<float>(row, col); e = image.at<float>(row, col - 1) - image.at<float>(row, col); w = image.at<float>(row, col + 1) - image.at<float>(row, col); nc = exp(-n*n / k2); sc = exp(-s*s / k2); ec = exp(-e*e / k2); wc = exp(-w*w / k2); result.at<float>(row, col) = image.at<float>(row, col) + lambda*(n*nc + s*sc + e*ec + w*wc); } } } 运行效果 好久没发啦,最近比较忙,但我会一直坚持发!
OpenCV实现手写体数字训练与识别 机器学习(ML)是OpenCV模块之一,对于常见的数字识别与英文字母识别都可以做到很高的识别率,完成这类应用的主要思想与方法是首选对训练图像数据完成预处理与特征提取,根据特征数据组成符合OpenCV要求的训练数据集与标记集,然后通过机器学习的KNN、SVM、ANN等方法完成训练,训练结束之后保存训练结果,对待检测的图像完成分割、二值化、ROI等操作之后,加载训练好的分类数据,就可以预言未知分类。 一:数据集 这里使用的数据集是mnist 手写体数字数据集、关于数据集的具体说明如下: 数据集名称 说明 train-images-idx3-ubyte.gz 训练图像28x28大小,6万张 train-labels-idx1-ubyte.gz 每张图像的数字标记,6万条 t10k-images-idx3-ubyte.gz 测试数据集、1万张图像28x28 t10k-labels-idx1-ubyte.gz 测试数据集标记,表示图像数字 上述数据集数据组成内部结构,图像是以灰度每个字节表示一个像素点的灰度值,图像的总数、宽与高的大小从开始位置读取,说明如下: 开始移位 类型 值 描述 0000 4字节int类型 0x00000803(2051) 魔数 0004 4字节int类型 60000 图像数目 0008 4字节int类型 28 图像高度 00012 4字节int类型 28 图像宽度 标记部分数据组成如下: 开始移位 类型 值 描述 0000 4字节int类型 0x00000801(2049) 魔数 0004 4字节int类型 60000 标记数目 0008 1字节ubyte ?? 对应图像数字 0009 1字节ubyte ?? 对应图像数字 - 读取图像数据集 Mat readImages(int opt) { int idx = 0; ifstream file; Mat img; if (opt == 0) { cout << "\n Training..."; file.open("D:/vcprojects/images/mnist/train-images.idx3-ubyte", ios::binary); } else { cout << "\n Test..."; file.open("D:/vcprojects/images/mnist/t10k-images.idx3-ubyte", ios::binary); } // check file if (!file.is_open()) { cout << "\n File Not Found!"; return img; } /* byte 0 - 3 : Magic Number(Not to be used) byte 4 - 7 : Total number of images in the dataset byte 8 - 11 : rows of each image in the dataset byte 12 - 15 : cols of each image in the dataset */ int magic_number = 0; int number_of_images = 0; int height = 0; int width = 0; file.read((char*)&magic_number, sizeof(magic_number)); magic_number = reverseDigit(magic_number); file.read((char*)&number_of_images, sizeof(number_of_images)); number_of_images = reverseDigit(number_of_images); file.read((char*)&height, sizeof(height)); height = reverseDigit(height); file.read((char*)&width, sizeof(width)); width = reverseDigit(width); Mat train_images = Mat(number_of_images, height*width, CV_8UC1); cout << "\n No. of images:" << number_of_images <<endl; Mat digitImg = Mat::zeros(height, width, CV_8UC1); for (int i = 0; i < number_of_images; i++) { int index = 0; for (int r = 0; r<height; ++r) { for (int c = 0; c<width; ++c) { unsigned char temp = 0; file.read((char*)&temp, sizeof(temp)); index = r*width + c; train_images.at<uchar>(i, index) = (int)temp; digitImg.at<uchar>(r, c) = (int)temp; } } if (i < 100) { imwrite(format("D:/vcprojects/images/mnist/images/digit_%d.png", i), digitImg); } } train_images.convertTo(train_images, CV_32FC1); return train_images; } 读取标记数据集 Mat readLabels(int opt) { int idx = 0; ifstream file; Mat img; if (opt == 0) { cout << "\n Training..."; file.open("D:/vcprojects/images/mnist/train-labels.idx1-ubyte"); } else { cout << "\n Test..."; file.open("D:/vcprojects/images/mnist/t10k-labels.idx1-ubyte"); } // check file if (!file.is_open()) { cout << "\n File Not Found!"; return img; } /* byte 0 - 3 : Magic Number(Not to be used) byte 4 - 7 : Total number of labels in the dataset */ int magic_number = 0; int number_of_labels = 0; file.read((char*)&magic_number, sizeof(magic_number)); magic_number = reverseDigit(magic_number); file.read((char*)&number_of_labels, sizeof(number_of_labels)); number_of_labels = reverseDigit(number_of_labels); cout << "\n No. of labels:" << number_of_labels << endl; Mat labels = Mat(number_of_labels, 1, CV_8UC1); for (long int i = 0; i<number_of_labels; ++i) { unsigned char temp = 0; file.read((char*)&temp, sizeof(temp)); //printf("temp : %d\n ", temp); labels.at<uchar>(i, 0) = temp; } labels.convertTo(labels, CV_32SC1); return labels; } 二:训练与测试 对上述数据集,我们不使用提取特征方式,而是采用纯像素数据作为输入,分别使用KNN与SVM对数据集进行训练与测试,比较他们最终的识别率。 KNN方式 KNN是最简单的机器学习方法、主要是计算目标与模型之间的空间向量距离得到最终预测分类结果。训练的代码如下: void knnTrain() { Mat train_images = readImages(0); Mat train_labels = readLabels(0); printf("\n read mnist train dataset successfully...\n"); Ptr<ml::KNearest> knn = ml::KNearest::create(); knn->setDefaultK(5); knn->setIsClassifier(true); Ptr<ml::TrainData> tdata = ml::TrainData::create(train_images, ml::ROW_SAMPLE, train_labels); knn->train(tdata); knn->save("D:/vcprojects/images/mnist/knn_knowledge.yml"); } 测试代码如下: void testMnist() { //Ptr<ml::SVM> svm = Algorithm::load<ml::SVM>("D:/vcprojects/images/mnist/knn_knowledge.yml"); // SVM-POLY - 98% Ptr<ml::KNearest> knn = Algorithm::load<ml::KNearest>("D:/vcprojects/images/mnist/knn_knowledge.yml"); // KNN - 97% Mat train_images = readImages(1); Mat train_labels = readLabels(1); printf("\n read mnist test dataset successfully...\n"); float total = train_images.rows; float correct = 0; Rect rect; rect.x = 0; rect.height = 1; rect.width = (28 * 28); for (int i = 0; i < total; i++) { int actual = train_labels.at<int>(i); rect.y = i; Mat oneImage = train_images(rect); //int digit = svm->predict(oneImage); Mat result; float predicted = knn->predict(oneImage, result); int digit = static_cast<int>(predicted); if (digit == actual) { correct++; } } printf("\n recognize rate : %.2f \n", correct / total); } SVM方式 SVM的全称是支掌向量机,本来是用来对数据进行二分类的预测与分析、后来扩展到可以对数据进行回归与多分类预测与分析,主要是把数据映射到高维数据空间、把靠近高维数据的部分称为支掌向量(SV)。SVM根据使用的核不同、参数不同,可以得到不同的分类与预测结果、所以在OpenCV中使用SVM做分类的时候,尽量推荐大家使用train_auto方法来训练、但是train_auto运行时间一般都会比较久,有时候可能长达数天。 三:应用 训练好的数据保存在本地,初始化加载,使用对象的识别方法就可以预测分类、进行对象识别。当然这么做,还需要对输入的手写数字图像进行二值化、分割、调整等预处理之后才可以传入进行预测。完整的步骤如下: 以下是两个测试图像识别结果: - 注意点: 最终要把图像Mat对象转换为CV_32FC1的灰度,否则可能报错! 欢迎继续关注本人博客,只分享干货!
基于OpenCV实现二维码发现与定位 在如今流行扫描的年代,应用程序实现二维码扫描检测与识别已经是应用程序的标配、特别是在移动端、如果你的应用程序不能自动发现检测二维码,自动定位二维码你都不好意思跟别人打招呼,二维码识别与解析基于ZXing包即可。难点就在于如何从画面中快速而准确的找到二维码区域,寻找到二维码三个匹配模式点。 一:二维码的结构与基本原理 标准的二维码结构如下: 特别要关注的是图中三个黑色正方形区域,它们就是用来定位一个二维码的最重要的三个区域,我们二维码扫描与检测首先要做的就是要发现这三个区域,如果找到这个三个区域,我们就成功的发现一个二维码了,就可以对它定位与识别了。二维码其它各个部分的说明如下: 三个角上的正方形区域从左到右,从上到下黑白比例为1:1:3:1:1。 不管角度如何变化,这个是最显著的特征,通过这个特征我们就可以实现二维码扫描检测与定位。 二:算法各部与输出 1. 首先把输入图像转换为灰度图像 2. 通过OTSU转换为二值图像 3. 对二值图像使用轮廓发现得到轮廓 4. 根据二维码三个区域的特征,对轮廓进行面积与比例过滤得到最终结果显示如下: 三:程序运行演示与代码实现 下面的图片左侧为原图、右侧为二维码定位结果 程序各个步骤完整源代码如下 #include <opencv2/opencv.hpp> #include <math.h> #include <iostream> using namespace cv; using namespace std; void scanAndDetectQRCode(Mat & image, int index); bool isXCorner(Mat &image); bool isYCorner(Mat &image); Mat transformCorner(Mat &image, RotatedRect &rect); int main(int argc, char** argv) { /*for (int i = 1; i < 25; i++) { Mat qrcode_image = imread(format("D:/gloomyfish/qrcode/%d.jpg", i)); scanAndDetectQRCode(qrcode_image, i); } return 0; */ Mat src = imread("D:/gloomyfish/qrcode_99.jpg"); if (src.empty()) { printf("could not load image...\n"); return -1; } namedWindow("input image", CV_WINDOW_AUTOSIZE); imshow("input image", src); Mat gray, binary; cvtColor(src, gray, COLOR_BGR2GRAY); imwrite("D:/gloomyfish/outimage/qrcode_gray.jpg", gray); threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU); imwrite("D:/gloomyfish/outimage/qrcode_binary.jpg", binary); // detect rectangle now vector<vector<Point>> contours; vector<Vec4i> hireachy; Moments monents; findContours(binary.clone(), contours, hireachy, RETR_LIST, CHAIN_APPROX_SIMPLE, Point()); Mat result = Mat::zeros(src.size(), CV_8UC3); for (size_t t = 0; t < contours.size(); t++) { double area = contourArea(contours[t]); if (area < 100) continue; RotatedRect rect = minAreaRect(contours[t]); // 根据矩形特征进行几何分析 float w = rect.size.width; float h = rect.size.height; float rate = min(w, h) / max(w, h); if (rate > 0.85 && w < src.cols/4 && h<src.rows/4) { printf("angle : %.2f\n", rect.angle); Mat qr_roi = transformCorner(src, rect); if (isXCorner(qr_roi) && isYCorner(qr_roi)) { drawContours(src, contours, static_cast<int>(t), Scalar(0, 0, 255), 2, 8); imwrite(format("D:/gloomyfish/outimage/contour_%d.jpg", static_cast<int>(t)), qr_roi); drawContours(result, contours, static_cast<int>(t), Scalar(255, 0, 0), 2, 8); } } } imshow("result", src); imwrite("D:/gloomyfish/outimage/qrcode_patters.jpg", src); waitKey(0); return 0; } 欢迎继续关注本博客,加入OpenCV学习群
这个是我们去年想做的一个项目,后来因为各种原因就此搁浅了。但是算法部分我已经把它基本成型了,对各种光线条件下都可以准确的找到嘴唇,提取唇形、然后通过色彩渲染自动变化颜色,实现各种颜色的口红实时渲染,基于OpenCV与Android NDK完成的算法演示。 算法的主要思路首先是通过人脸检测寻找到人脸区域,一旦找到之后就会使用跟踪算法对人脸部位进行跟踪、人脸检测算法可以选择Face++或者OpenCV自带的算法,然后对下部区域进行嘴唇检测,找到之后,选择不同光照下的嘴唇图像,提取ROI如下: 提取了超过1000张作为算法测试,根据这些提取得到上部唇形 进一步处理之后得到下部唇形状 对整个唇形数据处理之后得到 基本的命中率在99%左右,通过缓存等技术手段,达到实时不丢帧渲染,最终的渲染效果如下: 红色简单着色 蓝色简单着色 算法不足之处 渲染这块后来因为各种原因,没有去做,主要是没有钱继续下去,就成这个样子!但是整个基于OpenCV实现的移动端AR口红渲染整个实现步骤基本如此! 欢迎大家留言!!!
线性回归(Linear Regression) 梯度下降算法在机器学习方法分类中属于监督学习。利用它可以求解线性回归问题,计算一组二维数据之间的线性关系,假设有一组数据如下下图所示 其中X轴方向表示房屋面积、Y轴表示房屋价格。我们希望根据上述的数据点,拟合出一条直线,能跟对任意给定的房屋面积实现价格预言,这样求解得到直线方程过程就叫线性回归,得到的直线为回归直线,数学公式表示如下: 二:梯度下降 (Gradient Descent) 三:代码实现 数据读入 public List<DataItem> getData(String fileName) { List<DataItem> items = new ArrayList<DataItem>(); File f = new File(fileName); try { if (f.exists()) { BufferedReader br = new BufferedReader(new FileReader(f)); String line = null; while((line = br.readLine()) != null) { String[] data = line.split(","); if(data != null && data.length == 2) { DataItem item = new DataItem(); item.x = Integer.parseInt(data[0]); item.y = Integer.parseInt(data[1]); items.add(item); } } br.close(); } } catch (IOException ioe) { System.err.println(ioe); } return items; } 归一化处理 public void normalization(List<DataItem> items) { float min = 100000; float max = 0; for(DataItem item : items) { min = Math.min(min, item.x); max = Math.max(max, item.x); } float delta = max - min; for(DataItem item : items) { item.x = (item.x - min) / delta; } } 梯度下降 public float[] gradientDescent(List<DataItem> items) { int repetion = 1500; float learningRate = 0.1f; float[] theta = new float[2]; Arrays.fill(theta, 0); float[] hmatrix = new float[items.size()]; Arrays.fill(hmatrix, 0); int k=0; float s1 = 1.0f / items.size(); float sum1=0, sum2=0; for(int i=0; i<repetion; i++) { for(k=0; k<items.size(); k++ ) { hmatrix[k] = ((theta[0] + theta[1]*items.get(k).x) - items.get(k).y); } for(k=0; k<items.size(); k++ ) { sum1 += hmatrix[k]; sum2 += hmatrix[k]*items.get(k).x; } sum1 = learningRate*s1*sum1; sum2 = learningRate*s1*sum2; // 更新 参数theta theta[0] = theta[0] - sum1; theta[1] = theta[1] - sum2; } return theta; } 价格预言 public float predict(float input, float[] theta) { float result = theta[0] + theta[1]*input; return result; } 线性回归图 public void drawPlot(List<DataItem> series1, List<DataItem> series2, float[] theta) { int w = 500; int h = 500; BufferedImage plot = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); Graphics2D g2d = plot.createGraphics(); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setPaint(Color.WHITE); g2d.fillRect(0, 0, w, h); g2d.setPaint(Color.BLACK); int margin = 50; g2d.drawLine(margin, 0, margin, h); g2d.drawLine(0, h-margin, w, h-margin); float minx=Float.MAX_VALUE, maxx=Float.MIN_VALUE; float miny=Float.MAX_VALUE, maxy=Float.MIN_VALUE; for(DataItem item : series1) { minx = Math.min(item.x, minx); maxx = Math.max(maxx, item.x); miny = Math.min(item.y, miny); maxy = Math.max(item.y, maxy); } for(DataItem item : series2) { minx = Math.min(item.x, minx); maxx = Math.max(maxx, item.x); miny = Math.min(item.y, miny); maxy = Math.max(item.y, maxy); } // draw X, Y Title and Aixes g2d.setPaint(Color.BLACK); g2d.drawString("价格(万)", 0, h/2); g2d.drawString("面积(平方米)", w/2, h-20); // draw labels and legend g2d.setPaint(Color.BLUE); float xdelta = maxx - minx; float ydelta = maxy - miny; float xstep = xdelta / 10.0f; float ystep = ydelta / 10.0f; int dx = (w - 2*margin) / 11; int dy = (h - 2*margin) / 11; // draw labels for(int i=1; i<11; i++) { g2d.drawLine(margin+i*dx, h-margin, margin+i*dx, h-margin-10); g2d.drawLine(margin, h-margin-dy*i, margin+10, h-margin-dy*i); int xv = (int)(minx + (i-1)*xstep); float yv = (int)((miny + (i-1)*ystep)/10000.0f); g2d.drawString(""+xv, margin+i*dx, h-margin+15); g2d.drawString(""+yv, margin-25, h-margin-dy*i); } // draw point g2d.setPaint(Color.BLUE); for(DataItem item : series1) { float xs = (item.x - minx) / xstep + 1; float ys = (item.y - miny) / ystep + 1; g2d.fillOval((int)(xs*dx+margin-3), (int)(h-margin-ys*dy-3), 7,7); } g2d.fillRect(100, 20, 20, 10); g2d.drawString("训练数据", 130, 30); // draw regression line g2d.setPaint(Color.RED); for(int i=0; i<series2.size()-1; i++) { float x1 = (series2.get(i).x - minx) / xstep + 1; float y1 = (series2.get(i).y - miny) / ystep + 1; float x2 = (series2.get(i+1).x - minx) / xstep + 1; float y2 = (series2.get(i+1).y - miny) / ystep + 1; g2d.drawLine((int)(x1*dx+margin-3), (int)(h-margin-y1*dy-3), (int)(x2*dx+margin-3), (int)(h-margin-y2*dy-3)); } g2d.fillRect(100, 50, 20, 10); g2d.drawString("线性回归", 130, 60); g2d.dispose(); saveImage(plot); } 四:总结 本文通过最简单的示例,演示了利用梯度下降算法实现线性回归分析,使用更新收敛的算法常被称为LMS(Least Mean Square)又叫Widrow-Hoff学习规则,此外梯度下降算法还可以进一步区分为增量梯度下降算法与批量梯度下降算法,这两种梯度下降方法在基于神经网络的机器学习中经常会被提及,对此感兴趣的可以自己进一步探索与研究。 只分享干货,不止于代码
一:介绍 图像反向投影的最终目的是获取ROI然后实现对ROI区域的标注、识别、测量等图像处理与分析,是计算机视觉与人工智能的常见方法之一。图像反向投影通常是彩色图像投影效果会比灰度图像效果要好,原因在于彩色图像带有更多对象细节信息,在反向投影的时候更加容易判断、而转为灰度图像会导致这些细节信息丢失、从而导致分割失败。最常见的是基于图像直方图特征的反向投影。我们这里介绍一种跟直方图反向投影不一样的彩色图像反向投影方法,通过基于高斯的概率分布公式(PDF)估算,反向投影得到对象区域,该方法也可以看做最简单的图像分割方法。缺点是对象颜色光照改变和尺度改变不具备不变性特征。所以需要在光照度稳定情况下成像采集图像数据。在这种情况下使用的高斯概率密度公式为: 1.输入模型M,对M的每个像素点(R,G,B)计算I=R+G+B r=R/I, g=G/I, b=B/I 2. 根据得到权重比例值,计算得到对应的均值 与标准方差 3. 对输入图像的每个像素点计算根据高斯公式计算P(r)与P(g)的乘积 4. 归一化之后输出结果,即为最终基于高斯PDF的反向投影图像 二:代码实现 代码实现是基于OpenCV 3.2完成的C++代码,首先加载模型对象,计算出均值与标准方差以后,把均值与标准方差作为参数代入上述高斯概率分布公式,对每个像素点的每个通道值求取可能性值,然后求各个通道的可能性乘积作为该点的PDF,得到图像就是反向投影图像,以此为模板,可以得到分割图像。实现图像分割。完整的代码显示如下: #include <opencv2/opencv.hpp> #include <iostream> #include <math.h> using namespace cv; using namespace std; int main(int argc, char** argv) { Mat src = imread("D:/gloomyfish/gc_test.png"); Mat model = imread("D:/gloomyfish/gm.png"); if (src.empty() || model.empty()) { printf("could not load image...\n"); return -1; } imshow("input image", src); Mat R = Mat::zeros(model.size(), CV_32FC1); Mat G = Mat::zeros(model.size(), CV_32FC1); int r = 0, g = 0, b = 0; float sum = 0; for (int row = 0; row < model.rows; row++) { uchar* current = model.ptr<uchar>(row); for (int col = 0; col < model.cols; col++) { b = *current++; g = *current++; r = *current++; sum = b + g + r; R.at<float>(row, col) = r / sum; G.at<float>(row, col) = g / sum; } } Mat mean, stddev; double mr, devr; double mg, devg; meanStdDev(R, mean, stddev); mr = mean.at<double>(0, 0); devr = mean.at<double>(0, 0); meanStdDev(G, mean, stddev); mg = mean.at<double>(0, 0); devg = mean.at<double>(0, 0); int width = src.cols; int height = src.rows; float pr = 0, pg = 0; Mat result = Mat::zeros(src.size(), CV_32FC1); for (int row = 0; row < height; row++) { uchar* currentRow = src.ptr<uchar>(row); for (int col = 0; col < width; col++) { b = *currentRow++; g = *currentRow++; r = *currentRow++; sum = b + g + r; float red = r / sum; float green = g / sum; pr = (1 / (devr*sqrt(2 * CV_PI)))*exp(-(pow((red - mr), 2)) / (2 * pow(devr, 2))); pg = (1 / (devg*sqrt(2 * CV_PI)))*exp(-(pow((green - mg),2)) / (2 * pow(devg, 2))); sum = pr*pg; result.at<float>(row, col) = sum; } } Mat img(src.size(), CV_8UC1); normalize(result, result, 0, 255, NORM_MINMAX); result.convertTo(img, CV_8U); Mat segmentation; src.copyTo(segmentation, img); imshow("backprojection demo", img); imshow("segmentation demo", segmentation); waitKey(0); return 0; } 三:运行效果 模型图 原图 反向投影图像 最终得到分割出来的鲜花对象图像 欢迎继续关注本博客,只分享干货,不止于代码!
直方图反向投影算法介绍与实现 概念介绍 直方图反向投影简单的说就是可以通过它来实现图像分割,背景与对象分离,对已知对象位置进行定位。反向投影在模式匹配、对象识别、视频跟踪中均有应用,OpenCV中经典算法之一CAMeanShift就是基于反向投影实现对已知对象的位置查找与标记、从而达到连续跟踪。反向投影的概念第一次提出是在Michael.J.Swain与Dana H. Ballard的《Indexing via Color Histograms》论文中。 算法流程 直方图反向投影简单的说就是可以通过它来实现图像分割,背景与对象分离,对已知对假设模型图像的颜色直方图为M、目标图像颜色直方图为I、直方图反向投影首先通过计算比率 得到第三个直方图R=M/I,然后根据图像I(x, y)每个像素颜色值的索引查找R得到每个像素点直方图分布概率图像、对图像每个像素点得到I(x,y)=min(Rh(x,y), 1) ,对得到结果图像进行卷积计算,Mask大小默认为3x3或者5x5,形状一般情况下取圆形。对卷积之后的结果计算最大值所在位置即为对象所在位置。此时可以归一化为0~255之间输出图像即可。总结一下可以分为如下几步 1.计算模型的直方图M 2.计算目标图像的直方图I 3.用M除以I得到比率直方图R 4.循环每个像素点I(x, y)根据像素值映射成为直方图R的分布概率 5.卷积操作 6.归一化与现实输出 算法实现 计算图像的直方图 。这里需要注意的是输入的图像一般都是RGB图像,计算得到RGB直方图,需要对图像进行降维处理,常见是把255x255x255变为16x16x16大小,或者把RGB图像转换为HSV图像,计算H与S两个通道的直方图。 // 计算直方图 int[] input = new int[width*height]; float[] output = new float[width*height]; getRGB(src, 0, 0, width, height, input); int[] iHist = calculateHistorgram(input, width, height); 计算比率直方图 R float[] rHist = new float[iHist.length]; for(int i=0; i<iHist.length; i++) { float a = mHist[i]; float b = iHist[i]; rHist[i] = a / b; } 根据像素值查找R,得到分布概率权重 int index = 0; int bidx = 0; int tr=0, tg=0, tb=0; int level = 256 / bins; float[] rimage = new float[output.length]; for(int row=0; row<height; row++) { for(int col=0; col<width; col++) { index = row * width + col; tr = (input[index] >> 16) & 0xff; tg = (input[index] >> 8) & 0xff; tb = input[index] & 0xff; bidx = (tr / level) + (tg / level)*bins + (tb / level)*bins*bins; rimage[index] = Math.min(1, rHist[bidx]); } } 计算卷积,使用3x3的模板 int offset = 0; float sum = 0; System.arraycopy(rimage, 0, output, 0, output.length); for(int row=1; row<height-1; row++) { offset = width * row; for(int col=1; col<width-1; col++) { sum += rimage[offset+col]; sum += rimage[offset+col-1]; sum += rimage[offset+col+1]; sum += rimage[offset+width+col]; sum += rimage[offset+width+col-1]; sum += rimage[offset+width+col+1]; sum += rimage[offset-width+col]; sum += rimage[offset-width+col-1]; sum += rimage[offset-width+col+1]; output[offset+col] = sum / 9.0f; sum = 0f; // for next } } 归一化与显示 // 归一化 float min = 1000; float max = 0; for(int i=0; i<output.length; i++) { min = Math.min(min, output[i]); max = Math.max(max, output[i]); } float delta = max - min; for(int i=0; i<output.length; i++) { output[i] = ((output[i] - min)/delta)*255; } // 阈值显示 int[] out = new int[output.length]; for(int i=0; i<out.length; i++) { int pv = (int)output[i]; if(pv < 50) pv = 0; out[i] = (0xff << 24) | (pv << 16) | (pv << 8) | pv; } 模型图像与目标图像、运行效果 颜色模型图像 目标图像 根据颜色模型图像,实现直方图投影,可以准确的找到皇马队长水爷所在区域! 总结 直方图反向投影算法在图像处理与模式识别与匹配中是一种非常有效与常用的算法。这里代码实现是基于最开始提到的论文! **只分享干货,助你在人工智能与计算机视觉的道路上前进! 欢迎关注本博客与本人微信公众号【OpenCV学堂】**
程序员学好英语是伪命题 我写这篇文章的起因是因为在其它IT网站看到一篇文章上面讲程序员学英语如何重要,看完之后感觉如鲠在喉、不吐不快。感觉有些人已经是别有用心,是非颠倒,在他们眼中作为程序员英语的重要性已经远远超过技术。支撑他们理论最重要有三点 英语是计算机的母语,不懂英文更本无法学习编程!其实是翻看市面上所有的编程语言,那几十个英文关键字,任何人只要花两天时间都可以倒背如流,但是学习编程肯定是曲折漫长。这个也是那些人最大的谬论,我试问你学习数学,上面字母是阿拉伯字母、希腊字母,你怎么不学希腊文和阿拉伯文, 把这个作为程序员必须学习好英语真TMD的荒谬! 计算机所有的先进技术资料都是英文的,不懂英文更本没法学,这点看似无懈可击,但是仔细一想简直是TMD的扯淡,难道只有计算机相关资料是英文,其它学科都是中文,难道其它学科就不需要看懂英文资料吗? 程序员的基本能力应该是看懂技术资料,所以偏面强调程序员学英文重要性靠这条根本占不住脚。 沟通需要,在国内的IT公司中有一个很奇怪的现象,一个外国人跟一群中国人一起工作结果是大家都讲起了英文,看上起好像很高大尚、国际化团队,实质是自卑心理作祟。这个时候我们要大声讲中文才对,然后找个外语系的给翻译一下。让程序员舍本逐末去干翻译的事情,最终的结果是荒废了技术,翻译也没干好,哪天团队解散了,因为没技术工种都找不到,而他所谓的英文好,沟通好,放在国外连小学二年级学生都可以轻松超过他!说句不好听的话,有鸟用!这个理由是最具有欺骗性理由。这个也是很多职场资深人士感叹的十年外企一场空,除了会讲两句洋文之外,一无是处,本来就机会成为技术精英、却死抱职场三脚猫英语、甘愿成为所谓的外企白领! 下面分析一下哪些人会鼓吹程序员学习英语 技术上无所作为,是IT管理人员,这类人通常英文比普通程序员要好,为了在技术人员面前刷存在感,整体鼓吹英语对程序员的重要性。 一些职场培训机构,他们这么做是看重了程序员口袋中的钱。 公司老板,因为老板从节约成本角度出发,希望你既是技术人员,又可以做翻译,这样他就省钱开心死啦! 外资企业,因为老板都是讲英语,所以他希望减少沟通成本,希望手下技术人员跟他一样会讲英语,这个也是很多人最大的一个误区,其实老板不是想跟聊天,你只要会计算机专业词汇,专业术语回答问题就会八九不离十,不要担心老板说你听不懂,总有人会听懂,实在听不懂老板也会找翻译的,老板强迫你学习英语只能说明一个问题,他想降低成本,不想花钱!所以只有牺牲你了!这个时候你要看清楚,你是走技术路线还是就此告别码农生涯,专职忽悠! 但是程序员作为专业技术人员,偏面的学习英语最终的结果就是不伦不类、即做不了翻译,也写不好代码、辜负了青春、荒废了光阴! 等待他们的就是职场危机,到了30就要转行!不然就没法混的境地! 所以我个人认为作为程序员最核心的能力是技术能力,程序员要想法设法的提高自己的技术能力,少看BBC、VOA、说实话看的再多、听的再多,还是不会写程序!因为你跳槽的去一个新公司最看重还是你的核心能力-技术执行力,而不是讲两句洋文,做假洋鬼子! 此外程序员作为技术人员跟其他行业的技术人员一样,必须要懂行业英语,学习专业词汇、看懂专业论文与著作,这个是作为一个高级技术人员必须具备的技能之一。事实是越早明白这个道理,越早远离新东方、EF等英语培训机构,就可以越早的学好技术,增加自我价值,实现人生梦想,在我所知道和认识的CSDN博客专家中、他们中没有一个是因为英语好而成为博客专家,无一例外的都是技术领域的精英! 最后 如果你真的觉得英语重要,你可以做翻译,而不是做程序员! 作为程序员很多问题无法解决不是因为你外语不够好! 而是因为你技术没了! 不忘初心、方得始终!
图像处理之高斯混合模型 一:概述 高斯混合模型(GMM)在图像分割、对象识别、视频分析等方面均有应用,对于任意给定的数据样本集合,根据其分布概率, 可以计算每个样本数据向量的概率分布,从而根据概率分布对其进行分类,但是这些概率分布是混合在一起的,要从中分离出单个样本的概率分布就实现了样本数据聚类,而概率分布描述我们可以使用高斯函数实现,这个就是高斯混合模型-GMM。 这种方法也称为D-EM即基于距离的期望最大化。 三:算法步骤 1.初始化变量定义-指定的聚类数目K与数据维度D 2.初始化均值、协方差、先验概率分布 3.迭代E-M步骤 - E步计算期望 - M步更新均值、协方差、先验概率分布 -检测是否达到停止条件(最大迭代次数与最小误差满足),达到则退出迭代,否则继续E-M步骤 4.打印最终分类结果 四:代码实现 package com.gloomyfish.image.gmm; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * * @author gloomy fish * */ public class GMMProcessor { public final static double MIN_VAR = 1E-10; public static double[] samples = new double[]{10, 9, 4, 23, 13, 16, 5, 90, 100, 80, 55, 67, 8, 93, 47, 86, 3}; private int dimNum; private int mixNum; private double[] weights; private double[][] m_means; private double[][] m_vars; private double[] m_minVars; /*** * * @param m_dimNum - 每个样本数据的维度, 对于图像每个像素点来说是RGB三个向量 * @param m_mixNum - 需要分割为几个部分,即高斯混合模型中高斯模型的个数 */ public GMMProcessor(int m_dimNum, int m_mixNum) { dimNum = m_dimNum; mixNum = m_mixNum; weights = new double[mixNum]; m_means = new double[mixNum][dimNum]; m_vars = new double[mixNum][dimNum]; m_minVars = new double[dimNum]; } /*** * data - 需要处理的数据 * @param data */ public void process(double[] data) { int m_maxIterNum = 100; double err = 0.001; boolean loop = true; double iterNum = 0; double lastL = 0; double currL = 0; int unchanged = 0; initParameters(data); int size = data.length; double[] x = new double[dimNum]; double[][] next_means = new double[mixNum][dimNum]; double[] next_weights = new double[mixNum]; double[][] next_vars = new double[mixNum][dimNum]; List<DataNode> cList = new ArrayList<DataNode>(); while(loop) { Arrays.fill(next_weights, 0); cList.clear(); for(int i=0; i<mixNum; i++) { Arrays.fill(next_means[i], 0); Arrays.fill(next_vars[i], 0); } lastL = currL; currL = 0; for (int k = 0; k < size; k++) { for(int j=0;j<dimNum;j++) x[j]=data[k*dimNum+j]; double p = getProbability(x); // 总的概率密度分布 DataNode dn = new DataNode(x); dn.index = k; cList.add(dn); double maxp = 0; for (int j = 0; j < mixNum; j++) { double pj = getProbability(x, j) * weights[j] / p; // 每个分类的概率密度分布百分比 if(maxp < pj) { maxp = pj; dn.cindex = j; } next_weights[j] += pj; // 得到后验概率 for (int d = 0; d < dimNum; d++) { next_means[j][d] += pj * x[d]; next_vars[j][d] += pj* x[d] * x[d]; } } currL += (p > 1E-20) ? Math.log10(p) : -20; } currL /= size; // Re-estimation: generate new weight, means and variances. for (int j = 0; j < mixNum; j++) { weights[j] = next_weights[j] / size; if (weights[j] > 0) { for (int d = 0; d < dimNum; d++) { m_means[j][d] = next_means[j][d] / next_weights[j]; m_vars[j][d] = next_vars[j][d] / next_weights[j] - m_means[j][d] * m_means[j][d]; if (m_vars[j][d] < m_minVars[d]) { m_vars[j][d] = m_minVars[d]; } } } } // Terminal conditions iterNum++; if (Math.abs(currL - lastL) < err * Math.abs(lastL)) { unchanged++; } if (iterNum >= m_maxIterNum || unchanged >= 3) { loop = false; } } // print result System.out.println("=================最终结果================="); for(int i=0; i<mixNum; i++) { for(int k=0; k<dimNum; k++) { System.out.println("[" + i + "]: "); System.out.println("means : " + m_means[i][k]); System.out.println("var : " + m_vars[i][k]); System.out.println(); } } // 获取分类 for(int i=0; i<size; i++) { System.out.println("data[" + i + "]=" + data[i] + " cindex : " + cList.get(i).cindex); } } /** * * @param data */ private void initParameters(double[] data) { // 随机方法初始化均值 int size = data.length; for (int i = 0; i < mixNum; i++) { for (int d = 0; d < dimNum; d++) { m_means[i][d] = data[(int)(Math.random()*size)]; } } // 根据均值获取分类 int[] types = new int[size]; for (int k = 0; k < size; k++) { double max = 0; for (int i = 0; i < mixNum; i++) { double v = 0; for(int j=0;j<dimNum;j++) { v += Math.abs(data[k*dimNum+j] - m_means[i][j]); } if(v > max) { max = v; types[k] = i; } } } double[] counts = new double[mixNum]; for(int i=0; i<types.length; i++) { counts[types[i]]++; } // 计算先验概率权重 for (int i = 0; i < mixNum; i++) { weights[i] = counts[i] / size; } // 计算每个分类的方差 int label = -1; int[] Label = new int[size]; double[] overMeans = new double[dimNum]; double[] x = new double[dimNum]; for (int i = 0; i < size; i++) { for(int j=0;j<dimNum;j++) x[j]=data[i*dimNum+j]; label=Label[i]; // Count each Gaussian counts[label]++; for (int d = 0; d < dimNum; d++) { m_vars[label][d] += (x[d] - m_means[types[i]][d]) * (x[d] - m_means[types[i]][d]); } // Count the overall mean and variance. for (int d = 0; d < dimNum; d++) { overMeans[d] += x[d]; m_minVars[d] += x[d] * x[d]; } } // Compute the overall variance (* 0.01) as the minimum variance. for (int d = 0; d < dimNum; d++) { overMeans[d] /= size; m_minVars[d] = Math.max(MIN_VAR, 0.01 * (m_minVars[d] / size - overMeans[d] * overMeans[d])); } // Initialize each Gaussian. for (int i = 0; i < mixNum; i++) { if (weights[i] > 0) { for (int d = 0; d < dimNum; d++) { m_vars[i][d] = m_vars[i][d] / counts[i]; // A minimum variance for each dimension is required. if (m_vars[i][d] < m_minVars[d]) { m_vars[i][d] = m_minVars[d]; } } } } System.out.println("=================初始化================="); for(int i=0; i<mixNum; i++) { for(int k=0; k<dimNum; k++) { System.out.println("[" + i + "]: "); System.out.println("means : " + m_means[i][k]); System.out.println("var : " + m_vars[i][k]); System.out.println(); } } } /*** * * @param sample - 采样数据点 * @return 该点总概率密度分布可能性 */ public double getProbability(double[] sample) { double p = 0; for (int i = 0; i < mixNum; i++) { p += weights[i] * getProbability(sample, i); } return p; } /** * Gaussian Model -> PDF * @param x - 表示采样数据点向量 * @param j - 表示对对应的第J个分类的概率密度分布 * @return - 返回概率密度分布可能性值 */ public double getProbability(double[] x, int j) { double p = 1; for (int d = 0; d < dimNum; d++) { p *= 1 / Math.sqrt(2 * 3.14159 * m_vars[j][d]); p *= Math.exp(-0.5 * (x[d] - m_means[j][d]) * (x[d] - m_means[j][d]) / m_vars[j][d]); } return p; } public static void main(String[] args) { GMMProcessor filter = new GMMProcessor(1, 2); filter.process(samples); } } 结构类DataNode package com.gloomyfish.image.gmm; public class DataNode { public int cindex; // cluster public int index; public double[] value; public DataNode(double[] v) { this.value = v; cindex = -1; index = -1; } } 五:结果 这里初始中心均值的方法我是通过随机数来实现,GMM算法运行结果跟初始化有很大关系,常见初始化中心点的方法是通过K-Means来计算出中心点。大家可以尝试修改代码基于K-Means初始化参数,我之所以选择随机参数初始,主要是为了省事! 不炒作概念,只分享干货! 请继续关注本博客!
计算样本数据的方差, 标准方差与协方差 在图像处理中有时候会涉及计算图像像素数据的方差,标准方差与协方差等统计学属性作为中间数据。因此知道什么是方差、标准方差、协方差很重要。 二:代码实现 Java代码实现计算数据的方差,标准方差、协方差 package com.gloomyfish.image.gmm; public class CalculateVariance { public double mean(double[] data) { double sum = 0; int len = data.length; for(int i=0; i<len; i++) { sum += data[i]; } return sum / len; } public double variance(double[] data) { double mean = mean(data); double sum = 0; int len = data.length; double delta = 0; for(int i=0; i<len; i++) { delta = data[i] - mean; sum += (delta*delta); } return sum / len; } public double sd(double[] data) { return Math.sqrt(variance(data)); } public double covariance(double[] X, double[] Y) { double mx = mean(X); double my = mean(Y); // 理论上应该通过插值保证长度一致 int len = X.length == Y.length ? X.length : Math.min(X.length, Y.length); double sum = 0; for(int i=0; i<len; i++) { sum += ((X[i]-mx)*(Y[i]-my)); } return sum / len; } public static void main(String[] args) { int len = 20; double[] X = new double[len]; double[] Y = new double[len]; for(int i=0; i<len; i++) { X[i] = Math.random()*100; Y[i] = Math.random()*100; } CalculateVariance cv = new CalculateVariance(); System.out.println("X Mean: " + cv.mean(X)); System.out.println("Y Mean: " + cv.mean(Y)); System.out.println(); System.out.println("X Variance: " + cv.variance(X)); System.out.println("Y Variance: " + cv.variance(Y)); System.out.println(); System.out.println("X Standard Deviation: " + cv.sd(X)); System.out.println("Y Standard Deviation: " + cv.sd(Y)); System.out.println(); System.out.println("XY Covariance: " + cv.covariance(X, Y)); System.out.println(); } } 三:意义表述 协方差可以分析样本数据之间的线性相关性,协方差为正数时候,一般情况表示相关,协方差为负数的时候则表示不相关,常见的相关性计算就是基于协方差实现。在图像的直方图数据比较中是常规的手段之一。OpenCV与ImageJ中均有代码实现。
Android Studio上NDK编程步骤与演示 在AndroidStudio(AS)上搞NDK编程首先要下载与安装NDK,搞好了这步之后。只需要以下几步配置与操作就可以轻松开始NDK编程与运行。 新建一个纯Android项目(不包含C++支持) 在新项目中创建一个新Java文件为BitmapProcessor.java, 定义两个本地方法,代码实现如下: package com.gloomyfish.ndkdemo; import android.graphics.Bitmap; /** * Created by jia20003 on 2017/5/18. */ public class BitmapProcessor { static { System.loadLibrary("BitmapProcessor"); } public native void gray(Bitmap bm); public native void inverse(Bitmap bm); } 编译含有本地方法的Java文件和产生.h的C++文件 首先新建一个bat脚本文件javahrun.bat然后写入如下内容 set JAVA_HOME="C:\Program Files\Java\jdk1.8.0_92" set path=%JAVA_HOME%\bin;%path% set classpath=.;%classpath%;%JAVA_HOME%\lib;C:\Users\Administrator\AppData\Local\Android\sdk\platforms\android-24\android.jar javah com.gloomyfish.ndkdemo.BitmapProcessor 首先用javac BitmapProcessor.java这句替换脚本文件中的最后一句,然后保存该文件到项目所在的目录下app\src\main\java\com\gloomyfish\ndkdemo位置之后,通过cmd命令行执行javahrun.bat即可。 成功执行的话会生成一个.class文件在相同目录下面 然后退到目录app\src\main\java\下面,同时把bat文件修改为之前的脚本,直接执行就会生成.h的头文件,在app\src\main目录下新建jni与jniLIB两个目录文件夹之后,把.h头文件copy到jni文件夹中,这个时候在AS看到的目录结构应该如下: 新建一个C++源文件命名为BitmapProcessor.cpp, 实现头文件中定义与声明的两个本地方法即可,注意这个可以在VS2015中完成。设置一下头文件即可,如果你不知道该include多少个或者哪个,直接把目录%disk_dir%Android\android-ndk-r13b\platforms\android-24\arch-arm\usr\include下的全部添加到VS2015的include配置中即可。这样写完程序,确保没有C++的语法错误,然后copy到AS中。 编译含有C++的NDK项目 首先要设置一下NDK的路径, 右键项目打开Module Settings 然后设置好NDK路径即可。 其次,在app对应的build.gradle文件添加如下一段脚本 最后在gradle.properties文件中添加上一句话 android.useDeprecatedNdk=true 如果不加的话,编译就会又错误出现。 然后【build】->【clean build】, 【rebuild】 就编译好啦! 使用so库与编写java相关代码 首先把编译生成的app\build\intermediates\ndk\debug\lib下面的文件全部copy到之前建好的jniLIB里面即可。然后完成Java部分代码编写,实现用户交互与UI响应操作。 这里我通过JNI实现了在C++层对Bitmap对象的灰度化操作与颜色取反操作然后返回结果。运行效果如下: 相关Java与C++代码实现 MainActivity.java package com.gloomyfish.ndkdemo; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.ImageView; public class MainActivity extends AppCompatActivity implements View.OnClickListener{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button grayBtn = (Button)this.findViewById(R.id.gray_btn); Button invertBtn = (Button)this.findViewById(R.id.invert_btn); grayBtn.setOnClickListener(this); invertBtn.setOnClickListener(this); } @Override public void onClick(View v) { int id = v.getId(); switch (id) { case R.id.gray_btn: convert2Gray(); break; case R.id.invert_btn: invertImage(); break; default: break; } } private void invertImage() { Bitmap bm = BitmapFactory.decodeResource(this.getResources(), R.drawable.test); BitmapProcessor processor = new BitmapProcessor(); processor.inverse(bm); ImageView iv = (ImageView)this.findViewById(R.id.imageView_001); iv.setImageBitmap(bm); } private void convert2Gray() { Bitmap bm = BitmapFactory.decodeResource(this.getResources(), R.drawable.test); BitmapProcessor processor = new BitmapProcessor(); processor.gray(bm); ImageView iv = (ImageView)this.findViewById(R.id.imageView_001); iv.setImageBitmap(bm); } } BitmapProcessor.cpp // // Created by gloomy fish on 2017/5/18. // #include<android/bitmap.h> #include<android/log.h> #include<com_gloomyfish_ndkdemo_BitmapProcessor.h> #ifndef eprintf #define eprintf(...) __android_log_print(ANDROID_LOG_ERROR,"@",__VA_ARGS__) #endif #define RGBA_A(p) (((p) & 0xFF000000) >> 24) #define RGBA_R(p) (((p) & 0x00FF0000) >> 16) #define RGBA_G(p) (((p) & 0x0000FF00) >> 8) #define RGBA_B(p) ((p) & 0x000000FF) #define MAKE_RGBA(r,g,b,a) (((a) << 24) | ((r) << 16) | ((g) << 8) | (b)) JNIEXPORT void JNICALL Java_com_gloomyfish_ndkdemo_BitmapProcessor_gray (JNIEnv *env, jobject clazz, jobject bmpObj) { AndroidBitmapInfo bmpInfo={0}; if(AndroidBitmap_getInfo(env,bmpObj,&bmpInfo)<0) { eprintf("invalid bitmap\n"); return; } void * dataFromBmp = NULL; int res = AndroidBitmap_lockPixels(env, bmpObj, &dataFromBmp); if(dataFromBmp == NULL) { eprintf("could not retrieve pixels from bitmap\n"); return; } eprintf("Effect: %dx%d, %d\n", bmpInfo.width, bmpInfo.height, bmpInfo.format); int x = 0; int y = 0; int width = bmpInfo.width; int height = bmpInfo.height; for(y=0; y<height; y++) { for(x=0; x<width; x++) { int a = 0, r = 0, g = 0, b = 0; void *pixel = NULL; pixel = ((uint32_t *)dataFromBmp) + y * width + x; uint32_t v = *(uint32_t *)pixel; a = RGBA_A(v); r = RGBA_R(v); g = RGBA_G(v); b = RGBA_B(v); // Grayscale int gray = (r * 38 + g * 75 + b * 15) >> 7; // Write the pixel back *((uint32_t *)pixel) = MAKE_RGBA(gray, gray, gray, a); } } AndroidBitmap_unlockPixels(env, bmpObj); } JNIEXPORT void JNICALL Java_com_gloomyfish_ndkdemo_BitmapProcessor_inverse (JNIEnv *env, jobject clazz, jobject bmpObj) { AndroidBitmapInfo bmpInfo={0}; if(AndroidBitmap_getInfo(env,bmpObj,&bmpInfo)<0) { eprintf("invalid bitmap\n"); return; } void * dataFromBmp = NULL; int res = AndroidBitmap_lockPixels(env, bmpObj, &dataFromBmp); if(dataFromBmp == NULL) { eprintf("could not retrieve pixels from bitmap\n"); return; } eprintf("Effect: %dx%d, %d\n", bmpInfo.width, bmpInfo.height, bmpInfo.format); int x = 0; int y = 0; int width = bmpInfo.width; int height = bmpInfo.height; for(y=0; y<height; y++) { for(x=0; x<width; x++) { int a = 0, r = 0, g = 0, b = 0; void *pixel = NULL; pixel = ((uint32_t *)dataFromBmp) + y * width + x; uint32_t v = *(uint32_t *)pixel; a = RGBA_A(v); r = RGBA_R(v); g = RGBA_G(v); b = RGBA_B(v); // 取反操作 r = 255 - r; g = 255 - g; b = 255 - b; // Write the pixel back *((uint32_t *)pixel) = MAKE_RGBA(r, g, b, a); } } AndroidBitmap_unlockPixels(env, bmpObj); } PS: 我之所有把图像处理相关的都整到C++这一层来,是因为Android Studio 上用Java干这个事情真TMD太慢了,唯一C++才是唯一出路,想做AR或者图像处理与视频分析必须要学会此技能!
Apache HTTP配置反向代理入门 反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器或者外部网络上其它IP地址服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个服务器。 配置反向代理 客户端通过Internet请求HTTP页面,当请求到达Apache代理网关服务器, 代理服务器根据URL请求地址,转发到对应的服务器上,获取内容返回给代理服务器,从而返回给客户端。 Apache支持反向代理配置,只需要如下几步, 第一步 - 下载与安装 下载apache-http服务器,我下载的是这个httpd-2.2.25-win32-x86-openssl-0.9.8y.msi, 我也看到很多人下载了源码,然后自己编译,本人C++水平有限,所以还是选择了直接下载installer, 双击开始安装,需要填写域名与服务器名称,如果有就真实填写,如果没有均填写localhost,邮件地址随便写一个,安装结束之后,打开浏览器,输入安装时候填写的域名,结果显示如下: 说明apache htt安装成功。 第二步:配置代理映射 到安装好的apache文件目录conf文件下,找到httpd.conf文件 找到如下配置,去掉#可以启动HTTP反向代理功能 LoadModule proxy_module modules/mod_proxy.so LoadModule proxy_http_module modules/mod_proxy_http.so 在文件最后加上如下一段 我是下载了Tomcat, 启动Tomcat,然后通过apache访问Tomcat/webapps下面自带的examples,配置的代理映射如上。重启apache之后,访问jsp样例显示在浏览器如下: 如果还有其它的地址需要映射,直接按行在添加在之间即可
基于一维级联快速膨胀与腐蚀算法 一:基本原理 膨胀与腐蚀是图像形态学两个基本操作之一,传统的代码实现都是基于二维窗口卷积模式,对于正常的3x3窗口要八次与运算,而基于一维级联方式先X方向后Y方向只需要4次与运算即可。对于结构元素比较大的矩形来说,我们还可以通过连续的3x3的级联腐蚀或者膨胀来替代,假设对于11x11窗口大小腐蚀来说,正常的计算需要120次的与操作,而通过一维级联腐蚀只需要在X方向10次与操作,Y方向10次与操作,总计2x10=20次与操作即可实现。这样就极大的提高了二值图像腐蚀与膨胀的计算效率。图示如下: 二:代码实现 快速版本 package com.gloomyfish.ii.demo; public class FastErode extends AbstractByteProcessor { private byte[] data; private int radius; // must be odd public FastErode() { this.radius = 25; } public void setRadius(int radius) { this.radius = radius; if(radius % 2 == 0) { throw new RuntimeException("invalid parameters"); } } public void setData(byte[] data) { this.data = data; } @Override public void process(int width, int height) { int size = width*height; byte[] output = new byte[size]; System.arraycopy(data, 0, output, 0, size); // X Direction int xr = radius/2; byte c = (byte)0; int offset = 0; for(int row=0; row<height; row++) { for(int col=0; col<width; col++) { c = data[row*width+col]; if((c&0xff) == 0)continue; for(int x=-xr; x<=xr; x++) { if(x==0)continue; offset = x + col; if(offset < 0) { offset = 0; } if(offset >= width) { offset = width - 1; } c &=data[row*width+offset]; } if(c == 0){ output[row*width+col] = (byte)0; } } } System.arraycopy(output, 0, data, 0, size); // Y Direction int yr = radius/2; c = 0; offset = 0; for(int col=0; col<width; col++) { for(int row=0; row<height; row++) { c = data[row*width+col]; if((c&0xff) == 0)continue; for(int y=-yr; y<=yr; y++) { if(y == 0)continue; offset = y + row; if(offset < 0) { offset = 0; } if(offset >= height) { offset = height - 1; } c &=data[offset*width+col]; } if(c == 0){ output[row*width+col] = (byte)0; } } } System.arraycopy(output, 0, data, 0, size); } } 传统版本 @Override public void process(int width, int height) { int size = width*height; byte[] output = new byte[size]; IntIntegralImage grayii = new IntIntegralImage(); grayii.setImage(data); grayii.process(width, height); int yr = radius/2; int xr = radius/2; System.arraycopy(data, 0, output, 0, size); byte c = 0; int nx=0, ny=0; for(int row=0; row<height; row++) { for(int col=0; col<width; col++) { c = data[row*width+col]; if(c == 0)continue; for(int y=-yr; y<=yr; y++) { ny = y + row; if(ny < 0 || ny >= height){ ny = 0; } for(int x=-xr; x<=xr; x++) { nx = x+col; if(nx < 0 || nx >= width) { nx = 0; } c &= data[ny*width+nx]&0xff; } } if(c == 0) { output[row*width+col] = (byte)0; } } } System.arraycopy(output, 0, data, 0, size); } 三:耗时比较 对一张大小为381x244大小二值图像一维快速与传统腐蚀操作耗时比较结果如下(Win64,CPU i5, JDK8 64位): 无论是卷积还是高斯模糊,还是形态学操作,理论上都是卷积计算,但是在实际编码过程中基于对计算耗时考虑都是进行了各种有效变换从而提高计算速度与减少执行时间,所以对于任何看似简单的图像操作,所以理论一定要联系实践!不然长期如此的结果就是眼高手低! 愿与各位共勉!祝各位五一劳动节快乐! 请继续关注本博客与本人微信公众号! 专注图像处理与OpenCV学习!
Java使用OpenCV3.2实现视频读取与播放 OpenCV从3.x版本开始其JAVA语言的SDK支持视频文件读写,这样就极大的方便了广大Java语言开发者学习与使用OpenCV,通过摄像头或者视频文件读取帧的内容与播放,完成视频内容分析与对象跟踪等各种应用开发任务。可以说OpenCV C++ SDK可以做到绝大多数事情,在OpenCV3.x版本上用Java都可以完成,这样就为很多Java开发者学习OpenCV打开了方便之门。 实现思路 首先用OpenCV相关API读取视频流或者视频文件的每一帧,然后通过Swing JComponent组件实现视频每一帧的更新显示,我模仿了C++的HIGHGUI里面创建窗口与显示图像接口,基于Swing实现了一个视频播放窗口类,把读取到的每一帧都传给它就可以实现连续显示即播放。每帧之间相隔100毫秒,我是通过Java线程Sleep方法实现。 运行效果 - USB摄像头读取每帧 运行效果 - 视频文件读取每帧 代码实现 视频文件读取 package com.gloomyfish.video.demo; import java.awt.Dimension; import java.awt.image.BufferedImage; import org.opencv.core.Core; import org.opencv.core.Mat; import org.opencv.videoio.VideoCapture; public class VideoDemo { public static void main(String[] args) { System.loadLibrary(Core.NATIVE_LIBRARY_NAME); // 打开摄像头或者视频文件 VideoCapture capture = new VideoCapture(); //capture.open(0); capture.open("D:/vcprojects/images/768x576.avi"); if(!capture.isOpened()) { System.out.println("could not load video data..."); return; } int frame_width = (int)capture.get(3); int frame_height = (int)capture.get(4); ImageGUI gui = new ImageGUI(); gui.createWin("OpenCV + Java视频读与播放演示", new Dimension(frame_width, frame_height)); Mat frame = new Mat(); while(true) { boolean have = capture.read(frame); Core.flip(frame, frame, 1);// Win上摄像头 if(!have) break; if(!frame.empty()) { gui.imshow(conver2Image(frame)); gui.repaint(); } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } public static BufferedImage conver2Image(Mat mat) { int width = mat.cols(); int height = mat.rows(); int dims = mat.channels(); int[] pixels = new int[width*height]; byte[] rgbdata = new byte[width*height*dims]; mat.get(0, 0, rgbdata); BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); int index = 0; int r=0, g=0, b=0; for(int row=0; row<height; row++) { for(int col=0; col<width; col++) { if(dims == 3) { index = row*width*dims + col*dims; b = rgbdata[index]&0xff; g = rgbdata[index+1]&0xff; r = rgbdata[index+2]&0xff; pixels[row*width+col] = ((255&0xff)<<24) | ((r&0xff)<<16) | ((g&0xff)<<8) | b&0xff; } if(dims == 1) { index = row*width + col; b = rgbdata[index]&0xff; pixels[row*width+col] = ((255&0xff)<<24) | ((b&0xff)<<16) | ((b&0xff)<<8) | b&0xff; } } } setRGB( image, 0, 0, width, height, pixels); return image; } /** * A convenience method for setting ARGB pixels in an image. This tries to avoid the performance * penalty of BufferedImage.setRGB unmanaging the image. */ public static void setRGB( BufferedImage image, int x, int y, int width, int height, int[] pixels ) { int type = image.getType(); if ( type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB ) image.getRaster().setDataElements( x, y, width, height, pixels ); else image.setRGB( x, y, width, height, pixels, 0, width ); } } 视频与图像显示窗口类 package com.gloomyfish.video.demo; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import javax.swing.JComponent; import javax.swing.JDialog; public class ImageGUI extends JComponent { /** * */ private static final long serialVersionUID = 1L; private BufferedImage image; public ImageGUI() { } @Override protected void paintComponent(Graphics g) { Graphics2D g2d = (Graphics2D)g; if(image == null) { g2d.setPaint(Color.BLACK); g2d.fillRect(0, 0, this.getWidth(), this.getHeight()); } else { g2d.drawImage(image, 0, 0, this.getWidth(), this.getHeight(), null); System.out.println("show frame..."); } } public void createWin(String title) { JDialog ui = new JDialog(); ui.setTitle(title); ui.getContentPane().setLayout(new BorderLayout()); ui.getContentPane().add(this, BorderLayout.CENTER); ui.setSize(new Dimension(330, 240)); ui.setVisible(true); } public void createWin(String title, Dimension size) { JDialog ui = new JDialog(); ui.setTitle(title); ui.getContentPane().setLayout(new BorderLayout()); ui.getContentPane().add(this, BorderLayout.CENTER); ui.setSize(size); ui.setVisible(true); } public void imshow(BufferedImage image) { this.image = image; this.repaint(); } } 关注公众号,获取更多图像处理知识与OpenCV相关知识 【gloomyfish-贾志刚】
UMat对象起源 OpenCV3中引入了一个新的图像容器对象UMat,它跟Mat有着多数相似的功能和相同的API函数,但是代表的意义却太不一样。要说到UMat对象的来龙去脉,必须首先从OpenCL来开始说,OpenCL是一个面向异构系统通用的并行编程标准,这个标准最早是苹果公司提出,后来变成了一个国际标准,目的是通过它开发通用的GPU计算软件,中国的华为是该标准的成员之一。说的直白点就是如果CPU或者GPU支持OpenCL标准,就可以通过OpenCL相关编程实现使用GPU计算。OpenCV2.x开始支持它,不过那个时候这个功能很不好用,大致一般正常基于CPU的读写视频一帧图像代码如下: cv::Mat inMat, outMat; vidInput >> inMat; cv::cvtColor(inMat, outMat, cv::COLOR_RGB2GRAY); vidOutput << outMat; 基于OpenCL的GPU方式读写视频一帧图像代码如下: cv::Mat inMat, outMat; vidInput >> inMat; cv::ocl::oclMat inOclMat(inMat); cv::ocl::oclMat outOclMat; cv::ocl::cvtColor(inOclMat, outOclMat, cv::COLOR_RGB2GRAY); outMat = outOclMat; vidOutput << outMat; 上述代码通过添加ocl前缀空间实现OpenCL支持设备的GPU运算能力提高。但是上述代码在不支持OpenCL的平台上还会运行失败,使用起来及其不方便。对开发者来说不是统一API和底层透明。 于是OpenCV在3.0版本中开始引入了T-API的设计理念,即通过设计一套对开发者来说底层透明,接口统一的API调用方式,避免由于系统不支持OpenCL而导致程序运行失败,这个就是UMat图像容器类型。通过使用UMat对象,OpenCV会自动在支持OpenCL的设备上使用GPU运算,在不支持OpenCL的设备仍然使用CPU运算,这样就避免了程序运行失败,而且统一了接口。上述代码在OpenCV3中使用UMat改下如下: cv::UMat inMat, outMat; vidInput >> inMat; cv::cvtColor(inMat, outMat, cv::COLOR_RGB2GRAY); vidOutput << outMat; 这样就无需像OpenCV2中那样通过显式声明的调用方式。很明显UMat与Mat极其类似。而且两者之间是可以相互转换的。 Mat与UMat相互转换 从UMat中获取Mat对象使用UMat的get方法UMat::getMat(int access_flags)支持的FLAG如下: ACCESS_READ ACCESS_WRITE ACCESS_RW ACCESS_MASK ACCESS_FAST 最常用的就是读写,注意当使用这种方式的时候UMat对象将会被LOCK直到CPU使用获取Mat对象完成操作,销毁临时Mat对象之后,UMat才可以再被使用。 把Mat转换为UMat 通过Mat::getUMat()之后就获取一个UMat对象,同样在UMat对象操作期间,作为父对象Mat也会被LOCK直到子对象UMat销毁之后才可以继续使用。 OpenCV的官方文档说不鼓励在一个方法和一段代码中同时使用Mat与UMat两种方式,因为这样做真的非常危险。此外Mat与UMat还可以相互拷贝,但是这种方式也不是OpenCV官方提倡与推荐的,所以尽量别用这种方式。 一个同UMat读取视频并灰度化完整的例子 #include <opencv2/opencv.hpp> #include <opencv2/tracking.hpp> #include <iostream> using namespace cv; using namespace std; int main(int argc, char** argv) { VideoCapture capture; capture.open("D:/vcprojects/images/sample.mp4"); if (!capture.isOpened()) { printf("could not load video data...\n"); return -1; } // UMat方式读取视频,转为灰度显示-自动启用GPU计算 // 如果显卡支持OpenCL UMat frame, gray; namedWindow("UMat Demo", CV_WINDOW_AUTOSIZE); while (capture.read(frame)) { cvtColor(frame, gray, COLOR_BGR2GRAY); imshow("UMat Demo", gray); char c = waitKey(100); if (c == 27) { break; } } // 释放资源 capture.release(); waitKey(0); return 0; } 特别注意 代码基于VS2015与OpenCV3.1实现,欢迎大家继续关注本人博客!分享有用实用的图像处理技术与OpenCV相关技术文章,本人会用不停止!!!
一:准备 前几天在写代码的时候发现周围有人都换到了OpenCV3.2上面去啦,我当时就把OpenCV3.1包给删啦,立马下载OpenCV3.2,下载地址在这 里:http://opencv.org/opencv-3-2.html。 选择Windows自解压的那个连接点击进去即可下载OpenCV3.2的Windows版本。下载以后解压缩到指定目录即可。 扩展模块下载地址 https://github.com/opencv/opencv_contrib 同样下载好之后先解压缩到指定目录即可。 然后就可以下载CMake了,我用的是CMake3.7.2这个版本,貌似不是最新版本,大家可以下载最新版本。下载安装好了之后就可以开始编译了。 二:编译OpenCV3.2 说一下机器环境 Win764位 + VS2015。 首先要打开CMake GUI然后设置好源代码路径与编译路径,显示如下: 点击【configure】之后会弹出对话框,让你选择编译的位数与版本,记得一定选择VS2015 + Win64的,(当然要根据实际情况来),选择好啦显示如下: 点击【Finish】就会开始配置编译,如果一切顺利就会看到如下界面 在一堆红色区域的Name列对应有一个是设置扩展模块路径的额,看下图的蓝色矩形框,选择设置好即可。 设置好OpenCV扩展模块的路径之后再次点击【configure】按钮。如果一切正常结束之后再点击【generate】按钮。结束之后CMake就编译好啦。显然如下图: 然后在设置的编译路径上D:\opencv3.2\opencv\newbuild目录里面会有个OpenCV.sln文件,双击就可以在VS2015中打开找到 -CMakeTargets->INSTALL右键在弹出的菜单中选择生成即可 如果一切OK,就会生成install目录,以我本机的目录结构为例 D:\opencv3.2\opencv\newbuild\install 点击进去,配置好VS2015之后即可使用。 上面说的是理论应该这样顺利,但是实际不是这么回事情。 几个要注意坑 坑一: CMake的时候报Download错误与MD5文件校验错误,愿意是因为OpenCV3.2中会去下载谷歌的protobuff和TensorFlow相关第三方程序,结果下载不了,网络就挂啦!原因是OpenCV3.2集成了深度学习框架TensorFlow相关的接口。 坑二 找不到ippicvmt.lib,我也不知道怎么会事情,OpenCV3.2居然没有它编译放到install/lib里面去,而是在第三方的那个目录下面lib里面,所以我手动copy了放到一起。 坑三 我在正常配置之后,在Tracking模块中发现selectROI函数居然用不了,搞了半天,是因为tracking.hpp居然没有把它作为头文件包含进来,这个跟OpenCV3.2的教程上有点不一致。所以我手动包含了一下。最终我的OpenCV3.2+VS2015的配置搞好啦! 测试程序运行结果:
OpenCV概述 OpenCV做为功能强大的计算机视觉开源框架,包含了500多个算法实现,而且还在不断增加,其最新版本已经更新到3.2。其SDK支持Android与Java平台开发,对于常见的图像处理需求几乎都可以满足,理应成为广大Java与Android程序员的首先的图像处理框架。Java中使用OpenCV的配置及其简单,可以毫不客气的说几乎是零配置都可以。 一:配置 配置引入OpenCV相关jar包,首先要下载OpenCV的自解压版本,下载地址: http://opencv.org/opencv-3-2.html 然后拉到网页的最下方,下载Windows自解压开发包 下载好了双击解压缩之后找到build路径,显示如下: 双击打开Java文件夹, 里面有一个jar直接导入到Eclipse中的新建项目中去, 然后把x64里面的dll文件copy到Eclipse中使用的Java JDK bin和jre/bin目录下面即可。环境就配置好啦,简单吧!配置好的最终项目结构: 二:加载图像与像素操作 读入一张图像 -, 一句话搞定 Mat src = Imgcodecs.imread(imageFilePath); if(src.empty()) return; 将Mat对象转换为BufferedImage对象 public BufferedImage conver2Image(Mat mat) { int width = mat.cols(); int height = mat.rows(); int dims = mat.channels(); int[] pixels = new int[width*height]; byte[] rgbdata = new byte[width*height*dims]; mat.get(0, 0, rgbdata); BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); int index = 0; int r=0, g=0, b=0; for(int row=0; row<height; row++) { for(int col=0; col<width; col++) { if(dims == 3) { index = row*width*dims + col*dims; b = rgbdata[index]&0xff; g = rgbdata[index+1]&0xff; r = rgbdata[index+2]&0xff; pixels[row*width+col] = ((255&0xff)<<24) | ((r&0xff)<<16) | ((g&0xff)<<8) | b&0xff; } if(dims == 1) { index = row*width + col; b = rgbdata[index]&0xff; pixels[row*width+col] = ((255&0xff)<<24) | ((b&0xff)<<16) | ((b&0xff)<<8) | b&0xff; } } } setRGB( image, 0, 0, width, height, pixels); return image; } 将BufferedImage对象转换为Mat对象 public Mat convert2Mat(BufferedImage image) { int width = image.getWidth(); int height = image.getHeight(); Mat src = new Mat(new Size(width, height), CvType.CV_8UC3); int[] pixels = new int[width*height]; byte[] rgbdata = new byte[width*height*3]; getRGB( image, 0, 0, width, height, pixels ); int index = 0, c=0; int r=0, g=0, b=0; for(int row=0; row<height; row++) { for(int col=0; col<width; col++) { index = row*width + col; c = pixels[index]; r = (c&0xff0000)>>16; g = (c&0xff00)>>8; b = c&0xff; index = row*width*3 + col*3; rgbdata[index] = (byte)b; rgbdata[index+1] = (byte)g; rgbdata[index+2] = (byte)r; } } src.put(0, 0, rgbdata); return src; } 特别要说明一下,BufferedImage与Mat的RGB通道顺序是不一样,正好相反,在Mat对象中三通道的顺序为BGR而在BufferedImage中为RGB。 从Mat中读取全部像素(其中image为Mat类型数据) int width = image.cols(); int height = image.rows(); int dims = image.channels(); byte[] data = new byte[width*height*dims]; image.get(0, 0, data); 遍历像素操作与保存改变 int index = 0; int r=0, g=0, b=0; for(int row=0; row<height; row++) { for(int col=0; col<width*dims; col+=dims) { index = row*width*dims + col; b = data[index]&0xff; g = data[index+1]&0xff; r = data[index+2]&0xff; r = 255 - r; g = 255 - g; b = 255 - b; data[index] = (byte)b; data[index+1] = (byte)g; data[index+2] = (byte)r; } } image.put(0, 0, data); 保存Mat对象为图像文件 - 一句话可以搞定 Imgcodecs.imwrite(filePath, src); OpenCV代码运行与测试 调节明暗程度 - 亮度降低 调节明暗程度 - 亮度提升 高斯模糊 锐化 梯度 灰度化 上述效果完整Java代码如下: package com.gloomyfish.opencvdemo; import org.opencv.core.Core; import org.opencv.core.CvType; import org.opencv.core.Mat; import org.opencv.core.Size; import org.opencv.imgproc.Imgproc; public class ImageFilters { /** - 反色处理 - */ public Mat inverse(Mat image) { int width = image.cols(); int height = image.rows(); int dims = image.channels(); byte[] data = new byte[width*height*dims]; image.get(0, 0, data); int index = 0; int r=0, g=0, b=0; for(int row=0; row<height; row++) { for(int col=0; col<width*dims; col+=dims) { index = row*width*dims + col; b = data[index]&0xff; g = data[index+1]&0xff; r = data[index+2]&0xff; r = 255 - r; g = 255 - g; b = 255 - b; data[index] = (byte)b; data[index+1] = (byte)g; data[index+2] = (byte)r; } } image.put(0, 0, data); return image; } public Mat brightness(Mat image) { // 亮度提升 Mat dst = new Mat(); Mat black = Mat.zeros(image.size(), image.type()); Core.addWeighted(image, 1.2, black, 0.5, 0, dst); return dst; } public Mat darkness(Mat image) { // 亮度降低 Mat dst = new Mat(); Mat black = Mat.zeros(image.size(), image.type()); Core.addWeighted(image, 0.5, black, 0.5, 0, dst); return dst; } public Mat gray(Mat image) { // 灰度 Mat gray = new Mat(); Imgproc.cvtColor(image, gray, Imgproc.COLOR_BGR2GRAY); return gray; } public Mat sharpen(Mat image) { // 锐化 Mat dst = new Mat(); float[] sharper = new float[]{0, -1, 0, -1, 5, -1, 0, -1, 0}; Mat operator = new Mat(3, 3, CvType.CV_32FC1); operator.put(0, 0, sharper); Imgproc.filter2D(image, dst, -1, operator); return dst; } public Mat blur(Mat image) { // 高斯模糊 Mat dst = new Mat(); Imgproc.GaussianBlur(image, dst, new Size(15, 15), 0); return dst; } public Mat gradient(Mat image) { // 梯度 Mat grad_x = new Mat(); Mat grad_y = new Mat(); Mat abs_grad_x = new Mat(); Mat abs_grad_y = new Mat(); Imgproc.Sobel(image, grad_x, CvType.CV_32F, 1, 0); Imgproc.Sobel(image, grad_y, CvType.CV_32F, 0, 1); Core.convertScaleAbs(grad_x, abs_grad_x); Core.convertScaleAbs(grad_y, abs_grad_y); grad_x.release(); grad_y.release(); Mat gradxy = new Mat(); Core.addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 10, gradxy); return gradxy; } } 可以说简单到哭,此外OpenCV For Java支持各种的图像处理包括形态学操作,二值图像分析、图像特征检测与识别、模板匹配、直方图相关功能等等。常见的机器学习算法与图像分析方法。可以说是功能最强大的图像处理SDK与开发平台之一,本人继续发掘分享! 特别注意 在调用之前,一定要加上这句话 System.loadLibrary(Core.NATIVE_LIBRARY_NAME); 目的是加载OpenCV API相关的DLL支持,没有它是不会正确运行的。以上代码与功能实现是基于JDK8 64位与OpenCV 3.2版本。 欢迎大家继续关注本博客!
OpenCV 3.1.0编译与添加扩展模块 最近在弄个东西,需要把OpenCV的扩展模块中的xfeatures给包含进来,发现要自己编译OpenCV3.1.0与其扩展模块才可以实现。经过一番实践,终于编译完成,总结了一下,其实很简单,只要如下三步即可实现。在正式开始之前,有些准备工作需要做,就是下载OpenCV3.1.0还有其扩展模块,以及CMake GUI工具。 OpenCV3.1.0下载地址: https://sourceforge.net/projects/opencvlibrary/files/opencv-win/3.1.0/opencv-3.1.0.exe/download OpenCVcontrib下载地址: https://github.com/opencv/opencv_contrib cmake-gui下载地址(3.7.2): https://cmake.org/download/ 把OpenCV就解压缩到指定目录,其中我是把OpenCV解压缩到 然后安装CMake GUI,默认安装即可。 好了之后就正式开始。 第一步 配置与生成CMake, 打开CMake GUI之后,选择好路径,点击配置,在打开的对话框中一定要选择VS14 Windows 64才可以。然后它就会自动运行得到如下结果: 然后找到OPENCV_EXTRA_MODULE_PATH设置扩展模块的路径运行完成显示如下: 第二步: 配置VS生成installer,首先到CMake的Build输出目录 D:\opencv3.1\opencv\newbuild下找到OpenCV.sln文件,双击打开之后,右键选择 ->重新生成解决方案,然后在找到CMakeTargets->INSTALL, 右键选择生成installer即可。这样就会在D:\opencv3.1\opencv\newbuild下面多出一个installer的文件夹,到如果能成功生成,编译就结束了。 第三步: 重新配置OpenCV,新建一个项目打开,到【视图】-》【其它窗口】-》【属性管理器】然后选择64 debug下的User Cpp从属性中打开配置窗口 分别设置好 包含目录 库目录 还有附件依赖项中添加如下: opencv_calib3d310d.lib opencv_core310d.lib opencv_features2d310d.lib opencv_flann310d.lib opencv_highgui310d.lib opencv_imgcodecs310d.lib opencv_imgproc310d.lib opencv_ml310d.lib opencv_objdetect310d.lib opencv_photo310d.lib opencv_shape310d.lib opencv_stitching310d.lib opencv_superres310d.lib opencv_ts310d.lib opencv_video310d.lib opencv_videoio310d.lib opencv_videostab310d.lib ippicvmt.lib opencv_xfeatures2d310d.lib opencv_xobjdetect310d.lib 最后千万别忘记把bin目录改过来, 我的编译生成的OpenCV v14/bin的目录如下: D:\opencv3.1\opencv\newbuild\install\x64\vc14\bin 添加到系统的环境变量中即可。把原来的去掉。这样就可以使用OpenCV扩展模块xfeatures2d了。测试代码与运行结果如下: #include <stdio.h> #include <iostream> #include "opencv2/core.hpp" #include "opencv2/features2d.hpp" #include "opencv2/xfeatures2d.hpp" #include "opencv2/highgui.hpp" using namespace cv; using namespace cv::xfeatures2d; using namespace std; int main(int argc, char** argv) { Mat img_1 = imread("D:/vcprojects/images/test.png", IMREAD_GRAYSCALE); if (img_1.empty()) { printf("could not load image...\n"); return -1; } imshow("input image", img_1); int minHessian = 400; Ptr<SURF> detector = SURF::create(minHessian); vector<KeyPoint> keypoints; detector->detect(img_1, keypoints); Mat img_keypoints1; drawKeypoints(img_1, keypoints, img_keypoints1, Scalar::all(-1), DrawMatchesFlags::DEFAULT); namedWindow("key points", CV_WINDOW_AUTOSIZE); imshow("key points", img_keypoints1); waitKey(0); return 0; } 基于xfeature实现SURF特征检测的运行结果如下: 关注微信公众号【OpenCV学堂】获取图像处理相关知识。
图像处理之局部二值特征 一:局部二值模式(LBP)介绍 局部二值模式(Local Binary Pattern)主要用来实现2D图像纹理分析。其基本思想是用每个像素跟它周围的像素相比较得到局部图像结构,假设中心像素值大于相邻像素值则则相邻像素点赋值为1,否则赋值为0,最终对每个像素点都会得到一个二进制八位的表示,比如11100111。假设3x3的窗口大小,这样对每个像素点来说组合得到的像素值的空间为[0~2^8]。这种结果我称为图像的局部二值模式或者简写为了LBP。 二:局部二值模式(LBP)扩展 对于这种固定窗口大小方式的局部二值模式,很多人很快就发现它的弊端,不能很好的反映出图像结构,于是高人纷纷上阵把它改为窗口大小可变,而且把矩形结构改成圆形结构。而且还总结出来如下一系列的典型结构单元: 该操作是基于原来的局部二值模式的扩展,所以又被称为扩展的局部二值模式。但是一旦改为圆形的时候,寻找八个点坐标可能会产生小数坐标,这个时候就需要通过插值方式产生该像素点的像素值,最常见的插值方式基于双线性插值。这样就完成了任意尺度上的局部二值模式的采样。 三:运行 输入图像与3x3默认的LBP运行结果如下: 在扩展模式下半径分别为1、3、5、7时候的运行结果: 四:代码实现 - 基于OpenCV实现 简单说一下步骤 1. 读入图像 2. 彩色图像转灰度 3. 默认LBP处理操作 4. 扩展LBP处理操作 完整的源代码如下: #include <opencv2/opencv.hpp> #include <iostream> #include "math.h" using namespace cv; using namespace std; int max_thresh = 20; int current_radius = 5; Mat gray_src, LBP_image, ELBP_image; void Demo_ELBP(int, void*); int main(int argc, char** argv) { Mat src, dst; src = imread("D:/vcprojects/images/cat.jpg"); if (src.empty()) { printf("could not load image...\n"); return -1; } const char* output_tt = "LBP Result"; namedWindow("input image", CV_WINDOW_AUTOSIZE); namedWindow(output_tt, CV_WINDOW_AUTOSIZE); imshow("input image", src); // convert to gray cvtColor(src, gray_src, COLOR_BGR2GRAY); int width = gray_src.cols; int height = gray_src.rows; // default LBP image LBP_image = Mat::zeros(src.rows - 2, src.cols - 2, CV_8UC1); for (int row = 1; row < height-1; row++) { for (int col = 1; col < width-1; col++) { uchar center = gray_src.at<uchar>(row, col); uchar code = 0; code |= (gray_src.at<uchar>(row - 1, col - 1) > center) << 7; code |= (gray_src.at<uchar>(row - 1, col) > center) << 6; code |= (gray_src.at<uchar>(row - 1, col + 1) > center) << 5; code |= (gray_src.at<uchar>(row, col + 1) > center) << 4; code |= (gray_src.at<uchar>(row+ 1, col + 1) > center) << 3; code |= (gray_src.at<uchar>(row + 1, col) > center) << 2; code |= (gray_src.at<uchar>(row + 1, col - 1) > center) << 1; code |= (gray_src.at<uchar>(row, col - 1) > center) << 0; LBP_image.at<uchar>(row- 1, col - 1) = code; } } imshow(output_tt, LBP_image); // extend LBP namedWindow("ELBP_Result", CV_WINDOW_AUTOSIZE); createTrackbar("Radius:", "ELBP_Result", &current_radius, max_thresh, Demo_ELBP); Demo_ELBP(0, 0); waitKey(0); return 0; } void Demo_ELBP(int, void*) { int offset = current_radius * 2; ELBP_image = Mat::zeros(gray_src.rows - offset, gray_src.cols - offset, CV_8UC1); int height = gray_src.rows; int width = gray_src.cols; int neighbors = 8; for (int n = 0; n<neighbors; n++) { // sample points float x = static_cast<float>(current_radius) * cos(2.0*CV_PI*n / static_cast<float>(neighbors)); float y = static_cast<float>(current_radius) * -sin(2.0*CV_PI*n / static_cast<float>(neighbors)); // relative indices int fx = static_cast<int>(floor(x)); int fy = static_cast<int>(floor(y)); int cx = static_cast<int>(ceil(x)); int cy = static_cast<int>(ceil(y)); // fractional part float ty = y - fy; float tx = x - fx; // set interpolation weights float w1 = (1 - tx) * (1 - ty); float w2 = tx * (1 - ty); float w3 = (1 - tx) * ty; float w4 = tx * ty; // iterate through your data for (int i = current_radius; i < gray_src.rows - current_radius; i++) { for (int j = current_radius; j < gray_src.cols - current_radius; j++) { float t = w1*gray_src.at<uchar>(i + fy, j + fx) + w2*gray_src.at<uchar>(i + fy, j + cx) + w3*gray_src.at<uchar>(i + cy, j + fx) + w4*gray_src.at<uchar>(i + cy, j + cx); // we are dealing with floating point precision, so add some little tolerance ELBP_image.at<uchar>(i - current_radius, j - current_radius) += ((t > gray_src.at<uchar>(i, j)) && (abs(t - gray_src.at<uchar>(i, j)) > std::numeric_limits<float>::epsilon())) << n; } } } imshow("ELBP_Result", ELBP_image); } 请继续关注本博客,同时关注微信公众号【OpenCV学堂】定期推送更多干货,图像处理算法!
图像处理之积分图应用四(基于局部均值的图像二值化算法) 基本原理 均值法,选择的阈值是局部范围内像素的灰度均值(gray mean),该方法的一个变种是用常量C减去均值Mean,然后根据均值实现如下操作: pixel = (pixel > (mean - c)) ? object : background 其中默认情况下参数C取值为0。object表示前景像素,background表示背景像素。 实现步骤 1. 彩色图像转灰度图像 2. 获取灰度图像的像素数据,预计算积分图 3. 根据输入的参数窗口半径大小从积分图中获取像素总和,求得平均值 4.循环每个像素,根据局部均值实现中心像素的二值化赋值 5.输入二值图像 运行结果: 代码实现: package com.gloomyfish.ii.demo; import java.awt.image.BufferedImage; public class FastMeanBinaryFilter extends AbstractImageOptionFilter { private int constant; private int radius; public FastMeanBinaryFilter() { constant = 10; radius = 7; // 1,2,3,4,5,6,7,8 } public int getConstant() { return constant; } public void setConstant(int constant) { this.constant = constant; } public int getRadius() { return radius; } public void setRadius(int radius) { this.radius = radius; } @Override public BufferedImage process(BufferedImage image) { int width = image.getWidth(); int height = image.getHeight(); BufferedImage dest = createCompatibleDestImage( image, null ); // 图像灰度化 int[] inPixels = new int[width*height]; int[] outPixels = new int[width*height]; byte[] binData = new byte[width*height]; getRGB( image, 0, 0, width, height, inPixels ); int index = 0; for(int row=0; row<height; row++) { int ta = 0, tr = 0, tg = 0, tb = 0; for(int col=0; col<width; col++) { index = row * width + col; ta = (inPixels[index] >> 24) & 0xff; tr = (inPixels[index] >> 16) & 0xff; tg = (inPixels[index] >> 8) & 0xff; tb = inPixels[index] & 0xff; int gray= (int)(0.299 *tr + 0.587*tg + 0.114*tb); binData[index] = (byte)gray; } } // per-calculate integral image IntIntegralImage grayii = new IntIntegralImage(); grayii.setImage(binData); grayii.process(width, height); int yr = radius; int xr = radius; int size = (yr * 2 + 1)*(xr * 2 + 1); for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { index = row * width + col; // 计算均值 int sr = grayii.getBlockSum(col, row, (yr * 2 + 1), (xr * 2 + 1)); int mean = sr / size; int pixel = binData[index]&0xff; // 二值化 if(pixel > (mean-constant)) { outPixels[row * width + col] = (0xff << 24) | (0xff << 16) | (0xff << 8) | 0xff; } else { outPixels[row * width + col] = (0xff << 24) | (0x00 << 16) | (0x00 << 8) | 0x00; } } } // 返回结果 setRGB(dest, 0, 0, width, height, outPixels); return dest; } } 2017年已经开始啦!博客每个月都会有图像处理相关技术文章更新,欢迎大家继续关注!
图像处理之三角法图像二值化 三角法求阈值最早见于Zack的论文《Automatic measurement of sister chromatid exchange frequency》主要是用于染色体的研究,该方法是使用直方图数据,基于纯几何方法来寻找最佳阈值,它的成立条件是假设直方图最大波峰在靠近最亮的一侧,然后通过三角形求得最大直线距离,根据最大直线距离对应的直方图灰度等级即为分割阈值,图示如下: 对上图的详细解释: 在直方图上从最高峰处bmx到最暗对应直方图bmin(p=0)%构造一条直线,从bmin处开始计算每个对应的直方图b到直线的垂直距离,知道bmax为止,其中最大距离对应的直方图位置即为图像二值化对应的阈值T。 扩展情况: 有时候最大波峰对应位置不在直方图最亮一侧,而在暗的一侧,这样就需要翻转直方图,翻转之后求得值,用255减去即得到为阈值T。扩展情况的直方图表示如下: 二:算法步骤 1. 图像转灰度 2. 计算图像灰度直方图 3. 寻找直方图中两侧边界 4. 寻找直方图最大值 5. 检测是否最大波峰在亮的一侧,否则翻转 6. 计算阈值得到阈值T,如果翻转则255-T 三:代码实现 package com.gloomyfish.filter.study; import java.awt.image.BufferedImage; public class TriangleBinaryFilter extends AbstractBufferedImageOp{ public TriangleBinaryFilter() { System.out.println("triangle binary filter"); } @Override public BufferedImage filter(BufferedImage src, BufferedImage dest) { int width = src.getWidth(); int height = src.getHeight(); if ( dest == null ) dest = createCompatibleDestImage( src, null ); // 图像灰度化 int[] inPixels = new int[width*height]; int[] outPixels = new int[width*height]; getRGB( src, 0, 0, width, height, inPixels ); int index = 0; for(int row=0; row<height; row++) { int ta = 0, tr = 0, tg = 0, tb = 0; for(int col=0; col<width; col++) { index = row * width + col; ta = (inPixels[index] >> 24) & 0xff; tr = (inPixels[index] >> 16) & 0xff; tg = (inPixels[index] >> 8) & 0xff; tb = inPixels[index] & 0xff; int gray= (int)(0.299 *tr + 0.587*tg + 0.114*tb); inPixels[index] = (ta << 24) | (gray << 16) | (gray << 8) | gray; } } // 获取直方图 int[] histogram = new int[256]; for(int row=0; row<height; row++) { int tr = 0; for(int col=0; col<width; col++) { index = row * width + col; tr = (inPixels[index] >> 16) & 0xff; histogram[tr]++; } } int left_bound = 0, right_bound = 0, max_ind = 0, max = 0; int temp; boolean isflipped = false; int i=0, j=0; int N = 256; // 找到最左边零的位置 for( i = 0; i < N; i++ ) { if( histogram[i] > 0 ) { left_bound = i; break; } } // 位置再移动一个步长,即为最左侧零位置 if( left_bound > 0 ) left_bound--; // 找到最右边零点位置 for( i = N-1; i > 0; i-- ) { if( histogram[i] > 0 ) { right_bound = i; break; } } // 位置再移动一个步长,即为最右侧零位置 if( right_bound < N-1 ) right_bound++; // 在直方图上寻找最亮的点Hmax for( i = 0; i < N; i++ ) { if( histogram[i] > max) { max = histogram[i]; max_ind = i; } } // 如果最大值落在靠左侧这样就无法满足三角法求阈值,所以要检测是否最大值是否靠近左侧 // 如果靠近左侧则通过翻转到右侧位置。 if( max_ind-left_bound < right_bound-max_ind) { isflipped = true; i = 0; j = N-1; while( i < j ) { // 左右交换 temp = histogram[i]; histogram[i] = histogram[j]; histogram[j] = temp; i++; j--; } left_bound = N-1-right_bound; max_ind = N-1-max_ind; } // 计算求得阈值 double thresh = left_bound; double a, b, dist = 0, tempdist; a = max; b = left_bound-max_ind; for( i = left_bound+1; i <= max_ind; i++ ) { // 计算距离 - 不需要真正计算 tempdist = a*i + b*histogram[i]; if( tempdist > dist) { dist = tempdist; thresh = i; } } thresh--; // 对已经得到的阈值T,如果前面已经翻转了,则阈值要用255-T if( isflipped ) thresh = N-1-thresh; // 二值化 System.out.println("final threshold value : " + thresh); for(int row=0; row<height; row++) { for(int col=0; col<width; col++) { index = row * width + col; int gray = (inPixels[index] >> 8) & 0xff; if(gray > thresh) { gray = 255; outPixels[index] = (0xff << 24) | (gray << 16) | (gray << 8) | gray; } else { gray = 0; outPixels[index] = (0xff << 24) | (gray << 16) | (gray << 8) | gray; } } } // 返回二值图像 setRGB(dest, 0, 0, width, height, outPixels ); return dest; } } 四:运行结果 2016年最后一篇,这里祝大家元旦快乐,欢迎在2017继续关注本博客,分享有用实用的图像处理知识本人会一直坚持到永远! 学习图像处理基础入门课程 - 点击这里
关河无尽处,风雪有行人 - 我的2016年总结 2016年我做为个人独立开发者渡过的完整一年,用一句话说理想是丰满的,现实是骨感的,本来计划在2016年想自己做个图像处理方面的产品,但是迫于生活压力,不得不接下一个又一个的小项目以维持生计,还没有摆脱生存压力,这个也是个人开发者来说最重要的一课,首先要生存下去,然后才是自我发展。从生存的角度来说,作为个人独立开发者我已经做到了,但是也只是做到了而已。并没有实现自己心中根本改观与初衷目标。回首这一年经历过的几个项目风风雨雨的历程,感慨良多、收获不少! 技术上 2016年自己渐渐的把重心都在了图像处理这个领域、深入学习了OpenCV框架,同时也录制了相关的视频教程。毕竟作为个人独立开发者技术永远是自己安身立命之根本,不敢懈怠。其次对C++与JNI开发也多少有点涉及,这个也是自己做项目需要,没办法只有攻克它,才能做好项目。Java作为我已经使用10多年的语言依然一直在用。唯一改变的是对技术的理解与宏观感觉。 关于合作 从2015年我作为个人开发者出来以后就陆陆续续跟别人合作开发过项目,遇到过各种各样的人。个人最痛苦与最重要的领悟有两条,第一条千万不要轻信别人的许诺、第二条千万不要给别人轻易许诺。套用我一哥们的话说:"凡是提出合作做项目而不谈钱而且不愿意出钱的都是在耍流氓"。跟这类人合作是绝对不可能有好结果的。此外千万不要轻信合作时候许诺股份与期权这种事情,基本上这个东西兑现的可能性跟你在大街买彩票中500万的概率差不多。真的有人愿意考虑给你这么钱的时候你要考虑一下你是否自己值这么钱。 关于做项目 今年做的几个项目基本上都是遇到很多坑,这其中我也学到不少东西,这个算是自己教的学费吧。其实无论做什么项目,都会遇到坑的,要相信自己一定可以搞定,有时候多尝试一次就会出正确结果,特别做搞图像处理算法,没有可以复制的经验,靠的就是自己强大的自信与坚韧不拔的毅力。要相信自己可以解决这个问题。此外做项目最重要的是跟客户做好沟通与进度汇报,让客户知道你没有忘记他,一直在干活,很快会交付! 感悟与总结 作为个人独立开发者,最孤独的事情就是做东西遇到问题没有人一起讨论,最需要警惕的事情就是一定要时刻提醒自己别浪费时间,做好自我管理。浪费时间与精力对个人开发者来说是大忌,因为这意味着浪费钱与生命。2016年我最主要的收入都是来自项目,此外我也尝试通过录制图像处理视频课程给自己带来收入,暂时已经有了五门相关课程,但是目前来说效果不佳,自己还需要更多的努力与市场调查! 总结一下2016年自己走过的路挺适合一句古诗: “关河无尽处、风雪有行人”!
图像处理之角点检测与亚像素角点定位 角点是图像中亮度变化最强地方反映了图像的本质特征,提取图像中的角点可以有效提高图像处理速度与精准度。所以对于整张图像来说特别重要,角点检测与提取的越准确图像处理与分析结果就越接近真实。同时角点检测对真实环境下的对象识别、对象匹配都起到决定性作用。Harris角点检测是图像处理中角点提取的经典算法之一,应用范围广发,在经典的SIFT特征提取算法中Harris角点检测起到关键作用。通常对角点检测算法都有如下要求: 1. 基于灰度图像、能够自动调整运行稳定,检测出角点的数目。 2. 对噪声不敏感、有一定的噪声抑制,有较强的角点角点检测能力。 3. 准确性够高,能够正确发现角点位置 4. 算法尽可能的快与运行时间短 Harris角点检测基本上满足了上述四点要求,所以被广发应用,除了Harris角点检测,另外一种常见的角点检测算法-Shi-Tomasi角点检测也得到了广发应用,OpenCV中对这两种算法均有实现API可以调用。关于Harris角点检测原理可以看我之前写的博文: http://blog.csdn.net/jia20003/article/details/16908661 关于Shi-Tomasi角点检测,与Harris角点检测唯一不同就是在计算角点响应值R上面。 然后根据输入的阈值T大于该阈值的R对应像素点即为图像中角点位置坐标。此刻坐标往往都是整数出现,而在真实的世界中坐标多数时候都不是整数,假设我们计算出来的角点位置P(34, 189)而实际上准确角点位置是P(34.278, 189.706)这样带小数的位置,而这样的准确位置寻找过程就叫做子像素定位或者亚像素定位。这一步在SURF与SIFT算法中都有应用而且非常重要。常见的亚像素级别精准定位方法有三类: 1. 基于插值方法 2. 基于几何矩寻找方法 3. 拟合方法 - 比较常用 拟合方法中根据使用的公式不同可以分为高斯曲面拟合与多项式拟合等等。以高斯拟合为例 这样就求出了亚像素的位置。使用亚像素位置进行计算得到结果将更加准确,对图像特征提取、匹配结果效果显著。OpenCV中已经对角点检测实现了亚像素级别的API可以调用。 代码演示OpenCV亚像素角点检测例子: #include <opencv2/opencv.hpp> #include <iostream> using namespace cv; using namespace std; Mat src, gray_src; int max_corners = 10; int max_trackbar = 30; const char* output_title = "subpxiel-result"; void GoodFeature2Track_Demo(int, void*); int main(int argc, char** argv) { src = imread("D:/vcprojects/images/home.jpg"); if (src.empty()) { printf("could not load image...\n"); return -1; } cvtColor(src, gray_src, COLOR_BGR2GRAY); namedWindow("input", CV_WINDOW_AUTOSIZE); namedWindow(output_title, CV_WINDOW_AUTOSIZE); imshow("input", src); createTrackbar("Corners:", output_title, &max_corners, max_trackbar, GoodFeature2Track_Demo); GoodFeature2Track_Demo(0, 0); waitKey(0); return 0; } void GoodFeature2Track_Demo(int, void*) { if (max_corners < 1) { max_corners = 1; } vector<Point2f> corners; double qualityLevel = 0.01; double minDistance = 10; int blockSize = 3; double k = 0.04; goodFeaturesToTrack(gray_src, corners, max_corners, qualityLevel, minDistance, Mat(), blockSize, false, k); cout << "number of corners : " << corners.size() << endl; Mat copy = src.clone(); for (size_t t = 0; t < corners.size(); t++) { circle(copy, corners[t], 4, Scalar(255, 0, 0), 2, 8, 0); } imshow(output_title, copy); // locate corner point on sub pixel level Size winSize = Size(5, 5); Size zerozone = Size(-1, -1); TermCriteria criteria = TermCriteria(TermCriteria::EPS + TermCriteria::MAX_ITER, 40, 0.001); cornerSubPix(gray_src, corners, winSize, zerozone, criteria); for (size_t t = 0; t < corners.size(); t++) { cout << (t+1) << ".point[x, y]=" << corners[t].x << "," << corners[t].y << endl; } return; } 原图如下: 运行结果: 转载请注明来自【jia20003】的博客!
OpenCV中图像算术操作与逻辑操作 在图像处理中有两类最重要的基础操作分别是图像点操作与块操作,简单点说图像点操作就是图像每个像素点的相关逻辑与几何运算、块操作最常见就是基于卷积算子的各种操作、实现各种不同的功能。今天小编就跟大家一起学习OpenCV中图像点操作相关的函数与应用场景。常见算术运算包括加、减、乘、除,逻辑运算包括与、或、非、异或。准备工作: 选择两张大小一致的图像如下、加载成功以后显示如下: 加法操作结果如下: 减法操作结果如下: 乘法操作结果如下: 除法操作结果如下: 权重加法操作结果如下: 异或与非操作结果如下: 代码如下: Mat src1, src2, dst; src1 = imread("D:/vcprojects/images/test1.png"); src2 = imread("D:/vcprojects/images/moon.png"); const char* input_title1 = "input image - 1"; const char* input_title2 = "input image - 2"; namedWindow(input_title1, CV_WINDOW_AUTOSIZE); namedWindow(input_title2, CV_WINDOW_AUTOSIZE); imshow(input_title1, src1); imshow(input_title2, src2); // create result windows and background image const char* output_title = "result image"; namedWindow(output_title, CV_WINDOW_AUTOSIZE); Mat bgImg = Mat(src1.size(), src1.type()); Mat whiteImg = Mat(src1.size(), src1.type()); whiteImg = Scalar(255, 255, 255); // 临时图像 Mat skel(src1.size(), CV_8UC1, Scalar(0)); Mat temp(src1.size(), CV_8UC1); Mat element = getStructuringElement(MORPH_CROSS, Size(3, 3), Point(-1, -1)); bool done = false; int index = 9, c; while (true) { switch (index) { case 1: // 加操作 add(src1, src2, dst, Mat(), -1); imshow(output_title, dst); break; case 2: // 减操作 subtract(src1, src2, dst, Mat(), -1); imshow(output_title, dst); break; case 3: // 乘操作 bgImg = Scalar(2, 2, 2); multiply(src1, bgImg, dst, 1.0, -1); imshow(output_title, dst); break; case 4: // 除操作 bgImg = Scalar(2, 2, 2); divide(src1, bgImg, dst, 1.0, -1); imshow(output_title, dst); break; case 5: // 基于权重加法 - 调节亮度 addWeighted(src1, 1.5, src2, 0.5, 0, dst, -1); imshow(output_title, dst); break; case 6: // 逻辑非 bitwise_not(src1, dst, Mat()); imshow(output_title, dst); break; case 7: subtract(whiteImg, src1, dst, Mat(), -1); imshow(output_title, dst); break; case 8: // 逻辑异或 bgImg = Scalar(255, 255, 255); bitwise_xor(src1, bgImg, dst, Mat()); imshow(output_title, dst); break; default: imshow(output_title, src2); break; } c = waitKey(500); if ((char)c == 27) { break; } if(c > 0) { index = c % 9; } } 此外我们还可以基于逻辑操作与形态学的腐蚀操作实现二值图像的骨架提取,Demo演示结果如下: 代码实现如下: // 提取骨架 // 转灰度与二值化 cvtColor(src1, src1, COLOR_BGR2GRAY); threshold(src1, dst, 127, 255, CV_THRESH_BINARY); //bitwise_not(src1, src1); do { // 开操作 - 确保去掉小的干扰块 morphologyEx(src1, temp, MORPH_OPEN, element); // 取反操作 bitwise_not(temp, temp); // 得到与源图像不同 bitwise_and(src1, temp, temp); // 使用它提取骨架、得到是仅仅比源图像小一个像素 bitwise_or(skel, temp, skel); // 每次循环腐蚀,通过不断腐蚀的方式得到框架 erode(src1, src1, element); // 对腐蚀之后的图像寻找最大值,如果被完全腐蚀则说明 // 只剩下背景黑色、已经得到骨架,退出循环 double max; minMaxLoc(src1, 0, &max); done = (0 == max); } while (!done); // 显示骨架 imshow(output_title, skel); 总结: 通过上述代码演示,可以发现简单的图像算术运算也可以发挥大作用,基于黑色背景图像与原图权重叠加可以实现图像亮度调整、基于乘法可以实现对比度调整。基于逻辑操作与腐蚀操作可以实现二值图像的骨架提取。
Android Studio 2.2 中支持NDK开发HelloJNI例子 首先说一下运行的开发环境 * Win7 64位 * Android Studio 2.2 * NDK版本是64位 r13b 首先在AndroidStuido中创建一个空白项目,创建好之后,选择【File】->【project structure】显示如下: 添加好NDK支持,记得提前下载安装好就行啦。这里直接选择到安装好的路径即可。然后新建一个Java类,添加两个本地方法,保存之后。 package basictutorial.gloomyfish.com.myndkdemo; /** * Created by Administrator on 2016/11/26. */ public class HelloJNI { public native static String greet(); public native static int sum(int a, int b); } 看一下项目目录结构如下: 选择【Build】->【Make Project】之后。就会在你的项目的如下目录: myndkdemo\app\build\intermediates\classes\debug生成编译好的class文件。 然后在Android Studio中打开终端命令行 通过cd目录进入到当前项目main目录,显示如下: 然后执行: javah -d jni -classpath .;%project_dir%/app\build\intermediates\classes\debug; basictutorial.gloomyfish.com.myndkdemo.HelloJNI 执行javah命令就会生成jni目录与C++的头文件。然后到JNI目录里面新建一个文件取名hello.cpp。在Android Studio中刷新一下你的目录就可以看到 双击打开Hello.cpp文件,把头文件中生成的两个方法copy到cpp文件中。然后做如下修改 #include <jni.h> #include <basictutorial_gloomyfish_com_myndkdemo_HelloJNI.h> #include <math.h> /** * return string */ JNIEXPORT jstring JNICALL Java_basictutorial_gloomyfish_com_myndkdemo_HelloJNI_greet (JNIEnv *env, jclass thiz) { char* result = "this is call from JNI C++ side"; jstring param3 = env->NewStringUTF(result); return param3; } /** * calculate and return */ JNIEXPORT jint JNICALL Java_basictutorial_gloomyfish_com_myndkdemo_HelloJNI_sum (JNIEnv *env, jclass thiz, jint a, jint b) { jint sum = pow(a, 2) + pow(b, 2); return sum; } 然后开始通过【Build】->【Build Project】开始编译,如果得到如下错误 在gradle.properties文件中添加如下语句 android.useDeprecatedNdk=true 然后继续执行编译动作,基本就OK了,这样我们就是实现了两个C++的功能一个是返回字符串进行问候、另外一个是计算两个数的平方和返回结果。设计一个Android界面可以输入两个数值、然后点击计算调用C++功能完成计算之后返回结果显示通过在Android Activity中调用实现整个过程,代码如下: package basictutorial.gloomyfish.com.myndkdemo; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; public class MainActivity extends AppCompatActivity implements View.OnClickListener{ static { System.loadLibrary("HelloJNI"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button calcBtn = (Button)this.findViewById(R.id.calculate_button); calcBtn.setOnClickListener(this); TextView txtView = (TextView)this.findViewById(R.id.my_textView); txtView.setText(HelloJNI.greet()); } @Override public void onClick(View v) { EditText eidtText1 = (EditText)this.findViewById(R.id.editText1); EditText eidtText2 = (EditText)this.findViewById(R.id.editText2); String atext = eidtText1.getText().toString(); String btext = eidtText2.getText().toString(); int a = Integer.parseInt(atext); int b = Integer.parseInt(btext); TextView resultView = (TextView)this.findViewById(R.id.sum_textView); resultView.setText("平方和计算结果:" + HelloJNI.sum(a, b)); } } 运行显示结果如下: XML的内容如下: <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="demo.jni.gloomyfish.com.jnidemoapp.MainActivity"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/my_textView" android:text="Hello World!" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/my_textView" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_marginTop="10dp" android:id="@+id/input_numbers"> <EditText android:layout_width="wrap_content" android:layout_height="wrap_content" android:inputType="numberDecimal" android:layout_weight="1" android:text="10" android:hint="请输入数字..." android:ems="10" android:id="@+id/editText1" /> <EditText android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:inputType="numberDecimal" android:text="10" android:hint="请输入数字..." android:ems="10" android:id="@+id/editText2" /> </LinearLayout> <TextView android:text="TextView" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@+id/input_numbers" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_marginTop="10dp" android:id="@+id/sum_textView" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/sum_textView" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:text="计算" android:layout_marginTop="10dp" android:id="@+id/calculate_button"/> </RelativeLayout> build.gradle的ndk相关脚本如下: defaultConfig { applicationId "basictutorial.gloomyfish.com.myndkdemo" minSdkVersion 14 targetSdkVersion 24 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" ndk { moduleName "HelloJNI" ldLibs "log", "z", "m" abiFilters "armeabi", "armeabi-v7a", "x86" } }
Tesseract OCR集成Android Studio实现OCR识别 介绍 Tesseract OCR谷歌开源的OCR识别引擎,支持多国文字包括中文简体与繁体。最新的版本是3.x。可以通过安装程序安装在机器上然后通过命令行运行该程序识别各种图片中的文字、同时还提供二次开发包,支持二次开发包括C、C++语言。也可以被移植到Android平台实现移动应用领域的OCR识别APP。 下载 在Android平台上使用Tesseract OCR首先要下载Tess2工程,它是专门针对Android平台编译出来的,下载地址如下 - https://github.com/rmtheis/tess-two 。下载解压缩之后你就会看到如下目录结构: 然后打开Android Studio新建一个项目应用,选择导入Module之后选择导入红色圆圈中的tess-two文件夹,导入之后你就会看到如下: 项目必须是支持NDK的,所以要在Project Structure中指明NDK的路径。原因是tess-two是个NDK项目,没有NDK支持无法完成编译。 此外你可能还会遇到没有android-maven的错误,把下面的脚本加到build.gradle的最上面即可: buildscript { repositories { jcenter() } dependencies { classpath ‘com.android.tools.build:gradle:2.1.2’ classpath ‘org.codehaus.groovy:groovy-backports-compat23:2.3.5’ classpath ‘com.jfrog.bintray.gradle:gradle-bintray-plugin:1.0’ classpath ‘com.github.dcendents:android-maven-gradle-plugin:1.5’ } } private void initTessBaseData() { mTess = new TessBaseAPI(); String datapath = Environment.getExternalStorageDirectory() + “/tesseract/”; // String language = “num”; String language = “eng”; File dir = new File(datapath + “tessdata/”); if (!dir.exists()) dir.mkdirs(); mTess.init(datapath, language); } public void onClick(View v) { Bitmap bitmap = BitmapFactory.decodeResource(this.getResources(), R.drawable.textimage); mTess.setImage(bitmap); String result = mTess.getUTF8Text(); TextView txtView = (TextView)this.findViewById(R.id.idCard_textView); txtView.setText(“结果为:” + result); ImageView imgView = (ImageView)this.findViewById(R.id.imageView); imgView.setImageBitmap(bitmap); } 显示结果如下:
在Android Studio 2.2上集成OpenCV4Android SDK OpenCV官方的教程是基于Eclipse配置开发环境,但是Eclipse已经被Google抛弃了,所以我是写这篇文章的前三天刚刚开始用Android Studio 2.2版本,很多Gradle脚本也不熟悉,只能各种查找。经过一番痛苦的领悟终于把OpenCV4Android集成到我在Android Studio中创建的项目上了,并且写了测试程序。下面说一下如何实现集成步骤,首先是准备工作要做好: 下载好Android Studio 2.2版本 下载好OpenCV4AndroidSDK - 去OpenCV社区官网即可得到。 下载之后解压缩到D:\OpenCV-2.4.11-android-sdk\OpenCV-android-sdk apk目录里面放的是OpenCV Manager的安装文件,是不同CPU版本要选择不同apk安装文件,这种方式调用OpenCV比较麻烦。不推荐! doc目录里面放的是相关文档与OpenCV的LOGO samples里面放的是OpenCV4Android的演示代码,参考价值很大,值得关注 sdk里面放内容就是我们要重点关注的,集成到Android Studio中的项目上去的东西。 双击打开sdk文件夹就会看到: 准备工作做好之后,首先就是要在Android Studio中创建一个Android项目,创建好之后,选择File->New->Import Module 然后选择到SDK路径下的JAVA 导入之后,你就会看到 就说明成功导入了,然后打开Module Settings 添加依赖之后,就可以在项目中引用OpenCV相关API代码了,如果你此刻运行测试apk程序,它就会提示你安装OpenCV Manager这个东西。对多数开发者来说这不算配置成功,这样自己的APP就无法独立安装,必须依赖OpenCV Manager这个apk文件才可以运行,这个时候就该放大招来解决这个问题,首先把我们准备阶段看到SDK下面native文件下所有的文件都copy到你创建好的项目libs目录下,然后在gradle中加上如下一段脚本: task nativeLibsToJar(type: Jar, description: 'create a jar archive of the native libs') { destinationDir file("$buildDir/native-libs") baseName 'native-libs' from fileTree(dir: 'libs', include: '**/*.so') into 'lib/' } tasks.withType(JavaCompile) { compileTask -> compileTask.dependsOn(nativeLibsToJar) } 然后还要加上这句话: compile fileTree(dir: "$buildDir/native-libs", include: 'native-libs.jar') 最后一步,检查一下gradle文件: 如此配置之后你就再也不需要其它任何配置了,这样既避免了NDK繁琐又不用依赖OpenCV Manager第三方APP,你的APP就可以直接使用OpenCV了。 特别说明: 此配置方式OpenCV加载必须通过静态加载方式,以下为Demo测试程序代码: package com.example.administrator.helloworld; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.provider.MediaStore; import android.support.annotation.NonNull; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.support.v7.view.ActionMode; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.ImageView; import android.widget.Toast; import org.opencv.android.BaseLoaderCallback; import org.opencv.android.OpenCVLoader; import org.opencv.android.Utils; import org.opencv.core.Core; import org.opencv.core.CvType; import org.opencv.core.Mat; import org.opencv.core.Size; import org.opencv.imgproc.Imgproc; import java.io.InputStream; import static android.widget.Toast.makeText; public class MainActivity extends AppCompatActivity { private double max_size = 1024; private int PICK_IMAGE_REQUEST = 1; private ImageView myImageView; private Bitmap selectbp; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); staticLoadCVLibraries(); myImageView = (ImageView)findViewById(R.id.imageView); myImageView.setScaleType(ImageView.ScaleType.FIT_CENTER); Button selectImageBtn = (Button)findViewById(R.id.select_btn); selectImageBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // makeText(MainActivity.this.getApplicationContext(), "start to browser image", Toast.LENGTH_SHORT).show(); selectImage(); } }); Button processBtn = (Button)findViewById(R.id.process_btn); processBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // makeText(MainActivity.this.getApplicationContext(), "hello, image process", Toast.LENGTH_SHORT).show(); convertGray(); } }); } //OpenCV库静态加载并初始化 private void staticLoadCVLibraries(){ boolean load = OpenCVLoader.initDebug(); if(load) { Log.i("CV", "Open CV Libraries loaded..."); } } private void convertGray() { Mat src = new Mat(); Mat temp = new Mat(); Mat dst = new Mat(); Utils.bitmapToMat(selectbp, src); Imgproc.cvtColor(src, temp, Imgproc.COLOR_BGRA2BGR); Log.i("CV", "image type:" + (temp.type() == CvType.CV_8UC3)); Imgproc.cvtColor(temp, dst, Imgproc.COLOR_BGR2GRAY); Utils.matToBitmap(dst, selectbp); myImageView.setImageBitmap(selectbp); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if(requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null && data.getData() != null) { Uri uri = data.getData(); try { Log.d("image-tag", "start to decode selected image now..."); InputStream input = getContentResolver().openInputStream(uri); BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeStream(input, null, options); int raw_width = options.outWidth; int raw_height = options.outHeight; int max = Math.max(raw_width, raw_height); int newWidth = raw_width; int newHeight = raw_height; int inSampleSize = 1; if(max > max_size) { newWidth = raw_width / 2; newHeight = raw_height / 2; while((newWidth/inSampleSize) > max_size || (newHeight/inSampleSize) > max_size) { inSampleSize *=2; } } options.inSampleSize = inSampleSize; options.inJustDecodeBounds = false; options.inPreferredConfig = Bitmap.Config.ARGB_8888; selectbp = BitmapFactory.decodeStream(getContentResolver().openInputStream(uri), null, options); myImageView.setImageBitmap(selectbp); } catch (Exception e) { e.printStackTrace(); } } } private void selectImage() { Intent intent = new Intent(); intent.setType("image/*"); intent.setAction(Intent.ACTION_GET_CONTENT); startActivityForResult(Intent.createChooser(intent,"选择图像..."), PICK_IMAGE_REQUEST); } } 选择一张图像加载之后显示: 点击【处理】之后,通过调用OpenCV API实现了灰度转换 特别说明:这种方式调用OpenCV无需NKD以及安装OpenCV Manager。应该是广大Android开发人员最喜欢的一种方式。 免费环境搭建视频查看: OpenCV For Android 基础入门视频
在Android Studio中如何调整字体大小 在Android Studio中如何删除项目 相信刚开始用Android Studio的开发者都没有少被这两个问题折磨 本人以前都是用Eclipse做安卓程序开发的,前两天心血来潮决定抛弃Eclipse转而使用使用谷歌的开发工具Android Studio。刚下载安装好Android Studio时候本人一阵激动,立刻生成了HelloWorld发现代码字体很小,看着难受,就想通过偏好设置(Preference)设置一下字体,找了半天发现没有这个菜单,后发才发现在Android Studio中调整代码字体菜单不是一般的恶搞。我记录下来给后来者 Android Studio 中调整字体大小 首先要到这里 点击之后就会看到 直接点击【OK】保存之后,再去这个地方 点击Setting之后就会看到这个画面: 就会看到跟Eclipse中类似的功能了,可以各种设置。 怎么删除项目 删除一个项目要首先到File->ProjectSturcture 打开是这个样子的 选择你要删除的项目,然后点击上面的【—】即可。
图像处理之积分图应用三(基于NCC快速相似度匹配算法) 基于Normalized cross correlation(NCC)用来比较两幅图像的相似程度已经是一个常见的图像处理手段。在工业生产环节检测、监控领域对对象检测与识别均有应用。NCC算法可以有效降低光照对图像比较结果的影响。而且NCC最终结果在0到1之间,所以特别容易量化比较结果,只要给出一个阈值就可以判断结果的好与坏。传统的NCC比较方法比较耗时,虽然可以通过调整窗口大小和每次检测的步长矩形部分优化,但是对工业生产检测然后不能达到实时需求,通过积分图像实现预计算,比较模板图像与生产出电子版之间的细微差异,可以帮助企业提高产品质量,减少次品出厂率,把控质量。 一:NCC相关的数学知识 什么是NCC - (normalized cross correlation)归一化的交叉相关性,是数学上统计两组数据之间是否有关系的判断方法,貌似搞大数据分析比较流行相关性分析和计算。正常的计算公式如下: mxn表示窗口大小,这样的计算复杂度就为O(m x n x M x N)。从上面公式就可以看出其中均值和平方和可以通过积分图预计算得到,对于模板和目标图像大小一致的应用场景来说 NCC的计算公式可以表示为如下: 其中根据积分图像可以提前计算出任意窗口大小和与平方和,这样就对 上述两个计算实现了窗口半径无关的常量时间计算,唯一缺少的是下面计算公式 通过积分图像建立起来窗口下面的待检测图像与模板图像的和与平方和以及他们的交叉乘积五个积分图索引之后,这样就完成了整个预计算生成。依靠索引表查找计算结果,NCC就可以实现线性时间的复杂度计算,而且时间消耗近似常量跟窗口半径大小无关,完全可以满足实时对象检测工业环境工作条件。 二:算法步骤 1. 预计算模板图像和目标图像的积分图 2. 根据输入的窗口半径大小使用积分图完成NCC计算 3. 根据阈值得到匹配或者不匹配区域。 4. 输出结果 为了减小计算量,我们可以要把输入的图像转换为灰度图像,在灰度图像的基础上完成整个NCC计算检测。我们这个给出的基于RGB图像的NCC计算完整代码,读者可以在此基础上修改实现单通道图像检测。 三: 运行结果: 输入的模板图像与待检测图像,左边是模板图像,右边是待检测图像,左上角有明显污点。图像显示如下: 输入待检测图像与模板比较以及检测计算出NCC的图像显示如下: 其中左侧是待检测图像,上面有黑色污点,右侧输出的非黑色区域表明,程序已经发现此区域与标准模板不同,越白的区域表示周围跟模板相同位置反差越大,越是可疑的污染点,这样就可以得到准确定位,最终带检测图像绘制最可疑红色矩形窗口区域 四:相关代码实现 1. 计算两张图像每个像素交叉乘积的积分图代码如下: public void caculateXYSum(byte[] x, byte[] y, int width, int height) { if(x.length != y.length) return; xysum = new float[width*height]; this.width = width; this.height = height; // rows int px = 0, py = 0; int offset = 0, uprow=0, leftcol=0; float sp2=0, sp3=0, sp4=0; for(int row=0; row<height; row++ ) { offset = row*width; uprow = row-1; for(int col=0; col<width; col++) { leftcol=col-1; px=x[offset]&0xff; py=y[offset]&0xff; int p1 = px*py; // 计算平方查找表 sp2=(leftcol<0) ? 0:xysum[offset-1]; // p(x-1, y) sp3=(uprow<0) ? 0:xysum[offset-width]; // p(x, y-1); sp4=(uprow<0||leftcol<0) ? 0:xysum[offset-width-1]; // p(x-1, y-1); xysum[offset]=p1+sp2+sp3-sp4; offset++; } } } 获取任意窗口大小的交叉乘积的代码如下: public float getXYBlockSum(int x, int y, int m, int n) { int swx = x + n/2; int swy = y + m/2; int nex = x-n/2-1; int ney = y-m/2-1; float sum1, sum2, sum3, sum4; if(swx >= width) { swx = width - 1; } if(swy >= height) { swy = height - 1; } if(nex < 0) { nex = 0; } if(ney < 0) { ney = 0; } sum1 = xysum[ney*width+nex]; sum4 = xysum[swy*width+swx]; sum2 = xysum[swy*width+nex]; sum3 = xysum[ney*width+swx]; return ((sum1 + sum4) - sum2 - sum3); } 其余部分的积分图计算,参见本人博客《图像处理之积分图算法》2. 预计算建立积分图索引的代码如下: // per-calculate integral image for targetImage byte[] R = new byte[width * height]; byte[] G = new byte[width * height]; byte[] B = new byte[width * height]; getRGB(width, height, pixels, R, G, B); IntIntegralImage rii = new IntIntegralImage(); rii.setImage(R); rii.process(width, height); IntIntegralImage gii = new IntIntegralImage(); gii.setImage(G); gii.process(width, height); IntIntegralImage bii = new IntIntegralImage(); bii.setImage(B); bii.process(width, height); // setup the refer and target image index sum table rii.caculateXYSum(R, referRGB[0].getImage(), width, height); gii.caculateXYSum(G, referRGB[1].getImage(), width, height); bii.caculateXYSum(B, referRGB[2].getImage(), width, height); int size = (xr * 2 + 1) * (yr * 2 + 1); 3. 通过积分图查找实现快速NCC计算的代码如下: int r1=0, g1=0, b1=0; int r2=0, g2=0, b2=0; float sr1=0.0f, sg1=0.0f, sb1 = 0.0f; float sr2=0.0f, sg2=0.0f, sb2 = 0.0f; float xyr = 0.0f, xyg = 0.0f, xyb = 0.0f; for (int row = yr; row < height - yr; row++) { for (int col = xr; col < width - xr; col++) { r1 = rii.getBlockSum(col, row, (yr * 2 + 1), (xr * 2 + 1)); g1 = gii.getBlockSum(col, row, (yr * 2 + 1), (xr * 2 + 1)); b1 = bii.getBlockSum(col, row, (yr * 2 + 1), (xr * 2 + 1)); r2 = referRGB[0].getBlockSum(col, row, (yr * 2 + 1), (xr * 2 + 1)); g2 = referRGB[1].getBlockSum(col, row, (yr * 2 + 1), (xr * 2 + 1)); b2 = referRGB[2].getBlockSum(col, row, (yr * 2 + 1), (xr * 2 + 1)); sr1 = rii.getBlockSquareSum(col, row, (yr * 2 + 1), (xr * 2 + 1)); sg1 = gii.getBlockSquareSum(col, row, (yr * 2 + 1), (xr * 2 + 1)); sb1 = bii.getBlockSquareSum(col, row, (yr * 2 + 1), (xr * 2 + 1)); sr2 = referRGB[0].getBlockSquareSum(col, row, (yr * 2 + 1), (xr * 2 + 1)); sg2 = referRGB[1].getBlockSquareSum(col, row, (yr * 2 + 1), (xr * 2 + 1)); sb2 = referRGB[2].getBlockSquareSum(col, row, (yr * 2 + 1), (xr * 2 + 1)); xyr = rii.getXYBlockSum(col, row, (yr * 2 + 1), (xr * 2 + 1)); xyg = gii.getXYBlockSum(col, row, (yr * 2 + 1), (xr * 2 + 1)); xyb = bii.getXYBlockSum(col, row, (yr * 2 + 1), (xr * 2 + 1)); float nccr = calculateNCC(r1, r2, sr1, sr2, xyr, size); float nccg = calculateNCC(g1, g2, sg1, sg2, xyg, size); float nccb = calculateNCC(b1, b2, sb1, sb2, xyb, size); outPixels[row * width + col] = (nccr + nccg + nccb); } } System.out.println("time consum : " + (System.currentTimeMillis() - time)); 4. 归一化输出NCC图像与结果代码如下: // normalization the data float max = 0.0f, min = 100.0f; for(int i=0; i<outPixels.length; i++) { max = Math.max(max, outPixels[i]); min = Math.min(min, outPixels[i]); } // create output image float delta = max - min; BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); int ry = -1; int rx = -1; for(int row = 0; row<height; row++) { for(int col=0; col<width; col++) { int gray = (int)(((outPixels[row*width+col]-min) / delta) *255); gray = 255 - gray; if(min == outPixels[row*width+col]) { bi.setRGB(col, row, Color.RED.getRGB()); ry = row; rx = col; } else { int color = (0xff << 24) | (gray << 16) | (gray << 8) | gray; bi.setRGB(col, row, color); } } } if(rx > 0 && ry > 0) { Graphics2D g2d = image.createGraphics(); g2d.setPaint(Color.RED); g2d.drawRect(rx-xr, ry-yr, xr*2, yr*2); } 相比传统的NCC计算方法,此方法的计算效率是传统方法几百倍提升,而且窗口越大效率提升越明显,有人对此作出的统计如下: 可见基于积分图快速NCC可以极大提升执行效率减少计算时间,实现窗口半径无关NCC比较。 最后 本文是关于积分图使用的第三篇文章,可以说积分图在实际图像处理中应用十分广泛,本人会继续努力深挖与大家分享。希望各位顶下次文以表支持, 谢谢!本人坚持分享有用实用的图像处理算法!需要大家多多支持。
一:前期微信支付扫盲知识 前提条件是已经有申请了微信支付功能的公众号,然后我们需要得到公众号APPID和微信商户号,这个分别在微信公众号和微信支付商家平台上面可以发现。其实在你申请成功支付功能之后,微信会通过邮件把Mail转给你的,有了这些信息之后,我们就可以去微信支付服务支持页面:https://pay.weixin.qq.com/service_provider/index.shtml 打开这个页面,点击右上方的链接【开发文档】会进入到API文档说明页面,看起来如下 选择红色圆圈的扫码支付就是我们要做接入方式,鼠标移动到上面会提示你去查看开发文档,如果这个都不知道怎么查看,可以洗洗睡了,你真的不合适做程序员,地址如下: https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_1在浏览器中打开之后会看到 我们重点要关注和阅读的内容我已经用红色椭圆标注好了,首先阅读【接口规则】里面的协议规范,开玩笑这个都不读你就想做微信支付,这个就好比你要去泡妞,得先收集点基本背景信息,了解对方特点,不然下面还怎么沟通。事实证明只有会泡妞得程序员才是好销售。跑题了我们接下来要看一下【场景介绍】中的案例与规范,只看一下记得一定要微信支付的LOGO下载下来,是为了最后放到我们自己的扫码支付网页上,这样看上去比较专业一点。之后重点关注【模式二】 我们这里就是要采用模式二的方式实现PC端页面扫码支付功能。 微信官方对模式二的解释是这样的“商户后台系统先调用微信支付的统一下单接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。注意:code_url有效期为2小时,过期后扫码不能再发起支付”。看明白了吧就是我们首先要调用微信提供统一下单接口,得到一个关键信息code_url(至于这个code_url是什么鬼,我也不知道),然后我们通过自己的程序把这个URL生成一个二维码,生成二维码我这里用了Google的zxing库。然后把这个二维码显示在你的PC端网页上就行啦。这样终端用户一扫码就支付啦,支付就完成啦,看到这里你肯定很激动,发现微信支付如此简单,等等还有个事情我们还不知道,客户知道付钱了,我们服务器端还不知道呢,以微信开发人员的智商他们早就想到这个问题了,所以让你在调用统一下单接口的时候其中有个必填的参数就是回调URL,就是如果客户端付款成功之后微信会通过这个URL向我们自己的服务器提交一些数据,然后我们后台解析这些数据,完成我们自己操作。这样我们才知道客户是否真的已经通过微信付款了。这样整个流程才结束,这个就是模式二。微信用一个时序图示这样表示这个过程的。 表达起来比较复杂,看上去比较吃力,总结一下其实我们服务器该做的事情就如下件: 1. 通过统一下单接口传入正确的参数(当然要包括我们的回调URL)与签名验证,从返回数据中得到code_url的对应数据 2. 根据code_url的数据我们自己生成一个二维码图片,显示在浏览器网页上 3. 在回调的URL中添加我们自己业务逻辑处理。 至此扫盲结束了,你终于知道扫码支付什么个什么样的流程了,下面我们就一起来扒扒它的相关API使用,做好每步处理。 二:开发过程 在开发代码之前,请先准备几件事情。 1. 添加ZXing的maven依赖 2. 添加jdom的maven依赖 3.下载Java版本SDK演示程序,地址在这里 https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=11_1 我们需要MD5Util.java和XMLUtil.java两个文件 4. 我们使用HttpClient版本是4.5.1,记得添加Maven依赖 上面准备工作做好以后,继续往下看: 首先我们要调用微信的统一下单接口,我们点击【API列表】中的统一下单会看到这样页面: 以本人调用实际情况为例,如下的参数是必须要有的,为了大家的方便我已经把它变成一个POJO的对象, 代码如下: public class UnifiedorderDto implements WeiXinConstants { private String appid; private String body; private String device_info; private String mch_id; private String nonce_str; private String notify_url; private String openId; private String out_trade_no; private String spbill_create_ip; private int total_fee; private String trade_type; private String product_id; private String sign; public UnifiedorderDto() { this.appid = APPID; this.mch_id = WXPAYMENTACCOUNT; this.device_info = DEVICE_INFO_WEB; this.notify_url = CALLBACK_URL; this.trade_type = TRADE_TYPE_NATIVE; } public String getAppid() { return appid; } public void setAppid(String appid) { this.appid = appid; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } public String getDevice_info() { return device_info; } public void setDevice_info(String device_info) { this.device_info = device_info; } public String getMch_id() { return mch_id; } public void setMch_id(String mch_id) { this.mch_id = mch_id; } public String getNonce_str() { return nonce_str; } public void setNonce_str(String nonce_str) { this.nonce_str = nonce_str; } public String getNotify_url() { return notify_url; } public void setNotify_url(String notify_url) { this.notify_url = notify_url; } public String getOpenId() { return openId; } public void setOpenId(String openId) { this.openId = openId; } public String getOut_trade_no() { return out_trade_no; } public void setOut_trade_no(String out_trade_no) { this.out_trade_no = out_trade_no; } public String getSpbill_create_ip() { return spbill_create_ip; } public void setSpbill_create_ip(String spbill_create_ip) { this.spbill_create_ip = spbill_create_ip; } public int getTotal_fee() { return total_fee; } public void setTotal_fee(int total_fee) { this.total_fee = total_fee; } public String getTrade_type() { return trade_type; } public void setTrade_type(String trade_type) { this.trade_type = trade_type; } public String getSign() { return sign; } public void setSign(String sign) { this.sign = sign; } public String getProduct_id() { return product_id; } public void setProduct_id(String product_id) { this.product_id = product_id; } public String generateXMLContent() { String xml = "<xml>" + "<appid>" + this.appid + "</appid>" + "<body>" + this.body + "</body>" + "<device_info>WEB</device_info>" + "<mch_id>" + this.mch_id + "</mch_id>" + "<nonce_str>" + this.nonce_str + "</nonce_str>" + "<notify_url>" + this.notify_url + "</notify_url>" + "<out_trade_no>" + this.out_trade_no + "</out_trade_no>" + "<product_id>" + this.product_id + "</product_id>" + "<spbill_create_ip>" + this.spbill_create_ip+ "</spbill_create_ip>" + "<total_fee>" + String.valueOf(this.total_fee) + "</total_fee>" + "<trade_type>" + this.trade_type + "</trade_type>" + "<sign>" + this.sign + "</sign>" + "</xml>"; return xml; } public String makeSign() { String content ="appid=" + this.appid + "&body=" + this.body + "&device_info=WEB" + "&mch_id=" + this.mch_id + "&nonce_str=" + this.nonce_str + "¬ify_url=" + this.notify_url + "&out_trade_no=" + this.out_trade_no + "&product_id=" + this.product_id + "&spbill_create_ip=" + this.spbill_create_ip+ "&total_fee=" + String.valueOf(this.total_fee) + "&trade_type=" + this.trade_type; content = content + "&key=" + WeiXinConstants.MD5_API_KEY; String esignature = WeiXinPaymentUtil.MD5Encode(content, "utf-8"); return esignature.toUpperCase(); } } 其中各个成员变量的解释可以参见【统一下单接口】的说明即可。 有这个之后我们就要要设置的内容填写进去,去调用该接口得到返回数据,从中拿到code_url的数据然后据此生成一个二维图片,把图片的地址返回给PC端网页,然后它就会显示出来,这里要特别说明一下,我们自己PC端网页在点击微信支付的时候就会通过ajax方式调用我们自己后台的SpringMVC Controller然后在Controller的对应方法中通过HTTPClient完成对微信统一下单接口调用解析返回的XML数据得到code_url的值,生成二维码之后返回给前台网页。Controller中实现的代码如下: Map<String,Object> result=new HashMap<String,Object>(); UnifiedorderDto dto = new UnifiedorderDto(); if(cash == null || "".equals(cash)) { result.put("error", "cash could not be zero"); return result; } int totalfee = 100*Integer.parseInt(cash); logger.info("total recharge cash : " + totalfee); dto.setProduct_id(String.valueOf(System.currentTimeMillis())); dto.setBody("repair"); dto.setNonce_str(String.valueOf(System.nanoTime())); LoginInfo loginInfo = LoginInfoUtil.getLoginInfo(); // 通过我们后台订单号+UUID为身份识别标志 dto.setOut_trade_no("你的订单号+关键信息,微信回调之后传回,你可以验证"); dto.setTotal_fee(totalfee); dto.setSpbill_create_ip("127.0.0.1"); // generate signature dto.setSign(dto.makeSign()); logger.info("sign : " + dto.makeSign()); logger.info("xml content : " + dto.generateXMLContent()); try { HttpClient httpClient = HttpClientBuilder.create().build(); HttpPost post = new HttpPost(WeiXinConstants.UNIFIEDORDER_URL); post.addHeader("Content-Type", "text/xml; charset=UTF-8"); StringEntity xmlEntity = new StringEntity(dto.generateXMLContent(), ContentType.TEXT_XML); post.setEntity(xmlEntity); HttpResponse httpResponse = httpClient.execute(post); String responseXML = EntityUtils.toString(httpResponse.getEntity(), "UTF-8"); logger.info("response xml content : " + responseXML); // parse CODE_URL CONTENT Map<String, String> resultMap = (Map<String, String>)XMLUtil.doXMLParse(responseXML); logger.info("response code_url : " + resultMap.get("code_url")); String codeurl = resultMap.get("code_url"); if(codeurl != null && !"".equals(codeurl)) { String imageurl = generateQrcode(codeurl); result.put("QRIMAGE", imageurl); } post.releaseConnection(); } catch(Exception e) { e.printStackTrace(); } result.put("success", "1"); return result; 生成二维码的代码如下: private String generateQrcode(String codeurl) { File foldler = new File(basePath + "qrcode"); if(!foldler.exists()) { foldler.mkdirs(); } String f_name = UUIDUtil.uuid() + ".png"; try { File f = new File(basePath + "qrcode", f_name); FileOutputStream fio = new FileOutputStream(f); MultiFormatWriter multiFormatWriter = new MultiFormatWriter(); Map hints = new HashMap(); hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); //设置字符集编码类型 BitMatrix bitMatrix = null; bitMatrix = multiFormatWriter.encode(codeurl, BarcodeFormat.QR_CODE, 300, 300,hints); BufferedImage image = toBufferedImage(bitMatrix); //输出二维码图片流 ImageIO.write(image, "png", fio); return ("qrcode/" + f_name); } catch (Exception e1) { e1.printStackTrace(); return null; } } 此时如何客户端微信扫码之后,微信就会通过回调我们制定URL返回数据给我们。在回调方法中完成我们自己的处理,这里要特别注意的是你的回调接口必须通过HTTP POST方法实现,否则无法接受到XML数据。回调处理的代码如下: @RequestMapping(value = "/your_callback_url", method = RequestMethod.POST) @ResponseBody public void finishPayment(HttpServletRequest request, HttpServletResponse response) { try { logger.info("start to callback from weixin server: " + request.getRemoteHost()); Map<String, String> resultMap = new HashMap<String, String>(); InputStream inputStream = request.getInputStream(); // 读取输入流 SAXBuilder saxBuilder= new SAXBuilder(); Document document = saxBuilder.build(inputStream); // 得到xml根元素 Element root = document.getRootElement(); // 得到根元素的所有子节点 List list = root.getChildren(); Iterator it = list.iterator(); while(it.hasNext()) { Element e = (Element) it.next(); String k = e.getName(); String v = ""; List children = e.getChildren(); if(children.isEmpty()) { v = e.getTextNormalize(); } else { v = XMLUtil.getChildrenText(children); } resultMap.put(k, v); } // 验证签名!!! /* String[] keys = resultMap.keySet().toArray(new String[0]); Arrays.sort(keys); String kvparams = ""; for(int i=0; i<keys.length; i++) { if(keys[i].equals("esign")) { continue; } // 签名算法 if(i == 0) { kvparams += (keys[i] + "=" + resultMap.get(keys[i])); } else { kvparams += ("&" + keys[i] + "=" + resultMap.get(keys[i])); } } String esign = kvparams + "&key=" + WeiXinConstants.MD5_API_KEY; String md5esign = WeiXinPaymentUtil.MD5Encode(esign, "UTF-8"); if(!md5esign.equals(resultMap.get("sign"))) { return; }*/ //关闭流 // 释放资源 inputStream.close(); inputStream = null; String returnCode = resultMap.get("return_code"); String outtradeno = resultMap.get("out_trade_no"); // 以分为单位 int nfee = Integer.parseInt(resultMap.get("total_fee")); logger.info("out trade no : " + outtradeno); logger.info("total_fee : " + nfee); // 业务处理流程 if("SUCCESS".equals(returnCode)) { // TODO: your business process add here response.getWriter().print(XMLUtil.getRetResultXML(resultMap.get("return_code"), resultMap.get("return_code"))); } else { response.getWriter().print(XMLUtil.getRetResultXML(resultMap.get("return_code"), resultMap.get("return_msg"))); } } catch(IOException ioe) { ioe.printStackTrace(); } catch (JDOMException e1) { e1.printStackTrace(); } } 微信官方Java版Demo用到的XMLUtil和MD5Util的两个类记得拿过来改一下,演示代码可以在它的官方演示页面找到,相关maven依赖如下: <dependency> <groupId>jdom</groupId> <artifactId>jdom</artifactId> <version>1.1</version> </dependency> <dependency> <groupId>com.google.zxing</groupId> <artifactId>core</artifactId> <version>3.3.0</version> </dependency> 最后要特别注意的是关于签名,签名生成MD5的类我是从微信官网直接下载Java版Demo程序获取的,建议你也是,因为这个是确保MD5签名是一致的最佳选择。具体的生成签名的算法可以查看微信官方文档,这里也强烈建议大家一定要官方API说明,你开发中所遇到各种问题90%都是因为不看官方文档,而是轻信某人博客!这个才是我写这篇文章的真正目的和用意,根据官方文档,用我的Java代码实现,微信PC端网页扫码支付必定在你的WEB应用中飞起来。
图像处理之形态学梯度计算 源代码基于OpenCV实现,原因是太懒了,不想再用Java从头写了! 一:概念介绍 形态学操作膨胀与腐蚀图像形态学中最基本的两个形态学操作、常常被组合起来一起使用实现一些复杂的图像形态学操作,计算图像的形态学梯度是形态学重要操作之一是有膨胀和腐蚀基础操作适当的组合形成。可以计算的梯度常见如下四种: 基本梯度 基本梯度是用膨胀后的图像减去腐蚀后的图像得到差值图像,称为梯度图像也是OpenCV中支持的计算形态学梯度的方法,而此方法得到梯度有被称为基本梯度。 内部梯度 是用原图像减去腐蚀之后的图像得到差值图像,称为图像的内部梯度 外部梯度 图像膨胀之后再减去原来的图像得到的差值图像,称为图像的外部梯度。 方向梯度 方向梯度是使用X方向与Y方向的直线作为结构元素之后得到图像梯度,X的结构元素分布膨胀与腐蚀得到图像之后求差值得到称为X方向梯度,用Y方向直线做结构分别膨胀与腐蚀之后得到图像求差值之后称为Y方向梯度。 二:主要API介绍 OpenCV中腐蚀与膨胀操作的API分别为: erode() // 腐蚀 第一个参数是输入图像、第二个参数是输出图像、第三个参数结构元素、第四个表示锚点位置,默认情况下Point(-1, -1),膨胀跟腐蚀的输入参数一样。 dilate() // 膨胀 使用的结构元素通过如下的API创建: getStructuringElement()创建结构元素,第一个参数是结构元素形状、第二个参数大小一定要是奇数。 第三个参数表示中心位置,默认Point(-1, -1) 两张图像相减的API如下: subtract()第一个与第二个参数为输入图像Src1和Src2第三个参数是得到的计算后图像dst 计算公式是Dst = saturate(Src1 - Src2) 三:运行效果 XY方向 演示代码如下: #include <opencv2/opencv.hpp> #include <iostream> #include <math.h> using namespace std; using namespace cv; int main(int argc, char** argv) { Mat src, gray_src, dst; src = imread("D:/vcprojects/images/carlines.png"); if (!src.data) { printf("could not load image...\n"); return -1; } char input_title[] = "input image"; char output_title[] = "Basic Gradient Image"; namedWindow(input_title, CV_WINDOW_AUTOSIZE); namedWindow(output_title, CV_WINDOW_AUTOSIZE); imshow(input_title, src); cvtColor(src, gray_src, CV_BGR2GRAY); // calculate basic gradient same as morphologyEx() in opencv Mat kernel = getStructuringElement(MORPH_RECT, Size(5, 5), Point(-1, -1)); Mat erode_ouput, dilate_output; erode(gray_src, erode_ouput, kernel, Point(-1, -1)); dilate(gray_src, dilate_output, kernel, Point(-1, -1)); // calculate basic gradient subtract(dilate_output, erode_ouput, dst, Mat()); imshow(output_title, dst); // calculate internal gradient Mat internalGradientImg; subtract(gray_src, erode_ouput, internalGradientImg, Mat()); imshow("Internal Gradient", internalGradientImg); // calculate external gradient Mat externalGradientImg; subtract(dilate_output, gray_src, externalGradientImg, Mat()); imshow("External Gradient", externalGradientImg); // directional gradient Mat hse = getStructuringElement(MORPH_RECT, Size(src.cols/16, 1), Point(-1, -1)); Mat vse = getStructuringElement(MORPH_RECT, Size(1, src.rows/16), Point(-1, -1)); Mat erode_direct, dilate_direct; Mat binImg, xdirectImg, ydirectImg; // 二值化方法 threshold(gray_src, binImg, 0, 255, CV_THRESH_OTSU | CV_THRESH_BINARY); // X direction erode(binImg, erode_direct, hse, Point(-1, -1)); dilate(binImg, dilate_direct, hse, Point(-1, -1)); subtract(dilate_direct, erode_direct, xdirectImg, Mat()); imshow("X directional gradient", xdirectImg); // Y direction erode(binImg, erode_direct, vse, Point(-1, -1)); dilate(binImg, dilate_direct, vse, Point(-1, -1)); subtract(dilate_direct, erode_direct, ydirectImg, Mat()); imshow("Y directional gradient", ydirectImg); waitKey(0); return 0; } 欢迎加入图像处理学习群,里面高手林立!
图像处理之图像内插值与外插值 两张图像混合时通过内插与外插值方法可以实现图像亮度、对比度、饱和度、填色、锐化等常见的图像处理操作。在两张图像混合时最常见是线性插值方法,使用的混合权重公式如下: 这个就是两张图像最常见的混合公式,其实我们很少考虑到值大于1的情况,当这个时候得到的效果跟在值属于[0,1]之间相反,我们称之为两张图像混合的外插值方法,而常见的值属于[0,1]之间称之为内插值方法。外插值可以用来生成跟内插值效果相反的图像,比如内插值模糊图像,通过外插值可以去模糊,外插值可以调节饱和度,可以实现图像一些列的处理比如亮度、饱和度、对比度、锐化调整。 一:改变亮度 创建一张跟输入图像大小一致的黑色图像,对图像混合时使用内插值方法,我们可以得到一个比较暗的版本图像,通过混合时候使用外插值方法,得到一个亮度更高的版本图像。效果如下: 代码如下: package com.gloomyfish.ii.demo; import java.awt.image.BufferedImage; import java.util.Arrays; public class AdjustBrightness extends AbstractImageOptionFilter { private float alpha; public AdjustBrightness() { alpha = 0.5f; } public float getAlpha() { return alpha; } public void setAlpha(float alpha) { this.alpha = alpha; } @Override public BufferedImage process(BufferedImage image) { int width = image.getWidth(); int height = image.getHeight(); // create black image BufferedImage bimage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); int r = 0, g = 0, b = 0; int pixel = (0xff << 24) | (r << 16) | (g << 8) | b; int[] values = new int[width * height]; Arrays.fill(values, pixel); setRGB(bimage, 0, 0, width, height, values); // adjust contrast int[] pixels = new int[width * height]; int[] outPixels = new int[width * height]; getRGB(image, 0, 0, width, height, pixels); int index = 0; for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { index = row*width + col; // black image int r1 = (values[index]&0xff0000)>>16; int g1 = (values[index]&0xff00)>>8; int b1 = values[index]&0xff; // target image int r2 = (pixels[index]&0xff0000)>>16; int g2 = (pixels[index]&0xff00)>>8; int b2 = pixels[index]&0xff; r = clamp((int)(r1*(1.0 - alpha) + r2*alpha)); g = clamp((int)(g1*(1.0 - alpha) + g2*alpha)); b = clamp((int)(b1*(1.0 - alpha) + b2*alpha)); outPixels[index] = (0xff << 24) | (r << 16) | (g << 8) | b; } } BufferedImage dest = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); setRGB(dest, 0, 0, width, height, outPixels); return dest; } } 二:调整对比度 计算当前输入图像的平均亮度得到一张常量亮度的图像,用该图像跟原图像进行权重混合当alpha值在0到1之间是内插值得到对比度降低的图片,当值大于1时候是外插值得到对比度提高图片。效果如下: 代码如下: package com.gloomyfish.ii.demo; import java.awt.image.BufferedImage; import java.util.Arrays; public class AdjustContrast extends AbstractImageOptionFilter { private float alpha; public AdjustContrast() { alpha = 0.5f; } public float getAlpha() { return alpha; } public void setAlpha(float alpha) { this.alpha = alpha; } @Override public BufferedImage process(BufferedImage image) { int width = image.getWidth(); int height = image.getHeight(); int[] pixels = new int[width * height]; int[] outPixels = new int[width * height]; getRGB(image, 0, 0, width, height, pixels); // the average luminance image int[] values = createLuminanceImage(width, height, pixels); int r=0, g=0, b=0; // adjust contrast int index = 0; for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { index = row*width + col; // constant means image int r1 = (values[index]&0xff0000)>>16; int g1 = (values[index]&0xff00)>>8; int b1 = values[index]&0xff; // target image int r2 = (pixels[index]&0xff0000)>>16; int g2 = (pixels[index]&0xff00)>>8; int b2 = pixels[index]&0xff; r = clamp((int)(r1*(1.0 - alpha) + r2*alpha)); g = clamp((int)(g1*(1.0 - alpha) + g2*alpha)); b = clamp((int)(b1*(1.0 - alpha) + b2*alpha)); outPixels[index] = (0xff << 24) | (r << 16) | (g << 8) | b; } } BufferedImage dest = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); setRGB(dest, 0, 0, width, height, outPixels); return dest; } private int[] createLuminanceImage(int width, int height, int[] pixels) { int r = 0, g = 0, b = 0; int index = 0; double sumr = 0, sumg = 0, sumb = 0; for(int row=0; row<height; row++) { for(int col=0; col<width; col++) { r = (pixels[index]&0xff0000)>>16; g = (pixels[index]&0xff00)>>8; b = pixels[index]&0xff; sumr += r; sumg += g; sumb += b; // double gray = (0.2126 * r + 0.7152 * g + 0.0722 * b); } } int tp = width * height; r= (int)(sumr / tp); g= (int)(sumg / tp); b= (int)(sumb / tp); int pixel = (0xff << 24) | (r << 16) | (g << 8) | b; int[] values = new int[width * height]; Arrays.fill(values, pixel); return values; } } 三:模糊与锐化 对输入图像进行模糊得到一张模糊版本的图像跟原图像进行混合当alpha值在0~1之间时内插值得到轻微模糊图像,当值大于1时候得到反模糊的锐化效果图像。效果显示如下: 代码显示如下: package com.gloomyfish.ii.demo; import java.awt.image.BufferedImage; public class AdjustBlurAndSharpen extends AbstractImageOptionFilter { private float alpha; private BufferedImage blurImage; public AdjustBlurAndSharpen() { alpha = 0.5f; } public float getAlpha() { return alpha; } public void setAlpha(float alpha) { this.alpha = alpha; } public BufferedImage getBlurImage() { return blurImage; } public void setBlurImage(BufferedImage blurImage) { this.blurImage = blurImage; } @Override public BufferedImage process(BufferedImage image) { int width = image.getWidth(); int height = image.getHeight(); // read blur image int r=0, g=0, b=0; int[] values = new int[width * height]; getRGB(blurImage, 0, 0, width, height, values); // adjust contrast int[] pixels = new int[width * height]; int[] outPixels = new int[width * height]; getRGB(image, 0, 0, width, height, pixels); int index = 0; for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { index = row * width + col; // blur image int r1 = (values[index] & 0xff0000) >> 16; int g1 = (values[index] & 0xff00) >> 8; int b1 = values[index] & 0xff; // original image int r2 = (pixels[index] & 0xff0000) >> 16; int g2 = (pixels[index] & 0xff00) >> 8; int b2 = pixels[index] & 0xff; // final result r = clamp((int) (r1 * (1.0 - alpha) + r2 * alpha)); g = clamp((int) (g1 * (1.0 - alpha) + g2 * alpha)); b = clamp((int) (b1 * (1.0 - alpha) + b2 * alpha)); outPixels[index] = (0xff << 24) | (r << 16) | (g << 8) | b; } } BufferedImage dest = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); setRGB(dest, 0, 0, width, height, outPixels); return dest; } } 内插值与外插值通过两张图像的权重混合巧妙的实现了常见的图像亮度、对比度、模糊与锐化操作,这样的操作简单直观,避免了亮度调整时候色彩空间转换和锐化时候需要掩膜计算的问题。是一种新的调整图像对比度、亮度、模糊与锐化的手段。本篇文章也是本人第100篇图像处理文章,分享有用知识,各位支持是我坚持下来的最大动力
JavaJNI开发时常用数据类型与C++中数据类型转换 常见的数据类型对应关系如下: 此外我们经常用String类型,它可以通过如下的API实现jstring到char*之间的相互转换constchar* szString = env-> GetStringUTFChars(jstring, 0);这种转换出来的char*类型数据,使用完之后一定要调用 env-> ReleaseStringUTFChars(jstring, szString);释放掉,不然会导致内存泄漏,如果忘记JVM会崩溃的。从C++中创建一个新的字符串然后返回的代码如下: // create jstring jstring computerName = env->NewStringUTF(chRtn); return computerName; 其中chRtn是char数组类型的指针。 下面是一个Java通过JNI接口调用C++的Windows接口实现电脑名称和用户名称查询的例子,首先定义JNI接口类如下package com.gloomyfish.jnidemo; public class HelloJNI { public native int helloJNI(String name); public native String getComputerName(); public native String getUserName(); public native double calculateDistance(double x, double y); public static void main(String[] args) { System.loadLibrary("jnitest"); HelloJNI happ = new HelloJNI(); happ.helloJNI("gloomyfish"); double sum = happ.calculateDistance(3, 4); String computerName = happ.getComputerName(); String user = happ.getUserName(); System.out.println("sum : " + sum); System.out.println("computer name : " + computerName); System.out.println("current user : " + user); System.out.println(); } } C++中实现如下: // jnitest.cpp : 定义 DLL 应用程序的导出函数。 // #include "stdafx.h" #include <windows.h> #include <stdlib.h> #include "math.h" #include "com_gloomyfish_jnidemo_HelloJNI.h" JNIEXPORT jint JNICALL Java_com_gloomyfish_jnidemo_HelloJNI_helloJNI (JNIEnv *env, jobject obj, jstring param) { // convert java string type to c++ char* type const char* name = env->GetStringUTFChars(param, 0); printf("%s %s\n", "Hello JNI, I am ", name); // release memory env->ReleaseStringUTFChars(param, name); // return 0; return 0; } JNIEXPORT jstring JNICALL Java_com_gloomyfish_jnidemo_HelloJNI_getComputerName (JNIEnv *env, jobject obj) { // define the buffer size const int MAX_BUFFER_LEN = 500; TCHAR infoBuf[MAX_BUFFER_LEN]; DWORD bufCharCount = MAX_BUFFER_LEN; GetComputerName(infoBuf, &bufCharCount); // conver to jstring printf("computer name : %ls\n", infoBuf); int nLen = WideCharToMultiByte(CP_ACP, 0, infoBuf, -1, NULL, 0, NULL, NULL); char* chRtn = new char[nLen]; WideCharToMultiByte(CP_ACP, 0, infoBuf, -1, chRtn, nLen, NULL, NULL); // create jstring jstring computerName = env->NewStringUTF(chRtn); return computerName; } JNIEXPORT jstring JNICALL Java_com_gloomyfish_jnidemo_HelloJNI_getUserName (JNIEnv *env, jobject obj) { // define the buffer size const int MAX_BUFFER_LEN = 500; TCHAR infoBuf[MAX_BUFFER_LEN]; DWORD bufCharCount = MAX_BUFFER_LEN; GetUserName(infoBuf, &bufCharCount); // conver to jstring printf("current user : %ls\n", infoBuf); int nLen = WideCharToMultiByte(CP_ACP, 0, infoBuf, -1, NULL, 0, NULL, NULL); char* chRtn = new char[nLen]; WideCharToMultiByte(CP_ACP, 0, infoBuf, -1, chRtn, nLen, NULL, NULL); // create jstring jstring username = env->NewStringUTF(chRtn); return username; } JNIEXPORT jdouble JNICALL Java_com_gloomyfish_jnidemo_HelloJNI_calculateDistance (JNIEnv *env, jobject obj, jdouble x, jdouble y) { double cx = x; double cy = y; double sum = pow(cx, 2) + pow(cy, 2); return sqrt(sum); } 运行结果如下: 其中通过命令行实现JNI头文件生成,命令如下: javah com.gloomyfish.jnidemo.HelloJNI
图像处理之积分图像应用一(半径无关的快速模糊算法) 一:基本原理概述 传统的图像空间域卷积模糊算法,当窗口大小改变时卷积模糊时间也会变化,而且随着窗口尺寸越大计算量也越大,算法运行时间约越长。在很多时候无法满足实时性要求。而基于积分图像可以实现对窗口区域和大小的快速计算,把传统卷积模糊计算受窗口大小影响消除,把卷积模糊变成一个与窗口大小半径无关的常量时间完成的操作。关于如何从图像本身得到积分图像的算法请看上一篇文章《图像处理之积分图像算法》 二:详细解释 以5x5的窗口大小为例,假设图像I、积分图像II、处理之后模糊图像BI、则传统空间域卷积实现的图像均值模糊对每个像素点公式表示如下: 基于积分图像计算每个像素点模糊公式表示如下: 上述基于传统的均值模糊计算得到模糊之后的结果要计算24次加法和一次除法共计25次计算,而基于积分图像则只需要一次加法两次减法和一次除法共计四次计算,而且基于传统卷积均值模糊计算当窗口大小越大计算次数也越多,而基于积分图像则计算次数保持常量不变,是一个半径无关的均值模糊算法。 三:代码实现 积分图像算法实现参见:http://blog.csdn.net/jia20003/article/details/52710751 传统模式的卷积模糊代码如下: package com.gloomyfish.ii.demo; import java.awt.image.BufferedImage; public class Convolution2DFilter extends AbstractImageOptionFilter { // 窗口半径大小 private int xr; private int yr; public Convolution2DFilter() { xr = 1; yr = 1; } public int getXr() { return xr; } public void setXr(int xr) { this.xr = xr; } public int getYr() { return yr; } public void setYr(int yr) { this.yr = yr; } @Override public BufferedImage process(BufferedImage image) { long time = System.currentTimeMillis(); int width = image.getWidth(); int height = image.getHeight(); int[] pixels = new int[width * height]; int[] outPixels = new int[width * height]; getRGB(image, 0, 0, width, height, pixels); int size = (xr * 2 + 1) * (yr * 2 + 1); int r = 0, g = 0, b = 0; for (int row = yr; row < height - yr; row++) { for (int col = xr; col < width - xr; col++) { int sr = 0, sg = 0, sb = 0; // 鍗风Н鎿嶄綔/妯℃澘璁$畻 for (int i = -yr; i <= yr; i++) { int roffset = row + i; for (int j = -xr; j <= xr; j++) { int coffset = col + j; sr += ((pixels[roffset * width + coffset] >> 16) & 0xff); sg += (pixels[roffset * width + coffset] >> 8) & 0xff; sb += (pixels[roffset * width + coffset] & 0xff); } } r = sr / size; g = sg / size; b = sb / size; outPixels[row * width + col] = (0xff << 24) | (r << 16) | (g << 8) | b; } } System.out.println("Convolution2DFilter ->> time duration : " + (System.currentTimeMillis() - time)); BufferedImage dest = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); setRGB(dest, 0, 0, width, height, outPixels); return dest; } } 基于积分图像的快速模糊代码如下: package com.gloomyfish.ii.demo; import java.awt.image.BufferedImage; public class FastBlurFilter extends AbstractImageOptionFilter { // 窗口半径大小 private int xr; private int yr; public FastBlurFilter() { xr = 1; yr = 1; } public int getXr() { return xr; } public void setXr(int xr) { this.xr = xr; } public int getYr() { return yr; } public void setYr(int yr) { this.yr = yr; } @Override public BufferedImage process(BufferedImage image) { long time = System.currentTimeMillis(); int width = image.getWidth(); int height = image.getHeight(); // get image data int[] pixels = new int[width * height]; int[] outPixels = new int[width * height]; getRGB(image, 0, 0, width, height, pixels); int size = (xr * 2 + 1) * (yr * 2 + 1); int r = 0, g = 0, b = 0; // per-calculate integral image byte[] R = new byte[width*height]; byte[] G = new byte[width*height]; byte[] B = new byte[width*height]; getRGB(width, height, pixels, R, G, B); IntIntegralImage rii = new IntIntegralImage(); rii.setImage(R); rii.process(width, height); IntIntegralImage gii = new IntIntegralImage(); gii.setImage(G); gii.process(width, height); IntIntegralImage bii = new IntIntegralImage(); bii.setImage(B); bii.process(width, height); for (int row = yr; row < height - yr; row++) { for (int col = xr; col < width - xr; col++) { int sr = rii.getBlockSum(col, row, (yr * 2 + 1), (xr * 2 + 1)); int sg = gii.getBlockSum(col, row, (yr * 2 + 1), (xr * 2 + 1)); int sb = bii.getBlockSum(col, row, (yr * 2 + 1), (xr * 2 + 1)); r = sr / size; g = sg / size; b = sb / size; outPixels[row * width + col] = (0xff << 24) | (r << 16) | (g << 8) | b; } } System.out.println("FastBlurFilter ->> time duration : " + (System.currentTimeMillis() - time)); BufferedImage dest = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); setRGB(dest, 0, 0, width, height, outPixels); return dest; } /** Returns the red, green and blue planes as 3 byte arrays. */ public void getRGB(int width, int height, int[] pixels, byte[] R, byte[] G, byte[] B) { int c, r, g, b; for (int i=0; i < width*height; i++) { c = pixels[i]; r = (c&0xff0000)>>16; g = (c&0xff00)>>8; b = c&0xff; R[i] = (byte)r; G[i] = (byte)g; B[i] = (byte)b; } } } 四:效率之比 分别把窗口半径调整到1、3、10、20的情况下,对同一张图像做模糊处理,CPU耗时直方图如下: 可见在其它条件不改变的情况下,窗口半径越大,两者之间执行时间差距越大。各位国庆节快乐!
图像处理之积分图算法 一:积分图来源与发展 积分图像是Crow在1984年首次提出,是为了在多尺度透视投影中提高渲染速度。随后这种技术被应用到基于NCC的快速匹配、对象检测和SURF变换中、基于统计学的快速滤波器等方面。积分图像是一种在图像中快速计算矩形区域和的方法,这种算法主要优点是一旦积分图像首先被计算出来我们可以计算图像中任意大小矩形区域的和而且是在常量时间内。这样在图像模糊、边缘提取、对象检测的时候极大降低计算量、提高计算速度。第一个应用积分图像技术的应用是在Viola-Jones的对象检测框架中出现。 二:积分图像概念 在积分图像(Integral Image - ii)上任意位置(x, y)处的ii(x, y)表示该点左上角所有像素之和,表示如下: 从给定图像I从上到下、从左到右计算得到和的积分图像公式如下: 其中(x<0 || y<0) 时ii(x,y)=0, i(x,y)=0 得到积分图像之后,图像中任意矩形区域和通过如下公式计算: 三:代码实现: 积分图像算法的Java代码实现如下: package com.gloomyfish.ii.demo; public class IntIntegralImage extends AbstractByteProcessor { // sum index tables private int[] sum; private int[] squaresum; // image private byte[] image; private int width; private int height; public byte[] getImage() { return image; } public void setImage(byte[] image) { this.image = image; } public int getBlockSum(int x, int y, int m, int n) { int swx = x + n/2; int swy = y + m/2; int nex = x-n/2-1; int ney = y-m/2-1; int sum1, sum2, sum3, sum4; if(swx >= width) { swx = width - 1; } if(swy >= height) { swy = height - 1; } if(nex < 0) { nex = 0; } if(ney < 0) { ney = 0; } sum1 = sum[ney*width+nex]; sum4 = sum[swy*width+swx]; sum2 = sum[swy*width+nex]; sum3 = sum[ney*width+swx]; return ((sum1 + sum4) - sum2 - sum3); } public int getBlockSquareSum(int x, int y, int m, int n) { int swx = x + n/2; int swy = y + m/2; int nex = x-n/2-1; int ney = y-m/2-1; int sum1, sum2, sum3, sum4; if(swx >= width) { swx = width - 1; } if(swy >= height) { swy = height - 1; } if(nex < 0) { nex = 0; } if(ney < 0) { ney = 0; } sum1 = squaresum[ney*width+nex]; sum4 = squaresum[swy*width+swx]; sum2 = squaresum[swy*width+nex]; sum3 = squaresum[ney*width+swx]; return ((sum1 + sum4) - sum2 - sum3); } @Override public void process(int width, int height) { this.width = width; this.height = height; sum = new int[width*height]; squaresum = new int[width*height]; // rows int p1=0, p2=0, p3=0, p4; int offset = 0, uprow=0, leftcol=0; int s=0; for(int row=0; row<height; row++ ) { offset = row*width; uprow = row-1; for(int col=0; col<width; col++) { leftcol=col-1; p1=image[offset]&0xff;// p(x, y) p2=(leftcol<0) ? 0:sum[offset-1]; // p(x-1, y) p3=(uprow<0) ? 0:sum[offset-width]; // p(x, y-1); p4=(uprow<0||leftcol<0) ? 0:sum[offset-width-1]; // p(x-1, y-1); s = sum[offset]= p1+p2+p3-p4; squaresum[offset]=s*s; // System.out.print("\t[" + offset+"]=" + s); offset++; } // System.out.println(); } } public static void main(String[] args) { byte[] data = new byte[]{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}; IntIntegralImage ii = new IntIntegralImage(); ii.setImage(data); ii.process(7, 3); int sum = ii.getBlockSum(3, 2, 3, 3); System.out.println("sum = " + sum); } } 后续应用相关博文会陆续出炉!
一: Mat介绍 OpenCV刚出来的时候图像加载内存之后的对象是IplImage作为数据对象,里面存储了图像的像素数据和宽、高、位图深度、图像大小、通道数等基本属性。IplImage结构是C语言体系下定义出来的接口,使用它时候最大的问题是要自己负责内存管理,控制内存分配和释放,很容易导致内存问题。 OpenCV从2.0版本开始引入Mat对象,它会自动分配和释放内存,让开发人员把精力放在图像处理问题上面,不被内存问题所困扰,Mat是一个C++的类对象,它有两个部分组成:一个矩阵头部分(包含矩阵大小、存储方法等)另外一部分是一个指针指向矩阵像素的,其中矩阵头部分是固定常量大小。但是整个矩阵的大小跟图像实际大小有关系。 另外一个改进就是引用计数系统,对Mat对象来说,图像处理可能是一系列算法的组合,会调用多个图像处理的算法,这个时候Mat作为引用被传到各个对应的算法处理函数,除非有必要,一般情况下只会复制Mat的头和指针,不会真正复制像素数据本身。 例子如下: Mat A, C A = imread(argv[1], IMREAD_COLOR); Mat B(A); // copy constructor C = A; 上面的三个指针,虽然头部不一样,但是都指向同一个图像的像素数据矩阵。通常我们需要创建ROI区域,可以使用Mat来实现: Mat D (A, Rect(10, 10, 100, 100)); 如果想跟图像数据一起复制,OpenCV提供了两个相关的API操作:cv::Mat::clone()与cv::Mat::copyTo()。 Mat F = A.clone(); Mat G; A.copyTo(G); 所以关于使用Mat图像像素矩阵要记住如下四点: 1. 输出图像的内存分配是自动的 2. 使用OpenCV的C++接口,不需要考虑内存管理问题 3. 赋值操作和拷贝构造函数操作,只会复制头部分,像素数据部分仍然相同 4. 使用克隆clone和copyTo将会复制图像数据矩阵Mat 二:存储方法 这里是指存储图像像素值,它跟两个因素有关系,一个是色彩空间另外一个数据类型。常见的色彩空间包括: RGB - 最常见的色彩空间,OpenCV标准显示都是基于RGB色彩空间 HSV/HLS - 三个通道分别为HUE、饱和度、亮度,一个常用的例子就是排出光线干扰,丢弃最后一个通道的值处理。 YCrCb - 在JPEG图像格式中比较流行使用 每一种色彩空间的每个通道值都有它自己的取值范围和应用场景。 创建一个Mat对象 cv::Mat::Mat 构造函数 Mat M(2,2, CV_8UC3, Scalar(0,0,255)); cout << "M = " << endl << " " << M << endl << endl; 其中前两个参数分别表示行(row)跟列(column)、第三个参数CV_8UC3中的8表示每个通道占8位、U表示无符号、C表示Char类型、3表示通道数目是3,第四个参数是个向量表示初始化每个像素值是多少,向量长度对应通道数目一致。创建多维数据: int sz[3] = {2,2,2}; Mat L(3,sz, CV_8UC(1), Scalar::all(0)); 上面是一个创建多维(大于2维)数组的例子,只是第二个参数不通,其它都跟第一个例子相似。 cv::Mat::create功能 M.create(4, 4, CV_8UC(2)); cout << "M = "<< endl << " " << M << endl << endl; 这种构造函数无法初始化每个像素值。此外OpenCV还提供类似Matlab风格的初始化函数: Mat E = Mat::eye(4, 4, CV_64F); cout << "E = " << endl << " " << E << endl << endl; Mat O = Mat::ones(2, 2, CV_32F); cout << "O = " << endl << " " << O << endl << endl; Mat Z = Mat::zeros(3,3, CV_8UC1); cout << "Z = " << endl << " " << Z << endl << endl; 定义小的数组,可以使用如下方式: Mat C = (Mat_<double>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0); cout << "C = " << endl << " " << C << endl << endl; 克隆定义好的二维数组数据 Mat RowClone = C.row(1).clone(); cout << "RowClone = " << endl << " " << RowClone << endl << endl; 参考资料: http://docs.opencv.org/3.1.0/d6/d6d/tutorial_mat_the_basic_image_container.html
Java通过JNI实现调用C++程序 好久没碰JNI这个东西了,刚工作的时候自己写过点东西,这么些年很少用到,最近一个项目又用到它了,因此总结一下给自己留个记号!省下下次再用到到处查资料。Java通过JNI实现调用C或者C++写的程序,实现对底层或者下位机的读写通讯,在桌面开发中是经常遇到的。这里通过一个演示程序,实现Java通过JNI实现C++方法调用。要完成Java JNI调用C++程序,需要如下几步: 第一步:创建一个Java Class文件,定义好本地方法接口API,其中本地方法前面要加上关键字native才可以。 package com.gloomyfish.jnidemo; public class HelloJNI { public native int helloJNI(String name); } 第二步:是要编译对应的Java文件HelloJNI.java通过javah这个命令行即可,我这里写了个bat文件,这样可以指定JDK版本,只要把这个bat文件放到对应的eclipse编译好的build或者bin或者target目录下,然后双击运行即可,bat文件的内容如下: set JAVA_HOME=D:\jdk1.6.0_16 set path=%JAVA_HOME%\bin;%path% set classpath=.;%classpath%;%JAVA_HOME%\lib javah com.gloomyfish.jnidemo.HelloJNI 运行之后会得到com_gloomyfish_jnidemo_HelloJNI.h文件,打开查看内容应该显示如下: /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_gloomyfish_jnidemo_HelloJNI */ #ifndef _Included_com_gloomyfish_jnidemo_HelloJNI #define _Included_com_gloomyfish_jnidemo_HelloJNI #ifdef __cplusplus extern "C" { #endif /* * Class: com_gloomyfish_jnidemo_HelloJNI * Method: helloJNI * Signature: (Ljava/lang/String;)I */ JNIEXPORT jint JNICALL Java_com_gloomyfish_jnidemo_HelloJNI_helloJNI (JNIEnv *, jobject, jstring); #ifdef __cplusplus } #endif #endif 第三步:打开VS2015新建一个win32项目 点击下一步选择应用程序类型中DLL,显示如下: 然后点击【完成】,目录结构显示如下图双击打开jnitest.cpp, 右键添加生成的JNI头文件 此外还要添加JDK目录下面include里面的三个头文件jni.h与jawt_md.h与jni_md.h所在的目录。添加方法,右键【jnitest】选择->属性 然后完成如下的C++代码 // jnitest.cpp : 定义 DLL 应用程序的导出函数。 // #include "stdafx.h" #include "com_gloomyfish_jnidemo_HelloJNI.h" JNIEXPORT jint JNICALL Java_com_gloomyfish_jnidemo_HelloJNI_helloJNI (JNIEnv *env, jobject obj, jstring param) { // convert java string type to c++ char* type const char* name = env->GetStringUTFChars(param, 0); printf("%s %s", "Hello JNI, I am ", name); // release memory env->ReleaseStringUTFChars(param, name); // return 0; return 0; } 生成解决方案之后得到jnitest.dll文件,把DLL文件copy到对应的JDK的bin目录下面和JRE的bin目录下,然后在Java程序中添加如下测试代码:
OpenCV3.1中读写图像与读写像素 一:读图像,显示到窗口 从本地目录读写一张RGB图像到内存对象Mat中并把它显示到指定窗口。 相关函数: - imread 加载图像文件 - imshow 显示图像 - namedWindow 创建窗口 相关代码: #include<opencv2\opencv.hpp> #include<highgui.h> using namespace cv; int main(int argc, char** argv) { // read image Mat picture = imread("test.jpg"); // create window namedWindow("My Test", CV_WINDOW_AUTOSIZE); // display imshow("My Test", picture); // 关闭 waitKey(0); destroyWindow("My Test"); destroyWindow("My Gray Test"); return 0; } 运行结果: 二:写图像文件到本地 将加载以后的RGB图像转换为灰度图像,然后保存到本地的目录路径。 相关函数: - cvtColor 将图像从RGB彩色图像转换为灰度图像 -imwrite 将内存中Mat对象图像保存为本地图像文件。 相关代码: #include<opencv2\opencv.hpp> #include<highgui.h> using namespace cv; int main(int argc, char** argv) { // read image Mat picture = imread("test.jpg"); // 转换为灰度图像 Mat gray_image; cvtColor(picture, gray_image, COLOR_BGR2GRAY); imwrite("D:/gloomyfish/graytest.jpg", gray_image); // create windows namedWindow("My Test", CV_WINDOW_AUTOSIZE); namedWindow("My Gray Test", CV_WINDOW_AUTOSIZE); // display image imshow("My Test", picture); imshow("My Gray Test", gray_image); // 关闭 waitKey(0); destroyWindow("My Test"); destroyWindow("My Gray Test"); return 0; } 运行结果:会把看到这张灰度图像保存到D盘gloomyfish文件夹下: 三:读写像素 从Mat中读出每个像素值,与255取反值然后重新写入到Mat中,并在窗口中显示结果。首先要获取通道数据,检测到图像是RGB三通道还是单通道图像,然后获取Mat中的像素块指针,从上到下,从左到右访问每个像素即可。灰度图像Mat中像素数据存储 Mat中彩色RGB图像数据存储: 相关函数: Mat.copyTo 把图像的数据从当前的Mat对象拷贝到一个新的Mat对象中去 Mat.channels 返回图像的通道数 相关代码: #include<opencv2\opencv.hpp> #include<highgui.h> using namespace cv; int main(int argc, char** argv) { // read image Mat image = imread("test.jpg"); // 对图像进行所有像素用 (255- 像素值) Mat invertImage; image.copyTo(invertImage); // 获取图像宽、高 int channels = image.channels(); int rows = image.rows; int cols = image.cols * channels; if (image.isContinuous()) { cols *= rows; rows = 1; } // 每个像素点的每个通道255取反 uchar* p1; uchar* p2; for (int row = 0; row < rows; row++) { p1 = image.ptr<uchar>(row);// 获取像素指针 p2 = invertImage.ptr<uchar>(row); for (int col = 0; col < cols; col++) { *p2 = 255 - *p1; // 取反 p2++; p1++; } } // create windows namedWindow("My Test", CV_WINDOW_AUTOSIZE); namedWindow("My Invert Image", CV_WINDOW_AUTOSIZE); // display image imshow("My Test", image); imshow("My Invert Image", invertImage); // 关闭 waitKey(0); destroyWindow("My Test"); destroyWindow("My Invert Image"); return 0; } 显示取反之后图像: 欢迎继续关注本人博客,加入图像处理QQ群讨论学习!
在二值图像处理特别是OCR识别与匹配中,都要通过对字符进行细化以便获得图像的骨架,通过zhang-suen细化算法获得图像,作为图像的特征之一,常用来作为识别或者模式匹配。 一:算法介绍 Zhang-Suen细化算法通常是一个迭代算法,整个迭代过程分为两步: Step One:循环所有前景像素点,对符合如下条件的像素点标记为删除: 1. 2 <= N(p1) <=6 2. S(P1) = 1 3. P2 * P4 * P6 = 0 4. P4 * P6 * P8 = 0 其中N(p1)表示跟P1相邻的8个像素点中,为前景像素点的个数 S(P1)表示从P2 ~ P9 ~ P2像素中出现0~1的累计次数,其中0表示背景,1表示前景 完整的P1 ~P9的像素位置与举例如下: 其中 N(p1) = 4, S(P1) = 3, P2*P4*P6=0*0*0=0, P4*P6*P8=0*0*1=0, 不符合条件,无需标记为删除。 Step Two:跟Step One很类似,条件1、2完全一致,只是条件3、4稍微不同,满足如下条件的像素P1则标记为删除,条件如下: 1. 2 <= N(p1) <=6 2. S(P1) = 1 3. P2 * P4 * P8 = 0 4. P2 * P6 * P8 = 0 循环上述两步骤,直到两步中都没有像素被标记为删除为止,输出的结果即为二值图像细化后的骨架。 二:代码实现步骤 1. 二值化输入图像,初始化图像像素对应的标记映射数组 BufferedImage binaryImage = super.process(image); int width = binaryImage.getWidth(); int height = binaryImage.getHeight(); int[] pixels = new int[width*height]; int[] flagmap = new int[width*height]; getRGB(binaryImage, 0, 0, width, height, pixels); Arrays.fill(flagmap, 0); 2. 迭代细化算法(Zhang-Suen) a. Step One private boolean step1Scan(int[] input, int[] flagmap, int width, int height) { boolean stop = true; int bc = 255 - fcolor; int p1=0, p2=0, p3=0; int p4=0, p5=0, p6=0; int p7=0, p8=0, p9=0; int offset = 0; for(int row=1; row<height-1; row++) { offset = row*width; for(int col=1; col<width-1; col++) { p1 = (input[offset+col]>>16)&0xff; if(p1 == bc) continue; p2 = (input[offset-width+col]>>16)&0xff; p3 = (input[offset-width+col+1]>>16)&0xff; p4 = (input[offset+col+1]>>16)&0xff; p5 = (input[offset+width+col+1]>>16)&0xff; p6 = (input[offset+width+col]>>16)&0xff; p7 = (input[offset+width+col-1]>>16)&0xff; p8 = (input[offset+col-1]>>16)&0xff; p9 = (input[offset-width+col-1]>>16)&0xff; // match 1 - 前景像素 0 - 背景像素 p1 = (p1 == fcolor) ? 1 : 0; p2 = (p2 == fcolor) ? 1 : 0; p3 = (p3 == fcolor) ? 1 : 0; p4 = (p4 == fcolor) ? 1 : 0; p5 = (p5 == fcolor) ? 1 : 0; p6 = (p6 == fcolor) ? 1 : 0; p7 = (p7 == fcolor) ? 1 : 0; p8 = (p8 == fcolor) ? 1 : 0; p9 = (p9 == fcolor) ? 1 : 0; int con1 = p2+p3+p4+p5+p6+p7+p8+p9; String sequence = "" + String.valueOf(p2) + String.valueOf(p3) + String.valueOf(p4) + String.valueOf(p5) + String.valueOf(p6) + String.valueOf(p7) + String.valueOf(p8) + String.valueOf(p9) + String.valueOf(p2); int index1 = sequence.indexOf("01"); int index2 = sequence.lastIndexOf("01"); int con3 = p2*p4*p6; int con4 = p4*p6*p8; if((con1 >= 2 && con1 <= 6) && (index1 == index2) && con3 == 0 && con4 == 0) { flagmap[offset+col] = 1; stop = false; } } } return stop; } b. Step Two private boolean step2Scan(int[] input, int[] flagmap, int width, int height) { boolean stop = true; int bc = 255 - fcolor; int p1=0, p2=0, p3=0; int p4=0, p5=0, p6=0; int p7=0, p8=0, p9=0; int offset = 0; for(int row=1; row<height-1; row++) { offset = row*width; for(int col=1; col<width-1; col++) { p1 = (input[offset+col]>>16)&0xff; if(p1 == bc) continue; p2 = (input[offset-width+col]>>16)&0xff; p3 = (input[offset-width+col+1]>>16)&0xff; p4 = (input[offset+col+1]>>16)&0xff; p5 = (input[offset+width+col+1]>>16)&0xff; p6 = (input[offset+width+col]>>16)&0xff; p7 = (input[offset+width+col-1]>>16)&0xff; p8 = (input[offset+col-1]>>16)&0xff; p9 = (input[offset-width+col-1]>>16)&0xff; // match 1 - 前景像素 0 - 背景像素 p1 = (p1 == fcolor) ? 1 : 0; p2 = (p2 == fcolor) ? 1 : 0; p3 = (p3 == fcolor) ? 1 : 0; p4 = (p4 == fcolor) ? 1 : 0; p5 = (p5 == fcolor) ? 1 : 0; p6 = (p6 == fcolor) ? 1 : 0; p7 = (p7 == fcolor) ? 1 : 0; p8 = (p8 == fcolor) ? 1 : 0; p9 = (p9 == fcolor) ? 1 : 0; int con1 = p2+p3+p4+p5+p6+p7+p8+p9; String sequence = "" + String.valueOf(p2) + String.valueOf(p3) + String.valueOf(p4) + String.valueOf(p5) + String.valueOf(p6) + String.valueOf(p7) + String.valueOf(p8) + String.valueOf(p9) + String.valueOf(p2); int index1 = sequence.indexOf("01"); int index2 = sequence.lastIndexOf("01"); int con3 = p2*p4*p8; int con4 = p2*p6*p8; if((con1 >= 2 && con1 <= 6) && (index1 == index2) && con3 == 0 && con4 == 0) { flagmap[offset+col] = 1; stop = false; } } } return stop; } c. 检查如果上述两部没有任何像素被标记,则停止迭代,否则继续执行a, b3. 返回细化后的图像,并显示 三:运行效果 四:完整的Zhang-suen算法代码实现: import java.awt.image.BufferedImage; import java.util.Arrays; public class ZhangSuenThinFilter extends BinaryFilter { private int fcolor; public ZhangSuenThinFilter() { fcolor = 0; } public int getFcolor() { return fcolor; } public void setFcolor(int fcolor) { this.fcolor = fcolor; } @Override public BufferedImage process(BufferedImage image) { BufferedImage binaryImage = super.process(image); int width = binaryImage.getWidth(); int height = binaryImage.getHeight(); int[] pixels = new int[width*height]; int[] flagmap = new int[width*height]; getRGB(binaryImage, 0, 0, width, height, pixels); Arrays.fill(flagmap, 0); // 距离变化 boolean stop = false; while(!stop) { // step one boolean s1 = step1Scan(pixels, flagmap, width, height); deletewithFlag(pixels, flagmap); Arrays.fill(flagmap, 0); // step two boolean s2 = step2Scan(pixels, flagmap, width, height); deletewithFlag(pixels, flagmap); Arrays.fill(flagmap, 0); if(s1 && s2) { stop = true; } } // 结果 BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); setRGB(bi, 0, 0, width, height, pixels); return bi; } private void deletewithFlag(int[] pixels, int[] flagmap) { int bc = 255 - fcolor; for(int i=0; i<pixels.length; i++) { if(flagmap[i] == 1) { pixels[i] = (0xff << 24) | ((bc&0xff) << 16) | ((bc&0xff) << 8) | (bc&0xff); } } } private boolean step1Scan(int[] input, int[] flagmap, int width, int height) { boolean stop = true; int bc = 255 - fcolor; int p1=0, p2=0, p3=0; int p4=0, p5=0, p6=0; int p7=0, p8=0, p9=0; int offset = 0; for(int row=1; row<height-1; row++) { offset = row*width; for(int col=1; col<width-1; col++) { p1 = (input[offset+col]>>16)&0xff; if(p1 == bc) continue; p2 = (input[offset-width+col]>>16)&0xff; p3 = (input[offset-width+col+1]>>16)&0xff; p4 = (input[offset+col+1]>>16)&0xff; p5 = (input[offset+width+col+1]>>16)&0xff; p6 = (input[offset+width+col]>>16)&0xff; p7 = (input[offset+width+col-1]>>16)&0xff; p8 = (input[offset+col-1]>>16)&0xff; p9 = (input[offset-width+col-1]>>16)&0xff; // match 1 - 前景像素 0 - 背景像素 p1 = (p1 == fcolor) ? 1 : 0; p2 = (p2 == fcolor) ? 1 : 0; p3 = (p3 == fcolor) ? 1 : 0; p4 = (p4 == fcolor) ? 1 : 0; p5 = (p5 == fcolor) ? 1 : 0; p6 = (p6 == fcolor) ? 1 : 0; p7 = (p7 == fcolor) ? 1 : 0; p8 = (p8 == fcolor) ? 1 : 0; p9 = (p9 == fcolor) ? 1 : 0; int con1 = p2+p3+p4+p5+p6+p7+p8+p9; String sequence = "" + String.valueOf(p2) + String.valueOf(p3) + String.valueOf(p4) + String.valueOf(p5) + String.valueOf(p6) + String.valueOf(p7) + String.valueOf(p8) + String.valueOf(p9) + String.valueOf(p2); int index1 = sequence.indexOf("01"); int index2 = sequence.lastIndexOf("01"); int con3 = p2*p4*p6; int con4 = p4*p6*p8; if((con1 >= 2 && con1 <= 6) && (index1 == index2) && con3 == 0 && con4 == 0) { flagmap[offset+col] = 1; stop = false; } } } return stop; } private boolean step2Scan(int[] input, int[] flagmap, int width, int height) { boolean stop = true; int bc = 255 - fcolor; int p1=0, p2=0, p3=0; int p4=0, p5=0, p6=0; int p7=0, p8=0, p9=0; int offset = 0; for(int row=1; row<height-1; row++) { offset = row*width; for(int col=1; col<width-1; col++) { p1 = (input[offset+col]>>16)&0xff; if(p1 == bc) continue; p2 = (input[offset-width+col]>>16)&0xff; p3 = (input[offset-width+col+1]>>16)&0xff; p4 = (input[offset+col+1]>>16)&0xff; p5 = (input[offset+width+col+1]>>16)&0xff; p6 = (input[offset+width+col]>>16)&0xff; p7 = (input[offset+width+col-1]>>16)&0xff; p8 = (input[offset+col-1]>>16)&0xff; p9 = (input[offset-width+col-1]>>16)&0xff; // match 1 - 前景像素 0 - 背景像素 p1 = (p1 == fcolor) ? 1 : 0; p2 = (p2 == fcolor) ? 1 : 0; p3 = (p3 == fcolor) ? 1 : 0; p4 = (p4 == fcolor) ? 1 : 0; p5 = (p5 == fcolor) ? 1 : 0; p6 = (p6 == fcolor) ? 1 : 0; p7 = (p7 == fcolor) ? 1 : 0; p8 = (p8 == fcolor) ? 1 : 0; p9 = (p9 == fcolor) ? 1 : 0; int con1 = p2+p3+p4+p5+p6+p7+p8+p9; String sequence = "" + String.valueOf(p2) + String.valueOf(p3) + String.valueOf(p4) + String.valueOf(p5) + String.valueOf(p6) + String.valueOf(p7) + String.valueOf(p8) + String.valueOf(p9) + String.valueOf(p2); int index1 = sequence.indexOf("01"); int index2 = sequence.lastIndexOf("01"); int con3 = p2*p4*p8; int con4 = p2*p6*p8; if((con1 >= 2 && con1 <= 6) && (index1 == index2) && con3 == 0 && con4 == 0) { flagmap[offset+col] = 1; stop = false; } } } return stop; } } 觉得不错请支持一下!
最近做了一个项目最后要把算法整成C++的DLL方式给别人调用,朋友给我推荐了用CxImage这个库来读写图像文件,所以我就用了,基于CxImage做了调用算法的Demo程序,已经给别人测试了,现在总结一下CxImage使用中遇到的那些坑。坑一:我是VS2015上面用CxImage这个库的,而且是别人已经编译好的LIB与头文件直接给我了,他还是VC6.0上面搞出来的,所以我一使用这个库遇到第一个问题就是在Link的时候给我报缺少DLL文件mfc42d.dll和msvcrtd.dll这两个DLL文件找不到,我的是VS2015当然没有这个两个东西,还好CSDN博客专家群里面有个哥们很好,传给我了,帮我解决了这个问题。后来我找到CxImage上code project上面的文章,发现它果然是用VC6.0编译的。http://www.codeproject.com/Articles/1300/CxImage坑二:然后我就开始使用这个库了,只要加上如下的头文件:#include "ximage.h"#ifdef _DEBUG#define new DEBUG_NEW#endif#pragma comment(lib,"cximaged.lib")发现我编译怎么也不通过,后来我才发现原因了,我是好久不搞C++了,设置Link的路径不对。可见我已经把我知道的东西全部还给我的大学老师了。解决这个问题就可以使用了,CxImage可以读写几何所有常见格式图像文件,我主要用它来读写JPG和PNG文件,这样就可以省去我很多力气。读JPG文件的代码如下: AfxMessageBox("从这里打开图像文件..."); CString strFile = _T(""); CFileDialog dlgFile(TRUE, NULL, NULL, OFN_HIDEREADONLY, _T("Describe Files (*.jpg)|*.jpg|All Files (*.*)|*.*||"), NULL); if (dlgFile.DoModal()) { strFile = dlgFile.GetPathName(); } AfxMessageBox(strFile); bool blLoad = this->image.Load(strFile, CXIMAGE_FORMAT_JPG); if (this->image.IsValid()) { AfxMessageBox("文件装载成功..."); this->Invalidate(); } 坑三: 也是因为我好久不搞C++了,我有个统计直方图的来了个数组定义,然后就直接开始统计直方图了,其实C++中数组一定要初始化,不然它的初始值真的不是0啊,我把它当成Java来搞了,发现统计出来的直方图数据居然有负值,经过这个血泪教育我知道VC++一定要初始化数组,不然结果很吓人。坑四:然后我就读像素了,CxImage中读图像像素数据的我找了好久发现只有一个GetBits()官方的API上面说这个可以获得像素数组的指针,指向第0个像素地址,可是API注释上面还有一句英文说这个API非常危险,要小心使用。还好我胆子比较大,不管它就直接用了,发现这个里面有个大坑,就是它的像素指针的第一个是我们倒是第一行,我们要读一幅图像从上到下,从左到右顺序的话,在图像高度的方向就要从height-1读到0,而不是从0读到height-1.最终我实现了利用CxImage读取图像像素数据,实现图像灰度化,同时还画了两条红色的线。代码如下: if (this->image.IsValid()) { AfxMessageBox("图像灰度化..."); int maxY = image.GetHeight(), int maxX = image.GetWidth(); int size = maxX*maxY; BYTE* bs = image.GetBits(); int w=((((24 * image.GetWidth()) + 31) / 32) * 4); // 4字节对齐 for (int j=0; j<maxY; j++) { for (int i=0; i<maxX; i++) { BYTE *b = bs+(image.GetHeight()-1-j)*w +i*3; int blue =*(b); int green =*(b+1); int red =*(b+2); int grayVal=(int)red*0.3+(int)green*0.59+(int)blue*0.11; CString csStr; csStr.Format("%d", grayVal); if(i >10 && i<100) { if(j == 50 || j== 100) { *b = 0; //blue *(b+1) = 0; // green *(b+2) = 255; // red } else { *b = grayVal; *(b+1) = grayVal; *(b+2) = grayVal; } } else { *b = grayVal; *(b+1) = grayVal; *(b+2) = grayVal; } } } this->Invalidate(); }显示图像在View中,VC++中显示图像一定要放到OnDraw方法中 不然就得自己做各种窗口事件响应处理实现重绘制。代码如下: // TODO: 在此处为本机数据添加绘制代码 if (this->image.IsValid()) { int h = this->image.GetHeight(), // + 256 int w = this->image.GetWidth(); CRect rc; GetClientRect(&rc); rc.right = rc.left +w; rc.bottom = rc.top + h; this->image.Draw(pDC->GetSafeHdc(), rc); }这样就搞定了CxImage读写图像文件的全部内容,需要说明一下 我这里的图像都是RGB三通道的,在读取像素之前,最好检查一 下它是几通道的图像。载入一张图片: 利用CxImage读取图像实现灰度图像效果: 发现这样我就可以把精力放到图像处理算法本身了,用VC++搞图像 处理的算法也是可以的,没想象中那么恶搞!
一:基本原理 图像卷积处理实现锐化有一种常用的算法叫做Unsharpen Mask方法,这种锐化的方法就是对原图像先做一个高斯模糊,然后用原来的图像减去一个系数乘以高斯模糊之后的图像,然后再把值Scale到0~255的RGB像素值范围之内。基于USM锐化的方法可以去除一些细小的干扰细节和噪声,比一般直接使用卷积锐化算子得到的图像锐化结果更加真实可信。USM锐化公式表示如下: (源图像– w*高斯模糊)/(1-w);其中w表示权重(0.1~0.9),默认为0.6 二:实现步骤 1. 读入图像的像素数据 2. 对图像像素数据实现高斯模糊, 3. 根据输入参数w,对图像上的每个像素点,使用USM锐化公式计算每个像素点锐化之后的像素 4. 构建一张新的输出图像,返回显示 三:运行效果 四:代码实现 高斯模糊代码如下: int width = image.getWidth(); int height = image.getHeight(); // generateKerneal(); generateKerneal2D(); int radius = (int)(this.sigma*2); // 二维高斯模糊 int[] pixels = new int[width*height]; int[] outPixels = new int[width*height]; getRGB(image, 0, 0 , width, height, pixels); int r=0, g=0, b=0; int r1=0, g1=0, b1=0; for(int row=0; row<height; row++) { int offset = row*width; for(int col=1; col<width-1; col++) { double sr=0, sg=0, sb=0; // 二维高斯窗口 for(int i=-radius; i<=radius; i++) { int roffset = row + i; if(roffset < 0) { roffset = 0; } if(roffset >= height) { roffset = height-1; } int offset1 = roffset*width; for(int j=-radius; j<=radius; j++) { int coffset = j+col; if(coffset < 0 ) { coffset = 0; } if(coffset >= width) { coffset = width-1; } r1 = (pixels[offset1+coffset]>>16)&0xff; g1 = (pixels[offset1+coffset]>>8)&0xff; b1 = (pixels[offset1+coffset]&0xff); sr += kernel2D[i+radius][j+radius]*r1; sg += kernel2D[i+radius][j+radius]*g1; sb += kernel2D[i+radius][j+radius]*b1; } } r = (int)sr; g = (int)sg; b = (int)sb; outPixels[offset+col]=(0xff<<24) | (r<<16) | (g << 8) | b; } } 基于高斯模糊的代码是USM锐化的代码如下: int width = image.getWidth(); int height = image.getHeight(); int[] pixels1 = new int[width*height]; int[] pixels2 = new int[width*height]; getRGB(image, 0, 0 , width, height, pixels1); // 高斯模糊 BufferedImage blurImage = super.process(image); getRGB(blurImage, 0, 0 , width, height, pixels2); // USM 锐化 int[] output = new int[width*height]; int r=0, g=0, b=0; int r1=0, g1=0, b1=0; int r2=0, g2=0, b2=0; for(int i=0; i<pixels1.length; i++) { r1 = (pixels1[i] >> 16)&0xff; g1 = (pixels1[i] >> 8)&0xff; b1 = pixels1[i]&0xff; r2 = (pixels2[i] >> 16)&0xff; g2 = (pixels2[i] >> 8)&0xff; b2 = pixels2[i]&0xff; r = (int)((r1-weight*r2)/(1-weight)); g = (int)((g1-weight*g2)/(1-weight)); b = (int)((b1-weight*b2)/(1-weight)); output[i]=(0xff<<24) | (clamp(r)<<16) | (clamp(g) << 8) | clamp(b); } BufferedImage dest = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); setRGB(dest, 0, 0, width, height, output); return dest; 其中BufferedImage blurImage = super.process(image);调用的代码为高斯模糊
关于图像处理方面的收获: 五月中旬的时候接了个细胞检测的活,要求识别白细胞、红细胞、脂肪球、霉菌几种 细胞,大致看了客户发给我显微镜上的图片,发现能做,于是就接了下来,客户告诉 我最终的程序要是C++的编译成DLL给他们的应用程序调用才可以,本人因为一直做 Java,做C++还是12年前毕业设计的时候做了图像相关的东西。从那之后,做项目偶 尔也会搞点C++但是基本上就一直停留在Hello World的水平上。我的想法是先把算法 用Java实现,然后我找人合作帮忙把算法从Java翻译成C++,经过大概一个月左右的 时间Java版本的算法已经实现,并且用客户给我的那些测试图片数据,发现除了白细 胞识别率有点低,其它的识别都还不错,基本都在70%左右,而且阴性的检测准确率 高达96%,已经满足实际需要。其实做这个识别算法我是基于模板匹配方法,但是唯 一不同的时候我是先找到ROI区域,然后只对ROI区域去做模板匹配,这样速度就非常 快,基本满足了客户要求。在做这个项目的过程中,自己对一些基础的问题又有了近 一步的认知,对SMOOTH、先图像梯度在灰度还是先灰度在梯度的区别都有了新的认 知,发现顺序不同差别还是很大的,对图像二值化、二值图像的填充、腐蚀与膨胀、 开闭操作、边缘提取等都更加的熟悉了。同时对霍夫直线检测、二值图像的连通区域 查找等算法也有近一步的认识,在特征提取方面自己先后尝试了LBP、HOG、几何距 灰度共生矩阵等又温习和进一步理解应用。通过综合运用这些知识,基于提取到的模 板数据、实现了对细胞的快速分类与识别。 关于编程方面的收获: 我在学习图像处理之初,就下定决心不去依赖任何库,这样做有好处也有坏处,好处 就是逼着自己去实现一些常见的图像处理方法,坏处就工作效率比较低,特别是开始 阶段,我在用Java做这些算法的时候也特别注意借鉴ImageJ中的代码,能直接拿过来 用的,我基本都直接拿过来了,HOG是我自己用Java写的,因为ImageJ中我没发现 这个就导致我的Java算法在翻译到C++的时候有很多选择,可以用openCV等开源库 可以那些API我也没仔细看,心想找个人帮我翻译成C++的就好了,不需要任何库 毕竟我的Java版算法也是我自己实现的,不依赖任何库,我估计的时间是一周左右 我很幸运的找到一个很好的C++哥们,他也是CSDN的一个熟人,花了两周晚上的时 间我们把Java的算法转成C++的了,然后编译测试通过,改了几个小问题就发现运行 结果跟Java没有差别,这个让我很高兴,觉得帮我翻译C++的哥们太给力了,希望我 们一直合作下去。 当然这个过程中我们也踩了不少坑,其中最大的一个就是我Java做的算法很多变 量都没有初始化为0,到了C++上面就悲剧了,还有一些数组也没有初始化就直接进行 像素或者统计操作,结果也很悲剧,都是很大的坑。费了我们不少精力。其中更有一 个很恶搞的问题,我定义的数字是字节类型的,结果里面的数据是INT的,长度就会溢 出,结果就很悲剧。我也学会一些简单的C++知识,学会用vs2015搞东西,学会了用 CxImage这个库来读图像文件和像素数据。复活了我一些VC++的知识。我记得我上次 用VC++的时候还是6.0,我这个跨度比较大。项目总结的时候那哥们还给我提几点建议 这样方面他把代码转到C++。一个很认真的哥们,他的建议: 对以后咱们合作中的Java代码提两点建议: 1.定义变量、数组时必须初始化。 2.逻辑层和视图层分离,算法中只传像素数组, 不要有BufferedImage对象及图像读定相关的操作, 这些都放到视图层。对图像的处理,其实就是对像素的处理。 那么处理函数的定义其实只需要传入一个待处理的像素数组, 返回一个处理后的结果像素数组。Java定义可以为: public abstract int [] process( int[] pixs,int width, int height); 对应的c++定义为 virtual int process (EUINT32 * pSrcPix ,int nSrcW , int nSrcH , EUINT32 *& pDest , int & nDestW , int &nDestH ) = 0; 关于我们: 如果能在一起做事情,一定要相互信任,特别是经济上一定要相互讲清楚,只有这样 团队才会相互信任,才有可能一起做更多的事情。要让客户看到自己的努力,认可自 己的劳动,成功就不会太远了。现在我做Java,主攻图像处理的各种算法,他主攻 C++,我们相互是对方的老师和良师。就在这个项目结束的时候我们又接一个新的图 像处理的活,希望会一起走的更远。
图像处理之HOG特征提取算法 HOG(Histogram of Oriented Gradient)特征在对象识别与模式匹配中是一种常见的特征提取算法,是基于本地像素块进行特征直方图提取的一种算法,对象局部的变形与光照影响有很好的稳定性,最初是用HOG特征来来识别人像,通过HOG特征提取+SVM训练,可以得到很好的效果,OpenCV已经有了。HOG特征提取的大致流程如下: 第一步: Gamma校正,主要是对输入图像进行校正,主要是为了补偿显示器带来的灰度偏差。常见的系数在2.5左右,这个方面的资料非常多。ImageJ的源代码中也有Gamma校正的实现,可以参照。 第二步: 图像转灰度,这个也属于常见操作了。 第三步: 计算图像的梯度与方向,可以使用SOBEL算子实现,最终得到图像的梯度振幅与角度。 第四步: 将图像划分为8x8的小网格,对每个小网格内的图像做梯度方向直方图,每个8x8=64个像素为一个CELL,对每个CELL根据角度分为9个直方图块(BIN),每个BIN的范围是20度。假设在CELL的某个像素点的角度是10,则把他对应的梯度值累加放到第一个编号是0的直方图中,最终得到编号是0~8的各个直方图的数据。这样做的一个不好的地方是,没有精准反应出来梯度权重对相邻直方图的影响,得到直方图也不是反锯齿的数据,所以一个刚好的方法,是根据角度的值,计算对应像素的梯度在左右相邻直方图上的权重,根据权重累加相应的值到相邻的直方图中。这样就完成了HOG中最重要的一步,权重角度直方图数据统计。CELL网格分割图如下: 得到对应的直方图如下: 角度直方图的编号与角度范围。 五:块描述子 将2x2的网格单元组合成为一个大的块(Block)对每个块之间有1/2部分是重叠区域。主要是将每个Cell的直方图合并为一个大的直方图向量,这样每个块就有36个向量描述子。对每个块的描述子做归一化处理,常见的归一化处理为L2-norm或者L1-norm,公式如下: 这样就得到每个块的描述子,对一个对象特征来说块可以是矩形的也可以是圆形的,根据要提取对象特征决定。得到特征之后,在目标图像上以一个CELL大小为步长,检测目标图像上是否有匹配的对象特征,对象特征匹配可以基于相似度,最常见的是欧几里得距离与巴斯系数。 举例: 对于64x128的像素块,可以分为8x16个Cell分为7x15个块(R-HOG) 总计的直方图向量数为:7x15x2x2x9 = 3780个向量 关键部分的代码实现: public static List<HOGBlock> extract(byte[] gradient, int[] orientation, int width, int height) { // cell histograms int step = 8; int index = 0; int numRowBins = height / step; int numColBins = width / step; int binindex = 0, theta=0, gw = 0; float ww=0, wn=0, wp=0; HOGCell[][] cells = new HOGCell[numRowBins][numColBins]; for (int row = 0; row < height; row += step) { for (int col = 0; col < width; col += step) { int roffset = 0, coffset = 0; cells[row / step][col / step] = new HOGCell(); cells[row / step][col / step].row = row; cells[row / step][col / step].col = col; cells[row / step][col / step].bins = new double[9]; for (int y = 0; y < step; y++) { for (int x = 0; x < step; x++) { roffset = y + row; if (roffset >= height) { roffset = 0; } coffset = x + col; if (coffset >= width) { coffset = 0; } index = roffset * width + coffset; theta = orientation[index]; // 计算权重梯度,一次双线性插值 ww = theta % 20; if(ww >= 10) { wn = ww - 10; wp = (20-wn) / 20.0f; } else { wn = 10 - ww; wp = (20-wn) / 20.0f; } // 获取方向 binindex = theta / 20; if (binindex >= 9) { binindex = 8; } // 权重梯度值累加, 反锯齿 gw = (gradient[index]&0xff); if(ww >=10) { cells[row / step][col / step].bins[binindex] += (wp*gw); if(binindex < 8) { cells[row / step][col / step].bins[binindex+1] += ((1.0-wp)*gw); } } else { cells[row / step][col / step].bins[binindex] += (wp*gw); if(binindex > 0) { cells[row / step][col / step].bins[binindex-1] += ((1.0-wp)*gw); } } } } } } // merge as blocks for 2x2 cells, if cells less than 2x2 cells, just one // block index = 0; List<HOGBlock> blocks = new ArrayList<HOGBlock>(); for (int i = 0; i < numRowBins - 1; i++) { for (int j = 0; j < numColBins - 1; j++) { int cellxoff = j + 1; int cellyoff = i + 1; if (cellxoff >= numColBins) { cellxoff = 0; } if (cellyoff >= numRowBins) { cellyoff = 0; } // 2x2 HOGCell cell1 = cells[i][j]; HOGCell cell2 = cells[i][cellxoff]; HOGCell cell3 = cells[cellyoff][j]; HOGCell cell4 = cells[cellyoff][cellxoff]; HOGBlock block = new HOGBlock(); block.vector = generateBlockVector(cell1, cell2, cell3, cell4); block.width = 2; block.height = 2; block.xpos = cell1.col; block.ypos = cell1.row; block.bindex = index; blocks.add(index, block); index++; } } // Block 归一化 for (HOGBlock cellsBlock : blocks) { blockL1SquareNorm(cellsBlock); } return blocks; }
图像处理之基于泛红算法的二值图像内部区域填充 一:基本原理 在二值图像处理中有个常用的操作叫做Hole Fill意思是填充所有封闭区域的内部,这种算法在二值图像基础上的对象识别与提取有很大作用。基于泛红填充算法实现二值图像内部区域填充是一直快速填充算法。 因为泛红填充通常需要指定从一个点开始,填满整个封闭区域,对一张二值图像来说,我们最好的办法是把背景当成一个封闭区域,从上向下从左到右查到第一个背景像素点,基于扫描线算法实现全部填充成一个非0,255之外的一个像素值,我这里是127。填充完成之后再一次从左到右,从上向下检查每个像素值如果是127的则为背景像素,否则全部设为前景像素。这样就完成二值图像每个封闭区域内部填充。 二:算法实现步骤 1. 读取二值图像 2. 基于扫描线算法对背景像素区域进行泛红填充,填充值为127 3. 循环每个像素对值为127的设为背景像素,其它值设为前景像素 4. 返回填充之后的二值图像像素数据 三:代码实现 package com.gloomyfish.basic.imageprocess; public class FloodFillAlgorithm extends AbstractByteProcessor { private int foreground; private int background; private byte[] data; private int width; private int height; // stack data structure private int maxStackSize = 500; // will be increased as needed private int[] xstack = new int[maxStackSize]; private int[] ystack = new int[maxStackSize]; private int stackSize; public FloodFillAlgorithm(byte[] data) { this.data = data; this.foreground = 0; this.background = 255 - this.foreground; } public int getColor(int x, int y) { int index = y * width + x; return data[index] & 0xff; } public void setColor(int x, int y, int newColor) { int index = y * width + x; data[index] = (byte) newColor; } public void floodFillScanLineWithStack(int x, int y, int newColor, int oldColor) { if (oldColor == newColor) { System.out.println("do nothing !!!, filled area!!"); return; } emptyStack(); int y1; boolean spanLeft, spanRight; push(x, y); while (true) { x = popx(); if (x == -1) return; y = popy(); y1 = y; while (y1 >= 0 && getColor(x, y1) == oldColor) y1--; // go to line top/bottom y1++; // start from line starting point pixel spanLeft = spanRight = false; while (y1 < height && getColor(x, y1) == oldColor) { setColor(x, y1, newColor); if (!spanLeft && x > 0 && getColor(x - 1, y1) == oldColor)// just // keep // left // line // once // in // the // stack { push(x - 1, y1); spanLeft = true; } else if (spanLeft && x > 0 && getColor(x - 1, y1) != oldColor) { spanLeft = false; } if (!spanRight && x < width - 1 && getColor(x + 1, y1) == oldColor) // just // keep // right // line // once // in // the // stack { push(x + 1, y1); spanRight = true; } else if (spanRight && x < width - 1 && getColor(x + 1, y1) != oldColor) { spanRight = false; } y1++; } } } private void emptyStack() { while (popx() != -1) { popy(); } stackSize = 0; } final void push(int x, int y) { stackSize++; if (stackSize == maxStackSize) { int[] newXStack = new int[maxStackSize * 2]; int[] newYStack = new int[maxStackSize * 2]; System.arraycopy(xstack, 0, newXStack, 0, maxStackSize); System.arraycopy(ystack, 0, newYStack, 0, maxStackSize); xstack = newXStack; ystack = newYStack; maxStackSize *= 2; } xstack[stackSize - 1] = x; ystack[stackSize - 1] = y; } final int popx() { if (stackSize == 0) return -1; else return xstack[stackSize - 1]; } final int popy() { int value = ystack[stackSize - 1]; stackSize--; return value; } @Override public void process(int width, int height) { this.width = width; this.height = height; for (int y = 0; y < height; y++) { if (getColor(0, y) == background) floodFillScanLineWithStack(0, y, 127, 255); if (getColor(width - 1, y) == background) floodFillScanLineWithStack(width - 1, y, 127, 255); } for (int x = 0; x < width; x++) { if (getColor(x, 0) == background) floodFillScanLineWithStack(x, 0, 127, 255); if (getColor(x, height - 1) == background) floodFillScanLineWithStack(x, height - 1, 127, 255); } int p = 0; for (int i = 0; i < data.length; i++) { p = data[i]&0xff; if (p == 127) data[i] = (byte)255; else data[i] = (byte)0; } } } 四:运行结果 使用代码,要先读取二值图像数据到data字节数组: FloodFillAlgorithm ffa = new FloodFillAlgorithm(data); ffa.process(width, height);
在《Java数字图像处理-编程技巧与应用实践》一书的第九章讲到了Canny边缘检测的代码实现,在求取梯度与角度处理,非最大信号压制之后,有一步是通过两个阈值(高低阈值)实现边缘断线的连接,得到完整的边缘,之前给出的代码是固定阈值,这个有两个改动: 改动一: 改为自动阈值了,高的阈值是梯度的平均值means的两倍,低阈值是平均值的二分之一。 改动二: 高低值连接的时候递归程序改为非递归程序,通过图的深度优先队列实现边缘像素连接。 这两个改动的原因是有读者向我反馈说提供的代码在处理大图片的时候递归的代码导致栈溢出错误,如今改为非递归之后,我测试过1600x1200的图片进行边缘提取,完全没有问题。 先看效果:原图: 处理之后: 源代码改动说明: 不再使用follow(col, row, offset, lowThreshold);方法 直接改为非递归的图的深度优先队列进行处理,对每个像素点构建PixelNode数据结构 package com.book.chapter.nine; public class PixelNode { protected int row; protected int col; protected int index; protected boolean high; } 自动阈值与非递归的代码如下: // 寻找最大与最小值 float min = 255; float max = 0; double sum = 0; double count = 0; for(int i=0; i<magnitudes.length; i++) { if(magnitudes[i] == 0) continue; min = Math.min(min, magnitudes[i]); max = Math.max(max, magnitudes[i]); sum += magnitudes[i]; count++; } System.out.println("Image Max Gradient = " + max + " Mix Gradient = " + min); float means = (float)(sum / count); System.out.println("means = " + (sum / count)); // 通常比值为 TL : TH = 1 : 3, 根据两个阈值完成二值化边缘连接 // 边缘连接-link edges Arrays.fill(data, 0); int offset = 0; highThreshold = means * 2; lowThreshold = means / 2; System.out.println("ddddddddddddddddddddddddd"); Queue<PixelNode> queue = new LinkedList<PixelNode>(); for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { if(magnitudes[offset] >= highThreshold && data[offset] == 0) { PixelNode pn = new PixelNode(); pn.row = row; pn.col = col; pn.index = offset; pn.high = true; queue.add(pn); while(!queue.isEmpty()) { PixelNode node = queue.poll(); int x0 = (node.col == 0) ? node.col : node.col - 1; int x2 = (node.col == width - 1) ? node.col : node.col + 1; int y0 = node.row == 0 ? node.row : node.row - 1; int y2 = node.row == height -1 ? node.row : node.row + 1; data[node.index] = magnitudes[node.index]; for (int x = x0; x <= x2; x++) { for (int y = y0; y <= y2; y++) { int i2 = x + y * width; if ((y != node.row || x != node.col) && data[i2] == 0 && magnitudes[i2] >= lowThreshold) { PixelNode middleNode = new PixelNode(); middleNode.col = x; middleNode.row = y; middleNode.index = i2; middleNode.high = false; queue.offer(middleNode); } } } } } queue.clear(); offset++; } } 其它部分的源代码参考这里http://blog.csdn.net/jia20003/article/details/41173767 不再赘述!
基于查找表的快速Gamma校正 在图像预处理中经常通过Gamma校正实现像素修正,常见的Gamma校正是按照公式 对每个像素进行校正,这样做对一张图片还好,当你有大量图片需要做相同处理的时 候计算量就会变得很大,这个时候可以通过建立查找表,然后根据查找表映射实现快 速的Gamma校正。Gamma校正的数学公式如下: gamma的取值范围为0.05~5之间。 其中P(x,y)表示每个像素值,对每个像素进行Gamma校正之后就得到了处理后的图像。整 个处理流程如下: 1. 读取输入图像的像素数据 2. 根据公式建立查找表(LUT)映射 3. 根据每个像素值映射到查找表中Gamma校正后的像素值 4. 输出处理之后的图像像素数据 彩色图像需要对各个通道实现上述处理,灰度图像只要单通道处理即可。 Gamma校正的效果如下: 源代码如下: package com.gloomyfish.filter.study; import java.awt.image.BufferedImage; public class GammaFilter extends AbstractBufferedImageOp { private int[] lut; private double gamma; public GammaFilter() { this.lut = new int[256]; this.gamma = 0.5; } @Override public BufferedImage filter(BufferedImage src, BufferedImage dst) { int width = src.getWidth(); int height = src.getHeight(); if (dst == null) dst = createCompatibleDestImage(src, null); // Gamma correction int[] inPixels = new int[width * height]; int[] outPixels = new int[width * height]; getRGB(src, 0, 0, width, height, inPixels); setupGammaLut(); int index = 0; for (int row = 0; row < height; row++) { int ta = 0, tr = 0, tg = 0, tb = 0; for (int col = 0; col < width; col++) { index = row * width + col; ta = (inPixels[index] >> 24) & 0xff; tr = (inPixels[index] >> 16) & 0xff; tg = (inPixels[index] >> 8) & 0xff; tb = inPixels[index] & 0xff; outPixels[index] = (ta << 24) | (lut[tr] << 16) | (lut[tg] << 8) | lut[tb]; } } // 返回结果 setRGB(dst, 0, 0, width, height, outPixels); return dst; } private void setupGammaLut() { for (int i = 0; i < 256; i++) { lut[i] = (int) (Math.exp(Math.log(i / 255.0) * gamma) * 255.0); } } } 业精于勤,荒于嬉;行成于思,毁于随
一:JVM中内存 JVM中内存通常划分为两个部分,分别为堆内存与栈内存,栈内存主要用执行线程方法 存放本地临时变量与线程中方法执行时候需要的引用对象地址。JVM所有的对象信息都 存放在堆内存中,相比栈内存,堆内存可以所大的多,所以JVM一直通过对堆内存划分 不同的功能区块实现对堆内存中对象管理。 堆内存不够最常见的错误就是OOM(OutOfMemoryError) 栈内存溢出最常见的错误就是StackOverflowError,程序有递归调用时候最容易发生 二:堆内存划分 在JDK7以及其前期的JDK版本中,堆内存通常被分为三块区域Nursery内存(young generation)、长时内存(old generation)、永久内存(Permanent Generation for VM Matedata),显示如下图: 其中最上一层是Nursery内存,一个对象被创建以后首先被放到Nursery中的Eden内 存中,如果存活期超两个Survivor之后就会被转移到长时内存中(Old Generation)中 永久内存中存放着对象的方法、变量等元数据信息。通过如果永久内存不够,我们 就会得到如下错误: java.lang.OutOfMemoryError: PermGen 而在JDK8中情况发生了明显的变化,就是一般情况下你都不会得到这个错误,原因 在于JDK8中把存放元数据中的永久内存从堆内存中移到了本地内存(native memory) 中,JDK8中JVM堆内存结构就变成了如下: 这样永久内存就不再占用堆内存,它可以通过自动增长来避免JDK7以及前期版本中 常见的永久内存错误(java.lang.OutOfMemoryError: PermGen),也许这个就是你的 JDK升级到JDK8的理由之一吧。当然JDK8也提供了一个新的设置Matespace内存 大小的参数,通过这个参数可以设置Matespace内存大小,这样我们可以根据自己 项目的实际情况,避免过度浪费本地内存,达到有效利用。 -XX:MaxMetaspaceSize=128m 设置最大的元内存空间128兆 注意:如果不设置JVM将会根据一定的策略自动增加本地元内存空间。 如果你设置的元内存空间过小,你的应用程序可能得到以下错误: java.lang.OutOfMemoryError: Metadata space
Spring3 MVC中使用Swagger生成API文档 一:Swagger介绍 Swagger是当前最好用的Restful API文档生成的开源项目,通过swagger-spring项目 实现了与SpingMVC框架的无缝集成功能,方便生成spring restful风格的接口文档, 同时swagger-ui还可以测试spring restful风格的接口功能。其官方网站为: http://swagger.io/ 二:Swagger集成Spring3 MVC步骤 Swagger集成springMVC步骤大致只有如下几步: 1.在pom.xml文件中添加swagger相关的依赖 <!-- swagger API document --> <dependency> <groupId>com.mangofactory</groupId> <artifactId>swagger-springmvc</artifactId> <version>0.6.5</version> </dependency> 2.创建classpath路径下创建一个swagger.properties, 添加如下内容: documentation.services.version=1.0 documentation.services.basePath=http://localhost:8080/yourcontextpath 3.在springMVC的main-servlet.xml文件添加如下配置 <context:property-placeholder location="classpath:swagger.properties" /> <bean id="documentationConfig" class="com.mangofactory.swagger.configuration.DocumentationConfig" /> 4.重新打包部署你的项目到WEB服务器,访问地址 http://localhost:8080/your-contextpath /api-docs即可看到注解生成的API说明 三:常见swagger注解一览与使用 APIs.@Api @ApiClass @ApiError @ApiErrors @ApiOperation @ApiParam @ApiParamImplicit @ApiParamsImplicit @ApiProperty @ApiResponse @ApiResponses @ApiModel 在代码中使用例子: import java.util.HashMap; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import com.wordnik.swagger.annotations.ApiOperation; @Controller @RequestMapping("/api/swagger") public class SwaggerDemoController { private static final Logger logger = LoggerFactory.getLogger(SwaggerDemoController.class); @ApiOperation(value = "query api basic information") @RequestMapping(value = "/info", method = RequestMethod.GET) @ResponseBody public Map<String, String> queryAPIInfo() { logger.info("查询更新新版本号"); Map<String, String> map = new HashMap<String, String>(); map.put("language", "Java"); map.put("format", "JSON"); map.put("tools", "swagger"); map.put("version", "1.0"); return map; } @ApiOperation(value = "query data with parameters") @RequestMapping(value = "/data", method = RequestMethod.GET) @ResponseBody public Map<String, String> queryData(@RequestParam String words) { logger.info("查询更新新版本号"); Map<String, String> map = new HashMap<String, String>(); map.put("keyword", words); map.put("data", "this is demo data"); return map; } } 四:运行swagger-ui测试接口 下载swagger-ui的最新版本到本地,改名为swagger-ui,把dist下面的部署到tomcat 或者任何WEB服务器上,启动后访问如下地址: http://localhost:8080/swagger-ui 注意把swagger-ui中的index.html中的http://petstore.swagger.io/v2/swagger.json改为 http://localhost:8080/your-contextpath /api-docs保存,然后在启动WEB服务器, 显示如下: 展开输入参数以后,点击【try it out】即可测试接口,查看返回数据。注意:加上之后启动报Bean not found mapping之类的错误,请在对应 xml文件中加上如下的配置: <context:annotation-config /> <mvc:default-servlet-handler />
Java线程学习经典例子-读写者演示 Java线程学习最经典的例子-读写者,主要用到Thread相关知识如下: - 线程的start与run - 线程的休眠(sleep) - 数据对象加锁(synchronized) - 数据对象的等待与释放(wait and notify) 程序实现: -ObjectData数据类对象,通过synchronized关键字实现加锁,在线程读写者中使用。 -ConsumerThread消费者线程,读取数据对象中count值之后,通知生产者线程 -ProductThread生产者线程,对数据对象中count值操作,每次加1,然后通知消费者线程 类结构图如下: 代码实现 消费者-读线程 package com.gloomyfish.jse.thirdteen; public class ConsumerThread extends Thread { private ObjectData data; public ConsumerThread(ObjectData data) { this.data = data; } @Override public void run() { while(true) { try { synchronized (data) { data.wait(); data.read(); data.notify(); } } catch (InterruptedException e) { e.printStackTrace(); } } } } 写线程-生产者线程 package com.gloomyfish.jse.thirdteen; public class ProductThread extends Thread { private ObjectData data; public ProductThread(ObjectData data) { this.data = data; } @Override public void run() { while (true) { try { synchronized (data) { data.write(); Thread.sleep(3000); data.notify(); data.wait(); } } catch (InterruptedException e) { e.printStackTrace(); } } } } 数据对象类 package com.gloomyfish.jse.thirdteen; public class ObjectData { private int count; public ObjectData() { count = 0; } public void read() { System.out.println("read count : " + count); System.out.println(); } public void write() { count++; System.out.println("write count : " + count); } } 测试代码: public static void main(String[] args) { ObjectData data = new ObjectData(); ConsumerThread ct = new ConsumerThread(data); ProductThread pt = new ProductThread(data); ct.start(); pt.start(); } 总结: 示例程序代码完成了线程之间如何通过wait与notify实现数据的读写 同步控制。演示了Java的同步关键字synchronized的用法与线程的用法。