Last active
March 20, 2019 06:18
-
-
Save stackia/5e6fc0a3f1196042c1075109dd149825 to your computer and use it in GitHub Desktop.
giveaway.su 验证码辅助输入脚本
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name giveaway.su 验证码输入辅助 | |
// @namespace http://tampermonkey.net/ | |
// @version 0.2 | |
// @description 自动识别验证码+验证码输入键盘,必须安装 Liana 字体方可正常使用 | |
// @author Stackia | |
// @require https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js | |
// @match https://giveaway.su/giveaway/view/* | |
// @updateURL https://gist.githubusercontent.com/stackia/5e6fc0a3f1196042c1075109dd149825/raw/giveaway_su_captcha_helper.user.js | |
// @downloadURL https://gist.githubusercontent.com/stackia/5e6fc0a3f1196042c1075109dd149825/raw/giveaway_su_captcha_helper.user.js | |
// @supportURL https://steamcn.com/t279555-1-1 | |
// ==/UserScript== | |
(function () { | |
let text = 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя' | |
let textGroups = ['аийлмшя', 'бвъыь', 'гч', 'дзруф', 'еёосэю', 'жкх', 'нпт', 'цщ'] | |
let knownChars = [] | |
for (let i = 0; i < text.length; ++i) { | |
let charCanvas = document.createElement('canvas') | |
charCanvas.width = 60 | |
charCanvas.height = 60 | |
const charCtx = charCanvas.getContext('2d') | |
charCtx.fillStyle = 'white' | |
charCtx.fillRect(0, 0, charCanvas.width, charCanvas.height) | |
charCtx.fillStyle = 'black' | |
charCtx.font = '46px Liana' | |
charCtx.fillText(text[i], 30, 30) | |
let charImageData = charCtx.getImageData(0, 0, charCanvas.width, charCanvas.height) | |
let charBoundary = getImageBoundaryTrim(charImageData) | |
charCanvas.width = charBoundary.width | |
charCanvas.height = charBoundary.height | |
charCtx.putImageData(charImageData, -charBoundary.x, -charBoundary.y) | |
knownChars.push({ | |
char: text[i], | |
imageData: charCtx.getImageData(0, 0, charCanvas.width, charCanvas.height) | |
}) | |
} | |
function getPointGrey(imageData, x, y) { | |
let idx = (x + y * imageData.width) * 4 | |
return (x < 0 || x >= imageData.width || y < 0 || y >= imageData.height) ? 255 : imageData.data[idx] | |
} | |
function greyscale(imageData) { | |
const threshold = 220 | |
for (let x = 0; x < imageData.width; x++) { | |
for (let y = 0; y < imageData.height; y++) { | |
let idx = (x + y * imageData.width) * 4 | |
let r = imageData.data[idx] | |
let g = imageData.data[idx + 1] | |
let b = imageData.data[idx + 2] | |
let grey = 0.299 * r + 0.587 * g + 0.114 * b | |
grey = grey < threshold ? 0 : 255 | |
imageData.data[idx] = grey | |
imageData.data[idx + 1] = grey | |
imageData.data[idx + 2] = grey | |
imageData.data[idx + 3] = 255 | |
} | |
} | |
} | |
function scaleImageData(imageData, scale) { | |
let canvas1 = document.createElement('canvas') | |
canvas1.width = imageData.width | |
canvas1.height = imageData.height | |
canvas1.getContext('2d').putImageData(imageData, 0, 0) | |
let canvas2 = document.createElement('canvas') | |
canvas2.width = Math.floor(imageData.width * scale) | |
canvas2.height = Math.floor(imageData.height * scale) | |
let ctx = canvas2.getContext('2d') | |
ctx.scale(scale, scale) | |
ctx.drawImage(canvas1, 0, 0) | |
return ctx.getImageData(0, 0, canvas2.width, canvas2.height) | |
} | |
function getImageBoundaryTrim(imageData) { | |
let startX = imageData.width - 1, startY = imageData.height - 1, endX = 0, endY = 0 | |
for (let x = 0; x < imageData.width; x++) { | |
for (let y = 0; y < imageData.height; y++) { | |
let idx = (x + y * imageData.width) * 4 | |
let grey = imageData.data[idx] | |
if (grey === 255) continue | |
if (x < startX) startX = x | |
if (x > endX) endX = x | |
if (y < startY) startY = y | |
if (y > endY) endY = y | |
} | |
} | |
return { | |
x: startX, | |
y: startY, | |
width: endX - startX + 1, | |
height: endY - startY + 1 | |
} | |
} | |
function execute() { | |
let input = $('#form-captcha input') | |
$('#modal-captcha .modal-dialog').css('width', '574px') | |
$('img[data-captcha]').css('width', 'auto') | |
let detectCaptchaBtn = $('<button type="button" class="btn btn-sm btn-success">尝试识别</button>') | |
input.css({ | |
'font-family': 'Liana', | |
'font-size': '60px', | |
'text-align': 'center', | |
'min-height': '90px', | |
'padding-bottom': '26px' | |
}) | |
$('#form-captcha').append(detectCaptchaBtn) | |
let keyboard = $('<div></div>') | |
keyboard.css({ | |
'margin-top': '33px', | |
'font-family': 'Liana', | |
'display': 'flex', | |
'flex-wrap': 'wrap', | |
'align-items': 'center', | |
'justify-content': 'center' | |
}) | |
let btnTypes = ['info', 'success', 'warning', 'danger', 'default'] | |
for (let i = 0; i < textGroups.length; ++i) { | |
for (let j = 0; j < textGroups[i].length; ++j) { | |
let key = $(`<div class="btn btn-sm btn-${btnTypes[i % 5]}">${textGroups[i][j]}</div>`) | |
key.css({ | |
'width': '60px', | |
'height': '60px', | |
'line-height': '52px', | |
'font-size': '46px', | |
'margin': '8px' | |
}) | |
key.click(() => { | |
let caretPos = input[0].selectionStart | |
let currentText = input.val() | |
input.val(currentText.substring(0, caretPos) + textGroups[i][j] + currentText.substring(caretPos)) | |
input[0].selectionStart = caretPos + 1 | |
}) | |
keyboard.append(key) | |
} | |
} | |
$('#form-captcha').append(keyboard) | |
detectCaptchaBtn.click(() => { | |
detectCaptchaBtn.html('<i class="fa fa-cog fa-spin"></i> 识别中') | |
window.setTimeout(() => { | |
let canvas = document.createElement('canvas') | |
let ctx = canvas.getContext('2d') | |
let captchaImg = $('img[data-captcha]')[0] | |
canvas.width = captchaImg.width | |
canvas.height = captchaImg.height | |
ctx.fillStyle = 'white' | |
ctx.fillRect(0, 0, canvas.width, canvas.height) | |
ctx.drawImage(captchaImg, 0, 0) | |
let imageData = ctx.getImageData(1, 1, canvas.width - 2, canvas.height - 2) // 去黑边 | |
// 灰度化 二值化 | |
greyscale(imageData) | |
// 裁切 | |
let boundary = getImageBoundaryTrim(imageData) | |
canvas.width = boundary.width | |
canvas.height = boundary.height | |
ctx.putImageData(imageData, -boundary.x, -boundary.y) | |
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) | |
// 投影统计 | |
let stats = new Array(imageData.height).fill(0) | |
for (let y = 0; y < imageData.height; y++) { | |
for (let x = 0; x < imageData.width; x++) { | |
let idx = (x + y * imageData.width) * 4 | |
stats[y] += imageData.data[idx] === 255 ? 0 : 1 | |
} | |
} | |
// 曲线平滑 | |
for (let y = 0; y < stats.length; y++) { | |
let previous = y - 1 < 0 ? 0 : stats[y - 1] | |
let next = y + 1 >= stats.length ? 0 : stats[y + 1] | |
stats[y] = (previous + next + stats[y]) / 3 | |
} | |
// 确认基线 | |
let midTop = 0 | |
let midBottom = imageData.height | |
const statsThreshold = 15 | |
for (let y = 0; y < stats.length; y++) { | |
if (stats[y] < statsThreshold) continue | |
midTop = y | |
break | |
} | |
for (let y = stats.length - 1; y >= 0; y--) { | |
if (stats[y] < statsThreshold) continue | |
midBottom = y | |
break | |
} | |
// 重合度检测 | |
let firstResult = [] | |
const overlapThreshold = 0.85 | |
for (let k = 0; k < knownChars.length; ++k) { | |
let testChar = knownChars[k] | |
let scaledImageData = scaleImageData(testChar.imageData, (midBottom - midTop) / 12) | |
// let scaledImageData = testChar.imageData | |
greyscale(scaledImageData) | |
let scaledImageHistogramX = new Array(scaledImageData.width).fill(0) | |
let scaledImageHistogramY = new Array(scaledImageData.height).fill(0) | |
let scaledImageCenter = { x: 0, y: 0 } | |
let scaledImageTotalGrey = 0 | |
for (let x = 0; x < scaledImageData.width; ++x) { | |
for (let y = 0; y < scaledImageData.height; ++y) { | |
let grey = getPointGrey(scaledImageData, x, y) | |
if (grey === 0) { | |
scaledImageHistogramX[x]++ | |
scaledImageHistogramY[y]++ | |
} | |
scaledImageTotalGrey += grey | |
scaledImageCenter.x += x * grey | |
scaledImageCenter.y += y * grey | |
} | |
} | |
scaledImageCenter.x = scaledImageCenter.x / scaledImageTotalGrey | |
scaledImageCenter.y = scaledImageCenter.y / scaledImageTotalGrey | |
let scaledImageDiagonal = Math.sqrt(scaledImageData.width * scaledImageData.width + scaledImageData.height * scaledImageData.height) | |
let possibilities = [] | |
for (let left = 0; left < imageData.width - scaledImageData.width; ++left) { | |
for (let top = 0; top < imageData.height - scaledImageData.height; ++top) { | |
let count = 0 | |
let histogramX = new Array(scaledImageData.width).fill(0) | |
let histogramY = new Array(scaledImageData.height).fill(0) | |
let center = { x: 0, y: 0 } | |
let totalGrey = 0 | |
for (let x = left; x < left + scaledImageData.width; ++x) { | |
for (let y = top; y < top + scaledImageData.height; ++y) { | |
let grey = getPointGrey(imageData, x, y) | |
if (grey === getPointGrey(scaledImageData, x - left, y - top)) count++ | |
if (grey === 0) { | |
histogramX[x - left]++ | |
histogramY[y - top]++ | |
} | |
totalGrey += grey | |
center.x += (x - left) * grey | |
center.y += (y - top) * grey | |
} | |
} | |
let distenceX = 0, distenceY = 0 | |
for (let x = 0; x < histogramX.length; ++x) { | |
distenceX += (histogramX[x] - scaledImageHistogramX[x]) * (histogramX[x] - scaledImageHistogramX[x]) | |
} | |
for (let y = 0; y < histogramY.length; ++y) { | |
distenceY += (histogramY[y] - scaledImageHistogramY[y]) * (histogramY[y] - scaledImageHistogramY[y]) | |
} | |
center.x = center.x / totalGrey | |
center.y = center.y / totalGrey | |
let o1 = count / (scaledImageData.width * scaledImageData.height) | |
let o2 = 1 - Math.sqrt(distenceX) / Math.sqrt(((scaledImageData.height * scaledImageData.height) * histogramX.length)) | |
let o3 = 1 - Math.sqrt(distenceY) / Math.sqrt(((scaledImageData.width * scaledImageData.width) * histogramY.length)) | |
let distenceCenter = Math.sqrt((center.x - scaledImageCenter.x) * (center.x - scaledImageCenter.x) + (center.y - scaledImageCenter.y) * (center.y - scaledImageCenter.y)) | |
let o4 = 1 - distenceCenter / (scaledImageDiagonal / 2) | |
possibilities.push({ | |
overlap: 0.55 * o1 + 0.2 * o2 + 0.2 * o3 + 0.05 * o4, | |
x: left, | |
y: top, | |
position: left + scaledImageData.width, | |
width: scaledImageData.width, | |
height: scaledImageData.height, | |
char: testChar.char | |
}) | |
} | |
} | |
possibilities.sort((a, b) => b.overlap - a.overlap) | |
for (let i = 0; i < possibilities.length; ++i) { | |
if (possibilities[i].overlap < overlapThreshold) break | |
if (possibilities[i].y > imageData.height / 2) continue | |
firstResult.push(possibilities[i]) | |
} | |
} | |
firstResult.sort((a, b) => a.position - b.position) | |
let secondResult = [], lastX = -3 | |
const minCharWidth = 4 | |
for (let i = 0; i < firstResult.length; ++i) { | |
if (secondResult.length === 0 || firstResult[i].position - lastX > minCharWidth) { | |
secondResult.push([firstResult[i]]) | |
} else { | |
secondResult[secondResult.length - 1].push(firstResult[i]) | |
} | |
lastX = firstResult[i].position | |
} | |
let finalResult = [] | |
for (let i = 0; i < secondResult.length; ++i) { | |
secondResult[i].sort((a, b) => b.overlap - a.overlap) | |
finalResult.push(secondResult[i][0]) | |
} | |
input.val(finalResult.map(r => r.char).join('')) | |
detectCaptchaBtn.text('尝试识别') | |
}, 300) | |
}) | |
} | |
let intervalHandler = window.setInterval(() => { | |
if ($('#form-captcha')[0]) { | |
window.clearInterval(intervalHandler) | |
execute() | |
} | |
}, 500) | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment