python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python wxPython照片标注

Python结合wxPython实现照片标注工具的实战教学

作者:winfredzhang

这篇文章主要为大家详细介绍了Python如何基于 wxPython 的照片标注工具,可以实现在图片上添加序号标注并支持两个标注点之间的曲线连线,感兴趣的小伙伴可以了解下

摘要

最近在完善一个基于 wxPython 的照片标注工具,核心功能是:在图片上添加序号标注,并支持两个标注点之间的曲线连线。本文记录一次完整的交互优化过程,包括模式互斥、工具栏按钮状态、连线箭头显示、弧度拖拽、Reset 模式以及测试验证。

关键词:wxPython图片标注连线工具Toolbar曲线拖拽Python GUI

一、功能目标

这次优化主要围绕照片标注工具的交互体验展开,目标如下:

  1. 标注模式和连线模式互斥,同一时间只能启用一种模式。
  2. 工具栏按钮表现为 option button 效果:按下一个,另一个自动弹起。
  3. 界面显示当前所属模式。
  4. 增加 Reset 模式按钮,用于清空当前选择并回到默认标注模式。
  5. 修复连线箭头不显示的问题。
  6. 连线弧度支持向线的两个方向拖拽调整。

二、模式互斥设计

最开始的问题是:标注模式和连线模式分别由两个布尔变量控制:

self.annotation_mode = True
self.connection_mode = False

如果多个地方手动修改这两个变量,很容易出现状态不一致,比如两个模式同时为 True,或者按钮显示和实际模式不一致。

因此我抽出了统一的模式切换逻辑:

MODE_ANNOTATION = 'annotation'
MODE_CONNECTION = 'connection'

def normalize_mode_flags(mode):
    if mode == MODE_ANNOTATION:
        return True, False
    if mode == MODE_CONNECTION:
        return False, True
    raise ValueError(f'Unsupported mode: {mode}')

然后在 ImagePanel 中统一使用 set_mode()

def set_mode(self, mode):
    self.annotation_mode, self.connection_mode = normalize_mode_flags(mode)
    self.connection_start = None
    self.dragging_annotation = None
    self.dragging_connection_control = None

    if mode == MODE_ANNOTATION:
        self.selected_connection = None
        self.main_frame.select_connection_in_list(None)
    else:
        self.selected_annotation = None
        self.main_frame.select_annotation_in_list(None)

    if hasattr(self.main_frame, 'sync_mode_tools'):
        self.main_frame.sync_mode_tools(mode)

    if hasattr(self.main_frame, 'update_mode_display'):
        self.main_frame.update_mode_display(mode)

    self.Refresh()

这样所有模式切换都走同一个入口,状态管理会稳定很多。

三、Toolbar 按钮改成 Option Button 效果

普通的 wx.ITEM_CHECK 更像独立开关,不适合“二选一”的模式切换。

因此工具栏按钮改用 AddRadioTool()

ann_bmp = make_tool_bitmap('annotation')
self.ann_tool = tool_bar.AddRadioTool(
    1002, '标注', ann_bmp, wx.NullBitmap, '标注模式'
)

conn_bmp = make_tool_bitmap('connection')
self.conn_tool = tool_bar.AddRadioTool(
    1003, '连线', conn_bmp, wx.NullBitmap, '连线模式'
)

再通过统一函数同步按钮状态:

def sync_mode_tools(self, mode):
    toolbar = self.GetToolBar()
    if not toolbar:
        return

    states = get_mode_tool_states(mode)
    toolbar.ToggleTool(1002, states[MODE_ANNOTATION])
    toolbar.ToggleTool(1003, states[MODE_CONNECTION])

这样点击“标注”时,“连线”会自动弹起;点击“连线”时,“标注”会自动弹起。

四、显示当前模式

为了让用户知道当前处于哪种状态,我在右侧面板增加了模式显示:

