Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Brush/Pen transparency weirdness #16

Open
a-hurst opened this issue Jan 11, 2018 · 15 comments
Open

Brush/Pen transparency weirdness #16

a-hurst opened this issue Jan 11, 2018 · 15 comments
Labels
bug help wanted Primary maintainers may not have time to resolve this

Comments

@a-hurst
Copy link
Contributor

a-hurst commented Jan 11, 2018

For various reasons, I've been using a pretty hacky method for applying transparency to aggdraw-drawn textures to surfaces in the Python package I maintain. This consisted of slicing off the 'A' value of any RGBA colours, creating a Brush and/or Pen with full opacity, and then applying the transparency value manually to the 'A' value for each pixel in the image using numpy. Last week, however, I figured out a way to fix the underlying problem that necessitated this workaround and decided to remove it and start using the regular Brush/Pen opacity values.

Much to my surprise, however, this produced dramatically different (and wrong) results. See the following two images:

screen shot 2018-01-11 at 10 58 54 am

screen shot 2018-01-11 at 11 04 32 am

In both images, the semi-transparent circle over the '6' is supposed to have a fill value of (192, 192, 192, 128). The first is done with the old numpy transparency hack, the second with an aggdraw Brush. As you can see, the aggdraw one is considerably darker than it should be.

Curious, I tried playing around with the Pillow image context aggdraw is drawing to, and sure enough, the fill colour of the fully transparent background affects the resulting colour of the aggdraw-drawn circle. For example, when setting opacity with aggdraw, drawing on a background with (255, 255, 255, 0) fill results in an actual fill (printing out the the pixel values to terminal) of (223, 223, 223, 127), whereas drawing on a background of (0, 0, 0, 0) fill results in an actual fill of (95, 95, 95, 127). When using the Numpy hack, it results in a background of (192, 192, 192, 128) regardless of what colour the transparent background fill has.

I'm guessing what's happening here is that aggdraw is trying to make up the difference between the background colour and the Brush colour as if it were an RGB surface without alpha, resulting in double-opacity. Is this something that's a quick fix in aggdraw, or is it a problem with the underling AGG library itself? I'd try my hand at a fix, but I have a lot on my plate programming-wise right now and I'm not too experienced in C.

Thanks in advance!

@djhoese
Copy link
Member

djhoese commented Jan 11, 2018

Are you outputting PNG images and that's what you are looking at? Are you showing the image object inside a GUI? What GUI framework? Which version of python? Given that the python 3 support is much less tested than the python 2 version of the library I'd be curious if switching from 3 to 2 changes your results.

Also what OS?

@a-hurst
Copy link
Contributor Author

a-hurst commented Jan 11, 2018

Thanks for the quick response! This is running on Python 2.7 on macOS (10.11), and is being rendered to a PyOpenGL context that's created with PySDL2. It's a runtime environment for cognitive psychology experiments, and all shape drawing is done with aggdraw as a back-end (my pictures are just screenshots).

The relevant drawing code is a bit complicated since there's a "shape object" class and a bunch of subclasses with their own drawing methods, but here's the simplified version:

# create drawing context
self.canvas = Image.new("RGBA", self.dimensions, (0, 0, 0, 0))
self.surface = Draw(self.canvas)
self.surface.setantialias(True)
# create fill
if len(color)==3: # fill colour is given in in object init
	color += [255]
self.fill = Brush(tuple(color[:3]), color[3])
# draw shape
self.surface.ellipse([x1, y1, x2, y2], self.stroke, self.fill) # x1, y1, etc. are generated based on given shape dimensions
self.surface.flush() # flush aggdraw drawings to Pillow Image context
self.rendered = numpy.asarray(self.canvas) # render Pillow image to numpy array for PyOpenGL

I'll have to check and see if it still behaves the same if I'm drawing directly to an aggdraw-created context without the Pillow "canvas". For the time being, a workaround that half-fixes it is to create the draw context image with the fill colour of the shape I'm drawing (e.g. Image.new("RGBA", self.dimensions, (192, 192, 192, 0)) makes my example in my original post work as expected).

@djhoese
Copy link
Member

djhoese commented Jan 11, 2018

Is there a reason you have your background fill with an alpha of 0? I get similar results to you in my own tests, but if I set the alpha of the Image to 255 then the results make a lot more sense. I'm not sure you can have predictable results when you are saving a transparent image...right?

@djhoese
Copy link
Member

djhoese commented Jan 11, 2018

Here is my example code:

import aggdraw
from PIL import Image
im = Image.new("RGBA", (100, 100), (255, 255, 255, 255))
draw = aggdraw.Draw(im)
draw.setantialias(True)
fill = aggdraw.Brush((255, 255, 0, 128))
draw.ellipse((25, 25, 75, 75), 2, fill)
draw.flush()

I've swapped the image fill from black to white and 0 to 255 alpha and I'm kind of wondering if the color of a transparent shape over a transparent background is a little undefined.

@djhoese
Copy link
Member

djhoese commented Jan 11, 2018

