QPixmap maintain aspect ratio - python

I'm writing a program that will allow me to upload photos to TUMBLR via their API, I've got the uploading working (thanks to you guys).
I've put a 'queueBox' on the side of the GUI, which displays the image names, and they are stored in a QListWidget. I've put this in my Main Class' constructor:
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.setupUi(self)
self.queueBox.itemClicked.connect(self.displayPhoto)
and I have this method:
def displayPhoto(self, item):
tempName = (item.text())
print tempName
self.myLabel.setPixmap(QtGui.QPixmap(_fromUtf8(directory + '\\' + tempName)))
## self.myLabel.pixmap(QPixmap.scaled(aspectRatioMode = Qt.IgnoreAspectRatio))
## ^ ^ ^ What do I do with this? How do I set it to maintain aspect ratio?
## Currently it says ''NameError: global name 'Qt' is not defined''
This sucessfully draws the image on to myLabel which is a QLabel, however, It is very scaled, I have
self.myLabel.setScaledContents(True)
in my ui_mainWindow class, and if I turn it to False, it fixes the scaling but it only shows a small portion of the image because the image is much larger than the QLabel. What I want is to be able to maintain the aspect ratio, so it doesn't look scaled and horrible.
I found this: http://www.riverbankcomputing.co.uk/static/Docs/PyQt4/html/qpixmap.html
and it says how to use it, however I can't get it to work as shown in the code above in my comments. Does anyone know how to use this? If so, can you provide me with an example, I've tried searching but most of the results I get are working examples in C++, not python.
Thanks!

Get rid of the
self.myLabel.setScaledContents(True)
call (or set it to False). It is filling your widget with the pixmap without caring about the aspect ratio.
If you need to resize a QPixmap, as you have found, scaled is the required method. But you are invoking it wrong. Let's look at the definition:
QPixmap QPixmap.scaled (self,
int width,
int height,
Qt.AspectRatioMode aspectRatioMode = Qt.IgnoreAspectRatio,
Qt.TransformationMode transformMode = Qt.FastTransformation)
Return type of this function is QPixmap, so it returns a scaled copy of the original pixmap.
Then you need a width and a height, describing the (maximum) final size of the pixmap.
Two more optional parameters. aspectRatioMode deals with the, well aspect ratio. The documentation details the different options and their effects. transformMode defines how (which algorithm) the scaling is done. It might change the final quality of your image. You probably don't need this one.
So, putting it together you should have (Qt namespace is inside QtCore):
# substitute the width and height to desired values
self.myLabel.setPixmap(QtGui.QPixmap(_fromUtf8(directory + '\\' + tempName)).scaled(width, height, QtCore.Qt.KeepAspectRatio))
Alternatively, if you have a fixed size QLabel, you could call the .size() method to get the size from it:
self.myLabel.setPixmap(QtGui.QPixmap(_fromUtf8(directory + '\\' + tempName)).scaled(self.myLabel.size(), QtCore.Qt.KeepAspectRatio))
Note: You might want to use os.path.join(directory, tempName) for the directory + '\\' + tempName part.

