I’m trying to compose a TrueColor image from three raster channels a VIIRS granule stored in a netCDF4 file. I’d like to do this in pure Julia but haven’t figured it out yet. I can read the data using NCDatasets.jl, but can’t figure out the composing of the RGB image so I can plot with GeoMakie.jl using Rasters.jl or the like in color constructed from three input channels.
The alternative I have tried is to use GMT.jl and RemoteS.jl. The example on the RemoteS.jl page is elegant, however everything I’ve tried to read in the data using gmtread() has been unsuccessful. My best attempt is below…
So I’m stuck. Does anyone have any suggestions for getting something like this working. I’m open to any and all solutions.
Many thanks
# following https://github.com/GenericMappingTools/RemoteS.jl
using RemoteS, GMT
Ib = gmtread("VJ102MOD.A2022017.0218.021.2022017084708.nc", varname=:observation_data/M03, grd=true);
Ig = gmtread("VJ102MOD.A2022017.0218.021.2022017084708.nc", varname=:observation_data/M04, grd=true);
Ir = gmtread("VJ102MOD.A2022017.0218.021.2022017084708.nc", varname=:observation_data/M05, grd=true);
Irgb = truecolor(Ir, Ig, Ib);
imshow(Irgb)
You can use Rasters and NCDatasets to load the data and then construct an RGB image from that.
using Rasters
using NCDatasets
using Colors
ras = Raster("pathtodata.nc")
rgb = RGB.(view(ras, Band=1)./255, view(ras, Band=2)./255, view(ras, Band=3)./255)
Here you can select the Bands or however your dimension is named with the normal Selectors from DimensionalData.
If your data contains missing values you can overload the RGB of three missing inputs, but that seems to be a bit dangerous.
I’m not sure that a Symbol like :observation_data/M03 containing a slash is supposed to work. Note that in the above example the truecolor function is expecting a UInt16 image (don’t remember if other types are accepted too). If gmtread is failing maybe gdalread can save your day. Do you have a pointer to the "VJ102MOD.A2022017.0218.021.2022017084708.nc" file so that I can try it?
@joa-quim Thanks for the reply. Storing netCDF4 datasets in groups is fairly common Here’s a link to a file. The values should be readily converted to UInt16 if necessary.
Having trouble loading the data with this simple Raster call. The bands are stored in separate netCDF datasets inside a parent group, “observation_data”, like this:
I3 = gmtread("VJ102MOD.A2022017.0218.021.2022017084708.nc", varname="observation_data/M03", gdal=true);
Warning 1: dimension #1 (number_of_pixels) is not a Longitude/X dimension.
Warning 1: dimension #0 (number_of_lines) is not a Latitude/Y dimension.
I4 = gmtread("VJ102MOD.A2022017.0218.021.2022017084708.nc", varname="observation_data/M04", gdal=true);
Warning 1: dimension #1 (number_of_pixels) is not a Longitude/X dimension.
Warning 1: dimension #0 (number_of_lines) is not a Latitude/Y dimension.
I5 = gmtread("VJ102MOD.A2022017.0218.021.2022017084708.nc", varname="observation_data/M05", gdal=true);
Warning 1: dimension #1 (number_of_pixels) is not a Longitude/X dimension.
Warning 1: dimension #0 (number_of_lines) is not a Latitude/Y dimension.
Irgb = truecolor(I3, I4, I5);
Note, one can use gdalread and it’s even a bit simpler syntax (but used code is the same)
Now, this is doing an automatic scaling to UInt8 and I need to check if it’s doing automatic histogram stretch or not.
The result is not very exciting but although it seems to be a gray image, it’s in fact a RGB color one, but one where layers have very similar value and hence it looks gray. The white lines are in the data.
It’s common to organize datasets in groups in the larger netCDF files used in the Earth Observation community (EU Copernicus and NASA). The instrument data and derived science data products are often complex - the groups help to organize things.
Thanks for opening an issue. I can provide additional example files if desired (e.g. from TROPOMI).
The real question is how can we represent these with DimensionalData.jl (so Rasters.jl/YAXArrays.jl etc). We don’t have a concept of nestedness. It would be like a DimStack of DimStack. Maybe that will work without much hassle.
Or is it better to flatten and combine the names like :observation_data_MO3
For now we can also just add a group keyword to get a Raster/RasterStack for a specific group, without the nesting
Adding the group keyword seems good if you can also specify the exact dataset. Reading all datasets in a group may work in some cases but not all. Many datasets are 2-d so they could stack easily, but some datasets are 3-d (native raster stacks) while others may be 1-d. Users specify which to read that are conformable, or read them each manually and create their own raster stacks as they see fit maybe? Perhaps this is moving away from the way things are done in Rasters.jl though…
PS I don’t think there any need to capture the nestedness of the original datasets in the internal (to Julia) representation of the data.
That looks similar to my image using my old methods in IDL, though I get a color image and it’s reversed left-to-right. I’ll try to get color out of your code as a starting point. I appreciate it.
I think the lines are expected because of how the instrument samples data in the flight direction (S-N) to accommodate larger fields of view towards the edges of the scan.
Rasters/DimensionalData can mix layer dimensions, they dont all have to be 2d.
Another question: if there are groups, does loading the first group with info on how to load the other groups with a keyword sound like a good default?
That sounds like a good idea for files where the first subgroup contains only datasets, but some groups may have a mix of datasets and groups at a second level. Not sure how to handle that effectively by default. Users who know the layout of the file can pick and choose though.
As a intermediate solution you can construct a Raster from the subgroup that you are interested in.
And then you can use the scaleminmax function from ImageCore to scale your image and construct a colorview for it.
I am not sure whether the coloring of the image is as expected. You might want to play around with the minima and maxima setting and rather use the quantile function to get some not as extreme values.
Constructing the Raster objects that way feels a bit hacky and it would be good when the missing value could be constructed directly from the metadata of the Arrays.
The problem with this data is that the bands are UInt16 arrays but they have scale two factors, and I’m emphasizing the plural because each band has a scale_factor and a radiance_scale_factor. Which one of the two should be applied? Next issue is that after applying a scale factor we no longer have a UInt16 but instead a Float array. And how then do the necessary histogram stretch that is necessary to make a color image really look like a color image?
You can do this to see the bands metadata (only showing a part of it)
If I do not apply the scale factor (which GMT.jl does by default when if finds one in a bands metadata) and skip the _FillValue’s and compute the band’s histogram plus the automatic limits for the histogram stretching, I get this (not very different for the M4 & M5 bands).
The problem is that doing this for the 3 (and tried also with manual limits) the RGB composition does not comes very convincing. So basically, I don’t know more what to do with this type of data.