def parse_map_info(self, frame: Frame) -> Tuple[Optional[str], Optional[str]]: map_info_image = self.REGIONS["map_info"].extract_one(frame.image) yellow_text = cv2.inRange( cv2.cvtColor(map_info_image, cv2.COLOR_BGR2HSV_FULL), ((30 / 360) * 255, 0.5 * 255, 0.6 * 255), ((45 / 360) * 255, 1.0 * 255, 1.0 * 255), ) yellow_text = cv2.filter2D(yellow_text, -1, np.ones((4, 2)) / (4 * 2)) yellow_text_left = np.argmax(np.sum(yellow_text, axis=0) / 255 > 4) map_image, mode_image = ( map_info_image[:, :yellow_text_left - 20], map_info_image[:, yellow_text_left - 5:], ) map_text = imageops.tesser_ocr(np.min(map_image, axis=2), whitelist=string.ascii_uppercase + " ", scale=2, invert=True) mode_text = imageops.tesser_ocr(np.max(mode_image, axis=2), whitelist=string.ascii_uppercase + " ", scale=2, invert=True) if len(map_text) < 4 or len(mode_text) < 4: logger.warning( f"Unexpected map/mode text: {map_text} | {mode_text}") return None, None else: logger.debug(f"Got map={map_text}, mode={mode_text}") return map_text, mode_text
def _get_placed(self, frame: Frame) -> Optional[int]: placed_image = self.REGIONS["squad_placed"].extract_one( frame.image).copy() cv2.normalize(placed_image, placed_image, 0, 255, cv2.NORM_MINMAX) orange = cv2.inRange( placed_image, np.array(self.PLACED_COLOUR) - 40, np.array(self.PLACED_COLOUR) + 40, ) text = imageops.tesser_ocr(orange, whitelist=string.digits + "#") if text and text[0] == "#": try: placed = int(text[1:]) except ValueError: logger.warning(f"Could not parse {text!r} as number") return None else: logger.debug(f"Parsed {text!r} as {placed}") if 1 <= placed <= 30: return placed else: logger.warning(f"Rejected placed={placed}") else: logger.warning(f'Rejected placed text {text!r} - did not get "#"') return None
def _get_bearing(self, frame: Frame, debug_image: Optional[np.ndarray]) -> Optional[int]: bearing_image = self.REGIONS["bearing"].extract_one( frame.image_yuv[:, :, 0]) _, bearing_thresh = cv2.threshold(bearing_image, 190, 255, cv2.THRESH_BINARY) if debug_image is not None: debug_image[90:90 + bearing_image.shape[0], 1020:1020 + bearing_image.shape[1], ] = cv2.cvtColor( bearing_image, cv2.COLOR_GRAY2BGR) debug_image[90:90 + bearing_image.shape[0], 1100:1100 + bearing_image.shape[1], ] = cv2.cvtColor( bearing_thresh, cv2.COLOR_GRAY2BGR) bearing = imageops.tesser_ocr( bearing_thresh, expected_type=int, engine=ocr.tesseract_ttlakes_digits, warn_on_fail=False, ) if bearing is None or not 0 <= bearing <= 360: logger.debug(f"Got invalid bearing: {bearing}") return None if bearing is not None: logger.debug(f"Got bearing={bearing}") return bearing else: return None
def _process_yellowtext(self, image: np.ndarray) -> Optional[int]: # mask out only yellow text (digits) yellow = cv2.inRange(image, (0, 40, 150), (90, 230, 255)) yellow = cv2.dilate(yellow, None) yellowtext_image = cv2.bitwise_and( image, cv2.cvtColor(yellow, cv2.COLOR_GRAY2BGR)) yellowtext_image_g = np.max(yellowtext_image, axis=2) yellowtext_image_g = cv2.erode(yellowtext_image_g, np.ones((2, 2))) text = imageops.tesser_ocr( yellowtext_image_g, engine=imageops.tesseract_lstm, scale=4, blur=4, invert=True, ) otext = text text = text.upper() for s1, s2 in "|1", "I1", "L1", "O0", "S5", "B6": text = text.replace(s1, s2) for hashchar in "#H": text = text.replace(hashchar, "") logger.info(f"Got text={otext} -> {text}") try: return int(text) except ValueError: logger.warning(f"Could not parse {text!r} as int") return None
def ocr_region(self, frame: Frame, target_region: str): region = self.REGIONS[target_region].extract_one(frame.image) gray = 255 - imageops.normalise(np.min(region, axis=2)) text = imageops.tesser_ocr( gray, engine=imageops.tesseract_lstm, ) return text
def process(self, frame: Frame) -> bool: # timer_y = self.REGIONS['timer'].extract_one(frame.image_yuv[:, :, 0]) # _, timer_y_thresh = cv2.threshold(timer_y, 230, 255, cv2.THRESH_BINARY) spike_planted_im = self.REGIONS["spike_planted"].extract_one(frame.image) spike_planted_thresh = cv2.inRange( spike_planted_im, (0, 0, 130), (10, 10, 250), ) # cv2.imshow('spike_planted_im', spike_planted_im) # cv2.imshow('spike_planted_thresh', spike_planted_thresh) # cv2.imshow('SPIKE_PLANTED_TEMPLATE', self.SPIKE_PLANTED_TEMPLATE) spike_planted_match = np.max( cv2.matchTemplate( spike_planted_thresh, self.SPIKE_PLANTED_TEMPLATE, cv2.TM_CCORR_NORMED, ) ) logger.debug(f"Spike planted match: {spike_planted_match:.2f}") spike_planted = bool(spike_planted_match > self.SPIKE_PLANTED_REQUIRED_MATCH) if spike_planted: buy_phase = False else: buy_phase_gray = np.min(self.REGIONS["buy_phase"].extract_one(frame.image), axis=2) buy_phase_norm = imageops.normalise(buy_phase_gray, bottom=80) # cv2.imshow('buy_phase_norm', buy_phase_norm) buy_phase_match = np.max( cv2.matchTemplate(buy_phase_norm, self.BUY_PHASE_TEMPLATE, cv2.TM_CCORR_NORMED) ) logger.debug(f"Buy phase match: {buy_phase_match}") buy_phase = buy_phase_match > 0.9 countdown_text = None if not spike_planted: countdown_gray = np.min(self.REGIONS["timer"].extract_one(frame.image), axis=2) countdown_norm = 255 - imageops.normalise(countdown_gray, bottom=80) # debugops.test_tesser_engines( # countdown_norm # ) countdown_text = imageops.tesser_ocr( countdown_norm, # whitelist=string.digits + ':.', engine=imageops.tesseract_only, ) if len(countdown_text) > 6: countdown_text = None frame.valorant.timer = Timer( spike_planted=spike_planted, buy_phase=buy_phase, countdown=countdown_text, ) draw_timer(frame.debug_image, frame.valorant.timer) return frame.valorant.timer.valid
def parse_result_and_map( self, frame: Frame) -> Tuple[Optional[str], Optional[str]]: result_im = self.REGIONS["result"].extract_one(frame.image) gray = np.max(result_im, axis=2) # mask out white/gray text (this is map and match time info) white_text = ((gray > 100) & (np.ptp(result_im, axis=2) < 20)).astype(np.uint8) * 255 white_text = cv2.erode(white_text, None) white_text = np.sum(white_text, axis=0) / 255 right = np.argmax(white_text > 2) if right > 150: right -= 10 logger.info( f"Trimming width of result image {gray.shape[1]} -> {right} to cut white text" ) gray = gray[:, :right] else: right = gray.shape[1] result_text = imageops.tesser_ocr(gray, whitelist="".join( set("".join(self.RESULTS))), invert=True) result = textops.matches(result_text, self.RESULTS) if np.min(result) > 2: logger.warning( f"Could not identify result from {result_text!r} (match={np.min(result)})" ) return None, None result = self.RESULTS[arrayops.argmin(result)] logger.debug(f"Got result {result} from {result_text!r}") # TODO: test this with "draw" result map_image = self.REGIONS["map_name"].extract_one(frame.image)[:, right:] gray = np.min(map_image, axis=2) map_text = textops.strip_string( imageops.tesser_ocr(gray, whitelist=string.ascii_uppercase + " :'", invert=True, scale=2), string.ascii_uppercase + " ", ) logger.debug(f"Parsed map as {map_text}") return result, map_text
def _ocr_playername(self, player_name_image: np.ndarray) -> str: # crop out crown _, thresh = cv2.threshold(player_name_image, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) mnv, mxv, mnl, mxl = cv2.minMaxLoc(cv2.matchTemplate(thresh, self.CROWN, cv2.TM_CCORR_NORMED)) if mxv > 0.99: player_name_image = player_name_image[:, mxl[0] + self.CROWN.shape[1] :] player_name = imageops.tesser_ocr(player_name_image, scale=4) return player_name
def process(self, frame: Frame): if frame.apex.apex_play_menu_match: return frame.apex.apex_play_menu_match >= self.REQUIRED_MATCH y = frame.image_yuv[:, :, 0] ready_button = self.REGIONS["ready_button"].extract_one(y) t, thresh = cv2.threshold(ready_button, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) ready_match = np.max(cv2.matchTemplate(thresh, self.READY, cv2.TM_CCORR_NORMED)) if ready_match >= self.REQUIRED_MATCH: cancel_match = 0.0 else: cancel_match = np.max(cv2.matchTemplate(thresh, self.CANCEL, cv2.TM_CCORR_NORMED)) frame.apex.apex_play_menu_match = round(float(max(ready_match, cancel_match)), 5) _draw_buttons_match(frame.debug_image, ready_match, cancel_match, self.REQUIRED_MATCH) if ready_match >= self.REQUIRED_MATCH or cancel_match >= self.REQUIRED_MATCH: player_name_image = self.REGIONS["player_name"].extract_one(y) mate1, mate2 = self.REGIONS["squadmates"].extract(y) rank_text_region = self.REGIONS["rank_text"].extract_one(y) rank_text = imageops.tesser_ocr(rank_text_region, invert=True, engine=imageops.tesseract_lstm) rp_text_region = self.REGIONS["rp_text"].extract_one(y) rp_text = imageops.tesser_ocr(rp_text_region, invert=True, engine=imageops.tesseract_lstm) frame.apex.apex_play_menu = PlayMenu( player_name=self._ocr_playername(player_name_image), squadmates=(self._ocr_playername(mate1), self._ocr_playername(mate2)), ready=cancel_match >= self.REQUIRED_MATCH, rank_text=rank_text, rp_text=rp_text, ) self.REGIONS.draw(frame.debug_image) _draw_play_menu(frame.debug_image, frame.apex.apex_play_menu) return True else: return False
def process(self, frame: Frame) -> bool: y = frame.image_yuv[:, :, 0] tank_region = np.max(self.REGIONS["tank_region"].extract_one(frame.image), axis=2) _, thresh = cv2.threshold(tank_region, 100, 255, cv2.THRESH_BINARY) # cv2.imshow('thresh1', thresh) tank_match_sm = cv2.matchTemplate(thresh, self.TANK_TEMPLATE, cv2.TM_CCORR_NORMED) _, match_sm, _, mxloc_sm = cv2.minMaxLoc(tank_match_sm) tank_match_lg = cv2.matchTemplate(thresh, self.TANK_LARGE_TEMPLATE, cv2.TM_CCORR_NORMED) _, match_lg, _, mxloc_lg = cv2.minMaxLoc(tank_match_lg) lock_match = cv2.matchTemplate(thresh, self.LOCK_TEMPLATE, cv2.TM_CCORR_NORMED) _, match_lock, _, mxloc_lock = cv2.minMaxLoc(lock_match) matched_i = arrayops.argmax([match_sm, match_lg, match_lock]) # print([match_sm, match_lg, match_lock]) match = [match_sm, match_lg, match_lock][matched_i] matched = ["tank", "tank_lg", "lock"][matched_i] best_match_pos = [mxloc_sm, mxloc_lg, mxloc_lock][matched_i] match_x = best_match_pos[0] # print(matched, match_x) frame.overwatch.role_select_match = round(match, 2) if match > self.REQUIRED_MATCH: grouped = match_x < 150 logger.debug( f"Found match for {matched!r} with match={match:0.3f} ({match_sm:.2f}, {match_lg:.2f}, {match_lock:.2f}), x={match_x} => grouped={grouped}" ) suffix = "_group" if grouped else "_solo" frame.overwatch.role_select = RoleSelect( placement_text=imageops.tesser_ocr_all( self.REGIONS["placements" + suffix].extract(y), whitelist=string.digits + "/-" ), sr_text=big_noodle.ocr_all(self.REGIONS["srs" + suffix].extract(y), height=23, invert=True), account_name=imageops.tesser_ocr( self.REGIONS["account_name"].extract_one(y), engine=imageops.tesseract_lstm ), grouped=grouped, image=lazy_upload( "role_select", self.REGIONS.blank_out(frame.image), frame.timestamp, selection="last" ), ) if frame.debug_image is not None: self.REGIONS.draw(frame.debug_image) _draw_role_select(frame.debug_image, frame.overwatch.role_select) return True return False
def _parse_squads_left_text(self, luma: np.ndarray, has_badge: bool) -> str: prefix = "ranked_" if has_badge else "" region = self.REGIONS[prefix + "squads_left"].extract_one(luma) squads_left_text = imageops.tesser_ocr(region, engine=imageops.tesseract_lstm, scale=2, invert=True).upper() squads_left_text = ("".join(c for c in squads_left_text if c in string.ascii_uppercase + string.digits + " ").strip().replace( "B", "6")) return squads_left_text
def process(self, frame: Frame) -> bool: if frame.overwatch.main_menu or frame.overwatch.play_menu: return True self.REGIONS.draw(frame.debug_image) if self.detect_main_menu(frame): version_region = self.REGIONS["version"].extract_one(frame.image) thresh = imageops.otsu_thresh_lb_fraction(version_region, 0.75) version = imageops.tesser_ocr(thresh, whitelist=string.digits + ".-", invert=True, scale=4, blur=2) frame.overwatch.main_menu = MainMenu(version=version) _draw_main_menu(frame.debug_image, frame.overwatch.main_menu) return True elif self.detect_play_menu(frame): # placement_region = self.REGIONS['placement_matches'].extract_one(frame.image) # placement_region = cv2.cvtColor(placement_region, cv2.COLOR_BGR2GRAY) # _, thresh = cv2.threshold(placement_region, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) # match = 1 - float(np.min(cv2.matchTemplate(thresh, self.PLACEMENT_MATCHES_TEMPLATE, cv2.TM_SQDIFF_NORMED))) # is_placements = match > self.PLACEMENT_MATCHES_TEMPLATE_THRESHOLD # # if not is_placements: # group_sr_region = self.REGIONS['group_sr'].extract_one(frame.image) # color_variance = np.mean(np.var(group_sr_region, axis=(0, 1))) # if color_variance < 100: # # only one color - maybe placement # logger.warning(f'Got low color variance ({color_variance:.2f}) - ignoring parsed SR') # sr = None # else: # sr = self.read_sr(frame) # else: # sr = None # # frame.overwatch.play_menu = PlayMenu( # placements=is_placements, # sr=sr, # image=lazy_upload( # 'sr_full', # self.REGIONS['sr_full'].extract_one(frame.image), # frame.timestamp # ) # ) # # _draw_play_menu(frame.debug_image, frame.overwatch.play_menu) return True return False
def _get_players_alive(self, luma: np.ndarray, has_badge: bool) -> Optional[int]: prefix = "ranked_" if has_badge else "" region = self.REGIONS[prefix + "alive"].extract_one(luma) players_alive = imageops.tesser_ocr( region, engine=ocr.tesseract_ttlakes_digits, scale=4, expected_type=int) # shows a '?' if below 10 if players_alive and 10 <= players_alive <= 60: return players_alive else: logger.warning(f"Rejecting players_alive={players_alive}") return None
def _parse_killed_name(self, frame, row, killed_agent_x) -> Optional[str]: killed_name_gray = self._get_region( frame.image_yuv, row.center[1] - 10, row.center[1] + 10, row.center[0] + 10, killed_agent_x - 10, 0, debug_name="killed_name", debug_image=frame.debug_image, ) if killed_name_gray.shape[1] == 0: return None killed_name_norm = 255 - imageops.normalise(killed_name_gray, min=170) return textops.strip_string( imageops.tesser_ocr(killed_name_norm, engine=imageops.tesseract_lstm).upper(), alphabet=string.ascii_uppercase + string.digits + "# ", )
def _get_kills(self, luma: np.ndarray, mode: str) -> Optional[int]: prefix = (mode + "_") if mode else "" key = prefix + "kills" if key not in self.REGIONS.regions: key = "kills" region = self.REGIONS[key].extract_one(luma) _, kills_thresh = cv2.threshold(region, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) kills_thresh = cv2.copyMakeBorder(kills_thresh, 5, 5, 0, 5, cv2.BORDER_CONSTANT, value=0) match = cv2.matchTemplate(kills_thresh, self.SKULL_TEMPLATE, cv2.TM_CCORR_NORMED) mn, mx, mnloc, mxloc = cv2.minMaxLoc(match) if mx > 0.9: kills_image = region[:, mxloc[0] + self.SKULL_TEMPLATE.shape[1]:] # cv2.imshow('kills', cv2.resize(kills_image, (100, 100))) kills_text = (imageops.tesser_ocr(kills_image, engine=imageops.tesseract_lstm, scale=2, invert=True).upper().strip()) for s1, s2 in self.SUBS: kills_text = kills_text.replace(s1, s2) try: kills = int(kills_text) if 0 < kills <= 50: return kills else: logger.warning(f"Rejecting kills={kills}") return None except ValueError: logger.warning(f"Cannot parse kills={kills_text!r} as int") return None else: return None
def _parse_xp_breakdown(self, y: np.ndarray) -> XPStats: xp_breakdown_image = self.REGIONS["xp_fields"].extract_one(y) xp_breakdown_image = cv2.adaptiveThreshold( xp_breakdown_image, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 63, -30, ) lines = imageops.tesser_ocr( xp_breakdown_image, whitelist=string.ascii_letters + string.digits + "() \n", engine=imageops.tesseract_lstm_multiline, ) for s1, s2 in self.SUBS: lines = lines.replace(s1, s2) xp_stats = XPStats() for line in lines.splitlines(): stat_name, stat_value = self._parse_stat(line) if stat_name == "Won Match": xp_stats.won = True elif stat_name == "Top 3 Finish": xp_stats.top3_finish = True elif stat_name and stat_value is not None: # require stat value parsed correctly if stat_name == "Time Survived": xp_stats.time_survived = mmss_to_seconds(stat_value) elif stat_name == "Kills": xp_stats.kills = stat_value elif stat_name == "Damage Done": xp_stats.damage_done = stat_value elif stat_name == "Revive Ally": xp_stats.revive_ally = stat_value elif stat_name == "Respawn Ally": xp_stats.respawn_ally = stat_value return xp_stats
def _parse_score_report(self, y: np.ndarray) -> ScoreReport: rp_report_image = self.REGIONS["rp_fields"].extract_one(y) lines = [] for line in range(3): line_im = rp_report_image[line * 40 + 5:(line + 1) * 40 - 7, 5:] lines.append( imageops.tesser_ocr(line_im, engine=imageops.tesseract_lstm, invert=True, scale=2)) score_report = ScoreReport() for line in lines: valid = False if ":" in line: stat_name, stat_value = line.lower().replace(" ", "").split(":", 1) if stat_name == "entrycost": score_report.entry_rank = stat_value.lower() valid = True elif stat_name == "kills": try: score_report.kills = int(stat_value.replace("o", "0")) except ValueError: logger.warning( f'Could not parse Score Report > kills: {stat_value!r}" as int' ) else: valid = True elif stat_name == "matchplacement": stat_value = stat_value.replace("#", "") try: score_report.placement = int( stat_value.replace("o", "0").split("/", 1)[0]) except ValueError: logger.warning( f'Could not parse Score Report > placement: {stat_value!r}" as placement' ) else: valid = True if not valid: logger.warning(f"Unknown line in score report: {line!r}") score_adjustment_image = self.REGIONS["score_adjustment"].extract_one( y) score_adjustment_text = imageops.tesser_ocr( score_adjustment_image, engine=imageops.tesseract_lstm, invert=True, scale=1) score_adjustment_text_strip = (textops.strip_string( score_adjustment_text, alphabet=string.digits + "RP+-").replace( "RP", "").replace("+", "").replace("-", "")) try: score_report.rp_adjustment = int(score_adjustment_text_strip) except ValueError: logger.warning( f'Could not parse Score Report > score adjustment: {score_adjustment_text!r}" as valid adjustment' ) current_rp_image = self.REGIONS["current_rp"].extract_one(y) current_rp_text = imageops.tesser_ocr(current_rp_image, engine=imageops.tesseract_lstm, invert=True, scale=1) current_rp_text_strip = textops.strip_string(current_rp_text, alphabet=string.digits + "RP").replace("RP", "") try: score_report.current_rp = int(current_rp_text_strip) except ValueError: logger.warning( f'Could not parse Score Report > current RP: {current_rp_text!r}" as valid RP' ) return score_report
def process(self, frame: Frame): y = cv2.cvtColor(frame.image, cv2.COLOR_BGR2YUV)[:, :, 0] # The text moves depending on normal or elite queue # Look for the "head" template showing players alive head_region = np.max(self.REGIONS["head_region"].extract_one( frame.image), axis=2) _, head_thresh = cv2.threshold(head_region, 200, 255, cv2.THRESH_BINARY) head_match = cv2.matchTemplate(head_thresh, self.HEAD_TEMPLATE, cv2.TM_CCORR_NORMED) mnv, mxv, mnl, mxl = cv2.minMaxLoc(head_match) frame.apex.match_status_match = round(float(mxv), 2) if mxv < 0.9: return False badge_image = self.REGIONS["rank_badge"].extract_one(frame.image) # cv2.imshow('rank_badge_image', badge_image) # print(rank_badge_matches) # 90 for unranked, 15 for ranked has_badge = mxl[0] < 30 mode = None if has_badge: mode_badge_matches = self._parse_badge(badge_image, self.MODE_TEMPLATES) if mode_badge_matches[0] < 750: mode = "duos" squads_left_text = self._parse_squads_left_text(y, has_badge) squads_left = self._get_squads_left(squads_left_text, mode) if not squads_left: mode = "solos" solos_players_left = self._get_squads_left(squads_left_text, mode) else: solos_players_left = None if not mode and has_badge: mode = "ranked" if mode == "ranked": rank_badge_matches = self._parse_badge(badge_image, self.RANK_TEMPLATES) rank_text_image = self.REGIONS["rank_text"].extract_one( frame.image_yuv[:, :, 0]) rank_text = imageops.tesser_ocr( rank_text_image, whitelist="IV", scale=3, invert=True, engine=imageops.tesseract_only, ) rp_text_image = self.REGIONS["ranked_rp"].extract_one( frame.image_yuv[:, :, 0]) rp_text = imageops.tesser_ocr( rp_text_image, whitelist=string.digits + "+-RP", scale=3, invert=True, engine=imageops.tesseract_only, ) else: rank_badge_matches = None rank_text = None rp_text = None frame.apex.match_status = MatchStatus( squads_left=squads_left, players_alive=self._get_players_alive(y, has_badge) if squads_left and squads_left > 4 else None, kills=self._get_kills(y, mode), ranked=mode == "ranked", rank_badge_matches=rank_badge_matches, rank_text=rank_text, rp_text=rp_text, solos_players_left=solos_players_left, mode=mode, ) self.REGIONS.draw(frame.debug_image) _draw_status(frame.debug_image, frame.apex.match_status) return True
def process(self, frame: Frame) -> bool: result_y = self.REGIONS["result"].extract_one(frame.image_yuv[:, :, 0]) _, result_thresh = cv2.threshold(result_y, 220, 255, cv2.THRESH_BINARY) match, result = imageops.match_templates( result_thresh, self.RESULTS, cv2.TM_CCORR_NORMED, required_match=self.RESULT_TEMPLATE_REQUIRED_MATCH, previous_match_context=(self.__class__.__name__, "result"), ) if match > self.RESULT_TEMPLATE_REQUIRED_MATCH: logger.debug(f"Round result is {result} with match={match}") score_ims = self.REGIONS["scores"].extract(frame.image) score_gray = [ imageops.normalise(np.max(im, axis=2)) for im in score_ims ] scores = imageops.tesser_ocr_all( score_gray, expected_type=int, engine=din_next_regular_digits, invert=True, ) logger.debug(f"Round score is {scores}") frame.valorant.postgame = Postgame( victory=result == "victory", score=(scores[0], scores[1]), map=imageops.ocr_region(frame, self.REGIONS, "map"), game_mode=imageops.ocr_region(frame, self.REGIONS, "game_mode"), image=lazy_upload("postgame", self.REGIONS.blank_out(frame.image), frame.timestamp), ) draw_postgame(frame.debug_image, frame.valorant.postgame) sort_mode_gray = np.min( self.SCOREBOARD_REGIONS["scoreboard_sort_mode"].extract_one( frame.image), axis=2) sort_mode_filt = 255 - imageops.normalise(sort_mode_gray, bottom=75) # cv2.imshow('sort_mode_gray', sort_mode_gray) sort_mode = imageops.tesser_ocr(sort_mode_filt, engine=imageops.tesseract_lstm) sort_mode_match = max([ levenshtein.ratio( textops.strip_string(sort_mode).upper(), expected) for expected in self.SCOREBOARD_SORT_MODES ]) logger.debug( f"Got scoreboard sort mode: {sort_mode!r} match={sort_mode_match:.2f}" ) if sort_mode_match > 0.75: frame.valorant.scoreboard = self._parse_scoreboard(frame) draw_scoreboard(frame.debug_image, frame.valorant.scoreboard) return True return False
def _parse_scoreboard(self, frame: Frame) -> Scoreboard: agent_images = self.SCOREBOARD_REGIONS["agents"].extract(frame.image) name_images = self.SCOREBOARD_REGIONS["names"].extract(frame.image) stat_images = self.SCOREBOARD_REGIONS["stats"].extract(frame.image) stat_images_filt = [ self._filter_statrow_image(im) for im in stat_images ] stat_image_rows = [ stat_images_filt[r * 8:(r + 1) * 8] for r in range(10) ] # cv2.imshow( # 'stats', # np.vstack([ # np.hstack([self._filter_statrow_image(n)] + r) # for n, r in zip(name_images, stat_image_rows) # ]) # ) stats = [] for i, (agent_im, name_im, stat_row) in enumerate( zip(agent_images, name_images, stat_image_rows)): agent_match, agent = imageops.match_templates( agent_im, self.AGENT_TEMPLATES, method=cv2.TM_SQDIFF, required_match=self.AGENT_TEMPLATE_REQUIRED_MATCH, use_masks=True, previous_match_context=(self.__class__.__name__, "scoreboard", "agent", i), ) if agent_match > self.AGENT_TEMPLATE_REQUIRED_MATCH: agent = None row_bg = name_im[np.max(name_im, axis=2) < 200] row_color = np.median(row_bg, axis=0).astype(np.int) # cv2.imshow('name', self._filter_statrow_image(name_im)) # cv2.waitKey(0) stat = PlayerStats( agent, imageops.tesser_ocr( self._filter_statrow_image(name_im), engine=imageops.tesseract_lstm, ), row_color[0] > row_color[2], *imageops.tesser_ocr_all( stat_row, expected_type=int, engine=din_next_regular_digits, ), ) stats.append(stat) logger.debug( f"Got player stats: {stat} - agent match={agent_match:.2f}, row colour={tuple(row_color)}" ) return Scoreboard( stats, image=lazy_upload("scoreboard", self.SCOREBOARD_REGIONS.blank_out(frame.image), frame.timestamp), )
def _process_player_stats(self, y: np.ndarray, duos: bool = False, shunt: int = 0) -> Tuple[PlayerStats, ...]: name_images = self.REGIONS["names"].shunt(x=shunt).extract(y) names = [] for im in name_images: # self._mask_components_touching_edges(im) im = 255 - cv2.bitwise_and( im, cv2.dilate( cv2.threshold(im, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1], None, ), ) im = cv2.resize(im, (0, 0), fx=2, fy=2) im = cv2.GaussianBlur(im, (0, 0), 1) name = imageops.tesser_ocr( im, engine=imageops.tesseract_lstm, ).replace(" ", "") match = np.mean(imageops.tesseract_lstm.AllWordConfidences()) logger.info(f"Got name {name!r} ~ {match:1.2f}") if match < 0.75: name = imageops.tesser_ocr( im, engine=imageops.tesseract_only, ) logger.info(f"Using {name!r} instead") names.append(name) stat_images = self.REGIONS["stats"].shunt(x=shunt).extract(y) # for im in stat_images: # self._mask_components_touching_edges(im) stats = imageops.tesser_ocr_all( stat_images, engine=ocr.tesseract_ttlakes_digits_specials, ) for i in range(len(stats)): value = stats[i] logger.debug(f"Got stat {i}: {value!r}") if value: value = value.lower().replace(" ", "") for c1, c2 in "l1", "i1", "o0", (":", ""): value = value.replace(c1, c2) value = textops.strip_string(value, string.digits + "/") if i < 3: try: stats[i] = tuple([int(v) for v in value.split("/")]) except ValueError as e: logger.warning(f'Could not parse {value!r} as 3 ints" {e}') stats[i] = None elif 6 <= i <= 8: # survival time if stats[i] is not None: try: seconds = int(value) except ValueError as e: logger.warning( f'Could not parse "{stats[i]}" as int: {e}') seconds = None else: seconds = mmss_to_seconds(seconds) logger.info(f"MM:SS {stats[i]} -> {seconds}") stats[i] = seconds else: try: stats[i] = int(value) except ValueError as e: logger.warning(f'Could not parse {value!r} as int" {e}') stats[i] = None # typing: ignore # noinspection PyTypeChecker count = 3 if not duos else 2 r = tuple([PlayerStats(names[i], *stats[i::3]) for i in range(count)]) for s in r: if not s.kills: pass elif len(s.kills) == 3: s.assists = s.kills[1] s.knocks = s.kills[2] s.kills = s.kills[0] else: s.kills = s.kills[0] logger.info(f"Got {pprint.pformat(r)}") return r
def update(_: int) -> None: scale = max(1, cv2.getTrackbarPos("scale", "ocr")) if scale < 5: scale = 1 / (5 - scale) else: scale = scale - 3 blur = max(0, cv2.getTrackbarPos("blur", "ocr") / 10) invert = cv2.getTrackbarPos("invert", "ocr") print(scale, blur, invert) table = [] if "engine" not in kwargs: for name, engine in [ ("tesseract_lstm", imageops.tesseract_lstm), ("tesseract_futura", imageops.tesseract_futura), ("tesseract_only", imageops.tesseract_only), ("tesseract_ttlakes_digits", overtrack_cv.games.apex.ocr.tesseract_ttlakes_digits), ("tesseract_ttlakes", overtrack_cv.games.apex.ocr.tesseract_ttlakes), ("tesseract_ttlakes_medium", overtrack_cv.games.apex.ocr.tesseract_ttlakes_medium), ("tesseract_arame", overtrack_cv.games.apex.ocr.tesseract_arame), ("tesseract_mensura", overtrack_cv.games.apex.ocr.tesseract_mensura), ( "tesseract_ttlakes_digits_specials", overtrack_cv.games.apex.ocr. tesseract_ttlakes_digits_specials, ), ( "tesseract_ttlakes_bold_digits_specials", overtrack_cv.games.apex.ocr. tesseract_ttlakes_bold_digits_specials, ), ]: imageops.tesser_ocr(im, scale=scale, blur=blur, invert=bool(invert), engine=engine, **kwargs) table.append( (name, engine.GetUTF8Text(), engine.AllWordConfidences())) else: engine = kwargs["engine"] imageops.tesser_ocr( im, scale=scale, blur=blur, invert=bool(invert), engine=engine, **{k: v for (k, v) in kwargs.items() if k != "engine"}, ) table.append( ("", engine.GetUTF8Text(), engine.AllWordConfidences())) import tabulate print(tabulate.tabulate(table)) print()
def process(self, frame: Frame): y = frame.image_yuv[:, :, 0] your_squad_image = self.REGIONS["your_squad"].extract_one(y) t, thresh = cv2.threshold(your_squad_image, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) match, key = imageops.match_templates(thresh, self.TEMPLATES, cv2.TM_CCORR_NORMED, self.REQUIRED_MATCH) frame.apex.your_squad_match = round(match, 4) if match < self.REQUIRED_MATCH: return False name1_trios = np.min(self.REGIONS["names"].extract_one(frame.image), axis=2) name1_duos = np.min(self.REGIONS["names_duos"].extract_one(frame.image), axis=2) # name1_thresh_value = max(np.max(name1_duos), np.max(name1_trios)) * 0.95 name1_thresh_value = 240 # logger.debug(f"Name thresh: {name1_thresh_value}") name1_trios_score = int(np.sum(name1_trios > name1_thresh_value)) name1_duos_score = int(np.sum(name1_duos > name1_thresh_value)) logger.debug(f"Trios name score: {name1_trios_score} vs duos name score: {name1_duos_score}") # self.duos = name1_duos_score and name1_duos_score > name1_trios_score self.duos = name1_trios_score < 100 logger.info(f"Using duos={self.duos}") if key == "your_squad": names_region_name = "names_duos" if self.duos else "names" names = imageops.tesser_ocr_all( self.REGIONS[names_region_name].extract(y), engine=imageops.tesseract_lstm, invert=True, ) frame.apex.your_squad = YourSquad( tuple(self._to_name(n) for n in names), mode="duos" if self.duos else None, images=lazy_upload( "your_squad", np.hstack(self.REGIONS[names_region_name].extract(frame.image)), frame.timestamp, ), ) self.REGIONS.draw(frame.debug_image) _draw_squad(frame.debug_image, frame.apex.your_squad) elif key == "your_selection": frame.apex.your_selection = YourSelection( name=self._to_name( imageops.tesser_ocr( self.REGIONS["names"].extract(y)[1], engine=imageops.tesseract_lstm, invert=True, ) ), image=lazy_upload( "your_selection", self.REGIONS["names"].extract(frame.image)[1], frame.timestamp, ), ) self.REGIONS.draw(frame.debug_image) _draw_squad(frame.debug_image, frame.apex.your_selection) elif key == "champion_squad": names_region_name = "names_duos" if self.duos else "names" names = imageops.tesser_ocr_all( self.REGIONS[names_region_name].extract(y), engine=imageops.tesseract_lstm, invert=True, ) frame.apex.champion_squad = ChampionSquad( tuple(self._to_name(n) for n in names), mode="duos" if self.duos else None, images=lazy_upload( "champion_squad", np.hstack(self.REGIONS[names_region_name].extract(frame.image)), frame.timestamp, ), ) self.REGIONS.draw(frame.debug_image) _draw_squad(frame.debug_image, frame.apex.champion_squad) return True