Aller au contenu

Pipeline de vision

Cette page s’adresse à celles et ceux qui veulent comprendre pourquoi OculiX trouve — ou rate — un match. En une phrase : derrière chaque find() il y a matchTemplate d’OpenCV, doublé d’un repli en feature matching, le tout enveloppé dans une couche JNA appelée Apertix qui évite les conflits classiques de bibliothèques natives sous Java.

flowchart TD
    A["Votre script<br/>(Jython / Java)"] --> B["sikuli.script<br/>Screen · Region · Pattern"]
    B --> C["org.sikuli.script.Finder<br/>similarité · target offset · clipping de region"]
    C --> D["Apertix<br/>OpenCV 4.10.0 via JNA"]
    D --> E["Libs OpenCV natives<br/>embarquées dans le JAR"]
    style E fill:#fff3cd,stroke:#ffc107,color:#856404

OculiX dépend d’Apertix, une compilation JNA personnalisée d’OpenCV 4.10.0, à la place de l’artefact plus connu org.openpnp:opencv. Deux raisons :

Plus de conflit avec System.loadLibrary

Apertix charge OpenCV via JNA, qui n’entre pas en concurrence avec les autres bibliothèques JNI sous Windows. Le mélange OpenCV + VNC + JFreeChart, qui pose problème depuis dix ans avec l’artefact classique, fonctionne sans patch.

Une version d'OpenCV figée

OpenCV 4.10.0, compilé depuis les sources sous Windows x86-64 avec MSVC. Chaque release d’OculiX est buildée contre cette même version exacte. Comportement reproductible d’une machine à l’autre.

Coordonnées Maven :

<dependency>
<groupId>io.github.julienmerconsulting.apertix</groupId>
<artifactId>opencv</artifactId>
<version>4.10.0-0</version>
</dependency>

Le dépôt vit ici : github.com/julienmerconsulting/Apertix.

Quand vous écrivez Region.find("button.png"), OculiX appelle la fonction matchTemplate d’OpenCV avec la métrique TM_CCOEFF_NORMED. Concrètement :

  1. L’image button.png capturée devient le template.
  2. La capture actuelle de la region devient la scène.
  3. OpenCV fait glisser le template sur chaque pixel de la scène et calcule un score de corrélation normalisé entre 0,0 et 1,0.
  4. Le pixel au score le plus élevé devient le match candidat.
  5. Si ce score atteint la similarité demandée (0,7 par défaut), OculiX renvoie un Match. Sinon il lève FindFailed.

Le template matching est précis au pixel près, mais sensible à l’échelle. Si la même cible est rendue 10 % plus grande sur l’écran d’exécution — High DPI, changement de thème — le score chute. Deux façons d’absorber ça :

Abaisser le seuil de similarité

Pattern("button.png").similar(0.6) élargit la tolérance pour un seul appel sans toucher au reste du script.

Recapturer à la bonne échelle

Plus simple, plus fiable, plus rapide quand la cible est très déformée par rapport à la capture d’origine.

Feature matching — quand le template ne suffit plus

Section intitulée « Feature matching — quand le template ne suffit plus »

Si le template matching échoue trop souvent — rotation, changement d’échelle, variations de lumière — OculiX bascule sur du feature matching :

finder = Finder(image)
finder.findFeatures("logo.png")
if finder.hasNext():
print finder.next()

Le feature matching s’appuie sur les descripteurs ORB (Oriented FAST and Rotated BRIEF). Plus lent que le template matching, mais résistant à de petites rotations, à de l’occlusion partielle, et à un facteur d’échelle modéré. Utile quand :

  • la cible se déplace à l’intérieur d’une fenêtre (drag-and-drop),
  • la cible tourne (boussole, indicateur rotatif),
  • la même image s’affiche à plusieurs tailles (UI responsive).

Region.right(N) ne déclenche aucune capture. La méthode se contente d’ajuster le rectangle de recherche. La capture écran réelle a lieu paresseusement, au prochain find(), wait() ou click().

D’où la différence radicale de coût entre un find() imbriqué qui cible une petite region et un find() qui balaie tout l’écran. OpenCV scanne dans les deux cas, mais sur des surfaces qui n’ont rien à voir.

# Bon — OpenCV scanne 300 × 50 px
btn = dialog.right(300).find("save.png")
# Mauvais — OpenCV scanne 1920 × 1080 px à chaque appel
btn = Screen(0).find("save.png")

Chaque moniteur dispose de sa propre instance Screen(n). Screen(0) est l’écran principal. Screen.getNumberScreens() indique combien il y en a au total.

for i in range(Screen.getNumberScreens()):
s = Screen(i)
print "Écran %d : %d × %d à (%d, %d)" % (i, s.getW(), s.getH(), s.getX(), s.getY())

Une capture ou un match restent confinés à un seul Screen, sauf si vous fusionnez explicitement des regions provenant de plusieurs écrans.

C’est l’outil à dégainer en premier quand un script clique au mauvais endroit :

match = find("button.png")
match.highlight(2) # encadré rouge pendant 2 s
match.highlight(2, "green") # idem en vert

Vous voyez immédiatement où OculiX pense que le match se trouve. Neuf bugs « pourquoi a-t-il cliqué là ? » sur dix se résolvent en cinq secondes après un coup de highlight.

Run → Run Slow Motion dans l’IDE met chaque match brièvement en évidence avant l’action correspondante. Pratique pour :

  • présenter un script à un non-technicien,
  • repérer un miss-click intermittent,
  • enregistrer un screencast propre d’un parcours.
Settings.MinSimilarity = 0.7 # seuil minimum par défaut
Settings.AlwaysResize = 1.0 # mise à l'échelle des captures avant matching
Settings.WaitScanRate = 3 # scans OpenCV par seconde pendant wait()
Settings.MoveMouseDelay = 0.3 # durée du déplacement du curseur (rendu visible)
Settings.AutoWaitTimeout = 3.0 # attente implicite avant chaque action
Settings.SaveLastImage = True # écrit le dernier match raté dans ./lastImage.png

Le pipeline de vision est indépendant de la source de l’image. VNCScreen, ADBScreen et le Screen local exposent la même API find/click/type — ils diffèrent uniquement par l’endroit d’où viennent les captures. La pile OpenCV en aval ne se soucie pas de savoir si l’image vient de votre moniteur, d’un téléphone Android via ADB, ou d’une machine distante via VNC.

# Même script, trois sources différentes
local_btn = Screen(0).find("save.png")
android_btn = ADBScreen.start(adb_path).find("save.png")
remote_btn = VNCScreen.start("192.168.1.10", 5900, "", 1920, 1080).find("save.png")

Sur un laptop milieu de gamme 2024, écran 1920×1080, bouton 100×30 :

OpérationDurée typique
Screen(0).capture()~30 ms
find() sur écran entier~50 ms
find() sur region 300×100~5 ms
findFeatures() sur écran entier~200 ms
Region.text() (Tesseract)~150 ms
PaddleOCREngine.recognize()~300 ms (CPU)

L’OCR coûte une dizaine de fois plus cher que le matching d’image. Tant que l’apparence de la cible est stable, restez sur du matching d’image.