PyQt5 - Bitmap Mask Operations

So I am working on a new weird idea yet again and I have some new questions regarding pyqt.

Consider some raster gradients like these:
4_SCREEN_EXTRA

So my questions goes as follows:

  1. Is it possible to use this gradient as a transparency mask for a color I choose within my code?
  2. If I have more than one image can I stack them in layers and use the transparency like you would expect in a layer stack?
  3. Can you perform operations like levels onto a image to adjust the gradient?

well I think I did most of the code for it just lacking the representation part.
only thing I managed to verify up until now is question 2)
your able to set a layers stack with transparency. no artifacts with transparencies which was a pleasant surprise. And it works with Pixmap and CSS alike.


searching for 1) and 3)

Hi

For 1), you want to get something like this?
mask

I’m not really sure of what you really want to get :slight_smile:

For 2), it seems you’ve got a solution… it could be useful for other to provide how you did it :wink:
Don’t really know your solution, but yes it’s possible to do it programmatically with code (no “CSS”, just python code)

For 3), I’m not sure that PyQt provides high-level methods to do things like this; you might need to implement this by yourself… but maybe with “Qt CSS” there’s some possibilities…

Grum999

1 Like

I am trying to make a material looking preview thing like those you have in 3D.
But I am doing a sort of pre-baked render and you change the colors of it directly according to a adapted shader.
changing the colors on the sphere is my issue now.
I am working on a nice set of passes to make a nice display of a material.

current progress:

That effect is exactly what I am trying to achieve. how did you do it?

For 2) when I ended the main code of what I wanted I started making proper renders with a alpha channel instead of black and white painted one. It is dumb but if you place a widget inside another widget (can be widget or frame or whatever) they can act as layers if the image they have inside has a transparency. My anticipation going in was that it would cause issues and it would be needed some clip or mask or something of the sort.

If 3) is not possible I might make more materials or something. But adjusting the levels would make exposure and stuff like able to be implemented too.

To change the color, I use a ‘multiply’ blending mode for composition.

You need to create a pixmap, set a canvas and work on canvas.

  • first action = set background color = color of sphere
  • second action = set composition mode
  • third action = draw your image

Note: if you use a png image source and want to keep black/white in final rendering, the multiply method can’t be used.
But as a png image source can manage alpha channel, then take advantage of alpha channel for transparency and just keep default composition mode

Here is the source code I used:

from PyQt5.Qt import *
from PyQt5 import QtCore
from PyQt5.QtGui import (
        QColor,
        QImage,
        QPixmap,
    )
from PyQt5.QtWidgets import (
        QApplication,
        QDialog,
        QLabel,
        QHBoxLayout
    )


def asMask(fileName):
    pxmAsMask = QPixmap.fromImage(QImage(fileName))
    
    pxmTgt = QPixmap(pxmAsMask.width(), pxmAsMask.height())
    
    canvas = QPainter()
    canvas.begin(pxmTgt)
    
    canvas.fillRect(0,0,pxmTgt.width(), pxmTgt.height(), QBrush(QColor(255, 0, 0)))
    
    canvas.setCompositionMode(QPainter.CompositionMode_Multiply)
    canvas.drawImage(0,0,pxmAsMask.toImage())
    
    canvas.end()

    return pxmTgt
    
def dispResult(pixmap):
    dlg = QDialog(Application.activeWindow().qwindow())
    dlg.setModal(True)
    layout = QHBoxLayout(dlg)

    lbl0 = QLabel("")
    lbl0.setPixmap(pixmap)

    layout.addWidget(lbl0)
    dlg.exec()

dispResult(asMask("/home/grum/Temp/Tmp/as_mask.png"))

This method can also be used for question 2) I think

For the 3) why to not use gradients drawn over the ball instead of using a png image as source?

Grum999

1 Like

multiply blending mode?
uhmm. I might need some time to digest this then.
multiplying a alpha with a color, makes a full color result. which voids all the transparencies. I am gonna see if there is other blending modes or if sourceIn can crop it out again.

