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

一、功能目标
这次优化主要围绕照片标注工具的交互体验展开,目标如下:
- 标注模式和连线模式互斥,同一时间只能启用一种模式。
- 工具栏按钮表现为 option button 效果:按下一个,另一个自动弹起。
- 界面显示当前所属模式。
- 增加 Reset 模式按钮,用于清空当前选择并回到默认标注模式。
- 修复连线箭头不显示的问题。
- 连线弧度支持向线的两个方向拖拽调整。
二、模式互斥设计
最开始的问题是:标注模式和连线模式分别由两个布尔变量控制:
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))
这样曲线可以向线段两侧自由拖动,交互更加自然。
九、测试验证
为了避免后续修改破坏交互逻辑,我增加了单元测试,覆盖:
- 标注模式和连线模式互斥。
- 工具栏按钮状态符合 option button 语义。
- 新建连线默认带终点箭头。
- 连线端点从标注圆边缘开始。
- 弧度可以向两个方向拖拽。
- wx 绘制点位会从 float 转为 int。
测试命令:
python -m unittest discover -s tests
语法检查:
python -m py_compile photo_annotator.py
最终结果:
Ran 11 tests in 0.000s
OK
以上就是Python结合wxPython实现照片标注工具的实战教学的详细内容,更多关于Python wxPython照片标注的资料请关注脚本之家其它相关文章!
相关文章
Python深度学习pytorch神经网络多层感知机简洁实现
这篇文章主要为大家讲解了Python深层学习中pytorch神经网络多层感知机的简洁实现方式,有需要的朋友可以借鉴参考下,希望能够有所帮助2021-10-10
python之语句mode = 'test' if y&nb
这篇文章主要介绍了python之语句mode = 'test' if y is None else 'train'问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2023-02-02


最新评论