Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Bug]: 3D scatter plot flips alpha order depending on depth relative to camera #22861

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

Closed
nhansendev opened this issue Apr 18, 2022 · 7 comments · Fixed by #29287
Closed

[Bug]: 3D scatter plot flips alpha order depending on depth relative to camera #22861

nhansendev opened this issue Apr 18, 2022 · 7 comments · Fixed by #29287

Comments

@nhansendev
Copy link
Contributor

Bug summary

When plotting a 3D scatter plot with multiple alpha values the resulting alpha values that are plotted depend on the order of points relative to the camera. This behavior continues even if depthshade=False is used. The behavior stops if the points are plotted individually using a for loop.

Code for reproduction

import matplotlib.pyplot as plt

fig = plt.figure()
ax = fig.add_subplot(projection="3d")

X = [i for i in range(10)]
Y = [i for i in range(10)]
Z = [i for i in range(10)]
S = [(i+1)*400 for i in range(10)]
A = [i/10 for i in range(10)]

ax.scatter(xs=X, ys=Y, zs=Z, s=S, alpha=A, depthshade=False)

plt.show()

Actual outcome

Ex: In the images below you can see the result of slightly rotating the same plot so that one end of the line of points is closer or further from the camera. This causes the list of alpha values to be applied in either the intended order, or the reverse of that order.

Screenshot from 2022-04-17 13-37-52
Screenshot from 2022-04-17 13-38-01

Expected outcome

Simply: the 3D scatter plot alpha values when depthshade=False is used should not depend on the depth from the camera.

Additional information

I've seen closed issues where something very similar was fixed for old versions of matplotlib (python 2 era), so it looks like this bug has resurfaced?

Operating system

Ubuntu

Matplotlib Version

matplotlib 3.5.1

Matplotlib Backend

No response

Python version

Python 3.9.5

Jupyter version

No response

Installation

pip

@timhoffm
Copy link
Member

Likely the same as #19787 but we missed handling Alpha in that context correctly.

@nhansendev
Copy link
Contributor Author

@timhoffm Thanks for the response. Do you have any guess on when this bug will be addressed? I imagine if it hasn't come up until now it's not going to be a high priority.

@timhoffm
Copy link
Member

timhoffm commented May 2, 2022

We have limited core developer resources and mpl_toolkits/mplot3d is only a secondary priority for the core team. Likely this needs someone in the community to pick up and provide a fix.

@nhansendev
Copy link
Contributor Author

Small update: I found that disabling the alpha on the Path3DCollection to_rbga_array call fixes the behavior, (though things get weird if you turn depthshade back on). I haven't tested much to see why/what else this affects, but I thought it was interesting enough to comment.

def _maybe_depth_shade_and_sort_colors(self, color_array):
        color_array = (
            _zalpha(color_array, self._vzs)
            if self._vzs is not None and self._depthshade
            else color_array
        )
        if len(color_array) > 1:
            color_array = color_array[self._z_markers_idx]
        return mcolors.to_rgba_array(color_array) #, self._alpha) <---- here

@nhansendev
Copy link
Contributor Author

Update: It is clear that the behavior in my original post is being caused by the current normalization method for the depthshading, in combination with the self._alpha value described in my previous post. What is strange is that the color_array in return mcolors.to_rgba_array(color_array) seems to already be adjusted with the right alpha values to match the colors being drawn, which self._alpha then overwrites with the original alpha values. To make this clearer: the order of the alphas already matches the sorted list of colors to draw, but self._alpha sets the alphas to match the default order instead. Disabling this has not resulted in any strange behavior that I can find.

Also, per the comment on the _zalpha function, I have tried my hand at implementing an alpha-scaler which represents the viewing depth instead of the normalized z-depths.

The comment, for reference:

def _zalpha(colors, zs, dscl=None):
    """Modify the alphas of the color list according to depth."""
    # FIXME: This only works well if the points for *zs* are well-spaced
    #        in all three dimensions. Otherwise, at certain orientations,
    #        the min and max zs are very close together.
    #        Should really normalize against the viewing depth.

To address this I added some code to calculate a "scale" for the view:

def do_3d_projection(self, renderer=None):
        xs, ys, zs = self._offsets3d
        vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, self.axes.M)

        self.dscl = get_data_scale(vxs, vys, vzs) # <---- here

Where get_data_scale is defined as:

def get_data_scale(X, Y, Z):
    def _m(data):
        return np.power(max(data)-min(data), 2)

    return np.power(_m(X) + _m(Y) + _m(Z), 0.5)

The self.dscl value is passed when _zalpha is called and is used to calculate the alpha multipliers for the z-depths like so:
sats = np.clip((max(zs)-zs)/dscl+0.3, 0, 1)
Note: dscl is almost always > max(zs)-min(zs) in my testing, even as the view is zoomed in and out / shifted, so np.clip is added to account for the +0.3 (min transparency) and a few edge cases

This produces depth-shading results like so:
https://user-images.githubusercontent.com/9289200/166587524-79547cf5-1747-4f39-94a3-1b44f7705f24.mp4

I think it's not quite where it needs to be yet, since there are situations where everything approaches ~50% transparency (as seen when everything is close to parallel with the screen in the video) instead of approaching full opacity.

Please let me know if you have any thoughts/suggestions on what I've found. I'll keep working on improving the depth shading in the meantime.

@nhansendev
Copy link
Contributor Author

The depth shading can be configured in four ways, with the first representing the typically desired behavior:

sats = np.clip(1-(zs-min(zs))/dscl, 0.3, 1) # Solid near, transparent far, solid default (default meaning when all are ~equal depth)
sats = np.clip((max(zs)-zs)/dscl, 0.3, 1)   # Solid near, transparent far, transparent default
sats = np.clip(1-(max(zs)-zs)/dscl, 0.3, 1) # Transparent near, solid far, solid default
sats = np.clip((zs-min(zs))/dscl, 0.3, 1)   # Transparent near, solid far, transparent default

@nhansendev
Copy link
Contributor Author

nhansendev commented May 6, 2022

Also, some videos demonstrating the updated behavior with the 'default' depth shading mode described in the previous comment:
https://user-images.githubusercontent.com/9289200/167077545-a5780d37-ca7d-487b-95c9-6c69fdb4dabd.mp4
https://user-images.githubusercontent.com/9289200/167077574-b6ee5b23-a101-4961-9e22-c467775ddfe6.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment