okya for MapleStory
Deutsch
English한국어日本語简体中文繁體中文EspañolPortuguêsไทยTiếng ViệtDeutschFrançaisItalianoNederlands
In diesem Artikel⌄
okya for MapleStory›So funktioniert’s

Technik im Detail · Python / OpenCV

So funktioniert’s

Mit zehn gezielten Codeausschnitten und Algorithmusgrafiken nachvollziehen, wie Classic MapleStory Bot Bildschirmbilder in Strategieentscheidungen verwandelt.

Pixel → Beobachtung → Entscheidung → Eingabe

Bildauswertung schätzt Position und Status, der Planer wählt eine Aktion und die Laufzeit sendet lokale Tasten. Diese Ebenen sind getrennt: Ein erkanntes Monster drückt keine Taste, und eine gesendete Taste beweist nicht, dass das Spiel sie annimmt.

  1. 01Aufnahme
  2. 02Erkennung
  3. 03Entscheidung
  4. 04Lokale Eingabe

01Spielfenster aufnehmen#

Unter Windows bestimmt grab_client zuerst den Clientbereich, nimmt dessen Rechteck mit ImageGrab auf und konvertiert zu RGB. Ungültige Fenster- oder Bildmaße lösen einen Fehler aus, statt leere Bilder weiterzugeben.

Python · vitals.py
window = find_client_origin(title)
if window is None:
    raise RuntimeError(active_translate("error_game_window_not_found", title=title))
left, top, width, height = window
if width <= 1 or height <= 1:
    raise RuntimeError(active_translate("error_game_capture_unavailable"))
image = ImageGrab.grab((left, top, left + width, top + height)).convert("RGB")
if image.width <= 1 or image.height <= 1:
    raise RuntimeError(active_translate("error_empty_game_capture_retry"))
return image
grab_clientL354–363

Nur ein Plattformzweig: macOS nutzt native Aufnahme und separaten Zuschnitt. Spätere Erkennung arbeitet mit OpenCV-BGR-Arrays. Beim Austausch zwischen Pillow und OpenCV die Kanalreihenfolge beachten.

02Suchbereich mit ROI begrenzen#

Der Minikartendetektor startet oben links in einem begrenzten Bereich. Graustufen, Canny-Kanten und Konturen liefern Kandidaten; Markermittelpunkte grenzen das passende Rechteck weiter ein.

Den relevanten Bereich auswählen

Spielbild

Minikartenausschnitt

Spielbild und Minikartenausschnitt zeigen eine Region of Interest (ROI). Die Markierung ist illustrativ, kein gemessenes Detektorergebnis. Kleine Suchbereiche reduzieren irrelevante Kandidaten.
Python · map_position.py
if frame_bgr.size == 0 or frame_bgr.ndim != 3:
    return previous or generic_minimap_definition()
search = frame_bgr[:min(frame_bgr.shape[0], 420), :min(frame_bgr.shape[1], 430)]
edges = cv2.Canny(cv2.cvtColor(search, cv2.COLOR_BGR2GRAY), 45, 135)
contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

marker_centers = [
    (x + width / 2.0, y + height / 2.0)
    for x, y, width, height, _ in _marker_components(search, player_pixels)
]
if not marker_centers:
    return previous or generic_minimap_definition()
detect_minimap_definitionL95–106

Das ist nur der Einstieg, nicht die vollständige Rechteckbewertung. Bei ungültigen Bildern oder fehlenden Markern wird die vorherige Definition oder ein Standard genutzt. Dieser Rückfall ist keine neue bestätigte Erkennung.

03Figur per Graustufenkorrelation finden#

match_player_feature wandelt Bild und Figurenvorlage in Graustufen um und nutzt TM_CCORR_NORMED. Eine optionale Maske kann transparente Vorlagenpixel ausschließen.

Die Vorlage wandert über das Bild

Vorlage · T

Ähnlichkeitskarte · S(x, y)

Schema: Jede Position erhält einen Ähnlichkeitswert. Die helle Zelle steht für ein lokales Maximum, nicht für eine Messung dieses Bilds. Normalisierte Korrelation ist keine Wahrscheinlichkeit.
Python · vision.py
def match_player_feature(image_bgr: np.ndarray, template_bgr: np.ndarray,
                         mask: np.ndarray | None) -> np.ndarray:
    """Match normal player feature templates with tolerant grayscale correlation."""
    screen_gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
    template_gray = cv2.cvtColor(template_bgr, cv2.COLOR_BGR2GRAY)
    return cv2.matchTemplate(
        screen_gray,
        template_gray,
        cv2.TM_CCORR_NORMED,
        mask=mask,
    )
match_player_featureL100–110

