Загрузка данных


/* Автоматический разгадыватель капчи из 4ёх искажённых картинок на форуме 2ch.su. Установи браузерное расширение по типу Tampermonkey ( https://chromewebstore.google.com/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo?hl=ru&pli=1 ) и скопируй скрипт туда. Для смартфона подойдёт Firefox, Kiwi Browser и др.

Код можно скормить нейронке и она подтвердит что он безопасен.

Если есть проблемы при установке или баги, пишите в Github или прямо в комментах к этому посту */

// ==UserScript==
// @name         2ch-autosolver
// @namespace    http://tampermonkey.net/
// @version      20.06.2026
// @description  См. выше
// @include      https://2ch.su/*
// @include      https://2ch.org/*
// @include      https://2ch.life/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=2ch.su
// @require      https://code.jquery.com/jquery-3.1.0.min.js
// @require      https://gist.githubusercontent.com/JackOutsideTheBox/ca3d4aec3e1464d26c2f3c19f88d03de/raw/6447d74eccbba9378ccdcc066f73017265c5f1d2/opencvwithsift.js
// @grant        none
// ==/UserScript==

/**
 * Captcha Autosolver - JavaScript implementation using OpenCV.js
 * Performs SIFT-based feature matching to identify candidate icons within captcha images
 */

