okya for MapleStory
Nederlands
English한국어日本語简体中文繁體中文EspañolPortuguêsไทยTiếng ViệtDeutschFrançaisItalianoNederlands
In dit artikel⌄
okya for MapleStory›Zo werkt het

Technische uitleg · Python / OpenCV

Zo werkt het

Ontdek met tien gerichte codefragmenten en algoritmediagrammen hoe Classic MapleStory Bot schermbeelden omzet in strategiekeuzes.

Pixels → observaties → beslissingen → invoer

Beeldherkenning schat positie en status, de planner kiest een actie en de runtime verstuurt lokale toetsen. Dit zijn aparte lagen: een monster herkennen drukt geen toets in en versturen bewijst niet dat het spel de toets accepteert.

  1. 01Opname
  2. 02Herkenning
  3. 03Beslissing
  4. 04Lokale invoer

01Het spelvenster vastleggen#

Op Windows zoekt grab_client het clientgebied, legt dit vast met ImageGrab en converteert naar RGB. Ongeldige afmetingen geven een fout in plaats van een leeg beeld door te sturen.

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

Dit is één platformtak. macOS gebruikt native opname en apart bijsnijden. Herkenning werkt later met OpenCV-BGR-arrays: houd de kanaalvolgorde tussen Pillow en OpenCV expliciet.

02Zoekgebied beperken met een ROI#

De detector begint in een begrensd gebied linksboven. Grijswaarden, Canny-randen en contouren leveren kandidaten; markercentra begrenzen de juiste rechthoek verder.

Werk met het relevante gebied

Spelbeeld

Minimapuitsnede

Beeld en uitsnede tonen een interessegebied (ROI). De markering is een voorbeeld, geen gemeten detectie. Kleine zoekgebieden beperken 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

Dit is alleen het begin, niet de hele rechthoekbeoordeling. Bij ongeldige beelden of ontbrekende markers geldt de vorige definitie of een tijdelijke standaard; dat is geen nieuw bevestigde detectie.

03Personage zoeken met grijswaardencorrelatie#

match_player_feature zet beeld en sjabloon om naar grijswaarden en gebruikt TM_CCORR_NORMED. Een optioneel masker sluit transparante pixels uit.

Het sjabloon schuift over het beeld

Sjabloon · T

Scorekaart · S(x, y)

Schema: elke positie krijgt een score. De lichte cel is een lokaal maximum, geen meting op dit beeld. Genormaliseerde correlatie is geen kans.
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

De aanroeper verwijdert niet-eindige scores en vergelijkt de beste met de drempel. Hier is 0,995 een instelling, geen universeel nauwkeurigheidspercentage. Leg een stabiel kenmerk in beide richtingen vast.

04Sjabloonscores omzetten in doelen#

De ondoorzichtige tak van detect_template verwerpt te grote sjablonen, berekent een scorekaart en verzamelt posities vanaf de drempel.

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

De linkerbovenhoek wordt omgerekend naar horizontaal centrum en gekalibreerde voetpositie; duplicaten worden verwijderd. Transparante sjablonen gebruiken een aparte gemaskeerde correlatiemethode.

05Dubbele detecties samenvoegen#

Naburige posities of poses kunnen hetzelfde monster herkennen. _non_maximum sorteert op vertrouwen en houdt kandidaten op voldoende afstand van eerdere resultaten.

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

Dit gebruikt x/y-afstand, geen bounding-box-IoU. De sterkere nabije kandidaat wint. Te veel afstand voegt echte buren samen; te weinig laat duplicaten en vertekent tellingen.

06Minimapmarker met HSV segmenteren#

Eerst wordt exacte BGR-kleur gezocht. Zonder geldige component gebruikt de HSV-terugval het kleurbereik van het serverprofiel met extra verzadigingstolerantie.

Eerst kleur, dan geometrie

RGB → HSV → Binair masker

Component → Lokale positie (x, y)

Schema: een kleurbereik maakt een masker; verbonden componenten groeperen buurpixels. Afmetingen en oppervlakte tellen mee: niet elke gele pixel is de speler.
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 breedte, hoogte en oppervlakte. Dit verdraagt kleurverschillen zonder elke gele vorm te accepteren. Bereiken en geometrie horen bij het serverprofiel, niet één universele kleur.

07Marker omzetten in lokale mapcoördinaten#

De uitsnede volgt de gedetecteerde minimapgrenzen. Een serverspecifieke voetoffset geeft de standplaats; alleen bij precies één kandidaat volgt een positie.

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 is lokaal voor de minimap, niet de desktop. Grenzen blijven binnen het beeld. Geen of meerdere kandidaten geven None in plaats van een verzonnen positie.

08HP/MP lezen met kleuren en kolommen#

bar is de RGB-uitsnede, pixels de pixeltoegang en valid_columns begint leeg. Rode pixels stemmen voor HP, blauwe voor MP. Een kolom vereist genoeg passende rijen.

Meet vulling, lees geen tekst

100 × (x_last + 1) / width

Statusbalk → Meest rechtse geldige kolom

Schema: genoeg passende pixels maken een kolom geldig. De meest rechtse geldige kolom schat de vulling in een ingesteld gebied, zonder HP-/MP-cijfers te lezen.
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

Het percentage volgt de laatste geldige kolom, niet het aandeel kleurpixels. Geen kolom geeft nul; vanaf 95 wordt hier naar 100 afgerond. Verkeerde uitsneden of losse pixels beïnvloeden dit: controleer vóór automatisch herstel.

09Planning loshouden van beeldherkenning#

HorizontalHuntPlanner krijgt een Observation, geen screenshot. same_line_targets filtert voethoogtes. decide kiest het dichtstbijzijnde horizontale doel, valt binnen bereik aan, nadert anders of patrouilleert zonder doel.

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

Zuivere logica: Decision beschrijft intentie en verstuurt geen toetsen. Tests met kunstmatige posities controleren afstanden en richtingen zonder spel.

10Dure sjabloonherkenning minder vaak uitvoeren#

De runtime scheidt monsterherkenning van snellere routevolging. time.monotonic bepaalt verversingen; tussendoor wordt het laatste resultaat hergebruikt.

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

Bij onderdrukte detectie wordt de cache geleegd in plaats van oude doelen te bewaren. Minder rekenwerk maakt de leeftijd van gegevens belangrijk: cache bewijst geen huidige zichtbaarheid. Timing, stoppen en toetsen loslaten blijven runtimeverantwoordelijkheden.

Van herkenning naar een betrouwbaar systeem#

Test elke laag apart: screenshots voor herkenning, synthetische Observation voor planning en diagnose vóór toetsen. Een hoge score bewijst geen juiste herkenning. Controleer oude gegevens, afdekking en onduidelijke markers. Bij stoppen laat de runtime toetsen los. Geen garantie op compatibiliteit of toestemming voor automatisering.