Der Aufrufer bereinigt nicht endliche Werte und vergleicht den besten Treffer mit dem Schwellwert. Hier ist er 0,995 – eine Implementierungseinstellung, keine allgemeine Genauigkeitsangabe. Stabiles Merkmal in beiden Richtungen aufnehmen.

04Vorlagentreffer in Ziele umwandeln#

Der Zweig für undurchsichtige Vorlagen in detect_template verwirft zu große Vorlagen, berechnet eine Ähnlichkeitskarte und sammelt Positionen oberhalb des Schwellwerts.

Python · vision.py
if image_bgr.shape[0] < template.shape[0] or image_bgr.shape[1] < template.shape[1]:
    return []
scores = cv2.matchTemplate(image_bgr, template, cv2.TM_CCOEFF_NORMED)
ys, xs = np.where(scores >= threshold)
targets = [Target(int(x + template.shape[1] / 2),
                  int(y) + top_to_feet if top_to_feet is not None else feature_center_to_feet(int(y), template.shape[0]),
                  float(scores[y, x]))
           for y, x in zip(ys, xs)]
return _non_maximum(targets, 15)
detect_templateL217–225

Aus der oberen linken Trefferposition werden horizontales Zentrum und kalibrierte Fußposition berechnet, dann Duplikate entfernt. Transparente Vorlagen verwenden einen eigenen maskierten Korrelationszweig mit anderer Methode.

05Mehrfachtreffer zusammenführen#

Benachbarte Positionen oder Posen können dasselbe Monster treffen. _non_maximum sortiert nach Konfidenz und behält nur Kandidaten mit ausreichendem Abstand zu allen behaltenen Treffern.

Python · vision.py
def _non_maximum(points: list[Target], min_distance: int) -> list[Target]:
    kept: list[Target] = []
    for point in sorted(points, key=lambda item: item.confidence, reverse=True):
        if all(abs(point.center_x - other.center_x) > min_distance or abs(point.feet_y - other.feet_y) > min_distance for other in kept):
            kept.append(point)
    return kept
_non_maximumL194–199

Verwendet x/y-Abstand statt Bounding-Box-IoU. Der stärkere nahe Treffer gewinnt. Zu großer Abstand verschmilzt echte Nachbarn; zu kleiner lässt Duplikate stehen und verfälscht die Anzahl.

06Minikartenmarker mit HSV segmentieren#

Zuerst wird eine exakte BGR-Farbe gesucht. Fehlt eine gültige Komponente, verwendet der HSV-Rückfall den Farbbereich des Serverprofils mit zusätzlicher Sättigungstoleranz.

Erst Farbe, dann Geometrie

RGB → HSV → Binärmaske

Komponente → Lokale Position (x, y)

Schema: Ein Farbbereich ergibt eine Maske; zusammenhängende Komponenten bündeln Nachbarpixel. Breite, Höhe und Fläche werden geprüft – nicht jeder gelbe Pixel gilt als Spieler.
Python · map_position.py
hsv = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2HSV)
hue_low, saturation_low, value_low = player_pixels.activity_hsv_lower
lower = np.asarray(
    (hue_low, max(0, saturation_low - 40), value_low), dtype=np.uint8,
)
upper = np.asarray(player_pixels.activity_hsv_upper, dtype=np.uint8)
tolerant_mask = cv2.inRange(hsv, lower, upper)
return matching_components(tolerant_mask, exact_area=False)
_marker_componentsL79–86

matching_components filtert nach Breite, Höhe und Fläche. Das toleriert Farbverwaltungsunterschiede, nicht beliebige gelbe Formen. Farbbereiche und Geometrie gehören zum Serverprofil, nicht zu einer universellen Konstante.

07Marker in lokale Kartenkoordinaten umrechnen#

Bildzuschnitt anhand erkannter Minikartengrenzen. Ein serverspezifischer Fußversatz ergibt den Standpunkt; nur bei genau einem Kandidaten wird eine Position geliefert.

Python · map_position.py
right = min(frame_bgr.shape[1], minimap.end_x + 1)
bottom = min(frame_bgr.shape[0], minimap.end_y + 1)
if minimap.start_x >= right or minimap.start_y >= bottom:
    return None
image = frame_bgr[minimap.start_y:bottom, minimap.start_x:right]
candidates: list[tuple[int, int]] = []
for x, y, _, _, _ in _marker_components(image, player_pixels):
    # Each server profile maps the matched component's origin to the
    # marker's standing point with its own vertical foot offset.
    candidates.append((x, y + player_pixels.foot_offset))
if len(candidates) != 1:
    return None
x, foot_y = candidates[0]
source_width = max(1, right - minimap.start_x)
source_height = max(1, bottom - minimap.start_y)
source_x = min(source_width - 1, x)
source_y = min(source_height - 1, foot_y)
return float(source_x), float(source_y)
detect_minimap_playerL154–171