I imagine a gradient overly would cause a rubber banding effect on the colors displayed and it’s limits to the color underneath it. Not to mention some gradients would not be as linear and might cause some artefacts on some values? I think…

Having pixels act like a vector area to paint would be the best option. I made 5 colors as output but I could do a for loop and pump alot more considering the limit cases of the shader. 5 seems enough for now though.

You have different composition modes
=> https://doc.qt.io/qt-5/qpainter.html#CompositionMode-enum

The reason why I used a multiply blending mode is because the png file you’ve provided for example don’t have any alpha channel
So, to be able to replace the white part with a colour, there’s no other solution than applying a blending mode (same principle than layers blending mode in Krita)

With an alpha channel instead of a white opaque pixel, you can use the normal blending mode (and that will be a better choice)
Example:
With this image (alpha channel instead of white pixels)
mask2
you can put in comment the following line:

#canvas.setCompositionMode(QPainter.CompositionMode_Multiply)

No, for me a blending mode affects R, G and B channels, and don’t have impact on Alpha channel.
And I tested it with PyQt, there’s no impact to alpha channel.

Following function:

def buildMask(w, h):
    img=QImage(w, h, QImage.Format_ARGB32_Premultiplied)
    img.fill(Qt.transparent)
    pxmTgt = QPixmap.fromImage(img)
    
    gradientb = QRadialGradient(QPointF(w/3, h/4), 2*w/3)
    gradientb.setColorAt(0, QColor(255,0,0))    
    gradientb.setColorAt(1, Qt.black)
    
    gradientf = QRadialGradient(QPointF(w/3, h/4), 2*w/3)
    gradientf.setColorAt(0, Qt.white)
    gradientf.setColorAt(0.15, Qt.transparent)
    gradientf.setColorAt(1, Qt.transparent)


    canvas = QPainter()
    canvas.begin(pxmTgt)
    canvas.setRenderHint(QPainter.Antialiasing)

    canvas.setPen(Qt.NoPen)

    canvas.setBrush(gradientb)
    canvas.drawEllipse(QRect(20,20,w-40, h-40));

    canvas.setBrush(gradientf)
    canvas.drawEllipse(QRect(20,20,w-40, h-40));

    canvas.end()
    return pxmTgt
dispResult(buildMask(500,500))

Generate following image:

I agree it seems there’s a small problem on white/red limit
Not sure why… my eyes? or there’s really a problem… :thinking:

Use of a predefined png image have advantage (once it’s generated, will match exactly what you want) but have some disadvantage (if you want to have it in a docker, you might need to scale it, and then loose something… and if you want to manage white/black size it will be more difficult…)

Grum999

1 Like

Then I am doing something wrong for sure. I did a color to alpha version of the previous version so I have this image for testing. Is surrounded by alpha.
A

if I use the multiply


and with multiply commented out

it just seems that the alpha channel is not respected :frowning:

however if I use this image (no alpha), it gives what you have previously shown before I spoke of the transparency aspect:
4_SCREEN_EXTRA >

The images I have been using are all pngs not sure what I am doing wrong on my end.

I was investigating more blending modes and I think I found what gives the result I am looking for it is the blend mode = Destination In.


But the “layers” need to be swaped… This class is quite confusing still for me. Is there a way to blend them in another order?

Yeah, looking your png file with alpha channel, it’s normal.

  1. alpha is kept, this is why you have the red background
  2. the multiply value is properly applied on the white pixels (so, became red)
    At final result: everything is red :slight_smile:

I’m currently not able to search for a solution, but I think you have to play with multiple layers and blending mode to build the final expected result from different images.

QPainter provides CompositionMode_DestinationIn and CompositionMode_SourceIn
Maybe with the “Source In”, you’ll be able to kept the order that is logic for you? Not tested…

Grum999

I tried out several Source and Destination modes and none worked.

While I was looking at the results this looks strangely familiar to the multiplied and pre multiplied issue that happens with transparencies in post production work.

