├── README.md ├── main.js ├── project.json └── sliding_block.js /README.md: -------------------------------------------------------------------------------- 1 | # 虎哥Autoxjs本地滑块识别插件 2 | # 仅供学习参考,请勿用于非法用途 3 | 4 | 继autojs下架后 5 | 更新功能使得autoxjs可以继续使用 6 | 极验可能过不了 7 | 大部分平台自制滑块是可以过的 8 | 9 | ### 优势 10 | - 支持所有分辨率 11 | - 支持所有图形 12 | - 500毫秒以内识别 13 | - 覆盖市场大部分滑块(采用opencv轮廓识别,针对不同滑块种类采用不同算法) 14 | - 60-95%的识别率 15 | - 内置多种滑动模式,过大部分机器检测 16 | - 内置图片处理查看预览功能,方便调参,可通过循环递进调参遍历图片进行扩展,从而迅速找出合适的验证码参数 17 | 18 | ### 功能 19 | - 图像缩放(用于加快识别速度) 20 | - 图像识别检测 21 | - 目标缺口图像识别范围 22 | - 缺口颜色检测(分原始缺口颜色和目标缺口颜色) 23 | - 灰度值(二值化)检测(分原始缺口和目标缺口最大最小二值化值) 24 | - 缺口大小检测 25 | - 图像轮廓结果筛选 26 | - 面积筛选 27 | - 周长筛选 28 | - 宽高筛选 29 | - y轴筛选 30 | - 以上均有相似度调节 31 | - 识别轮廓结果图查看(分三环节,用于排查参数是否有误) 32 | - 大小筛选结果图 33 | - 面积周长筛选结果图 34 | - 宽高y轴筛选结果 35 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 本地滑块识别 3 | * autoxjs 4 | * by:虎哥 5 | */ 6 | // "ui"; 7 | runtime.images.initOpenCvIfNeeded(); 8 | 9 | importClass(org.opencv.imgproc.Imgproc); 10 | 11 | slidingBlock = require("./sliding_block.js"); 12 | 13 | let sizeObj = {}; 14 | let colorObj = {} 15 | 16 | 17 | init(); 18 | test(); //斗鱼滑块可直接测试使用 19 | 20 | 21 | function initDM(){ 22 | 23 | // 设置缺口颜色检测对象 24 | // ------------------------ 25 | colorObj.targetColor = "#333333"; 26 | colorObj.targetColorOffset = 60; 27 | colorObj.originColor = "#B7D340"; 28 | colorObj.originColorOffset = 80; 29 | 30 | 31 | // ------------------------- 32 | // 设置缺口大小检测对象 33 | sizeObj.width = 150; 34 | sizeObj.height = 150 35 | sizeObj.sizeOffset = 50; 36 | sizeObj.sim = 30 37 | } 38 | 39 | function showDM(){ 40 | initDM(); 41 | 42 | let picObj = idContains("captcha").findOne(5000).child(0).child(0); 43 | sleep(1000) 44 | let rect = picObj.bounds(); 45 | let img = capturePic(rect.left,rect.top,rect.width(),rect.height()); 46 | slidingBlock.discernSlidingblockTestByShape(img,1,null,1,colorObj,null,sizeObj,null,"/sdcard/Pictures/sliding/result1.png","/sdcard/Pictures/sliding/show.png",1,1); 47 | } 48 | 49 | 50 | 51 | function test(){ 52 | showDM(); 53 | } 54 | 55 | 56 | function init(){ 57 | console.show(); 58 | if(!requestScreenCapture()){ 59 | toast("请求申请截图权限失败"); 60 | exit(); 61 | } 62 | auto.waitFor(); 63 | } 64 | 65 | 66 | 67 | /** 68 | * 把屏幕中的滑块图片截取出来返回成image 69 | * @param {int} x 图片左坐标 70 | * @param {int} y 图片上坐标 71 | * @param {int} width 图片宽度 72 | * @param {int} height 图片高度 73 | * @returns 截取区域后的image图片 74 | */ 75 | function capturePic(x,y,width,height){ 76 | let src = captureScreen(); 77 | let clip = images.clip(src,x,y,width,height); 78 | src.recycle(); 79 | return clip; 80 | } 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "斗鱼注册机", 3 | "main": "main.js", 4 | "ignore": [ 5 | "build" 6 | ], 7 | "packageName": "com.hu", 8 | "versionName": "1.0.0", 9 | "versionCode": 1 10 | } -------------------------------------------------------------------------------- /sliding_block.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 虎哥Autojs本地滑块识别插件 3 | * 4 | * 支持所有分辨率 5 | * 6 | */ 7 | // 初始化opencv 8 | //引入包和类 9 | importPackage(org.opencv.core); 10 | importPackage(java.util); 11 | importClass(org.opencv.imgcodecs.Imgcodecs); 12 | importClass(org.opencv.imgproc.Imgproc); 13 | importPackage(java.lang) 14 | 15 | slidingBlock = {}; 16 | 17 | var imgPath, originThresholdImgPath, targetThresholdImgPath, gScale, gWidth, gHeight, gAreaOffset, mat, cList, isError = false, hasOrigin = false; 18 | var soX1, soY1, soX2, soY2, stX1, stY1, stX2, stY2, sSim; 19 | var shapeJudge = false; 20 | var gRightX; 21 | let oWidth,oHeight,oSim,imgWidth; 22 | 23 | /** 24 | * 25 | * @param {Image} img 滑块图片 26 | * @param {int} rScale 缩小比例,(0.1-1,省略默认1) 27 | * @param {JSON} rangObj 识别范围对象,用于检测缺口轮廓时识别的图片范围 28 | * @param {int} x1 左 29 | * @param {int} y1 上 30 | * @param {int} x2 右 31 | * @param {int} y2 下 32 | * @param {int} judgeType 检测轮廓机制 1=通过颜色判断轮廓和大小 2=通过二值化判断轮廓和大小 33 | * @param {JSON} colorObj (二选一)缺口颜色检测对象,当缺口颜色相同时使用 34 | * @param {string} targetColor 目标缺口颜色 35 | * @param {string} targetColorOffset 目标缺口颜色偏移(范围)(10-150,省略默认16) 36 | * @param {string} originColor 源缺口颜色(可省略) 37 | * @param {string} originColorOffset 源缺口颜色偏移(范围)(10-150,省略默认16) 38 | * @param {JSON} thresholdObj (二选一)缺口灰度值检测对象,当缺口颜色时使用 39 | * @param {int} min 二值化的最小值 40 | * @param {int} max 二值化的最大值 41 | * @param {JSON} sizeObj 缺口大小检测对象,当缺口大小相同时使用 42 | * @param {int} width 缺口宽 43 | * @param {int} height 缺口高 44 | * @param {int} sizeOffset 大小相似度(0-20,省略默认5) 45 | * @param {JSON} shapeObj 可省略,缺口形状检测对象,当滑块具有干扰缺口时使用,用来判断源缺口与目标缺口形状是否相同。 46 | * @param {JSON} origin 源缺口识别范围对象 47 | * @param {int} x1 左 48 | * @param {int} y1 上 49 | * @param {int} x2 右 50 | * @param {int} y2 下 51 | * @param {JSON} target 目标缺口识别范围对象 52 | * @param {int} x1 左 53 | * @param {int} y1 上 54 | * @param {int} x2 右 55 | * @param {int} y2 下 56 | * @param {int} sim 形状相似度(0-20,省略默认5) 57 | * @param {string} ImagePath 可省略,滑块图片保存路径 58 | * @returns 目标缺口中间x坐标值 59 | */ 60 | slidingBlock.discernSlidingblock = function (img, rScale, rangObj, judgeType, colorObj, thresholdObj, sizeObj, shapeObj, ImagePath) { 61 | 62 | 63 | let times = slidingBlockCore(img, rScale, rangObj, judgeType, colorObj, thresholdObj, sizeObj, shapeObj, ImagePath); 64 | 65 | 66 | if (isError) { //识别出错 67 | console.error("填写参数格式错误"); 68 | return -1; 69 | } else if (cList.size() > 0) { //说明找到了轮廓 70 | let points = cList.get(cList.size() - 1).toList(); 71 | let rightX = getPoint(points, 2); 72 | let leftX = getPoint(points, 4); 73 | 74 | resultX = (leftX + (rightX - leftX) / 2) / rScale; 75 | console.log("用时毫秒:", times); 76 | console.info("最终滑块结果为:x=", resultX); 77 | return resultX; 78 | } else { //未找到轮廓 79 | console.log("用时毫秒:", times); 80 | console.error("未找到x"); 81 | return -1; 82 | } 83 | 84 | 85 | } 86 | 87 | 88 | /** 89 | * 展示经过范围检测后的图片,范围检测为第一步 90 | * @param {Image} img 滑块图片 91 | * @param {int} rScale 缩小比例,(0.1-1,省略默认1) 92 | * @param {JSON} rangObj 识别范围对象,用于检测缺口轮廓时识别的图片范围 93 | * @param {int} x1 左 94 | * @param {int} y1 上 95 | * @param {int} x2 右 96 | * @param {int} y2 下 97 | * @param {int} judgeType 检测轮廓机制 1=通过颜色判断轮廓和大小 2=通过二值化判断轮廓和大小 98 | * @param {JSON} colorObj (二选一)缺口颜色检测对象,当缺口颜色相同时使用 99 | * @param {string} targetColor 目标缺口颜色 100 | * @param {string} targetColorOffset 目标缺口颜色偏移(范围)(10-150,省略默认16) 101 | * @param {string} originColor 源缺口颜色(可省略) 102 | * @param {string} originColorOffset 源缺口颜色偏移(范围)(10-150,省略默认16) 103 | * @param {JSON} thresholdObj (二选一)缺口灰度值检测对象,当缺口颜色时使用 104 | * @param {int} min 二值化的最小值 105 | * @param {int} max 二值化的最大值 106 | * @param {JSON} sizeObj 缺口大小检测对象,当缺口大小相同时使用 107 | * @param {int} width 缺口宽 108 | * @param {int} height 缺口高 109 | * @param {int} sizeOffset 大小相似度(0-20,省略默认5) 110 | * @param {JSON} shapeObj 可省略,缺口形状检测对象,当滑块具有干扰缺口时使用,用来判断源缺口与目标缺口形状是否相同。 111 | * @param {JSON} origin 源缺口识别范围对象 112 | * @param {int} x1 左 113 | * @param {int} y1 上 114 | * @param {int} x2 右 115 | * @param {int} y2 下 116 | * @param {JSON} target 目标缺口识别范围对象 117 | * @param {int} x1 左 118 | * @param {int} y1 上 119 | * @param {int} x2 右 120 | * @param {int} y2 下 121 | * @param {int} sim 形状相似度(0-20,省略默认5) 122 | * @param {string} ImagePath 可省略,滑块图片保存路径 123 | * @param {string} writePath 124 | * @param {int} showType 展示形式 1=展示轮廓图,2=展示二值化图 125 | * @param {int} showPos 展示图片位置 1=展示处理目标缺口后的图片,2=展示处理源缺口后的图片,可以与展示形式配合使用,如果展示形式等于1,那么将展示所有的轮廓(源缺口+目标缺口)(可省略,默认为1) 126 | */ 127 | slidingBlock.discernSlidingblockTestByRange = function (img, rScale, rangObj, judgeType, colorObj, thresholdObj, sizeObj, shapeObj, ImagePath, writePath, showType, showPos) { 128 | 129 | let list = getOutlineList1(img, rScale, rangObj, judgeType, colorObj, thresholdObj, sizeObj, shapeObj, ImagePath); 130 | slidingBlock.showImage(list, writePath, showType, showPos); 131 | } 132 | 133 | /** 134 | * 展示经过大小检测后的图片,大小检测为第二步 135 | */ 136 | slidingBlock.discernSlidingblockTestBySize = function (img, rScale, rangObj, judgeType, colorObj, thresholdObj, sizeObj, shapeObj, ImagePath, writePath, showType, showPos) { 137 | 138 | let list = getOutlineList2(img, rScale, rangObj, judgeType, colorObj, thresholdObj, sizeObj, shapeObj, ImagePath); 139 | slidingBlock.showImage(list, writePath, showType, showPos); 140 | } 141 | 142 | 143 | /** 144 | * 展示经过形状检测(去除干扰项)后的图片,形状检测为第三步 145 | */ 146 | slidingBlock.discernSlidingblockTestByShape = function (img, rScale, rangObj, judgeType, colorObj, thresholdObj, sizeObj, shapeObj, ImagePath, writePath, showType, showPos) { 147 | 148 | let list = getOutlineList3(img, rScale, rangObj, judgeType, colorObj, thresholdObj, sizeObj, shapeObj, ImagePath); 149 | slidingBlock.showImage(list, writePath, showType, showPos); 150 | } 151 | 152 | 153 | /** 154 | * 展示图片,内部方法 155 | * @param {Array} list 轮廓列表 156 | * @param {string} writePath 图片写入路径 157 | * @param {int} showType 展示形式:1=展示轮廓图,2=展示二值化图 158 | * @param {int} showPos 展示图片位置 1=展示处理目标缺口后的图片,2=展示处理源缺口后的图片,可以与展示形式配合使用,如果展示形式等于1,那么将展示所有的轮廓(源缺口+目标缺口)(可省略,默认为1) 159 | * @returns 160 | */ 161 | slidingBlock.showImage = function(list, writePath, showType, showPos) { 162 | if (isError) { 163 | console.error("填写参数格式错误"); 164 | return; 165 | } 166 | 167 | writePath = writePath == undefined || writePath == null ? "/sdcard/Pictures/sliding/-1.png" : writePath; 168 | showPos = showPos == undefined || showPos == null ? 1 : showPos; 169 | 170 | discernSlidingblockTestByList(list, writePath, showType, showPos) 171 | } 172 | 173 | 174 | /** 175 | * 被showImage调用,通过list轮廓列表展示图片,内部方法 176 | * @param {Array} list 轮廓列表 177 | * @param {string} writePath 图片写入路径 178 | * @param {int} showType 展示形式:1=展示轮廓图,2=展示二值化图 179 | * @param {int} showPos 展示图片位置 1=展示处理目标缺口后的图片,2=展示处理源缺口后的图片,可以与展示形式配合使用,如果展示形式等于1,那么将展示所有的轮廓(源缺口+目标缺口)(可省略,默认为1) 180 | */ 181 | function discernSlidingblockTestByList(list, writePath, showType, showPos) { 182 | let mat3; 183 | 184 | if (showType == 1) { 185 | // 读图片路径转为mat 186 | mat3 = Imgcodecs.imread(imgPath, 1); 187 | // 绘制轮廓 188 | Imgproc.drawContours(mat3, list, -1, new Scalar(0, 255, 0), Imgproc.LINE_4, Imgproc.LINE_AA); 189 | Imgcodecs.imwrite(writePath, mat3); 190 | app.viewFile(writePath); //显示图片 191 | } else if (showType == 2) { 192 | if (showPos == 2) { 193 | mat3 = Imgcodecs.imread(originThresholdImgPath, 0); 194 | } else if (showPos == 1) { 195 | mat3 = Imgcodecs.imread(targetThresholdImgPath, 0); 196 | } 197 | 198 | Imgcodecs.imwrite(writePath, mat3); 199 | app.viewFile(writePath); //显示图片 200 | } 201 | 202 | } 203 | 204 | /** 205 | * 放大或缩小长度,取决于gScale的值 206 | * @param {int} len 207 | * @returns 208 | */ 209 | function getLengthByScale(len) { 210 | return len * gScale; 211 | } 212 | 213 | /** 214 | * 获取第一步(范围检测)处理完的轮廓列表 215 | * @returns 轮廓列表 216 | */ 217 | function getOutlineList1(img, rScale, rangeObj, judgeType, colorObj, thresholdObj, sizeObj, shapeObj, ImagePath) { 218 | 219 | 220 | let targetColor, targetColorOffset, originColor, originColorOffset, width, height, sizeOffset, tMin, tMax, targetTempPath; 221 | let oX1, oY1, oX2, oY2; 222 | gScale = rScale; 223 | 224 | if (rangeObj == null) { 225 | // 如果为空则为全图范围查找 226 | oX1 = 0; 227 | oY1 = 0; 228 | oX2 = img.getWidth(); 229 | oY2 = img.getHeight(); 230 | } else { 231 | oX1 = rangeObj.x1; 232 | oY1 = rangeObj.y1; 233 | oX2 = rangeObj.x2; 234 | oY2 = rangeObj.y2; 235 | } 236 | 237 | oX1 = getLengthByScale(oX1); 238 | oY1 = getLengthByScale(oY1); 239 | oX2 = getLengthByScale(oX2); 240 | oY2 = getLengthByScale(oY2); 241 | 242 | 243 | if (sizeObj == null || sizeObj == undefined) { 244 | return setError("请添加大小判断对象!"); 245 | } 246 | 247 | if (shapeObj != null && shapeObj != undefined) { 248 | shapeJudge = true; 249 | soX1 = getLengthByScale(shapeObj.origin.x1); 250 | soY1 = getLengthByScale(shapeObj.origin.y1); 251 | soX2 = getLengthByScale(shapeObj.origin.x2); 252 | soY2 = getLengthByScale(shapeObj.origin.y2); 253 | 254 | stX1 = getLengthByScale(shapeObj.target.x1); 255 | stY1 = getLengthByScale(shapeObj.target.y1); 256 | stX2 = getLengthByScale(shapeObj.target.x2); 257 | stY2 = getLengthByScale(shapeObj.target.y2); 258 | sSim = getLengthByScale(shapeObj.sim); 259 | } 260 | 261 | width = getLengthByScale(sizeObj.width); 262 | height = getLengthByScale(sizeObj.height); 263 | oWidth = width; 264 | oHeight = height 265 | oSim = sizeObj.sim 266 | sizeOffset = getLengthByScale(sizeObj.sizeOffset) == undefined ? getLengthByScale(16) : getLengthByScale(sizeObj.sizeOffset); 267 | targetTempPath = "/sdcard/Pictures/sliding/-2.png"; 268 | originTempPath = "/sdcard/Pictures/sliding/-3.png"; 269 | ImagePath = ImagePath == undefined ? "/sdcard/Pictures/sliding/-99.png" : ImagePath; 270 | gWidth = width; 271 | gHeight = height; 272 | gAreaOffset = sizeOffset; 273 | imgPath = ImagePath; 274 | originThresholdImgPath = originTempPath; 275 | targetThresholdImgPath = targetTempPath; 276 | 277 | img = images.scale(img, rScale, rScale); 278 | imgWidth = img.getWidth(); 279 | imgHeight = img.getHeight(); 280 | images.save(img, ImagePath); 281 | mat = Imgcodecs.imread(ImagePath, 1); 282 | 283 | if (judgeType == 1) { 284 | if (colorObj == null || colorObj == undefined) { 285 | return setError("当前检测轮廓机制为颜色检测(1),请添加颜色判断对象!"); 286 | } 287 | 288 | targetColor = colorObj.targetColor; 289 | targetColorOffset = colorObj.targetColorOffset == undefined ? 16 : colorObj.targetColorOffset; 290 | let image1 = images.interval(img, targetColor, targetColorOffset); 291 | images.save(image1, targetTempPath); 292 | image1.recycle(); 293 | 294 | if (colorObj.originColor != undefined) { 295 | hasOrigin = true; 296 | originColor = colorObj.originColor; 297 | originColorOffset = colorObj.originColorOffset == undefined ? 16 : colorObj.originColorOffset; 298 | let image2 = images.interval(img, originColor, originColorOffset); 299 | images.save(image2, originTempPath); 300 | image2.recycle(); 301 | } 302 | } else if (judgeType == 2) { 303 | if (thresholdObj == null || thresholdObj == undefined) { 304 | return setError("当前检测轮廓机制为二值化检测(2),请添加灰度值判断对象!"); 305 | } 306 | 307 | 308 | if(shapeObj == null || shapeObj.target == null || shapeObj.origin == null){ 309 | soX1 = 0; 310 | soY1 = 0; 311 | soX2 = img.getWidth() / 4; 312 | soY2 = img.getHeight(); 313 | stX1 = soX2; 314 | stY1 = 0 315 | stX2 = img.getWidth() 316 | stY2 = img.getHeight(); 317 | }else{ 318 | soX1 = getLengthByScale(shapeObj.origin.x1); 319 | soY1 = getLengthByScale(shapeObj.origin.y1); 320 | soX2 = getLengthByScale(shapeObj.origin.x2); 321 | soY2 = getLengthByScale(shapeObj.origin.y2); 322 | 323 | stX1 = getLengthByScale(shapeObj.target.x1); 324 | stY1 = getLengthByScale(shapeObj.target.y1); 325 | stX2 = getLengthByScale(shapeObj.target.x2); 326 | stY2 = getLengthByScale(shapeObj.target.y2); 327 | sSim = getLengthByScale(shapeObj.sim); 328 | } 329 | 330 | 331 | //原缺口mat图像 332 | console.info(img.getWidth(),img.getHeight()); 333 | console.info(soX2,soY2); 334 | let oPath = "/sdcard/Pictures/sliding/oImg.png"; 335 | let oImg = images.clip(img, soX1, soY1, soX2-soX1, soY2-soY1); 336 | images.save(oImg,oPath); 337 | let oMat = Imgcodecs.imread(oPath, 1); 338 | oMin = thresholdObj.origin.min < 0 ? 0 : thresholdObj.origin.min; 339 | oMax = thresholdObj.origin.max > 255 ? 255 : thresholdObj.origin.max; 340 | let onMat = new Mat(); 341 | Imgproc.threshold(oMat, onMat, oMin, oMax, Imgproc.THRESH_BINARY_INV); 342 | 343 | //目标缺口mat图像 344 | let tPath = "/sdcard/Pictures/sliding/tImg.png"; 345 | let tImg = images.clip(img, stX1, stY1, stX2-stX1, stY2-stY1); 346 | images.save(tImg, tPath); 347 | let tMat = Imgcodecs.imread(tPath, 1); 348 | tMin = thresholdObj.target.min < 0 ? 0 : thresholdObj.target.min; 349 | tMax = thresholdObj.target.max > 255 ? 255 : thresholdObj.target.max; 350 | let tnMat = new Mat(); 351 | Imgproc.threshold(tMat, tnMat, tMin, tMax, Imgproc.THRESH_BINARY); 352 | 353 | let concatMat = new Mat( img.getHeight(), img.getWidth(), onMat.type()); 354 | Core.hconcat([onMat, tnMat], concatMat); 355 | 356 | Imgcodecs.imwrite(targetTempPath, concatMat); 357 | Imgcodecs.imwrite(imgPath, concatMat); 358 | 359 | 360 | oImg.recycle(); 361 | tImg.recycle(); 362 | } 363 | 364 | img.recycle(); 365 | 366 | let mat2 = Imgcodecs.imread(targetTempPath, 0); 367 | let list = new ArrayList(); 368 | let hierarchy = new Mat(); 369 | Imgproc.findContours(mat2, list, hierarchy, Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_NONE); 370 | if (hasOrigin) { 371 | let mat3 = Imgcodecs.imread(originTempPath, 0); 372 | let list2 = new ArrayList(); 373 | let hierarchy2 = new Mat(); 374 | Imgproc.findContours(mat3, list2, hierarchy2, Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_NONE); 375 | list.addAll(list2); 376 | } 377 | 378 | 379 | list = getGraphRangePointList(list, oX1, oY1, oX2, oY2); 380 | 381 | return list; 382 | } 383 | 384 | 385 | 386 | /** 387 | * 获取第二步(大小检测)处理完的轮廓列表 388 | * @returns 轮廓列表 389 | */ 390 | function getOutlineList2(img, rScale, rangeObj, judgeType, colorObj, thresholdObj, sizeObj, shapeObj, ImagePath) { 391 | let list = getOutlineList1(img, rScale, rangeObj, judgeType, colorObj, thresholdObj, sizeObj, shapeObj, ImagePath); 392 | 393 | if (list.size() > 1) { 394 | list = getShapePointList(list); 395 | } 396 | 397 | return list; 398 | } 399 | 400 | /** 401 | * 获取第三步(形状检测)处理完的轮廓列表 402 | * 废除的方法 403 | * @returns 轮廓列表 404 | */ 405 | function getOutlineList3(img, rScale, rangeObj, judgeType, colorObj, thresholdObj, sizeObj, shapeObj, ImagePath) { 406 | let list = getOutlineList2(img, rScale, rangeObj, judgeType, colorObj, thresholdObj, sizeObj, shapeObj, ImagePath); 407 | 408 | if (list.size() > 1) { 409 | list = getSizePointList(list); 410 | } 411 | 412 | return list; 413 | } 414 | 415 | 416 | function getSizePointList(filteredContours){ 417 | filteredContours = sortFilter(filteredContours,"height",oHeight-oSim*oHeight/100,oHeight+oSim*oHeight/100); 418 | filteredContours = sortFilter(filteredContours,"width",oWidth-oSim*oWidth/100,oWidth+oSim*oWidth/100); 419 | filteredContours = sortFilter(filteredContours,"y",0,imgWidth); 420 | 421 | let isResult = false; 422 | if(filteredContours.size() > 1){ 423 | for (let i = 0; i < filteredContours.size(); i++) { 424 | let rect = Imgproc.boundingRect(filteredContours.get(i)); 425 | if(rect.x < imgWidth / 5){ 426 | isResult = true; 427 | break; 428 | } 429 | } 430 | } 431 | 432 | if(!isResult){ 433 | filteredContours = new ArrayList(); 434 | } 435 | console.log("最终结果数量:%d个", filteredContours.size()); 436 | 437 | return filteredContours; 438 | } 439 | 440 | /** 441 | * 排序过滤 442 | * @returns 443 | */ 444 | function sortFilter(list,property,minFilter,maxFilter){ 445 | let filteredContours4 = new ArrayList(); 446 | //排序 过滤y 447 | for (let i = 0; i < list.size(); i++) { 448 | let inserted = false; 449 | let e = list.get(i); 450 | for (let j = 0; j < filteredContours4.size(); j++) { 451 | 452 | let rect1 = Imgproc.boundingRect(e); 453 | let rect2 = Imgproc.boundingRect(list.get(j)); 454 | let r = eval("rect1."+property + " < " + "rect2." + property); 455 | if (r) { 456 | filteredContours4.add(j, e); 457 | inserted = true; 458 | break; 459 | } 460 | } 461 | if (!inserted) { 462 | filteredContours4.add(e); 463 | } 464 | } 465 | 466 | let filteredContours5 = new ArrayList(); 467 | let sim = oSim 468 | 469 | console.log(filteredContours4.size()) 470 | for (let i = 0; i < filteredContours4.size(); i++) { 471 | for (let j = i + 1; j < filteredContours4.size(); j++) { 472 | 473 | let rect1 = Imgproc.boundingRect(filteredContours4.get(i)); 474 | let rect2 = Imgproc.boundingRect(filteredContours4.get(j)); 475 | let difference =eval("Math.abs(rect1."+property+" - rect2."+property+")"); 476 | let threshold = eval("rect1."+property + " * sim / 100"); 477 | let evalStr = "rect1."+property +" >= " + minFilter +" && rect1."+property + " <= " + maxFilter; 478 | let r = eval(evalStr); 479 | console.log("形状筛选"+property+":"+JSON.stringify(rect1)+","+JSON.stringify(rect2) + "--"+ r); 480 | if ( difference <= threshold && r) { 481 | console.log("形状筛选合格值"+property+":"+JSON.stringify(rect1)+","+JSON.stringify(rect2)); 482 | filteredContours5.add(filteredContours4.get(i)); 483 | filteredContours5.add(filteredContours4.get(j)); 484 | break; 485 | } 486 | } 487 | } 488 | 489 | let filteredContoursSet5 = new HashSet(filteredContours5); 490 | filteredContours5 = new ArrayList(filteredContoursSet5) 491 | 492 | return filteredContours5; 493 | } 494 | 495 | 496 | /** 497 | * 内部调用滑块识别方法,计算出图形x坐标位置并返回处理时间 498 | * @returns 499 | */ 500 | function slidingBlockCore(img, rScale, rangeObj, judgeType, colorObj, thresholdObj, sizeObj, shapeObj, ImagePath) { 501 | 502 | let start = new Date().getTime(); 503 | let list = getOutlineList3(img, rScale, rangeObj, judgeType, colorObj, thresholdObj, sizeObj, shapeObj, ImagePath); 504 | 505 | cList = list; 506 | 507 | let end = new Date().getTime(); 508 | return end - start; 509 | } 510 | 511 | /** 512 | * 当识别出错时产生报错,一般出现于用户参数填写错误。 513 | * @param {string} str 报错信息 514 | * @returns x坐标,直接返回-1 515 | */ 516 | function setError(str) { 517 | console.error(str); 518 | isError = true; 519 | return -1; 520 | } 521 | 522 | 523 | /** 524 | * 525 | * 通过形状检测(去除干扰项)查找符合条件的list轮廓列表,并将符合的列表返回 526 | * @param {Array} list 轮廓列表 527 | * @param {int} soX1 源缺口查找范围最左边 528 | * @param {int} soY1 源缺口查找范围最上边 529 | * @param {int} soX2 源缺口查找范围最右边 530 | * @param {int} soY2 源缺口查找范围最下边 531 | * @param {int} stX1 目标缺口查找范围最左边 532 | * @param {int} stY1 目标缺口查找范围最上边 533 | * @param {int} stX2 目标缺口查找范围最右边 534 | * @param {int} stY2 目标缺口查找范围最下边 535 | * @param {int} sSim 源缺口与目标缺口相似度,(0-15,一般5就可以了) 536 | * @returns 轮廓列表 537 | */ 538 | function getShapePointList(list) { 539 | 540 | let filteredContours = new ArrayList(); 541 | let minArea = oHeight * oWidth / 30 //面积 542 | let maxLength = 12 * (oHeight + oWidth) //周长 543 | let sim = oSim 544 | console.log(minArea +":"+maxLength+":"+sim); 545 | 546 | // //排序 547 | // for (let i = 0; i < list.size(); i++) { 548 | // let inserted = false; 549 | // let e = list.get(i); 550 | // for (let j = 0; j < filteredContours.size(); j++) { 551 | // if (Imgproc.contourArea(e) < Imgproc.contourArea(filteredContours.get(j))) { 552 | // filteredContours.add(j, e); 553 | // inserted = true; 554 | // break; 555 | // } 556 | // } 557 | // if (!inserted) { 558 | // filteredContours.add(e); 559 | // } 560 | // } 561 | 562 | // let filteredContours1 = new ArrayList() 563 | 564 | // for (let i = 0; i < filteredContours.size(); i++) { 565 | // for (let j = i + 1; j < filteredContours.size(); j++) { 566 | 567 | // let area1 = Imgproc.contourArea(filteredContours.get(i)); 568 | // let area2 = Imgproc.contourArea(filteredContours.get(j)); 569 | // let difference = Math.abs(area1 - area2); 570 | // let threshold = area1 * sim / 100; // 计算阈值 571 | 572 | // console.log(area1,area2) 573 | // if (minArea <= area1 && minArea <= area2 && difference <= threshold) { 574 | // console.log("面积筛选合格值:"+area1+","+area2); 575 | // filteredContours1.add(filteredContours.get(i)); 576 | // filteredContours1.add(filteredContours.get(j)); 577 | // break; 578 | // } 579 | // } 580 | // } 581 | 582 | let filteredContoursSet = new HashSet(list); 583 | filteredContours1 = new ArrayList(filteredContoursSet) 584 | 585 | let filteredContours2 = new ArrayList(); 586 | //排序 587 | for (let i = 0; i < filteredContours1.size(); i++) { 588 | let inserted = false; 589 | let e = filteredContours1.get(i); 590 | for (let j = 0; j < filteredContours2.size(); j++) { 591 | if ( Imgproc.arcLength(new MatOfPoint2f(e.toArray()),true) < Imgproc.arcLength(new MatOfPoint2f(filteredContours2.get(j).toArray()),true)) { 592 | filteredContours2.add(j, e); 593 | inserted = true; 594 | break; 595 | } 596 | } 597 | if (!inserted) { 598 | filteredContours2.add(e); 599 | } 600 | } 601 | 602 | let filteredContours3 = new ArrayList() 603 | for (let i = 0; i < filteredContours2.size(); i++) { 604 | for (let j = i + 1; j < filteredContours2.size(); j++) { 605 | 606 | let length1 = Imgproc.arcLength(new MatOfPoint2f(filteredContours2.get(i).toArray()),true); 607 | let length2 = Imgproc.arcLength(new MatOfPoint2f(filteredContours2.get(j).toArray()),true); 608 | let difference = Math.abs(length1 - length2); 609 | let threshold = length1 * sim / 100; // 计算阈值 610 | 611 | console.log(length1,length2) 612 | if (difference <= threshold && length1 < maxLength) { 613 | console.log("周长筛选合格值:"+length1+","+length2); 614 | filteredContours3.add(filteredContours2.get(i)); 615 | filteredContours3.add(filteredContours2.get(j)); 616 | break; 617 | 618 | } 619 | } 620 | } 621 | 622 | 623 | let filteredContoursSet3 = new HashSet(filteredContours3); 624 | filteredContours3 = new ArrayList(filteredContoursSet3) 625 | 626 | console.log("通过周长面积特征对比发现符合特征轮廓数量:%d个", filteredContours3.size()); 627 | 628 | return filteredContours3; 629 | } 630 | 631 | 632 | 633 | 634 | /** 635 | * 求出近似范围数组 636 | * @param {int} ArrayList 637 | * @returns 638 | */ 639 | function findNumbersWithinRange(array,sim) { 640 | let result = new ArrayList(); 641 | 642 | // 对数组进行排序 643 | Arrays.sort(array); 644 | 645 | for (let i = 0; i < array.size() - 1; i++) { 646 | for (let j = i + 1; j < array.size(); j++) { 647 | let difference = Math.abs(array.get(i) - array.get(j)); 648 | let threshold = array.get(i) * sim / 100; // 计算阈值 649 | 650 | if (difference <= threshold) { 651 | result.add(array.get(i)); 652 | result.add(array.get(j)); 653 | } 654 | } 655 | } 656 | 657 | return result; 658 | } 659 | 660 | 661 | /** 662 | * 通过大小检测查找符合条件的list轮廓列表,并将符合的列表返回 663 | * @param {Array} list 轮廓列表 664 | * @returns 轮廓列表 665 | */ 666 | function getGraphSizePointList(list) { 667 | let points = new ArrayList(); 668 | for (let i = 0; i < list.size(); i++) { 669 | let matOfPoint = list.get(i); 670 | let ofPoint = judgeSize(matOfPoint, gWidth, gHeight, gAreaOffset); 671 | if (ofPoint != null) { 672 | points.add(ofPoint); 673 | } 674 | } 675 | console.log("通过缺口大小对比发现符合特征轮廓数量:%d个", points.size()); 676 | return points; 677 | } 678 | 679 | 680 | /** 681 | * 通过范围检测查找符合条件的list轮廓列表,并将符合的列表返回 682 | * @param {Array} list 轮廓列表 683 | * @param {int} leftX 图片最左边 684 | * @param {int} topY 图片最上边 685 | * @param {int} rightX 图片最右边 686 | * @param {int} bottomY 图片最下边 687 | * @returns 轮廓列表 688 | */ 689 | function getGraphRangePointList(list, leftX, topY, rightX, bottomY) { 690 | let points = new ArrayList(); 691 | for (let i = 0; i < list.size(); i++) { 692 | let matOfPoint = list.get(i); 693 | let maxY = getPoint(matOfPoint.toList(), 1); 694 | let maxX = getPoint(matOfPoint.toList(), 2); 695 | let minY = getPoint(matOfPoint.toList(), 3); 696 | let minX = getPoint(matOfPoint.toList(), 4); 697 | if (minX >= leftX && maxX <= rightX && minY >= topY && maxY <= bottomY) { 698 | points.add(matOfPoint); 699 | } 700 | } 701 | console.log("通过缺口位置范围对比发现符合特征轮廓数量:%d个", points.size()); 702 | if (points.size() == 0) { 703 | console.error("请检查坐标范围数组是否填写正确!"); 704 | } 705 | return points; 706 | } 707 | 708 | /** 709 | * 判断当前传入的轮廓是否符合指定大小,符合返回此轮廓,不符合返回null 710 | * @param {Object} point 单个轮廓 711 | * @param {int} width 检测轮廓宽度 712 | * @param {int} height 检测轮廓高度 713 | * @param {int} offset 检测轮廓大小相似度(0-20) 714 | * @returns 单个轮廓 715 | */ 716 | function judgeSize(point, width, height, offset) { 717 | let points = point.toList(); 718 | let maxY = 0; 719 | let maxX = 0; 720 | let minY = Integer.MAX_VALUE; 721 | let minX = Integer.MAX_VALUE; 722 | for (let i = 0; i < points.size(); i++) { 723 | let point1 = points.get(i); 724 | 725 | if (point1.y > maxY) { 726 | maxY = point1.y; 727 | } 728 | 729 | if (point1.x > maxX) { 730 | maxX = point1.x; 731 | } 732 | 733 | if (point1.y < minY) { 734 | minY = point1.y; 735 | } 736 | 737 | if (point1.x < minX) { 738 | minX = point1.x; 739 | } 740 | } 741 | 742 | let maxWidth = maxX - minX; 743 | let maxHeight = maxY - minY; 744 | 745 | if (maxWidth <= (width + offset) && maxWidth >= (width - offset) && maxHeight <= (height + offset) && maxHeight >= (height - offset)) { 746 | return point; 747 | } 748 | 749 | return null; 750 | } 751 | 752 | /** 753 | * 获取极限坐标值 754 | * @param list 坐标数组 755 | * @param type 获取坐标类型:1=最长y,2=最长x,3=最短y,4=最短x 756 | * @return 指定坐标值 757 | */ 758 | function getPoint(list, type) { 759 | let maxY = 0; 760 | let maxX = 0; 761 | let minY = Integer.MAX_VALUE; 762 | let minX = Integer.MAX_VALUE; 763 | for (let i = 0; i < list.size(); i++) { 764 | let point = list.get(i); 765 | 766 | if (type == 1 && point.y > maxY) { 767 | maxY = point.y; 768 | } 769 | 770 | if (type == 2 && point.x > maxX) { 771 | maxX = point.x; 772 | } 773 | 774 | if (type == 3 && point.y < minY) { 775 | minY = point.y; 776 | } 777 | 778 | if (type == 4 && point.x < minX) { 779 | minX = point.x; 780 | } 781 | } 782 | 783 | let result = -1; 784 | if (maxY > 0) { 785 | result = maxY; 786 | } else if (maxX > 0) { 787 | result = maxX; 788 | } else if (minY < Integer.MAX_VALUE) { 789 | result = minY; 790 | } else if (minX < Integer.MAX_VALUE) { 791 | result = minX; 792 | } 793 | 794 | return result; 795 | } 796 | 797 | /** 798 | * 获取第一波大致结果 799 | */ 800 | slidingBlock.getFirstSlideResult = function (img, rScale, rangObj, judgeType, colorObj, thresholdObj, sizeObj, shapeObj, ImagePath) { 801 | 802 | let list = getOutlineList2(img, rScale, rangObj, judgeType, colorObj, thresholdObj, sizeObj, shapeObj, ImagePath); 803 | return list; 804 | } 805 | 806 | /** 807 | * 获取结果 808 | */ 809 | slidingBlock.getSlideResult = function (img, rScale, rangObj, judgeType, colorObj, thresholdObj, sizeObj, shapeObj, ImagePath) { 810 | 811 | let list = getOutlineList3(img, rScale, rangObj, judgeType, colorObj, thresholdObj, sizeObj, shapeObj, ImagePath); 812 | return list; 813 | } 814 | 815 | 816 | 817 | /** 818 | * 仿人工滑动滑块 819 | * @param {int} x1 滑动初始x坐标 820 | * @param {int} y1 滑动初始y坐标 821 | * @param {int} x2 滑动目标x坐标 822 | * @param {int} y2 滑动目标y坐标 823 | * @param {int} x3 滑块图片的右坐标 824 | * @param {int} type 仿人工滑动类型 825 | * 0=机器滑动(均匀速度滑动) 826 | * 1=超出滑块缺口后慢慢返回直到对准缺口(默认) 827 | * 2=当快到达滑块缺口后慢慢滑动直到对准缺口 828 | * 3=四阶贝塞尔曲线方式滑动,四阶贝塞尔曲线百度地址:https://baike.baidu.com/item/%E8%B4%9D%E5%A1%9E%E5%B0%94%E6%9B%B2%E7%BA%BF 829 | */ 830 | slidingBlock.personSwipe = function (x1, y1, x2, y2, x3, type) { 831 | type = type == undefined || type == null ? 1 : type; 832 | gRightX = x3; 833 | var runSwipe = "swipe" + type + "(" + x1 + "," + y1 + "," + x2 + "," + y2 + ")" 834 | eval(runSwipe); 835 | } 836 | 837 | /** 838 | * 滑块拖动0:机器滑动(均匀速度滑动) 839 | * @param {int} x1 滑动初始x坐标 840 | * @param {int} y1 滑动初始y坐标 841 | * @param {int} x2 滑动目标x坐标 842 | * @param {int} y2 滑动目标y坐标 843 | */ 844 | function swipe0(x1, y1, x2, y2) { 845 | let stayTimes = (x2 - x1) * 5; 846 | swipe(x1, y1, x2, y2, stayTimes); 847 | } 848 | 849 | /** 850 | * 滑块拖动1:超出滑块缺口后慢慢返回直到对准缺口 851 | * @param {int} x1 滑动初始x坐标 852 | * @param {int} y1 滑动初始y坐标 853 | * @param {int} x2 滑动目标x坐标 854 | * @param {int} y2 滑动目标y坐标 855 | */ 856 | function swipe1(x1, y1, x2, y2) { 857 | let x4 = (x2 + 100) > gRightX ? gRightX : x2 + 100 858 | let times = (parseInt((x4 - x1) / 3) + 200 + 100 * 5) * 5 859 | console.log("滑动用时:"+times); 860 | let posArr = [0,times]; //滑动坐标数组 861 | 862 | for(let i=x1;i <=x4;i+=3){ 863 | posArr.push(pushPosArr(i,y2)) 864 | } 865 | 866 | let stayX = posArr[posArr.length-1][0]; 867 | for(let i = stayX;i >= (stayX-2);i-=0.01){ 868 | posArr.push([i,y2]); 869 | } 870 | 871 | x4 = x4 - 2; 872 | for(let i = x4; i >= x2; i-=0.2){ 873 | posArr.push(pushPosArr(i,y2)) 874 | } 875 | 876 | gestures(posArr); 877 | } 878 | 879 | 880 | /** 881 | * 滑块拖动2:当快到达滑块缺口后慢慢滑动直到对准缺口 882 | * @param {int} x1 滑动初始x坐标 883 | * @param {int} y1 滑动初始y坐标 884 | * @param {int} x2 滑动目标x坐标 885 | * @param {int} y2 滑动目标y坐标 886 | */ 887 | function swipe2(x1, y1, x2, y2) { 888 | 889 | let x4 = (x2 - 100) < x1 ? x1 : x2 - 100 890 | let times = (parseInt((x4 - x1) / 3) + 200 + (x2 - x4) * 5) * 5 891 | console.log("滑动用时:"+times); 892 | let posArr = [0,times]; //滑动坐标数组 893 | 894 | for(let i=x1;i <=x4;i+=3){ 895 | posArr.push(pushPosArr(i,y2)) 896 | } 897 | 898 | let stayX = posArr[posArr.length-1][0]; 899 | for(let i = stayX;i <= (stayX+2);i+=0.01){ 900 | posArr.push([i,y2]); 901 | } 902 | 903 | x4 = x4 + 2; 904 | for(let i = x4; i <= x2; i+=0.2){ 905 | posArr.push(pushPosArr(i,y2)) 906 | } 907 | 908 | gestures(posArr); 909 | } 910 | 911 | /** 912 | * 滑块拖动3:四阶贝塞尔曲线方式滑动 913 | * @param {int} x1 滑动初始x坐标 914 | * @param {int} y1 滑动初始y坐标 915 | * @param {int} x2 滑动目标x坐标 916 | * @param {int} y2 滑动目标y坐标 917 | */ 918 | function swipe3(x1, y1, x2, y2) { 919 | randomSwipe(x1, y1, x2, y2); 920 | } 921 | 922 | 923 | function bezierCreate(x1, y1, x2, y2, x3, y3, x4, y4) { 924 | //构建参数 925 | var h = 100; 926 | var cp = [{ x: x1, y: y1 + h }, { x: x2, y: y2 + h }, { x: x3, y: y3 + h }, { x: x4, y: y4 + h }]; 927 | var numberOfPoints = 100; 928 | var curve = []; 929 | var dt = 1.0 / (numberOfPoints - 1); 930 | //计算轨迹 931 | for (var i = 0; i < numberOfPoints; i++) { 932 | var ax, bx, cx; 933 | var ay, by, cy; 934 | var tSquared, tCubed; 935 | var result_x, result_y; 936 | cx = 3.0 * (cp[1].x - cp[0].x); 937 | bx = 3.0 * (cp[2].x - cp[1].x) - cx; 938 | ax = cp[3].x - cp[0].x - cx - bx; 939 | cy = 3.0 * (cp[1].y - cp[0].y); 940 | by = 3.0 * (cp[2].y - cp[1].y) - cy; 941 | ay = cp[3].y - cp[0].y - cy - by; 942 | var t = dt * i 943 | tSquared = t * t; 944 | tCubed = tSquared * t; 945 | result_x = (ax * tCubed) + (bx * tSquared) + (cx * t) + cp[0].x; 946 | result_y = (ay * tCubed) + (by * tSquared) + (cy * t) + cp[0].y; 947 | curve[i] = { 948 | x: result_x, 949 | y: result_y 950 | }; 951 | } 952 | 953 | //轨迹转路数组 954 | var array = []; 955 | for (var i = 0; i < curve.length; i++) { 956 | try { 957 | var j = (i < 100) ? i : (199 - i); 958 | xx = parseInt(curve[j].x) 959 | yy = parseInt(Math.abs(100 - curve[j].y)) 960 | } catch (e) { 961 | break 962 | } 963 | array.push([xx, yy]) 964 | } 965 | return array 966 | } 967 | 968 | 969 | 970 | /** 971 | * 把x和y坐标放入数组中 972 | * @param {int} x x坐标 973 | * @param {int} y y坐标 974 | */ 975 | function pushPosArr(x,y){ 976 | let y2 = randomNum(y-5, y+5); 977 | return [x,y2]; 978 | } 979 | 980 | /** 981 | * 生成随机数 982 | * @param {int} min 最小值 983 | * @param {int} max 最大值 984 | * @returns 随机数 985 | */ 986 | function randomNum(min, max) { 987 | // console.log(min,max); 988 | let r = Math.floor(Math.random() * (max - min + 1) + min); 989 | // console.log(r); 990 | return r; 991 | } 992 | 993 | 994 | /** 995 | * 真人模拟滑动函数 996 | * 997 | * 传入值:起点终点坐标 998 | * 效果:模拟真人滑动 999 | */ 1000 | function randomSwipe(sx, sy, ex, ey) { 1001 | //设置随机滑动时长范围 1002 | var timeMin = 2500 1003 | var timeMax = 3500 1004 | //设置控制点极限距离 1005 | var leaveHeightLength = 500 1006 | //根据偏差距离,应用不同的随机方式 1007 | if (Math.abs(ex - sx) > Math.abs(ey - sy)) { 1008 | var my = (sy + ey) / 2 1009 | var y2 = my + random(0, leaveHeightLength) 1010 | var y3 = my - random(0, leaveHeightLength) 1011 | var lx = (sx - ex) / 3 1012 | if (lx < 0) { lx = -lx } 1013 | var x2 = sx + lx / 2 + random(0, lx) 1014 | var x3 = sx + lx + lx / 2 + random(0, lx) 1015 | } else { 1016 | var mx = (sx + ex) / 2 1017 | var y2 = mx + random(0, leaveHeightLength) 1018 | var y3 = mx - random(0, leaveHeightLength) 1019 | 1020 | var ly = (sy - ey) / 3 1021 | if (ly < 0) { ly = -ly } 1022 | var y2 = sy + ly / 2 + random(0, ly) 1023 | var y3 = sy + ly + ly / 2 + random(0, ly) 1024 | } 1025 | 1026 | //获取运行轨迹,及参数 1027 | var time = [0, random(timeMin, timeMax)] 1028 | var track = bezierCreate(sx, sy, x2, y2, x3, y3, ex, ey) 1029 | console.log("滑动用时:"+time[1]); 1030 | 1031 | //滑动 1032 | let str = time.concat(track); 1033 | gestures(str) 1034 | } 1035 | 1036 | 1037 | 1038 | module.exports = slidingBlock;//回调 --------------------------------------------------------------------------------