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.
- 01Opname
- 02Herkenning
- 03Beslissing
- 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.
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 imagegrab_clientL354–363Dit 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
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–106Dit 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)
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–110De 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.
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–225De 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.
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–199Dit 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)
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–86matching_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.
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–171x/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
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–308Het 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.
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–60Zuivere 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.
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_monstersHorizontalHuntRunner._monsters_for_frameL155–166Bij 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.