Skip to main content

Effortlessly transform functions into asynchronous elements for building high-performance pipelines

Project description

func2stream v1.0

中文 | English

[!TIP] 把函数串成异步流水线,工作量和写注释一样,但性能天差地别


你的原生代码:

def detect(tensor):
    return model(tensor)

只需加个返回注解:

def detect(tensor) -> "boxes":
    return model(tensor)

然后放进 Pipeline,自动异步:

Pipeline([... preprocess, detect, display]).start()

这不是魔法,只是我认为没人会注解字符串,所以在这个前提下,完成了很多工作。🤷‍♂️

为什么设计了 func2stream

目的:最小侵入串行代码,让阶段N的处理与阶段N+1的处理在时间上重叠。

对于实时推理,或希望处理序列数据(如视频)的场景,你有一堆处理函数,顺序调用时,吞吐量很低。

但在开发时,如果过早考虑并行,会到处管理变量和状态,增加代码复杂度。

# 例如一个实时美颜程序
while True:
    frame = camera.read()
    tensor = some_preprocess(frame)
    face_boxes = detect(tensor)
    landmarks,crops = get_face_crop(tensor, face_boxes)
    beautified = beautify_model(crops)
    display(frame, landmarks, beautified)

需求:让它们并行运行、不增加代码复杂度、专注于业务逻辑。

传统做法:写线程、队列,代码变乱,无法专注业务逻辑,再加上全局变量到处飞,改一处动全身。