PyQt5 code change update:
The above answer of avaris needed a PyQt5 update because it breaks.
QPixmap.scaled (self, int width, int height, Qt.AspectRatioMode aspectRatioMode = Qt.IgnoreAspectRatio
Keeping the self in the code results in below traceback error.
TypeError: arguments did not match any overloaded call: scaled(self, int, int, aspectRatioMode: Qt.AspectRatioMode = Qt.IgnoreAspectRatio, transformMode: Qt.TransformationMode = Qt.FastTransformation): argument 1 has unexpected type 'MainUI' scaled(self, QSize, aspectRatioMode: Qt.AspectRatioMode = Qt.IgnoreAspectRatio, transformMode: Qt.TransformationMode = Qt.FastTransformation): argument 1 has unexpected type 'MainUI'
Thus this should be (without "self", "Qt") as stated below:
QPixmap.scaled (int width, int height, aspectRatioMode = IgnoreAspectRatio
or:
QPixmap.scaled (int width, int height, aspectRatioMode = 0)
KeepAspectRatio = 2... but used as provided by aspectRatioMode = 2 in above code. Enjoy!

Related

QFontMetrics leave extra space between lines

I'm trying to draw multiple paragraphs of text with PySide6's QPainter and QFontMetrics. I want to draw them with the same spacing as they would have if I drew them all in a single block of text, but the line spacing isn't quite right.
In the following example, the font metrics say that the font's line spacing is 17. When I measure a single line of text, the bounding rectangle is indeed 17 pixels high. However, when I measure two lines of text, the bounding rectangle is 35 pixels high, not 34. Where does the extra pixel come from, and can I see it on some property of the font or the font metrics?
from PySide6.QtGui import QFont, QFontMetrics
from PySide6.QtWidgets import QApplication
app = QApplication()
font = QFont()
metrics = QFontMetrics(font)
print(metrics.lineSpacing()) # 17
print(metrics.boundingRect(0, 0, 100, 100, 0, 'A').height()) # 17
print(metrics.boundingRect(0, 0, 100, 100, 0, 'A\nB').height()) # 35 != 17 * 2
print(metrics.leading()) # 0
print(metrics.ascent()) # 14
print(metrics.descent()) # 3
By the way, it isn't always one extra pixel. If I make the font bigger, the extra space increases.
Update
I thought I had figured this out with musicamante's suggestion of switching from QFontMetrics to QFontMetricsF, but there's still a difference.
from PySide6.QtCore import QRectF
from PySide6.QtGui import QFont, QFontMetricsF
from PySide6.QtWidgets import QApplication
app = QApplication()
font = QFont()
metrics = QFontMetricsF(font)
print(metrics.height()) # 16.8125
print(metrics.boundingRect(QRectF(0, 0, 100, 100),
0,
'A').getCoords()) # (0.0, 0.0, 9.9375, 16.8125)
print(metrics.boundingRect(QRectF(0, 0, 100, 100),
0,
'A\nB').getCoords()) # (0.0, 0.0, 9.9375, 34.8125)
# Note the height of that rect doesn't match the next calculation.
print(metrics.height() + metrics.lineSpacing()) # 34.046875
# I can't see any combination of these numbers that makes 34.8125
print(metrics.lineSpacing()) # 17.234375
print(metrics.leading()) # 0.421875
print(metrics.ascent()) # 13.984375
print(metrics.descent()) # 2.828125
First of all, the height of a bounding rect of a font metrics doesn't depend on the characters used, but on the font specifications.
Two lines of text don't have the double of the height() of the bounding rect of a single line: instead, you have to consider the lineSpacing().
In practice, the height of a bounding rect is normally the sum of:
the height() multiplied the number of lines;
the leading() multiplied by the number of spaces between the lines (aka: number of lines - 1);
Or, similarly, the sum of:
the ascent();
the lineSpacing() multiplied by the number of spaces between lines;
the descent();
Note that, obviously, the number of lines depends on the input text and the given options, for instance, if word wrapping was enabled and any of the source lines didn't fit the given source rectangle.
Also consider that most fonts are vectorial, meaning that their coordinates and metrics are proportional and in floating point values. QFontMetrics, instead, works with integer values for simplicity and optimization reasons, so you might get inconsistent results caused by rounding in cases for which the point size doesn't give rounded values: non integer numbers are generally "floored" (like int() in python).
In your case, the leading is probably more than 0 (but still less than 1), so you don't get a proper sum of the aforementioned heights.
Specifically, QFontMetrics.boundingRect() returns a QRect resulting by the QRectF.toAlignedRect() of the computed formatted text, which is always "the smallest possible integer rectangle that completely contains this rectangle".
If you need to get precise coordinates, you need to use QFontMetricsF, which is the floating point counterpart of the default basic QFontMetrics.
That said, if you plan on drawing formatted text with QPainter, then consider using QTextDocument or, at least, QTextLayout, which is consistent with the standard Qt text drawing and is generally faster, more reliable and "simpler" (well, once you get to know it). While it might seem a bit too complex than required, it's actually what Qt does when calling boundingRect(), so if you need custom painting, the QTextLayout option is actually better, especially if you can combine it with some smart caching (see QPicture) to avoid the common python bottleneck.

How to use a 3d array (image-like) as an index-map to slice a 4d array (video) (a generalization of the rolling shutter effect)

I've been fooling around lately with taking the webcam's video steam and giving it a pixel-dependent time delay.
A very simple example for that idea is the famous rolling shutter, but when applied in order of seconds instead of 1/100ths, it looks like this https://youtu.be/mQ0hS7l9ckY
Now, rolling shutter is fun and all, but I want something more general. I want a delay map, a (height, width, 3) shaped array that tells my how far back to go in the video. A pseudo-code for this would be
output_image[y, x, c] = video_cache[delay_map[y,x,c], y, x, c]
where the first index of the video cache is time, y,x are self-explanatory, and c is the color channel (BGR because open cv is weird).
In essence, each pixel of the output is a pixel of the video at the same position, but at a time determined by the delay map at the very same position.
Here's the solution I have now: I flattened everything, I access the video cache similar to how you unravel multi-index nonsense, and once I'm done I reshape the result into an image.
This solution works pretty fast, and I'm pretty proud of it. It almost keeps up with my webcam's frame rate (I think I average on 20 of these per second).
I think the flattening and reshaping of each frame costs me some time, and if I could get rid of those I'd get much better results.
Link to the whole file at the bottom.
Here's a skeleton of my implementation.
I have a class called CircularCacheDelayAccess. It stores a cache of video frames (with given number of frames, called cache_size in my implementation). It enables you to store frames, and get the delay-mapped frame.
Instead of pushing all the frames around each time I store a new one, I keep an index that goes around in a circle, and video[delay=3] would be found via something like cache[index-3]. Thanks to python's funny negative index tricks, I don't even have to get the positive modulo.
The delay_map is actually a float array; when I use circ_cache.getFrame I input the integer part of delay_map.flatten(), and then I use the fractional part to interpolate between frames.
class CircularCacheDelayAccess:
def __init__(self, img_shape: tuple, cache_size: int):
self.image_shape = img_shape
self.cache_size = cache_size
# some useful stuff
self.multi_index_shape = (cache_size,) + img_shape
self.image_size = int(np.prod(img_shape))
self.size = cache_size * self.image_size
# the index, going around in circles
self.cache_index = 0
self.cache = np.empty(self.size)
# raveled_image_indices is a running index over a frame; it is the same thing as writing
# y, x, c = np.mgrid[0:height, 0:width, 0:3]
# raveled_image_indices = c + 3 * (x + width * y)
# but it's a lot easier
self.raveled_image_indices = np.arange(self.image_size)
def store(self, image: np.ndarray):
# (in my implementation I check that the shape matches and raise a ValueError if it does not)
self.cache_index = (self.cache_index + 1) % self.cache_size
# since cache holds entire image frames, the start of each frame is index * image size
cIndex = self.image_size * self.cache_index
self.cache[cIndex: cIndex + self.image_size] = image.flatten()
def getFrame(self, delay_map: np.ndarray):
# delay_map may either have shape == self.image_shape, or shape = (self.image_size,)
# (more asserts, for the shape of delay_map, and to check its values do not exceed the cache size)
# (if delay_map.shape == image_shape, I flatten it. If we were already given a flattened version,
# there's no need to do so)
frame = self.cache[self.image_size * (self.cache_index - delay_map) + self.raveled_image_indices]\
.reshape(self.image_shape)
return frame
As I've already stated, this works pretty good, but I think I could get it to work better if I could just side-step the flatten and reshape steps.
Also, keeping a flattened version of an array that makes sense in its full-shaped form is pretty awkward.
And, I've mentioned the interpolation part. It felt wrong to do that in CircularCacheDelayAccess, but doing the interpolation after I getFrame twice means I need the fractional part of delay_map to be in the full-shaped form, and I need the int part flattened, which is pretty silly.\
Here are some fun examples which would probably be pretty hard to understand without seeing the video, but are still fun to look at. It looks even better with a face, but I don't think I should show my face here, so sorry about that:
horizontal rolling shutter, color delay psychedelia, my weirdest effect so far
And here is a link to the entire code, with capture and stuff if you wanna mess around with it and read the entire code.
Thanks in advance!

What causes a form to shift from its saved screen position when reloading?

When implementing the frm_obj.setGeometry(x_pos, y_pos, h_dim, v_dim) method to restore saved position and size, the form will sometimes shift 31px up and 8px left of the saved position. The 31px appears to be the height of the title bar; I have no idea where the 8px difference derives from. I can add a move() function with the same values for x and y after the setGeometry() to correct it; however, it then may (or may not) move down and right 31 and 8 px, respectively.
What would cause this and how can I check for that condition?
The code that retrieves and sets the form geometry is roughly as follows:
def get_geom(frm):
config = configparser.RawConfigParser()
config.read('settings.cfg')
x_pos = int(config.get('dialogPos', 'xpos'))
y_pos = int(config.get('dialogPos', 'ypos'))
h_dim = int(config.get('dialogPos', 'hdim'))
v_dim = int(config.get('dialogPos', 'vdim'))
frm.setGeometry(x_pos, y_pos, h_dim, v_dim)
# The move() function seems to "randomly" cause the form to crawl down/right 31x8 px
# Without it, it "randomly" crawls up/left the same dimentions.
frm.move(x_pos, y_pos)

Screenshot [ctypes.windll CreateDCFromHandle]

I am creating a screenshot module using only pure python (ctypes), no big lib like win32, wx, QT, ... It has to manage multi-screens (what PIL and Pillow cannot).
Where I am blocking is when calling CreateDCFromHandle, ctypes.windll.gdi32 does not know this function. I looked at win32 source code to being inspired, but useless. As said in comment, this function does not exist in the MSDN, so what changes should I apply to take in consideration other screens?
This is the code which works for the primary monitor, but not for others: source code.
It is blocking at the line 35. I tried a lot of combinations, looking for answers here and on others websites. But nothing functional for me ... It is just a screenshot!
Do you have clues?
Thanks in advance :)
Edit, I found my mystake! This is the code that works:
srcdc = ctypes.windll.user32.GetWindowDC(0)
memdc = ctypes.windll.gdi32.CreateCompatibleDC(srcdc)
bmp = ctypes.windll.gdi32.CreateCompatibleBitmap(srcdc, width, height)
ctypes.windll.gdi32.SelectObject(memdc, bmp)
ctypes.windll.gdi32.BitBlt(memdc, 0, 0, width, height, srcdc, left, top, SRCCOPY)
bmp_header = pack('LHHHH', calcsize('LHHHH'), width, height, 1, 24)
c_bmp_header = c_buffer(bmp_header)
c_bits = c_buffer(' ' * (height * ((width * 3 + 3) & -4)))
got_bits = ctypes.windll.gdi32.GetDIBits(memdc, bmp, 0, height,
c_bits, c_bmp_header, DIB_RGB_COLORS)
# Here, got_bits should be equal to height to tell you all goes well.
French article with full explanations : Windows : capture d'écran
Edit, I found my mystake! This is the code that works:
srcdc = ctypes.windll.user32.GetWindowDC(0)
memdc = ctypes.windll.gdi32.CreateCompatibleDC(srcdc)
bmp = ctypes.windll.gdi32.CreateCompatibleBitmap(srcdc, width, height)
ctypes.windll.gdi32.SelectObject(memdc, bmp)
ctypes.windll.gdi32.BitBlt(memdc, 0, 0, width, height, srcdc, left, top, SRCCOPY)
bmp_header = pack('LHHHH', calcsize('LHHHH'), width, height, 1, 24)
c_bmp_header = c_buffer(bmp_header)
c_bits = c_buffer(' ' * (height * ((width * 3 + 3) & -4)))
got_bits = ctypes.windll.gdi32.GetDIBits(
memdc, bmp, 0, height, c_bits, c_bmp_header, DIB_RGB_COLORS)
# Here, got_bits should be equal to height to tell you all goes well.
This isn't a Windows API function. You will need a combination of EnumDisplayDevices and CreateDC. Be aware that you must append "A" or "W" to the names of the functions depending on if you want to use ANSI strings or Unicode (widechar) strings.
Looking at the source for pywin32, CreateDCFromHandle is a fabrication. It does not exist in the Windows API; it is simply a bridge converting a Windows API thing into a pywin32 thing.
Since you're using ctypes rather than pywin32, no conversion is necessary; see if you can skip that step:
hwin = user.GetDesktopWindow()
hwindc = user.GetWindowDC(monitor['hmon'])
memdc = gdi.CreateCompatibleDC(hwindc)
When you're trying to do some native-Windows API thing with ctypes in Python, I find it more helpful to look at existing C code which already uses the Windows API rather than using Python code that uses a wrapper around it.

Maintaining view/scroll position in QGraphicsView when swapping images

I'm having trouble with zooming TIFF images loaded into a QGraphicsView with QGraphicsPixmapItem.
The problem is more maintaining image quality along with having a zoom speed that doesn't make the application choke. To begin with I was just replacing the image with a scaled QPixmap - I used Qt.FastTransformation while the user was holding down a horizontal slider and then when the slider was released replaced the pixmap again with another scaled pixmap using Qt.SmoothTransformation. This gave a nice quality zoomed image but the zooming was jerky after the image size started to increase to larger than its original size; zooming out of the image was fine.
Using QTransform.fromScale() on the QGraphicsView gives much smoother zooming but a lower quality image, even when applying .setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform | QPainter.HighQualityAntialiasing) to the QGraphicsView.
My latest approach is to combine the two methods and use a QTransform on the QGraphicsView to have the smooth zooming but when the user releases the slider replace the image in the QGraphicsView with a scaled pixmap. This works great, but the position in the view is lost - the user zooms in to one area and because the scaled pixmap is larger the view jumps to another location when the slider is released and the higher quality scaled pixmap replaces the previous image.
I figured that as the width height ratio is the same in both images I could take the percentages of the scrollbars before the image swap and apply the same percentages after the swap and things should work out fine. This works well mostly, but there are still times when the view 'jumps' after swapping the image.
I'm pretty sure I'm doing something quite wrong here. Does anybody know of a better way to do this, or can anyone spot something in the code below that could cause this jumping?
This is the code to save/restore the scrollbar location. They are methods of a subclassed QGraphicsView:
def store_scrollbar_position(self):
x_max = self.horizontalScrollBar().maximum()
if x_max:
x = self.horizontalScrollBar().sliderPosition()
self.scroll_x_percentage = x * (100 / float(x_max))
y_max = self.verticalScrollBar().maximum()
if y_max:
y = self.verticalScrollBar().sliderPosition()
self.scroll_y_percentage = y * (100 / float(y_max))
def restore_scrollbar_position(self):
x_max = self.horizontalScrollBar().maximum()
if self.scroll_x_percentage and x_max:
x = x_max * (float(self.scroll_x_percentage) / 100)
self.horizontalScrollBar().setSliderPosition(x)
y_max = self.verticalScrollBar().maximum()
if self.scroll_y_percentage and y_max:
y = y_max * (float(self.scroll_y_percentage) / 100)
self.verticalScrollBar().setSliderPosition(y)
And here is how I'm doing the scaling. self.imageFile is a QPixmap and self.image is my QGraphicsPixmapItem. Again, part of a subclassed QGraphicsView. The method is attached to the slider movement with the highQuality parameter set to False. It is called again on slider release with highQuality as True to swap the image.
def setImageScale(self, scale=None, highQuality=True):
if self.imageFile.isNull():
return
if scale is None:
scale = self.scale
self.scale = scale
self.image.setPixmap(self.imageFile)
self.scene.setSceneRect(self.image.boundingRect())
self.image.setPos(0, 0)
if not highQuality:
self.setTransform(QTransform.fromScale(self.scaleFactor, self.scaleFactor))
self.store_scrollbar_position()
else:
self.image.setPixmap(self.imageFile.scaled(self.scaleFactor * self.imageFile.size(),
Qt.KeepAspectRatio, Qt.SmoothTransformation))
self.setTransform(self.transform)
self.scene.setSceneRect(self.image.boundingRect())
self.image.setPos(0, 0)
self.restore_scrollbar_position()
return
Any help would be appreciated. I'm starting to get quite frustrated with this now.
I found a solution that works better than the code I first posted. It's still not perfect, but is much improved. Just in case anyone else is trying to solve a similar problem...
When setting the low quality image I call this method added to my QGraphicsView:
def get_scroll_state(self):
"""
Returns a tuple of scene extents percentages.
"""
centerPoint = self.mapToScene(self.viewport().width()/2,
self.viewport().height()/2)
sceneRect = self.sceneRect()
centerWidth = centerPoint.x() - sceneRect.left()
centerHeight = centerPoint.y() - sceneRect.top()
sceneWidth = sceneRect.width()
sceneHeight = sceneRect.height()
sceneWidthPercent = centerWidth / sceneWidth if sceneWidth != 0 else 0
sceneHeightPercent = centerHeight / sceneHeight if sceneHeight != 0 else 0
return sceneWidthPercent, sceneHeightPercent
This gets stored in self.scroll_state. When setting the high quality image I call another function to restore the percentages used in the previous function.
def set_scroll_state(self, scroll_state):
sceneWidthPercent, sceneHeightPercent = scroll_state
x = (sceneWidthPercent * self.sceneRect().width() +
self.sceneRect().left())
y = (sceneHeightPercent * self.sceneRect().height() +
self.sceneRect().top())
self.centerOn(x, y)
This sets the center position to the same location (percentage-wise) as I was at before swapping the image.

Categories