def process(self, frame: Frame) -> bool:
        im = self.REGIONS["assemble_your_team"].extract_one(frame.image)
        thresh = cv2.inRange(cv2.cvtColor(im, cv2.COLOR_BGR2HSV_FULL),
                             self.ASSEMBLE_HSV_RANGE[0],
                             self.ASSEMBLE_HSV_RANGE[1])
        match = np.max(
            cv2.matchTemplate(thresh, self.ASSEMBLE_TEMPLATE,
                              cv2.TM_CCORR_NORMED))

        frame.overwatch.assemble_your_team_match = round(float(match), 5)
        if match > self.ASSEMBLE_THRESH:
            map_image = self.REGIONS["map"].extract_one(frame.image)
            map_thresh = imageops.otsu_thresh_lb_fraction(
                np.min(map_image, axis=2), 1.1)
            map_text = big_noodle.ocr(map_thresh, channel=None, threshold=None)

            mode_image = self.REGIONS["mode"].extract_one(frame.image_yuv[:, :,
                                                                          0])
            mode_thresh = cv2.adaptiveThreshold(mode_image, 255,
                                                cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                                                cv2.THRESH_BINARY, 23, -10)
            mode_text = big_noodle.ocr(mode_thresh,
                                       channel=None,
                                       threshold=None)

            name_images = self.REGIONS["blue_names"].extract(frame.image)
            blue_thresh = [
                cv2.inRange(i, (200, 145, 0), (255, 255, 130))
                for i in name_images
            ]
            green_thresh = [
                cv2.inRange(i, (0, 220, 200), (100, 255, 255))
                for i in name_images
            ]
            name_thresh = [
                cv2.bitwise_or(i1, i2)
                for i1, i2 in zip(blue_thresh, green_thresh)
            ]
            names = [
                big_noodle.ocr(
                    i[:, :, 1],
                    channel=None,
                    threshold=t,
                    # debug=True
                ) for i, t in zip(name_images, name_thresh)
            ]

            frame.overwatch.assemble_your_team = AssembleYourTeam(
                map=map_text,
                mode=mode_text,
                blue_names=names,
                is_in_queue=self.detect_in_queue(frame),
            )
            return True

        return False
 def _parse_state(self, frame: Frame) -> str:
     map_info_image = self.REGIONS["potg_eliminted_deathspec"].extract_one(frame.image)
     yellow_text = cv2.inRange(
         cv2.cvtColor(map_info_image, cv2.COLOR_BGR2HSV_FULL),
         ((35 / 360) * 255, 0.5 * 255, 0.8 * 255),
         ((55 / 360) * 255, 1.0 * 255, 1.0 * 255),
     )
     p = np.sum(yellow_text > 0) / np.prod(yellow_text.shape)
     state = ""
     if 0.05 < p < 0.4:
         state_text = big_noodle.ocr(yellow_text, channel=None)
         if state_text and len(state_text) > 5:
             state_text_matches = textops.matches(state_text, self.STATES)
             match_i: int = arrayops.argmin(state_text_matches)
             match = state_text_matches[match_i]
             if match < 7:
                 state = self.STATES[match_i]
                 logger.info(
                     f"Got state={state_text!r} (text fill: {p*100:.0f}%) -> best match: {state!r} (match={match})"
                 )
             else:
                 logger.warning(
                     f'Got state={state_text!r}, but this was not recognized as a valid state (closest was "{self.STATES[match_i]}", match={match})'
                 )
     return state
    def parse_score_screen(self, frame: Frame) -> Optional[ScoreScreen]:
        score_ims = self.REGIONS["score"].extract(frame.image)
        try:
            blue_score, red_score = big_noodle.ocr_all_int(score_ims,
                                                           height=212)
        except ValueError as ve:
            logger.warning(f"Failed to parse scores: {ve}")
            return None

        logger.debug(f"Got score {blue_score} / {red_score}")

        # manual thresholding
        im = self.REGIONS["round_text"].extract_one(frame.image)
        # debugops.manual_thresh_otsu(im)
        # im = np.min(im, axis=2)
        # _, thresh = cv2.threshold(im, imageops.otsu_thresh(im, 200, 255), 255, cv2.THRESH_BINARY)
        # round_text = big_noodle.ocr(thresh, threshold=None, height=70, debug=True)
        round_text = big_noodle.ocr(im,
                                    channel="min",
                                    threshold="otsu_above_mean",
                                    height=72,
                                    debug=False)

        round_number = None
        match = self.ROUND_N_COMPLETE.match(round_text)
        if match:
            round_number = int(match.group(1).replace("O", "0"))
            logger.debug(
                f"Got round {round_number} from round string {round_text!r}")
        else:
            logger.warning(
                f"Could not parse round from round string {round_text!r}")

        return ScoreScreen(blue_score, red_score, round_number)
    def parse_final_score(self, frame: Frame) -> Optional[FinalScore]:
        score_ims = self.REGIONS["final_score"].extract(frame.image)
        score_ims = [
            imageops.otsu_thresh_lb_fraction(im, 1.4) for im in score_ims
        ]
        try:
            blue_score, red_score = big_noodle.ocr_all_int(score_ims,
                                                           channel=None,
                                                           threshold=None,
                                                           height=127)
        except ValueError as ve:
            logger.warning(f"Failed to parse final score: {ve}")
            return None

        logger.debug(f"Got final score {blue_score} / {red_score}")

        im = cv2.cvtColor(
            self.REGIONS["final_result_text"].extract_one(frame.image),
            cv2.COLOR_BGR2HSV_FULL)
        thresh = cv2.inRange(im, (0, 0, 240), (255, 255, 255))
        result_text = big_noodle.ocr(thresh,
                                     channel=None,
                                     threshold=None,
                                     height=120,
                                     debug=False)
        matches = textops.matches(result_text, self.RESULTS)
        result: Optional[str]
        if np.min(matches) > 2:
            if blue_score is not None and red_score is not None:
                if blue_score > red_score:
                    result = "VICTORY"
                elif red_score > blue_score:
                    result = "DEFEAT"
                else:
                    result = "DRAW"
                logger.warning(
                    f"Could not identify result from {result_text!r} (match={np.min(matches)}) - "
                    f"using score {blue_score}, {red_score} to infer result={result}"
                )
            else:
                logger.warning(
                    f"Could not identify result from {result_text!r} (match={np.min(matches)}) and did not parse scores"
                )
                result = None
        else:
            result = self.RESULTS[arrayops.argmin(matches)]
            logger.debug(f"Got result {result} from {result_text!r}")

        return FinalScore(blue_score, red_score, result)
    def process(self, frame: Frame) -> bool:
        if not self.detect_tab(frame):
            return False

        player_name_image, player_hero_image = self.REGIONS[
            "player_hero"].extract(frame.image)
        images = NameImages(
            blue_team=self._mask_roles_out(self.REGIONS["blue_names"].extract(
                frame.image)),
            red_team=self.REGIONS["red_names"].extract(frame.image),
            ult_images=self.REGIONS["ults"].extract(frame.image),
            player_name_image=player_name_image,
            player_hero_image=player_hero_image,
            hero_icons_red=self.REGIONS["hero_icons_red"].extract(frame.image),
            hero_icons_blue=self.REGIONS["hero_icons_blue"].extract(
                frame.image),
        )

        player_hero_text = big_noodle.ocr(player_hero_image)
        hero = textops.best_match(
            player_hero_text.lower(),
            [h[1].key for h in data.heroes.items()],
            list(data.heroes.keys()),
            threshold=3,
        )

        heroes_played = self.parse_heroes(images)
        map_text, mode_text = self.parse_map_info(frame)

        stats: Optional[Stats]
        if hero:
            stat_values = []
            for i, im in enumerate(self.REGIONS["stats"].extract(frame.image)):
                masked = self._filter_digit_components(im)
                stat_values.append(digit.ocr(masked, 1.0))

            stat_names_row_1 = [s.name for s in data.generic_stats[:3]]
            stat_names_row_2 = [s.name for s in data.generic_stats[3:]]
            hero_stat_names_row_1 = [
                s.name for s in data.heroes[hero].stats[0]
            ]
            hero_stat_names_row_2 = [
                s.name for s in data.heroes[hero].stats[1]
            ]
            stat_names = stat_names_row_1 + hero_stat_names_row_1 + stat_names_row_2 + hero_stat_names_row_2

            stat_parsed: Dict[str, Optional[int]] = dict(
                zip(stat_names, stat_values))

            if stat_parsed["objective time"] is not None:
                stat_parsed["objective time"] = textops.mmss_to_seconds(
                    stat_parsed["objective time"])
                logger.debug(
                    f'Transformed MMSS objective time to {stat_parsed["objective time"]}'
                )

            stats = Stats(
                hero,
                eliminations=stat_parsed["eliminations"],
                objective_kills=stat_parsed["objective kills"],
                objective_time=stat_parsed["objective time"],
                hero_damage_done=stat_parsed["hero damage done"],
                healing_done=stat_parsed["healing done"],
                deaths=stat_parsed["deaths"],
                hero_specific_stats={
                    s.name: stat_parsed[s.name]
                    for s in itertools.chain.from_iterable(
                        data.heroes[hero].stats)
                },
            )
            logger.info(f"Parsed stats as {stats}")

        else:
            logger.warning(f"Could not recognise {player_hero_text} as a hero")
            stats = None

        frame.overwatch.tab_screen = TabScreen(
            map=map_text,
            mode=mode_text,
            blue_team=big_noodle.ocr_all(images.blue_team, channel="max"),
            blue_team_hero=heroes_played[6:12],
            blue_team_ults=[0 for _ in range(6)],
            red_team=big_noodle.ocr_all(images.red_team, channel="r"),
            red_team_hero=heroes_played[:6],
            player_name=big_noodle.ocr(player_name_image),
            player_hero=hero,
            stats=stats,
        )
        _draw_tab_screen(frame.debug_image, frame.overwatch.tab_screen)

        return True