返回

PyQt5 QImage 图像失真:原因分析与终极解决方案

python

PyQt5 中 QImage 显示图像失真问题分析与解决

在使用 PyQt5 处理图像时,开发者可能会遇到一个常见的问题:使用 QImage 和 NumPy 数组显示图像时出现失真。问题通常表现为图像的颜色、比例或方向不正确。这类问题经常令初学者感到困惑,其根本原因往往是数据类型、内存布局和色彩通道的不匹配。

问题分析

在代码示例中,HoughCircleDialog 类尝试将 OpenCV 处理后的 NumPy 数组转换为 QImage 来显示。关键代码段如下:

img_rgb = cv2.cvtColor(img_show, cv2.COLOR_BGR2RGB)
q_img = QImage(img_rgb.data, img_rgb.shape[1], img_rgb.shape[0], QImage.Format_RGB888)

问题可能出现在以下几个方面:

  1. 颜色通道顺序: OpenCV 默认使用 BGR 颜色通道顺序,而 PyQt5 的 QImage 使用 RGB 通道顺序。使用 cv2.cvtColor(img_show, cv2.COLOR_BGR2RGB) 可以正确转换。
  2. 步长 (Bytes Per Line):QImage 的宽度不是4的倍数时,默认的字节对齐方式可能导致每行的步长与 NumPy 数组不匹配。这通常导致图像显示拉伸或失真。
  3. QPixmap 缩放问题: QPixmap.scaled()方法缩放图像时,可能会导致质量损失,特别是缩放到小于原尺寸很多的情况下。
  4. 原始图片预处理方式: 原始图片的读取方式以及初始预处理,如旋转、裁剪,可能在最开始就造成了图像显示上的偏差。

解决方案

解决方案 1: 修正字节对齐问题,确保步长正确

问题

当图片宽度不是4的倍数时,内存对齐会导致实际图像数据的每一行的末尾都会加上padding。这样直接使用 QImage会导致图片在垂直方向出现拉伸、重叠。

解决方案:

在构造 QImage 时,通过指定正确的字节对齐,确保图像每行数据对应内存中的连续数据块。需要将宽度 * 像素通道数(BGR为3)作为每行步长。

img_rgb = cv2.cvtColor(img_show, cv2.COLOR_BGR2RGB)
height, width, channel = img_rgb.shape
bytes_per_line = channel * width # 计算每行占用的字节数
q_img = QImage(img_rgb.data, width, height, bytes_per_line, QImage.Format_RGB888)

操作步骤:

  1. 修改 update_image() 函数中创建 QImage 对象的部分, 使用修正后的代码。
  2. 重新运行程序,观察图像是否恢复正常。

代码示例:

    def update_image(self):
        # 获取当前滑块值...

        # Hough Circle Transform...
        # (省略Hough圆检测相关代码...)

        # 显示图片
        img_show = self.img_ori.copy()
        if circles is not None:
            circles = np.uint16(np.around(circles))
            for i in circles[0, :]:
                cv2.circle(img_show, (i[0], i[1]), i[2], (0, 0, 255), 2) 
        self.img_show = img_show
        self.circles = circles

        img_rgb = cv2.cvtColor(img_show, cv2.COLOR_BGR2RGB)
        height, width, channel = img_rgb.shape
        bytes_per_line = channel * width
        q_img = QImage(img_rgb.data, width, height, bytes_per_line, QImage.Format_RGB888)
        scaled_img = QPixmap.fromImage(q_img).scaled(
            self.image_label.width(), self.image_label.height(),
            Qt.KeepAspectRatio, Qt.SmoothTransformation
        )
        self.image_label.setPixmap(scaled_img)

解决方案 2:使用 copy()fromBuffer()创建QImage

问题:

直接使用NumPy数组的内存指针,如果后续有内存上的操作或者这个数组被其他地方改变可能会影响 QImage 的显示。

解决方案:

创建 QImage 时,通过使用 .copy()创建一个NumPy数组副本,或使用 fromBuffer(),防止数据被修改。 fromBuffer()方法会将 bytes 转换为QImage对象,此过程是会生成一个新的 QImage 对象。

代码示例 (使用 .copy()):

    def update_image(self):
      # (省略前面代码)
      img_rgb = cv2.cvtColor(img_show, cv2.COLOR_BGR2RGB)
      height, width, channel = img_rgb.shape
      bytes_per_line = channel * width
      img_copy = img_rgb.copy()
      q_img = QImage(img_copy.data, width, height, bytes_per_line, QImage.Format_RGB888)

      scaled_img = QPixmap.fromImage(q_img).scaled(
            self.image_label.width(), self.image_label.height(),
            Qt.KeepAspectRatio, Qt.SmoothTransformation
        )
      self.image_label.setPixmap(scaled_img)

代码示例 (使用 fromBuffer()):

    def update_image(self):
      # (省略前面代码)
      img_rgb = cv2.cvtColor(img_show, cv2.COLOR_BGR2RGB)
      height, width, channel = img_rgb.shape
      bytes_per_line = channel * width
      q_img = QImage(bytes(img_rgb.data), width, height, QImage.Format_RGB888)

      scaled_img = QPixmap.fromImage(q_img).scaled(
            self.image_label.width(), self.image_label.height(),
            Qt.KeepAspectRatio, Qt.SmoothTransformation
        )
      self.image_label.setPixmap(scaled_img)

操作步骤:

  1. 修改 update_image() 函数中创建 QImage 对象的部分, 使用.copy()fromBuffer()的方法。
  2. 重新运行程序,观察图像是否恢复正常。

解决方案 3: 使用原尺寸 QPixmap 显示,再缩放QLabel

问题:
QPixmap的初始大小与目标 QLabel的大小差别较大,特别当原图的宽度高度都大于label时候,缩放可能会造成失真。

解决方案:

先使用 QPixmap.fromImage创建QPixmap对象,并确保这个对象是原始图像大小。 然后使用 QLabel.setPixmap设置 QPixmap , 再让QLabel本身处理显示,保持宽高比,从而避免缩放造成的失真。

    def update_image(self):
        # (省略前面代码)

        img_rgb = cv2.cvtColor(img_show, cv2.COLOR_BGR2RGB)
        height, width, channel = img_rgb.shape
        bytes_per_line = channel * width
        q_img = QImage(img_rgb.data, width, height, bytes_per_line, QImage.Format_RGB888)
        
        pixmap = QPixmap.fromImage(q_img) # 获取原始大小pixmap
        self.image_label.setPixmap(pixmap)
        self.image_label.setScaledContents(True) # 设置缩放属性

操作步骤:

  1. 修改update_image()函数,按照示例进行修改。
  2. 移除缩放逻辑,并在初始化时 设置QLabelsetScaledContents属性为True

安全建议

  1. 验证图像格式: 在进行转换之前,验证 OpenCV 加载的图像格式。有时格式错误是导致显示失真的根源。
  2. 调试步骤: 当出现问题时,首先在 OpenCV 窗口中显示图像以排除 OpenCV 本身的问题。如果 OpenCV 显示正常,则问题可能出现在 PyQt5 的图像转换或显示环节。
  3. 资源清理: 如果使用 .copy()创建NumPy副本,记得及时释放无用变量的内存,以免内存泄漏。
  4. 仔细检查所有依赖库: 注意检查 opencv-python 的版本, 以避免一些已知的Bug导致一些预处理错误。

这些方法通常能够帮助解决PyQt5 中 QImage 显示图像失真问题。 开发者应根据实际情况选择合适的解决方案,并在遇到新问题时仔细检查代码,善用调试工具来定位问题。