某易滑块图片分析
某易的滑块验证,发起请求后会分别返回一张 JPG 背景图和一张 PNG 滑块图。
背景图中会有一块明显的浅色缺口,其余部分会有比较复杂的轮廓。
滑块图具有一定的高度,但可能会与背景图高度不相等。
滑块和缺口是明显对应的,只要提取背景图的边缘和滑块的轮廓,再进行模板匹配,就可以得到正确的滑动距离。
边缘检测的写法
使用 Imgproc.filter2D
方法,传入边缘检测算子,即可实现边缘检测。也可以直接调用 Scharr
、Canny
、Laplacian
等方法。
以 sobel
算子为例:
package opencv.edge;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import utils.ImageUtils;
import utils.ImageViewer;
import static org.opencv.imgcodecs.Imgcodecs.IMREAD_GRAYSCALE;
/**
* @author yuanbug
* date: 2020/5/13
*/
public class Sobel {
static {
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}
private static final String IMAGE_PATH = "D:/test/lena.jpg";
public static final Mat SOBEL_X_KERNEL = new Mat(9, 9, CvType.CV_32F) {{
put(0, 0, -1);
put(0, 1, 0);
put(0, 2, 1);
put(1, 0, -2);
put(1, 1, 0);
put(1, 2, 2);
put(2, 0, -1);
put(2, 1, 0);
put(2, 2, 1);
}};
public static final Mat SOBEL_Y_KERNEL = new Mat(9, 9, CvType.CV_32F) {{
put(0, 0, -1);
put(0, 1, -2);
put(0, 2, -1);
put(1, 0, 0);
put(1, 1, 0);
put(1, 2, 0);
put(2, 0, 1);
put(2, 1, 2);
put(2, 2, 1);
}};
public static void main(String[] args) {
Mat source = Imgcodecs.imread(IMAGE_PATH, IMREAD_GRAYSCALE);
Mat xDestination = new Mat(source.rows(), source.cols(), source.type());
Mat yDestination = new Mat(source.rows(), source.cols(), source.type());
Imgproc.filter2D(source, xDestination, -1, SOBEL_X_KERNEL);
Imgproc.filter2D(source, yDestination, -1, SOBEL_Y_KERNEL);
new ImageViewer(ImageUtils.horizontalConcat(source, xDestination, yDestination), "sobel", true);
}
}
运行结果:
轮廓检测的写法
要做轮廓检测可以使用 findContours
函数。对于复杂图片,需要先做一下自适应阈值化。
代码也很简单:
package opencv.contour;
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.Point;
import org.opencv.core.Scalar;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import utils.ImageUtils;
import utils.ImageViewer;
import java.util.ArrayList;
import java.util.List;
import static org.opencv.imgcodecs.Imgcodecs.IMREAD_GRAYSCALE;
/**
* @author yuanbug
* date: 2020/5/13
*/
public class Contour {
static {
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}
private static final String IMAGE_PATH = "D:/test/lena.jpg";
public static void main(String[] args) {
Mat source = Imgcodecs.imread(IMAGE_PATH, IMREAD_GRAYSCALE);
Mat destination = source.clone();
Mat contoursMat = new Mat(source.rows(), source.width(), source.type());
List<MatOfPoint> contours = new ArrayList<>();
Imgproc.adaptiveThreshold(destination, destination, 255, Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY_INV, 7, 10);
Imgproc.findContours(destination, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE, new Point(0, 0));
Imgproc.drawContours(contoursMat, contours, -1, new Scalar(255, 0, 0, 0));
new ImageViewer(ImageUtils.horizontalConcat(source, destination, contoursMat), "轮廓", true);
}
}
运行结果:
模板匹配的写法
做模板匹配,需要注意结果 Mat 的行数、列数、格式要求,剩下的也就只有调用库函数了。
调包谁不会啊:
package opencv.match;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.Point;
import org.opencv.core.Scalar;
import org.opencv.highgui.HighGui;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import static org.opencv.imgcodecs.Imgcodecs.IMREAD_COLOR;
import static org.opencv.imgcodecs.Imgcodecs.IMREAD_GRAYSCALE;
/**
* @author yuanbug
* date: 2020/5/13
*/
public class TemplateMatch {
static {
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}
private static final String LENA_PATH = "D:/test/lena.jpg";
private static final String LENA_FACE_PATH = "D:/test/face.jpg";
public static void main(String[] args) {
Mat lena = Imgcodecs.imread(LENA_PATH, IMREAD_COLOR);
Mat face = Imgcodecs.imread(LENA_FACE_PATH, IMREAD_COLOR);
Mat result = new Mat(lena.rows() - face.rows() + 1, lena.cols() - face.cols() + 1, CvType.CV_32FC1);
Imgproc.matchTemplate(lena, face, result, Imgproc.TM_CCORR_NORMED);
// 拿位置
Core.MinMaxLocResult locResult = Core.minMaxLoc(result);
double x = locResult.maxLoc.x;
double y = locResult.maxLoc.y;
// 画矩形
Imgproc.rectangle(lena, new Point(x, y), new Point(x + face.cols(), y + face.rows()), new Scalar(0, 0, 255), 2, Imgproc.LINE_AA);
HighGui.imshow("模板匹配", lena);
HighGui.waitKey();
}
}
运行结果:
某易滑块位置识别
无非就是把前面的几个操作一锅乱炖而已:
package opencv;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.Point;
import org.opencv.core.Scalar;
import org.opencv.highgui.HighGui;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import utils.ImageUtils;
import java.util.ArrayList;
import java.util.List;
import static org.opencv.imgcodecs.Imgcodecs.IMREAD_GRAYSCALE;
/**
* @author yuanbug
* date: 2020/5/13
*/
public class NeteaseSlideCrack {
static {
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}
private static final String BACKGROUND_PATH = "D:/test/wyyd_bg.jpg";
private static final String PIECE_PATH = "D:/test/wyyd_hk.png";
private static Mat findBackgroundEdge() {
Mat background = Imgcodecs.imread(BACKGROUND_PATH, IMREAD_GRAYSCALE);
Mat backgroundEdge = new Mat(background.rows(), background.cols(), background.type());
// 乱写的两个阈值,效果似乎还不错
Imgproc.Canny(background, backgroundEdge, 300, 500);
return backgroundEdge;
}
private static Mat findPieceContour() {
Mat piece = Imgcodecs.imread(PIECE_PATH, IMREAD_GRAYSCALE);
Mat pieceContour = new Mat(piece.rows(), piece.width(), piece.type());
List<MatOfPoint> contours = new ArrayList<>();
// 这一步自适应阈值化不做也可以,滑块的轮廓本来就很清晰,做一下可以减少滑块内部图案的影响
Imgproc.adaptiveThreshold(piece, piece, 255, Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY_INV, 3, 5);
Imgproc.findContours(piece, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE, new Point(0, 0));
Imgproc.drawContours(pieceContour, contours, -1, new Scalar(255, 255, 255));
return pieceContour;
}
public static void main(String[] args) {
Mat pieceContour = findPieceContour();
Mat backgroundEdge = findBackgroundEdge();
Mat preProcessResult = ImageUtils.horizontalConcat(backgroundEdge, pieceContour);
Mat result = new Mat(backgroundEdge.rows() - pieceContour.rows() + 1, backgroundEdge.cols() - pieceContour.cols() + 1, CvType.CV_32FC1);
Imgproc.matchTemplate(backgroundEdge, pieceContour, result, Imgproc.TM_CCORR_NORMED);
Core.MinMaxLocResult locResult = Core.minMaxLoc(result);
double x = locResult.maxLoc.x;
double y = locResult.maxLoc.y;
System.out.println("水平偏移:" + x);
// 转回彩图,方便看清楚矩形红边
Imgproc.cvtColor(backgroundEdge, backgroundEdge, Imgcodecs.IMREAD_COLOR);
Imgproc.rectangle(backgroundEdge, new Point(x, y), new Point(x + pieceContour.cols(), y + pieceContour.rows()), new Scalar(0, 0, 255), 2, Imgproc.LINE_AA);
HighGui.imshow("预处理", preProcessResult);
HighGui.imshow("模板匹配", backgroundEdge);
HighGui.waitKey();
}
}
运行结果:
用 Java 调 OpenCV 还是挺简单的,目前遇到的最大麻烦在于搞清楚每个函数到底是在哪个类里面,明明写 Python 的时候全部都用 cv2
就行。