ok-script 0.0.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- autohelper/AutoHelper.py +116 -0
- autohelper/__init__.py +0 -0
- autohelper/capture/BaseCaptureMethod.py +30 -0
- autohelper/capture/HwndWindow.py +138 -0
- autohelper/capture/__init__.py +0 -0
- autohelper/capture/adb/ADBCaptureMethod.py +29 -0
- autohelper/capture/adb/DeviceManager.py +209 -0
- autohelper/capture/adb/__init__.py +0 -0
- autohelper/capture/adb/bin/__init__.py +0 -0
- autohelper/capture/adb/targets.py +61 -0
- autohelper/capture/adb/vbox.py +140 -0
- autohelper/capture/windows/WindowsGraphicsCaptureMethod.py +228 -0
- autohelper/capture/windows/__init__.py +0 -0
- autohelper/capture/windows/d3d11.py +74 -0
- autohelper/capture/windows/utils.py +149 -0
- autohelper/capture/windows/window.py +36 -0
- autohelper/color/Color.py +41 -0
- autohelper/color/__init__.py +0 -0
- autohelper/config/Config.py +79 -0
- autohelper/config/InfoDict.py +16 -0
- autohelper/config/__init__.py +0 -0
- autohelper/feature/Box.py +199 -0
- autohelper/feature/Feature.py +20 -0
- autohelper/feature/FeatureSet.py +207 -0
- autohelper/feature/FindFeature.py +52 -0
- autohelper/feature/__init__.py +0 -0
- autohelper/gui/App.py +120 -0
- autohelper/gui/Communicate.py +22 -0
- autohelper/gui/MainWindow.py +69 -0
- autohelper/gui/TabTitles.py +56 -0
- autohelper/gui/__init__.py +5 -0
- autohelper/gui/debug/AspectRatioWidget.py +31 -0
- autohelper/gui/debug/DebugTab.py +37 -0
- autohelper/gui/debug/FrameWidget.py +125 -0
- autohelper/gui/debug/InfoWidget.py +40 -0
- autohelper/gui/debug/LoggerWidget.py +74 -0
- autohelper/gui/debug/__init__.py +0 -0
- autohelper/gui/i18n/__init__.py +0 -0
- autohelper/gui/i18n/path.py +3 -0
- autohelper/gui/icon/__init__.py +0 -0
- autohelper/gui/icon/icon.py +6 -0
- autohelper/gui/loading/LoadingWindow.py +142 -0
- autohelper/gui/loading/__init__.py +0 -0
- autohelper/gui/overlay/OverlayWindow.py +34 -0
- autohelper/gui/overlay/__init__.py +0 -0
- autohelper/gui/resources.py +485 -0
- autohelper/gui/tasks/ConfigItemFactory.py +14 -0
- autohelper/gui/tasks/StartButton.py +47 -0
- autohelper/gui/tasks/TaskOpButton.py +33 -0
- autohelper/gui/tasks/TaskTab.py +121 -0
- autohelper/gui/tasks/TooltipTableWidget.py +39 -0
- autohelper/gui/tasks/__init__.py +0 -0
- autohelper/gui/util/Alert.py +20 -0
- autohelper/gui/util/InitWorker.py +17 -0
- autohelper/gui/util/__init__.py +0 -0
- autohelper/gui/widget/ListTableWidgetItem.py +27 -0
- autohelper/gui/widget/NumericTableWidgetItem.py +33 -0
- autohelper/gui/widget/RoundCornerContainer.py +59 -0
- autohelper/gui/widget/TabWidget.py +30 -0
- autohelper/gui/widget/UpdateConfigWidgetItem.py +16 -0
- autohelper/gui/widget/YesNonWidgetItem.py +23 -0
- autohelper/gui/widget/__init__.py +0 -0
- autohelper/interaction/ADBInteraction.py +33 -0
- autohelper/interaction/BaseInteraction.py +36 -0
- autohelper/interaction/Win32Interaction.py +56 -0
- autohelper/interaction/__init__.py +0 -0
- autohelper/logging/Logger.py +103 -0
- autohelper/logging/__init__.py +0 -0
- autohelper/ocr/OCR.py +50 -0
- autohelper/ocr/__init__.py +0 -0
- autohelper/predict/__init__.py +0 -0
- autohelper/predict/predict.py +45 -0
- autohelper/rotypes/Windows/Foundation/Collections.py +60 -0
- autohelper/rotypes/Windows/Foundation/__init__.py +153 -0
- autohelper/rotypes/Windows/Globalization/__init__.py +25 -0
- autohelper/rotypes/Windows/Graphics/Capture/__init__.py +110 -0
- autohelper/rotypes/Windows/Graphics/DirectX/Direct3D11/__init__.py +41 -0
- autohelper/rotypes/Windows/Graphics/DirectX/__init__.py +167 -0
- autohelper/rotypes/Windows/Graphics/Imaging/__init__.py +34 -0
- autohelper/rotypes/Windows/Graphics/__init__.py +5 -0
- autohelper/rotypes/Windows/Media/Ocr/__init__.py +76 -0
- autohelper/rotypes/Windows/Media/__init__.py +0 -0
- autohelper/rotypes/Windows/Security/Cryptography/__init__.py +18 -0
- autohelper/rotypes/Windows/Security/__init__.py +0 -0
- autohelper/rotypes/Windows/Storage/Streams/__init__.py +27 -0
- autohelper/rotypes/Windows/Storage/__init__.py +0 -0
- autohelper/rotypes/Windows/__init__.py +0 -0
- autohelper/rotypes/__init__.py +4 -0
- autohelper/rotypes/delegate.py +105 -0
- autohelper/rotypes/export.py +21 -0
- autohelper/rotypes/idldsl.py +272 -0
- autohelper/rotypes/inspectable.py +79 -0
- autohelper/rotypes/roapi.py +30 -0
- autohelper/rotypes/types.py +77 -0
- autohelper/rotypes/winstring.py +44 -0
- autohelper/save/BlackBarProcessor.py +25 -0
- autohelper/save/PostProcessor.py +6 -0
- autohelper/save/SaveByInterval.py +30 -0
- autohelper/save/SaveByKeyPress.py +27 -0
- autohelper/save/SaveMethodBase.py +46 -0
- autohelper/save/__init__.py +0 -0
- autohelper/scene/FeatureScene.py +14 -0
- autohelper/scene/Scene.py +21 -0
- autohelper/scene/__init__.py +0 -0
- autohelper/stats/StreamStats.py +34 -0
- autohelper/stats/__init__.py +0 -0
- autohelper/task/BaseTask.py +109 -0
- autohelper/task/ExecutorOperation.py +119 -0
- autohelper/task/FindFeatureTask.py +6 -0
- autohelper/task/TaskExecutor.py +249 -0
- autohelper/task/__init__.py +0 -0
- autohelper/util/__init__.py +0 -0
- autohelper/util/json.py +27 -0
- autohelper/util/list.py +2 -0
- autohelper/util/path.py +37 -0
- autohelper/util/thread.py +12 -0
- autohelper/util/win32_process.py +86 -0
- autohelper/util/yaml.py +28 -0
- ok_script-0.0.7.dist-info/METADATA +64 -0
- ok_script-0.0.7.dist-info/RECORD +122 -0
- ok_script-0.0.7.dist-info/WHEEL +5 -0
- ok_script-0.0.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from autohelper.util.json import read_json_file, write_json_file
|
|
2
|
+
from autohelper.util.path import get_path_relative_to_exe
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Config:
|
|
6
|
+
def __init__(self, default, folder, name):
|
|
7
|
+
super().__init__()
|
|
8
|
+
self.config_file = get_path_relative_to_exe(folder, f"{name}.json")
|
|
9
|
+
self.config = read_json_file(self.config_file)
|
|
10
|
+
if self.config is None:
|
|
11
|
+
self.config = default
|
|
12
|
+
elif verify_config(self.config, default):
|
|
13
|
+
self.save_file()
|
|
14
|
+
|
|
15
|
+
def save_file(self):
|
|
16
|
+
write_json_file(self.config_file, self.config)
|
|
17
|
+
|
|
18
|
+
def update(self, *args, **kwargs):
|
|
19
|
+
self.config.update(*args, **kwargs)
|
|
20
|
+
self.save_file()
|
|
21
|
+
|
|
22
|
+
def get(self, key, default=None):
|
|
23
|
+
return self.config.get(key, default)
|
|
24
|
+
|
|
25
|
+
def keys(self):
|
|
26
|
+
return self.config.keys()
|
|
27
|
+
|
|
28
|
+
def values(self):
|
|
29
|
+
return self.config.values()
|
|
30
|
+
|
|
31
|
+
def items(self):
|
|
32
|
+
return self.config.items()
|
|
33
|
+
|
|
34
|
+
def pop(self, key, default=None):
|
|
35
|
+
result = self.config.pop(key, default)
|
|
36
|
+
self.save_file()
|
|
37
|
+
return result
|
|
38
|
+
|
|
39
|
+
def popitem(self):
|
|
40
|
+
result = self.config.popitem()
|
|
41
|
+
self.save_file()
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
def setdefault(self, key, default=None):
|
|
45
|
+
result = self.config.setdefault(key, default)
|
|
46
|
+
self.save_file()
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
def clear(self):
|
|
50
|
+
self.config.clear()
|
|
51
|
+
self.save_file()
|
|
52
|
+
|
|
53
|
+
def __len__(self):
|
|
54
|
+
return len(self.config)
|
|
55
|
+
|
|
56
|
+
def __setitem__(self, key, value):
|
|
57
|
+
self.config[key] = value
|
|
58
|
+
self.save_file()
|
|
59
|
+
|
|
60
|
+
def __getitem__(self, key):
|
|
61
|
+
return self.config[key]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def verify_config(config, default_config):
|
|
65
|
+
modified = False
|
|
66
|
+
|
|
67
|
+
# Remove entries that do not exist in default_config
|
|
68
|
+
for key in list(config.keys()):
|
|
69
|
+
if key not in default_config:
|
|
70
|
+
del config[key]
|
|
71
|
+
modified = True
|
|
72
|
+
|
|
73
|
+
# Check entries in default_config
|
|
74
|
+
for key, default_value in default_config.items():
|
|
75
|
+
if key not in config or type(config[key]) != type(default_value):
|
|
76
|
+
config[key] = default_value
|
|
77
|
+
modified = True
|
|
78
|
+
|
|
79
|
+
return not modified
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from autohelper.gui.Communicate import communicate
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class InfoDict(dict):
|
|
5
|
+
|
|
6
|
+
def __delitem__(self, key):
|
|
7
|
+
super().__delitem__(key)
|
|
8
|
+
communicate.task_info.emit()
|
|
9
|
+
|
|
10
|
+
def clear(self):
|
|
11
|
+
super().clear()
|
|
12
|
+
communicate.task_info.emit()
|
|
13
|
+
|
|
14
|
+
def __setitem__(self, key, value):
|
|
15
|
+
super().__setitem__(key, value)
|
|
16
|
+
communicate.task_info.emit()
|
|
File without changes
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import random
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Box:
|
|
7
|
+
def __init__(self, x: int, y: int, width: int, height: int, confidence: float = 1, name=None) -> None:
|
|
8
|
+
self.name = name
|
|
9
|
+
self.x = x
|
|
10
|
+
self.y = y
|
|
11
|
+
self.width = width
|
|
12
|
+
self.height = height
|
|
13
|
+
self.confidence = confidence
|
|
14
|
+
|
|
15
|
+
def __eq__(self, other):
|
|
16
|
+
if not isinstance(other, Box):
|
|
17
|
+
# Don't attempt to compare against unrelated types
|
|
18
|
+
return NotImplemented
|
|
19
|
+
|
|
20
|
+
return (self.x == other.x and
|
|
21
|
+
self.y == other.y and
|
|
22
|
+
self.width == other.width and
|
|
23
|
+
self.height == other.height and
|
|
24
|
+
self.confidence == other.confidence and
|
|
25
|
+
self.name == other.name)
|
|
26
|
+
|
|
27
|
+
def __repr__(self):
|
|
28
|
+
return self.name
|
|
29
|
+
|
|
30
|
+
def __str__(self) -> str:
|
|
31
|
+
if self.name is not None:
|
|
32
|
+
return f"Box(name='{self.name}', x={self.x}, y={self.y}, width={self.width}, height={self.height}, confidence={round(self.confidence * 100)})"
|
|
33
|
+
return f"Box(x={self.x}, y={self.y}, width={self.width}, height={self.height}, confidence={round(self.confidence * 100)})"
|
|
34
|
+
|
|
35
|
+
def closest_distance(self, other):
|
|
36
|
+
# Calculate the sides of the boxes
|
|
37
|
+
left1, right1 = self.x, self.x + self.width
|
|
38
|
+
top1, bottom1 = self.y, self.y + self.height
|
|
39
|
+
left2, right2 = other.x, other.x + other.width
|
|
40
|
+
top2, bottom2 = other.y, other.y + other.height
|
|
41
|
+
|
|
42
|
+
# Horizontal distance
|
|
43
|
+
if right1 < left2:
|
|
44
|
+
horizontal_distance = left2 - right1
|
|
45
|
+
elif right2 < left1:
|
|
46
|
+
horizontal_distance = left1 - right2
|
|
47
|
+
else:
|
|
48
|
+
horizontal_distance = 0
|
|
49
|
+
|
|
50
|
+
# Vertical distance
|
|
51
|
+
if bottom1 < top2:
|
|
52
|
+
vertical_distance = top2 - bottom1
|
|
53
|
+
elif bottom2 < top1:
|
|
54
|
+
vertical_distance = top1 - bottom2
|
|
55
|
+
else:
|
|
56
|
+
vertical_distance = 0
|
|
57
|
+
|
|
58
|
+
# If boxes overlap or touch, the closest distance is 0
|
|
59
|
+
if horizontal_distance == 0 and vertical_distance == 0:
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
# If boxes are diagonally aligned, calculate diagonal distance
|
|
63
|
+
return math.sqrt(horizontal_distance ** 2 + vertical_distance ** 2)
|
|
64
|
+
|
|
65
|
+
def relative_with_variance(self, relative_x=0.5, relative_y=0.5):
|
|
66
|
+
# Calculate the center of the box
|
|
67
|
+
center_x = self.x + self.width * relative_x
|
|
68
|
+
center_y = self.y + self.height * relative_y
|
|
69
|
+
|
|
70
|
+
# Add random variance
|
|
71
|
+
variance = random.uniform(0, 0.1)
|
|
72
|
+
center_x_with_variance = center_x + variance
|
|
73
|
+
center_y_with_variance = center_y + variance
|
|
74
|
+
return round(center_x_with_variance), round(center_y_with_variance)
|
|
75
|
+
|
|
76
|
+
def copy(self, x_offset=0, y_offset=0, width_offset=0, height_offset=0, name=None):
|
|
77
|
+
return Box(self.x + x_offset, self.y + y_offset, self.width + width_offset, self.height + height_offset,
|
|
78
|
+
self.confidence, name or self.name)
|
|
79
|
+
|
|
80
|
+
def center(self):
|
|
81
|
+
return self.x + self.width / 2, self.y + self.height / 2
|
|
82
|
+
|
|
83
|
+
def find_closest_box(self, direction: str, boxes: list):
|
|
84
|
+
orig_center_x, orig_center_y = self.center()
|
|
85
|
+
|
|
86
|
+
def distance_criteria(box):
|
|
87
|
+
# Calculate center points for comparison
|
|
88
|
+
box_center_x, box_center_y = box.center()
|
|
89
|
+
|
|
90
|
+
dx = box_center_x - orig_center_x
|
|
91
|
+
dy = box_center_y - orig_center_y
|
|
92
|
+
distance = math.sqrt(dx ** 2 + dy ** 2)
|
|
93
|
+
if box == self:
|
|
94
|
+
return float('inf')
|
|
95
|
+
elif direction == 'up' and self.y - (box.y + box.height / 2) >= 0:
|
|
96
|
+
return distance
|
|
97
|
+
elif direction == 'down' and box.y - (self.y + self.height / 2) >= 0:
|
|
98
|
+
return distance
|
|
99
|
+
elif direction == 'left' and self.x - (box.x + box.width / 2) >= 0:
|
|
100
|
+
return distance
|
|
101
|
+
elif direction == 'right' and box.x - (self.x + self.width / 2) >= 0:
|
|
102
|
+
return distance
|
|
103
|
+
else:
|
|
104
|
+
return float('inf')
|
|
105
|
+
|
|
106
|
+
filtered_boxes = sorted(boxes, key=distance_criteria)
|
|
107
|
+
# Removed debug print statement for cleanliness
|
|
108
|
+
|
|
109
|
+
for box in filtered_boxes:
|
|
110
|
+
if distance_criteria(box) != float('inf'):
|
|
111
|
+
return box
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def sort_boxes(boxes):
|
|
116
|
+
return sorted(boxes, key=lambda box: (box.y, box.x if abs(box.y - boxes[0].y) < 6 else 0))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def find_box_by_name(boxes, names) -> Box:
|
|
120
|
+
if isinstance(names, (str, re.Pattern)):
|
|
121
|
+
names = [names]
|
|
122
|
+
|
|
123
|
+
result = None
|
|
124
|
+
priority = len(names)
|
|
125
|
+
|
|
126
|
+
for box in boxes:
|
|
127
|
+
for i, name in enumerate(names):
|
|
128
|
+
if (isinstance(name, str) and name == box.name) or (
|
|
129
|
+
isinstance(name, re.Pattern) and re.search(name, box.name)):
|
|
130
|
+
if i < priority:
|
|
131
|
+
priority = i
|
|
132
|
+
result = box
|
|
133
|
+
if i == 0:
|
|
134
|
+
break
|
|
135
|
+
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def find_boxes_within_boundary(boxes, boundary_box):
|
|
140
|
+
"""
|
|
141
|
+
Find all boxes that are entirely within the specified boundary box.
|
|
142
|
+
|
|
143
|
+
Parameters:
|
|
144
|
+
- boxes (list[Box]): List of Box objects to check.
|
|
145
|
+
- boundary_box (Box): The boundary Box object.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
- list[Box]: Boxes found within the boundary box.
|
|
149
|
+
"""
|
|
150
|
+
within_boundary = []
|
|
151
|
+
|
|
152
|
+
for box in boxes:
|
|
153
|
+
# Check if box is within boundary_box
|
|
154
|
+
if (box.x >= boundary_box.x and
|
|
155
|
+
box.y >= boundary_box.y and
|
|
156
|
+
box.x + box.width <= boundary_box.x + boundary_box.width and
|
|
157
|
+
box.y + box.height <= boundary_box.y + boundary_box.height):
|
|
158
|
+
within_boundary.append(box)
|
|
159
|
+
|
|
160
|
+
return within_boundary
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def crop_image(image, box=None):
|
|
164
|
+
if box is not None:
|
|
165
|
+
if (box.x >= 0 and box.y >= 0 and
|
|
166
|
+
box.x + box.width <= image.shape[1] and # image.shape[1] is the width of the image
|
|
167
|
+
box.y + box.height <= image.shape[0]): # image.shape[0] is the height of the image
|
|
168
|
+
|
|
169
|
+
# Extract the region of interest (ROI) using slicing
|
|
170
|
+
|
|
171
|
+
return image[box.y:box.y + box.height, box.x:box.x + box.width, :3]
|
|
172
|
+
else:
|
|
173
|
+
# Return some error value or raise an exception
|
|
174
|
+
# For example, return 0 or None
|
|
175
|
+
return image # or None, or raise an exception
|
|
176
|
+
else:
|
|
177
|
+
return image
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def find_boxes_by_name(boxes, names) -> list[Box]:
|
|
181
|
+
# Ensure names is always a list
|
|
182
|
+
if isinstance(names, (str, re.Pattern)):
|
|
183
|
+
names = [names]
|
|
184
|
+
|
|
185
|
+
result = []
|
|
186
|
+
|
|
187
|
+
for box in boxes:
|
|
188
|
+
# Flag to track if the box has been matched and should be added
|
|
189
|
+
matched = False
|
|
190
|
+
for name in names:
|
|
191
|
+
if matched:
|
|
192
|
+
break # Stop checking names if we've already matched this box
|
|
193
|
+
if (isinstance(name, str) and name == box.name) or (
|
|
194
|
+
isinstance(name, re.Pattern) and re.search(name, box.name)):
|
|
195
|
+
matched = True
|
|
196
|
+
if matched:
|
|
197
|
+
result.append(box)
|
|
198
|
+
|
|
199
|
+
return result
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Feature:
|
|
5
|
+
def __init__(self, mat: np.ndarray, x: int, y: int, width: int, height: int) -> None:
|
|
6
|
+
"""
|
|
7
|
+
Initialize a Feature with an image (Mat) and its bounding box coordinates.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
mat (np.ndarray): The OpenCV Mat object representing the image.
|
|
11
|
+
x (int): The x-coordinate of the top-left corner of the bounding box.
|
|
12
|
+
y (int): The y-coordinate of the top-left corner of the bounding box.
|
|
13
|
+
width (int): The width of the bounding box.
|
|
14
|
+
height (int): The height of the bounding box.
|
|
15
|
+
"""
|
|
16
|
+
self.mat = mat
|
|
17
|
+
self.x = x
|
|
18
|
+
self.y = y
|
|
19
|
+
self.width = width
|
|
20
|
+
self.height = height
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Dict
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
import cv2
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from autohelper.feature.Box import Box, sort_boxes
|
|
11
|
+
from autohelper.feature.Feature import Feature
|
|
12
|
+
from autohelper.gui.Communicate import communicate
|
|
13
|
+
from autohelper.logging.Logger import get_logger
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FeatureSet:
|
|
19
|
+
# Category_name to OpenCV Mat
|
|
20
|
+
featureDict: Dict[str, Feature] = {}
|
|
21
|
+
|
|
22
|
+
def __init__(self, coco_folder: str, default_horizontal_variance=0,
|
|
23
|
+
default_vertical_variance=0, default_threshold=0.95) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Initialize the FeatureSet by loading images and annotations from a COCO dataset.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
coco_folder (str): Directory containing the JSON file and images.
|
|
29
|
+
width (int): Scale images to this width.
|
|
30
|
+
height (int): Scale images to this height.
|
|
31
|
+
"""
|
|
32
|
+
self.coco_folder = coco_folder
|
|
33
|
+
|
|
34
|
+
# Process images and annotations
|
|
35
|
+
self.width = 0
|
|
36
|
+
self.height = 0
|
|
37
|
+
if default_threshold == 0:
|
|
38
|
+
default_threshold = 0.95
|
|
39
|
+
self.default_threshold = default_threshold
|
|
40
|
+
self.default_horizontal_variance = default_horizontal_variance
|
|
41
|
+
self.default_vertical_variance = default_vertical_variance
|
|
42
|
+
|
|
43
|
+
def check_size(self, frame):
|
|
44
|
+
height, width = frame.shape[:2]
|
|
45
|
+
if self.width != width or self.height != height and height > 0 and width > 0:
|
|
46
|
+
logger.info(f"FeatureSet: Width and height changed from {self.width}x{self.height} to {width}x{height}")
|
|
47
|
+
self.width = width
|
|
48
|
+
self.height = height
|
|
49
|
+
self.process_data()
|
|
50
|
+
|
|
51
|
+
def process_data(self) -> None:
|
|
52
|
+
"""
|
|
53
|
+
Process the images and annotations from the COCO dataset.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
width (int): Target width for scaling images.
|
|
57
|
+
height (int): Target height for scaling images.
|
|
58
|
+
"""
|
|
59
|
+
self.featureDict.clear()
|
|
60
|
+
json_path = f'{self.coco_folder}/_annotations.coco.json'
|
|
61
|
+
with open(json_path, 'r') as file:
|
|
62
|
+
data = json.load(file)
|
|
63
|
+
|
|
64
|
+
# Create a map from image ID to file name
|
|
65
|
+
image_map = {image['id']: image['file_name'] for image in data['images']}
|
|
66
|
+
|
|
67
|
+
# Create a map from category ID to category name
|
|
68
|
+
category_map = {category['id']: category['name'] for category in data['categories']}
|
|
69
|
+
|
|
70
|
+
for annotation in data['annotations']:
|
|
71
|
+
image_id = annotation['image_id']
|
|
72
|
+
category_id = annotation['category_id']
|
|
73
|
+
bbox = annotation['bbox']
|
|
74
|
+
|
|
75
|
+
# Load and scale the image
|
|
76
|
+
image_path = f'{self.coco_folder}/{image_map[image_id]}'
|
|
77
|
+
image = cv2.imread(image_path)
|
|
78
|
+
original_width, _ = image.shape[:2]
|
|
79
|
+
if image is None:
|
|
80
|
+
logger.error(f'Could not read image {image_path}')
|
|
81
|
+
continue
|
|
82
|
+
scale_x, scale_y = self.width / image.shape[1], self.height / image.shape[0]
|
|
83
|
+
image = cv2.resize(image, (self.width, self.height))
|
|
84
|
+
|
|
85
|
+
# Calculate the scaled bounding box
|
|
86
|
+
x, y, w, h = bbox
|
|
87
|
+
x, y, w, h = round(x * scale_x), round(y * scale_y), round(w * scale_x), round(h * scale_y)
|
|
88
|
+
|
|
89
|
+
# Crop the image to the bounding box
|
|
90
|
+
cropped_image = image[y:y + h, x:x + w, :3]
|
|
91
|
+
|
|
92
|
+
# Store in featureDict using the category name
|
|
93
|
+
category_name = category_map[category_id]
|
|
94
|
+
logger.debug(
|
|
95
|
+
f"loaded {category_name} resized width {self.width} / original_width:{original_width},scale_x:{scale_x},scale_y:{scale_y}")
|
|
96
|
+
if category_name in self.featureDict:
|
|
97
|
+
raise ValueError(f"Multiple boxes found for category {category_name}")
|
|
98
|
+
self.featureDict[category_name] = Feature(cropped_image, x, y, w, h)
|
|
99
|
+
|
|
100
|
+
def save_images(self, target_folder: str) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Save all images in the featureDict to the specified folder.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
target_folder (str): The folder where images will be saved.
|
|
106
|
+
"""
|
|
107
|
+
# Ensure the target folder exists
|
|
108
|
+
os.makedirs(target_folder, exist_ok=True)
|
|
109
|
+
|
|
110
|
+
# Iterate through the featureDict and save each image
|
|
111
|
+
for category_name, image in self.featureDict.items():
|
|
112
|
+
# Construct the filename
|
|
113
|
+
file_name = f"{category_name}.jpg"
|
|
114
|
+
file_path = os.path.join(target_folder, file_name)
|
|
115
|
+
|
|
116
|
+
# Save the image
|
|
117
|
+
cv2.imwrite(file_path, image.mat)
|
|
118
|
+
print(f"Saved {file_path}")
|
|
119
|
+
|
|
120
|
+
def find_one(self, mat: np.ndarray, category_name: str, horizontal_variance: float = 0,
|
|
121
|
+
vertical_variance: float = 0,
|
|
122
|
+
threshold=0) -> Box:
|
|
123
|
+
boxes = self.find_feature(mat, category_name, horizontal_variance=horizontal_variance,
|
|
124
|
+
vertical_variance=vertical_variance, threshold=threshold)
|
|
125
|
+
if len(boxes) > 1:
|
|
126
|
+
logger.warning(f"find_one:found too many {len(boxes)} return first", file=sys.stderr)
|
|
127
|
+
if len(boxes) >= 1:
|
|
128
|
+
return boxes[0]
|
|
129
|
+
|
|
130
|
+
def find_feature(self, mat: np.ndarray, category_name: str, horizontal_variance: float = 0,
|
|
131
|
+
vertical_variance: float = 0, threshold: float = 0) -> List[Box]:
|
|
132
|
+
"""
|
|
133
|
+
Find a feature within a given variance.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
mat (np.ndarray): The image in which to find the feature.
|
|
137
|
+
category_name (str): The category name of the feature to find.
|
|
138
|
+
horizontal_variance (float): Allowed horizontal variance as a percentage of width.
|
|
139
|
+
vertical_variance (float): Allowed vertical variance as a percentage of height.
|
|
140
|
+
threshold: Allowed confidence threshold for the feature
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
List[Box]: A list of boxes where the feature is found.
|
|
144
|
+
"""
|
|
145
|
+
self.check_size(mat)
|
|
146
|
+
|
|
147
|
+
if threshold == 0:
|
|
148
|
+
threshold = self.default_threshold
|
|
149
|
+
if horizontal_variance == 0:
|
|
150
|
+
horizontal_variance = self.default_horizontal_variance
|
|
151
|
+
if vertical_variance == 0:
|
|
152
|
+
vertical_variance = self.default_vertical_variance
|
|
153
|
+
if category_name not in self.featureDict:
|
|
154
|
+
raise ValueError(f"FeatureSet: {category_name} not found in featureDict")
|
|
155
|
+
|
|
156
|
+
feature = self.featureDict[category_name]
|
|
157
|
+
feature_width, feature_height = feature.width, feature.height
|
|
158
|
+
|
|
159
|
+
# Define search area using variance
|
|
160
|
+
search_x1 = max(0, round(feature.x - self.width * horizontal_variance))
|
|
161
|
+
search_y1 = max(0, round(feature.y - self.height * vertical_variance))
|
|
162
|
+
search_x2 = min(self.width, round(feature.x + feature_width + self.width * horizontal_variance))
|
|
163
|
+
search_y2 = min(self.height, round(feature.y + feature_height + self.height * vertical_variance))
|
|
164
|
+
|
|
165
|
+
search_area = mat[search_y1:search_y2, search_x1:search_x2, :3]
|
|
166
|
+
# Crop the search area from the image
|
|
167
|
+
# print(f"search_area: ({self.width,self.height})({search_x1},{search_x2},{search_y1},{search_y2}) ({get_depth(search_area),get_depth(feature.mat)})")
|
|
168
|
+
|
|
169
|
+
# cv2.imwrite("images/test.jpg", search_area)
|
|
170
|
+
|
|
171
|
+
# Template matchingTM_CCORR_NORMED
|
|
172
|
+
# result = cv2.matchTemplate(search_area, feature.mat, cv2.TM_CCOEFF_NORMED)
|
|
173
|
+
result = cv2.matchTemplate(search_area, feature.mat, cv2.TM_CCOEFF_NORMED)
|
|
174
|
+
|
|
175
|
+
# Define a threshold for acceptable matches
|
|
176
|
+
locations = filter_and_sort_matches(result, threshold, feature_width, feature_height)
|
|
177
|
+
boxes = []
|
|
178
|
+
|
|
179
|
+
for loc in locations: # Iterate through found locations
|
|
180
|
+
x, y = loc[0] + search_x1, loc[1] + search_y1
|
|
181
|
+
confidence = result[loc[1], loc[0]] # Retrieve the confidence score
|
|
182
|
+
boxes.append(Box(x, y, feature_width, feature_height, confidence, category_name))
|
|
183
|
+
# cv2.rectangle(mat, (x, y), (x + feature_width,y+feature_height),(0, 255, 0), 2)
|
|
184
|
+
# cv2.imwrite("images/test.jpg", mat)
|
|
185
|
+
|
|
186
|
+
result = sort_boxes(boxes)
|
|
187
|
+
communicate.draw_box.emit(category_name, result, "red")
|
|
188
|
+
return result
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def filter_and_sort_matches(result, threshold, width, height):
|
|
192
|
+
# Filter matches based on the threshold
|
|
193
|
+
loc = np.where(result >= threshold)
|
|
194
|
+
|
|
195
|
+
# Zip the locations into a list of tuples and sort by threshold in descending order
|
|
196
|
+
matches = sorted(zip(*loc[::-1]), key=lambda p: result[p[::-1]], reverse=True)
|
|
197
|
+
|
|
198
|
+
# Filter out overlapping matches
|
|
199
|
+
unique_matches = []
|
|
200
|
+
for pt in matches:
|
|
201
|
+
if all(not (pt[0] >= m[0] - width and pt[0] <= m[0] + width and
|
|
202
|
+
pt[1] >= m[1] - height and pt[1] <= m[1] + height)
|
|
203
|
+
for m in unique_matches):
|
|
204
|
+
unique_matches.append(pt)
|
|
205
|
+
|
|
206
|
+
# print(f"result {len(result)} loc {len(loc)} matches {len(matches)} unique_matches {unique_matches}")
|
|
207
|
+
return unique_matches
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from autohelper.feature.Box import Box
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FindFeature:
|
|
8
|
+
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.feature_set = None
|
|
11
|
+
self.executor = None
|
|
12
|
+
|
|
13
|
+
def find(self, feature_name, horizontal_variance=0, vertical_variance=0, threshold=0) -> List[Box]:
|
|
14
|
+
if self.executor.frame is None:
|
|
15
|
+
return list()
|
|
16
|
+
return self.feature_set.find_feature(self.executor.frame, feature_name, horizontal_variance, vertical_variance,
|
|
17
|
+
threshold)
|
|
18
|
+
|
|
19
|
+
def find_and_set(self, features, horizontal_variance=0, vertical_variance=0, threshold=0):
|
|
20
|
+
ret = True
|
|
21
|
+
if features is None:
|
|
22
|
+
raise Exception("features cannot be None")
|
|
23
|
+
if isinstance(features, str):
|
|
24
|
+
features = [features]
|
|
25
|
+
for feature in features:
|
|
26
|
+
result = self.find_one(feature, horizontal_variance, vertical_variance, threshold)
|
|
27
|
+
if result is None:
|
|
28
|
+
ret = False
|
|
29
|
+
setattr(self, feature, result)
|
|
30
|
+
return ret
|
|
31
|
+
|
|
32
|
+
def wait_and_click(self, feature, horizontal_variance=0, vertical_variance=0, threshold=0, relative_x=0.5,
|
|
33
|
+
relative_y=0.5,
|
|
34
|
+
time_out=0, pre_action=None, post_action=None):
|
|
35
|
+
box = self.wait_until(lambda: self.find_one(feature, horizontal_variance, vertical_variance, threshold),
|
|
36
|
+
time_out,
|
|
37
|
+
pre_action,
|
|
38
|
+
post_action)
|
|
39
|
+
if box is not None:
|
|
40
|
+
self.click_box(box, relative_x, relative_y)
|
|
41
|
+
return True
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
def find_one(self, feature_name, horizontal_variance=0, vertical_variance=0, threshold=0) -> Box:
|
|
45
|
+
boxes = self.find(feature_name, horizontal_variance, vertical_variance, threshold)
|
|
46
|
+
if len(boxes) > 0:
|
|
47
|
+
if len(boxes) > 1:
|
|
48
|
+
print(f"find_one:found {feature_name} too many {len(boxes)}", file=sys.stderr)
|
|
49
|
+
return boxes[0]
|
|
50
|
+
|
|
51
|
+
def on_feature(self, boxes):
|
|
52
|
+
pass
|
|
File without changes
|