速度可能起来了,但掉头发的速度也起来了。(bushi

现在

Pipeline([
    DataSource(camera.read),
    some_preprocess,
    detect,
    get_face_crop,
    beautify_model,
    display,
]).start()

每个函数自动在独立线程运行,数据自动传递,无需在全局暴露 queue、thread 等基础设施。

完整可运行示例:samples/beauty_filter_mock.py


安装

pip install git+https://github.com/bicheng/func2stream.git

如何使用?

在现有代码的基础上,只需要针对可参与异步流水线的函数做修改,加 -> "数据名" 即可。

from func2stream import Pipeline, DataSource

# ─── 工具函数(不进流水线,不加 -> "数据名")─────────────────

def normalize(img):
    return img.astype(np.float32) / 255.0

def get_eye_positions(x, y, w, h):
    return [(x + w//3, y + h//3), (x + 2*w//3, y + h//3)]

def apply_brightness(crop, factor=0.9):
    return crop * factor + (1 - factor)


# ─── 流水线函数(加 -> "数据名")──────────────────────────────

def some_preprocess(frame) -> "tensor":
    return normalize(frame)                                         # 调用工具函数

def detect(tensor) -> "face_boxes":
    return model.detect(tensor)

def get_face_crop(tensor, face_boxes) -> ("landmarks", "crops"):   # 多输出
    landmarks, crops = [], []
    for (x, y, w, h) in face_boxes:
        landmarks.append(get_eye_positions(x, y, w, h))            # 调用工具函数
        crops.append(tensor[y:y+h, x:x+w])
    return landmarks, crops

def beautify_model(crops) -> "beautified":
    return [apply_brightness(crop) for crop in crops]              # 调用工具函数

def display(frame, landmarks, beautified) -> "displayed":          # 多输入
    cv2.imshow("win", frame)
    return True


# ─── 组装 ──────────────────────────────────────────────────────

Pipeline([
    DataSource(camera.read),    # → frame
    some_preprocess,            # → tensor
    detect,                     # → face_boxes
    get_face_crop,              # → landmarks, crops
    beautify_model,             # → beautified
    display,                    # → displayed
]).start()

数据流frame → tensor → face_boxes → (landmarks, crops) → beautified → displayed


@init_ctx 持有状态,避免全局变量

你可能会遇到这样的开发问题:

  • 全局变量满天飞
  • 流水线内需要持有状态(模型、计数器、配置等)
  • 写一个类属于过度设计

@init_ctx 把它们打包在一起。

完整可运行示例:samples/tracker_mock.py

例子:多目标追踪器

from func2stream import Pipeline, DataSource, init_ctx

@init_ctx
def create_tracker(model_path, threshold=0.5):
    # ─── 状态:模型、计数器、追踪历史 ───────────────────────────
    model = load_model(model_path)
    frame_count = 0
    track_history = {}
    
    # ─── 流水线函数 ────────────────────────────────────────────
    def detect(frame) -> "boxes":
        return [b for b in model(frame) if b.conf > threshold]
    
    def track(frame, boxes) -> "tracks":
        nonlocal frame_count
        frame_count += 1
        # ... 追踪逻辑,更新 track_history ...
        return tracks
    
    def draw(frame, tracks) -> "frame":
        for t in tracks:
            cv2.rectangle(frame, t.bbox, (0, 255, 0), 2)
        return frame
    
    # ─── 工具函数(不进流水线)─────────────────────────────────
    def get_frame_count() -> int:
        return frame_count
    
    def get_track_history() -> dict:
        return track_history
    
    return locals()


# 同一个工厂,创建两个互相隔离的追踪器
tracker_front = create_tracker("yolo.pt", threshold=0.7)
tracker_rear = create_tracker("yolo.pt", threshold=0.5)

p1 = Pipeline([
    DataSource(front_camera.read),
    tracker_front.detect,
    tracker_front.track,
    tracker_front.draw,
    display,
])

p2 = Pipeline([
    DataSource(rear_camera.read),
    tracker_rear.detect,
    tracker_rear.track,
    tracker_rear.draw,
    display,
])

p1.start()
p2.start()

# 查看状态
while min(tracker_front.get_frame_count(), tracker_rear.get_frame_count()) < 100:
    print(f"front: {tracker_front.get_frame_count()}")
    print(f"rear: {tracker_rear.get_frame_count()}")
    time.sleep(1)

gpu_model() 处理 GPU 资源

gpu_model() 可以避免 GPU 模型跨线程执行时的性能问题。

from func2stream import Pipeline, DataSource, init_ctx, gpu_model

@init_ctx
def create_detector(threshold=0.5):
    # 主线程变量
    frame_count = 0
    
    # GPU 模型 - 延迟到工作线程创建
    model = gpu_model(lambda: TRTModel(device='cuda'))
    
    def detect(frame) -> "boxes":
        nonlocal frame_count
        frame_count += 1
        return [b for b in model(frame) if b.conf > threshold]
    
    def get_count():
        return frame_count
    
    return locals()

ctx = create_detector(threshold=0.7)
Pipeline([DataSource(camera.read), ctx.detect, display]).start()

gpu_model() 接受一个无参 lambda,在工作线程首次访问时执行。调用方式与原模型一致。

示例:samples/gpu_model_trt.py


常见误区

误区 1:流水线函数不再能嵌套调用

解释:如果你有多个流水线函数,不要在某一个流水线函数里直接调用另一个流水线函数。

PS: 后期会想办法支持嵌套

def to_gray(frame) -> "gray":
    return cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

# 💥 流水线函数会被自动包装,行为不再是直接调用
def step1(frame) -> "edges":
    gray = to_gray(frame)  # 不能在这里调用 to_gray
    return cv2.Canny(gray, 50, 150)

# -------------------------------------------------------------
# ✅ 方案一:如果overlap的性能收益比较小,合并处理步骤到一个函数
def step1(frame) -> "edges":
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    return cv2.Canny(gray, 50, 150)

Pipeline([
    DataSource(...),
    step1,
])

# -------------------------------------------------------------
# ✅ 方案二:拆成独立步骤,让 Pipeline 自动串联
def canny(gray) -> "edges":
    return cv2.Canny(gray, 50, 150)

Pipeline([
    DataSource(...),
    to_gray,   # 先执行,产出 gray
    canny,     # 后执行,自动读取 gray
    ...
])

误区 2:流水线中使用的函数忘了写 -> "数据名"

PS: 这是唯一工作量,好好写吧

def process(frame):
    return frame * 2

Pipeline([
    DataSource(...),
    process,  # 💥 Pipeline 会警告,且函数运行时收到的并非 frame
])

# ✅ 放进 Pipeline 的函数,必须声明输出
def process(frame) -> "result":
    return frame * 2

误区 3:类型注解

# 👀 -> int 是类型注解,Pipeline 不会识别它
def process(frame) -> int:
    return frame * 2

# ✅ 用引号包裹的字符串才会被识别
def process(frame) -> "result":
    return frame * 2

v1.0 Breaking Changes

相比之下,早期版本更像是 geek 的玩具,现已升级到 v1.0,以下 API 已移除,使用就像注释一样简单

移除项 替代方案
@from_ctx(get=[], ret=[]) 使用 -> "key" 返回注解
build_ctx() DataSource 自动构建 ctx
MapReduce 实验性功能,已弃用(见 Roadmap)
nodrop() 无替代,该功能无使用场景

v1.0 的核心改进

  • 零装饰器数据流:只需 -> "key" 注解,Pipeline 自动识别并包装
  • 隐式 Context:参数名即读取键,无需手动声明
  • 状态隔离@init_ctx 封装模型、计数器等,避免全局变量

Roadmap

并行分支与同步(规划中)

Pipeline([
    preprocess,
    (                        # ← 元组 = 并行分支
        [branch_a1, branch_a2],   # 分支1:串行子流水线
        [branch_b1],              # 分支2
        branch_c,                 # 分支3:单函数
    ),                       # ← 元组结束 = 隐式同步点
    merge_results,           # 收到 (a2_out, b1_out, c_out) 打包
    postprocess,
]).start()

设计原则:同步点语法隐式,元组结束即同步,无需显式 Barrier。


许可证

MPL-2.0

Project details


Release history Release notifications | RSS feed

This version

1.0

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

func2stream-1.0.tar.gz (22.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

func2stream-1.0-py3-none-any.whl (24.2 kB view details)

Uploaded Python 3

File details

Details for the file func2stream-1.0.tar.gz.

File metadata

  • Download URL: func2stream-1.0.tar.gz
  • Upload date:
  • Size: 22.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for func2stream-1.0.tar.gz
Algorithm Hash digest
SHA256 9f5b4c61305194b4ae44431e332d85c08291b32b3018c9d66b725b78742cac0e
MD5 0e4efce77c49831016fd43846f9d201d
BLAKE2b-256 aa3f97b82b0a8ac9ad8abb5ec7a06cc77c35e4802e4754b319bedfb6ccbe3489

See more details on using hashes here.

Provenance

The following attestation bundles were made for func2stream-1.0.tar.gz:

Publisher: python-publish.yml on BICHENG/func2stream

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file func2stream-1.0-py3-none-any.whl.

File metadata

  • Download URL: func2stream-1.0-py3-none-any.whl
  • Upload date:
  • Size: 24.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for func2stream-1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b9428e0a6d35b2ff03294444210e5839e25f95737dc40f442635f13a67fdcdac
MD5 4f39a102823c9365191d11ccdcfe72c1
BLAKE2b-256 92f3b038bd5f031c5657a8fd914f7acd8373764c444537e21a7a83fdd8545fc5

See more details on using hashes here.

Provenance

The following attestation bundles were made for func2stream-1.0-py3-none-any.whl:

Publisher: python-publish.yml on BICHENG/func2stream

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page