Jade Dungeon

OpenCV 基础

基本的矩阵操作

矩阵点的类型

org.opencv.core.CvType的常量定义了像素的类型,格式:

CV_<bit_depth>{U|S|F}C(<number_of_channels>)
  • bit_depth部分指定的字段长度。
  • {U|S|F}是数据的类型:无符整数、符号整数、浮点数。 其中32位浮点的范围从-FLT_MAXFLT_MAX,包括INFNAN; 其中64位浮点的范围从-DBL_MAXDBL_MAX,包括INFNAN
  • number_of_channels指定色彩通道,如单通道的黑白与3个通道的彩色。

例:

CvType.CV_8UC1    // 8位无符整数,单色
CvType.CV_8UC3    // 8位无符整数,三通道彩色
CvType.CV_32FC1   // 32位浮点,单色。
CvType.CV_64FC1   // 64位浮点,单色。

矩阵

创建矩阵

  • org.opencv.core.Mat抽象了矩阵。
  • org.opencv.core.Size定义矩阵的尺寸。
  • org.opencv.core.Scalar定义一个点的RGB值。

在定义矩阵时,指定大小与数据类型:

val image2 = new Mat(480, 640, CvType.CV_8UC3)

val image3 = new Mat(new Size(480, 640), CvType.CV_8UC3)

还可以指定参数scalar定义如何初始化矩阵中所有的值:

val image = new Mat(new Size(3, 3), CvType.CV_8UC3, 
	new Scalar(Array[Double](128, 3, 4)))

// [128, 3, 4,  128, 3, 4,  128, 3, 4;
//  128, 3, 4,  128, 3, 4,  128, 3, 4;
//  128, 3, 4,  128, 3, 4,  128, 3, 4]

矩阵图片格式的转换

Mat.convertTo(mat, CvType)用来转换图片的格式:

val byteImage = new Mat();
originalImage.convertTo(byteImage, CvType.CV_8UC3);

常用属性

logDebug("image2: {} rows: {} cols: {} elementsize: {}", 
	image2, image2.rows, image2.cols, image2.elemSize)

// image2: Mat [ 480*640*CV_8UC3, isCont=true, isSubmat=false, 
//		nativeObj=0x7fc9fcd20bb0, dataAddr=0x7fc9fcd8b020 ] 
//		rows: 480 cols: 640 elementsize: 3
  • isCont:是否有额外的外层填充。在某些平台上可以利用JNI加速。
  • isSubmat:是否引用自其他的矩阵。
  • nativeObj:本地地址。
  • dataAddr:全局地址。
  • total():总像素数量。

像素的基本操作

在Java中使用OpenCV一般步骤为:

  1. 按矩阵大小分配内存空间到数组。
  2. 把图像加载到数组中。
  3. 操作数组。
  4. 把数组中的内容再回到回矩阵中。

也可以不用中间数组,直接在矩阵上修改:

设置像素的值

Mat.put()方法设置像素的值:

Mat.put(x: Int, y: Int, point :Array[Byte])
  • 注意: Java里没有无符整形,所以只能用整形int n = n & 0xff得到一个无符的字节。
  • 彩色图像每个像素的通道顺序为RGB。
  • 修改像素的值:
val p = Array[Byte](1, 2, 3)

val image = new Mat(new Size(3, 3), CvType.CV_8UC3, 
	new Scalar(Array[Double](128, 3, 4)))
for (i <- 0 until image.rows; j <- 0 until image.cols) image.put(i, j, p)

// [1, 2, 3,   1, 2, 3,   1, 2, 3;
//  1, 2, 3,   1, 2, 3,   1, 2, 3;
//  1, 2, 3,   1, 2, 3,   1, 2, 3]

取得像素的值

Mat.get(x, y, Array[Byte])用来取得一个像素的值。 例:过滤掉每个像素中的蓝色:

val px = new Array[Byte](3)
for (i <- 0 until image.rows; j <- 0 until image.cols) {
	image.get(i, j, px)
	image.put(i, j, Array[Byte](px(i), px(j), 0))
}
// [1, 1, 0,   1, 2, 0,   1, 3, 0;
//  2, 1, 0,   2, 2, 0,   2, 3, 0;
//  3, 1, 0,   3, 2, 0,   3, 3, 0]

图片文件读取为矩阵

org.opencv.imgcodecs.Imgcodecsimread()方法从图片文件加载矩阵:

def openFile(filename: String): Mat = {
	val img = Imgcodecs.imread(filename)
	if (img.dataAddr == 0) 
		throw new Exception("Cannot open file: " + filename)
	img
}

