diff --git a/Project.toml b/Project.toml index e4b3618..b64708f 100644 --- a/Project.toml +++ b/Project.toml @@ -1,11 +1,10 @@ name = "RemoteSensingToolbox" uuid = "c88070b3-ddf6-46c7-b699-196056389566" authors = ["Joshua Billson"] -version = "0.1.0" +version = "0.2.0" [deps] ArchGDAL = "c9ce4bd3-c3d5-55b8-8973-c0e20141b8c3" -CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" @@ -19,6 +18,13 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" TableOperations = "ab02a1b2-a7df-11e8-156e-fb1833f50b87" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" +[weakdeps] +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" + +[extensions] +RemoteSensingToolboxMakieExt = "CairoMakie" + [compat] ArchGDAL = "0.10" CairoMakie = "0.10" diff --git a/ext/RemoteSensingToolboxMakieExt.jl b/ext/RemoteSensingToolboxMakieExt.jl new file mode 100644 index 0000000..80f2082 --- /dev/null +++ b/ext/RemoteSensingToolboxMakieExt.jl @@ -0,0 +1,54 @@ +module RemoteSensingToolboxMakieExt + +using RemoteSensingToolbox, CairoMakie, Rasters, Statistics +using Pipe: @pipe +import Tables + +function RemoteSensingToolbox.plot_signatures(bandset::Type{<:AbstractBandset}, raster::AbstractRasterStack, shp, label::Symbol; colors=Makie.wong_colors()) + # Create Figure + fig = Figure(resolution=(1000,500)) + + # Create Axis + ax = Axis( + fig[1,1], + xlabel="Wavelength (nm)", + ylabel="Reflectance", + xlabelfont=:bold, + ylabelfont=:bold, + xlabelpadding=10.0, + ylabelpadding=10.0, + ) + + # Plot Signatures + RemoteSensingToolbox.plot_signatures!(ax, bandset, raster, shp, label; colors=colors) + + # Add Legend + Legend(fig[1,2], ax, "Classification") + + # Return Figure + return fig +end + +function RemoteSensingToolbox.plot_signatures!(ax, bandset::Type{<:AbstractBandset}, raster::AbstractRasterStack, shp, label::Symbol; colors=Makie.wong_colors()) + # Extract Signatures + extracted = @pipe RemoteSensingToolbox.extract_signatures(raster, shp, label) |> RemoteSensingToolbox.fold_rows(mean, _, :label) + + # Plot Signatures + RemoteSensingToolbox.plot_signatures!(ax, bandset, extracted, :label, colors) +end + +function RemoteSensingToolbox.plot_signatures!(ax, bandset::Type{<:AbstractBandset}, sigs, labelcolumn::Symbol, colors) + # Extract Signatures + cols = Tables.columnnames(sigs) + bs = filter(x -> x in cols, bands(bandset)) + sig_matrix = hcat([Tables.getcolumn(sigs, b) for b in bs]...) + + # Plot Signatures + xs = [wavelength(bandset, b) for b in bs] + labels = Tables.getcolumn(sigs, labelcolumn) + for i in 1:size(sig_matrix,1) + lines!(ax, xs, sig_matrix[i,:], label=labels[i]; color=colors[i]) + end +end + +end \ No newline at end of file diff --git a/src/Bandsets/interface.jl b/src/Bandsets/interface.jl index 32e7201..f3a356b 100644 --- a/src/Bandsets/interface.jl +++ b/src/Bandsets/interface.jl @@ -89,7 +89,7 @@ Read and decode the quality assurance mask for the given `AbstractBandset`. - `src`: Either a directory containing the quality assurance mask named according to standard conventions or the file itself. # Returns -The decoded quality assurance mask as a `RasterStack`. Masked values are encoded as 1, non-masked values as 0, and missing values as 255. +The decoded quality assurance mask as a `RasterStack`. Encodes masked values as 1 and non-masked values as 0. # Example ```julia-repl diff --git a/src/RemoteSensingToolbox.jl b/src/RemoteSensingToolbox.jl index 0d41215..da2ecde 100644 --- a/src/RemoteSensingToolbox.jl +++ b/src/RemoteSensingToolbox.jl @@ -2,7 +2,6 @@ module RemoteSensingToolbox import ArchGDAL import ImageCore -import CairoMakie import Tables import TableOperations using OrderedCollections diff --git a/src/Spectral/Spectral.jl b/src/Spectral/Spectral.jl index 9b46a2c..0c6014e 100644 --- a/src/Spectral/Spectral.jl +++ b/src/Spectral/Spectral.jl @@ -8,7 +8,6 @@ using Logging using LinearAlgebra using Pipe: @pipe -import CairoMakie: Figure, Axis, lines!, Legend, save, Makie.wong_colors, cgrad import RemoteSensingToolbox: align_rasters, efficient_read, RasterTable, transform_column, dropmissing, fold_rows import ..Bandsets: AbstractBandset, wavelength, bands, wavelengths diff --git a/src/Spectral/analysis.jl b/src/Spectral/analysis.jl index a296e10..38ad028 100644 --- a/src/Spectral/analysis.jl +++ b/src/Spectral/analysis.jl @@ -1,5 +1,5 @@ """ - extract_signatures(stack::AbstractRasterStack, shp, label::Symbol; drop_missing=false) + extract_signatures(stack::AbstractRasterStack, shp, label::Symbol; drop_missing=true) Extract signatures from the given `RasterStack` within regions specified by a provided shapefile. @@ -7,7 +7,7 @@ Extract signatures from the given `RasterStack` within regions specified by a pr - `stack`: The `RasterStack` from which to extract spectral signatures. - `shp`: A `Tables.jl` compatible object containing a :geometry column storing a `GeoInterface.jl` compatible geometry and a label column indicating the land cover type. - `label`: The column in `shp` corresponding to the land cover type. -- 'drop_missing': Drop all rows with at least one missing value in either the bands or labels (default = true). +- `drop_missing`: Drop all rows with at least one missing value in either the bands or labels (default = true). # Returns A `RasterTable` consisting of rows for each observed signature and columns storing the respective bands and land cover type. @@ -39,7 +39,7 @@ julia> extract_signatures(landsat, shp, :C_name) |> DataFrame """ function extract_signatures(stack::AbstractRasterStack, shp, label::Symbol; drop_missing=true) # Prepare Labels - labels = shp[:,label] + labels = Tables.getcolumn(shp, label) fill_to_label = Set(labels) |> enumerate |> Dict label_to_fill = Set(labels) |> enumerate .|> reverse |> Dict @@ -84,29 +84,8 @@ shp = Shapefile.Table("data/landcover/landcover.shp") |> DataFrame plot_signatures(Landsat8, landsat, shp, :MC_name) ``` """ -function plot_signatures(bandset::Type{<:AbstractBandset}, raster::AbstractRasterStack, shp, label::Symbol; colors=wong_colors()) - # Create Figure - fig = Figure(resolution=(1000,500)) - - # Create Axis - ax = Axis( - fig[1,1], - xlabel="Wavelength (nm)", - ylabel="Reflectance", - xlabelfont=:bold, - ylabelfont=:bold, - xlabelpadding=10.0, - ylabelpadding=10.0, - ) - - # Plot Signatures - plot_signatures!(ax, bandset, raster, shp, label; colors=colors) - - # Add Legend - Legend(fig[1,2], ax, "Classification") - - # Return Figure - return fig +function plot_signatures(args...; kwargs...) + error("`plot_signatures` requires `CairoMakie` to be activated in your environment! Run `import CairoMakie` to fix this problem.") end """ @@ -146,15 +125,6 @@ plot_signatures!(ax2, Sentinel2, sentinel, shp, :MC_name; colors=cgrad(:tab10)) Legend(fig[:,2], ax1) ``` """ -function plot_signatures!(ax, bandset::Type{<:AbstractBandset}, raster::AbstractRasterStack, shp, label::Symbol; colors=wong_colors()) - # Extract Signatures - extracted = @pipe extract_signatures(raster, shp, label) |> fold_rows(mean, _, :label) - - # Prepare Signatures For Plotting - sigs = Tables.matrix(extracted)[:,2:end] .|> Float32 - labels = Tables.matrix(extracted)[:,1] .|> string - bands = filter(!=(:label), Tables.columnnames(extracted)) - - # Plot Signatures - _plot_signatures!(ax, bandset, sigs, bands, labels; colors=colors) +function plot_signatures!(args...; kwargs...) + error("`plot_signatures!` requires `CairoMakie` to be activated in your environment! Run `import CairoMakie` to fix this problem.") end \ No newline at end of file diff --git a/src/Spectral/utils.jl b/src/Spectral/utils.jl index e190178..baa2944 100644 --- a/src/Spectral/utils.jl +++ b/src/Spectral/utils.jl @@ -1,30 +1,3 @@ function _rasterize(shp, to, fill) Rasters.rasterize(last, shp, to=to, fill=fill, verbose=false, progress=false) -end - -function _sort_signature(bandset::Type{<:AbstractBandset}, reflectances::Vector{<:Number}, bands::Vector{Symbol}) - sorted = @pipe zip(wavelength.(bandset, bands), reflectances) |> collect |> sort(_, by=first) - return (first.(sorted), last.(sorted)) -end - -function _plot_signatures!(ax, bandset::Type{<:AbstractBandset}, sigs::Matrix{<:AbstractFloat}, bands::Vector{Symbol}, labels; colors=wong_colors(), kwargs...) - # Check Arguments - (size(sigs, 2) != length(bands)) && throw(ArgumentError("Length of signatures ($(size(sigs, 2))) must be equal to number of bands ($(length(bands)))!")) - (size(sigs, 1) != length(labels)) && throw(ArgumentError("Number of signatures ($(size(sigs, 1))) must be equal to number of labels ($(length(labels)))")) - - # Plot Signatures - for i in 1:size(sigs,1) - _plot_signature!(ax, bandset, sigs[i,:], bands, label=labels[i]; color=colors[i], kwargs...) - end -end - -function _plot_signature!(ax, bandset::Type{<:AbstractBandset}, signature::Vector{<:AbstractFloat}, bands::Vector{Symbol}; kwargs...) - # Check Arguments - (length(signature) != length(bands)) && throw(ArgumentError("Length of signatures ($(length(sigs))) must be equal to number of bands ($(length(bands)))!")) - - # Sort Bands In Ascending Order - x, y = _sort_signature(bandset, signature, bands) - - # Plot Signature - lines!(ax, x, y; kwargs...) end \ No newline at end of file diff --git a/src/raster_table.jl b/src/raster_table.jl index d9c2421..67cadde 100644 --- a/src/raster_table.jl +++ b/src/raster_table.jl @@ -3,6 +3,13 @@ mutable struct RasterTable <: Tables.AbstractColumns cols::Vector{Vector} end +function _dim_cols(raster) + ds = Iterators.product(dims(raster)...) + xs = @pipe map(first, ds) |> reshape(_, :) + ys = @pipe map(x -> x[2], ds) |> reshape(_, :) + return (xs, ys) +end + function _replace_missing(raster::AbstractRaster) m = missingval(raster) r = reshape(raster, :) diff --git a/src/visualization.jl b/src/visualization.jl index 1ea3f0f..b9fa458 100644 --- a/src/visualization.jl +++ b/src/visualization.jl @@ -76,6 +76,8 @@ end function plot_mask(mask, classes, figure=(;), legend=(;)) # Create Color Gradient + return 1 + """ colors = CairoMakie.cgrad(:viridis, length(classes), categorical=true) # Create Plot @@ -87,12 +89,14 @@ function plot_mask(mask, classes, figure=(;), legend=(;)) CairoMakie.Legend(fig[1,2], elements, classes, "Legend", legend...) return fig + """ end function plot_image(img) - fig, ax, plt = @pipe img |> rotr90 |> CairoMakie.image(_, axis=(;aspect=CairoMakie.DataAspect()), figure=(; resolution=reverse(size(img)) .+ 64)) - CairoMakie.hidedecorations!(ax) - return fig, ax, plt + return 1 + #fig, ax, plt = @pipe img |> rotr90 |> CairoMakie.image(_, axis=(;aspect=CairoMakie.DataAspect()), figure=(; resolution=reverse(size(img)) .+ 64)) + #CairoMakie.hidedecorations!(ax) + #return fig, ax, plt end "Adjust image histogram by performing a linear stretch to squeeze all values between the percentiles `lower` and `upper` into the range [0,1]." diff --git a/test/Project.toml b/test/Project.toml index 9b85f72..a3905fe 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,5 +1,8 @@ [deps] ArchGDAL = "c9ce4bd3-c3d5-55b8-8973-c0e20141b8c3" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" Pipe = "b98c9c47-44ae-5843-9183-064241ee97a0" +Shapefile = "8e980c4a-a4fe-5da2-b3a7-4b4b0353a2f4" +Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/data/landcover/landcover.dbf b/test/data/landcover/landcover.dbf new file mode 100644 index 0000000..4b2fd29 Binary files /dev/null and b/test/data/landcover/landcover.dbf differ diff --git a/test/data/landcover/landcover.prj b/test/data/landcover/landcover.prj new file mode 100644 index 0000000..5dc2a9e --- /dev/null +++ b/test/data/landcover/landcover.prj @@ -0,0 +1 @@ +PROJCS["WGS_1984_UTM_Zone_11N",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",500000.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",-117.0],PARAMETER["Scale_Factor",0.9996],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]] \ No newline at end of file diff --git a/test/data/landcover/landcover.shp b/test/data/landcover/landcover.shp new file mode 100644 index 0000000..f694072 Binary files /dev/null and b/test/data/landcover/landcover.shp differ diff --git a/test/data/landcover/landcover.shx b/test/data/landcover/landcover.shx new file mode 100644 index 0000000..65f2b6d Binary files /dev/null and b/test/data/landcover/landcover.shx differ diff --git a/test/data/landcover/landcover.tif b/test/data/landcover/landcover.tif new file mode 100644 index 0000000..07d10d6 Binary files /dev/null and b/test/data/landcover/landcover.tif differ diff --git a/test/runtests.jl b/test/runtests.jl index 5f08dca..f7194ee 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,7 +1,9 @@ using RemoteSensingToolbox using Test using Images +using Shapefile import ArchGDAL +import Tables using Pipe: @pipe @testset "Landsat" begin @@ -79,3 +81,20 @@ end @test names(sentinel) == names(recovered) @test all(isapprox.(tocube(recovered).data, tocube(sentinel).data, atol=0.1)) end + +@testset "makie" begin + + # Load Sentinel + sentinel = @pipe read_bands(Sentinel2, "data/sentinel/") |> dn_to_reflectance(Sentinel2, _) + + # Read Shapefile + shp = Shapefile.Table("data/landcover/landcover.shp") |> Tables.columntable + + # Should Throw Error Telling Us To Load CairoMakie + @test_throws ErrorException plot_signatures(Sentinel2, sentinel, shp, :MC_name) + + using CairoMakie + + # Should Run Now That CairoMakie is Loaded + fig = plot_signatures(Sentinel2, sentinel, shp, :MC_name) +end