Or what if you use (255, 255, 255, 0) as the background? Does that make more sense as a result?

@djhoese
Copy link
Member

djhoese commented Jan 11, 2018

Also FYI I get the same results on python 2 and 3 with the version of aggdraw on master (no current changes from last PyPI release).

@djhoese
Copy link
Member

djhoese commented Jan 11, 2018

I get this weird affect without using PIL directly and just using aggdraw's Draw. I wonder if this is a bug in the C++ agg library. aggdraw does use a pretty old version. It is possible something like this has been fixed. It would be difficult to update aggdraw though.

@a-hurst
Copy link
Contributor Author

a-hurst commented Jan 11, 2018

The background fill has an alpha of 0 because the generated images are being used as textures by OpenGL, which handles transparency itself (i.e. all shapes and text in my screenshots above are rendered as textures and are then drawn to the display buffer). In order for overlapping textures to be able to blend, they need to be RGBA. Using a background with full opacity results in everything having a square of the background fill colour around it:

screen shot 2018-01-07 at 8 54 37 pm

If I use (255, 255, 255, 0) as the background, the texture gets brighter than it should be if there's any transparency in the shape drawn on it. If set the 'RGB' values of the background are set to the values of the Brush used to draw on it, the opacity works as expected.

Maybe AGG isn't set up to handle transparency properly when drawing to a surface with an alpha channel, so it just uses the same logic as if it's drawing to one with full opacity (i.e blending the brush colour with the background colour)? On a surface with 0 opacity I'd expect it to draw with the unaltered given Brush/Pen colour but with the opacity given (with results like I got with my hacky numpy workaround), but I'm not sure how the logic would work for something being drawn on a surface with, say, half-transparency.

@a-hurst
Copy link
Contributor Author

a-hurst commented Jan 11, 2018

There's one fork of aggdraw with the backend updated to AGG 2.4 I've come across, I'll test it out and see if it works. Unfortunately it also has different line join properties (rounded corners by default) which make it not really work as a drop-in replacement for existing projects.

EDIT: Nope, using the 2.4-based aggdraw doesn't fix it either.

@djhoese djhoese added bug help wanted Primary maintainers may not have time to resolve this labels Mar 3, 2018
@djhoese
Copy link
Member

djhoese commented Apr 21, 2018

@a-hurst This should hopefully be fixed in the current master branch and v1.3.1 release I just made. If you get a chance let me know how it goes.

@djhoese
Copy link
Member

djhoese commented Nov 26, 2018

@a-hurst I just noticed this is still open. Have you tested this lately? Does this work?

@a-hurst
Copy link
Contributor Author

a-hurst commented Nov 26, 2018

@a-hurst sorry, I realize it's been a while since you provided a potential fix and I never tested it. In my own code I used the workaround I mentioned above of setting the canvas RGB values to the RGB values of for the shape drawn on it to make everything look right, so since it was working fine that way I'd forgotten about this.

I just tested this again with 1.3.8, and unfortunately it looks like the bug is still present. Here's a screenshot of a paradigm where the Brush for the black shapes is set to 25% transparency ([0, 0, 0, 64]). Here's what it looks like when the Image.new canvas is created with a background colour of (0, 0, 0, 0):

screen shot 2018-11-26 at 12 12 37 am

Here's the exact same thing, except the Image.new canvas is created with a background colour of (255, 255, 255, 0):

screen shot 2018-11-26 at 12 13 45 am

I should really come up with a minimum reproducible example for this, but the above at least illustrates the issue.

@djhoese
Copy link
Member

djhoese commented Nov 26, 2018

Ok thanks. I reread this whole issue and now re-remember what is going on. This is very likely a bug in agg underneath. Even worse is that you said the agg 2.4 fork of this repository didn't fix your issue. That is really our only solution right now. Maybe if we can get this down to some low-level agg calls we can figure this out.

Note: I want to do something similar to this with our satpy library where we draw a ton of lines on an image with a transparent background and then burn the lines on to another image later (or cache the lines on disk). I'm not sure we need transparent lines so we may not run in to the same issue.

@a-hurst
Copy link
Contributor Author

a-hurst commented Dec 24, 2019

While trying to dig up info on the Brush size regression w/ the move to 2.4 (i.e. the larger asterisk in #61), I accidentally stumbled upon this, which I think finally explains this bug: https://sourceforge.net/p/agg/discussion/118993/thread/859d8954/

Basically, the expected type of alpha handling here only works if the destination layer format is pixfmt_rgba32_pre, whereas aggdraw's just using regular pixfmt_rgba32 for RGBA shapes. I'll have to look at the docs a bit more to understand how it differs (and whether it would be easy to substitute for pixfmt_rgba32 in aggdraw's case), but at least now we know why this is happening!

@djhoese
Copy link
Member

djhoese commented Dec 27, 2019

Wow! Great find. Any idea what has to happen in the code to make this work?

Side note: What are we going to do about the website being gone?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug help wanted Primary maintainers may not have time to resolve this
Projects
None yet
Development

No branches or pull requests

2 participants