Issue
I'm trying to produce an onion skin effect using a QLabel in PyQt. In the simplified example below, three images are loaded in and drawn to the label using QPainter.
from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QLabel
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtGui import QImage, QPixmap, QPainter
import sys
from pathlib import Path
class MainWindow(QWidget):
def __init__(self):
super().__init__()
# -------------------------------------------------------------
# Define the display.
self.display = QLabel()
# -------------------------------------------------------------
# Import the frames
frame_1 = QImage(str(Path(f'fixtures/test_onion_skin/frame_{1}.png')))
frame_2 = QImage(str(Path(f'fixtures/test_onion_skin/frame_{2}.png')))
frame_3 = QImage(str(Path(f'fixtures/test_onion_skin/frame_{3}.png')))
# -------------------------------------------------------------
# Populate the display
frame_1_scaled = frame_1.scaled(self.size(), Qt.KeepAspectRatio)
frame_2_scaled = frame_2.scaled(self.size(), Qt.KeepAspectRatio)
frame_3_scaled = frame_3.scaled(self.size(), Qt.KeepAspectRatio)
base_pixmap = QPixmap(frame_1_scaled.size())
painter = QPainter(base_pixmap)
painter.drawImage(QPoint(), frame_3_scaled)
painter.setOpacity(0.5)
painter.drawImage(QPoint(), frame_2_scaled)
painter.setOpacity(0.3)
painter.drawImage(QPoint(), frame_1_scaled)
painter.end()
self.display.setPixmap(base_pixmap)
# -------------------------------------------------------------
# Set the layout.
layout = QGridLayout()
layout.addWidget(self.display, 0, 0)
self.setLayout(layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Ideally, the last image would show as fully opaque, with earlier images having an increasingly higher transparency. Instead, I'm getting an output where all three images blend together equally. This seems like a simple problem to fix, but my 'Google Fu' hasn't yielded much this time around.
Edit
Here are the image files. They seem have automatically been converted to .jpg unfortunately. If there's a better way to include them please let me know.
Edit 2
After some experimentation I've decided to compromise and allow some 'blending' of the base image. I'm working with raw images from a camera device, so the background of each image is always going to be non-transparent.
In case anyone is interested here is the code:
from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QLabel, QSizePolicy
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtGui import QImage, QPixmap, QPainter
import sys
from pathlib import Path
class MainWindow(QWidget):
def __init__(self):
super().__init__()
# -------------------------------------------------------------
# Define values used for image painting.
self.first_opacity = 1 # Set first image to fully opaque so it does not blend into background.
self.falloff_value = 0.15 # The opacity of the second image.
self.falloff_rate = 0.5 # A factor used to decrement subsequent image transparencies.
# -------------------------------------------------------------
# Define the display.
self.display = QLabel()
self.display.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
self.display.setMinimumSize(1, 1)
# -------------------------------------------------------------
# Import frames.
self.images = []
for i in range(1, 4, 1):
self.images.append(QImage(str(Path(f'fixtures/test_onion_skin/frame_{i}.png'))))
# -------------------------------------------------------------
# Set the display.
self.update_display()
# -------------------------------------------------------------
# Set the layout.
layout = QGridLayout()
layout.addWidget(self.display, 0, 0)
self.setLayout(layout)
def update_display(self):
# -------------------------------------------------------------
# Define the base pixmap on which to merge images.
base_pixmap = QPixmap(self.display.size())
base_pixmap.fill(Qt.transparent)
# -------------------------------------------------------------
# Preform paint cycle for images.
painter = QPainter(base_pixmap)
for (image, opacity) in zip(reversed(self.images), reversed(self.get_opacities(len(self.images)))):
painter.setOpacity(opacity)
painter.drawImage(QPoint(), image.scaled(base_pixmap.size(), Qt.KeepAspectRatio))
painter.end()
# -------------------------------------------------------------
self.display.setPixmap(base_pixmap)
def get_opacities(self, num_images):
# -------------------------------------------------------------
# Define a list to store image opacity values.
opacities = [self.first_opacity]
value = self.falloff_value
# -------------------------------------------------------------
# Calculate additional opacity values if more than one image is desired.
if num_images > 1:
num_decrements = num_images - 1
for i in range(1, num_decrements + 1, 1):
opacities.insert(0, value)
value *= self.falloff_rate
# -------------------------------------------------------------
return opacities
def resizeEvent(self, event):
self.update_display()
event.accept()
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Solution
Since the OP does not provide the images then the problem can be caused by:
- The order of how opacity is set.
- The background color of the image is not transparent.
For my demo I will use this gif and since the background is not transparent (which would be ideal) then I will apply a mask when I paint each image.
import os
from pathlib import Path
import sys
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtGui import QColor, QImageReader, QPainter, QPixmap, QRegion
from PyQt5.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget
CURRENT_DIRECTORY = Path(__file__).resolve().parent
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.display = QLabel(alignment=Qt.AlignCenter)
lay = QVBoxLayout(self)
lay.addWidget(self.display)
background_color = QColor("white")
filename = os.fspath(CURRENT_DIRECTORY / "Animhorse.gif")
image_reader = QImageReader(filename)
pixmap = QPixmap(image_reader.size())
pixmap.fill(background_color)
images = []
while image_reader.canRead():
images.append(image_reader.read())
painter = QPainter(pixmap)
for image, opacity in zip(images[3:6], (0.3, 0.7, 1.0)):
painter.setOpacity(opacity)
p = QPixmap.fromImage(image)
mask = p.createMaskFromColor(background_color, Qt.MaskInColor)
painter.setClipRegion(QRegion(mask))
painter.drawImage(QPoint(), image)
painter.end()
self.display.setPixmap(pixmap)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Answered By - eyllanesc
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.