diff --git a/assets/jp/combat/GET_ITEMS_1.png b/assets/jp/combat/GET_ITEMS_1.png index b321db895..1c68ab63c 100644 Binary files a/assets/jp/combat/GET_ITEMS_1.png and b/assets/jp/combat/GET_ITEMS_1.png differ diff --git a/assets/jp/combat/GET_ITEMS_2.png b/assets/jp/combat/GET_ITEMS_2.png index d69a8e86f..91d02538b 100644 Binary files a/assets/jp/combat/GET_ITEMS_2.png and b/assets/jp/combat/GET_ITEMS_2.png differ diff --git a/assets/jp/combat/GET_ITEMS_3.png b/assets/jp/combat/GET_ITEMS_3.png index d69a8e86f..8748a7e5b 100644 Binary files a/assets/jp/combat/GET_ITEMS_3.png and b/assets/jp/combat/GET_ITEMS_3.png differ diff --git a/campaign/Readme.md b/campaign/Readme.md index 6b9b05042..eb7caa365 100644 --- a/campaign/Readme.md +++ b/campaign/Readme.md @@ -301,3 +301,4 @@ To add a new event, add a new row in here, and run `python -m module.config.conf | 20260528 | event 20220818 cn | Operation Convergence | - | - | - | 復刻遠匯點作戰 | | 20260605 | event 20260520 cn | Alliance Before the Hagiobull | - | - | - | 聖印前的同盟 | | 20260618 | event 20240521 cn | Light of the Martyrium Rerun | 复刻绽放于辉光之城 | Light of the Martyrium Rerun | 赫輝のマルティリウム(復刻) | - | +| 20260625 | event 20230223 cn | Revelations of Dust | - | - | - | 復刻湮燼塵墟 | diff --git a/module/combat/assets.py b/module/combat/assets.py index 33128635d..f1bc48ea8 100644 --- a/module/combat/assets.py +++ b/module/combat/assets.py @@ -29,10 +29,10 @@ EXP_INFO_B = Button(area={'cn': (332, 107, 387, 118), 'en': (332, 107, 387, 118) EXP_INFO_C = Button(area={'cn': (332, 56, 345, 107), 'en': (332, 56, 345, 107), 'jp': (332, 56, 345, 107), 'tw': (332, 56, 345, 107)}, color={'cn': (198, 208, 198), 'en': (198, 208, 198), 'jp': (198, 208, 198), 'tw': (198, 208, 198)}, button={'cn': (1133, 634, 1262, 650), 'en': (1133, 634, 1262, 650), 'jp': (1133, 634, 1262, 650), 'tw': (1133, 634, 1262, 650)}, file={'cn': './assets/cn/combat/EXP_INFO_C.png', 'en': './assets/en/combat/EXP_INFO_C.png', 'jp': './assets/jp/combat/EXP_INFO_C.png', 'tw': './assets/tw/combat/EXP_INFO_C.png'}) EXP_INFO_D = Button(area={'cn': (328, 45, 341, 119), 'en': (328, 45, 341, 119), 'jp': (328, 45, 341, 119), 'tw': (328, 45, 341, 119)}, color={'cn': (199, 208, 199), 'en': (199, 208, 199), 'jp': (199, 208, 199), 'tw': (199, 208, 199)}, button={'cn': (1133, 634, 1262, 650), 'en': (1133, 634, 1262, 650), 'jp': (1133, 634, 1262, 650), 'tw': (1133, 634, 1262, 650)}, file={'cn': './assets/cn/combat/EXP_INFO_D.png', 'en': './assets/en/combat/EXP_INFO_D.png', 'jp': './assets/jp/combat/EXP_INFO_D.png', 'tw': './assets/tw/combat/EXP_INFO_D.png'}) EXP_INFO_S = Button(area={'cn': (342, 107, 389, 119), 'en': (342, 107, 389, 119), 'jp': (342, 107, 389, 119), 'tw': (342, 107, 389, 119)}, color={'cn': (233, 242, 127), 'en': (233, 242, 127), 'jp': (233, 242, 127), 'tw': (233, 242, 127)}, button={'cn': (1133, 634, 1262, 650), 'en': (1133, 634, 1262, 650), 'jp': (1133, 634, 1262, 650), 'tw': (1133, 634, 1262, 650)}, file={'cn': './assets/cn/combat/EXP_INFO_S.png', 'en': './assets/en/combat/EXP_INFO_S.png', 'jp': './assets/jp/combat/EXP_INFO_S.png', 'tw': './assets/tw/combat/EXP_INFO_S.png'}) -GET_ITEMS_1 = Button(area={'cn': (538, 217, 741, 253), 'en': (551, 223, 736, 250), 'jp': (548, 217, 741, 253), 'tw': (539, 217, 742, 253)}, color={'cn': (160, 192, 248), 'en': (166, 194, 235), 'jp': (144, 183, 250), 'tw': (155, 190, 248)}, button={'cn': (1000, 631, 1055, 689), 'en': (999, 630, 1047, 691), 'jp': (1000, 631, 1055, 689), 'tw': (1000, 631, 1055, 689)}, file={'cn': './assets/cn/combat/GET_ITEMS_1.png', 'en': './assets/en/combat/GET_ITEMS_1.png', 'jp': './assets/jp/combat/GET_ITEMS_1.png', 'tw': './assets/tw/combat/GET_ITEMS_1.png'}) +GET_ITEMS_1 = Button(area={'cn': (538, 217, 741, 253), 'en': (551, 223, 736, 250), 'jp': (539, 220, 741, 252), 'tw': (539, 217, 742, 253)}, color={'cn': (160, 192, 248), 'en': (166, 194, 235), 'jp': (146, 184, 249), 'tw': (155, 190, 248)}, button={'cn': (1000, 631, 1055, 689), 'en': (999, 630, 1047, 691), 'jp': (1000, 631, 1055, 689), 'tw': (1000, 631, 1055, 689)}, file={'cn': './assets/cn/combat/GET_ITEMS_1.png', 'en': './assets/en/combat/GET_ITEMS_1.png', 'jp': './assets/jp/combat/GET_ITEMS_1.png', 'tw': './assets/tw/combat/GET_ITEMS_1.png'}) GET_ITEMS_1_RYZA = Button(area={'cn': (564, 217, 721, 245), 'en': (577, 211, 704, 239), 'jp': (566, 217, 719, 244), 'tw': (564, 218, 723, 246)}, color={'cn': (176, 199, 243), 'en': (172, 199, 246), 'jp': (179, 201, 243), 'tw': (173, 197, 242)}, button={'cn': (1000, 631, 1055, 689), 'en': (1000, 631, 1055, 689), 'jp': (1000, 631, 1055, 689), 'tw': (1000, 631, 1055, 689)}, file={'cn': './assets/cn/combat/GET_ITEMS_1_RYZA.png', 'en': './assets/en/combat/GET_ITEMS_1_RYZA.png', 'jp': './assets/jp/combat/GET_ITEMS_1_RYZA.png', 'tw': './assets/tw/combat/GET_ITEMS_1_RYZA.png'}) -GET_ITEMS_2 = Button(area={'cn': (538, 146, 742, 182), 'en': (551, 149, 735, 175), 'jp': (547, 143, 742, 179), 'tw': (538, 148, 741, 182)}, color={'cn': (160, 192, 248), 'en': (167, 195, 235), 'jp': (145, 183, 250), 'tw': (155, 190, 248)}, button={'cn': (1000, 631, 1055, 689), 'en': (999, 630, 1047, 691), 'jp': (1000, 631, 1055, 689), 'tw': (1000, 631, 1055, 689)}, file={'cn': './assets/cn/combat/GET_ITEMS_2.png', 'en': './assets/en/combat/GET_ITEMS_2.png', 'jp': './assets/jp/combat/GET_ITEMS_2.png', 'tw': './assets/tw/combat/GET_ITEMS_2.png'}) -GET_ITEMS_3 = Button(area={'cn': (539, 143, 742, 179), 'en': (548, 136, 740, 172), 'jp': (547, 143, 742, 179), 'tw': (546, 145, 742, 178)}, color={'cn': (161, 193, 248), 'en': (152, 185, 237), 'jp': (145, 183, 250), 'tw': (156, 190, 248)}, button={'cn': (1000, 631, 1055, 689), 'en': (999, 630, 1047, 691), 'jp': (1000, 631, 1055, 689), 'tw': (1000, 631, 1055, 689)}, file={'cn': './assets/cn/combat/GET_ITEMS_3.png', 'en': './assets/en/combat/GET_ITEMS_3.png', 'jp': './assets/jp/combat/GET_ITEMS_3.png', 'tw': './assets/tw/combat/GET_ITEMS_3.png'}) +GET_ITEMS_2 = Button(area={'cn': (538, 146, 742, 182), 'en': (551, 149, 735, 175), 'jp': (536, 146, 741, 182), 'tw': (538, 148, 741, 182)}, color={'cn': (160, 192, 248), 'en': (167, 195, 235), 'jp': (145, 182, 249), 'tw': (155, 190, 248)}, button={'cn': (1000, 631, 1055, 689), 'en': (999, 630, 1047, 691), 'jp': (1000, 631, 1055, 689), 'tw': (1000, 631, 1055, 689)}, file={'cn': './assets/cn/combat/GET_ITEMS_2.png', 'en': './assets/en/combat/GET_ITEMS_2.png', 'jp': './assets/jp/combat/GET_ITEMS_2.png', 'tw': './assets/tw/combat/GET_ITEMS_2.png'}) +GET_ITEMS_3 = Button(area={'cn': (539, 143, 742, 179), 'en': (548, 136, 740, 172), 'jp': (540, 143, 742, 179), 'tw': (546, 145, 742, 178)}, color={'cn': (161, 193, 248), 'en': (152, 185, 237), 'jp': (145, 182, 248), 'tw': (156, 190, 248)}, button={'cn': (1000, 631, 1055, 689), 'en': (999, 630, 1047, 691), 'jp': (1000, 631, 1055, 689), 'tw': (1000, 631, 1055, 689)}, file={'cn': './assets/cn/combat/GET_ITEMS_3.png', 'en': './assets/en/combat/GET_ITEMS_3.png', 'jp': './assets/jp/combat/GET_ITEMS_3.png', 'tw': './assets/tw/combat/GET_ITEMS_3.png'}) GET_ITEMS_3_CHECK = Button(area={'cn': (335, 184, 947, 203), 'en': (335, 184, 947, 203), 'jp': (335, 184, 947, 203), 'tw': (335, 184, 947, 203)}, color={'cn': (84, 95, 109), 'en': (84, 95, 109), 'jp': (84, 95, 109), 'tw': (84, 95, 109)}, button={'cn': (335, 184, 947, 203), 'en': (335, 184, 947, 203), 'jp': (335, 184, 947, 203), 'tw': (335, 184, 947, 203)}, file={'cn': './assets/cn/combat/GET_ITEMS_3_CHECK.png', 'en': './assets/en/combat/GET_ITEMS_3_CHECK.png', 'jp': './assets/jp/combat/GET_ITEMS_3_CHECK.png', 'tw': './assets/tw/combat/GET_ITEMS_3_CHECK.png'}) GET_SHIP = Button(area={'cn': (1104, 610, 1110, 630), 'en': (1104, 610, 1110, 630), 'jp': (1104, 610, 1110, 630), 'tw': (1104, 610, 1110, 630)}, color={'cn': (255, 255, 255), 'en': (255, 255, 255), 'jp': (255, 255, 255), 'tw': (255, 255, 255)}, button={'cn': (1000, 631, 1055, 689), 'en': (999, 630, 1047, 691), 'jp': (1000, 631, 1055, 689), 'tw': (1000, 631, 1055, 689)}, file={'cn': './assets/cn/combat/GET_SHIP.png', 'en': './assets/en/combat/GET_SHIP.png', 'jp': './assets/jp/combat/GET_SHIP.png', 'tw': './assets/tw/combat/GET_SHIP.png'}) LOADING_BAR = Button(area={'cn': (33, 676, 1247, 680), 'en': (33, 676, 1247, 680), 'jp': (33, 676, 1247, 680), 'tw': (33, 676, 1247, 680)}, color={'cn': (172, 205, 232), 'en': (172, 205, 232), 'jp': (172, 205, 232), 'tw': (172, 205, 232)}, button={'cn': (33, 676, 1247, 680), 'en': (33, 676, 1247, 680), 'jp': (33, 676, 1247, 680), 'tw': (33, 676, 1247, 680)}, file={'cn': './assets/cn/combat/LOADING_BAR.png', 'en': './assets/en/combat/LOADING_BAR.png', 'jp': './assets/jp/combat/LOADING_BAR.png', 'tw': './assets/tw/combat/LOADING_BAR.png'}) diff --git a/module/commission/project.py b/module/commission/project.py index afdc8c809..cd88c1628 100644 --- a/module/commission/project.py +++ b/module/commission/project.py @@ -1,6 +1,5 @@ from datetime import datetime, timedelta -import module.config.server as server from module.base.decorator import Config from module.base.filter import Filter from module.base.utils import * @@ -23,20 +22,54 @@ COMMISSION_FILTER = Filter( ) -class SuffixOcr(Ocr): - def pre_process(self, image): - image = super().pre_process(image) +def crop_suffix_image(image, area): + """ + Args: + image (np.ndarray): + area (tuple): Commission name area. - left = np.where(np.min(image[5:-5, :], axis=0) < 85)[0] - # Look back several pixels - if server.server in ['jp']: - look_back = 21 - else: - look_back = 18 - if len(left): - image = image[:, left[-1] - look_back:] + Returns: + np.ndarray | None: Cropped suffix image, black letters on white background. + """ + name_image = crop(image, area) + name_image = extract_letters(name_image, letter=(255, 255, 255), threshold=128).astype(np.uint8) - return image + line = cv2.reduce(name_image[5:-5, :], 0, cv2.REDUCE_AVG).flatten() + columns = np.where(line < 250)[0] + if not len(columns): + return None + + # Look back several pixels from the rightmost letter to include Roman numerals. + threshold = 250 + look_back = 10 + for i in range(columns[-1], 0, -1): + if line[i] > threshold: + if columns[-1] - i > look_back: + look_back = columns[-1] - i + break + + left = columns[-1] - look_back + right = columns[-1] + 1 + x1, y1 = area[0:2] + suffix_area = area_offset((left - 3, -3, right + 3, name_image.shape[0] + 3), (x1, y1)) + image = crop(image, suffix_area) + image = extract_letters(image, letter=(255, 255, 255), threshold=128).astype(np.uint8) + return image + + +def image_hash(image): + """ + Args: + image (np.ndarray): + + Returns: + str: + """ + if image is None: + return '' + + import hashlib + return hashlib.md5(image.tobytes()).hexdigest() class Commission: @@ -46,10 +79,10 @@ class Commission: name: str # If success to parse commission name valid: bool - # Suffix in roman numerals - # May be wrong if commission does not have a suffix - # Value: ⅠⅡⅢⅤⅣⅥ - suffix: str + # Cropped suffix image, black letters on white background, or None + suffix_image: np.ndarray + # Hash of suffix image, used only for logging, or empty string if suffix_image is None + suffix_hash: str # Genre name in project_data.py # Value: major_comm, daily_resource, urgent_cube, ... genre: str @@ -113,8 +146,8 @@ class Commission: self.genre = self.commission_name_parse(self.name) # Suffix - ocr = SuffixOcr(button, lang='azur_lane', letter=(255, 255, 255), threshold=128, alphabet='IV') - self.suffix = self.beautify_name(ocr.ocr(self.image)) + self.suffix_image = crop_suffix_image(self.image, self.button.area) + self.suffix_hash = image_hash(self.suffix_image) # Duration time area = area_offset((290, 68, 390, 95), self.area[0:2]) @@ -160,8 +193,8 @@ class Commission: self.genre = self.commission_name_parse(self.name) # Suffix - ocr = SuffixOcr(button, lang='azur_lane', letter=(255, 255, 255), threshold=128, alphabet='IV') - self.suffix = self.beautify_name(ocr.ocr(self.image)) + self.suffix_image = crop_suffix_image(self.image, self.button.area) + self.suffix_hash = image_hash(self.suffix_image) # Duration time area = area_offset((290, 68, 390, 95), self.area[0:2]) @@ -209,8 +242,8 @@ class Commission: self.genre = self.commission_name_parse(self.name) # Suffix - ocr = SuffixOcr(button, lang='azur_lane', letter=(255, 255, 255), threshold=128, alphabet='IV') - self.suffix = self.beautify_name(ocr.ocr(self.image)) + self.suffix_image = crop_suffix_image(self.image, self.button.area) + self.suffix_hash = image_hash(self.suffix_image) # Duration time area = area_offset((290, 68, 390, 95), self.area[0:2]) @@ -254,8 +287,8 @@ class Commission: self.genre = self.commission_name_parse(self.name) # Suffix - ocr = SuffixOcr(button, lang='azur_lane', letter=(255, 255, 255), threshold=128, alphabet='IV') - self.suffix = self.beautify_name(ocr.ocr(self.image)) + self.suffix_image = crop_suffix_image(self.image, self.button.area) + self.suffix_hash = image_hash(self.suffix_image) # Duration time area = area_offset((290, 68, 390, 95), self.area[0:2]) @@ -288,7 +321,7 @@ class Commission: self.status = dic[int(np.argmax(color))] def __str__(self): - name = f'{self.name} | {self.suffix}' + name = f'{self.name} | {self.suffix_hash}' if self.suffix_hash else self.name if not self.valid: return f'{name} (Invalid)' info = {'Genre': self.genre, 'Status': self.status, 'Duration': self.duration} @@ -315,7 +348,7 @@ class Commission: if self.genre != other.genre or self.status != other.status: return False if self.category_str == 'daily': - if self.suffix != other.suffix: + if not self.suffix_match(other): return False if self.genre == 'urgent_box': for tag in ['NYB', 'BIW']: @@ -332,7 +365,7 @@ class Commission: return False if self.repeat_count != other.repeat_count: return False - if self.genre in ['extra_oil', 'night_oil'] and self.suffix != other.suffix: + if self.genre in ['extra_oil', 'night_oil'] and not self.suffix_match(other): return False return True @@ -340,6 +373,35 @@ class Commission: def __hash__(self): return hash(f'{self.genre}_{self.name}') + def suffix_match(self, other, similarity=0.75): + """ + Args: + other (Commission): + similarity (float): 0-1. Similarity. + + Returns: + bool: + """ + if self.suffix_image is None and other.suffix_image is None: + return True + if self.suffix_image is None or other.suffix_image is None: + return False + + def match(image, template): + template = crop(template, (3, 3, template.shape[1] - 3, template.shape[0] - 3), copy=False) + if image.shape[0] < template.shape[0] or image.shape[1] < template.shape[1]: + return 0.0 + + res = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED) + _, sim, _, _ = cv2.minMaxLoc(res) + return sim + + sim = max( + match(self.suffix_image, other.suffix_image), + match(other.suffix_image, self.suffix_image) + ) + return sim >= similarity + def parse_time(self, string): """ Args: diff --git a/module/config/argument/args.json b/module/config/argument/args.json index 374ee6097..e0b7497e9 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -1643,8 +1643,8 @@ "type": "select", "value": "campaign_main", "option": [ - "event_20240521_cn", - "event_20260520_cn" + "event_20230223_cn", + "event_20240521_cn" ], "option_cn": [ "event_20240521_cn" @@ -1656,11 +1656,11 @@ "event_20240521_cn" ], "option_tw": [ - "event_20260520_cn" + "event_20230223_cn" ], "option_bold": [ - "event_20240521_cn", - "event_20260520_cn" + "event_20230223_cn", + "event_20240521_cn" ] }, "Mode": { @@ -1925,8 +1925,8 @@ "type": "select", "value": "campaign_main", "option": [ - "event_20240521_cn", - "event_20260520_cn" + "event_20230223_cn", + "event_20240521_cn" ], "option_cn": [ "event_20240521_cn" @@ -1938,11 +1938,11 @@ "event_20240521_cn" ], "option_tw": [ - "event_20260520_cn" + "event_20230223_cn" ], "option_bold": [ - "event_20240521_cn", - "event_20260520_cn" + "event_20230223_cn", + "event_20240521_cn" ] }, "Mode": { @@ -2322,8 +2322,8 @@ "type": "select", "value": "campaign_main", "option": [ - "event_20240521_cn", - "event_20260520_cn" + "event_20230223_cn", + "event_20240521_cn" ], "option_cn": [ "event_20240521_cn" @@ -2335,11 +2335,11 @@ "event_20240521_cn" ], "option_tw": [ - "event_20260520_cn" + "event_20230223_cn" ], "option_bold": [ - "event_20240521_cn", - "event_20260520_cn" + "event_20230223_cn", + "event_20240521_cn" ] }, "Mode": { @@ -4069,8 +4069,8 @@ "type": "select", "value": "campaign_main", "option": [ - "event_20240521_cn", - "event_20260520_cn" + "event_20230223_cn", + "event_20240521_cn" ], "option_cn": [ "event_20240521_cn" @@ -4082,11 +4082,11 @@ "event_20240521_cn" ], "option_tw": [ - "event_20260520_cn" + "event_20230223_cn" ], "option_bold": [ - "event_20240521_cn", - "event_20260520_cn" + "event_20230223_cn", + "event_20240521_cn" ] }, "Mode": { @@ -4483,8 +4483,8 @@ "type": "select", "value": "campaign_main", "option": [ - "event_20240521_cn", - "event_20260520_cn" + "event_20230223_cn", + "event_20240521_cn" ], "option_cn": [ "event_20240521_cn" @@ -4496,11 +4496,11 @@ "event_20240521_cn" ], "option_tw": [ - "event_20260520_cn" + "event_20230223_cn" ], "option_bold": [ - "event_20240521_cn", - "event_20260520_cn" + "event_20230223_cn", + "event_20240521_cn" ] }, "Mode": { @@ -4897,8 +4897,8 @@ "type": "select", "value": "campaign_main", "option": [ - "event_20240521_cn", - "event_20260520_cn" + "event_20230223_cn", + "event_20240521_cn" ], "option_cn": [ "event_20240521_cn" @@ -4910,11 +4910,11 @@ "event_20240521_cn" ], "option_tw": [ - "event_20260520_cn" + "event_20230223_cn" ], "option_bold": [ - "event_20240521_cn", - "event_20260520_cn" + "event_20230223_cn", + "event_20240521_cn" ] }, "Mode": { @@ -5311,8 +5311,8 @@ "type": "select", "value": "campaign_main", "option": [ - "event_20240521_cn", - "event_20260520_cn" + "event_20230223_cn", + "event_20240521_cn" ], "option_cn": [ "event_20240521_cn" @@ -5324,11 +5324,11 @@ "event_20240521_cn" ], "option_tw": [ - "event_20260520_cn" + "event_20230223_cn" ], "option_bold": [ - "event_20240521_cn", - "event_20260520_cn" + "event_20230223_cn", + "event_20240521_cn" ] }, "Mode": { @@ -5715,8 +5715,8 @@ "type": "select", "value": "campaign_main", "option": [ - "event_20240521_cn", - "event_20260520_cn" + "event_20230223_cn", + "event_20240521_cn" ], "option_cn": [ "event_20240521_cn" @@ -5728,11 +5728,11 @@ "event_20240521_cn" ], "option_tw": [ - "event_20260520_cn" + "event_20230223_cn" ], "option_bold": [ - "event_20240521_cn", - "event_20260520_cn" + "event_20230223_cn", + "event_20240521_cn" ] }, "Mode": { diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index 64cedf885..60930b400 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -742,7 +742,7 @@ "event_20220915_cn": "復刻紫絳槿嵐", "event_20221124_cn": "復刻鍊金術士與秘密遺跡群島", "event_20221222_cn": "復刻定向折疊", - "event_20230223_cn": "湮燼塵墟", + "event_20230223_cn": "復刻湮燼塵墟", "event_20230525_cn": "空相交會點", "event_20230803_cn": "奏響鳶尾之歌", "event_20230817_cn": "愚者的天平",