0

I am using a QML Canvas to draw an image. The image is loaded in the backend using Python, OpenCV and NumPy and provided to QML via a custom image provider, which inherits from QQuickImageProvider. To reload an image in QML, I do the following:

unloadImage('image://<providerName>/<ID>')
loadImage('image://<providerName>/<ID>')

As long as the image is sufficiently big, this approach works well. But as soon as the image size falls under a certain threshold (on my PC roughly 1.57 MB), the unloadImage function seems to stop working. After calling it, I shouldn't be able to paint the image again, but that still works. Also the, requestImage function of my QQuickImageProvider is no longer called, which is also the case if I don't call unloadImage, before calling the loadImage function.

Does anybody know what I am doing wrong here?

Edit:

Here is a minimal code example, which reproduces my error:

main.py

import sys
import time

from PySide2 import QtGui, QtQml
from PySide2.QtQuick import QQuickImageProvider
from PySide2.QtGui import QImage
from PySide2.QtCore import QObject, Signal, Slot

import numpy as np
import cv2

class ImageProvider(QQuickImageProvider, QObject):
    def __init__(self):
        # Load images:
        # - The first image is scaled large enough
        self.img1 = np.require(np.flip(cv2.imread("messi5.jpg"), 2), np.uint8, "C")
        self.img1 = cv2.resize(self.img1, (0,0), fx=3, fy=3) 
        # - The second image is too small and won't allow a reload
        self.img2 = np.require(np.flip(cv2.imread("lena.jpg"), 2), np.uint8, "C")

        # Shown image
        self.image = None

        # Initialize parent objects
        QObject.__init__(self)
        QQuickImageProvider.__init__(self, QQuickImageProvider.Image)

    # =======
    # Signals
    # =======
    updateImage = Signal()

    # =====
    # Slots
    # =====
    def requestImage(self, ident, size, requestedSize):
        print("ImageProvider: loading image")

        format = QImage.Format_RGB888
        (h, w) = self.image.shape[:2]
        line_len = self.image.strides[0]

        img = QImage(self.image, w, h, line_len, format)

        return img

    @Slot(result="QVariantList")
    def getImageSize(self):
        h, w = self.image.shape[:2]
        return [w, h]

    @Slot()
    def changeImage(self):
        if np.array_equal(self.image, self.img1):
            self.show(self.img2) 
        else:
            self.show(self.img1) 

    # ==============
    # public methods
    # ==============
    def show(self, Image):
        self.image = Image
        self.updateImage.emit()

if __name__ == "__main__":
    # Create app
    app = QtGui.QGuiApplication(sys.argv)
    engine = QtQml.QQmlApplicationEngine()

    # Create backend
    image_provider = ImageProvider()

    # Register backend
    engine.addImageProvider("CustProv", image_provider)
    engine.rootContext().setContextProperty("CustProv", image_provider)

    # Load window
    engine.load('main.qml')
    win = engine.rootObjects()[0]
    win.show()

    # Show image
    image_provider.show(image_provider.img1)

    sys.exit(app.exec_())

main.qml

import QtQuick 2.0
import QtQuick.Controls 2.0

ApplicationWindow {
    id: root
    visible: true
    width: 800
    height: 480

    // =============
    // Event Handler
    // =============
    onHeightChanged: {
        imagePainter.resize()
    } 

    onWidthChanged: {
        imagePainter.resize()
    } 

    // ========
    // Children
    // ========
    Item {
        anchors {
            top: parent.top
            bottom: button.top
            right: parent.right
            left: parent.left
            bottomMargin: 10
        }

        Canvas {
            id: imagePainter
            anchors {
                horizontalCenter: parent.horizontalCenter
                verticalCenter: parent.verticalCenter
            }

            // =================
            // Custom Properties
            // =================
            property var size: {
                'width': -1,
                'height': -1
            }
            property real scale: 1
            property var image: 'image://CustProv/image'

            // =========
            // Functions
            // =========
            function resize() {
                console.log("Resizing canvas contents")
                var wToH =  size.width/size.height
                if (parent.width/parent.height >= wToH) {
                    // image aspect ratio is narrower than parent 
                    // aspect ratio: Use full height
                    height = parent.height
                    width = wToH * parent.height
                    scale = height/size.height
                }
                else {    
                    // image aspect ratio is wider than parent 
                    // aspect ratio: use full width   
                    width = parent.width          
                    height = parent.width / wToH
                    scale = width/size.width
                }
                // repaint the image
                requestPaint();
            }

            function reload() {
                console.log("Reload triggered")
                // First, get the new image size
                var imSize = CustProv.getImageSize()
                size.width = imSize[0]
                size.height = imSize[1]
                resize()

                // Reload image
                unloadImage(image) <--- Seems to fail for small images
                loadImage(image)
            }

            // =============
            // Event Handler
            // =============
            Component.onCompleted: {
                console.log("connecting external signals")
                CustProv.updateImage.connect(reload)
            }

            onPaint: {
                // Invoked by requestPaint()
                if (!isImageLoaded(image)) {
                    return
                }
                var ctx = getContext('2d')
                ctx.clearRect(0, 0, width, height)
                ctx.scale(scale, scale)
                ctx.drawImage(image, 0, 0)
                ctx.scale(1/scale, 1/scale)
            }

            onImageLoaded: {
                requestPaint()
            }
        }
    }


    Button {
        id: button
        anchors {
            right: parent.right
            bottom: parent.bottom
            bottomMargin: 10
            rightMargin: 10
        }

        width: 200
        height: 25

        text: "Change Image"

        onClicked: {
            CustProv.changeImage()
        }
    }
}

I did my best to shrink the example to the absolute minimum, but couldn't get it smaller than that. Sorry.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Jan Osch
  • 143
  • 1
  • 10
  • Sorry. The images are both from the [OpenCV samples](https://github.com/opencv/opencv/tree/master/samples/data): messi5.jpg has 548x342 px and lena.jpg has 512x512 px – Jan Osch Nov 27 '19 at 08:25
  • mmm, the problem has nothing to do with size but with the Canvas cache. What is your main objective? – eyllanesc Nov 27 '19 at 18:49
  • @eyllanesc: Goal is to create an application, which lets you load images onto a canvas, in order to segment them semi-automatically (for example with GrabCut). In my processing pipeline, I regularely reload painted images to display masks or reflect changes from previous steps. – Jan Osch Nov 28 '19 at 12:17
  • In the processing task, you generate "n" intermediate images, and then you want to change the image you show. I am right? – eyllanesc Nov 28 '19 at 14:30

1 Answers1

2

As per documentation :

Images returned by a QQuickImageProvider are automatically cached, similar to any image loaded by the QML engine. When an image with a "image://" prefix is loaded from cache, requestImage() and requestPixmap() will not be called for the relevant image provider. If an image should always be fetched from the image provider, and should not be cached at all, set the cache property to false for the relevant Image, BorderImage or AnimatedImage object.

So, you need to set cache property of the Image component to false.

For reloading the image, you can simply retouch the source path to null and revert it to its previous value.

Soheil Armin
  • 2,725
  • 1
  • 6
  • 18
  • Thanks for your quick response. I am using the QML Canvas, not the QML Image, or any related type. I fear that canvases don't have the cache property, as they can contain multiple images. – Jan Osch Nov 25 '19 at 13:40
  • @JanOsch, so you just need to repaint your canvas. It would be good to add a minimal code – Soheil Armin Nov 25 '19 at 15:01