1212(grab the screen) is device-bound. OpenCV + NumPy come in via the project's
1313``je_open_cv`` dependency and are imported lazily. Imports no ``PySide6``.
1414"""
15+ import functools
1516from dataclasses import asdict , dataclass
1617from typing import Any , Dict , List , Optional , Sequence
1718
19+ from je_auto_control .utils .exception .exceptions import AutoControlScreenException
20+
1821# cv2 method name -> the OpenCV constant is resolved lazily in _method().
1922_METHOD_NAMES = ("ccoeff_normed" , "ccorr_normed" , "sqdiff_normed" )
2023ImageSource = Any
2124
2225
26+ def _contain_cv2_error (fn ):
27+ """Convert OpenCV's ``cv2.error`` into a contained AutoControlScreenException.
28+
29+ A degenerate template/mask makes ``cv2.matchTemplate``/``minMaxLoc`` raise
30+ ``cv2.error`` — a bare ``Exception`` subclass that is NOT in the executor's
31+ containment tuple, so it would escape and abort the whole automation run
32+ instead of being recorded as a failed match step.
33+ """
34+ @functools .wraps (fn )
35+ def wrapper (* args , ** kwargs ):
36+ import cv2
37+ try :
38+ return fn (* args , ** kwargs )
39+ except cv2 .error as error :
40+ raise AutoControlScreenException (
41+ f"{ fn .__name__ } failed: { error } " ) from error
42+ return wrapper
43+
44+
2345@dataclass (frozen = True )
2446class Match :
2547 """One template match: top-left (x, y), size, correlation score, scale."""
@@ -107,6 +129,7 @@ def _resize(template, scale: float):
107129 return cv2 .resize (template , new_size )
108130
109131
132+ @_contain_cv2_error
110133def _score_map (template : ImageSource , haystack : Optional [ImageSource ] = None , * ,
111134 region : Optional [Sequence [int ]] = None ,
112135 method : str = "ccoeff_normed" , scale : float = 1.0 ):
@@ -128,6 +151,7 @@ def _score_map(template: ImageSource, haystack: Optional[ImageSource] = None, *,
128151 return result , tmpl
129152
130153
154+ @_contain_cv2_error
131155def match_template (template : ImageSource , * , haystack : Optional [ImageSource ] = None ,
132156 region : Optional [Sequence [int ]] = None ,
133157 scales : Sequence [float ] = (1.0 ,), min_score : float = 0.8 ,
@@ -201,6 +225,7 @@ def _select_candidates(result, min_score: float, width: int, height: int,
201225 for x , y , s in zip (xs , ys , scores )]
202226
203227
228+ @_contain_cv2_error
204229def match_template_all (template : ImageSource , * ,
205230 haystack : Optional [ImageSource ] = None ,
206231 region : Optional [Sequence [int ]] = None ,
@@ -285,6 +310,7 @@ def _masked_scores(template: ImageSource, mask: Optional[ImageSource],
285310 return np .nan_to_num (result , nan = 0.0 , posinf = 0.0 , neginf = 0.0 ), tmpl
286311
287312
313+ @_contain_cv2_error
288314def match_masked (template : ImageSource , * , mask : Optional [ImageSource ] = None ,
289315 haystack : Optional [ImageSource ] = None ,
290316 region : Optional [Sequence [int ]] = None ,
@@ -308,6 +334,7 @@ def match_masked(template: ImageSource, *, mask: Optional[ImageSource] = None,
308334 round (float (max_val ), 4 ), 1.0 )
309335
310336
337+ @_contain_cv2_error
311338def match_masked_all (template : ImageSource , * , mask : Optional [ImageSource ] = None ,
312339 haystack : Optional [ImageSource ] = None ,
313340 region : Optional [Sequence [int ]] = None ,
0 commit comments