Add blend modes and blend groups for compositing#31162
Conversation
dee65fa to
37b0b9c
Compare
|
This looks interesting. Thanks for working on it. Since I’m not into the topic I can dare to ask the stupid questions:
|
Yup, that is correct: the history of how that "background" was constructed has no bearing on how the next Artist is blended in using its specific blend mode. It's also important to remember that that the "empty" background of an Axes is solid white, and thus not actually empty as far as these blend modes are concerned. For example, using "screen" to blend in an image on a truly empty background will just return the image, but on a white background will return solid white, which can make it look instead like the image call failed. That's why I turn off the face colors in my example above. What I still need to investigate is how my changes interact with collections of Artists. A user may want "over" blending (the default) within the collection before using a different blend mode for the collection as a whole.
Yes. In principle, the transformation functions for the hue/saturation/color/luminosity operators could have more than one possibility, but in practice I think everyone has simply used the same functions for decades (as defined in the PDF specification). |
37b0b9c to
bb554e7
Compare
2221d69 to
6d49bcf
Compare
|
This is pretty cool :-)
Actually I suspect that another possibility is to want some nonstandard blending between multiple artists, then "over" blending of the result over the background. In general I suspect this would be related to adding support for temporary, intermediate rendering buffers, which is also something that would be useful for other purposes e.g. contour label overplotting (#26971 (comment)). |
|
By the way, I decided to rename "over" to "normal". That mode of blending is referred to as "normal" often enough, and it makes it readily apparent to users that it is the standard choice (and the default). |
6d49bcf to
63f53c9
Compare
63f53c9 to
c9552d5
Compare
f49e8d0 to
f7af1e4
Compare
f7af1e4 to
d313054
Compare
d04f663 to
c287d35
Compare
If I'm following that correctly than the Which is fine/ a good example to post to the gallery, just thinking through where/how this is exposed. |
b04119a to
a12b397
Compare
|
Per the discussion at today's dev meeting, the renderer methods to open/close an isolated transparency group are implemented as new methods My niggling concern is that both |
|
The problem is that the main(?) point of open_group is to allow naming of svg groups ( |
|
Yeah, I think you're right on not putting in a warning. The false positive of emitting a warning when someone legitimately wants to name an SVG group something totally reasonable like "overlay" is not worth mitigating the hypothetical confusion of someone mistakenly calling |
|
I've implemented two more Porter-Duff compositing operators – "source" and "clear" – for both Agg and Cairo backends, but I think "source" is too confusing of a name. Since "source" provides the similar behavior as a knockout group, I have renamed it "knockout" for |
|
I've now implemented that a blend group can be an isolated group, a knockout group, both, or neither. Isolated groups are supported by all non-PS backends. Knockout groups are fully supported by only the PDF and PGF backends and partially supported by Agg and Cairo backends (cannot do non-isolated and knockout at the same time). If a knockout group is specified and the backend does not support it, the group will be rendered as non-knockout (in the images below, the panel on the right falls back to the panel to the left).
AggTop-right panel is not supported CairoTop-right panel is not supported SVGAll supported PGFAll supported Generating codeimport matplotlib
#matplotlib.use('TkCairo')
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.artist import Artist
from matplotlib.patches import Circle
class ArtistGroup(Artist):
def __init__(self, artist_list, *, group_blend_mode=None, group_alpha=1, knockout=False):
self._artist_list = artist_list
self._group_blend_mode = group_blend_mode
self._group_alpha = group_alpha
self._knockout = knockout
super().__init__()
def draw(self, renderer):
renderer.open_blend_group(self._group_blend_mode, alpha=self._group_alpha, knockout=self._knockout)
for a in self._artist_list:
if not a.is_transform_set():
a.set_transform(self.get_transform())
a.draw(renderer)
renderer.close_blend_group()
fig, axs = plt.subplots(2, 2, figsize=(6, 6), layout='constrained')
fig.set_facecolor("none")
for i, group_blend_mode in enumerate([None, "normal"]):
for j, knockout in enumerate([False, True]):
axs[i, j].set_facecolor("none")
axs[i, j].set_xlim(-1, 1)
axs[i, j].set_ylim(-1, 1)
axs[i, j].set_aspect("equal")
axs[i, j].imshow(np.arange(20*20).reshape((20, 20)) % 19,
cmap='Spectral', extent=[-1, 1, -1, 1])
left = Circle((-0.25, 0), 0.6, fc='y', alpha=0.75, blend_mode='multiply')
right = Circle((0.25, 0), 0.6, fc='g', alpha=0.75, blend_mode='multiply')
both = ArtistGroup([left, right], group_blend_mode=group_blend_mode, knockout=knockout)
axs[i, j].add_artist(both)
axs[0, 0].set_title('non-knockout')
axs[0, 1].set_title('knockout')
axs[0, 0].set_ylabel('non-isolated')
axs[1, 0].set_ylabel('isolated')
plt.show() |
|
Update: Now reverted, but I put the function at the bottom of this post in case someone in the future wants to give this approach another try I had been hoping to use the blend modes and blend groups to fix the issue of visible grid lines of I will actually likely revert this change because the alignment and edge aliasing is worse with this change, but it does get rid of the visible grid lines. Before this PRAfter this PRPreserving the attempt for posterity: def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight,
coordinates, offsets, offsetTrans, facecolors,
antialiased, edgecolors):
# Use the fallback drawing of each quad as a path if edges are stroked
if (edgecolors.size != 0 and edgecolors[0][3] != 0):
super().draw_quad_mesh(gc, master_transform, meshWidth, meshHeight,
coordinates, offsets, offsetTrans, facecolors,
antialiased, edgecolors)
return
# Otherwise, leverage the Gouraud code path with each triangle having
# three vertices of the same color, but common vertices of adjacent
# triangles can have different colors
if isinstance(coordinates, np.ma.MaskedArray):
p = coordinates.data
else:
p = coordinates
# TODO: adjust for offsets and offsetTrans
p_a = p[:-1, :-1]
p_b = p[:-1, 1:]
p_c = p[1:, 1:]
p_d = p[1:, :-1]
triangles = np.concatenate([
p_a, p_b, p_c,
p_c, p_a, p_d,
], axis=2).reshape((-1, 3, 2))
colors = np.repeat(facecolors, 6, axis=0).reshape((-1, 3, 4))
empty = (colors[:, 0, 3] == 0)
self.draw_gouraud_triangles(gc, triangles[~empty], colors[~empty],
master_transform) |
|
I didn't know, but am not really surprised that cairo's exact rendering depends on the details of the build. Looks like the difference is mostly related to antialiasing, i.e. the colors in the "middle" of the overlaying patches still match, so perhaps 1) disabling antialiasing throughout may help (set_antialias(False) on the patches)?, or 2) instead of an image comparison test, the test could be to draw two partially overlapping squares, then extract the rgba buffer and simply test the rgba values for a pixel in the middle of the first square, for a pixel in the middle of the second square, and for a pixel in the overlaying region. |
|
Here is how I have decided to resolve the Cairo image test:
|
|
This PR is ready for comprehensive review! The remaining open question in my mind is whether the helper Here's an example of using the "screen" blend mode to composite two images of the Sun at different wavelengths (171 angstroms in yellow, 211 angstroms in purple): |
|
I have relocated the two documentation pages to under "Colors" in the user guide, and linked to the blend-modes page from the existing description of normal alpha blending. Also, the conclusion at yesterday's dev meeting was that – for this PR – the helper class ( |