self.mode_label = wx.StaticText(right_panel, label='当前模式:标注模式')
self.mode_label.SetFont(
    wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)
)
self.mode_label.SetForegroundColour('#0066CC')

切换模式时更新:

def update_mode_display(self, mode):
    if hasattr(self, 'mode_label'):
        label = '当前模式:标注模式' if mode == MODE_ANNOTATION else '当前模式:连线模式'
        self.mode_label.SetLabel(label)

五、Reset 模式按钮

Reset 的作用不是删除数据,而是重置当前交互状态:

def on_reset_mode(self, event):
    self.image_panel.connection_start = None
    self.image_panel.selected_connection = None
    self.image_panel.selected_annotation = None
    self.image_panel.dragging_annotation = None
    self.image_panel.dragging_connection_control = None
    self.select_annotation_in_list(None)
    self.select_connection_in_list(None)
    self.image_panel.set_mode(MODE_ANNOTATION)

用户如果连线时选错了起点,或者调整状态混乱,点击 Reset 就能回到默认标注模式。

六、修复箭头不显示的问题

连线箭头始终看不到,根因并不是箭头没有画,而是箭头画在标注圆点中心附近,之后标注圆点又被绘制在上层,导致箭头被盖住了。

解决办法是:连线不要从圆心画到圆心,而是从标注圆边缘开始,到另一个标注圆边缘结束。

def calculate_connection_endpoints(start, end, inset):
    x1, y1 = start
    x2, y2 = end
    dx, dy = x2 - x1, y2 - y1
    dist = (dx**2 + dy**2)**0.5

    if dist <= 0 or inset <= 0:
        return start, end

    usable_inset = min(inset, dist / 2)
    ux, uy = dx / dist, dy / dist

    visible_start = (x1 + ux * usable_inset, y1 + uy * usable_inset)
    visible_end = (x2 - ux * usable_inset, y2 - uy * usable_inset)

    return visible_start, visible_end

绘制时使用收缩后的端点:

(x1, y1), (x2, y2) = calculate_connection_endpoints(
    (x1, y1), (x2, y2), endpoint_inset
)

这样箭头就不会再被标注圆点遮挡。

七、修复 wx.Point 的 float 报错

端点收缩后会得到浮点数,但 wx.Point() 需要整数,否则会报错:

TypeError: Point(): arguments did not match any overloaded call

因此增加一个转换函数:

def point_to_int_tuple(point):
    x, y = point
    return int(round(x)), int(round(y))

绘制前转换:

draw_x1, draw_y1 = point_to_int_tuple((x1, y1))
draw_x2, draw_y2 = point_to_int_tuple((x2, y2))

points = [
    wx.Point(draw_x1, draw_y1),
    wx.Point(int(cx), int(cy)),
    wx.Point(draw_x2, draw_y2)
]
dc.DrawSpline(points)

八、连线弧度支持双向拖拽

原来的弧度调整偏向单侧,拖到另一侧时效果不明显。

优化后使用“鼠标点在线段法线方向上的投影”来计算弧度偏移:

def calculate_curve_offset_from_point(start, end, target, base_curve):
    cx, cy, nx, ny, curve_amount = calculate_curve_control_point(
        start, end, base_curve, 0
    )

    if curve_amount <= 0:
        return 0

    target_x, target_y = target
    projected_delta = (target_x - cx) * nx + (target_y - cy) * ny
    normalized_offset = projected_delta / curve_amount

    return max(-3, min(3, normalized_offset))

这样曲线可以向线段两侧自由拖动,交互更加自然。

九、测试验证

为了避免后续修改破坏交互逻辑,我增加了单元测试,覆盖:

测试命令:

python -m unittest discover -s tests

语法检查:

python -m py_compile photo_annotator.py

最终结果:

Ran 11 tests in 0.000s

OK

以上就是Python结合wxPython实现照片标注工具的实战教学的详细内容,更多关于Python wxPython照片标注的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文