Skip to content

Commit f7761c1

Browse files
authored
Merge pull request #299 from cc004/fix
fix: ocr
2 parents 8ea1e86 + 600b44e commit f7761c1

1 file changed

Lines changed: 210 additions & 46 deletions

File tree

autopcr/util/unit_recognizer.py

Lines changed: 210 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,94 @@ def _icon_crop_with_pad(img: Image.Image, box: List[int], pad: int = 2) -> Image
133133
return img.crop((x + pad, y + pad, x + w - pad, y + h - pad))
134134
return img.crop((x, y, x + w, y + h))
135135

136+
@staticmethod
137+
def _legacy_cutting(img: Image.Image, mode: int):
138+
"""
139+
old_main.py 的 cutting 逻辑迁移版。
140+
mode=1: 返回最大矩形区域裁剪结果与边框
141+
mode=2: 返回头像框候选与被排除的其他框
142+
"""
143+
im_grey = img.convert('L')
144+
tot_area = im_grey.size[0] * im_grey.size[1]
145+
im_grey = im_grey.point(lambda x: 255 if x > 210 else 0)
146+
thresh = np.array(im_grey)
147+
148+
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
149+
150+
areas = []
151+
icon = []
152+
for contour in contours:
153+
area = cv2.contourArea(contour)
154+
areas.append(area)
155+
if area > 500 and mode == 2:
156+
x, y, w, h = cv2.boundingRect(contour)
157+
if h > 0 and 0.95 < (w / h) < 1.05:
158+
side = (w + h) // 2
159+
area_ratio = side * side / tot_area * 100 if tot_area > 0 else 0
160+
if area_ratio >= 0.5:
161+
icon.append([side, [x, y, w, h]])
162+
163+
if mode == 1:
164+
if not areas:
165+
raise ValueError("no contour found in legacy cutting")
166+
idx = areas.index(max(areas))
167+
x, y, w, h = cv2.boundingRect(contours[idx])
168+
cropped = thresh[y + 2:y + h - 2, x + 2:x + w - 2]
169+
return Image.fromarray(cropped), [x, y, w, h]
170+
171+
if mode == 2:
172+
if not icon:
173+
return [], []
174+
175+
kinds = {}
176+
for side, box in icon:
177+
category = -1
178+
for kind in kinds:
179+
ratio = side / kind
180+
if 0.9 < ratio < 1.1:
181+
category = kind
182+
kinds[kind].append(box)
183+
break
184+
if category == -1:
185+
kinds[side] = [box]
186+
187+
def cluster_weight(item):
188+
side = item[0]
189+
count = len(item[1])
190+
if count == 5:
191+
return 5000000 + side
192+
if count % 5 == 0:
193+
return 1000000 + side
194+
return count * 10000 + side
195+
196+
kinds_sorted = sorted(kinds.items(), key=cluster_weight, reverse=True)
197+
kind = kinds_sorted[0]
198+
if len(kind[1]) % 5 == 0:
199+
otherborder = []
200+
for other_kind in kinds_sorted[1:]:
201+
otherborder.extend(other_kind[1])
202+
return kind[1], otherborder
203+
return [item[1] for item in icon], []
204+
205+
raise ValueError(f"unsupported legacy cutting mode: {mode}")
206+
207+
@staticmethod
208+
def _legacy_cut(img: Image.Image, border: List[int]) -> Image.Image:
209+
x, y, w, h = border
210+
img_arr = np.array(img)
211+
img_arr = img_arr[y + 2:y + h - 2, x + 2:x + w - 2]
212+
return Image.fromarray(img_arr)
213+
214+
@staticmethod
215+
def _legacy_split_last_col_recs(recs: List[Tuple[int, int, int, int]]) -> Tuple[List[Tuple[int, int, int, int]], List[Tuple[int, int, int, int]]]:
216+
if not recs:
217+
return [], []
218+
recs_sorted = sorted(recs, key=lambda x: x[0], reverse=True)
219+
last_col_recs = [rec for rec in recs_sorted if abs(rec[0] - recs_sorted[0][0]) < recs_sorted[0][2] / 2]
220+
last_col_recs = sorted(last_col_recs, key=lambda x: x[1])
221+
remaining = list(set(recs_sorted) - set(last_col_recs))
222+
return remaining, last_col_recs
223+
136224
@staticmethod
137225
def _save_lru_result(cache: OrderedDict[bytes, Tuple[int, int]], key: bytes, value: Tuple[int, int], max_size: int):
138226
cache[key] = value
@@ -619,87 +707,150 @@ def detect_content_area(img: Image.Image) -> Tuple[Optional[Image.Image], Option
619707

620708
return None, None
621709

622-
async def recognize(self, image: Image.Image, debug: bool = False, d_file: str = "") -> Tuple[List[List[int]], str]:
710+
async def _detect_rows_legacy(self, image: Image.Image) -> List[List[List[int]]]:
623711
"""
624-
主入口:检测并识别图片中的所有角色
712+
先使用 old_main.py 的 cutting/getPos 思路定位头像框。
713+
这里只复用旧版找框逻辑,单头像识别仍走当前 recognize_unit。
625714
"""
626-
img = image.convert("RGBA")
627-
current_img = img
715+
current_img = image.convert("RGBA")
716+
current_detect_img = current_img
628717
actual_offset_x = 0
629718
actual_offset_y = 0
630-
631-
# 尝试检测循环 (自动裁剪/缩放)
632-
valid_boxes = []
633-
for _ in range(6): # 最多尝试6次
719+
720+
for attempt in range(6):
721+
border, _ = self._legacy_cutting(current_detect_img, 2)
722+
no_box_found = len(border) == 0
723+
724+
if border and len(border) >= 4:
725+
recs = set(tuple(rec) for rec in border)
726+
_, last_col_recs = self._legacy_split_last_col_recs(list(recs))
727+
row_cnt = len(last_col_recs)
728+
729+
if row_cnt > 0:
730+
arr: List[List[Optional[Tuple[int, int, int, int]]]] = [[None for _ in range(5)] for _ in range(row_cnt)]
731+
last_col_recs_ypos = [rec[1] for rec in last_col_recs]
732+
working_recs = list(recs)
733+
734+
for col_index in range(5):
735+
working_recs, last_col_recs = self._legacy_split_last_col_recs(working_recs)
736+
if not last_col_recs:
737+
break
738+
739+
for rec in last_col_recs:
740+
cell_crop = self._icon_crop_with_pad(current_img, list(rec), pad=2)
741+
uid, _, _, _ = await self.recognize_unit(cell_crop)
742+
if uid == 0:
743+
continue
744+
745+
most_near_row = 0
746+
for row_index in range(1, len(arr)):
747+
if abs(last_col_recs_ypos[row_index] - rec[1]) < abs(last_col_recs_ypos[most_near_row] - rec[1]):
748+
most_near_row = row_index
749+
750+
existing = arr[most_near_row][col_index]
751+
if existing is None or abs(last_col_recs_ypos[most_near_row] - existing[1]) > abs(last_col_recs_ypos[most_near_row] - rec[1]):
752+
arr[most_near_row][col_index] = rec
753+
754+
rows = []
755+
for row in arr:
756+
none_cnt = row.count(None)
757+
if none_cnt >= 2:
758+
continue
759+
760+
ordered_row = [row[4 - col_index] for col_index in range(5) if row[4 - col_index] is not None]
761+
if not ordered_row:
762+
continue
763+
764+
rows.append([
765+
[x + actual_offset_x, y + actual_offset_y, w, h]
766+
for x, y, w, h in ordered_row
767+
])
768+
769+
if rows:
770+
return rows
771+
772+
try:
773+
next_detect_img, border_rect = self._legacy_cutting(current_detect_img, 1)
774+
except Exception:
775+
return []
776+
777+
if attempt == 0 or no_box_found:
778+
next_detect_img = next_detect_img.point(lambda x: 0 if x > 128 else 255)
779+
780+
current_img = self._legacy_cut(current_img, border_rect)
781+
current_detect_img = next_detect_img
782+
actual_offset_x += border_rect[0]
783+
actual_offset_y += border_rect[1]
784+
785+
return []
786+
787+
def _detect_rows_modern(self, image: Image.Image) -> List[List[List[int]]]:
788+
"""
789+
新版基于投影/Canny 的找框逻辑。
790+
"""
791+
current_img = image.convert("RGBA")
792+
actual_offset_x = 0
793+
actual_offset_y = 0
794+
795+
for _ in range(6):
634796
valid_boxes, _ = self.detect_cells(current_img)
635-
636797
if valid_boxes:
637-
break
638-
639-
# 没找到则尝试裁剪内容区域继续找
798+
global_boxes = [[x + actual_offset_x, y + actual_offset_y, w, h] for x, y, w, h in valid_boxes]
799+
return self._group_boxes_by_y(global_boxes, 15)
800+
640801
cropped, rect = self.detect_content_area(current_img)
641-
if cropped is None:
642-
break
643-
802+
if cropped is None or rect is None:
803+
return []
804+
644805
current_img = cropped
645-
cx, cy, cw, ch = rect
806+
cx, cy, _, _ = rect
646807
actual_offset_x += cx
647808
actual_offset_y += cy
648-
649-
if not valid_boxes:
650-
return [], ""
651809

652-
# 还原全局坐标并排序
653-
global_boxes = [[x + actual_offset_x, y + actual_offset_y, w, h] for x, y, w, h in valid_boxes]
654-
rows = self._group_boxes_by_y(global_boxes, 15)
655-
810+
return []
811+
812+
async def _render_recognition(self, img: Image.Image, rows: List[List[List[int]]], debug: bool = False, d_file: str = "") -> Tuple[List[List[int]], str]:
656813
arr_uids_final = []
657-
658-
# 结果可视化准备
814+
659815
outp_img = img.copy()
660-
# 绘制半透明蒙版
661816
overlay = Image.new('RGBA', img.size, (0, 0, 0, 160))
662817
outp_img = Image.alpha_composite(outp_img, overlay)
663818
draw_outp = ImageDraw.Draw(outp_img)
664-
819+
665820
row_cnt = len(rows)
666821
max_cols = max((len(r) for r in rows), default=0)
667822
icon_size = 64
668-
823+
669824
compare_w = max(1, icon_size * max_cols + 16 * 2)
670825
compare_h = max(1, icon_size * 2 * row_cnt + 16 * (row_cnt + 1))
671826
compare_img = Image.new("RGBA", (compare_w, compare_h), (255, 255, 255, 255))
672-
827+
673828
for r_idx, r_list in enumerate(rows):
674-
r_list.sort(key=lambda b: b[0]) # 按X排序
829+
r_list = sorted(r_list, key=lambda b: b[0])
675830
row_uids = []
676-
831+
677832
for c_idx, rec in enumerate(r_list):
678833
rx, ry, rw, rh = rec
679834
cell_crop = self._icon_crop_with_pad(img, rec, pad=2)
680-
681-
# 识别
835+
682836
uid, star, name, score = await self.recognize_unit(cell_crop)
683-
837+
684838
if debug:
685839
print(f"Recognized: {name} (ID: {uid}), score: {score}")
686-
687-
# 将原图部分贴回(去除蒙板效果)
840+
688841
paste_x = rx + 2 if rw > 4 else rx
689842
paste_y = ry + 2 if rh > 4 else ry
690843
outp_img.paste(cell_crop, (paste_x, paste_y))
691-
692-
# 绘制边框
844+
693845
color = "red" if uid != 0 else "black"
694-
draw_outp.rectangle((rx, ry, rx+rw, ry+rh), outline=color, width=3)
695-
696-
# 绘制对比图
846+
draw_outp.rectangle((rx, ry, rx + rw, ry + rh), outline=color, width=3)
847+
697848
pos_x = 16 + icon_size * c_idx
698849
pos_y = 16 * (r_idx + 1) + icon_size * 2 * r_idx
699-
850+
700851
cell_resize = cell_crop.resize((64, 64))
701852
compare_img.paste(cell_resize, (pos_x, pos_y))
702-
853+
703854
if uid != 0:
704855
try:
705856
icon_bytes = await imagemgr.unit_icon(uid, star)
@@ -708,16 +859,17 @@ async def recognize(self, image: Image.Image, debug: bool = False, d_file: str =
708859
compare_img.paste(icon, (pos_x, pos_y + 64), icon)
709860
row_uids.append(uid)
710861
except Exception as e:
711-
if debug: print(f"Icon load failed: {e}")
712-
862+
if debug:
863+
print(f"Icon load failed: {e}")
864+
713865
row_uids.reverse()
714866
arr_uids_final.append(row_uids)
715867

716868
def img_to_b64(im):
717869
buf = BytesIO()
718870
im.save(buf, format='PNG')
719871
return f'[CQ:image,file=base64://{base64.b64encode(buf.getvalue()).decode()}]'
720-
872+
721873
if debug and d_file:
722874
try:
723875
p = Path(d_file)
@@ -728,6 +880,18 @@ def img_to_b64(im):
728880

729881
return arr_uids_final, f'{img_to_b64(outp_img)}\n{img_to_b64(compare_img)}'
730882

883+
async def recognize(self, image: Image.Image, debug: bool = False, d_file: str = "") -> Tuple[List[List[int]], str]:
884+
"""
885+
主入口:检测并识别图片中的所有角色
886+
"""
887+
img = image.convert("RGBA")
888+
rows = await self._detect_rows_legacy(img)
889+
if not rows:
890+
rows = self._detect_rows_modern(img)
891+
if not rows:
892+
return [], ""
893+
return await self._render_recognition(img, rows, debug=debug, d_file=d_file)
894+
731895

732896
# Global instance
733897
instance = UnitRecognizer.get_instance()

0 commit comments

Comments
 (0)