Python + OpenCV + Selenium: проблема при последовательных действиях "найти и кликнуть"

python
webdriver
opencv
Теги: #<Tag:0x00007f7b693ee748> #<Tag:0x00007f7b693ee5e0> #<Tag:0x00007f7b693ee478>

(Алексей Щербин) #1

Всем привет.
Была задача написать небольшой тест для проверки работоспособности canvas элемента на странице. Как-бы смоук тест на то, не отвалилась ли БД или еще какой из элементов. Для мониторинга. Решил использовать Python, Selenium Webdriver, OpenCV. Нашел статью, где популярно разжевано: https://www.linkedin.com/pulse/html-canvas-testing-selenium-opencv-maciej-kusz/

Все сделал, настроил. Вроде работает. Но столкнулся с проблемой и не знаю как решить.
Проблема: если после нахождения элемента на странице и клика по нему доабвить поиск другого элемента и клика уже по нему, то второй клик не отрабатывает (метод вроде отрабатывает, то глазами вижу что действия, которое должно быть после клика - нет).

Модуль поисковика элемента:
graphical_locator.py

import cv2
import numpy
from io import BytesIO
from PIL import Image


class GraphicalLocator(object):

    def __init__(self, img_path):
        self.locator = img_path
        # x, y position in pixels counting from left, top corner
        self.x = None
        self.y = None
        self.img = cv2.imread(img_path)
        self.width = self.img.shape[1]
        self.height = self.img.shape[0]
        self.threshold = None

    @property
    def center_x(self):
        if self.x and self.width:
            return self.x + int(self.width / 2)
        else:
            None

    @property
    def center_y(self):
        if self.y and self.height:
            return self.y + int(self.height / 2)
        else:
            None

    # Clear last found coordinates
    def find_me(self, drv):
        self.x = self.y = None
        # Get current screenshot of a web page
        scr = drv.get_screenshot_as_png()
        # Convert img to BytesIO
        scr = Image.open(BytesIO(scr))
        # Convert to format accepted by OpenCV
        scr = numpy.asarray(
            scr,
            dtype=numpy.float32
        ).astype(numpy.uint8)
        # Convert image from BGR to RGB format
        scr = cv2.cvtColor(scr, cv2.COLOR_BGR2RGB)

        # Image matching works only on gray images
        # (color conversion from RGB/BGR to GRAY scale)
        img_match = cv2.minMaxLoc(
            cv2.matchTemplate(
                cv2.cvtColor(
                    scr,
                    cv2.COLOR_RGB2GRAY
                ),
                cv2.cvtColor(
                    self.img,
                    cv2.COLOR_BGR2GRAY
                ),
                cv2.TM_CCOEFF_NORMED
            )
        )

        # Calculate position of found element
        self.x = img_match[3][0]
        self.y = img_match[3][1]

        # From full screenshot crop part that matches template image
        scr_crop = scr[
            self.y: (self.y + self.height),
            self.x: (self.x + self.width)
        ]

        # Calculate colors histogram of both template# and matching images and
        # compare them
        scr_hist = cv2.calcHist(
            [scr_crop],
            [0, 1, 2],
            None,
            [8, 8, 8],
            [0, 256, 0, 256, 0, 256]
        )
        img_hist = cv2.calcHist(
            [self.img],
            [0, 1, 2],
            None,
            [8, 8, 8],
            [0, 256, 0, 256, 0, 256]
        )
        comp_hist = cv2.compareHist(
            img_hist,
            scr_hist,
            cv2.HISTCMP_CORREL
        )

        # Save treshold matches of: graphical image and image histogram
        self.threshold = {
            'shape': round(img_match[1], 2),
            'histogram': round(comp_hist, 2)
        }

        # Return image with blue rectangle around match
        return cv2.rectangle(
            scr,
            (self.x, self.y), 
            (self.x + self.width, self.y + self.height),
            (0, 0, 255),
            2
        )

Модуль проверки:
check.py

import time
import os
from selenium.webdriver.common.action_chains import ActionChains
from .graphical_locator import GraphicalLocator


def check(driver):
    def click(path, shape, histogram):
        button = GraphicalLocator(
            os.path.abspath(path))
        button.find_me(driver)

        is_found = False

        print("\nSHAPE: ", path, button.threshold['shape'])
        print("\nHISTOGRAM: ", path, button.threshold['histogram'])

        if button.threshold['shape'] >= shape and \
                button.threshold['histogram'] >= histogram:
            is_found = True

        if is_found:
            action = ActionChains(driver)
            action.move_by_offset(button.center_x, button.center_y)
            action.click()
            action.perform()
            print("\nALREADY CLICKED")
            time.sleep(2)

            return True

        return False

    if click(
        path="checks/elements/button_1.png",
        shape=0.56,
        histogram=0.97,
    ):
            return click(
                path="checks/elements/button_2.png",
                shape=0.56,
                histogram=0.53,
            )

В итоге, первый клик проходит, это видно, а второй по коду проходит (click() возвращает True), но по факту нет. Кто с таким сталкивался? Куда копать?
Думал проблема в не очистке координат, но нет. Я же использую новый инстанс ActionChains. Как и GraphicalLocator.

python==3.5.2
selenium==3.14.0
chromedriver==2.45
opencv-python==3.4.4.19
numpy==1.15.4
Pillow==5.3.0

chrome==71.0.3578.98


(Алексей Щербин) #2

В общем, если кому вдруг интересно, я решил эту проблему. Дело в том, что метод move_by_offset() отсчитывает задаваемые координаты не с (0,0), а с последнего клика. Похоже это даже на уровне webdriver’а, так как я создаю для каждого клика отдельный инстанс.
Вот решение:

import time
import os
from selenium.webdriver.common.action_chains import ActionChains
from .graphical_locator import GraphicalLocator


def click(
        driver,
        path,
        shape,
        histogram,
        prev_center_x=0,
        prev_center_y=0,
):
    button = GraphicalLocator(
        os.path.abspath(path))
    button.find_me(driver)

    is_found = False

    print("\nSHAPE: ", path, button.threshold['shape'])
    print("\nHISTOGRAM: ", path, button.threshold['histogram'])

    if button.threshold['shape'] >= shape and \
            button.threshold['histogram'] >= histogram:
        is_found = True

    if is_found:
        action = ActionChains(driver)
        # print("\nCENTER X: ", button.center_x)
        # print("\nCENTER Y: ", button.center_y)
        action.move_by_offset(
            button.center_x - prev_center_x,
            button.center_y - prev_center_y,
        )
        action.click()
        action.perform()
        print("\nALREADY CLICKED")
        time.sleep(2)

        return True, button.center_x, button.center_y

    return False, 0, 0


clicked = click(
        driver=driver,
        path="checks/elements/button_1.png",
        shape=0.56,
        histogram=0.97,
    )
    if clicked[0]:
        return click(
            driver=driver,
            path="checks/elements/button_2.png",
            prev_center_x=clicked[1],
            prev_center_y=clicked[2],
            shape=0.56,
            histogram=0.53,
        )[0]