pyqt5 draggable rectangles on picture - python

I am very new to pyqt5 and trying to figure out how to replicate the following idea.
Load an image.
Draw MOVEABLE rectangle(s) on top.
-- by Moveable, I mean rectangles that I can move via click and drag after initial placement. Like if I dropped it on the top right of picture, I can later move it to middle of the picture.
-- rectangles(s) multiple is very important. hopefully I can add/remove them dynamically too.
ideally show a text associated with the rectangle, like a, b, c... that is editable. (nice to have)
The following picture from LabelImg show cases the idea very well.
At this point in time, I have been able to load a picture and display it via QLabels and converting to PixMap. The self.ui, is the typical syntax for loading a UI from QT Designer.
class ImageLoader(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.ui = Ui_Form() # from QT Designer
self.ui.setupUi(self)
self.ui.frame_image_label.setMouseTracking(True)
self.ui.frame_image_label.setAcceptDrops(True)
#.... other stuff not quite relevant #####
# a special draggable Label (see below)
# The following does NOT work. (And I want to be able to make multiple labels on key-press...)
self.special_label = DragLabel('hii', self.ui.frame_image_label)
def go_to_image(self):
'''Load the image from a video into the base QLabel to show'''
self.current_frame_number, img = self.video_folder.get_frame(self.current_frame_number)
if img is None:
return
img = self.video_folder.resize_image(image = img)
pixmap = convertCvImage2QtImage(img)
if pixmap.isNull():
return
self.ui.frame_image_label.setPixmap(pixmap)
I have also created a custom DragLabel
class DragLabel(QLabel):
def __init__(self, button_text, parent):
super().__init__(button_text, parent)
def mouseMoveEvent(self, event):
if event.buttons() == Qt.LeftButton:
mimeData = QMimeData()
drag = QDrag(self)
drag.setMimeData(mimeData)
pixmap = QPixmap(self.size())
self.render(pixmap)
drag.setPixmap(pixmap)
drag.exec_(Qt.MoveAction)
The current issues:
The special_label does show up in my UI. But it doesn't move when I drag it.
It is not constrained to my self.ui.frame_image_label area
In general, the reason Im not using the LabelImg, which is very nice, is because I'm trying to label a VIDEO, and am mainly use this as "correction", ie finding where the Area of Interest was incorrectly identified and then hand-correcting them. Hence the need to able to move my selections. Also, I am trying to embed "play/pause", slider selection of frames for ease of use.
Any help is appreciated.
Edit:
Musicamante has the correct idea in that we should swap to the Graphics View Framework and use views and scenes.
QLabels with pictures is not the correct idea.

Related

Setting the background color in ThumbnailCtrl in wxPython

I'm trying to use the ThumbnailCtrl and I would like to change the background color.
I would have expected this code to work
self.thumbnail_ctrl.SetBackgroundColour('red')
Unfortunately it does not works and I ended hacking a solution show in the following minimal example in which I used the internal field _scrolled setting the background color on it.
I'm still a beginner and would like to ask if I did something wrong with the expected call show above.
#!/usr/bin/env python
import wx
import wx.lib.agw.thumbnailctrl as TC
class MyPanel(wx.Panel):
def __init__(self, parent):
super().__init__(parent=parent)
self.thumbnail_ctrl = TC.ThumbnailCtrl(parent=self, imagehandler=TC.NativeImageHandler)
self.thumbnail_ctrl.ShowFileNames(False) # do not show the filename under the thumbs
self.thumbnail_ctrl.SetThumbOutline(TC.THUMB_OUTLINE_RECT) # outline the rect
self.main_sizer = wx.BoxSizer(wx.VERTICAL)
self.main_sizer.Add(window=self.thumbnail_ctrl, proportion=1, flag=wx.ALL|wx.EXPAND, border=0)
self.SetSizer(self.main_sizer)
# Hack to set the background color of self.thumbnail_ctrl
# self.thumbnail_ctrl.SetBackgroundColour('red') # Calling this does not works
self.thumbnail_ctrl._scrolled.SetBackgroundColour('red') # Calling this the background color is set to red
self.Refresh()
class MyFrame(wx.Frame):
def __init__(self):
super().__init__(parent=None, title='minimal set background example')
self.panel = MyPanel(parent=self)
self.Show()
if __name__ == '__main__':
app = wx.App(redirect=True)
frame = MyFrame()
app.MainLoop()
As far as I remember I haven’t put in any way to set the background color of Thumbnailctrl - I simply forgot I guess. So your solution, while of course a hack, it’s a perfectly valid one. You may want to open an issue or submit a PR to wxPython github so a proper SetBackgroundColour can be added to the class in the source.

How to add an invisible line in PyQt5

This attached image is the screenshot of an application developed using PyQt5.
The image clearly has an invisible line running in the middle of the boxes enclosing the contents.
What code should I add in my program to draw an invisible line overlaying all other objects created earlier. I couldn't find any documentation regarding this but as the image suggests, it has somehow been implemented.
A code snippet is not needed to be provided by me since this is a question about adding/developing a feature rather than debugging or changing any existing code.
Premise: what you provided as an example doesn't seem a very good thing to do. It also seems more a glich than a "feature", and adding "invisible" lines like that might result in an annoying GUI for the user. The only scenario in which I'd use it would be a purely graphical/fancy one, for which you actually want to create a "glitch" for some reason. Also, note that the following solutions are not easy, and their usage requires you an advanced skill level and experience with Qt, because if you don't really understand what's happening, you'll most certainly encounter bugs or unexpected results that will be very difficult to fix.
Now. You can't actually "paint an invisible line", but there are certain work arounds that can get you a similar result, depending on the situation.
The main problem is that painting (at least on Qt) happens from the "bottom" of each widget, and each child widget is painted over the previous painting process, in reverse stacking order: if you have widgets that overlap, the topmost one will paint over the other. This is more clear if you have a container widget (such as a QFrame or a QGroupBox) with a background color and its children use another one: the background of the children will be painted over the parent's.
The (theoretically) most simple solution is to have a child widget that is not added to the main widget layout manager.
Two important notes:
The following will only work if applied to the topmost widget on which the "invisible line" must be applied.
If the widget on which you apply this is not the top level window, the line will probably not be really invisible.
class TestWithChildLine(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
for row in range(3):
for col in range(6):
layout.addWidget(QtWidgets.QDial(), row, col)
# create a widget child of this one, but *do not add* it to the layout
self.invisibleWidget = QtWidgets.QWidget(self)
# ensure that the widget background is painted
self.invisibleWidget.setAutoFillBackground(True)
# and that it doesn't receive mouse events
self.invisibleWidget.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
def resizeEvent(self, event):
super().resizeEvent(event)
# create a rectangle that will be used for the "invisible" line, wide
# as the main widget but with 10 pixel height, then center it
rect = QtCore.QRect(0, 0, self.width(), 10)
rect.moveCenter(self.rect().center())
# set the geometry of the "invisible" widget to that rectangle
self.invisibleWidget.setGeometry(rect)
Unfortunately, this approach has a big issue: if the background color has an alpha component or uses a pixmap (like many styles do, and you have NO control nor access to it), the result will not be an invisible line.
Here is a screenshot taken using the "Oxygen" style (I set a 20 pixel spacing for the layout); as you can see, the Oxygen style draws a custom gradient for window backgrounds, which will result in a "not invisible line":
The only easy workaround for that is to set the background using stylesheets (changing the palette is not enough, as the style will still use its own way of painting using a gradient derived from the QPalette.Window role):
self.invisibleWidget = QtWidgets.QWidget(self)
self.invisibleWidget.setObjectName('InvisibleLine')
self.invisibleWidget.setAutoFillBackground(True)
self.invisibleWidget.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
self.setStyleSheet('''
TestWithChildFull, #InvisibleLine {
background: lightGray;
}
''')
The selectors are required to avoid stylesheet propagation to child widgets; I used the '#' selector to identify the object name of the "invisible" widget.
As you can see, now we've lost the gradient, but the result works as expected:
Now. There's another, more complicated solution, but that should work with any situation, assuming that you're still using it on a top level window.
This approach still uses the child widget technique, but uses QWidget.render() to paint the current background of the top level window on a QPixmap, and then set that pixmap to the child widget (which now is a QLabel).
The trick is to use the DrawWindowBackground render flag, which allows us to paint the widget without any children. Note that in this case I used a black background, which shows a "lighter" gradient on the borders that better demonstrate the effect:
class TestWithChildLabel(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
layout.setSpacing(40)
for row in range(3):
for col in range(6):
layout.addWidget(QtWidgets.QDial(), row, col)
self.invisibleWidget = QtWidgets.QLabel(self)
self.invisibleWidget.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
palette = self.palette()
palette.setColor(palette.Window, QtGui.QColor('black'))
self.setPalette(palette)
def resizeEvent(self, event):
super().resizeEvent(event)
pm = QtGui.QPixmap(self.size())
pm.fill(QtCore.Qt.transparent)
qp = QtGui.QPainter(pm)
maskRect = QtCore.QRect(0, 0, self.width(), 50)
maskRect.moveTop(50)
region = QtGui.QRegion(maskRect)
self.render(qp, maskRect.topLeft(), flags=self.DrawWindowBackground,
sourceRegion=region)
qp.end()
self.invisibleWidget.setPixmap(pm)
self.invisibleWidget.setGeometry(self.rect())
And here is the result:
Finally, an further alternative would be to manually apply a mask to each child widget, according to their position. But that could become really difficult (and possibly hard to manage/debug) if you have complex layouts or a high child count, since you'd need to set (or unset) the mask for all direct children each time a resize event occurs. I won't demonstrate this scenario, as I believe it's too complex and unnecessary.

How to Autoresize QLabel pixmap keeping ratio without using classes?

We are making a GUI using PyQt and Qt Designer. Now we need that an image(pixmap) placed in a QLabel rescales nicely keeping ratio when the window is resized.
I've been reading other questions/answers but all of them use extended classes. As we are making constant changes in our UI, and it's created with Qt Creator, the .ui and (corresponding).py files are automatically generated so, if I'm not wrong, using a class-solution is not a good option for us because we should manually change the name of the class each time we update the ui.
Is there any option to autoresize the pixmap in a QLAbel keeping the ratio and avoiding using extended clases?
Thanks.
There are a couple of ways to do this.
Firstly, you can promote your QLabel in Qt Designer to a custom subclass that is written in python. Right-click the QLabel and select "Promote to...", then give the class a name (e.g. "ScaledLabel") and set the header file to the python module that the custom subclass class will be imported from (e.g. 'mylib.classes').
The custom subclass would then re-implement the resizeEvent like this:
class ScaledLabel(QtGui.QLabel):
def __init__(self, *args, **kwargs):
QtGui.QLabel.__init__(self)
self._pixmap = QtGui.QPixmap(self.pixmap())
def resizeEvent(self, event):
self.setPixmap(self._pixmap.scaled(
self.width(), self.height(),
QtCore.Qt.KeepAspectRatio))
For this to work properly, the QLabel should have its size policy set to expanding or minimumExpanding, and the minimum size should be set to a small, non-zero value (so the image can be scaled down).
The second method avoids using a subclass and uses an event-filter to handle the resize events:
class MainWindow(QtGui.QMainWindow):
def __init__(self):
...
self._pixmap = QtGui.QPixmap(self.label.pixmap())
self.label.installEventFilter(self)
def eventFilter(self, widget, event):
if (event.type() == QtCore.QEvent.Resize and
widget is self.label):
self.label.setPixmap(self._pixmap.scaled(
self.label.width(), self.label.height(),
QtCore.Qt.KeepAspectRatio))
return True
return QtGui.QMainWindow.eventFilter(self, widget, event)
Set background-image:, background-repeat: and background-position QSS properties for your label. You may do it via Forms editor or in code QWidget::setStyleSheet.
A good starting point for QSS (with examples) - http://doc.qt.io/qt-5/stylesheet-reference.html
One way is to create a QWidget/QLabel subclass and reimplement the resizeEvent.
void QWidget::resizeEvent(QResizeEvent * event) [virtual protected]
This event handler can be reimplemented in a subclass to receive widget resize events which are passed in the event parameter. When resizeEvent() is called, the widget already has its new geometry. The old size is accessible through QResizeEvent::oldSize().
The widget will be erased and receive a paint event immediately after processing the resize event. No drawing need be (or should be) done inside this handler.
This would need to be done in C++ though, not PyQt.
Having that done, you could add your custom widget to the QtDesigner as follows:
Using Custom Widgets with Qt Designer
Incredibly, after seven years #ekhumoro's excellent answer is still pretty much the only working Python implementation that can be found around; everyone else tells what to do, but nobody gives the actual code.
In spite of this, it did not work at first for me, because I happened to have the pixmap generation somewhere else in the code - specifically, my pixmap was generated inside a function which was only activated when clicking on a button, so not during the window intialization.
After figuring out how #ekhumoro's second method worked, I edited it in order to accomodate this difference. In pratice I generalised the original code, also because I did not like (for efficiency reasons) how it added a new _pixmap attribute to the label, which seemed to be nothing more than a copy of the original pixmap.
The following his is my version; mind that I have not fully tested it, but since it is a shorter version of my original working code, it too should work just fine (corrections are welcome, though):
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# Initialize stuff here; mind that no pixmap is added to the label at this point
def eventFilter(self, widget, event):
if event.type() == QEvent.Resize and widget is self.label:
self.label.setPixmap(self.label.pixmap.scaled(self.label.width(), self.label.height(), aspectRatioMode=Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation))
return True
return QMainWindow.eventFilter(self, widget, event)
def apply_pixelmap(self, image): # This is where the pixmap is added. For simplicity, suppose that you pass a QImage as an argument to this function; however, you can obtain this in any way you like
pixmap = QPixmap.fromImage(image).scaled(new_w, new_h, aspectRatioMode=Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation)
self.label.setPixmap(pixmap)
self.label.pixmap = QPixmap(pixmap) # I am aware that this line looks like a redundancy, but without it the program does not work; I could not figure out why, so I will gladly listen to anyone who knows it
self.label.installEventFilter(self)
return
This works by setting the ScaledContents property to False and the SizePolicy to either Expanding or Ignored. Note that it might not work if the label containing the image is not set as the central widget (self.setCentralWidget(self.label), where self refers to MainWindow).

problems displaying wxBitmaps using wxPython

I've been having some problems with a program that I've been writing and would appreciate some help or input. For some background, I'm using Python 2.7 and wxPython in order to do a streaming webcam client. The client gets the images from the server in its own thread, and puts them into a Queue. The GUI thread then gets those images from the Queue and converts them to a wxBitmap object. This happens every .5 seconds and works just great. I am able to save the wxBitmap object as a file so I know that everything is working properly.
The problem that I'm having is actually getting the wxBitmap object to show up on my GUI. The only thing I seem to be able to make the GUI do is display a gray rectangle where the web cam image should be.
Here is my onPaint() that is called when I want to refresh the screen:
def onPaint(self,e):
## this is the function that actually draws and redraws the window
## to be displayed. I think it is something similar to blit()
## in other graphical display frameworks
print "in onPaint"
## create the device context object (graphics painter)
dc = wx.PaintDC(self)
dc.BeginDrawing()
## draw the bitmap to the screen
dc.DrawBitmap(self.imageBit,0,0,True)
dc.EndDrawing()
## test code.
## the following works and updates, which means that
## everything is being converted properly and updated.
## not sure why the dc won't paint it to the window.
self.imageBit.SaveFile("bit.bmp", wx.BITMAP_TYPE_BMP)
Simply put, I'm at a loss as to why its not working. from my research I've found that because I'm on a windows machine I needed the BeginDrawing() and EndDrawing() functions, so I added them. Still doesn't work. There are no errors or exceptions being thrown.
other questions that might help solve this issue:
I'm updating a wxFrame object. Maybe the wxPaintDC needs to operate in another type of container to work?
?
Actually, maybe my __init__ function is whats holding the problem. Am I setting this up properly?
class viewWindow(wx.Frame):
imgSizer = (480,360)
def __init__(self, *args, **kw):
## this is called when an instance of this class is created
super(viewWindow,self).__init__(*args,**kw)
## here is where the actual stuff inside the frame is set up.
self.pnl = wx.Panel(self)
## create a button that opens up a Connection Window
#test = wx.Button(self.pnl, label='Connection Settings')
## test.Bind(wx.EVT_BUTTON, self.openConnectionWindow)
## create the wxImage for the web cam pic
self.image = wx.EmptyImage(self.imgSizer[0],self.imgSizer[1])
## create the wxBitmap so that the wxImage can be displayed
self.imageBit = wx.BitmapFromImage(self.image)
## create a timer that will update the window based of frame rate
self.timex = wx.Timer(self, wx.ID_OK)
self.timex.Start(500)
self.Bind(wx.EVT_TIMER, self.redraw, self.timex)
## need to do the following in order to display images in wxPython:
self.Bind(wx.EVT_PAINT, self.onPaint)
self.SetSize(self.imgSizer)
self.SetTitle('View Window')
self.Show()
Anyways, thanks in advance for your help.
EDIT: I solved the problem accidentally by deleting the line self.pnl = wx.Panel(self).
So apparently it was rendering properly, but the bitmap was underneath the panel. Maybe? I'm not really sure. I'm new to this whole wxPython thing.
That appears to be what the wxPython demo is doing too: dc.DrawBitmap. And it works on Windows! At least, that's what they do in the AlphaDrawing demo. In the DrawImage demo, they use dc.Blit(). You might try that.
However, I wonder if you couldn't do it like I did with my photo viewer. I don't use DCs to draw, but instead just use a wx.StaticBitmap that I update.
This code works. It displays the images every time and all that. It does tend to 'flicker', though. So there is probably a better way of doing this that I'm not aware of.
class viewWindow(wx.Frame):
imgSizer = (480,360)
def __init__(self, parent, title="View Window"):
super(viewWindow,self).__init__(parent)
## create the menu and its sub trees
menubar = wx.MenuBar()
filemenu = wx.Menu()
menubar.Append(filemenu, 'File')
self.fitem = filemenu.Append(wx.ID_ANY, 'Open Connection Window')
self.Bind(wx.EVT_MENU, self.openConnectionWindow, self.fitem)
self.SetMenuBar(menubar)
## here is where the actual stuff inside the frame is set up.
self.pnl = wx.Panel(self)
self.vbox = wx.BoxSizer(wx.VERTICAL)
## create the wxImage for the web cam pic
self.image = wx.EmptyImage(self.imgSizer[0],self.imgSizer[1])
## create the wxBitmap so that the wxImage can be displayed
self.imageBit = wx.BitmapFromImage(self.image)
self.staticBit = wx.StaticBitmap(self.pnl,wx.ID_ANY, self.imageBit)
## add the staticBit to the sizer so it is rendered properly on resizes and such
## note: not actually needed to get the image to display, but reccommended for ease
## of layout
self.vbox.Add(self.staticBit)
## register the sizer with the panel so the panel knows to use it.
self.pnl.SetSizer(self.vbox)
## create a timer that will update the window based on frame rate
self.timex = wx.Timer(self, wx.ID_OK)
self.timex.Start(1000/framerate)
self.Bind(wx.EVT_TIMER, self.redraw, self.timex)
## set the size of the frame itself when it is first opened
self.SetSize(self.imgSizer)
self.Show()
def openConnectionWindow(self, e):
## this will open a new connection window
connect = connectionWindow(None)
def redraw(self,e):
## this function updates the frame with the latest web cam image that has been
## retrieved by the client thread from the server.
## get the newest image in the queue
if not imgQ.empty():
picz = imgQ.get()
## convert the image from a string to something usable (wxImage)
self.image.SetData(picz)
## from wxImage to wxBitmap
self.imageBit = wx.BitmapFromImage(self.image)
self.staticBit = wx.StaticBitmap(self.pnl,wx.ID_ANY, self.imageBit)
## refresh the frame
self.Refresh()
Several hours later, and researching a different question I had, I find this:
How to detect motion between two PIL images? (wxPython webcam integration example included)
That has example code in it that works beautifully.

What is the most efficient way to display multiple pixmaps into a scroll area?

I am trying to make an application that displays a PDF file, using PyQt4 and python-poppler-qt4.
So far I have managed to display the entire document by loading pixmaps generated with Poppler, set on a QLabel and appended to a QFrame. The QFrame is displayed in a QScrollArea.
It looks pretty good, until implementing zooming, which is done by regenerating the pixmaps all over again, with an incremented resolution. This process requires the entire document to be rendered into pixmaps, which obviously takes time and results into an unwanted lag.
Logic wants that I should display images of the pages I am seeing only (it sounds like quantum physics). I have two options in mind:
create blank pages with QLabels and load the image onto them when they become visible in the scroll area;
create only one page and add or remove precedent or subsequent pages right before it should be displayed.
I am not sure I am on the right track or whether there is an alternative.
The first option seems more feasible, because the visibility of a blank page determines when the pixmap has to be uploaded (although I have no idea how to delete that pixmap when the page is hidden). Yet I am not sure that zooming will be faster this way, since a document of, say, 600 pages, will have to be regenerated, albeit with blank pages.
The second option should definitely improve zooming since 1 to 4 pages at a time would have to be regenerated when zooming. In that second case however, I am not sure how to trigger the construction of pages.
What would you suggest?
wouldn't it be simple to forget the QLabels and directly draw the image:
from PyQt4.QtGui import *
import sys
app = QApplication(sys.argv)
class Test(QWidget):
def __init__(self):
super(Test, self).__init__()
self.painter = QPainter()
# placeholder for the real stuff to draw
self.image = QImage("/tmp/test.jpg")
def paintEvent(self, evt):
rect = evt.rect()
evt.accept()
print rect
self.painter.begin(self)
zoomedImage = self.image # ... calculate this for your images
sourceRect = rect # ... caluclate this ...
# draw it directly
self.painter.drawImage(rect, self.image, sourceRect)
self.painter.end()
t = Test()
t.setGeometry(0,0,600,800)
s = QScrollArea()
s.setWidget(t)
s.setGeometry(0,0,300,400)
s.show()
app.exec_()
I've worked out an answer, using option 1 in the question:
def moveEvent(self, event):
self.checkVisibility()
event.ignore()
def resizeEvent(self, event):
self.checkVisibility()
event.ignore()
def checkVisibility(self):
print "Checking visibility"
for page in self.getPages():
if not page.visibleRegion().isEmpty():
if page.was_visible:
pass
else:
print page.page_number, "became visible"
page.was_visible = True
self.applyImageToPage(page)
else:
if page.was_visible:
print page.page_number, "became invisible"
page.was_visible = False
else:
pass
def applyImageToPage(self, page):
print "applying image to page", page.page_number
source = self.getSourcePage(self.getPageNumber(page))
scale = self.display.scale
# this is where the error occurs
image = source.renderToImage(72 * scale, 72 * scale)
pixmap = QtGui.QPixmap.fromImage(image)
page.setPixmap(pixmap)

Categories