在Swing中显示图片

关键是把矩阵转为java.awt.image.BufferedImage

def toBufferedImage(matrix: Mat) = {
	// 按图像矩阵大小分配内存,再把图像矩阵加载到内存
	val bufferSize = matrix.channels() * matrix.cols() * matrix.rows()
	val buffer = new Array[Byte](bufferSize)
	matrix.get(0, 0, buffer);
	
	// 创建一个java图像缓存
	val image = new BufferedImage(matrix.cols(), matrix.rows(), 
		if (matrix.channels() > 1) BufferedImage.TYPE_3BYTE_BGR 
		else BufferedImage.TYPE_BYTE_GRAY)
	val targetPixels = image.getRaster().getDataBuffer(
			).asInstanceOf[java.awt.image.DataBufferByte].getData()
			
	// 把内存中的opencv图像矩阵内存复制到Java图像缓存中
	System.arraycopy(buffer, 0, targetPixels, 0, buffer.length)
	image
}

从摄像头采集视频

org.opencv.videoio.Videoio中定义的视频相关的常量。

org.opencv.videoio.VideoCapture类从摄像头采集照片和视频:

  • 构造函数VideoCapture(int deviceIdx)指定摄像头,0为默认计算机默认摄像头, 1可以更换来源
  • 构造函数VideoCapture(String fileName)读取指定的文件。

设置分辨率:

import org.opencv.videoio.VideoCapture;
import org.opencv.videoio.Videoio;

VideoCapture capture = new VideoCapture(0);
capture.set(Videoio.CAP_PROP_FRAME_WIDTH,640);
capture.set(Videoio.CAP_PROP_FRAME_HEIGHT,480);

成员方法isOpened()方法检查摄像头是否已经打开。

if (capture.isOpened()) {
	while (true) {
		/* ...  loop  ... */
	}
}

grab()方法可以快速地定位到下一帧,retrieve()方法解码并返回定位到的帧。

OpenCV的Java API里很多常量都没有定义,只能从C++源代码里把这些常量的值抄下来:

	val CAP_PROP_POS_MSEC             = Value( 0)
	val CAP_PROP_POS_FRAMES           = Value( 1)
	val CAP_PROP_POS_AVI_RATIO        = Value( 2)
	val CAP_PROP_FRAME_WIDTH          = Value( 3)
	val CAP_PROP_FRAME_HEIGHT         = Value( 4)
	val CAP_PROP_FPS                  = Value( 5)
	val CAP_PROP_FOURCC               = Value( 6)
	val CAP_PROP_FRAME_COUNT          = Value( 7)
	val CAP_PROP_FORMAT               = Value( 8)
	val CAP_PROP_MODE                 = Value( 9)
	val CAP_PROP_BRIGHTNESS           = Value(10)
	val CAP_PROP_CONTRAST             = Value(11)
	val CAP_PROP_SATURATION           = Value(12)
	val CAP_PROP_HUE                  = Value(13)
	val CAP_PROP_GAIN                 = Value(14)
	val CAP_PROP_EXPOSURE             = Value(15)
	val CAP_PROP_CONVERT_RGB          = Value(16)
	val CAP_PROP_WHITE_BALANCE_BLUE_U = Value(17)
	val CAP_PROP_RECTIFICATION        = Value(18)
	val CAP_PROP_MONOCROME            = Value(19)
	val CAP_PROP_SHARPNESS            = Value(20)
	val CAP_PROP_AUTO_EXPOSURE        = Value(21) // DC1394: exposure control done by camera, user can adjust refernce level using this feature
	val CAP_PROP_GAMMA                = Value(22)
	val CAP_PROP_TEMPERATURE          = Value(23)
	val CAP_PROP_TRIGGER              = Value(24)
	val CAP_PROP_TRIGGER_DELAY        = Value(25)
	val CAP_PROP_WHITE_BALANCE_RED_V  = Value(26)
	val CAP_PROP_ZOOM                 = Value(27)
	val CAP_PROP_FOCUS                = Value(28)
	val CAP_PROP_GUID                 = Value(29)
	val CAP_PROP_ISO_SPEED            = Value(30)
	val CAP_PROP_BACKLIGHT            = Value(32)
	val CAP_PROP_PAN                  = Value(33)
	val CAP_PROP_TILT                 = Value(34)
	val CAP_PROP_ROLL                 = Value(35)
	val CAP_PROP_IRIS                 = Value(36)
	val CAP_PROP_SETTINGS             = Value(37)