As where I was expecting for the transparencies to appear always displayed black.

P.S. - From their site “Several composition modes require an alpha channel in the source or target images to have an effect. For optimal performance the image format Format_ARGB32_Premultiplied is preferred.”

Maybe it is a issue with png and how it is set up?

Pre-multiplication really does not work… I am at a loss.

Hi

Can you provide code and pictures used?
I’ll try to take a look on it…

Grum999

at the moment I am working on a vector version even if it is more sucky to look at. I just want to finish this test so I can I see its behavior better and see how it looks. the code in the end will borrow kinda the same things with the gradients inside. Just need a a bit of time to run this test out.

1 Like

This is just the diffuse vectorized but it is horrid XD I made something really ugly!
Not quite as I imagined it, but this was my objective.
I will make some raster maps next @Grum999 so you can test them.

1 Like

so I did a small batch of color to alpha on the diffuse pass. The selected colors were selected from the “create list of colors from image” option.

Base Color (starting color):
color #ce8800
Diffuse (end result):
chrome_defract

Mask of listed Color:
#edb800 #edb800
#682b00 #682b00
#190000 #190000
#914000 #914000
#edb800 #edb800
#d38200 #d38200
#edb800 #edb800
#e7a800 #e7a800

you can choose 1 color you like best to test, I imagine #190000 should be quite noticiable.

the code you asked before, i only did variations on your code given because this class never gives expected results so I cant put my finger on it at all.

from PyQt5.Qt import *
from PyQt5 import QtCore
from PyQt5.QtGui import (
        QColor,
        QImage,
        QPixmap,
    )
from PyQt5.QtWidgets import (
        QApplication,
        QDialog,
        QLabel,
        QHBoxLayout
    )


def asMask(fileName):
    # pxmAsMask = QPixmap.fromImage(QImage(fileName))

    qimage = QImage(fileName).convertToFormat(QImage.Format_ARGB32_Premultiplied)
    #qimage.fill(Qt.transparent)
    pxmAsMask = QPixmap.fromImage(qimage)

    pxmTgt = QPixmap(pxmAsMask.width(), pxmAsMask.height())

    canvas = QPainter()
    canvas.begin(pxmTgt)

    canvas.fillRect(0,0,pxmTgt.width(), pxmTgt.height(), QBrush(QColor(255, 0, 0)))
    # canvas.fillRect(0,0,pxmTgt.width(), pxmTgt.height(), Qt.blue)

    canvas.setCompositionMode(QPainter.CompositionMode_DestinationIn)
    canvas.drawImage(0,0,pxmAsMask.toImage())

    canvas.end()

    return pxmTgt

def dispResult(pixmap):
    dlg = QDialog(Application.activeWindow().qwindow())
    dlg.setModal(True)
    layout = QHBoxLayout(dlg)

    lbl0 = QLabel("")
    lbl0.setPixmap(pixmap)

    layout.addWidget(lbl0)
    dlg.exec()
dispResult(asMask("C:/Users/EyeOd/AppData/Roaming/krita/pykrita/blank/Test/ABlack.png"))

# self.lb10.setStyleSheet("background-color: rgba(0,0,0,0);")

According to the documentation it should be a “Pre-Multiply” and “DestinationIn” but it only gives a black output instead of transparency every time. I prepared pngs with white_color+alpha and black_color+alpha and same results. It is like their compositor is broken or needs something I am not aware to render the alpha correctly.

On my side it seems to be Ok :thinking:

from os.path import basename
from PyQt5.Qt import *
from PyQt5 import QtCore
from PyQt5.QtGui import (
        QColor,
        QImage,
        QPixmap,
    )
from PyQt5.QtWidgets import (
        QApplication,
        QDialog,
        QLabel,
        QGridLayout
    )


