|
|
@ -124,8 +124,143 @@ function calculateDisorder(imgdata: ImageData) { |
|
|
|
return res / (total === 0 ? 1 : total); |
|
|
|
} |
|
|
|
|
|
|
|
/* |
|
|
|
* decide if a pixel is closer to black than to white. |
|
|
|
* return 0 for white, 1 for black |
|
|
|
*/ |
|
|
|
function pxlBlackOrWhite(r: number, g: number, b: number) { |
|
|
|
return (r + g + b > 384) ? 0 : 1; |
|
|
|
} |
|
|
|
|
|
|
|
/* |
|
|
|
* Get bordering pixels of transparent areas (the outline of the circles) |
|
|
|
* and return their coordinates with the neighboring color. |
|
|
|
*/ |
|
|
|
function getBoundries(imgdata: ImageData) { |
|
|
|
const data = imgdata.data; |
|
|
|
const width = imgdata.width; |
|
|
|
|
|
|
|
let i = data.length - 1; |
|
|
|
let cl = 0; |
|
|
|
let cr = 0; |
|
|
|
const chkArray = []; |
|
|
|
let opq = true; |
|
|
|
while (i > 0) { |
|
|
|
// alpha channel above 128 is assumed opaque
|
|
|
|
const a = data[i] > 128; |
|
|
|
if (a !== opq) { |
|
|
|
if ((data[i - 4] > 128) === opq) { |
|
|
|
// ignore just 1-width areas
|
|
|
|
i -= 4; |
|
|
|
continue; |
|
|
|
} |
|
|
|
if (a) { |
|
|
|
/* transparent pixel to its right */ |
|
|
|
/* |
|
|
|
// set to color blue (for debugging)
|
|
|
|
data[i + 4] = 255; |
|
|
|
data[i + 3] = 255; |
|
|
|
data[i + 2] = 0; |
|
|
|
data[i + 1] = 0; |
|
|
|
*/ |
|
|
|
const pos = (i + 1) / 4; |
|
|
|
const x = pos % width; |
|
|
|
const y = (pos - x) / width; |
|
|
|
// 1: black, 0: white
|
|
|
|
const clr = pxlBlackOrWhite(data[i - 1], data[i - 2], data[i - 3]); |
|
|
|
chkArray.push([x, y, clr]); |
|
|
|
cr += 1; |
|
|
|
} else { |
|
|
|
/* opaque pixel to its right */ |
|
|
|
/* |
|
|
|
// set to color red (for debugging)
|
|
|
|
data[i] = 255; |
|
|
|
data[i - 1] = 0; |
|
|
|
data[i - 2] = 0; |
|
|
|
data[i - 3] = 255; |
|
|
|
*/ |
|
|
|
const pos = (i - 3) / 4; |
|
|
|
const x = pos % width; |
|
|
|
const y = (pos - x) / width; |
|
|
|
// 1: black, 0: white
|
|
|
|
const clr = pxlBlackOrWhite(data[i + 1], data[i + 2], data[i + 3]); |
|
|
|
chkArray.push([x, y, clr]); |
|
|
|
cl += 1; |
|
|
|
} |
|
|
|
opq = a; |
|
|
|
} |
|
|
|
i -= 4; |
|
|
|
} |
|
|
|
return chkArray; |
|
|
|
} |
|
|
|
/* |
|
|
|
* slide the background image and compare the colors of the border pixels in |
|
|
|
* chkArray, the position with the most matches wins |
|
|
|
* Return in slider-percentage. |
|
|
|
*/ |
|
|
|
function getBestPos(bgdata: ImageData, chkArray: number[][], slideWidth: number) { |
|
|
|
const data = bgdata.data; |
|
|
|
const width = bgdata.width; |
|
|
|
let bestSimilarity = 0; |
|
|
|
let bestPos = 0; |
|
|
|
|
|
|
|
for (let s = 0; s <= slideWidth; s += 1) { |
|
|
|
let similarity = 0; |
|
|
|
const amount = chkArray.length; |
|
|
|
for (let p = 0; p < amount; p += 1) { |
|
|
|
const chk = chkArray[p]; |
|
|
|
const x = chk[0] + s; |
|
|
|
const y = chk[1]; |
|
|
|
const clr = chk[2]; |
|
|
|
const off = (y * width + x) * 4; |
|
|
|
const bgclr = pxlBlackOrWhite(data[off], data[off + 1], data[off + 2]); |
|
|
|
if (bgclr === clr) { |
|
|
|
similarity += 1; |
|
|
|
} |
|
|
|
} |
|
|
|
if (similarity > bestSimilarity) { |
|
|
|
bestSimilarity = similarity; |
|
|
|
bestPos = s; |
|
|
|
} |
|
|
|
} |
|
|
|
return bestPos / slideWidth * 100; |
|
|
|
} |
|
|
|
|
|
|
|
async function getImageDataFromURI(uri: string) { |
|
|
|
const image = await imageFromUri(uri); |
|
|
|
if (!image) |
|
|
|
throw new Error("No image"); |
|
|
|
const canvas = document.createElement('canvas'); |
|
|
|
canvas.width = image.width; |
|
|
|
canvas.height = image.height; |
|
|
|
const ctx = canvas.getContext('2d')!; |
|
|
|
ctx.drawImage(image, 0, 0); |
|
|
|
return ctx.getImageData(0, 0, canvas.width, canvas.height); |
|
|
|
} |
|
|
|
|
|
|
|
async function slideCaptcha(tfgElement: HTMLElement, tbgElement: HTMLElement, sliderElement: HTMLInputElement) { |
|
|
|
// get data uris for captcha back- and foreground
|
|
|
|
const tbgUri = tbgElement.style.backgroundImage.slice(5, -2); |
|
|
|
const tfgUri = tfgElement.style.backgroundImage.slice(5, -2); |
|
|
|
|
|
|
|
// load foreground (image with holes)
|
|
|
|
const igd = await getImageDataFromURI(tfgUri); |
|
|
|
// get array with pixels of foreground
|
|
|
|
// that we compare to background
|
|
|
|
const chkArray = getBoundries(igd); |
|
|
|
// load background (image that gets slid)
|
|
|
|
const sigd = await getImageDataFromURI(tbgUri); |
|
|
|
const slideWidth = sigd.width - igd.width; |
|
|
|
// slide, compare and get best matching position
|
|
|
|
const sliderPos = getBestPos(sigd, chkArray, slideWidth); |
|
|
|
// slide in the UI
|
|
|
|
sliderElement.value = '' + sliderPos; |
|
|
|
(sliderElement as any).dispatchEvent(new Event('input'), { bubbles: true }); |
|
|
|
return 0 - (sliderPos / 2); |
|
|
|
} |
|
|
|
|
|
|
|
// returns ImageData from captcha's background image, foreground image, and offset (ranging from 0 to -50)
|
|
|
|
function imageFromCanvas(img: HTMLImageElement, bg: HTMLImageElement, off: number) { |
|
|
|
async function imageFromCanvas(img: HTMLImageElement, bg: HTMLImageElement, off: number | null) { |
|
|
|
const h = img.height; |
|
|
|
const w = img.width; |
|
|
|
const th = 80; |
|
|
@ -163,44 +298,11 @@ function imageFromCanvas(img: HTMLImageElement, bg: HTMLImageElement, off: numbe |
|
|
|
ctx.drawImage(img, -w / 2, -h / 2, w, h); |
|
|
|
}; |
|
|
|
|
|
|
|
// if off is not specified and background image is present, try to figure out
|
|
|
|
// the best offset automatically; select the offset that has smallest value of
|
|
|
|
// calculateDisorder for the resulting image
|
|
|
|
if (bg && off == null) { |
|
|
|
let bestDisorder = 999; |
|
|
|
let bestImagedata: ImageData | null = null; |
|
|
|
let bestOff = -1; |
|
|
|
|
|
|
|
for (let off = 0; off >= -50; off--) { |
|
|
|
draw(off); |
|
|
|
|
|
|
|
let imgdata = ctx.getImageData(0, 0, canvas.width, canvas.height); |
|
|
|
const disorder = calculateDisorder(imgdata); |
|
|
|
|
|
|
|
if (disorder < bestDisorder) { |
|
|
|
bestDisorder = disorder; |
|
|
|
draw(off); |
|
|
|
imgdata = ctx.getImageData(0, 0, canvas.width, canvas.height); |
|
|
|
bestImagedata = imgdata; |
|
|
|
bestOff = off; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// not the best idea to do this here
|
|
|
|
setTimeout(function () { |
|
|
|
const bg = document.getElementById('t-bg'); |
|
|
|
const slider = document.getElementById('t-slider') as HTMLInputElement; |
|
|
|
if (!bg || !slider) return; |
|
|
|
|
|
|
|
slider.value = '' + (-bestOff * 2); |
|
|
|
bg.style.backgroundPositionX = bestOff + 'px'; |
|
|
|
}, 1); |
|
|
|
draw(bestOff); |
|
|
|
return bestImagedata; |
|
|
|
} else { |
|
|
|
draw(off); |
|
|
|
return ctx.getImageData(0, 0, canvas.width, canvas.height); |
|
|
|
off = await slideCaptcha(document.getElementById('t-fg')!, document.getElementById('t-bg')!, document.getElementById('t-slider') as HTMLInputElement); |
|
|
|
} |
|
|
|
draw(off || 0); |
|
|
|
return ctx.getImageData(0, 0, canvas.width, canvas.height); |
|
|
|
} |
|
|
|
|
|
|
|
// for debugging purposes
|
|
|
@ -306,7 +408,7 @@ async function predict(img: HTMLImageElement, bg: HTMLImageElement, off: number) |
|
|
|
if (!model) { |
|
|
|
model = await load(); |
|
|
|
} |
|
|
|
const image = imageFromCanvas(img, bg, off); |
|
|
|
const image = await imageFromCanvas(img, bg, off); |
|
|
|
if (!image) |
|
|
|
throw new Error("Failed to gen image"); |
|
|
|
const mono = toMonochrome(image.data); |
|
|
|