平滑滤镜

org.opencv.imgproc.Imgproc类提供了一系列图像的操作。

卷积操作:

核心遍历

通过核心的遍历可以实现很多滤镜效果, 把5x5大小的核心的中心点依次对准13x13目标图像的每一个点, 当核心位于图像的边缘时,核心中的点会位于目标图像外。

核心遍历

对于图像外点的值如果定义, 通过borderType定义:

  • Core.BORDER_REPLICATE:全按界内第一个点的值。
  • Core.BORDER_REFLECT:把边界作为对称轴,向边界内镜像取值。
  • Core.BORDER_REFLECT_101:把边界内的第一个点作为对称轴,向边界内镜像取值。
  • Core.BORDER_WRAP:按边界内的顺序循环填充。
  • Core.BORDER_CONSTANT:全都填充0。

核心遍历

常用的四种平滑滤镜:

  • 平均滤镜
  • 高斯滤镜
  • 中位数滤镜
  • 双向滤镜

平均值滤镜Imgproc.blur()

最简单的模糊方案是用一个三维的卷积核心(Kernel Convolution)来过滤图像。 比如从\(3 \times 3\)的卷积核心里算出9个点的平均值:

\[ \frac{1}{9} \begin{bmatrix} 1 & 1 & 1 \\ 1 & 1 & 1 \\ 1 & 1 & 1 \end{bmatrix} \]

方法blur可用于此类变换:

static void blur(Mat src, Mat dst, Size ksize) 
static void blur(Mat src, Mat dst, Size ksize, Point anchor) 
static void blur(Mat src, Mat dst, Size ksize, Point anchor, int borderType) 

参数:

  • size指定核心的大小,比如new Size(3.0, 3.0)指定了个\(3 \times 3\)的核心。
  • anchor指定锚点。
  • borderType指定当核心处于图像外时的行为。

高斯滤镜Imgproc.GaussianBlur()

按高斯函数,给核心中间的比重高,边上的比重低。

高斯函数

static void GaussianBlur(Mat src, Mat dst, Size ksize, double sigmaX) 

static void GaussianBlur(Mat src, Mat dst, Size ksize, double sigmaX, 
		double sigmaY)

static void GaussianBlur(Mat src, Mat dst, Size ksize, double sigmaX, 
		double sigmaY, int borderType)

例:

Imgproc.GaussianBlur(image, output, new Size(3.0, 3.0), 0)

这里的sigma设置为0并按以下公式从中心到边缘计算比重:

\[ sigma = 0.3 \times ((ksize - 1) \times 0.5 - 1) + 0.8 \]

其中的ksize是核心的孔大小,在这里是3

中位数滤镜Imgproc.medianBlur()

static void medianBlur(Mat src, Mat dst, int ksize) 

其中的ksize是核心的孔大小,在这里是3

双向滤镜Imgproc.bilateralFilter()

static void bilateralFilter(Mat src, Mat dst, int d, double sigmaColor, 
		double sigmaSpace) 

static void bilateralFilter(Mat src, Mat dst, int d, double sigmaColor, 
		double sigmaSpace, int borderType) 

绘图

绘圆

public static void circle(
		Mat img,        // 把圈画这图上
    Point center,   // 圆心
    int radius,     // 半径
    Scalar color,   // 圈的颜色
    int thickness,  // 浓度 厚度 default: 1
    int lineType,   // 线类型    default: LINE_8
    int shift)      //           default: 0

thickness: Thickness of the circle outline, if positive. Negative thickness means that a filled circle is to be drawn. 如果是正数,表示组成圆的线条的粗细程度。否则,表示圆是否被填充.

LineType C Python Java
4-connected lineLINE_4 cv.LINE_4 Imgproc.LINE_4
8-connected lineLINE_8 cv.LINE_8 Imgproc.LINE_8
antialiased lineLINE_AA cv.LINE_AA Imgproc.LINE_AA

shift: Number of fractional bits in the coordinates of the center and in the radius value. 圆心坐标点和半径值的小数点位数,比如:为0,就不变。为1,就把半径和前面参数 中设置的半径,X,Y的值都除10(小数点左移一位),为2就除100(小数点左移2位)。

形态学操作

形态操作会(Morphological Operator)改变图像的形状, 比如腐蚀(erosion)与膨胀(dilation)。

腐蚀操作

腐蚀操作

  • 原图像是A,3x3的活动块中心点叫作锚点(anchor),记为B。
  • 依次把锚点B对准原图像上的每个点上, 然后把锚点所在像素的颜色设置为活动块上最暗的那个点。
  • 遍历完所有的点后,图像A缩小为图像C

活动块的形状有以下三类:

  • Imgproc.CV_SHAPE_RECT
  • Imgproc.CV_SHAPE_ELLIPSE
  • Imgproc.CV_SHAPE_CROSS

Imgproc.getStructuringElement()用来生成一个活动块, 可以指定大小,形状,锚点的位置:

static Mat getStructuringElement(int shape, Size ksize)

static Mat getStructuringElement(int shape, Size ksize, Point anchor)

例子:

private[this] def getKernelFromShape(elemSize: Int, elemShape: Int): Mat = {
  Imgproc.getStructuringElement(elemShape, 
    new Size(elemSize * 2 + 1, elemSize * 2 + 1), 
    new Point(elemSize, elemSize));
}

def erode(input: Mat, elemSize: Int, elemShape: Int): Mat = {
  val outputImage = new Mat();
  val element = getKernelFromShape(elemSize, elemShape);
  Imgproc.erode(input,outputImage, element);
  outputImage
}

val result = erode(imgMtx, 3, Imgproc.CV_SHAPE_RECT);

图

膨胀操作

膨胀与腐蚀的操作类似,不过是把锚点所在的颜色换成活动块上最亮的点。

def dilate(input: Mat, elemSize: Int, elemShape: Int): Mat = {
  val outputImage = new Mat();
  val element = getKernelFromShape(elemSize, elemShape);
  Imgproc.dilate(input,outputImage, element);
  outputImage
}

val result = dilate(imgMtx, 3, Imgproc.CV_SHAPE_RECT);

结合腐蚀与膨胀操作

Imgproc.morphologyEx()方法可以结合腐蚀与膨胀操作实现一些特殊的效果:

static void	morphologyEx(Mat src, Mat dst, int op, Mat kernel)

static void	morphologyEx(Mat src, Mat dst, int op, Mat kernel, 
		Point anchor, int iterations)

static void	morphologyEx(Mat src, Mat dst, int op, Mat kernel, 
		Point anchor, int iterations, int borderType, Scalar borderValue)

op的值可以采用Imgproc中定义的常量:

  • MORPH_OPEN: 先腐蚀,再膨胀,可清除一些小亮点,放大局部低亮度的区域
  • MORPH_CLOSE: 先膨胀,再腐蚀,可清除小黑点
  • MORPH_GRADIENT: 膨胀图与腐蚀图之差,提取物体边缘
  • MORPH_TOPHAT: 原图像-开运算图,突出原图像中比周围亮的区域
  • MORPH_BLACKHAT: 闭运算图-原图像,突出原图像中比周围暗的区域

open:

图

close:

图

例子:

def morphOpen(input: Mat, elemSize: Int, elemShape: Int): Mat = {
  val outputImage = new Mat();
  val elem = getKernelFromShape(elemSize, elemShape);
  Imgproc.morphologyEx(input, outputImage, Imgproc.MORPH_OPEN, elem)
  outputImage
}

def morphClose(input: Mat, elemSize: Int, elemShape: Int): Mat = {
  val outputImage = new Mat();
  val elem = getKernelFromShape(elemSize, elemShape);
  Imgproc.morphologyEx(input, outputImage, Imgproc.MORPH_CLOSE, elem)
  outputImage
}

def morphGradient(input: Mat, elemSize: Int, elemShape: Int): Mat = {
  val outputImage = new Mat();
  val elem = getKernelFromShape(elemSize, elemShape);
  Imgproc.morphologyEx(input, outputImage, Imgproc.MORPH_GRADIENT, elem)
  outputImage
}

def morphTophat(input: Mat, elemSize: Int, elemShape: Int): Mat = {
  val outputImage = new Mat();
  val elem = getKernelFromShape(elemSize, elemShape);
  Imgproc.morphologyEx(input, outputImage, Imgproc.MORPH_TOPHAT, elem)
  outputImage
}

def morphBlackhat(input: Mat, elemSize: Int, elemShape: Int): Mat = {
  val outputImage = new Mat();
  val elem = getKernelFromShape(elemSize, elemShape);
  Imgproc.morphologyEx(input, outputImage, Imgproc.MORPH_BLACKHAT, elem)
  outputImage
}

溢色

溢色操作(flood fill)类似于Windows画板工具中的漆桶,

图

