上一期把GUI框架搭好了,剩下的核心就是显示区——QGraphicsView。
图像加载、缩放适配、AOI绘制、坐标转换——全都靠它。
一、为什么选QGraphicsView
| QLabel | 简单 | 无交互、无缩放 | 静态图标 |
| QWidget+绘画 | 灵活 | 需自己实现缩放/平移 | 简单自定义 |
| QGraphicsView | 内置缩放/平移、场景图、事件完备 | 学习曲线略陡 | 图像分析工具首选 |
二、ImageCanvas类结构
python
class ImageCanvas(QGraphicsView):
aoi_updated = Signal(dict)
def __init__(self, parent=None):
super().__init__(parent)
self.scene = QGraphicsScene(self)
self.setScene(self.scene)
self.setAlignment(Qt.AlignCenter)
self.setDragMode(QGraphicsView.NoDrag)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.setBackgroundBrush(QColor(30, 30, 30))
# 图像数据
self.img_path = None
self.original_img = None
self.displayed_img = None
self.rgb_img = None
# AOI状态
self.aoi_mode = 'rectangle'
self.aoi_shape = None
self.aoi_drawing = False
self.aoi_points = []
# AOI图形项
self.aoi_rect_item = None
self.aoi_ellipse_item = None
self.aoi_polygon_item = None
# 标定状态
self.calibration_mode = False
self.calibration_points = []
核心属性:
| original_img | ndarray | 原始灰度图,用于计算 |
| rgb_img | ndarray | RGB格式原图,用于显示叠加 |
| displayed_img | ndarray | 当前显示的图像数组 |
| aoi_mode | str | AOI模式:rectangle/circle/polygon |
| aoi_shape | dict | 当前AOI形状数据 |
| calibration_mode | bool | 是否处于标定模式 |
三、图像加载与格式转换
安全读取(支持中文路径)
python
def safe_imread(img_path, flags=cv2.IMREAD_UNCHANGED):
try:
img = cv2.imread(img_path, flags)
if img is not None:
return img
except:
pass
try:
with open(img_path, 'rb') as f:
data = f.read()
img_array = np.frombuffer(data, np.uint8)
img = cv2.imdecode(img_array, flags)
return img
except Exception as e:
print(f"Error reading image: {e}")
return None
格式统一(16-bit → 8-bit → RGB)
python
def load_image(self, img_path):
self.img_path = img_path
img = self.safe_imread(img_path)
if img is None:
return False
self.original_img = img
# 16-bit转8-bit
if img.dtype == np.uint16:
img = (img / 256).astype(np.uint8)
# 转为RGB
if len(img.shape) == 3:
if img.shape[-1] == 4:
img = cv2.cvtColor(img, cv2.COLOR_BGRA2RGB)
else:
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
else:
img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
self.rgb_img = img
self.displayed_img = img.copy()
self.update_display()
return True
NumPy → QImage(关键!)
python
def update_display(self):
if self.displayed_img is None:
return
h, w = self.displayed_img.shape[:2]
img_copy = np.ascontiguousarray(self.displayed_img)
qimg = QImage(
img_copy.data, w, h, 3 * w, QImage.Format_RGB888
).copy()
self.scene.clear()
pixmap = QPixmap.fromImage(qimg)
self.scene.addPixmap(pixmap)
self.scene.setSceneRect(0, 0, w, h)
self.fitInView(0, 0, w, h, Qt.KeepAspectRatio)
三个关键点:
| np.ascontiguousarray() | 确保内存连续,QImage要求 |
| QImage(…).copy() | 深拷贝,防止numpy数组被回收后显示异常 |
| QImage.Format_RGB888 | RGB三通道格式 |
四、窗口自适应缩放
python
def resizeEvent(self, event):
super().resizeEvent(event)
if self.displayed_img is not None:
h, w = self.displayed_img.shape[:2]
self.fitInView(0, 0, w, h, Qt.KeepAspectRatio)
fitInView参数:
| Qt.KeepAspectRatio | 保持宽高比,等比例缩放 |
| Qt.IgnoreAspectRatio | 忽略比例,拉伸填充 |
| Qt.KeepAspectRatioByExpanding | 保持比例,完全覆盖(可能裁剪) |
本项目用 KeepAspectRatio。
Tab切换时重新适配
python
def on_notebook_tab_changed(self, index):
if index == 0:
canvas = self.original_canvas
elif index == 1:
canvas = self.result_canvas
else:
phase_name = f'phase{index – 1}'
canvas = self.phase_canvases.get(phase_name)
if canvas and canvas.displayed_img is not None:
h, w = canvas.displayed_img.shape[:2]
canvas.fitInView(0, 0, w, h, Qt.KeepAspectRatio)
五、AOI绘制交互
三种AOI模式
| rectangle | 拖拽 | {type:'rectangle', x, y, w, h} |
| circle | 拖拽(圆心+半径) | {type:'circle', cx, cy, r} |
| polygon | 点击顶点,双击闭合 | {type:'polygon', points:[(x,y),…]} |
核心鼠标事件
python
def mousePressEvent(self, event):
if self.calibration_mode:
self._calibration_mouse_press(event)
return
pos = self.mapToScene(event.pos()) # 视图坐标 → 场景坐标
x, y = pos.x(), pos.y()
if self.aoi_mode == 'rectangle':
if event.button() == Qt.LeftButton:
self.aoi_drawing = True
self.aoi_points = [(x, y)]
self._clear_aoi_items()
pen = QPen(Qt.red, 2)
self.aoi_rect_item = self.scene.addRect(x, y, 0, 0, pen)
def mouseMoveEvent(self, event):
if not self.aoi_drawing:
return
pos = self.mapToScene(event.pos())
x, y = pos.x(), pos.y()
if self.aoi_mode == 'rectangle' and self.aoi_rect_item:
x0, y0 = self.aoi_points[0]
self.aoi_rect_item.setRect(
min(x0, x), min(y0, y),
abs(x – x0), abs(y – y0)
)
def mouseReleaseEvent(self, event):
if self.aoi_drawing:
self.aoi_drawing = False
if self.aoi_mode == 'rectangle':
rect = self.aoi_rect_item.rect()
self.aoi_shape = {
'type': 'rectangle',
'x': rect.x(), 'y': rect.y(),
'w': rect.width(), 'h': rect.height()
}
self.aoi_updated.emit(self.aoi_shape)
def mouseDoubleClickEvent(self, event):
if self.aoi_mode == 'polygon' and len(self.aoi_points) >= 3:
self.aoi_shape = {
'type': 'polygon',
'points': self.aoi_points.copy()
}
self.aoi_updated.emit(self.aoi_shape)
关键:mapToScene(event.pos()) 是坐标转换的核心。
六、AOI掩码生成
python
def get_aoi_mask(self, img_shape):
if not self.aoi_shape:
return None
h, w = img_shape[:2]
mask = np.zeros((h, w), dtype=np.uint8)
if self.aoi_shape['type'] == 'rectangle':
x = int(self.aoi_shape['x'])
y = int(self.aoi_shape['y'])
w = int(self.aoi_shape['w'])
h = int(self.aoi_shape['h'])
mask[y:y+h, x:x+w] = 255
elif self.aoi_shape['type'] == 'circle':
cx = int(self.aoi_shape['cx'])
cy = int(self.aoi_shape['cy'])
r = int(self.aoi_shape['r'])
y, x = np.ogrid[:h, :w]
mask[(x – cx) ** 2 + (y – cy) ** 2 <= r ** 2] = 255
elif self.aoi_shape['type'] == 'polygon':
pts = np.array([
[int(p[0]), int(p[1])] for p in self.aoi_shape['points']
], np.int32)
cv2.fillPoly(mask, [pts], 255)
return mask
七、结果叠加显示
python
PHASE_COLORS = {
'phase1': (255, 0, 0), # 红
'phase2': (0, 0, 255), # 蓝
'phase3': (0, 255, 0), # 绿
'phase4': (255, 255, 0), # 黄
'phase5': (255, 0, 255), # 品红
}
def display_overlay(self, masks):
if self.rgb_img is None:
return
result = self.rgb_img.copy()
for phase_name, mask in masks.items():
color = PHASE_COLORS.get(phase_name, (255, 255, 255))
result[mask > 0] = color
self.displayed_img = result
self.update_display()
八、踩坑记录
QImage显示为黑色:np.ascontiguousarray() + QImage(…).copy() 深拷贝
AOI绘制位置偏移:用 mapToScene() 转换坐标
Tab切换后图像未缩放:在Tab切换信号中重新适配
下篇预告
下一篇写图像预处理模块:16-bit转8-bit、高斯滤波、对比度增强。
