@@ -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
733897instance = UnitRecognizer .get_instance ()
0 commit comments