gomyck-tools 1.5.6__py3-none-any.whl → 1.5.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.
ctools/ml/ppi.py DELETED
@@ -1,275 +0,0 @@
1
- from io import BytesIO
2
- from queue import Queue
3
-
4
- import numpy as np
5
- import yaml
6
- from PIL import Image
7
- from paddle.inference import Config, create_predictor
8
-
9
- from ctools import path_info
10
- from ctools.ml.image_process import preprocess
11
- from ctools.ml.img_extractor import ClassRegionBase64ExtractorPIL
12
-
13
-
14
- class PaddlePredictorPool:
15
- """Predictor 池,用于多线程安全推理"""
16
- def __init__(self, config_path, pool_size: int = 4):
17
- """
18
- 初始化预测器池
19
- Note: 每个 Config 对象只能创建一个 predictor,所以我们需要保存 config_path
20
- """
21
- self.config_path = config_path
22
- self.pool = Queue()
23
- self._init_pool(pool_size)
24
-
25
- def _load_config_yaml(self):
26
- """加载 yaml 配置"""
27
- with open(self.config_path, "r", encoding="utf-8") as f:
28
- return yaml.safe_load(f)
29
-
30
- def _create_config(self):
31
- """为新的 predictor 创建一个新的 Config 对象"""
32
- cfg = self._load_config_yaml()
33
- model_dir = cfg.get("MODEL_DIR", "")
34
- model_file = cfg.get("MODEL_FILE", "")
35
- if not model_file:
36
- model_dir = path_info.get_app_path('mod/model.pdmodel')
37
- params_file = cfg.get("PARAMS_FILE", "")
38
- if not params_file:
39
- model_dir = path_info.get_app_path('mod/model.pdiparams')
40
- use_gpu = cfg.get("USE_GPU", False)
41
-
42
- if model_dir:
43
- config = Config(model_dir)
44
- else:
45
- config = Config(model_file, params_file)
46
-
47
- config.enable_memory_optim()
48
-
49
- if use_gpu:
50
- config.enable_use_gpu(1000, 0)
51
- else:
52
- config.set_cpu_math_library_num_threads(4)
53
- config.enable_mkldnn()
54
-
55
- return config
56
-
57
- def _init_pool(self, pool_size: int):
58
- """初始化池中的所有 predictor"""
59
- for _ in range(pool_size):
60
- config = self._create_config()
61
- predictor = create_predictor(config)
62
- self.pool.put(predictor)
63
-
64
- def acquire(self, timeout=None):
65
- """从池中获取一个 predictor"""
66
- return self.pool.get(timeout=timeout)
67
-
68
- def release(self, predictor):
69
- """将 predictor 放回池中"""
70
- self.pool.put(predictor)
71
-
72
-
73
- class PaddleInferenceEngine:
74
- def __init__(self, config_path, pool_size=4):
75
- self.config_path = config_path
76
- self.cfg = self._load_config(config_path)
77
- self.predictor_pool = PaddlePredictorPool(config_path, pool_size=pool_size)
78
-
79
- def _load_config(self, config_path):
80
- with open(config_path, "r", encoding="utf-8") as f:
81
- return yaml.safe_load(f)
82
-
83
- def predict(self, inputs, timeout=None):
84
- """线程安全预测"""
85
- predictor = self.predictor_pool.acquire(timeout=timeout)
86
- try:
87
- input_names = predictor.get_input_names()
88
- for name in input_names:
89
- if name not in inputs:
90
- raise ValueError(f"缺少模型输入: {name}")
91
- tensor = predictor.get_input_handle(name)
92
- data = inputs[name]
93
- tensor.reshape(data.shape)
94
- tensor.copy_from_cpu(data)
95
- predictor.run()
96
- outputs = []
97
- for name in predictor.get_output_names():
98
- out = predictor.get_output_handle(name)
99
- outputs.append(out.copy_to_cpu())
100
- return outputs
101
- finally:
102
- self.predictor_pool.release(predictor)
103
-
104
- def predict_image(self, img, im_size=320):
105
- if isinstance(img, bytes):
106
- img = Image.open(BytesIO(img)).convert("RGB")
107
- elif isinstance(img, Image.Image):
108
- img = img.convert("RGB")
109
- elif isinstance(img, np.ndarray):
110
- pass
111
- else:
112
- raise ValueError("Unsupported image type for predict_image")
113
- orig_img_np = np.array(img) if not isinstance(img, np.ndarray) else img
114
- data = preprocess(orig_img_np, im_size)
115
- scale_factor = np.array([im_size * 1. / orig_img_np.shape[0], im_size * 1. / orig_img_np.shape[1]]).reshape((1, 2)).astype(np.float32)
116
- im_shape = np.array([im_size, im_size]).reshape((1, 2)).astype(np.float32)
117
- outputs = self.predict({"image": data, "im_shape": im_shape, "scale_factor": scale_factor})
118
- return outputs, scale_factor, im_size
119
-
120
- def predict_image_and_extract(self, img, im_size=320, class_names=None, target_classes=None, threshold=0.3):
121
- """预测并提取检测区域"""
122
- raw_outputs, scale_factor, im_size_ret = self.predict_image(img, im_size=im_size)
123
-
124
- # 转换为 PIL Image
125
- if isinstance(img, bytes):
126
- pil_img = Image.open(BytesIO(img)).convert("RGB")
127
- elif isinstance(img, Image.Image):
128
- pil_img = img.convert("RGB")
129
- elif isinstance(img, np.ndarray):
130
- pil_img = Image.fromarray(img.astype('uint8')).convert("RGB")
131
- else:
132
- raise ValueError("Unsupported image type")
133
-
134
- # 提取检测区域
135
- extractor = ClassRegionBase64ExtractorPIL(class_names or [], target_classes=target_classes, threshold=threshold)
136
- detection_results = raw_outputs[0]
137
- return extractor.extract(pil_img, detection_results, scale_factor=scale_factor, im_size=im_size_ret)
138
-
139
- @staticmethod
140
- def _nms_detections(detections, iou_threshold=0.5):
141
- if len(detections) == 0:
142
- return detections
143
- dets = np.array(detections, dtype=np.float32)
144
- scores = dets[:, 1]
145
- sorted_idx = np.argsort(-scores)
146
- keep = []
147
- while len(sorted_idx) > 0:
148
- current_idx = sorted_idx[0]
149
- keep.append(current_idx)
150
- if len(sorted_idx) == 1:
151
- break
152
- current_box = dets[current_idx, 2:6]
153
- other_boxes = dets[sorted_idx[1:], 2:6]
154
- x1_inter = np.maximum(current_box[0], other_boxes[:, 0])
155
- y1_inter = np.maximum(current_box[1], other_boxes[:, 1])
156
- x2_inter = np.minimum(current_box[2], other_boxes[:, 2])
157
- y2_inter = np.minimum(current_box[3], other_boxes[:, 3])
158
- inter_area = np.maximum(0, x2_inter - x1_inter) * np.maximum(0, y2_inter - y1_inter)
159
- area_current = (current_box[2] - current_box[0]) * (current_box[3] - current_box[1])
160
- area_others = (other_boxes[:, 2] - other_boxes[:, 0]) * (other_boxes[:, 3] - other_boxes[:, 1])
161
- union_area = area_current + area_others - inter_area
162
- iou = inter_area / (union_area + 1e-6)
163
- valid_idx = np.where(iou < iou_threshold)[0] + 1
164
- sorted_idx = sorted_idx[valid_idx]
165
- return dets[keep].tolist()
166
-
167
- def predict_image_tiled_and_extract(self, img, im_size=320, tile_overlap=0.2, class_names=None, target_classes=None, threshold=0.3, nms_iou=0.5):
168
- """
169
- Tiled prediction for large images (2K, 4K, etc).
170
- Splits image into overlapping tiles, predicts each tile, merges results.
171
-
172
- Args:
173
- img: PIL Image, numpy array, or bytes
174
- im_size: model training resolution (e.g., 320)
175
- tile_overlap: overlap ratio between tiles (0.0-1.0), default 0.2
176
- class_names: list of class names
177
- target_classes: list of target classes to extract
178
- threshold: confidence threshold
179
- nms_iou: IoU threshold for NMS merging
180
-
181
- Returns:
182
- extracted_outputs: list of dicts with extracted regions (with coordinates mapped to original image)
183
- all_detections: list of raw detections [cat_id, score, xmin, ymin, xmax, ymax] (original image coords)
184
- """
185
- # Convert input to numpy array
186
- if isinstance(img, bytes):
187
- pil_img = Image.open(BytesIO(img)).convert("RGB")
188
- img_np = np.array(pil_img)
189
- elif isinstance(img, Image.Image):
190
- img_np = np.array(img.convert("RGB"))
191
- elif isinstance(img, np.ndarray):
192
- img_np = img
193
- else:
194
- raise ValueError("Unsupported image type for predict_image_tiled")
195
- orig_h, orig_w = img_np.shape[:2]
196
- # Calculate tile parameters
197
- stride = int(im_size * (1 - tile_overlap))
198
- stride = max(1, stride)
199
- # Generate tile coordinates
200
- tiles = []
201
- y_start = 0
202
- while y_start < orig_h:
203
- y_end = min(y_start + im_size, orig_h)
204
- # If last tile doesn't cover the bottom, adjust
205
- if y_end == orig_h and y_start > 0:
206
- y_start = max(0, orig_h - im_size)
207
- y_end = orig_h
208
- x_start = 0
209
- while x_start < orig_w:
210
- x_end = min(x_start + im_size, orig_w)
211
- # If last tile doesn't cover the right, adjust
212
- if x_end == orig_w and x_start > 0:
213
- x_start = max(0, orig_w - im_size)
214
- x_end = orig_w
215
- tiles.append((x_start, y_start, x_end, y_end))
216
- x_start += stride
217
- if x_end == orig_w:
218
- break
219
- y_start += stride
220
- if y_end == orig_h:
221
- break
222
- # Predict each tile and collect detections
223
- all_detections = []
224
- for x_start, y_start, x_end, y_end in tiles:
225
- tile_img = img_np[y_start:y_end, x_start:x_end]
226
- # Pad tile if smaller than im_size
227
- if tile_img.shape[0] < im_size or tile_img.shape[1] < im_size:
228
- pad_h = im_size - tile_img.shape[0]
229
- pad_w = im_size - tile_img.shape[1]
230
- tile_img = np.pad(tile_img, ((0, pad_h), (0, pad_w), (0, 0)), mode='constant', constant_values=0)
231
- # Run inference on tile
232
- try:
233
- tile_outputs, _, _ = self.predict_image(tile_img, im_size=im_size)
234
- tile_detections = tile_outputs[0]
235
- # Map coordinates back to original image
236
- for det in tile_detections:
237
- cat_id, score = det[0], det[1]
238
- xmin, ymin, xmax, ymax = det[2], det[3], det[4], det[5]
239
- # Scale from model resolution to tile resolution
240
- tile_h, tile_w = y_end - y_start, x_end - x_start
241
- xmin_tile = xmin * tile_w / im_size
242
- ymin_tile = ymin * tile_h / im_size
243
- xmax_tile = xmax * tile_w / im_size
244
- ymax_tile = ymax * tile_h / im_size
245
- # Translate to original image coordinates
246
- xmin_orig = xmin_tile + x_start
247
- ymin_orig = ymin_tile + y_start
248
- xmax_orig = xmax_tile + x_start
249
- ymax_orig = ymax_tile + y_start
250
- # Clip to image bounds
251
- xmin_orig = max(0, min(xmin_orig, orig_w))
252
- ymin_orig = max(0, min(ymin_orig, orig_h))
253
- xmax_orig = max(0, min(xmax_orig, orig_w))
254
- ymax_orig = max(0, min(ymax_orig, orig_h))
255
- all_detections.append([cat_id, score, xmin_orig, ymin_orig, xmax_orig, ymax_orig])
256
- except Exception as e:
257
- print(f"Error processing tile {(x_start, y_start, x_end, y_end)}: {e}")
258
- continue
259
- # Apply NMS to merge duplicate detections
260
- merged_detections = self._nms_detections(all_detections, iou_threshold=nms_iou)
261
-
262
- # Extract regions using the merged detections
263
- if isinstance(img, bytes):
264
- pil_img = Image.open(BytesIO(img)).convert("RGB")
265
- elif isinstance(img, Image.Image):
266
- pil_img = img.convert("RGB")
267
- elif isinstance(img, np.ndarray):
268
- pil_img = Image.fromarray(img_np.astype('uint8')).convert("RGB")
269
- else:
270
- raise ValueError("Unsupported image type")
271
-
272
- # Create a dummy scale_factor (1:1 since we're already in original coordinates)
273
- scale_factor = np.array([[1.0, 1.0]], dtype=np.float32)
274
- extractor = ClassRegionBase64ExtractorPIL(class_names or [], target_classes=target_classes, threshold=threshold)
275
- return extractor.extract(pil_img, merged_detections, scale_factor=scale_factor, im_size=orig_h)