1+ import math
2+ import threading
3+ import time
4+ import numpy as np
5+ import mss
6+
7+ from pynput import mouse
8+ from pynput .mouse import Controller as MouseController , Button
9+
10+ # Circle radius in pixels
11+ s = 400 # adjust as needed
12+
13+ # Drawing settings
14+ circle_duration = 0 # seconds to complete one circle
15+ min_steps = 0 # minimum number of segments in the circle
16+
17+ mousec = MouseController ()
18+ sct = mss .mss ()
19+
20+ _drawing = False
21+ _lock = threading .Lock ()
22+ _triggered = threading .Event ()
23+ _listener = None
24+
25+ # Two-point rectangle selection
26+ _first_click = None
27+ _second_click = None
28+
29+ def _find_white_center (left , top , width , height , thr = 200 ):
30+ # Capture region
31+ bbox = {"left" : int (left ), "top" : int (top ), "width" : int (width ), "height" : int (height )}
32+ shot = sct .grab (bbox )
33+ # shot pixels are BGRA
34+ arr = np .frombuffer (shot .raw , dtype = np .uint8 ).reshape (shot .height , shot .width , 4 )
35+ rgb = arr [:, :, :3 ][:, :, ::- 1 ] # to RGB
36+
37+ # Threshold for "white"
38+ mask = (rgb [:, :, 0 ] >= thr ) & (rgb [:, :, 1 ] >= thr ) & (rgb [:, :, 2 ] >= thr )
39+
40+ if not mask .any ():
41+ # Loosen threshold, then fallback to rect center
42+ thr2 = max (150 , thr - 50 )
43+ mask = (rgb [:, :, 0 ] >= thr2 ) & (rgb [:, :, 1 ] >= thr2 ) & (rgb [:, :, 2 ] >= thr2 )
44+ if not mask .any ():
45+ cx = width / 2.0
46+ cy = height / 2.0
47+ return (left + cx , top + cy )
48+
49+ ys , xs = np .nonzero (mask )
50+ cx = xs .mean ()
51+ cy = ys .mean ()
52+ return (left + cx , top + cy )
53+
54+ def _draw_circle (center , radius , duration , steps ):
55+ global _drawing
56+ with _lock :
57+ if _drawing :
58+ return
59+ _drawing = True
60+ try :
61+ cx , cy = int (center [0 ]), int (center [1 ])
62+
63+ # Move to starting point on circumference (angle = 0)
64+ start_x = int (cx + radius )
65+ start_y = int (cy )
66+ mousec .position = (start_x , start_y )
67+ time .sleep (0.01 )
68+
69+ # Hold mouse button and trace the circle
70+ mousec .press (Button .left )
71+ try :
72+ steps = max (steps , int (2 * math .pi * radius ))
73+ sleep_per_step = max (0.0005 , duration / steps )
74+
75+ for i in range (1 , steps + 1 ):
76+ theta = 2 * math .pi * (i / steps )
77+ x = int (cx + radius * math .cos (theta ))
78+ y = int (cy + radius * math .sin (theta ))
79+ mousec .position = (x , y )
80+ time .sleep (sleep_per_step )
81+ finally :
82+ mousec .release (Button .left )
83+ finally :
84+ with _lock :
85+ _drawing = False
86+
87+ def _maybe_process_rectangle ():
88+ global _first_click , _second_click , _listener
89+ if _first_click is None or _second_click is None :
90+ return
91+ if _triggered .is_set ():
92+ return
93+ _triggered .set ()
94+
95+ x1 , y1 = _first_click
96+ x2 , y2 = _second_click
97+ left = min (x1 , x2 )
98+ top = min (y1 , y2 )
99+ width = abs (x2 - x1 )
100+ height = abs (y2 - y1 )
101+ if width == 0 or height == 0 :
102+ # Fallback to single point; draw around it
103+ center = (left , top )
104+ else :
105+ center = _find_white_center (left , top , width , height , thr = 200 )
106+
107+ t = threading .Thread (
108+ target = _draw_circle ,
109+ args = (center , s , circle_duration , min_steps ),
110+ daemon = False
111+ )
112+ t .start ()
113+
114+ if _listener is not None :
115+ _listener .stop ()
116+
117+ def on_click (x , y , button , pressed ):
118+ global _first_click , _second_click
119+ if button is Button .left and not pressed and not _triggered .is_set ():
120+ if _first_click is None :
121+ _first_click = (int (x ), int (y ))
122+ print (f"First corner: { _first_click } . Click second corner." )
123+ else :
124+ _second_click = (int (x ), int (y ))
125+ print (f"Second corner: { _second_click } . Processing..." )
126+ _maybe_process_rectangle ()
127+
128+ if __name__ == "__main__" :
129+ print (f"Click two opposite corners of a rectangle that contains a white circle (RGB >= 200)." )
130+ print (f"A single circle of radius { s } px will be drawn centered on the detected circle." )
131+ with mouse .Listener (on_click = on_click ) as listener :
132+ _listener = listener
133+ listener .join ()
0 commit comments