(async () => {
    cv = (cv instanceof Promise) ? await cv : cv;

    class KpDesc {
        constructor() {
            this.keypoints;
            this.descriptors;
        }
    }

    class CandidateInfo {
        constructor(image) {
            this.image = image; // cv.Mat
            this.keypoints = new cv.KeyPointVector();
            this.descriptors = cv.Mat.zeros(this.image.rows, this.image.cols, cv.CV_32F);
        }
    }

    class CandidateResult {
        constructor(info, finalScore, combinedPoints, bestWindowPoints, approved) {
            this.info = info; //CandidateInfo
            this.finalScore = finalScore;
            this.combinedPoints = combinedPoints; // array of { x, y } points
            this.bestWindowPoints = bestWindowPoints; // array of { x, y } points
            this.approved = approved;
        }
    }

    class PointSet {
        constructor() {
            this.map = new Map();
        }

        add(point) {
            this.map.set(`${point.x},${point.y}`, point);
        }

        has(point) {
            return this.map.has(`${point.x},${point.y}`);
        }

        get size() {
            return this.map.size;
        }

        values() {
            return this.map.values();
        }

        [Symbol.iterator]() {
            return this.map.values();
        }
    }

    class CaptchaAutosolver {
        constructor() {
            this.candidateInfos = [];
            this.results = new Map(); //img -> CandidateResult
            this.originalCaptcha = null; // cv.Mat
            this.captchaVariants = [];

            // Algorithm parameters
            // Array of SIFT detectors with progressively more aggressive settings
            this.siftDetectors = [
                new cv.SIFT(0, 3, 0.04, 10, 1.2),     // default / conservative
                new cv.SIFT(0, 5, 0.03, 20, 1.2),     // slightly more keypoints
                new cv.SIFT(0, 10, 0.02, 30, 1.2),    // more aggressive
                new cv.SIFT(0, 15, 0.01, 40, 1.2)     // most aggressive
            ];
            this.captchaDetector = new cv.SIFT(0, 3, 0.04, 10, 1.6);

            this.MAX_SUBREGION_WIDTH = 60;
            this.MAX_SUBREGION_HEIGHT = 60;
        }

        /**
       * Clean up captcha variants and keypoint/descriptor data
       * @public
       */
        cleanup() {
            this.results.clear();

            for (const variant of this.captchaVariants) {
                variant.delete();
            }
            for (const kpd of this.captchaKpDesc) {
                kpd.keypoints.delete();
                kpd.descriptors.delete();
            }
            this.captchaVariants = [];
            this.captchaKpDesc = [];
        }

        /**
       * Convert HTMLImageElement to cv.Mat
       * @private
       * @param {HTMLImageElement} imgElement - HTML image element
       * @returns {cv.Mat} The image as cv.Mat
       */
        _imgToMat(imgElement) {
            const canvas = document.createElement('canvas');
            canvas.width = imgElement.width;
            canvas.height = imgElement.height;

            const ctx = canvas.getContext('2d');
            ctx.drawImage(imgElement, 0, 0);

            // Create cv.Mat from canvas (returns RGBA)
            const mat = cv.imread(canvas);

            this._opaqueToWhite(mat);
            return mat;
        }

        /**
       * Load captcha image from an HTML img element with data:image URI
       * @param {HTMLImageElement|string} imageSource - HTML img element or image element ID containing data:image
       */
        loadCaptcha(imageSource) {
            let imgElement;

            if (typeof imageSource === 'string') {
                imgElement = document.getElementById(imageSource);
                if (!imgElement) {
                    console.error(`Image element with ID '${imageSource}' not found`);
                    return;
                }
            } else {
                imgElement = imageSource;
            }

            if (!(imgElement instanceof HTMLImageElement)) {
                console.error("Source must be an HTMLImageElement or valid element ID");
                return;
            }

            // Convert img element to canvas, then to cv.Mat
            const captchaImage = this._imgToMat(imgElement);

            if (captchaImage.empty()) {
                console.error("Failed to convert image element to cv.Mat");
                return;
            }

            this._preprocessCaptcha(captchaImage);
            captchaImage.delete();
        }

        /**
       * Preprocess captcha image and prepare variants for matching
       * @private
       * @param {cv.Mat} captchaImage - The captcha image to preprocess
       */
        _preprocessCaptcha(captchaImage) {
            // Store original captcha for later use
            this.originalCaptcha = captchaImage.clone();

            // Prepare captcha variants: Lab channels and their inversions

            // Convert to Lab color space
            const reschemedCaptcha = new cv.Mat();
            cv.cvtColor(captchaImage, reschemedCaptcha, cv.COLOR_RGB2Lab);

            // Split Lab channels
            const channels = new cv.MatVector();
            cv.split(reschemedCaptcha, channels);

            // Apply CLAHE to each channel
            const clahe = new cv.CLAHE(5, new cv.Size(4, 4));

            for (let i = 0; i < 3; i++) {
                const channel = channels.get(i);
                const processed = new cv.Mat();
                clahe.apply(channel, processed);

                // Add channel and its inversion
                const inverted = new cv.Mat();
                cv.bitwise_not(processed, inverted);

                this.captchaVariants.push(processed);
                this.captchaVariants.push(inverted);
            }

            // Add grayscale variants
            const greyscaleCaptcha = new cv.Mat();
            cv.cvtColor(captchaImage, greyscaleCaptcha, cv.COLOR_BGR2GRAY);

            const equalCaptcha = new cv.Mat();
            cv.equalizeHist(greyscaleCaptcha, equalCaptcha);

            const claheCaptcha = new cv.Mat();
            clahe.apply(greyscaleCaptcha, claheCaptcha);

            const invGreyscale = new cv.Mat();
            cv.bitwise_not(greyscaleCaptcha, invGreyscale);

            const invEqualCaptcha = new cv.Mat();
            cv.bitwise_not(equalCaptcha, invEqualCaptcha);

            const invClaheCaptcha = new cv.Mat();
            cv.bitwise_not(claheCaptcha, invClaheCaptcha);

            this.captchaVariants.push(greyscaleCaptcha);
            this.captchaVariants.push(invGreyscale);

            this.captchaVariants.push(equalCaptcha);
            this.captchaVariants.push(invEqualCaptcha);

            this.captchaVariants.push(claheCaptcha);
            this.captchaVariants.push(invClaheCaptcha);

            // Extract keypoints and descriptors for all captcha variants
            const captchaKpDesc = [];
            for (let i = 0; i < this.captchaVariants.length; i++) {
                const kp = new KpDesc();
                kp.keypoints = new cv.KeyPointVector();
                kp.descriptors = cv.Mat.zeros(this.captchaVariants[i].rows, this.captchaVariants[i].cols, cv.CV_32F);

                this.captchaDetector.detectAndCompute(this.captchaVariants[i], new cv.Mat(), kp.keypoints, kp.descriptors);
                captchaKpDesc.push(kp);
            }

            this.captchaKpDesc = captchaKpDesc;

            // Cleanup temporary mats
            reschemedCaptcha.delete();
            channels.delete();
        }

        /**
       * Perform feature matching between candidate and captcha variants
       * Uses cached results if candidate has been matched before
       * @param {cv.Mat} img - The candidate icon
       * @param {Array} excludedWindows - Windows to exclude from matching
       * @returns {CandidateResult} Result containing match information and best window points
       */
        matchCandidate(img, excludedWindows = []) {
            const compensatePoorKpCount = (result) => {
                const keypointCount = result.info.keypoints.length;
                if (keypointCount <= -100) { //turn off
                    result.finalScore = Math.round((100.0 / keypointCount) * result.finalScore);
                }
            };

            // Check if we've already calculated matches for this candidate
            if (this.results.has(img.src)) {
                const result = this.results.get(img.src);
                const newBestWindow = this._findBestWindow(result.combinedPoints, excludedWindows);
                result.bestWindowPoints = newBestWindow;
                result.finalScore = newBestWindow.length;

                compensatePoorKpCount(result);
                return result;
            }

            let candidateImage = this._imgToMat(img);

            // Resize 2x using INTER_CUBIC interpolation
            let upscaledCandidateImage = new cv.Mat();
            let size = new cv.Size(candidateImage.cols * 2, candidateImage.rows * 2);
            cv.resize(candidateImage, upscaledCandidateImage, size, 0, 0, cv.INTER_CUBIC);

            const candidateInfo = new CandidateInfo(upscaledCandidateImage);

            // Try each SIFT detector with increasing aggressiveness
            for (let i = 0; i < this.siftDetectors.length; i++) {

                this.siftDetectors[i].detectAndCompute(candidateInfo.image, new cv.Mat(), candidateInfo.keypoints, candidateInfo.descriptors);

                if (candidateInfo.keypoints.size() >= 100) {
                    console.log(`Used SIFT instance ${i} for candidate ${img.src} with ${candidateInfo.keypoints.size()} keypoints.`);
                    break;
                }
            }

            if (candidateInfo.keypoints.size() < 100) {
                console.warn(`Even with maximum precision candidate ${img.src} only got ${candidateInfo.keypoints.size()} keypoints.`);
            }

            this.candidateInfos.push(candidateInfo);
            candidateImage.delete();

            const candidateKpts = candidateInfo.keypoints;
            const candidateDpts = candidateInfo.descriptors;
            const combinedPointsSet = new PointSet();

            for (let i = 0; i < this.captchaVariants.length; i++) {
                const captchaKpts = this.captchaKpDesc[i].keypoints;
                const captchaDpts = this.captchaKpDesc[i].descriptors;

                if (!captchaDpts || captchaDpts.empty() || !candidateDpts || candidateDpts.empty()) {
                    continue;
                }

                // Perform KNN matching with k=2
                const loweMatches = [];
                const matcher = new cv.BFMatcher(cv.NORM_L2, false);
                const knnMatches = new cv.DMatchVectorVector();

                matcher.knnMatch(candidateDpts, captchaDpts, knnMatches, 2);

                // Apply Lowe's ratio test
                for (let j = 0; j < knnMatches.size(); j++) {
                    const matches = knnMatches.get(j);
                    if (matches.size() === 2 && matches.get(0).distance < 0.8 * matches.get(1).distance) {
                        loweMatches.push(matches.get(0));
                    }
                }

                const uniquePoints = new PointSet();

                if (loweMatches.length >= 4) {
                    // Use RANSAC to estimate affine transformation
                    const candidatePts = [];
                    const captchaPts = [];

                    for (const m of loweMatches) {
                        candidatePts.push(candidateKpts.get(m.queryIdx).pt);
                        captchaPts.push(captchaKpts.get(m.trainIdx).pt);
                    }

                    // Convert to cv.Mat format for estimateAffineTransform
                    const srcMat = cv.matFromArray(candidatePts.length, 1, cv.CV_32FC2, candidatePts.flatMap(p => [p.x, p.y]));
                    const dstMat = cv.matFromArray(captchaPts.length, 1, cv.CV_32FC2, captchaPts.flatMap(p => [p.x, p.y]));

                    const inliersMask = new cv.Mat();
                    const affineMatrix = cv.estimateAffine2D(srcMat, dstMat, inliersMask, cv.RANSAC, 10.0);

                    // Collect inlier points
                    for (let j = 0; j < loweMatches.length; j++) {
                        if (inliersMask.ucharPtr(j)[0]) {
                            const point = captchaKpts.get(loweMatches[j].trainIdx).pt;
                            uniquePoints.add(point);
                        }
                    }

                    srcMat.delete();
                    dstMat.delete();
                    inliersMask.delete();
                    affineMatrix?.delete();
                } else {
                    // If fewer than 4 matches, collect all points
                    for (const m of loweMatches) {
                        const point = captchaKpts.get(m.trainIdx).pt;
                        uniquePoints.add(point);
                    }
                }

                // Filter applicable subregion and add to combined points
                if (uniquePoints.size > 0) {
                    const pointsArray = Array.from(uniquePoints);
                    const [isApplicable, filteredPoints] = this._isApplicableSubregion(pointsArray);

                    for (const pt of filteredPoints) {
                        combinedPointsSet.add(pt);
                    }
                }

                matcher.delete();
                knnMatches.delete();
            }

            // Convert combined points to array
            const combinedPoints = Array.from(combinedPointsSet);

            // Find best window without exclusions
            const bestWindowPoints = this._findBestWindow(combinedPoints, excludedWindows);

            const result = new CandidateResult(
                candidateInfo,
                bestWindowPoints.length,
                combinedPoints,
                bestWindowPoints,
                false
            );

            compensatePoorKpCount(result);

            this.results.set(img.src, result);

            return result;
        }

        /**
         * Check if a subregion is applicable (has at least 2 points in best window)
         * @private
         */
        _isApplicableSubregion(regionPoints) {
            if (regionPoints.length < 2) {
                return [false, []];
            }

            const bestWindowPoints = this._findBestWindow(regionPoints);

            if (bestWindowPoints.length >= 2) {
                return [true, bestWindowPoints];
            }

            return [false, []];
        }

        /**
         * Find best window for maximum point density
         * @private
         */
        _findBestWindow(combinedPoints, excludedWindows = []) {
            let filteredCombinedPoints = [];

            if (excludedWindows.length === 0) {
                filteredCombinedPoints = [...combinedPoints];
            } else {
                // Filter out points inside excluded rectangles
                for (const pt of combinedPoints) {
                    let excluded = false;
                    for (const r of excludedWindows) {
                        if (pt.x >= r.x && pt.x < r.x + r.width &&
                            pt.y >= r.y && pt.y < r.y + r.height) {
                            excluded = true;
                            break;
                        }
                    }
                    if (!excluded) {
                        filteredCombinedPoints.push(pt);
                    }
                }
            }

            if (filteredCombinedPoints.length === 0) {
                return [];
            }

            // Find bounding box of all points
            const ptsMat = cv.matFromArray(filteredCombinedPoints.length, 1, cv.CV_32FC2, filteredCombinedPoints.flatMap(p => [p.x, p.y]));
            const boundingBox = cv.boundingRect(ptsMat);
            ptsMat.delete();

            // Sliding window search
            let bestScore = 0;
            let bestWindow = new cv.Rect(0, 0, this.MAX_SUBREGION_WIDTH, this.MAX_SUBREGION_HEIGHT);

            const stepX = Math.floor(this.MAX_SUBREGION_WIDTH / 3);
            const stepY = Math.floor(this.MAX_SUBREGION_HEIGHT / 3);

            // Search within and beyond bounding box
            for (let x = boundingBox.x - this.MAX_SUBREGION_WIDTH; x < boundingBox.x + boundingBox.width; x += stepX) {
                for (let y = boundingBox.y - this.MAX_SUBREGION_HEIGHT; y < boundingBox.y + boundingBox.height; y += stepY) {
                    const currentWindow = new cv.Rect(x, y, this.MAX_SUBREGION_WIDTH, this.MAX_SUBREGION_HEIGHT);

                    // Count points within this window
                    let pointsInWindow = 0;
                    for (const pt of filteredCombinedPoints) {
                        if (pt.x >= currentWindow.x && pt.x < currentWindow.x + currentWindow.width &&
                            pt.y >= currentWindow.y && pt.y < currentWindow.y + currentWindow.height) {
                            pointsInWindow++;
                        }
                    }

                    // Update best window if this covers more points
                    if (pointsInWindow > bestScore) {
                        bestScore = pointsInWindow;
                        bestWindow = currentWindow;
                    }
                }
            }

            // Extract points covered by best window
            const bestWindowPoints = [];
            for (const pt of filteredCombinedPoints) {
                if (pt.x >= bestWindow.x && pt.x < bestWindow.x + bestWindow.width &&
                    pt.y >= bestWindow.y && pt.y < bestWindow.y + bestWindow.height) {
                    bestWindowPoints.push(pt);
                }
            }

            return bestWindowPoints;
        }

        /**
         * Convert semi-transparent pixels to white
         * @private
         */
        _opaqueToWhite(img) {
            const data = img.data;
            const rows = img.rows;
            const cols = img.cols;
            const channels = img.channels();

            if (channels === 4) {
                // RGBA format
                for (let i = 0; i < rows * cols * 4; i += 4) {
                    const alpha = data[i + 3];
                    if (alpha < 100) {
                        data[i] = 255;     // Blue
                        data[i + 1] = 255; // Green
                        data[i + 2] = 255; // Red
                        data[i + 3] = 255; // Alpha
                    }
                }
            }
        }
    }

    $('._captcha').prepend(`
<div class="captcha__autosolver-wrapper">
  <div class="captcha__autosolver">
    <div class="options__box">
      <input type="checkbox" js-target-autosolve id="autosolve">
      <label for="autosolve"><span title="Заработает со следующей капчи">Автоотгадывание капчи</span></label>
  	</div>
    <div class="options__box" style="visibility: hidden">
      <input type="checkbox" js-target-autoresend id="autoresend">
      <label for="autoresend"><span title="Отгадывать, пока не отправится">Автопереотправка при неудаче</span></label>
    </div>
	<div class="options__box" style="visibility: hidden">
      <input type="checkbox" js-target-hidecaptcha id="hidecaptcha">
      <label for="hidecaptcha"><span title="">Скрыть капчу</span></label>
    </div>
  </div>
</div>`).first().prepend(`
    <style id="autosolver-style">
      .captcha__autosolver-wrapper {
          margin-bottom: 20px;
      }
      .captcha__autosolver {
          margin: 10px auto -10px auto;
          width: fit-content;
          scale: 1.2;
          user-select: none;
      }
      .captcha__autosolver input:checked,
      .captcha__autosolver input:checked ~ label {
          font-weight: 700;
          color: var(--theme_default_link);
      }
	  .captcha__scanner {
          width: 5px;
          background-color: green;
          opacity: 0.9;
          position: absolute;
		  z-index: 2;
          box-shadow: 0em 0em 1em 6px green;
	  }

      .captcha__hidden {
          display: none !important;
      }
    </style>`);

    const captchaObserver = new MutationObserver(mutationsList => {
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList') {
                mutation.addedNodes.forEach(node => setTimeout(() => processNode(node), 100)); //workaround for firefox
            }
        }
    }
    );

    const responseObserver = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('alert')) {
                    const text = node.textContent.toLowerCase();

                    if (text.includes('успешно')) {
                        successCount++;
                        pendingResend = false;
                    } else if (text.includes('невалидна')) {
                        failCount++;
                        if (autoresend) {
                            pendingResend = true;
                            console.log('Pending resend flagged due to failed operation.');
                        }
                    }
                    showStats();
                    saveState();
                    // Persist updated config
                    console.log(`Success: ${successCount}, total: ${successCount + failCount}`);
                }
            }
            );
        }
        );
    }
    );

    function turnSolverOn() {

        captchaObserver.observe(document.querySelector('._captcha-content'), {
            childList: true,
            subtree: true
        });
        responseObserver.observe(document.getElementById('js-posts'), {
            childList: true,
            subtree: true
        });
    }

    function turnSolverOff() {
        captchaObserver.disconnect();
        responseObserver.disconnect();
        $('.captcha__scanner').remove();
    }

    function loadState() {
        const state = localStorage.getItem('autosolverOptions');
        if (state) {
            try {
                return JSON.parse(state);
            } catch (e) {
                console.warn('Failed to parse saved captcha options', e);
            }
        }
        return {
            autosolve: false,
            autoresend: false,
            hidecaptcha: false,
            successCount: 0,
            failCount: 0
        };
    }

    let { autosolve, autoresend, hidecaptcha, successCount, failCount } = loadState();
    successCount = parseInt(successCount);
    failCount = parseInt(failCount);
    let totalCount = () => successCount + failCount;
    let percentage = () => totalCount() === 0 ? 0 : Math.floor(successCount / totalCount() * 100);

    let pendingResend;
    let prevCaptcha = '';
    let solver;
    let chosenCandidates = [];

    // Refinement: iteratively exclude regions and recompute
    let excludedWindows = [];

	function statistics() {
        return `Авто-отгадано капч: ${successCount}/${totalCount()} (${percentage()}%)`;
    }

	function showStats() {
        $('#captcha-stats').text(statistics());
    }

    $('.header__opts').append(`<a id="captcha-stats-reset" class="header__menuitem" href="#" style="font-size: smaller; margin-left: auto;">[сброс]</a><div class="header__menuitem" id="captcha-stats">${statistics()}</div>`);

    function saveState() {
        localStorage.setItem('autosolverOptions', JSON.stringify({
            autosolve,
            autoresend,
            hidecaptcha,
            successCount,
            failCount
        }));
    }

    $('#captcha-stats-reset').on('click', function () {
		successCount = failCount = 0;
        showStats();
        saveState();
    });

    $('[id=autosolve]').on('change', function () {
        autosolve = $(this).prop('checked');
        const $additionals = $('[id=autoresend], [id=hidecaptcha]');
        if (autosolve) {
            $additionals.closest('.options__box').css('visibility', 'visible');
            turnSolverOn();
            processNode(document.querySelector('._captcha-content'), true);

        } else {
            turnSolverOff();
            $additionals.prop('checked', false).trigger('change'); //needs explicit call to react to checkbox state change
            $additionals.closest('.options__box').css('visibility', 'hidden');
        }
        saveState();
    });

    $('[id=autoresend]').on('change', function () {
        autoresend = $(this).prop('checked');
        saveState();
    });

    $('[id=hidecaptcha]').on('change', function () {
        hidecaptcha = $(this).prop('checked');
        if (hidecaptcha) {
            $('#autosolver-style').after('<style id="captcha-hider">._captcha-wrapper{display: none !important;}</style>');
        } else {
            $('[id=captcha-hider]').remove();
        }
        saveState();
    });

    if (autosolve)
        $('[id=autosolve]').click();
    if (autoresend)
        $('[id=autoresend]').click();
    if (hidecaptcha)
        $('[id=hidecaptcha]').click();

    // sync other instances
    $('.captcha__autosolver input').click(event => {
        const id = event.target.id;
        const isChecked = $(event.target).prop('checked');
        $(`input[id="${id}"]`).prop('checked', isChecked);
    }
    );

	window.addEventListener('storage', (event) => {
	  if (event.key === 'autosolverOptions') {
		({ autosolve, autoresend, hidecaptcha, successCount, failCount } = loadState());
		showStats();
		 console.log('Solver options synced from another tab');
	  }
	});

    function yieldToEventLoop() {
        return new Promise(resolve => setTimeout(resolve, 0));
    }


    //_captcha-keyboard-selected-icon
    async function processNode(node, force = false) {
        // Ensure it's an element
        if (node.nodeType === 1) {
            const captcha = node.querySelector('._captcha-image');
            const candidates = Array.from(node.querySelectorAll('._captcha-keyboard-button'));
            const success = node.querySelector('.captcha__msg-success-text');
            const error = node.querySelector('.captcha__msg-error');

            if (captcha && (captcha.src !== prevCaptcha || force)) {
                excludedWindows = [];

                $('.captcha__scanner').remove();

                console.log('New captcha image arrived:', captcha.src);
                chosenCandidates = Array.from(node.querySelectorAll('._captcha-keyboard-selected-icon')).map(chosen => chosen.src);

                // Iterate over each outmost container
                if (!hidecaptcha && candidates.length > 0) {
                    $('._captcha').each(async function () {
                        const $outmost = $(this);

                        const $img = $outmost.find('._captcha-image');

                        // Calculate the topOffset between captcha and outmost container
                        const captchaRect = $img[0].getBoundingClientRect();
                        const containerRect = $outmost[0].getBoundingClientRect();
                        const topOffset = Math.abs(captchaRect.top - containerRect.top);
                        const height = $img[0].height;

                        const scanner = document.createElement('div');
                        scanner.className = 'captcha__scanner';
                        scanner.style.top = `${topOffset}px`;
                        scanner.style.height = `${height}px`;

                        scanner.animate([{
                            transform: `translateX(${captchaRect.left - containerRect.left}px)`,
                            easing: "ease-in-out"
                        }, {
                            transform: `translateX(${captchaRect.right - containerRect.left}px)`,
                            easing: "ease-in-out"
                        }, {
                            transform: `translateX(${captchaRect.left - containerRect.left}px)`,
                            easing: "ease-in-out"
                        }], {
                            duration: 3000,
                            iterations: Infinity,
                        });
                        $outmost.prepend(scanner);
                        await yieldToEventLoop();

                    });
                }
                await yieldToEventLoop();

                solver?.cleanup();
                solver = new CaptchaAutosolver();
                solver.loadCaptcha(captcha);
                prevCaptcha = captcha.src;
            }

            await yieldToEventLoop();

            if (candidates.length > 0) {
                console.log('In process of solving captcha');

                // Calculate match counts and pick the best candidate
                let indexOfBest = -1;
                let bestCandidate = null;
                let bestResult = null;
                let bestScore = -1;

                for (let i = 0; i < candidates.length; i++) {
                    const img = candidates[i].querySelector('img');
                    const result = solver.matchCandidate(img, excludedWindows);
                    await yieldToEventLoop();

                    if (result.finalScore > bestScore) {
                        bestResult = result;
                        bestScore = result.finalScore;
                        bestCandidate = candidates[i];
                        indexOfBest = i;
                    }
                }

                bestResult.approved = true;

                // Create exclusion rectangle from best window points
                const ptsMat = cv.matFromArray(bestResult.bestWindowPoints.length, 1, cv.CV_32FC2, bestResult.bestWindowPoints.flatMap(p => [p.x, p.y]));
                const bb = cv.boundingRect(ptsMat);
                ptsMat.delete();
                excludedWindows.push(new cv.Rect(bb.x, bb.y, bb.width, bb.height));

                let bestCandidateData = bestCandidate.querySelector('img').src;
                chosenCandidates.push(bestCandidateData);
                console.log(`Clicking candidate ${indexOfBest} = ${bestCandidateData} with highest match count ${bestScore}`);
                bestCandidate.click();
            } else if (success && !force) {
                turnSolverOff();
                let chosenCandidatesIcons = `
<div class="_captcha-container">
	<div class="_captcha-wrapper">
		<div class="_captcha-keyboard-selected-list">
			${chosenCandidates.map(src => `
			<div class="_captcha-keyboard-selected-item" type="button">
				<img class="_captcha-keyboard-selected-icon" src="${src}">
			</div>`).join('')}
		</div>
		<div class="_captcha-image-container">
			<img js-target-image="" class="_captcha-image" src="${prevCaptcha}">
		</div>
	</div>
</div>`;
                $('._captcha-content').prepend(chosenCandidatesIcons);

                console.log('Solving finished');
                if (pendingResend) {
                    const submitId = document.getElementById('qr-postform').checkVisibility() ? 'qr-submit' : 'submit';
                    document.getElementById(submitId).click();
                }

                turnSolverOn();

            } else if (error) {
                $('.captcha__scanner').remove();
            }
        }
    }

})()