func2stream 0.0.1.dev240519063723__tar.gz → 1.0__tar.gz
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.
- func2stream-1.0/MANIFEST.in +27 -0
- func2stream-1.0/PKG-INFO +405 -0
- func2stream-1.0/README.md +371 -0
- func2stream-1.0/func2stream/__init__.py +21 -0
- func2stream-1.0/func2stream/basicnconst.py +60 -0
- func2stream-1.0/func2stream/core.py +470 -0
- func2stream-1.0/func2stream/implicit_ctx.py +168 -0
- func2stream-1.0/func2stream/utils.py +76 -0
- func2stream-1.0/func2stream/video.py +157 -0
- func2stream-1.0/func2stream.egg-info/SOURCES.txt +11 -0
- func2stream-1.0/setup.py +59 -0
- func2stream-0.0.1.dev240519063723/PKG-INFO +0 -74
- func2stream-0.0.1.dev240519063723/README.md +0 -56
- func2stream-0.0.1.dev240519063723/func2stream.egg-info/PKG-INFO +0 -74
- func2stream-0.0.1.dev240519063723/func2stream.egg-info/SOURCES.txt +0 -9
- func2stream-0.0.1.dev240519063723/func2stream.egg-info/dependency_links.txt +0 -1
- func2stream-0.0.1.dev240519063723/func2stream.egg-info/requires.txt +0 -2
- func2stream-0.0.1.dev240519063723/func2stream.egg-info/top_level.txt +0 -1
- func2stream-0.0.1.dev240519063723/setup.py +0 -36
- {func2stream-0.0.1.dev240519063723 → func2stream-1.0}/LICENSE +0 -0
- {func2stream-0.0.1.dev240519063723 → func2stream-1.0}/pyproject.toml +0 -0
- {func2stream-0.0.1.dev240519063723 → func2stream-1.0}/setup.cfg +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# func2stream MANIFEST.in
|
|
2
|
+
#
|
|
3
|
+
# PyPI 发布时的文件包含/排除规则
|
|
4
|
+
|
|
5
|
+
# 包含
|
|
6
|
+
include LICENSE
|
|
7
|
+
include README.md
|
|
8
|
+
include pyproject.toml
|
|
9
|
+
|
|
10
|
+
# 排除开发文档
|
|
11
|
+
prune docs/internal
|
|
12
|
+
|
|
13
|
+
# 排除测试和示例
|
|
14
|
+
prune samples
|
|
15
|
+
prune tests
|
|
16
|
+
|
|
17
|
+
# 排除开发配置
|
|
18
|
+
prune .github
|
|
19
|
+
prune .windsurf
|
|
20
|
+
prune .cursor
|
|
21
|
+
exclude .windsurfrules
|
|
22
|
+
exclude .gitattributes
|
|
23
|
+
|
|
24
|
+
# 排除构建产物
|
|
25
|
+
prune *.egg-info
|
|
26
|
+
global-exclude __pycache__
|
|
27
|
+
global-exclude *.py[cod]
|
func2stream-1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: func2stream
|
|
3
|
+
Version: 1.0
|
|
4
|
+
Summary: Effortlessly transform functions into asynchronous elements for building high-performance pipelines
|
|
5
|
+
Home-page: https://github.com/BICHENG/func2stream
|
|
6
|
+
Author: BI CHENG
|
|
7
|
+
License: MPL-2.0
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.6
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: loguru
|
|
17
|
+
Requires-Dist: numpy<1.22,>=1.19.5; python_version < "3.8"
|
|
18
|
+
Requires-Dist: numpy>=1.21.0; python_version >= "3.8"
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest>=6.2; extra == "test"
|
|
21
|
+
Provides-Extra: video
|
|
22
|
+
Requires-Dist: opencv-python>=4; extra == "video"
|
|
23
|
+
Dynamic: author
|
|
24
|
+
Dynamic: classifier
|
|
25
|
+
Dynamic: description
|
|
26
|
+
Dynamic: description-content-type
|
|
27
|
+
Dynamic: home-page
|
|
28
|
+
Dynamic: license
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
Dynamic: provides-extra
|
|
31
|
+
Dynamic: requires-dist
|
|
32
|
+
Dynamic: requires-python
|
|
33
|
+
Dynamic: summary
|
|
34
|
+
|
|
35
|
+
# func2stream v1.0
|
|
36
|
+
|
|
37
|
+
[中文](README.md) | [English](README_en.md)
|
|
38
|
+
|
|
39
|
+
> [!TIP]
|
|
40
|
+
> 把函数串成异步流水线,工作量和写注释一样,但性能天差地别
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
**你的原生代码:**
|
|
45
|
+
```python
|
|
46
|
+
def detect(tensor):
|
|
47
|
+
return model(tensor)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**只需加个返回注解:**
|
|
51
|
+
```python
|
|
52
|
+
def detect(tensor) -> "boxes":
|
|
53
|
+
return model(tensor)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**然后放进 Pipeline,自动异步:**
|
|
57
|
+
```python
|
|
58
|
+
Pipeline([... preprocess, detect, display]).start()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
这不是魔法,只是我认为没人会注解字符串,所以在这个前提下,完成了很多工作。🤷♂️
|
|
62
|
+
|
|
63
|
+
## 为什么设计了 func2stream
|
|
64
|
+
|
|
65
|
+
目的:最小侵入串行代码,让阶段N的处理与阶段N+1的处理在时间上重叠。
|
|
66
|
+
|
|
67
|
+
对于实时推理,或希望处理序列数据(如视频)的场景,你有一堆处理函数,顺序调用时,吞吐量很低。
|
|
68
|
+
|
|
69
|
+
但在开发时,如果过早考虑并行,会到处管理变量和状态,增加代码复杂度。
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
# 例如一个实时美颜程序
|
|
73
|
+
while True:
|
|
74
|
+
frame = camera.read()
|
|
75
|
+
tensor = some_preprocess(frame)
|
|
76
|
+
face_boxes = detect(tensor)
|
|
77
|
+
landmarks,crops = get_face_crop(tensor, face_boxes)
|
|
78
|
+
beautified = beautify_model(crops)
|
|
79
|
+
display(frame, landmarks, beautified)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**需求**:让它们并行运行、不增加代码复杂度、专注于业务逻辑。
|
|
83
|
+
|
|
84
|
+
**传统做法**:写线程、队列,代码变乱,无法专注业务逻辑,再加上全局变量到处飞,改一处动全身。
|
|
85
|
+
|
|
86
|
+
**速度可能起来了,但掉头发的速度也起来了。(bushi**
|
|
87
|
+
|
|
88
|
+
**现在**:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
Pipeline([
|
|
92
|
+
DataSource(camera.read),
|
|
93
|
+
some_preprocess,
|
|
94
|
+
detect,
|
|
95
|
+
get_face_crop,
|
|
96
|
+
beautify_model,
|
|
97
|
+
display,
|
|
98
|
+
]).start()
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
每个函数自动在独立线程运行,数据自动传递,无需在全局暴露 queue、thread 等基础设施。
|
|
102
|
+
|
|
103
|
+
> 完整可运行示例:[samples/beauty_filter_mock.py](samples/beauty_filter_mock.py)
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 安装
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pip install git+https://github.com/bicheng/func2stream.git
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 如何使用?
|
|
116
|
+
|
|
117
|
+
在现有代码的基础上,只需要针对可参与异步流水线的函数做修改,加 `-> "数据名"` 即可。
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from func2stream import Pipeline, DataSource
|
|
121
|
+
|
|
122
|
+
# ─── 工具函数(不进流水线,不加 -> "数据名")─────────────────
|
|
123
|
+
|
|
124
|
+
def normalize(img):
|
|
125
|
+
return img.astype(np.float32) / 255.0
|
|
126
|
+
|
|
127
|
+
def get_eye_positions(x, y, w, h):
|
|
128
|
+
return [(x + w//3, y + h//3), (x + 2*w//3, y + h//3)]
|
|
129
|
+
|
|
130
|
+
def apply_brightness(crop, factor=0.9):
|
|
131
|
+
return crop * factor + (1 - factor)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ─── 流水线函数(加 -> "数据名")──────────────────────────────
|
|
135
|
+
|
|
136
|
+
def some_preprocess(frame) -> "tensor":
|
|
137
|
+
return normalize(frame) # 调用工具函数
|
|
138
|
+
|
|
139
|
+
def detect(tensor) -> "face_boxes":
|
|
140
|
+
return model.detect(tensor)
|
|
141
|
+
|
|
142
|
+
def get_face_crop(tensor, face_boxes) -> ("landmarks", "crops"): # 多输出
|
|
143
|
+
landmarks, crops = [], []
|
|
144
|
+
for (x, y, w, h) in face_boxes:
|
|
145
|
+
landmarks.append(get_eye_positions(x, y, w, h)) # 调用工具函数
|
|
146
|
+
crops.append(tensor[y:y+h, x:x+w])
|
|
147
|
+
return landmarks, crops
|
|
148
|
+
|
|
149
|
+
def beautify_model(crops) -> "beautified":
|
|
150
|
+
return [apply_brightness(crop) for crop in crops] # 调用工具函数
|
|
151
|
+
|
|
152
|
+
def display(frame, landmarks, beautified) -> "displayed": # 多输入
|
|
153
|
+
cv2.imshow("win", frame)
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ─── 组装 ──────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
Pipeline([
|
|
160
|
+
DataSource(camera.read), # → frame
|
|
161
|
+
some_preprocess, # → tensor
|
|
162
|
+
detect, # → face_boxes
|
|
163
|
+
get_face_crop, # → landmarks, crops
|
|
164
|
+
beautify_model, # → beautified
|
|
165
|
+
display, # → displayed
|
|
166
|
+
]).start()
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**数据流**:`frame → tensor → face_boxes → (landmarks, crops) → beautified → displayed`
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 用 `@init_ctx` 持有状态,避免全局变量
|
|
174
|
+
|
|
175
|
+
你可能会遇到这样的开发问题:
|
|
176
|
+
- 全局变量满天飞
|
|
177
|
+
- 流水线内需要持有状态(模型、计数器、配置等)
|
|
178
|
+
- 写一个类属于过度设计
|
|
179
|
+
|
|
180
|
+
用 `@init_ctx` 把它们打包在一起。
|
|
181
|
+
|
|
182
|
+
> 完整可运行示例:[samples/tracker_mock.py](samples/tracker_mock.py)
|
|
183
|
+
|
|
184
|
+
### 例子:多目标追踪器
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from func2stream import Pipeline, DataSource, init_ctx
|
|
188
|
+
|
|
189
|
+
@init_ctx
|
|
190
|
+
def create_tracker(model_path, threshold=0.5):
|
|
191
|
+
# ─── 状态:模型、计数器、追踪历史 ───────────────────────────
|
|
192
|
+
model = load_model(model_path)
|
|
193
|
+
frame_count = 0
|
|
194
|
+
track_history = {}
|
|
195
|
+
|
|
196
|
+
# ─── 流水线函数 ────────────────────────────────────────────
|
|
197
|
+
def detect(frame) -> "boxes":
|
|
198
|
+
return [b for b in model(frame) if b.conf > threshold]
|
|
199
|
+
|
|
200
|
+
def track(frame, boxes) -> "tracks":
|
|
201
|
+
nonlocal frame_count
|
|
202
|
+
frame_count += 1
|
|
203
|
+
# ... 追踪逻辑,更新 track_history ...
|
|
204
|
+
return tracks
|
|
205
|
+
|
|
206
|
+
def draw(frame, tracks) -> "frame":
|
|
207
|
+
for t in tracks:
|
|
208
|
+
cv2.rectangle(frame, t.bbox, (0, 255, 0), 2)
|
|
209
|
+
return frame
|
|
210
|
+
|
|
211
|
+
# ─── 工具函数(不进流水线)─────────────────────────────────
|
|
212
|
+
def get_frame_count() -> int:
|
|
213
|
+
return frame_count
|
|
214
|
+
|
|
215
|
+
def get_track_history() -> dict:
|
|
216
|
+
return track_history
|
|
217
|
+
|
|
218
|
+
return locals()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# 同一个工厂,创建两个互相隔离的追踪器
|
|
222
|
+
tracker_front = create_tracker("yolo.pt", threshold=0.7)
|
|
223
|
+
tracker_rear = create_tracker("yolo.pt", threshold=0.5)
|
|
224
|
+
|
|
225
|
+
p1 = Pipeline([
|
|
226
|
+
DataSource(front_camera.read),
|
|
227
|
+
tracker_front.detect,
|
|
228
|
+
tracker_front.track,
|
|
229
|
+
tracker_front.draw,
|
|
230
|
+
display,
|
|
231
|
+
])
|
|
232
|
+
|
|
233
|
+
p2 = Pipeline([
|
|
234
|
+
DataSource(rear_camera.read),
|
|
235
|
+
tracker_rear.detect,
|
|
236
|
+
tracker_rear.track,
|
|
237
|
+
tracker_rear.draw,
|
|
238
|
+
display,
|
|
239
|
+
])
|
|
240
|
+
|
|
241
|
+
p1.start()
|
|
242
|
+
p2.start()
|
|
243
|
+
|
|
244
|
+
# 查看状态
|
|
245
|
+
while min(tracker_front.get_frame_count(), tracker_rear.get_frame_count()) < 100:
|
|
246
|
+
print(f"front: {tracker_front.get_frame_count()}")
|
|
247
|
+
print(f"rear: {tracker_rear.get_frame_count()}")
|
|
248
|
+
time.sleep(1)
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## 用 `gpu_model()` 处理 GPU 资源
|
|
255
|
+
|
|
256
|
+
`gpu_model()` 可以避免 GPU 模型跨线程执行时的性能问题。
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
from func2stream import Pipeline, DataSource, init_ctx, gpu_model
|
|
260
|
+
|
|
261
|
+
@init_ctx
|
|
262
|
+
def create_detector(threshold=0.5):
|
|
263
|
+
# 主线程变量
|
|
264
|
+
frame_count = 0
|
|
265
|
+
|
|
266
|
+
# GPU 模型 - 延迟到工作线程创建
|
|
267
|
+
model = gpu_model(lambda: TRTModel(device='cuda'))
|
|
268
|
+
|
|
269
|
+
def detect(frame) -> "boxes":
|
|
270
|
+
nonlocal frame_count
|
|
271
|
+
frame_count += 1
|
|
272
|
+
return [b for b in model(frame) if b.conf > threshold]
|
|
273
|
+
|
|
274
|
+
def get_count():
|
|
275
|
+
return frame_count
|
|
276
|
+
|
|
277
|
+
return locals()
|
|
278
|
+
|
|
279
|
+
ctx = create_detector(threshold=0.7)
|
|
280
|
+
Pipeline([DataSource(camera.read), ctx.detect, display]).start()
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
`gpu_model()` 接受一个无参 lambda,在工作线程首次访问时执行。调用方式与原模型一致。
|
|
284
|
+
|
|
285
|
+
> 示例:[samples/gpu_model_trt.py](samples/gpu_model_trt.py)
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## 常见误区
|
|
290
|
+
|
|
291
|
+
### 误区 1:流水线函数不再能嵌套调用
|
|
292
|
+
解释:如果你有多个流水线函数,不要在某一个流水线函数里直接调用另一个流水线函数。
|
|
293
|
+
|
|
294
|
+
PS: 后期会想办法支持嵌套
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
def to_gray(frame) -> "gray":
|
|
299
|
+
return cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
|
300
|
+
|
|
301
|
+
# 💥 流水线函数会被自动包装,行为不再是直接调用
|
|
302
|
+
def step1(frame) -> "edges":
|
|
303
|
+
gray = to_gray(frame) # 不能在这里调用 to_gray
|
|
304
|
+
return cv2.Canny(gray, 50, 150)
|
|
305
|
+
|
|
306
|
+
# -------------------------------------------------------------
|
|
307
|
+
# ✅ 方案一:如果overlap的性能收益比较小,合并处理步骤到一个函数
|
|
308
|
+
def step1(frame) -> "edges":
|
|
309
|
+
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
|
310
|
+
return cv2.Canny(gray, 50, 150)
|
|
311
|
+
|
|
312
|
+
Pipeline([
|
|
313
|
+
DataSource(...),
|
|
314
|
+
step1,
|
|
315
|
+
])
|
|
316
|
+
|
|
317
|
+
# -------------------------------------------------------------
|
|
318
|
+
# ✅ 方案二:拆成独立步骤,让 Pipeline 自动串联
|
|
319
|
+
def canny(gray) -> "edges":
|
|
320
|
+
return cv2.Canny(gray, 50, 150)
|
|
321
|
+
|
|
322
|
+
Pipeline([
|
|
323
|
+
DataSource(...),
|
|
324
|
+
to_gray, # 先执行,产出 gray
|
|
325
|
+
canny, # 后执行,自动读取 gray
|
|
326
|
+
...
|
|
327
|
+
])
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### 误区 2:流水线中使用的函数忘了写 `-> "数据名"`
|
|
332
|
+
|
|
333
|
+
PS: 这是唯一工作量,好好写吧
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
def process(frame):
|
|
337
|
+
return frame * 2
|
|
338
|
+
|
|
339
|
+
Pipeline([
|
|
340
|
+
DataSource(...),
|
|
341
|
+
process, # 💥 Pipeline 会警告,且函数运行时收到的并非 frame
|
|
342
|
+
])
|
|
343
|
+
|
|
344
|
+
# ✅ 放进 Pipeline 的函数,必须声明输出
|
|
345
|
+
def process(frame) -> "result":
|
|
346
|
+
return frame * 2
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### 误区 3:类型注解
|
|
350
|
+
|
|
351
|
+
```python
|
|
352
|
+
# 👀 -> int 是类型注解,Pipeline 不会识别它
|
|
353
|
+
def process(frame) -> int:
|
|
354
|
+
return frame * 2
|
|
355
|
+
|
|
356
|
+
# ✅ 用引号包裹的字符串才会被识别
|
|
357
|
+
def process(frame) -> "result":
|
|
358
|
+
return frame * 2
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## v1.0 Breaking Changes
|
|
365
|
+
|
|
366
|
+
相比之下,早期版本更像是 geek 的玩具,现已升级到 v1.0,以下 API 已移除,**使用就像注释一样简单**:
|
|
367
|
+
|
|
368
|
+
| 移除项 | 替代方案 |
|
|
369
|
+
|--------|----------|
|
|
370
|
+
| `@from_ctx(get=[], ret=[])` | 使用 `-> "key"` 返回注解 |
|
|
371
|
+
| `build_ctx()` | `DataSource` 自动构建 ctx |
|
|
372
|
+
| `MapReduce` | 实验性功能,已弃用(见 Roadmap) |
|
|
373
|
+
| `nodrop()` | 无替代,该功能无使用场景 |
|
|
374
|
+
|
|
375
|
+
**v1.0 的核心改进**:
|
|
376
|
+
- **零装饰器数据流**:只需 `-> "key"` 注解,Pipeline 自动识别并包装
|
|
377
|
+
- **隐式 Context**:参数名即读取键,无需手动声明
|
|
378
|
+
- **状态隔离**:`@init_ctx` 封装模型、计数器等,避免全局变量
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## Roadmap
|
|
383
|
+
|
|
384
|
+
### 并行分支与同步(规划中)
|
|
385
|
+
|
|
386
|
+
```python
|
|
387
|
+
Pipeline([
|
|
388
|
+
preprocess,
|
|
389
|
+
( # ← 元组 = 并行分支
|
|
390
|
+
[branch_a1, branch_a2], # 分支1:串行子流水线
|
|
391
|
+
[branch_b1], # 分支2
|
|
392
|
+
branch_c, # 分支3:单函数
|
|
393
|
+
), # ← 元组结束 = 隐式同步点
|
|
394
|
+
merge_results, # 收到 (a2_out, b1_out, c_out) 打包
|
|
395
|
+
postprocess,
|
|
396
|
+
]).start()
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
**设计原则**:同步点语法隐式,元组结束即同步,无需显式 Barrier。
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## 许可证
|
|
404
|
+
|
|
405
|
+
MPL-2.0
|