public static int floodFill(
		Mat image,         // 图片
    Mat mask,          // 遮盖
    Point seedPoint,   // 起始点
    Scalar newVal,     // 溢色的新颜色
    Rect rect,         // 方形的区域,限制重绘的边界
    Scalar loDiff,     // 最小差异
    Scalar upDiff,     // 最大差异
    int flags)         // 标记

mask - 一个比原图片长宽都大两人个像素的黑色遮盖层,记录上一次溢色操作溢出的区域。 上一次溢色的区域是被遮盖的,再次点这个区域中的点开始溢色是无效的, 这个可以防止重复不断地从一个点开始溢色到最后把整个图片都变成同一个颜色。 如果要取消遮盖,只要把mask设置为一个new Mat()就可以了。

val originalImage = openFile("testdata/../wiki_images/cathedral-small.jpg")

val mask =  new Mat()
mask.create(new Size(imgMtx.cols() + 2, imgMtx.rows() + 2), CvType.CV_8UC1)
mask.setTo(new Scalar(0))

flags – Operation flags. The first 8 bits contain a connectivity value. The default value of 4 means that only the four nearest neighbor pixels (those that share an edge) are considered. A connectivity value of 8 means that the eight nearest neighbor pixels (those that share a corner) will be considered. The next 8 bits (8-16) contain a value between 1 and 255 with which to fill the mask (the default value is 1).

For example, 4 | ( 255 << 8 ) will consider 4 nearest neighbours and fill the mask with a value of 255. The following additional options occupy higher bits and therefore may be further combined with the connectivity and mask fill values using bit-wise or (|):

  • FLOODFILL_FIXED_RANGE If set, the difference between the current pixel and seed pixel is considered. Otherwise, the difference between neighbor pixels is considered (that is, the range is floating).
  • FLOODFILL_MASK_ONLY If set, the function does not change the image (newVal is ignored), and only fills the mask with the value specified in bits 8-16 of flags as described above. This option only make sense in function variants that have the mask parameter.
val connectivity = 8    // value of connectivity can be 4 or 8

val newRange = if (range == FIXED_RANGE) Imgproc.FLOODFILL_FIXED_RANGE else 0
val flags = connectivity + (newMaskVal << 8) + newRange

图像金字塔

图像金字塔(Image Pyramids)

其实就是对图像进行放大或缩小。

高斯金字塔(Gaussian pyramid):用来下采样(downsample,也叫缩小像素采样,缩小图像)。 拉普拉斯金字塔(Laplacian pyramid):用来从金字塔底层图像重建上采样图像,教程未进一步介绍,需要后面再学习。

主要介绍了高斯金字塔,OpenCV中使用函数 pyrDown 和 pyrUp进行下采样和上采样。

下采样(pyrDown):原始图像位于金字塔最低层。对图像与高斯核作卷积,去除偶数行和列, 得到原图像四分之一的图像,放在原图像的上一层。重复该过程,图像越来越小,层次越来越高。 重复次数越多,图像越小,越位于金字塔的上层。不断迭代就得到整个金字塔。 这种下采样方式会损失精度,丢失部分图像信息。

上采样(pyrUp):将原始图像行数和列数扩大为原来的两倍,新增的行和列以0填充。 使用同上面一样的高斯核与放大后的图像作卷积,获得新增像素的近似值。

class org.opencv.imgproc.Imgproc {

	public static void pyrDown(
			Mat src,
			Mat dst,
			Size dstsize,
			int borderType)

	public static void pyrUp(
			Mat src,
			Mat dst,
			Size dstsize,
			int borderType)

}

Core.subtract

图

class org.opencv.core.Size {

	public static void subtract(
			Mat src1,
			Scalar src2,
			Mat dst,
			Mat mask,
			int dtype)

}

阈值

阈值

二值化图片时设置阈值:

class org.opencv.imgproc.Imgproc {

	public static double threshold(
			Mat src,
			Mat dst,
			double thresh,
			double maxval,
			int type)

}
Thresholding type Output when true Output when false
CV_THRESH_BINARY maxval 0
CV_THRESH_BINARY_INV 0 maxval
CV_THRESH_BINARY threshold source value
CV_TOZERO source value 0
CV_TOZERO_INV 0 source value

图

还有一个adaptiveThreshold()方法:

class org.opencv.imgproc.Imgproc {

		public static void adaptiveThreshold(
				Mat src,
				Mat dst,
				double maxValue,
				int adaptiveMethod,
				int thresholdType,
				int blockSize,    // (blockSize % 2 == 1) && (blockSize > 1)
				double C)

}