PR summary
This PR adds support for blend modes beyond alpha blending (e.g., "screen" or "hard light"), so closes #6210. With this PR, all artists can specify
blend_mode, and they are supported by Agg-based and Cairo-based backends, and mostly supported by SVG/PDF/PGF backends.Of course,
mplcairoprovides access to these blend modes, but this PR provides blend-mode support without needingcairo.Update: This PR uses this functionality to fix a long-standing bug (e.g., fixes #27016) with Agg rendering of Gouraud shading, where the edges of triangles would become visible when transparency is involved.
Update: This PR adds support for blend groups, which can be isolated, knockout, or both.
color dodge, color burn, hard light, soft light,
difference, exclusion
Agg showcase
Gouraud shading can look wrong with some blend modes, for the same reason it can look wrong under normal blend mode when alpha < 1, due to overlapping trianglesNow fixedCairo showcase
Gouraud shading is apparently not supported by the Cairo backend, so I commented out the pcolormesh callI added support for Gouraud shadingWindows
macOS
SVG showcase
PDF showcase
Figure_1.pdf
PGF showcase
Figure_1.pgf.pdf
Generating code
Put off to future work:
pcolormesh.snapbehaves when the mesh edges are not horizontal/verticalcontourf()/pcolor()/pcolormesh()to beTruePR checklist