x/y bezieht sich auf den Minikartenzuschnitt, nicht den Desktop. Grenzen werden auf das Bild begrenzt. Null oder mehrere Kandidaten ergeben None statt einer erfundenen Position.

08HP/MP über Farbe und Spalten lesen#

bar ist der RGB-Balkenzuschnitt, pixels dessen Pixelzugriff; valid_columns startet leer. Rotdominante Pixel stimmen für HP, blaudominante für MP. Genug passende Zeilen machen eine Spalte gültig.

Fülllänge statt Zahlen lesen

100 × (x_last + 1) / width

Statusbalken → Letzte gültige Spalte rechts

Schema: Genug passende Pixel machen eine Spalte gültig. Die äußerste rechte gültige Spalte schätzt die Fülllänge. Gemessen wird ein eingestellter Balkenbereich, kein HP-/MP-Zahlentext.
Python · vitals.py
valid_columns: list[int] = []
for column in range(bar.width):
    matches = 0
    for row in range(bar.height):
        red, green, blue = pixels[column, row]
        if kind == "hp":
            colored = red >= 130 and red >= green * 1.5 and red >= blue * 1.5
        else:
            colored = blue >= 110 and blue >= red * 1.25 and blue >= green * 1.15
        matches += int(colored)
    if matches >= max(2, int(bar.height * 0.2)):
        valid_columns.append(column)
if not valid_columns:
    return 0.0
percent = 100 * (valid_columns[-1] + 1) / bar.width
return 100.0 if percent >= 95 else round(percent, 1)
_bar_percent_from_imageL293–308

Der Prozentwert folgt der letzten gültigen Spalte, nicht dem Anteil aller Farbpixel. Ohne Spalte ist er null; ab 95 wird hier auf 100 gerundet. Falscher Zuschnitt oder Fremdpixel verfälschen den Wert – vor automatischen Tränken prüfen.

09Planung von Bilderkennung trennen#

HorizontalHuntPlanner bekommt eine Observation statt eines Bilds. same_line_targets filtert Fußhöhen. decide wählt das nächste horizontale Ziel, greift in Reichweite an, nähert sich sonst oder patrouilliert ohne Ziel.

Python · horizontal_hunt.py
def decide(self, observation: Observation) -> Decision:
    targets = self.same_line_targets(observation)
    if targets:
        target = min(targets, key=lambda m: abs(m.center_x - observation.player_x))
        delta = target.center_x - observation.player_x
        if abs(delta) <= self.config.target_distance:
            return Decision(HuntState.ATTACK, "attack_burst", "right" if delta >= 0 else "left", target)
        return Decision(HuntState.APPROACH, "move_pulse", "right" if delta > 0 else "left", target)
    return Decision(HuntState.PATROL, "move_pulse")
HorizontalHuntPlanner.decideL52–60

Reine Entscheidungslogik: Decision beschreibt eine Absicht und sendet keine Tasten. Tests mit künstlichen Positionen prüfen Entfernungsgrenzen und Richtungen ohne Spiel.

10Teure Vorlagensuche seltener ausführen#

Die Laufzeit trennt langsame Monstersuche von schneller Routenverfolgung. time.monotonic steuert Aktualisierungen; dazwischen wird das letzte Ergebnis genutzt.

Python · hunt_runtime.py
def _monsters_for_frame(self, frame: np.ndarray) -> tuple:
    """Run expensive monster matching at a lower rate than route tracking."""
    if self._monster_detection_suppressed():
        self.last_detected_monsters = ()
        self.last_monster_detection_at = 0.0
        return ()
    now = time.monotonic()
    if (self.last_monster_detection_at <= 0.0
            or now - self.last_monster_detection_at >= self.MONSTER_DETECTION_INTERVAL_SECONDS):
        self.last_detected_monsters = tuple(self._detect_monsters(frame))
        self.last_monster_detection_at = time.monotonic()
    return self.last_detected_monsters
HorizontalHuntRunner._monsters_for_frameL155–166

Bei unterdrückter Erkennung wird der Cache geleert, statt alte Ziele unbegrenzt zu behalten. Weniger Rechenarbeit macht das Cachealter relevant: Gespeicherte Beobachtungen beweisen keine aktuelle Sichtbarkeit. Timing, Stoppen und Tastenfreigabe bleiben Laufzeitaufgaben.

Von der Erkennung zum verlässlichen System#

Jede Ebene getrennt testen: Screenshots für Erkennung, synthetische Observation für Planung, Diagnose vor Tasteneingaben. Hohe Trefferwerte beweisen keine korrekte Erkennung. Alte Daten, Verdeckungen und unklare Marker prüfen. Beim Stoppen gibt die Laufzeit Tasten frei. Keine Garantie für Kompatibilität oder erlaubte Nutzung.