def checkerBoardBrush(size=32):
    """Return a checker board brush"""
    tmpPixmap = QPixmap(size,size)
    tmpPixmap.fill(QColor(255,255,255))
    brush = QBrush(QColor(220,220,220))

    canvas = QPainter()
    canvas.begin(tmpPixmap)
    canvas.setPen(Qt.NoPen)

    s1 = size>>1
    s2 = size - s1

    canvas.setRenderHint(QPainter.Antialiasing, False)
    canvas.fillRect(QRect(0, 0, s1, s1), brush)
    canvas.fillRect(QRect(s1, s1, s2, s2), brush)
    canvas.end()

    return QBrush(tmpPixmap)


def asMask(fileName):
    pxmTgt = QPixmap.fromImage(QImage(fileName))

    imgColor = QImage(pxmTgt.width(), pxmTgt.height(), QImage.Format_ARGB32_Premultiplied)
    imgColor.fill(QColor(255,0,0))
    pxmColor = QPixmap.fromImage(imgColor)

    canvas = QPainter()
    canvas.begin(pxmTgt)
    
    canvas.setCompositionMode(QPainter.CompositionMode_SourceIn)
    canvas.drawImage(0,0,pxmColor.toImage())

    canvas.end()
    
    return pxmTgt

    
    
def dispResult(files):
    dlg = QDialog(Application.activeWindow().qwindow())
    dlg.setModal(True)

    palette = QPalette()
    palette.setBrush(QPalette.Background, checkerBoardBrush())
    dlg.setPalette(palette)
    
    layout = QGridLayout(dlg)
    
    column=0
    for fileName in files:
        lbl = QLabel(basename(fileName))
        lbl.setStyleSheet("QLabel { color: black; }");
        layout.addWidget(lbl, 0, column)

        lbl = QLabel("")
        lbl.setPixmap(QPixmap.fromImage(QImage(fileName)))
        layout.addWidget(lbl, 1, column)

        lbl = QLabel("")
        lbl.setPixmap(asMask(fileName))
        layout.addWidget(lbl, 2, column)
        
        column+=1
        
    dlg.exec()

files=["/home/grum/Temporaire/Temp/tmp_imgforum/col_edb800.png",
       "/home/grum/Temporaire/Temp/tmp_imgforum/col_e7a800.png",
       "/home/grum/Temporaire/Temp/tmp_imgforum/col_d38200.png",
       "/home/grum/Temporaire/Temp/tmp_imgforum/col_914000.png",
       "/home/grum/Temporaire/Temp/tmp_imgforum/col_682b00.png",
       "/home/grum/Temporaire/Temp/tmp_imgforum/col_190000.png"]

results=[asMask(fileName) for fileName in files]

dispResult(files)

Gives:


As you can see (I’ve put a checkerboard as dialog background) the transparency is properly managed in Format_ARGB32_Premultiplied mode
(I used an image instead of drawing a rect just because it’s more eleagant and provides a faster execution result than fillRect, but otherwise result is the same)

I’m working under Linux, but normally I think the result should be the same under Windows…

And the order might be the order that’s seems the more logical for you if I understood (ie: png model below, applied color above)


Looking the video you posted, I understand you should have 3 layers:

  1. Base color (object color)
  2. Ambiant color
  3. Light color

And implementation might be something like this:

  • Base color is a drawn circle
  • Ambiant & light colors are pixmap on which you apply the color (using a blending mode)

So finally, you have 3 layers to merge to build the expected final result if I understood?

Grum999

1 Like

It is working on my side now too(windows).
it was just my inadequacy for sure on this class

I will start working on the maps and code right away.

I have been researching about this topic a bit and I think I might do a slight change of stance on my input and output values in order to accommodate various materials. or else I would be forced to ship a without end material pre rendered maps when i can just make everything editable with the various aspects of lighting.

Hopefully it will come out better and will allow people to make their own maps without the restrictions of light of the ones I give as the code looks good with those maps.

I redid a new sphere and I only have one pass changing colors but it is a good show example of it working. I just need to think the best way to interact with it.
control over all the passes or more generic controls with a 2D shader?

Blenders compositor is actually quite better than I remember it being. I was happy to use it this time around.