-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
Raster colorization via "raster-color"
#12368
Conversation
"raster-color"
<div id="controls"></div> | ||
<div id="gradientbg"><div id="gradient"></div></div> | ||
|
||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.min.js"></script> |
Check warning
Code scanning / CodeQL
Inclusion of functionality from an untrusted source
const tilesets = { | ||
"mapbox.terrain-rgb": "mapbox.terrain-rgb", | ||
"mapbox.satellite": "mapbox.satellite", | ||
"rreusser.796swou6 (pop log-density)": "rreusser.796swou6", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Included the sake of illustration and debugging, but we should probably remove tilesets on my account from the final PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks amazing, and such a cool feature for so little code! Especially love the API design and a big selection of demos. Great work @rreusser, let's get this in!
From gl-native perspective it looks good! There isn't too many changes and I can port it right away after it's merged. |
@endanke Thanks for the review! 🙇 Nothing is set in stone yet, so if you have any other opinions on the design, nothing is off the table. Points of contention/compromise, as I see it:
Anyway, just wanted to get any decisions which I still find slightly unsettling on the table before etching it in stone. Thanks again! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Amazing feature @rreusser! 😍
One question: would it be possible to by default infer raster-color-range
from the expression used in raster-color
? Or would this add too much complexity/inconsistency with other expressions given the inner contents of one pre-evaluation expression would be influencing the behavior of another?
@SnailBones I think it's possible, but the default value of |
One main thing has been holding this up: ensuring this will handle everything we want to throw at it. It's been suggested to use this for categorical data, so I've outlined below why this may be challenging. How does categorical data differ from quantitative?Categorical data means different input values are mapped to different colors, but that the order has no meaning. It's an encoding but not necessarily an ordering. Categories A, B, and C may correspond to RGB values 0, 127, and 255, but they could equally well be reordered. Quantitative data, of course can be mapped to 0-255 but the ordering of each value defines the meaning. Why does this matter?Colorization of categorical data results in artifacts at the boundary between categories. The reason comes down to order of operations. There are two operations relevant here: 1) interpolation, and 2) color scale sampling. The order in which these are performed makes a big difference. As a test, I made a simple texture with solid colors and no anti-aliasing and applied the d3 spectral color scale. Desired outputWe expect simple colorization, e.g.: Actual outputWhat we actually get is boundaries that traverse the entire color scale between categories, rather than categories meeting smoothly. Why does this happen? The average of black and white is 50% gray, so when we look up that color scale value, it could be anything. Below, input rgb(0,0,0) meets rgb(255,255,255). Due to linear interpolation, the result traverses the entire color scale and we get a whole rainbow where what we really want is direct interpolation from red to purple. Where does this interpolation happen?This interpolation happens in two places:
So deactivation at the Mapbox GL layer level works fine if you have full control over the tiling. If you don't control tiling, then this problematic interpolation is baked into the tileset and there's no going back. Can we work with this?If we'd like the ability to display categorical data, then raster-sampling must be set to |
Do we actually want direct interpolation from red to purple? Wouldn't it make more sense to have nearest neighbor or something like hqx for categorical data instead? Also non-linear interpolations in MTS is a good feature to have in any case, since linear interpolation looks nasty, and the price for a nicer interpolation only has to be paid once. Is that on the roadmap at all? If it is, we could add |
@AlexanderBelokon That's a good point. The above presumes that nearest neighbor sampling is acceptable. Nicer sampling is, of course, very much preferable, but in order to accomplish that I think we'd have to (re)implement linear or better filtering in the shader, after colorization, as opposed to relying on built-in linear texture filtering. |
This PR will happen, but we're putting it temporarily on hold as we more carefully consider the API design before permanently committing to it. |
@rreusser could we do some automatic switching between nearest and linear in the shader level? So we would have two color maps, one with nearest, one with linear. Then we would need to sample the neighbouring in the original image, and if the contrast between the neighbours is larger than a given threshold then we use the nearest variant, otherwise we use linear. |
@@ -6061,6 +6070,82 @@ | |||
}, | |||
"property-type": "data-constant" | |||
}, | |||
"raster-color": { | |||
"type": "color", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rreusser should we provide a default value similarly to heatmap-color
"default": [
"interpolate",
[
"linear"
],
[
"heatmap-density"
],
0,
"rgba(0, 0, 255, 0)",
0.1,
"royalblue",
0.3,
"cyan",
0.5,
"lime",
0.7,
"yellow",
1,
"red"
],
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have strong feelings on the default. I thought maybe grayscale was reasonable because we can really just say a lot less about the meaning of the data than we can for heatmap (which is necessarily point density).
Hi, just wanted to ask if there is any news on this or an estimate on when this PR will be merged. We are really looking forward to this longed-for feature :-) |
@ghalter, thanks for your interest! We're still thinking through the details of this design and when it will become generally available. We're aiming to have more details on our plan by the summer (late June). |
I love this feature and am probably using it a lot more than you intended at this point lol, but I just want to give some feedback from a future end-user. It works perfectly for applying color maps to grayscale imagery. Like you said there are some issues with categorical data, but generally resolved with 'nearest' resampling. However, that is a bit buggy on zooming in and out, etc. I would say categorical data is going to be a very common use case so probably worth getting right. Super stoked for this feature, not sure how much I can contribute, but I have a bunch of complex use cases up and running if you need help testing. This is gonna be super popular when it hits. |
Thanks for the feedback, @shermify! It's very helpful! 🙇 In general I think there's a lot to be said for the "less is more" philosophy, that properties should be added sparingly and not every conceivable variant needs to be configurable. The categorical use case does keep arising though, and I think it mostly comes down to the distinction between colorize then interpolate (categorical) vs. interpolate then colorize (quantitative). I wonder if something like a |
I agree, simpler is better. I don't know if it even needs to be explicitly supported as a mode if it can work with tweaking existing settings. I have a map with lots of categorical data that needs to be mapped to many colors and you can see how the issue crops up. However, setting raster-resampling to "nearest" works a bit better. You also need to set raster-fade-duration to 0 or it will "flash" the boundaries when tiles load in and it seems like there are some small rendering bugs when zooming in and out where the boundaries will suddenly pop in and "stick" for some reason. Other than that it seems to work fine. Performance is great even with a lot of categorical data and color. |
hey folks, us at cruise are greatly interested in this functionality and would appreciate a new version of mapbox to include this. i will also be filing up with a support ticket to understand when this will land |
FYI for anyone following this issue, this feature is currently in Mapbox v3.0.0-beta. It was merged in via a different branch. |
Thanks for pointing this out, @shermify! I'm indeed going to close this PR as the work made its way into the beta through a different commit. 🎉 |
This PR implements a
raster-color
style spec property as originally requested in #3889. It is closely analogous in both usage and implementation to heatmap-color. The essence of this feature is to reduce an RGBA pixel input to a scalar value, then to map that to a one-dimensional color scale. For efficiency, a lookup table is tabulated on the CPU and then sampled on the GPU, so that performance impact should be minimal.This PR implements the following style-spec paint properties:
raster-color
(expression
): defines a color map by which a raster layer is colorized. Must implement a style spec expression which operates on["raster-value"]
and returns a color.raster-color-mix
(number[4]
): specifies the combination of red, green, and blue channels used to reduce input to a scalar value for color map lookup. The scalar value is computed from the source colorsrc
using the formula:src.r * mix.r + src.g * mix.g + src.b * mix.b + mix.a
. Default is luminosity, i.e.[ 0.2126, 0.7152, 0.0722, 0 ]
.raster-color-range
(number[2]
): defines the range in which the color map is tabulated. The raster-color expression is evaluated at 1024 steps (non-configurable) uniformly distributed in the range specified by raster-color-range.The video below cycles through a number of use cases highlighted by the demo page, in particular:
Note: named color scales are not part of this feature. For a demo page, I've tabulated them using d3.
raster-color.mov
Hypsometric shading example
I think an example will make usage most clear. Let's consider what's basically the most nontrivial example, in which data is rgb-encoded. (RGB-encoding is not a requirement, and grayscale inputs permitting 256 levels, for example, require only specification of
raster-color
to style.) Terrain tiles are RGB-encoded via the formulaheight = -10000 + ((R * 256 * 256 + G * 256 + B) * 0.1)
. To decode elevation, we can specify:Note the extra factors of 256. This happens because [0, 255] is normalized to [0, 1] in the shader. We now have an elevation to work with, in meters. We can define the
raster-color
property:Note that the color map is parameterized in terms of meters, from sea level (0 meters) to mount everest (8848 meters). Then finally, we direct it to tabulate the color map in this range:
The final note is that RGB-encoded values must be sampled from uncompressed png tiles. For historical reasons, such tiles must be requested from Mapbox using the extension
.pngraw
. Furthermore, Mapbox may overwrite extensions when requesting tiles in order to minimize data transfer, so that you must usetransformRequest
to force requesting of PNG tiles. We must also avoid resampling, so that when requesting tiles from Mapbox we remove'@2x'
as well. For example:The result looks something like this:
For many uses like colorization of grayscale imagery, the default values of
raster-color-mix
andraster-color-range
would be fine and onlyraster-color
would need to be specified.Additional notes
raster-color-range
so that you can define the range over which the color map is defined and evaluated.raster-color-mix
formula. Instead, the source alpha (that is, the alpha value of a tile image pixel) is carried through and applied as opacity to the final, colorized tile. The fourth component ofraster-color-mix
is instead always added as a constant offset. This feels a bit clumsy, but is what enables straightforward decoding of rgb-encoded data. For terrain, it would be the -10000 offset. See, for example, the above terrain example.cc @mapbox/gl-native (It's my intent to work on implementation if/when we feel good about this PR. I believe all shader changes are contained by ifdef's so that this should be native-compatible, with the exception of me needing to figure out how to skip render tests.)
cc @mapbox/map-design-team for initial implementation
Launch Checklist
@mapbox/map-design-team
@mapbox/static-apis
if this PR includes style spec API or visual changes@mapbox/gl-native
if this PR includes shader changes or needs a native portmapbox-gl-js
changelog:<changelog>Add raster colorization for raster layers</changelog>