From 1bf197ca4409f66c68517d83bdda8069656424cb Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 28 Dec 2021 09:52:46 -0800 Subject: [PATCH 001/178] Begin pulling in code for other image packages to support headers. --- Project.toml | 12 ++- src/AstroImages.jl | 220 ++++++++++++++++++++++++++++++++------------- 2 files changed, 166 insertions(+), 66 deletions(-) diff --git a/Project.toml b/Project.toml index 03f49a08..19f1f1fc 100644 --- a/Project.toml +++ b/Project.toml @@ -4,26 +4,30 @@ authors = ["Mosè Giordano", "Rohit Kumar"] version = "0.2.0" [deps] +ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" FITSIO = "525bcba6-941b-5504-bd06-fd0dc1a4d2eb" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" +InlineStrings = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48" Interact = "c601a237-2ae4-5e1e-952c-7a85b0c7eef1" +MappedArrays = "dbb5928d-eab1-5f90-85c2-b9b0edb7c900" +OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043" RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" Reproject = "d1dcc2e6-806e-11e9-2897-3f99785db2ae" WCS = "15f3aee2-9e10-537f-b834-a6fb8bdb944d" -MappedArrays = "dbb5928d-eab1-5f90-85c2-b9b0edb7c900" [compat] -julia = "^1.0.0" Reproject = "^0.3.0" +julia = "^1.6.0" [extras] +JLD = "4138dd39-2aa7-5051-a626-17a0bb65d9c8" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Widgets = "cc8bc4a8-27d6-5769-a93b-9d913e69aa62" -JLD = "4138dd39-2aa7-5051-a626-17a0bb65d9c8" -SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" [targets] test = ["Test", "Random", "Widgets", "JLD", "SHA"] diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 6699e330..db6518fb 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -7,8 +7,8 @@ using FITSIO, FileIO, Images, Interact, Reproject, WCS, MappedArrays export load, AstroImage, ccd2rgb, set_brightness!, set_contrast!, add_label!, reset! _load(fits::FITS, ext::Int) = read(fits[ext]) -_load(fits::FITS, ext::NTuple{N, Int}) where {N} = ntuple(i-> read(fits[ext[i]]), N) -_load(fits::NTuple{N, FITS}, ext::NTuple{N, Int}) where {N} = ntuple(i -> _load(fits[i], ext[i]), N) +# _load(fits::FITS, ext::NTuple{N, Int}) where {N} = ntuple(i-> read(fits[ext[i]]), N) +# _load(fits::NTuple{N, FITS}, ext::NTuple{N, Int}) where {N} = ntuple(i -> _load(fits[i], ext[i]), N) _header(fits::FITS, ext::Int) = WCS.from_header(read_header(fits[ext], String))[1] _header(fits::FITS, ext::NTuple{N, Int}) where {N} = @@ -102,40 +102,149 @@ mutable struct Properties{P <: Union{AbstractFloat, FixedPoint}} end end -struct AstroImage{T<:Real,C<:Color, N, P} - data::NTuple{N, Matrix{T}} - minmax::NTuple{N, Tuple{T,T}} - wcs::NTuple{N, WCSTransform} - property::Properties{P} +struct AstroImage{T, N, TDat} <: AbstractArray{T,N} + data::TDat + # minmax::Tuple{T,T} + # minmaxdirty::Bool + # property::Properties{P} + headers::FITSHeader + wcs::WCSTransform end +Images.arraydata(img::AstroImage) = img.data +headers(img::AstroImage) = img.headers +wcs(img::AstroImage) = img.wcs + +export arraydata, headers, wcs + +struct Comment end +export Comment + +struct History end +export History + + +# extending the AbstractArray interface +Base.size(img::AstroImage) = size(arraydata(img)) +Base.length(img::AstroImage) = length(arraydata(img)) +Base.getindex(img::AstroImage, inds...) = getindex(arraydata(img), inds...) # default fallback for operations on Array +Base.setindex!(img::AstroImage, v, inds...) = setindex!(arraydata(img), v, inds...) # default fallback for operations on Array +Base.getindex(img::AstroImage, inds::AbstractString...) = getindex(headers(img), inds...) # accesing header using strings +Base.setindex!(img::AstroImage, v, inds::AbstractString...) = setindex!(headers(img), v, inds...) # modifying header using strings +Base.getindex(img::AstroImage, inds::Symbol...) = getindex(img, string.(inds)...) # accessing header using symbol +Base.setindex!(img::AstroImage, v, ind::Symbol) = setindex!(img, v, string(ind)) # modifying header using Symbol + +# Getting and setting comments +Base.getindex(img::AstroImage, ind::AbstractString, ::Type{Comment}) = get_comment(headers(img), ind) # accesing header comment using strings +Base.setindex!(img::AstroImage, v, ind::AbstractString, ::Type{Comment}) = set_comment!(headers(img), ind, v) # modifying header comment using strings +Base.getindex(img::AstroImage, ind::Symbol, ::Type{Comment}) = get_comment(headers(img), string(ind)) # accessing header comment using symbol +Base.setindex!(img::AstroImage, v, ind::Symbol, ::Type{Comment}) = set_comment!(headers(img), string(ind), v) # modifying header comment using Symbol + +# Support for special HISTORY and COMMENT entries +function Base.getindex(img::AstroImage, ::Type{History}) + hdr = headers(img) + ii = findall(==("HISTORY"), hdr.keys) + return view(hdr.comments, ii) +end +function Base.getindex(img::AstroImage, ::Type{Comment}) + hdr = headers(img) + ii = findall(==("COMMENT"), hdr.keys) + return view(hdr.comments, ii) +end +# Adding new history entries +function Base.push!(img::AstroImage, ::Type{History}, history::AbstractString) + hdr = headers(img) + push!(hdr.keys, "HISTORY") + push!(hdr.values, nothing) + push!(hdr.comments, history) +end + +Base.promote_rule(::Type{AstroImage{T}}, ::Type{AstroImage{V}}) where {T,V} = AstroImage{promote_type{T,V}} +# function Base.similar(img::AstroImage) where T +# dat = similar(arraydata(img)) +# _,_,C,P = TNCP(img) +# T2 = eltype(dat) +# N = length(size(dat)) +# return AstroImage{T2,N,C,P}( +# dat, +# (zero(dat),one(dat)), +# true, +# # TODO: +# # similar(img.wcs), +# img.wcs, +# Properties{Float64}(), +# FITSHeader(String[],[],String[]), +# ) +# end + + +# Broadcasting +# Base.copy(img::AstroImage) = AstroImage(copy(arraydata(img)), deepcopy(headers(img))) +# Base.convert(::Type{AstroImage{T}}, img::AstroImage{V}) where {T,V} = AstroImage{T}(arraydata(img), headers(img)) +# Base.view(img::AstroImage, inds...) = AstroImage(view(arraydata(img), inds...), headers(img)) +# Base.selectdim(img::AstroImage, d::Integer, idxs) = AstroImage(selectdim(arraydata(img), d, idxs), headers(img)) +# broadcast mechanics +Base.BroadcastStyle(::Type{<:AstroImage}) = Broadcast.ArrayStyle{AstroImage}() +function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{AstroImage}}, ::Type{T}) where T + img = find_img(bc) + dat = similar(arraydata(img), T, axes(bc)) + T2 = eltype(dat) + N = ndims(dat) + return AstroImage{T2,N,typeof(dat)}( + dat, + # img.minmax, + # true, + headers(img), + img.wcs, + # img.property, + ) +end +"`A = find_img(As)` returns the first AstroImage among the arguments." +find_img(bc::Base.Broadcast.Broadcasted) = find_img(bc.args) +find_img(args::Tuple) = find_img(find_img(args[1]), Base.tail(args)) +find_img(x) = x +find_img(::Tuple{}) = nothing +find_img(a::AstroImage, rest) = a +find_img(::Any, rest) = find_img(rest) + + """ AstroImage([color=Gray,] data::Matrix{Real}) AstroImage(color::Type{<:Color}, data::NTuple{N, Matrix{T}}) where {T<:Real, N} Construct an `AstroImage` object of `data`, using `color` as color map, `Gray` by default. """ -AstroImage(color::Type{<:Color}, data::Matrix{T}, wcs::WCSTransform) where {T<:Real} = - AstroImage{T,color, 1, Float64}((data,), (extrema(data),), (wcs,), Properties{Float64}()) -function AstroImage(color::Type{<:AbstractRGB}, data::NTuple{N, Matrix{T}}, wcs::NTuple{N, WCSTransform}) where {T <: Union{AbstractFloat, FixedPoint}, N} - if N == 3 - img = ccd2rgb((data[1], wcs[1]),(data[2], wcs[2]),(data[3], wcs[3])) - return AstroImage{T,color,N, widen(T)}(data, ntuple(i -> extrema(data[i]), N), wcs, Properties{widen(T)}(rgb_image = img)) - end -end -function AstroImage(color::Type{<:AbstractRGB}, data::NTuple{N, Matrix{T}}, wcs::NTuple{N, WCSTransform}) where {T<:Real, N} - if N == 3 - img = ccd2rgb((data[1], wcs[1]),(data[2], wcs[2]),(data[3], wcs[3])) - return AstroImage{T,color,N, Float64}(data, ntuple(i -> extrema(data[i]), N), wcs, Properties{Float64}(rgb_image = img)) - end +AstroImage(img::AstroImage) = img +AstroImage(data::AbstractArray{T,N}, headers::FITSHeader, wcs::WCSTransform) where {T,N} = + AstroImage{T,N,typeof(data)}(data, headers, wcs) + +# AstroImage(color::Type{<:Color}, data::AbstractArray{T,N}, wcs::WCSTransform) where {T<:Real,N<:Int} = +# AstroImage{T, N, color, Float64}(data, extrema(data), false, wcs, Properties{Float64}()) +# function AstroImage(color::Type{<:AbstractRGB}, data::NTuple{N, Matrix{T}}, wcs::NTuple{N, WCSTransform}) where {T <: Union{AbstractFloat, FixedPoint}, N} +# if N == 3 +# img = ccd2rgb((data[1], wcs[1]),(data[2], wcs[2]),(data[3], wcs[3])) +# return AstroImage{T,color,N, widen(T)}(data, ntuple(i -> extrema(data[i]), N), wcs, Properties{widen(T)}(rgb_image = img)) +# end +# end +# function AstroImage(color::Type{<:AbstractRGB}, data::NTuple{N, Matrix{T}}, wcs::NTuple{N, WCSTransform}) where {T<:Real, N} +# if N == 3 +# img = ccd2rgb((data[1], wcs[1]),(data[2], wcs[2]),(data[3], wcs[3])) +# return AstroImage{T,color,N, Float64}(data, ntuple(i -> extrema(data[i]), N), wcs, Properties{Float64}(rgb_image = img)) +# end +# end +function AstroImage( + # color::Type{<:Color}, + data::AbstractArray{T,N}, + # properties::Properties=Properties{Float64}(), + header::FITSHeader=FITSHeader(String[],[],String[]), + wcs::WCSTransform=only(WCS.from_header(string(header), ignore_rejected=true)) +) where {T<:Real, N} + return AstroImage{T,N,typeof(data)}(data, header, wcs) end -function AstroImage(color::Type{<:Color}, data::NTuple{N, Matrix{T}}, wcs::NTuple{N, WCSTransform}) where {T<:Real, N} - return AstroImage{T,color, N, Float64}(data, ntuple(i -> extrema(data[i]), N), wcs, Properties{Float64}()) -end -AstroImage(data::Matrix{T}) where {T<:Real} = AstroImage{T,Gray,1, Float64}((data,), (extrema(data),), (WCSTransform(2),), Properties{Float64}()) -AstroImage(data::NTuple{N, Matrix{T}}) where {T<:Real, N} = AstroImage{T,Gray,N, Float64}(data, ntuple(i -> extrema(data[i]), N), ntuple(i-> WCSTransform(2), N), Properties{Float64}()) -AstroImage(data::Matrix{T}, wcs::WCSTransform) where {T<:Real} = AstroImage{T,Gray,1, Float64}((data,), (extrema(data),), (wcs,), Properties{Float64}()) -AstroImage(data::NTuple{N, Matrix{T}}, wcs::NTuple{N, WCSTransform}) where {T<:Real, N} = AstroImage{T,Gray,N, Float64}(data, ntuple(i -> extrema(data[i]), N), wcs, Properties{Float64}()) +# AstroImage(data::Matrix{T}) where {T<:Real} = AstroImage{T,Gray,1, Float64}(data, (extrema(data),), (WCSTransform(2),), Properties{Float64}()) +# AstroImage(data::NTuple{N, Matrix{T}}) where {T<:Real, N} = AstroImage{T,Gray,N, Float64}(data, ntuple(i -> extrema(data[i]), N), ntuple(i-> WCSTransform(2), N), Properties{Float64}()) +# AstroImage(data::Matrix{T}, wcs::WCSTransform) where {T<:Real} = AstroImage{T,Gray,1, Float64}((data,), (extrema(data),), (wcs,), Properties{Float64}()) +# AstroImage(data::NTuple{N, Matrix{T}}, wcs::NTuple{N, WCSTransform}) where {T<:Real, N} = AstroImage{T,Gray,N, Float64}(data, ntuple(i -> extrema(data[i]), N), wcs, Properties{Float64}()) """ AstroImage([color=Gray,] filename::String, n::Int=1) @@ -145,35 +254,27 @@ Create an `AstroImage` object by reading the `n`-th extension from FITS file `fi Use `color` as color map, this is `Gray` by default. """ -AstroImage(color::Type{<:Color}, file::String, ext::Int) = - AstroImage(color, file, (ext,)) -AstroImage(color::Type{<:Color}, file::String, ext::NTuple{N, Int}) where {N} = - AstroImage(color, load(file, ext)...) - -AstroImage(file::String, ext::Int) = AstroImage(Gray, file, ext) -AstroImage(file::String, ext::NTuple{N, Int}) where {N} = AstroImage(Gray, file, ext) - -AstroImage(color::Type{<:Color}, fits::FITS, ext::Int) = - AstroImage(color, _load(fits, ext), WCS.from_header(read_header(fits[ext],String))[1]) -AstroImage(color::Type{<:Color}, fits::FITS, ext::NTuple{N, Int}) where {N} = - AstroImage(color, _load(fits, ext), ntuple(i -> WCS.from_header(read_header(fits[ext[i]], String))[1], N)) -AstroImage(color::Type{<:Color}, fits::NTuple{N, FITS}, ext::NTuple{N, Int}) where {N} = - AstroImage(color, ntuple(i -> _load(fits[i], ext[i]), N), ntuple(i -> WCS.from_header(read_header(fits[i][ext[i]], String))[1], N)) - -AstroImage(files::NTuple{N,String}) where {N} = - AstroImage(Gray, load(files)...) -AstroImage(color::Type{<:Color}, files::NTuple{N,String}) where {N} = - AstroImage(color, load(files)...) -AstroImage(file::String) = AstroImage((file,)) - -# Lazily reinterpret the image as a Matrix{Color}, upon request. -function render(img::AstroImage{T,C,N}, header_number = 1) where {T,C,N} - imgmin, imgmax = extrema(img.minmax[header_number]) - # Add one to maximum to work around this issue: - # https://github.com/JuliaMath/FixedPointNumbers.jl/issues/102 - f = scaleminmax(_float(imgmin), _float(max(imgmax, imgmax + one(T)))) - return colorview(C, f.(_float.(img.data[header_number]))) -end +# AstroImage(color::Type{<:Color}, file::String, ext::Int) = +# AstroImage(color, file, (ext,)) +# AstroImage(color::Type{<:Color}, file::String, ext::NTuple{N, Int}) where {N} = +# AstroImage(color, load(file, ext)...) + +# AstroImage(file::String, ext::Int) = AstroImage(Gray, file, ext) +# AstroImage(file::String, ext::NTuple{N, Int}) where {N} = AstroImage(Gray, file, ext) + +AstroImage(fits::FITS, ext::Int=1) = AstroImage(_load(fits, ext), read_header(fits[ext])) +# AstroImage(color::Type{<:Color}, fits::FITS, ext::NTuple{N, Int}) where {N} = +# AstroImage(color, _load(fits, ext), ntuple(i -> WCS.from_header(read_header(fits[ext[i]], String))[1], N)) +# AstroImage(color::Type{<:Color}, fits::NTuple{N, FITS}, ext::NTuple{N, Int}) where {N} = +# AstroImage(color, ntuple(i -> _load(fits[i], ext[i]), N), ntuple(i -> WCS.from_header(read_header(fits[i][ext[i]], String))[1], N)) + +# AstroImage(files::NTuple{N,String}) where {N} = +# AstroImage(Gray, load(files)...) +# AstroImage(color::Type{<:Color}, files::NTuple{N,String}) where {N} = +# AstroImage(color, load(files)...) +AstroImage(file::String) = AstroImage(FITS(file,"r")) + + """ set_brightness!(img::AstroImage, value::AbstractFloat) @@ -223,7 +324,7 @@ Resets AstroImage property fields. Sets brightness to 0.0, contrast to 1.0, empties label and form a fresh rgb_image without any brightness, contrast operations on it. """ -function reset!(img::AstroImage{T,C,N}) where {T,C,N} +function reset!(img::AstroImage{T,N}) where {T,N} img.property.contrast = 1.0 img.property.brightness = 0.0 img.property.label = [] @@ -234,11 +335,6 @@ function reset!(img::AstroImage{T,C,N}) where {T,C,N} end end -Images.colorview(img::AstroImage) = render(img) - -Base.size(img::AstroImage) = Base.size.(img.data) - -Base.length(img::AstroImage{T,C,N}) where {T,C,N} = N include("showmime.jl") include("plot-recipes.jl") From 9f80446f5ede17e652b3aa1c380e3b3047bf9ecd Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 28 Dec 2021 09:53:30 -0800 Subject: [PATCH 002/178] Add colormap support to `render` --- src/showmime.jl | 95 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/src/showmime.jl b/src/showmime.jl index c6dd36a8..c380b37b 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -8,8 +8,8 @@ Visualize the fits image by changing the brightness and contrast of image. Users can also provide their own range as keyword arguments. """ -function brightness_contrast(img::AstroImage{T,C,N}; brightness_range = 0:255, - contrast_range = 1:1000, header_number = 1) where {T,C,N} +function brightness_contrast(img::AstroImage{T,N}; brightness_range = 0:255, + contrast_range = 1:1000, header_number = 1) where {T,N} @manipulate for brightness in brightness_range, contrast in contrast_range _brightness_contrast(C, img.data[header_number], brightness, contrast) end @@ -18,3 +18,94 @@ end # This is used in Jupyter notebooks Base.show(io::IO, mime::MIME"text/html", img::AstroImage; kwargs...) = show(io, mime, brightness_contrast(img), kwargs...) + +# This is used in Jupyter notebooks +Base.show(io::IO, mime::MIME"image/png", img::AstroImage; kwargs...) = + show(io, mime, imshow(img), kwargs...) + +using MappedArrays +using ColorSchemes +using PlotUtils: zscale +export zscale + +const _default_clims = Ref{Any}(extrema) +const _default_cmap = Ref{Union{Symbol,Nothing}}(nothing) + +function set_cmap!(cmap) + _default_cmap[] = cmap +end + +function set_clims!(clims) + _default_clims[] = clims +end + +function imshow( + img::AbstractMatrix{T}; + clims=_default_clims[], + cmap=_default_cmap[] +) where {T} + # Users can pass clims as an array or tuple containing the minimum and maximum values + if typeof(clims) <: AbstractArray || typeof(clims) <: Tuple + if length(clims) != 2 + error("clims must have exactly two values if provided.") + end + imgmin = convert(T, first(clims)) + imgmax = convert(T, last(clims)) + # Or as a callable that computes them given an iterator + else + imgmin_0, imgmax_0 = clims(Iterators.filter(pix->isfinite(pix) && !ismissing(pix), img)) + imgmin = convert(T, imgmin_0) + imgmax = convert(T, imgmax_0) + end + return imshow(img,(imgmin,imgmax),cmap) +end +function imshow(img::AbstractMatrix{T}, clims::Union{<:AbstractArray{<:T},Tuple{T,T}}, cmap) where {T} + + if length(clims) != 2 + error("clims must have exactly two values if provided.") + end + imgmin, imgmax = clims + + # Pure grayscale display + if isnothing(cmap) + f = scaleminmax(_float(imgmin), _float(max(imgmax, imgmax + one(T)))) + return mappedarray(Gray ∘ f, img) + # Monochromatic image using a colormap + else + cscheme = ColorSchemes.colorschemes[cmap] + # We create a MappedArray that converts from image data + # to RGBA values on the fly according to a colorscheme. + return mappedarray(img) do pix + # We treat Inf values as white / -Inf as black + return if isinf(pix) + if pix > 0 + RGBA{T}(1,1,1,1) + else + RGBA{T}(0,0,0,1) + end + # We treat NaN/missing values as transparent + elseif !isfinite(pix) || ismissing(pix) + RGBA{T}(0,0,0,0) + else + RGBA{T}(get(cscheme::ColorScheme, pix, (imgmin, imgmax))) + end + end + end + +end +export imshow + + +# Lazily reinterpret the AstroImage as a Matrix{Color}, upon request. +# By itself, Images.colorview works fine on AstroImages. But +# AstroImages are not normalized to be between [0,1]. So we override +# colorview to first normalize the data using scaleminmax +function render(img::AstroImage{T,N}) where {T,N} + # imgmin, imgmax = img.minmax + imgmin, imgmax = extrema(img) + # Add one to maximum to work around this issue: + # https://github.com/JuliaMath/FixedPointNumbers.jl/issues/102 + f = scaleminmax(_float(imgmin), _float(max(imgmax, imgmax + one(T)))) + return colorview(Gray, f.(_float.(img.data))) +end +Images.colorview(img::AstroImage) = render(img) \ No newline at end of file From ba8459dfd28af080b4b80cd7af803614e3c66aa4 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 28 Dec 2021 11:41:06 -0800 Subject: [PATCH 003/178] Initial implementation of auto-updating the WCSTransform when headers are modified --- src/AstroImages.jl | 27 ++++-- src/wcs_headers.jl | 227 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+), 7 deletions(-) create mode 100644 src/wcs_headers.jl diff --git a/src/AstroImages.jl b/src/AstroImages.jl index db6518fb..7bfdf4d4 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -102,18 +102,24 @@ mutable struct Properties{P <: Union{AbstractFloat, FixedPoint}} end end -struct AstroImage{T, N, TDat} <: AbstractArray{T,N} +mutable struct AstroImage{T, N, TDat} <: AbstractArray{T,N} data::TDat # minmax::Tuple{T,T} # minmaxdirty::Bool # property::Properties{P} headers::FITSHeader wcs::WCSTransform + wcs_stale::Bool end Images.arraydata(img::AstroImage) = img.data headers(img::AstroImage) = img.headers -wcs(img::AstroImage) = img.wcs +function wcs(img::AstroImage) + if img.wcs_stale + img.wcs = only(WCS.from_header(string(headers(img)), ignore_rejected=true)) + end + return img.wcs +end export arraydata, headers, wcs @@ -130,10 +136,16 @@ Base.length(img::AstroImage) = length(arraydata(img)) Base.getindex(img::AstroImage, inds...) = getindex(arraydata(img), inds...) # default fallback for operations on Array Base.setindex!(img::AstroImage, v, inds...) = setindex!(arraydata(img), v, inds...) # default fallback for operations on Array Base.getindex(img::AstroImage, inds::AbstractString...) = getindex(headers(img), inds...) # accesing header using strings -Base.setindex!(img::AstroImage, v, inds::AbstractString...) = setindex!(headers(img), v, inds...) # modifying header using strings +function Base.setindex!(img::AstroImage, v, ind::AbstractString) # modifying header using a string + setindex!(headers(img), v, ind) + # Mark the WCS object as beign out of date if this was a WCS header keyword + if ind ∈ WCS_HEADERS_2 + img.wcs_stale = true + end + @show ind +end Base.getindex(img::AstroImage, inds::Symbol...) = getindex(img, string.(inds)...) # accessing header using symbol -Base.setindex!(img::AstroImage, v, ind::Symbol) = setindex!(img, v, string(ind)) # modifying header using Symbol - +Base.setindex!(img::AstroImage, v, ind::Symbol) = setindex!(img, v, string(ind)) # Getting and setting comments Base.getindex(img::AstroImage, ind::AbstractString, ::Type{Comment}) = get_comment(headers(img), ind) # accesing header comment using strings Base.setindex!(img::AstroImage, v, ind::AbstractString, ::Type{Comment}) = set_comment!(headers(img), ind, v) # modifying header comment using strings @@ -197,6 +209,7 @@ function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{AstroImage} headers(img), img.wcs, # img.property, + false, ) end "`A = find_img(As)` returns the first AstroImage among the arguments." @@ -239,7 +252,7 @@ function AstroImage( header::FITSHeader=FITSHeader(String[],[],String[]), wcs::WCSTransform=only(WCS.from_header(string(header), ignore_rejected=true)) ) where {T<:Real, N} - return AstroImage{T,N,typeof(data)}(data, header, wcs) + return AstroImage{T,N,typeof(data)}(data, header, wcs, false) end # AstroImage(data::Matrix{T}) where {T<:Real} = AstroImage{T,Gray,1, Float64}(data, (extrema(data),), (WCSTransform(2),), Properties{Float64}()) # AstroImage(data::NTuple{N, Matrix{T}}) where {T<:Real, N} = AstroImage{T,Gray,N, Float64}(data, ntuple(i -> extrema(data[i]), N), ntuple(i-> WCSTransform(2), N), Properties{Float64}()) @@ -335,7 +348,7 @@ function reset!(img::AstroImage{T,N}) where {T,N} end end - +include("wcs_headers.jl") include("showmime.jl") include("plot-recipes.jl") include("ccd2rgb.jl") diff --git a/src/wcs_headers.jl b/src/wcs_headers.jl new file mode 100644 index 00000000..a35d26bc --- /dev/null +++ b/src/wcs_headers.jl @@ -0,0 +1,227 @@ +const WCS_HEADERS_TEMPLATES = [ + "WCSAXESa", + "WCAXna", + "WCSTna", + "WCSXna", + "CRPIXja", + "jCRPna", + "jCRPXn", + "TCRPna", + "TCRPXn", + "PCi_ja", + "ijPCna", + "TPn_ka", + "TPCn_ka", + "CDi_ja", + "ijCDna", + "TCn_ka", + "TCDn_ka", + "CDELTia", + "iCDEna", + "iCDLTn", + "TCDEna", + "TCDLTn", + "CROTAi", + "iCROTn", + "TCROTn", + "CUNITia", + "iCUNna", + "iCUNIn", + "TCUNna", + "TCUNIn", + "CTYPEia", + "iCTYna", + "iCTYPn", + "TCTYna", + "TCTYPn", + "CRVALia", + "iCRVna", + "iCRVLn", + "TCRVna", + "TCRVLn", + "LONPOLEa", + "LONPna", + "LATPOLEa", + "LATPna", + "RESTFREQ", + "RESTFRQa", + "RFRQna", + "RESTWAVa", + "RWAVna", + "PVi_ma", + "iVn_ma", + "iPVn_ma", + "TVn_ma", + "TPVn_ma", + "PROJPm", + "PSi_ma", + "iSn_ma", + "iPSn_ma", + "TSn_ma", + "TPSn_ma", + "VELREF", + "CNAMEia", + "iCNAna", + "iCNAMn", + "TCNAna", + "TCNAMn", + "CRDERia", + "iCRDna", + "iCRDEn", + "TCRDna", + "TCRDEn", + "CSYERia", + "iCSYna", + "iCSYEn", + "TCSYna", + "TCSYEn", + "CZPHSia", + "iCZPna", + "iCZPHn", + "TCZPna", + "TCZPHn", + "CPERIia", + "iCPRna", + "iCPERn", + "TCPRna", + "TCPERn", + "WCSNAMEa", + "WCSNna", + "TWCSna", + "TIMESYS", + "TREFPOS", + "TRPOSn", + "TREFDIR", + "TRDIRn", + "PLEPHEM", + "TIMEUNIT", + "DATEREF", + "MJDREF", + "MJDREFI", + "MJDREFF", + "JDREF", + "JDREFI", + "JDREFF", + "TIMEOFFS", + "DATE-OBS", + "DOBSn", + "DATE-BEG", + "DATE-AVG", + "DAVGn", + "DATE-END", + "MJD-OBS", + "MJDOBn", + "MJD-BEG", + "MJD-AVG", + "MJDAn", + "MJD-END", + "JEPOCH", + "BEPOCH", + "TSTART", + "TSTOP", + "XPOSURE", + "TELAPSE", + "TIMSYER", + "TIMRDER", + "TIMEDEL", + "TIMEPIXR", + "OBSGEO-X", + "OBSGXn", + "OBSGEO-Y", + "OBSGYn", + "OBSGEO-Z", + "OBSGZn", + "OBSGEO-L", + "OBSGLn", + "OBSGEO-B", + "OBSGBn", + "OBSGEO-H", + "OBSGHn", + "OBSORBIT", + "RADESYSa", + "RADEna", + "RADECSYS", + "EPOCH", + "EQUINOXa", + "EQUIna", + "SPECSYSa", + "SPECna", + "SSYSOBSa", + "SOBSna", + "VELOSYSa", + "VSYSna", + "VSOURCEa", + "VSOUna", + "ZSOURCEa", + "ZSOUna", + "SSYSSRCa", + "SSRCna", + "VELANGLa", + "VANGna", + "RSUN_REF", + "DSUN_OBS", + "CRLN_OBS", + "HGLN_OBS", + "HGLT_OBS", + "NAXISn", + "CROTAn", + "PROJPn", + "CPDISja", + "CQDISia", + "DPja", + "DQia", + "CPERRja", + "CQERRia", + "DVERRa", + "A_ORDER", + "B_ORDER", + "AP_ORDER", + "BP_ORDER", + "A_DMAX", + "B_DMAX", + "A_p_q", + "B_p_q", + "AP_p_q", + "BP_p_q", + "CNPIX1", + "PPO3", + "PPO6", + "XPIXELSZ", + "YPIXELSZ", + "PLTRAH", + "PLTRAM", + "PLTRAS", + "PLTDECSN", + "PLTDECD", + "PLTDECM", + "PLTDECS", + "PLATEID", + "AMDXm", + "AMDYm", + "WATi_m" +] + +# Expand the headers containing lower case specifers into N copies +Is = [""; string.(1:4)] +# Find all lower case templates +const WCS_HEADERS_2 = Set(mapreduce(vcat, WCS_HEADERS_TEMPLATES) do template + if any(islowercase, template) + template_chars = Vector{Char}(template) + chars = template_chars[islowercase.(template_chars)] + out = String[template] + for replace_target in chars + newout = String[] + for template in out + for i in Is + push!(newout, replace(template, replace_target=>i)) + end + end + append!(out, newout) + end + out + else + template + end +end) + +## From 29df5c86b674332bfea50736c18f425d4f9e702c Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 28 Dec 2021 12:04:12 -0800 Subject: [PATCH 004/178] Fix plot recipes after struct changes --- src/plot-recipes.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index a1132e69..a9bace70 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -1,12 +1,12 @@ using RecipesBase -@recipe function f(img::AstroImage, header_number::Int) +@recipe function f(img::AstroImage) seriestype := :heatmap aspect_ratio := :equal # Right now we only support single frame images, # gray scale is a good choice. color := :grays - img.data[header_number] + arraydata(img) end @recipe function f(img::AstroImage) @@ -15,7 +15,7 @@ end # Right now we only support single frame images, # gray scale is a good choice. color := :grays - img.data[1] + arraydata(img) end @recipe function f(img::AstroImage, wcs::WCSTransform) @@ -26,7 +26,7 @@ end yformatter := y -> pix2world_yformatter(y, wcs) xlabel := labler_x(wcs) ylabel := labler_y(wcs) - img.data + arraydata(img) end function pix2world_xformatter(x, wcs::WCSTransform) From 34360e43d83ed2000d2fe63168aa990bf14847f4 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 28 Dec 2021 12:14:35 -0800 Subject: [PATCH 005/178] Added `percent` limit helper. Maybe this should go in PlotUtils? --- src/showmime.jl | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/showmime.jl b/src/showmime.jl index c380b37b..f65fdaa9 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -23,6 +23,7 @@ Base.show(io::IO, mime::MIME"text/html", img::AstroImage; kwargs...) = Base.show(io::IO, mime::MIME"image/png", img::AstroImage; kwargs...) = show(io, mime, imshow(img), kwargs...) +using Statistics using MappedArrays using ColorSchemes using PlotUtils: zscale @@ -39,6 +40,24 @@ function set_clims!(clims) _default_clims[] = clims end +""" + percent(99.5) + +Returns a function that calculates display limits that include the given +percent of the image data. + +Example: +```julia +julia> imshow(img, clims=percent(90)) +``` +This will set the limits to be the 5th percentile to the 95th percentile. +""" +function percent(perc::Number) + trim = (1 - perc/100)/2 + return (data) -> quantile(data, (trim, 1-trim)) +end +export percent + function imshow( img::AbstractMatrix{T}; clims=_default_clims[], From b675ff32c3588f661e3a30af0a02d5a70e3b6e0c Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 28 Dec 2021 12:22:21 -0800 Subject: [PATCH 006/178] Allow `percent` to work with `plot` as well as `imshow` --- src/showmime.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/showmime.jl b/src/showmime.jl index f65fdaa9..2b12dbb6 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -54,7 +54,9 @@ This will set the limits to be the 5th percentile to the 95th percentile. """ function percent(perc::Number) trim = (1 - perc/100)/2 - return (data) -> quantile(data, (trim, 1-trim)) + clims(data) = quantile(data, (trim, 1-trim)) + clims(data::AbstractMatrix) = quantile(vec(data), (trim, 1-trim)) + return clims end export percent From 056207ec3faa2cdcb077d5246c8f4217d2c9c469 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Wed, 29 Dec 2021 10:49:14 -0800 Subject: [PATCH 007/178] Filling out abstract array interface --- src/AstroImages.jl | 195 ++++++++++++++++++++++++++++++--------------- 1 file changed, 130 insertions(+), 65 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 7bfdf4d4..c804dee6 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -8,64 +8,64 @@ export load, AstroImage, ccd2rgb, set_brightness!, set_contrast!, add_label!, re _load(fits::FITS, ext::Int) = read(fits[ext]) # _load(fits::FITS, ext::NTuple{N, Int}) where {N} = ntuple(i-> read(fits[ext[i]]), N) -# _load(fits::NTuple{N, FITS}, ext::NTuple{N, Int}) where {N} = ntuple(i -> _load(fits[i], ext[i]), N) - -_header(fits::FITS, ext::Int) = WCS.from_header(read_header(fits[ext], String))[1] -_header(fits::FITS, ext::NTuple{N, Int}) where {N} = - ntuple(i -> WCS.from_header(read_header(fits[ext[i]], String))[1], N) -_header(fits::NTuple{N, FITS}, ext::NTuple{N, Int}) where {N} = - ntuple(i -> _header(fits[i], ext[i]), N) -""" - load(fitsfile::String, n=1) - -Read and return the data from `n`-th extension of the FITS file. - -Second argument can also be a tuple of integers, in which case a -tuple with the data of each corresponding extension is returned. -""" -function FileIO.load(f::File{format"FITS"}, ext::Int=1) - fits = FITS(f.filename) - out = _load(fits, ext) - header = _header(fits,ext) - close(fits) - return out, header -end +# # _load(fits::NTuple{N, FITS}, ext::NTuple{N, Int}) where {N} = ntuple(i -> _load(fits[i], ext[i]), N) + +# _header(fits::FITS, ext::Int) = WCS.from_header(read_header(fits[ext], String))[1] +# _header(fits::FITS, ext::NTuple{N, Int}) where {N} = +# ntuple(i -> WCS.from_header(read_header(fits[ext[i]], String))[1], N) +# _header(fits::NTuple{N, FITS}, ext::NTuple{N, Int}) where {N} = +# ntuple(i -> _header(fits[i], ext[i]), N) +# """ +# load(fitsfile::String, n=1) + +# Read and return the data from `n`-th extension of the FITS file. + +# Second argument can also be a tuple of integers, in which case a +# tuple with the data of each corresponding extension is returned. +# """ +# function FileIO.load(f::File{format"FITS"}, ext::Int=1) +# fits = FITS(f.filename) +# out = _load(fits, ext) +# header = _header(fits,ext) +# close(fits) +# return out, header +# end -function FileIO.load(f::File{format"FITS"}, ext::NTuple{N,Int}) where {N} - fits = FITS(f.filename) - out = _load(fits, ext) - header = _header(fits, ext) - close(fits) - return out, header -end +# function FileIO.load(f::File{format"FITS"}, ext::NTuple{N,Int}) where {N} +# fits = FITS(f.filename) +# out = _load(fits, ext) +# header = _header(fits, ext) +# close(fits) +# return out, header +# end -function FileIO.load(f::NTuple{N, String}) where {N} - fits = ntuple(i-> FITS(f[i]), N) - ext = indexer(fits) - out = _load(fits, ext) - header = _header(fits, ext) - for i in 1:N - close(fits[i]) - end - return out, header -end +# function FileIO.load(f::NTuple{N, String}) where {N} +# fits = ntuple(i-> FITS(f[i]), N) +# ext = indexer(fits) +# out = _load(fits, ext) +# header = _header(fits, ext) +# for i in 1:N +# close(fits[i]) +# end +# return out, header +# end -function indexer(fits::FITS) - ext = 0 - for (i, hdu) in enumerate(fits) - if hdu isa ImageHDU && length(size(hdu)) >= 2 # check if Image is atleast 2D - ext = i - break - end - end - if ext > 1 - @info "Image was loaded from HDU $ext" - elseif ext == 0 - error("There are no ImageHDU extensions in '$(fits.filename)'") - end - return ext -end -indexer(fits::NTuple{N, FITS}) where {N} = ntuple(i -> indexer(fits[i]), N) +# function indexer(fits::FITS) +# ext = 0 +# for (i, hdu) in enumerate(fits) +# if hdu isa ImageHDU && length(size(hdu)) >= 2 # check if Image is atleast 2D +# ext = i +# break +# end +# end +# if ext > 1 +# @info "Image was loaded from HDU $ext" +# elseif ext == 0 +# error("There are no ImageHDU extensions in '$(fits.filename)'") +# end +# return ext +# end +# indexer(fits::NTuple{N, FITS}) where {N} = ntuple(i -> indexer(fits[i]), N) # Images.jl expects data to be either a float or a fixed-point number. Here we define some # utilities to convert all data types supported by FITS format to float or fixed-point: @@ -85,6 +85,7 @@ for n in (8, 16, 32, 64) end end + mutable struct Properties{P <: Union{AbstractFloat, FixedPoint}} rgb_image::MappedArrays.MultiMappedArray{RGB{P},2,Tuple{Array{P,2},Array{P,2},Array{P,2}},Type{RGB{P}},typeof(ImageCore.extractchannels)} contrast::Float64 @@ -116,7 +117,9 @@ Images.arraydata(img::AstroImage) = img.data headers(img::AstroImage) = img.headers function wcs(img::AstroImage) if img.wcs_stale + @info "Regenerating WCS" img.wcs = only(WCS.from_header(string(headers(img)), ignore_rejected=true)) + img.wcs_stale = false end return img.wcs end @@ -171,6 +174,38 @@ function Base.push!(img::AstroImage, ::Type{History}, history::AbstractString) push!(hdr.comments, history) end +""" + copyheaders(img::AstroImage, data) -> imgnew +Create a new image copying the headers of `img` but +using the data of the AbstractArray `data`. Note that changing the +headers of `imgnew` does not affect the headers of `img`. +See also: [`shareheaders`](@ref). +""" +copyheaders(img::AstroImage, data::AbstractArray) = + AstroImage(data, deepcopy(headers(img)), img.wcs) + +""" + shareheaders(img::AstroImage, data) -> imgnew +Create a new image reusing the headers dictionary of `img` but +using the data of the AbstractArray `data`. The two images have +synchronized headers; modifying one also affects the other. +See also: [`copyheaders`](@ref). +""" +shareheaders(img::AstroImage, data::AbstractArray) = AstroImage(data, headers(img), img.wcs) +maybe_shareheaders(img::AstroImage, data) = shareheaders(img, data) +maybe_shareheaders(img::AbstractArray, data) = data + +# Iteration +# Defer to the array object in case it has special iteration defined +Base.iterate(img::AstroImage) = Base.iterate(arraydata(img)) +Base.iterate(img::AstroImage, s) = Base.iterate(arraydata(img), s) + +Images.restrict(img::AstroImage, ::Tuple{}) = img +Images.restrict(img::AstroImage, region::Dims) = shareheaders(img, restrict(arraydata(img), region)) + +# TODO: use WCS +# ImageCore.pixelspacing(img::ImageMeta) = pixelspacing(arraydata(img)) + Base.promote_rule(::Type{AstroImage{T}}, ::Type{AstroImage{V}}) where {T,V} = AstroImage{promote_type{T,V}} # function Base.similar(img::AstroImage) where T # dat = similar(arraydata(img)) @@ -191,9 +226,22 @@ Base.promote_rule(::Type{AstroImage{T}}, ::Type{AstroImage{V}}) where {T,V} = As # Broadcasting -# Base.copy(img::AstroImage) = AstroImage(copy(arraydata(img)), deepcopy(headers(img))) -# Base.convert(::Type{AstroImage{T}}, img::AstroImage{V}) where {T,V} = AstroImage{T}(arraydata(img), headers(img)) -# Base.view(img::AstroImage, inds...) = AstroImage(view(arraydata(img), inds...), headers(img)) +Base.copy(img::AstroImage) = AstroImage( + copy(arraydata(img)), + deepcopy(headers(img)), + # We copy the headers but share the WCS object. + # If the headers change such that wcs is now out of date, + # a new wcs will be generated when needed. + img.wcs +) +Base.convert(::Type{AstroImage}, A::AstroImage) = A +Base.convert(::Type{AstroImage}, A::AbstractArray) = AstroImage(A) +Base.convert(::Type{AstroImage{T}}, A::AstroImage{T}) where {T} = A +Base.convert(::Type{AstroImage{T}}, A::AstroImage) where {T} = shareheaders(A, convert(AbstractArray{T}, arraydata(A))) +Base.convert(::Type{AstroImage{T}}, A::AbstractArray{T}) where {T} = AstroImage(A) +Base.convert(::Type{AstroImage{T}}, A::AbstractArray) where {T} = AstroImage(convert(AbstractArray{T}, A)) + +Base.view(img::AstroImage, inds...) = shareheaders(img, view(arraydata(img), inds...)) # Base.selectdim(img::AstroImage, d::Integer, idxs) = AstroImage(selectdim(arraydata(img), d, idxs), headers(img)) # broadcast mechanics Base.BroadcastStyle(::Type{<:AstroImage}) = Broadcast.ArrayStyle{AstroImage}() @@ -202,14 +250,17 @@ function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{AstroImage} dat = similar(arraydata(img), T, axes(bc)) T2 = eltype(dat) N = ndims(dat) + # We copy the headers but share the WCS object. + # If the headers change such that wcs is now out of date, + # a new wcs will be generated when needed. return AstroImage{T2,N,typeof(dat)}( dat, # img.minmax, # true, - headers(img), + deepcopy(headers(img)), img.wcs, # img.property, - false, + false ) end "`A = find_img(As)` returns the first AstroImage among the arguments." @@ -228,8 +279,8 @@ find_img(::Any, rest) = find_img(rest) Construct an `AstroImage` object of `data`, using `color` as color map, `Gray` by default. """ AstroImage(img::AstroImage) = img -AstroImage(data::AbstractArray{T,N}, headers::FITSHeader, wcs::WCSTransform) where {T,N} = - AstroImage{T,N,typeof(data)}(data, headers, wcs) +# AstroImage(data::AbstractArray{T,N}, headers::FITSHeader, wcs::WCSTransform) where {T,N} = +# AstroImage{T,N,typeof(data)}(data, headers, wcs) # AstroImage(color::Type{<:Color}, data::AbstractArray{T,N}, wcs::WCSTransform) where {T<:Real,N<:Int} = # AstroImage{T, N, color, Float64}(data, extrema(data), false, wcs, Properties{Float64}()) @@ -249,9 +300,9 @@ function AstroImage( # color::Type{<:Color}, data::AbstractArray{T,N}, # properties::Properties=Properties{Float64}(), - header::FITSHeader=FITSHeader(String[],[],String[]), - wcs::WCSTransform=only(WCS.from_header(string(header), ignore_rejected=true)) -) where {T<:Real, N} + header::FITSHeader=emptyheaders(), + wcs::WCSTransform=wcsfromheaders(data,header) +) where {T, N} return AstroImage{T,N,typeof(data)}(data, header, wcs, false) end # AstroImage(data::Matrix{T}) where {T<:Real} = AstroImage{T,Gray,1, Float64}(data, (extrema(data),), (WCSTransform(2),), Properties{Float64}()) @@ -259,6 +310,18 @@ end # AstroImage(data::Matrix{T}, wcs::WCSTransform) where {T<:Real} = AstroImage{T,Gray,1, Float64}((data,), (extrema(data),), (wcs,), Properties{Float64}()) # AstroImage(data::NTuple{N, Matrix{T}}, wcs::NTuple{N, WCSTransform}) where {T<:Real, N} = AstroImage{T,Gray,N, Float64}(data, ntuple(i -> extrema(data[i]), N), wcs, Properties{Float64}()) +emptyheaders() = FITSHeader(String[],[],String[]) +function wcsfromheaders(data, head::FITSHeader) + wcsout = WCS.from_header(string(head), ignore_rejected=true) + if length(wcsout) == 1 + return only(wcsout) + elseif length(wcsout) == 0 + return WCSTransform(ndims(data)) + else + error("Mutiple WCSTransform returned from headers") + end +end + """ AstroImage([color=Gray,] filename::String, n::Int=1) AstroImage(color::Type{<:Color}, file::String, n::NTuple{N, Int}) where {N} @@ -289,6 +352,8 @@ AstroImage(file::String) = AstroImage(FITS(file,"r")) + + """ set_brightness!(img::AstroImage, value::AbstractFloat) From 73d427294d7982c950f6249a52c8972eb861d2d6 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Wed, 29 Dec 2021 10:49:36 -0800 Subject: [PATCH 008/178] ds9 stretches and imshow edge cases --- src/showmime.jl | 196 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 138 insertions(+), 58 deletions(-) diff --git a/src/showmime.jl b/src/showmime.jl index 2b12dbb6..c58b6b97 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -1,25 +1,25 @@ -_brightness_contrast(color, matrix::AbstractMatrix{T}, brightness, contrast) where {T} = - @. color(matrix / 255 * T(contrast) + T(brightness) / 255) +# _brightness_contrast(color, matrix::AbstractMatrix{T}, brightness, contrast) where {T} = +# @. color(matrix / 255 * T(contrast) + T(brightness) / 255) -""" - brightness_contrast(image::AstroImage; brightness_range = 0:255, contrast_range = 1:1000, header_number = 1) +# """ +# brightness_contrast(image::AstroImage; brightness_range = 0:255, contrast_range = 1:1000, header_number = 1) -Visualize the fits image by changing the brightness and contrast of image. +# Visualize the fits image by changing the brightness and contrast of image. -Users can also provide their own range as keyword arguments. -""" -function brightness_contrast(img::AstroImage{T,N}; brightness_range = 0:255, - contrast_range = 1:1000, header_number = 1) where {T,N} - @manipulate for brightness in brightness_range, contrast in contrast_range - _brightness_contrast(C, img.data[header_number], brightness, contrast) - end -end +# Users can also provide their own range as keyword arguments. +# """ +# function brightness_contrast(img::AstroImage{T,N}; brightness_range = 0:255, +# contrast_range = 1:1000, header_number = 1) where {T,N} +# @manipulate for brightness in brightness_range, contrast in contrast_range +# _brightness_contrast(C, img.data[header_number], brightness, contrast) +# end +# end # This is used in Jupyter notebooks -Base.show(io::IO, mime::MIME"text/html", img::AstroImage; kwargs...) = - show(io, mime, brightness_contrast(img), kwargs...) +# Base.show(io::IO, mime::MIME"text/html", img::AstroImage; kwargs...) = +# show(io, mime, brightness_contrast(img), kwargs...) -# This is used in Jupyter notebooks +# This is used in VSCode and others Base.show(io::IO, mime::MIME"image/png", img::AstroImage; kwargs...) = show(io, mime, imshow(img), kwargs...) @@ -40,31 +40,35 @@ function set_clims!(clims) _default_clims[] = clims end -""" - percent(99.5) +skipmissingnan(itr) = Iterators.filter(el->!ismissing(el) && isfinite(el), itr) -Returns a function that calculates display limits that include the given -percent of the image data. - -Example: -```julia -julia> imshow(img, clims=percent(90)) -``` -This will set the limits to be the 5th percentile to the 95th percentile. -""" -function percent(perc::Number) - trim = (1 - perc/100)/2 - clims(data) = quantile(data, (trim, 1-trim)) - clims(data::AbstractMatrix) = quantile(vec(data), (trim, 1-trim)) - return clims -end -export percent +# These reproduce the behaviour of DS9 according to http://ds9.si.edu/doc/ref/how.html +logstretch(x,a=1000) = log(a*x+1)/log(a) +powstretch(x,a=1000) = (a^x - 1)/a +sqrtstretch = sqrt +squarestretch(x) = x^2 +asinhstretch(x) = asinh(10x)/3 +sinhstretch(x) = sinh(3x)/10 +# The additional stretches reproduce behaviour from astropy +powerdiststretch(x, a=1000) = (a^x - 1) / (a - 1) +export logstretch, powstretch, sqrtstretch, squarestretch, asinhstretch, sinhstretch, powerdiststretch function imshow( img::AbstractMatrix{T}; clims=_default_clims[], - cmap=_default_cmap[] + stretch=identity, + cmap=_default_cmap[], ) where {T} + isempt = isempty(img) + if isempt + return + end + # Users will occaisionally pass in data that is 0D, filled with NaN, or filled with missing. + # We still need to do something reasonable in those caes. + nonempty = any(x-> !ismissing(x) && isfinite(x), img) + if !nonempty + return + end # Users can pass clims as an array or tuple containing the minimum and maximum values if typeof(clims) <: AbstractArray || typeof(clims) <: Tuple if length(clims) != 2 @@ -74,41 +78,65 @@ function imshow( imgmax = convert(T, last(clims)) # Or as a callable that computes them given an iterator else - imgmin_0, imgmax_0 = clims(Iterators.filter(pix->isfinite(pix) && !ismissing(pix), img)) + if nonempty + imgmin_0, imgmax_0 = clims(skipmissingnan(img)) + else + # Fallback for empty images + imgmin_0, imgmax_0 = 0,1 + end imgmin = convert(T, imgmin_0) imgmax = convert(T, imgmax_0) end - return imshow(img,(imgmin,imgmax),cmap) + normed = normedclampedview(img, (imgmin, imgmax)) + return _imshow(normed,stretch,cmap) end -function imshow(img::AbstractMatrix{T}, clims::Union{<:AbstractArray{<:T},Tuple{T,T}}, cmap) where {T} - - if length(clims) != 2 - error("clims must have exactly two values if provided.") +function _imshow(normed::AbstractArray{T}, stretch, cmap) where T + if T <: Union{Missing,<:Number} + TT = typeof(first(skipmissing(normed))) + else + TT = T end - imgmin, imgmax = clims - - # Pure grayscale display + # minstep(::Type{T}) where {T<:AbstractFloat} = eps(T) + # minstep(::Type{Bool}) = false + # minstep(T) = one(T) + # stretchmin = convert(TT, stretch(zero(TT)+minstep(TT))) + # stretchmax = convert(TT, stretch(one(T))) + stretchmin = 0 + stretchmax = 1 + # if T == Bool + # Tout = N0f8 + # else + Tout = T + # end + + # No color map if isnothing(cmap) - f = scaleminmax(_float(imgmin), _float(max(imgmax, imgmax + one(T)))) - return mappedarray(Gray ∘ f, img) + f = scaleminmax(stretchmin, stretchmax) + return mappedarray(normed) do pix + if ismissing(pix) + return Gray{TT}(0) + else + stretched = isfinite(pix) ? stretch(pix) : pix + return Gray{TT}(f(stretched)) + end + end # Monochromatic image using a colormap else cscheme = ColorSchemes.colorschemes[cmap] - # We create a MappedArray that converts from image data - # to RGBA values on the fly according to a colorscheme. - return mappedarray(img) do pix + return mappedarray(normed) do pix + stretched = !ismissing(pix) && isfinite(pix) ? stretch(pix) : pix + # We treat NaN/missing values as transparent + return if ismissing(stretched) || !isfinite(stretched) + RGBA{TT}(0,0,0,0) # We treat Inf values as white / -Inf as black - return if isinf(pix) - if pix > 0 - RGBA{T}(1,1,1,1) + elseif isinf(stretched) + if stretched > 0 + RGBA{TT}(1,1,1,1) else - RGBA{T}(0,0,0,1) + RGBA{TT}(0,0,0,1) end - # We treat NaN/missing values as transparent - elseif !isfinite(pix) || ismissing(pix) - RGBA{T}(0,0,0,0) else - RGBA{T}(get(cscheme::ColorScheme, pix, (imgmin, imgmax))) + RGBA{TT}(get(cscheme::ColorScheme, stretched, (stretchmin, stretchmax))) end end end @@ -116,6 +144,58 @@ function imshow(img::AbstractMatrix{T}, clims::Union{<:AbstractArray{<:T},Tuple{ end export imshow +""" + percent(99.5) + +Returns a function that calculates display limits that include the given +percent of the image data. + +Example: +```julia +julia> imshow(img, clims=percent(90)) +``` +This will set the limits to be the 5th percentile to the 95th percentile. +""" +function percent(perc::Number) + trim = (1 - perc/100)/2 + clims(data) = quantile(data, (trim, 1-trim)) + clims(data::AbstractMatrix) = quantile(vec(data), (trim, 1-trim)) + return clims +end +export percent + +# TODO: is this the correct function to extend? +# Instead of using a datatype like N0f32 to interpret integers as fixed point values in [0,1], +# we use a mappedarray to map the native data range (regardless of type) to [0,1] +Images.normedview(img::AstroImage{<:FixedPoint}) = img +function Images.normedview(img::AstroImage{T}) where T + imgmin, imgmax = extrema(skipmissingnan(img)) + Δ = abs(imgmax - imgmin) + Tout = _Float(T) + normeddata = mappedarray( + pix -> (pix - imgmin)/Δ, + pix_norm -> convert(T, pix_norm*Δ + imgmin), + img + ) + return shareheaders(img, normeddata) +end +export normedview + + +function normedclampedview(img::AbstractArray{T}, lims) where T + imgmin, imgmax = lims + Δ = abs(imgmax - imgmin) + normeddata = mappedarray( + pix -> clamp((pix - imgmin)/Δ, 0, 1), + pix_norm -> convert(T, pix_norm*Δ + imgmin), + img + ) + return maybe_shareheaders(img, normeddata) +end +function normedclampedview(img::AbstractArray{Bool}, lims) + return img +end +export normedclampedview # Lazily reinterpret the AstroImage as a Matrix{Color}, upon request. # By itself, Images.colorview works fine on AstroImages. But @@ -129,4 +209,4 @@ function render(img::AstroImage{T,N}) where {T,N} f = scaleminmax(_float(imgmin), _float(max(imgmax, imgmax + one(T)))) return colorview(Gray, f.(_float.(img.data))) end -Images.colorview(img::AstroImage) = render(img) \ No newline at end of file +Images.colorview(img::AstroImage) = render(img) From c332a391594dd7fe5b040424cddfa3fe86832389 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Wed, 29 Dec 2021 10:51:56 -0800 Subject: [PATCH 009/178] Add default stretch option --- src/showmime.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/showmime.jl b/src/showmime.jl index c58b6b97..7e055033 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -31,6 +31,7 @@ export zscale const _default_clims = Ref{Any}(extrema) const _default_cmap = Ref{Union{Symbol,Nothing}}(nothing) +const _default_stretch = Ref{Any}(identity) function set_cmap!(cmap) _default_cmap[] = cmap @@ -40,6 +41,10 @@ function set_clims!(clims) _default_clims[] = clims end +function set_stretch!(stretch) + _default_stretch[] = stretch +end + skipmissingnan(itr) = Iterators.filter(el->!ismissing(el) && isfinite(el), itr) # These reproduce the behaviour of DS9 according to http://ds9.si.edu/doc/ref/how.html @@ -56,7 +61,7 @@ export logstretch, powstretch, sqrtstretch, squarestretch, asinhstretch, sinhstr function imshow( img::AbstractMatrix{T}; clims=_default_clims[], - stretch=identity, + stretch=_default_stretch[], cmap=_default_cmap[], ) where {T} isempt = isempty(img) From b72b7b0c4a55a31defcc1f9ce8f6ecce76c5f53a Mon Sep 17 00:00:00 2001 From: William Thompson Date: Wed, 29 Dec 2021 14:13:31 -0800 Subject: [PATCH 010/178] Add back additional file loading methods --- src/AstroImages.jl | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index c804dee6..edb61ea1 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -15,21 +15,20 @@ _load(fits::FITS, ext::Int) = read(fits[ext]) # ntuple(i -> WCS.from_header(read_header(fits[ext[i]], String))[1], N) # _header(fits::NTuple{N, FITS}, ext::NTuple{N, Int}) where {N} = # ntuple(i -> _header(fits[i], ext[i]), N) -# """ -# load(fitsfile::String, n=1) +""" + load(fitsfile::String, n=1) -# Read and return the data from `n`-th extension of the FITS file. +Read and return the data from `n`-th extension of the FITS file. + +Second argument can also be a tuple of integers, in which case a +tuple with the data of each corresponding extension is returned. +""" +function FileIO.load(f::File{format"FITS"}, ext::Int=1) + return FITS(f.filename) do fits + AstroImage(fits, ext) + end +end -# Second argument can also be a tuple of integers, in which case a -# tuple with the data of each corresponding extension is returned. -# """ -# function FileIO.load(f::File{format"FITS"}, ext::Int=1) -# fits = FITS(f.filename) -# out = _load(fits, ext) -# header = _header(fits,ext) -# close(fits) -# return out, header -# end # function FileIO.load(f::File{format"FITS"}, ext::NTuple{N,Int}) where {N} # fits = FITS(f.filename) @@ -105,9 +104,6 @@ end mutable struct AstroImage{T, N, TDat} <: AbstractArray{T,N} data::TDat - # minmax::Tuple{T,T} - # minmaxdirty::Bool - # property::Properties{P} headers::FITSHeader wcs::WCSTransform wcs_stale::Bool @@ -193,7 +189,7 @@ See also: [`copyheaders`](@ref). """ shareheaders(img::AstroImage, data::AbstractArray) = AstroImage(data, headers(img), img.wcs) maybe_shareheaders(img::AstroImage, data) = shareheaders(img, data) -maybe_shareheaders(img::AbstractArray, data) = data +maybe_shareheaders(::AbstractArray, data) = data # Iteration # Defer to the array object in case it has special iteration defined @@ -224,7 +220,6 @@ Base.promote_rule(::Type{AstroImage{T}}, ::Type{AstroImage{V}}) where {T,V} = As # ) # end - # Broadcasting Base.copy(img::AstroImage) = AstroImage( copy(arraydata(img)), @@ -255,11 +250,8 @@ function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{AstroImage} # a new wcs will be generated when needed. return AstroImage{T2,N,typeof(dat)}( dat, - # img.minmax, - # true, deepcopy(headers(img)), img.wcs, - # img.property, false ) end @@ -271,7 +263,6 @@ find_img(::Tuple{}) = nothing find_img(a::AstroImage, rest) = a find_img(::Any, rest) = find_img(rest) - """ AstroImage([color=Gray,] data::Matrix{Real}) AstroImage(color::Type{<:Color}, data::NTuple{N, Matrix{T}}) where {T<:Real, N} @@ -296,10 +287,9 @@ AstroImage(img::AstroImage) = img # return AstroImage{T,color,N, Float64}(data, ntuple(i -> extrema(data[i]), N), wcs, Properties{Float64}(rgb_image = img)) # end # end +emptyheaders() = FITSHeader(String[],FITSIO.HeaderTypes[],String[]) function AstroImage( - # color::Type{<:Color}, data::AbstractArray{T,N}, - # properties::Properties=Properties{Float64}(), header::FITSHeader=emptyheaders(), wcs::WCSTransform=wcsfromheaders(data,header) ) where {T, N} @@ -310,7 +300,6 @@ end # AstroImage(data::Matrix{T}, wcs::WCSTransform) where {T<:Real} = AstroImage{T,Gray,1, Float64}((data,), (extrema(data),), (wcs,), Properties{Float64}()) # AstroImage(data::NTuple{N, Matrix{T}}, wcs::NTuple{N, WCSTransform}) where {T<:Real, N} = AstroImage{T,Gray,N, Float64}(data, ntuple(i -> extrema(data[i]), N), wcs, Properties{Float64}()) -emptyheaders() = FITSHeader(String[],[],String[]) function wcsfromheaders(data, head::FITSHeader) wcsout = WCS.from_header(string(head), ignore_rejected=true) if length(wcsout) == 1 @@ -339,6 +328,7 @@ Use `color` as color map, this is `Gray` by default. # AstroImage(file::String, ext::NTuple{N, Int}) where {N} = AstroImage(Gray, file, ext) AstroImage(fits::FITS, ext::Int=1) = AstroImage(_load(fits, ext), read_header(fits[ext])) +AstroImage(hdu::HDU) = AstroImage(read(hdu), read_header(hdu)) # AstroImage(color::Type{<:Color}, fits::FITS, ext::NTuple{N, Int}) where {N} = # AstroImage(color, _load(fits, ext), ntuple(i -> WCS.from_header(read_header(fits[ext[i]], String))[1], N)) # AstroImage(color::Type{<:Color}, fits::NTuple{N, FITS}, ext::NTuple{N, Int}) where {N} = From 65d126e9fa159d48bea0f179dfaf266af5fafe38 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Wed, 29 Dec 2021 14:14:14 -0800 Subject: [PATCH 011/178] Rename `imshow` to `imview` --- src/showmime.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/showmime.jl b/src/showmime.jl index 7e055033..f0cc8244 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -21,7 +21,7 @@ # This is used in VSCode and others Base.show(io::IO, mime::MIME"image/png", img::AstroImage; kwargs...) = - show(io, mime, imshow(img), kwargs...) + show(io, mime, imview(img), kwargs...) using Statistics using MappedArrays @@ -58,7 +58,7 @@ sinhstretch(x) = sinh(3x)/10 powerdiststretch(x, a=1000) = (a^x - 1) / (a - 1) export logstretch, powstretch, sqrtstretch, squarestretch, asinhstretch, sinhstretch, powerdiststretch -function imshow( +function imview( img::AbstractMatrix{T}; clims=_default_clims[], stretch=_default_stretch[], @@ -93,9 +93,9 @@ function imshow( imgmax = convert(T, imgmax_0) end normed = normedclampedview(img, (imgmin, imgmax)) - return _imshow(normed,stretch,cmap) + return _imview(normed,stretch,cmap) end -function _imshow(normed::AbstractArray{T}, stretch, cmap) where T +function _imview(normed::AbstractArray{T}, stretch, cmap) where T if T <: Union{Missing,<:Number} TT = typeof(first(skipmissing(normed))) else @@ -147,7 +147,7 @@ function _imshow(normed::AbstractArray{T}, stretch, cmap) where T end end -export imshow +export imview """ percent(99.5) @@ -157,7 +157,7 @@ percent of the image data. Example: ```julia -julia> imshow(img, clims=percent(90)) +julia> imview(img, clims=percent(90)) ``` This will set the limits to be the 5th percentile to the 95th percentile. """ From 5972a7cd392aecf7fd62e120bc9e7c677633f607 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Thu, 30 Dec 2021 07:44:24 -0800 Subject: [PATCH 012/178] Improve documentation --- Project.toml | 1 + src/AstroImages.jl | 178 +++++++++++++++++++++++++++++++++++++-------- src/showmime.jl | 12 +-- 3 files changed, 154 insertions(+), 37 deletions(-) diff --git a/Project.toml b/Project.toml index 19f1f1fc..f3e43d66 100644 --- a/Project.toml +++ b/Project.toml @@ -16,6 +16,7 @@ OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043" RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" Reproject = "d1dcc2e6-806e-11e9-2897-3f99785db2ae" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" WCS = "15f3aee2-9e10-537f-b834-a6fb8bdb944d" [compat] diff --git a/src/AstroImages.jl b/src/AstroImages.jl index edb61ea1..5759e61d 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -102,6 +102,11 @@ mutable struct Properties{P <: Union{AbstractFloat, FixedPoint}} end end + +""" +Provides access to a FITS image along with its accompanying +headers and WCS information, if applicable. +""" mutable struct AstroImage{T, N, TDat} <: AbstractArray{T,N} data::TDat headers::FITSHeader @@ -109,11 +114,41 @@ mutable struct AstroImage{T, N, TDat} <: AbstractArray{T,N} wcs_stale::Bool end + +# Think about re-creating an image wrapper type. +# This can have properties about the stretch that can +# be modified +# struct AstroImageColorView1 <: Ast +# end +# What do we want? Indexable just like it is currently +# Not settable though +# Same headers +# Would need to store: +# * clims +# * stretch +# * mappedarray +# * properties of stretches? +struct AstroImageView{T,N,TImg,TMap,#=P<:Properties=#} <: AbstractArray{T,N} + image::TImg + mapper::TMap + # properties::P +end +AstroImageView( + img::AbstractArray{T1,N}, + mapper::AbstractArray{T2,N}, +) where {T1,T2,N} = AstroImageView{T2,N,typeof(img),typeof(mapper)}(img,mapper) + + +""" + Images.arraydata(img::AstroImage) +""" Images.arraydata(img::AstroImage) = img.data +Images.arraydata(img::AstroImageView) = img.image +Images.arraydata(img::AstroImageView{T,N,AstroImage}) where {T,N} = arraydata(img.image) headers(img::AstroImage) = img.headers +headers(img::AstroImageView) = headers(img.image) function wcs(img::AstroImage) if img.wcs_stale - @info "Regenerating WCS" img.wcs = only(WCS.from_header(string(headers(img)), ignore_rejected=true)) img.wcs_stale = false end @@ -128,20 +163,34 @@ export Comment struct History end export History +AstroImageOrView = Union{AstroImage,AstroImageView} # extending the AbstractArray interface -Base.size(img::AstroImage) = size(arraydata(img)) -Base.length(img::AstroImage) = length(arraydata(img)) -Base.getindex(img::AstroImage, inds...) = getindex(arraydata(img), inds...) # default fallback for operations on Array +Base.size(img::AstroImageOrView) = size(arraydata(img)) +Base.length(img::AstroImageOrView) = length(arraydata(img)) + +function Base.getindex(img::AstroImage, inds...) + dat = getindex(arraydata(img), inds...) + if ndims(dat) == 0 + return dat + else + return copyheaders(img, dat) + end +end + +Base.getindex(img::AstroImageView, inds...) = getindex(img.mapper, inds...) # default fallback for operations on Array Base.setindex!(img::AstroImage, v, inds...) = setindex!(arraydata(img), v, inds...) # default fallback for operations on Array Base.getindex(img::AstroImage, inds::AbstractString...) = getindex(headers(img), inds...) # accesing header using strings +Base.getindex(img::AstroImageView, inds::AbstractString...) = getindex(headers(img), inds...) # accesing header using strings function Base.setindex!(img::AstroImage, v, ind::AbstractString) # modifying header using a string setindex!(headers(img), v, ind) # Mark the WCS object as beign out of date if this was a WCS header keyword if ind ∈ WCS_HEADERS_2 img.wcs_stale = true end - @show ind +end +function Base.setindex!(aview::AstroImageView, v, ind::AbstractString) # modifying header using a string + setindex!(aview.image, v, ind) end Base.getindex(img::AstroImage, inds::Symbol...) = getindex(img, string.(inds)...) # accessing header using symbol Base.setindex!(img::AstroImage, v, ind::Symbol) = setindex!(img, v, string(ind)) @@ -163,13 +212,14 @@ function Base.getindex(img::AstroImage, ::Type{Comment}) return view(hdr.comments, ii) end # Adding new history entries -function Base.push!(img::AstroImage, ::Type{History}, history::AbstractString) +function Base.push!(img::AstroImageOrView, ::Type{History}, history::AbstractString) hdr = headers(img) push!(hdr.keys, "HISTORY") push!(hdr.values, nothing) push!(hdr.comments, history) end +# TODO: do we need to adjust CRPIX_ when selecting a subset of the image? """ copyheaders(img::AstroImage, data) -> imgnew Create a new image copying the headers of `img` but @@ -193,16 +243,20 @@ maybe_shareheaders(::AbstractArray, data) = data # Iteration # Defer to the array object in case it has special iteration defined -Base.iterate(img::AstroImage) = Base.iterate(arraydata(img)) -Base.iterate(img::AstroImage, s) = Base.iterate(arraydata(img), s) +Base.iterate(img::AstroImageOrView) = Base.iterate(arraydata(img)) +Base.iterate(img::AstroImageOrView, s) = Base.iterate(arraydata(img), s) -Images.restrict(img::AstroImage, ::Tuple{}) = img +# Restrict downsizes images by roughly a factor of two. +# We want to keep the wrapper but downsize the underlying array +Images.restrict(img::AstroImageOrView, ::Tuple{}) = img Images.restrict(img::AstroImage, region::Dims) = shareheaders(img, restrict(arraydata(img), region)) +Images.restrict(imgview::AstroImageView, region::Dims) = restrict(imgview.mapper, region) -# TODO: use WCS +# TODO: use WCS info # ImageCore.pixelspacing(img::ImageMeta) = pixelspacing(arraydata(img)) Base.promote_rule(::Type{AstroImage{T}}, ::Type{AstroImage{V}}) where {T,V} = AstroImage{promote_type{T,V}} + # function Base.similar(img::AstroImage) where T # dat = similar(arraydata(img)) # _,_,C,P = TNCP(img) @@ -287,47 +341,80 @@ AstroImage(img::AstroImage) = img # return AstroImage{T,color,N, Float64}(data, ntuple(i -> extrema(data[i]), N), wcs, Properties{Float64}(rgb_image = img)) # end # end -emptyheaders() = FITSHeader(String[],FITSIO.HeaderTypes[],String[]) + +""" + emptyheaders() + +Convenience function to create a FITSHeader with no keywords set. +""" +emptyheaders() = FITSHeader(String[],[],String[]) +""" + emptywcs() + +Given an AbstractArray, return a blank WCSTransform of the appropriate +dimensionality. +""" +emptywcs(data::AbstractArray) = WCSTransform(ndims(data)) + +""" + AstroImage(data::AbstractArray, [headers::FITSHeader,] [wcs::WCSTransform,]) + +Create an AstroImage from an array, and optionally headers or headers and a +WCSTransform. +""" function AstroImage( data::AbstractArray{T,N}, header::FITSHeader=emptyheaders(), - wcs::WCSTransform=wcsfromheaders(data,header) + wcs::Union{WCSTransform,Nothing}=nothing ) where {T, N} - return AstroImage{T,N,typeof(data)}(data, header, wcs, false) + wcs_stale = isnothing(wcs) + if isnothing(wcs) + wcs = emptywcs(data) + end + # If the user passes in a WCSTransform of their own, we use it and mark + # wcs_stale=false. It will be kept unless they manually change a WCS header. + # If they don't pass anythin, we start with empty WCS information regardless + # of what's in the headers but we mark it as stale. + # If/when the WCS info is accessed via `wcs(img)` it will be computed and cached. + # This avoids those computations if the WCS transform is not needed. + # It also allows us to create images with invalid WCS headers, + # only erroring when/if they are used. + return AstroImage{T,N,typeof(data)}(data, header, wcs, wcs_stale) end -# AstroImage(data::Matrix{T}) where {T<:Real} = AstroImage{T,Gray,1, Float64}(data, (extrema(data),), (WCSTransform(2),), Properties{Float64}()) -# AstroImage(data::NTuple{N, Matrix{T}}) where {T<:Real, N} = AstroImage{T,Gray,N, Float64}(data, ntuple(i -> extrema(data[i]), N), ntuple(i-> WCSTransform(2), N), Properties{Float64}()) -# AstroImage(data::Matrix{T}, wcs::WCSTransform) where {T<:Real} = AstroImage{T,Gray,1, Float64}((data,), (extrema(data),), (wcs,), Properties{Float64}()) -# AstroImage(data::NTuple{N, Matrix{T}}, wcs::NTuple{N, WCSTransform}) where {T<:Real, N} = AstroImage{T,Gray,N, Float64}(data, ntuple(i -> extrema(data[i]), N), wcs, Properties{Float64}()) +AstroImage(data::AbstractArray, wcs::WCSTransform) = AstroImage(data, emptyheaders(), wcs) + -function wcsfromheaders(data, head::FITSHeader) +""" + wcsfromheaders(data::AbstractArray, headers::FITSHeader) + +Helper function to create a WCSTransform from an array and +FITSHeaders. +""" +function wcsfromheaders(data::AbstractArray, head::FITSHeader) wcsout = WCS.from_header(string(head), ignore_rejected=true) if length(wcsout) == 1 return only(wcsout) elseif length(wcsout) == 0 - return WCSTransform(ndims(data)) + return emptywcs(data) else error("Mutiple WCSTransform returned from headers") end end -""" - AstroImage([color=Gray,] filename::String, n::Int=1) - AstroImage(color::Type{<:Color}, file::String, n::NTuple{N, Int}) where {N} -Create an `AstroImage` object by reading the `n`-th extension from FITS file `filename`. +""" + AstroImage(fits::FITS, ext::Int=1) -Use `color` as color map, this is `Gray` by default. +Given an open FITS file from the FITSIO library, +load the HDU number `ext` as an AstroImage. """ -# AstroImage(color::Type{<:Color}, file::String, ext::Int) = -# AstroImage(color, file, (ext,)) -# AstroImage(color::Type{<:Color}, file::String, ext::NTuple{N, Int}) where {N} = -# AstroImage(color, load(file, ext)...) +AstroImage(fits::FITS, ext::Int=1) = AstroImage(fits[ext], read_header(fits[ext])) -# AstroImage(file::String, ext::Int) = AstroImage(Gray, file, ext) -# AstroImage(file::String, ext::NTuple{N, Int}) where {N} = AstroImage(Gray, file, ext) +""" + AstroImage(hdu::HDU) -AstroImage(fits::FITS, ext::Int=1) = AstroImage(_load(fits, ext), read_header(fits[ext])) +Given an open FITS HDU, load it as an AstroImage. +""" AstroImage(hdu::HDU) = AstroImage(read(hdu), read_header(hdu)) # AstroImage(color::Type{<:Color}, fits::FITS, ext::NTuple{N, Int}) where {N} = # AstroImage(color, _load(fits, ext), ntuple(i -> WCS.from_header(read_header(fits[ext[i]], String))[1], N)) @@ -338,10 +425,36 @@ AstroImage(hdu::HDU) = AstroImage(read(hdu), read_header(hdu)) # AstroImage(Gray, load(files)...) # AstroImage(color::Type{<:Color}, files::NTuple{N,String}) where {N} = # AstroImage(color, load(files)...) -AstroImage(file::String) = AstroImage(FITS(file,"r")) +""" + img = AstroImage(filename::AbstractString, ext::Integer=1) +Load an image HDU `ext` from the FITS file at `filename` as an AstroImage. +""" +function AstroImage(filename::AbstractString, ext::Integer=1) + return FITS(filename,"r") do fits + return AstroImage(fits[ext]) + end +end +""" + img1, img2 = AstroImage(filename::AbstractString, exts) +Load multiple image HDUs `exts` from an FITS file at `filename` as an AstroImage. +`exts` must be a tuple, range, or array of Integers. + +Example: +```julia +img1, img2 = AstroImage("abc.fits", (1,3)) # loads the first and third HDU as images. +imgs = AstroImage("abc.fits", 1:3) # loads the first three HDUs as images. +``` +""" +function AstroImage(filename::AbstractString, exts::Union{NTuple{N, <:Integer},AbstractArray{<:Integer}}) where {N} + return FITS(filename,"r") do fits + return map(exts) do ext + return AstroImage(fits[ext]) + end + end +end """ @@ -407,5 +520,6 @@ include("wcs_headers.jl") include("showmime.jl") include("plot-recipes.jl") include("ccd2rgb.jl") +include("reproject.jl") end # module diff --git a/src/showmime.jl b/src/showmime.jl index f0cc8244..86dc9f7b 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -20,7 +20,7 @@ # show(io, mime, brightness_contrast(img), kwargs...) # This is used in VSCode and others -Base.show(io::IO, mime::MIME"image/png", img::AstroImage; kwargs...) = +Base.show(io::IO, mime::MIME"image/png", img::AstroImage{T,2}; kwargs...) where {T} = show(io, mime, imview(img), kwargs...) using Statistics @@ -93,9 +93,9 @@ function imview( imgmax = convert(T, imgmax_0) end normed = normedclampedview(img, (imgmin, imgmax)) - return _imview(normed,stretch,cmap) + return _imview(img, normed,stretch,cmap) end -function _imview(normed::AbstractArray{T}, stretch, cmap) where T +function _imview(img, normed::AbstractArray{T}, stretch, cmap) where T if T <: Union{Missing,<:Number} TT = typeof(first(skipmissing(normed))) else @@ -117,7 +117,7 @@ function _imview(normed::AbstractArray{T}, stretch, cmap) where T # No color map if isnothing(cmap) f = scaleminmax(stretchmin, stretchmax) - return mappedarray(normed) do pix + mapper = mappedarray(normed) do pix if ismissing(pix) return Gray{TT}(0) else @@ -128,7 +128,7 @@ function _imview(normed::AbstractArray{T}, stretch, cmap) where T # Monochromatic image using a colormap else cscheme = ColorSchemes.colorschemes[cmap] - return mappedarray(normed) do pix + mapper = mappedarray(normed) do pix stretched = !ismissing(pix) && isfinite(pix) ? stretch(pix) : pix # We treat NaN/missing values as transparent return if ismissing(stretched) || !isfinite(stretched) @@ -146,6 +146,8 @@ function _imview(normed::AbstractArray{T}, stretch, cmap) where T end end + return AstroImageView(img, mapper) + end export imview From f0d7b7f4902051afecef480cb54beb5d1e01d08b Mon Sep 17 00:00:00 2001 From: William Thompson Date: Thu, 30 Dec 2021 07:44:39 -0800 Subject: [PATCH 013/178] Better support for `reproject` --- src/reproject.jl | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/reproject.jl diff --git a/src/reproject.jl b/src/reproject.jl new file mode 100644 index 00000000..87adf9b9 --- /dev/null +++ b/src/reproject.jl @@ -0,0 +1,34 @@ +#= +Additional methods to allow Reproject to work. +=# + +using Reproject + +""" + img_proj, mask = reproject(img_in::AstroImage, img_out::AstroImage) + +Reprojects the AstroImage `img_in` to the coordinates of `img_out` +according to the WCS information/headers using interpolation. +""" +function Reproject.reproject(img_in::AstroImage, img_out::AstroImage) + data_out, mask = reproject(img_in, img_out) + # TODO: should copy the WCS headers from img_out and the remaining + # headers from img_in. + return copyheaders(img_in, data_out) +end + +function Reproject.parse_input_data(input_data::AstroImage, hdu) + input_data, input_data.wcs +end +function Reproject.parse_output_projection(output_data::AstroImage, hdu) + output_data.wcs, size(output_data) +end +function Reproject.pad_edges(array_in::AstroImage{T}) where {T} + image = Matrix{T}(undef, size(array_in)[1] + 2, size(array_in)[2] + 2) + image[2:end-1,2:end-1] = array_in + image[2:end-1,1] = array_in[:,1] + image[2:end-1,end] = array_in[:,end] + image[1,:] = image[2,:] + image[end,:] = image[end-1,:] + return AstroImage(image, headers(array_in), wcs(array_in)) +end \ No newline at end of file From 7ca7ea63d14353592b72660dcf656c50a6ebecdc Mon Sep 17 00:00:00 2001 From: William Thompson Date: Thu, 30 Dec 2021 08:07:32 -0800 Subject: [PATCH 014/178] Performance fix for vector indexing --- src/AstroImages.jl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 5759e61d..f854e1dd 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -169,17 +169,21 @@ AstroImageOrView = Union{AstroImage,AstroImageView} Base.size(img::AstroImageOrView) = size(arraydata(img)) Base.length(img::AstroImageOrView) = length(arraydata(img)) +# Getting and setting data is forwarded to the underlying array +# Accessing a single value or a vector returns just the data. +# Accering a 2+D slice copies the headers and re-wraps the data. function Base.getindex(img::AstroImage, inds...) dat = getindex(arraydata(img), inds...) - if ndims(dat) == 0 + if ndims(dat) <= 1 return dat else return copyheaders(img, dat) end end - Base.getindex(img::AstroImageView, inds...) = getindex(img.mapper, inds...) # default fallback for operations on Array Base.setindex!(img::AstroImage, v, inds...) = setindex!(arraydata(img), v, inds...) # default fallback for operations on Array + +# Getting and setting comments Base.getindex(img::AstroImage, inds::AbstractString...) = getindex(headers(img), inds...) # accesing header using strings Base.getindex(img::AstroImageView, inds::AbstractString...) = getindex(headers(img), inds...) # accesing header using strings function Base.setindex!(img::AstroImage, v, ind::AbstractString) # modifying header using a string @@ -194,7 +198,6 @@ function Base.setindex!(aview::AstroImageView, v, ind::AbstractString) # modify end Base.getindex(img::AstroImage, inds::Symbol...) = getindex(img, string.(inds)...) # accessing header using symbol Base.setindex!(img::AstroImage, v, ind::Symbol) = setindex!(img, v, string(ind)) -# Getting and setting comments Base.getindex(img::AstroImage, ind::AbstractString, ::Type{Comment}) = get_comment(headers(img), ind) # accesing header comment using strings Base.setindex!(img::AstroImage, v, ind::AbstractString, ::Type{Comment}) = set_comment!(headers(img), ind, v) # modifying header comment using strings Base.getindex(img::AstroImage, ind::Symbol, ::Type{Comment}) = get_comment(headers(img), string(ind)) # accessing header comment using symbol From 287963636baa896fc2596e33cc9e0592f8ea89f5 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Thu, 30 Dec 2021 08:07:51 -0800 Subject: [PATCH 015/178] Work around issue introduced for displaying subranges --- src/showmime.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/showmime.jl b/src/showmime.jl index 86dc9f7b..4811e83d 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -146,7 +146,8 @@ function _imview(img, normed::AbstractArray{T}, stretch, cmap) where T end end - return AstroImageView(img, mapper) + # return AstroImageView(img, mapper) + return mapper end export imview From b175aefaed7da970bef609b52564a3db455a9f9c Mon Sep 17 00:00:00 2001 From: William Thompson Date: Thu, 30 Dec 2021 09:57:54 -0800 Subject: [PATCH 016/178] Fix for retrieving single elements from an image that happen to be missing --- src/AstroImages.jl | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index f854e1dd..88e34209 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -174,7 +174,9 @@ Base.length(img::AstroImageOrView) = length(arraydata(img)) # Accering a 2+D slice copies the headers and re-wraps the data. function Base.getindex(img::AstroImage, inds...) dat = getindex(arraydata(img), inds...) - if ndims(dat) <= 1 + # ndims is defined for Numbers but not Missing. + # This check is therefore necessary for img[1,1]->missing to work. + if ismissing(dat) || ndims(dat) <= 1 return dat else return copyheaders(img, dat) @@ -443,12 +445,14 @@ end img1, img2 = AstroImage(filename::AbstractString, exts) Load multiple image HDUs `exts` from an FITS file at `filename` as an AstroImage. -`exts` must be a tuple, range, or array of Integers. +`exts` must be a tuple, range, :, or array of Integers. +All listed HDUs in `exts` must be image HDUs or an error will occur. Example: ```julia img1, img2 = AstroImage("abc.fits", (1,3)) # loads the first and third HDU as images. imgs = AstroImage("abc.fits", 1:3) # loads the first three HDUs as images. +imgs = AstroImage("abc.fits", :) # loads all HDUs as images. ``` """ function AstroImage(filename::AbstractString, exts::Union{NTuple{N, <:Integer},AbstractArray{<:Integer}}) where {N} @@ -458,7 +462,13 @@ function AstroImage(filename::AbstractString, exts::Union{NTuple{N, <:Integer},A end end end - +function AstroImage(filename::AbstractString, ::Colon) where {N} + return FITS(filename,"r") do fits + return map(fits) do hdu + return AstroImage(hdu) + end + end +end """ set_brightness!(img::AstroImage, value::AbstractFloat) From 80f7484feb6de4ceb8871b3700de806599eea5b2 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Thu, 30 Dec 2021 09:58:07 -0800 Subject: [PATCH 017/178] Added docstrings --- src/showmime.jl | 166 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 128 insertions(+), 38 deletions(-) diff --git a/src/showmime.jl b/src/showmime.jl index 4811e83d..0c2742e2 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -29,22 +29,43 @@ using ColorSchemes using PlotUtils: zscale export zscale -const _default_clims = Ref{Any}(extrema) const _default_cmap = Ref{Union{Symbol,Nothing}}(nothing) +const _default_clims = Ref{Any}(extrema) const _default_stretch = Ref{Any}(identity) +""" + set_cmap!(cmap::Symbol) + set_cmap!(cmap::Nothing) + +Alter the default color map used to display images when using +`imview` or displaying an AstroImage. +""" function set_cmap!(cmap) _default_cmap[] = cmap end +""" + set_clims!(clims::Tuple) + set_clims!(clims::Function) +Alter the default limits used to display images when using +`imview` or displaying an AstroImage. +""" function set_clims!(clims) _default_clims[] = clims end +""" + set_stretch!(stretch::Function) +Alter the default value stretch functio used to display images when using +`imview` or displaying an AstroImage. +""" function set_stretch!(stretch) _default_stretch[] = stretch end +""" +Helper to iterate over data skipping missing and non-finite values. +""" skipmissingnan(itr) = Iterators.filter(el->!ismissing(el) && isfinite(el), itr) # These reproduce the behaviour of DS9 according to http://ds9.si.edu/doc/ref/how.html @@ -54,16 +75,70 @@ sqrtstretch = sqrt squarestretch(x) = x^2 asinhstretch(x) = asinh(10x)/3 sinhstretch(x) = sinh(3x)/10 -# The additional stretches reproduce behaviour from astropy +# These additional stretches reproduce behaviour from astropy powerdiststretch(x, a=1000) = (a^x - 1) / (a - 1) export logstretch, powstretch, sqrtstretch, squarestretch, asinhstretch, sinhstretch, powerdiststretch +""" + imview(img; clims=extrema, stretch=identity, cmap=nothing) + +Create a read only view of an array or AstroImage mapping its data values +to Colors according to `clims`, `stretch`, and `cmap`. + +The data is first clamped to `clims`, which can either be a tuple of (min, max) +values or a function accepting an iterator of pixel values that returns (min, max). +By default, `clims=extrema` i.e. the minimum and maximum of `img`. +Convenient functions to use for `clims` are: +`extrema`, `zscale`, and `percent(p)` + +Next, the data is rescaled to [0,1] and remapped according to the function `stretch`. +Stretch can be any monotonic fuction mapping values in the range [0,1] to some range [a,b]. +Note that `log(0)` is not defined so is not directly supported. +For a list of convenient stretch functions, see: +`logstretch`, `powstretch`, `squarestretch`, `asinhstretch`, `sinhstretch`, `powerdiststretch` + +Finally the data is mapped to RGB values according to `cmap`. If cmap is `nothing`, +grayscale is used. ColorSchemes.jl defines hundreds of colormaps. A few nice ones for +images innclude: `:viridis`, `:magma`, `:plasma`, `:thermal`, and `:turbo`. + +Crucially, this function returns a view over the underlying data. If `img` is updated +then those changes will be reflected by this view with the exception of `clims` which +is not recalculated. + +Note: if clims or stretch is a function, the pixel values passed in are first filtered +to remove non-finite or missing values. + +### Defaults +The default values of `clims`, `stretch`, and `cmap` are `extrema`, `identity`, and `nothing` +respectively. +You may alter these defaults using `AstroImages.set_clims!`, `AstroImages.set_stretch!`, and +`AstroImages.set_cmap!`. + +### Automatic Display +Arrays wrapped by `AstroImage()` get displayed as images automatically by calling +`imview` on them with the default settings when using displays that support showing PNG images. + +### Missing data +Pixels that are `NaN` or `missing` will be displayed as transparent when `cmap` is set +or black if. ++/- Inf will be displayed as black or white respectively. + +### Exporting Images +The view returned by `imview` can be saved using general `FileIO.save` methods. +Example: +```julia +v = imview(data, cmap=:magma, stretch=asinhstretch, clims=percent(95)) +save("output.png", v) +``` +""" function imview( img::AbstractMatrix{T}; clims=_default_clims[], stretch=_default_stretch[], cmap=_default_cmap[], ) where {T} + + # TODO: catch this in `show` instead of here. isempt = isempty(img) if isempt return @@ -79,21 +154,14 @@ function imview( if length(clims) != 2 error("clims must have exactly two values if provided.") end - imgmin = convert(T, first(clims)) - imgmax = convert(T, last(clims)) + imgmin = first(clims) + imgmax = last(clims) # Or as a callable that computes them given an iterator else - if nonempty - imgmin_0, imgmax_0 = clims(skipmissingnan(img)) - else - # Fallback for empty images - imgmin_0, imgmax_0 = 0,1 - end - imgmin = convert(T, imgmin_0) - imgmax = convert(T, imgmax_0) + imgmin, imgmax = clims(skipmissingnan(img)) end - normed = normedclampedview(img, (imgmin, imgmax)) - return _imview(img, normed,stretch,cmap) + normed = clampednormedview(img, (imgmin, imgmax)) + return _imview(img, normed, stretch, cmap) end function _imview(img, normed::AbstractArray{T}, stretch, cmap) where T if T <: Union{Missing,<:Number} @@ -101,20 +169,14 @@ function _imview(img, normed::AbstractArray{T}, stretch, cmap) where T else TT = T end - # minstep(::Type{T}) where {T<:AbstractFloat} = eps(T) - # minstep(::Type{Bool}) = false - # minstep(T) = one(T) - # stretchmin = convert(TT, stretch(zero(TT)+minstep(TT))) - # stretchmax = convert(TT, stretch(one(T))) - stretchmin = 0 - stretchmax = 1 - # if T == Bool - # Tout = N0f8 - # else - Tout = T - # end - - # No color map + if TT == Bool + TT = N0f8 + end + + stretchmin = stretch(zero(TT)) + stretchmax = stretch(one(TT)) + + # No color map: use Gray if isnothing(cmap) f = scaleminmax(stretchmin, stretchmax) mapper = mappedarray(normed) do pix @@ -125,16 +187,22 @@ function _imview(img, normed::AbstractArray{T}, stretch, cmap) where T return Gray{TT}(f(stretched)) end end - # Monochromatic image using a colormap + # Monochromatic image using a colormap via ColorSchemes else cscheme = ColorSchemes.colorschemes[cmap] - mapper = mappedarray(normed) do pix - stretched = !ismissing(pix) && isfinite(pix) ? stretch(pix) : pix + mapper = mappedarray(img, normed) do pixr, pixn + if ismissing(pixr) || !isfinite(pixr) || ismissing(pixn) || !isfinite(pixn) + # We check pixr in addition to pixn because we want to preserve if the pixels + # are +-Inf + stretched = pixr + else + stretched = stretch(pixn) + end # We treat NaN/missing values as transparent - return if ismissing(stretched) || !isfinite(stretched) + return if ismissing(stretched) || isnan(stretched) RGBA{TT}(0,0,0,0) # We treat Inf values as white / -Inf as black - elseif isinf(stretched) + elseif isinf(stretched) if stretched > 0 RGBA{TT}(1,1,1,1) else @@ -179,7 +247,6 @@ Images.normedview(img::AstroImage{<:FixedPoint}) = img function Images.normedview(img::AstroImage{T}) where T imgmin, imgmax = extrema(skipmissingnan(img)) Δ = abs(imgmax - imgmin) - Tout = _Float(T) normeddata = mappedarray( pix -> (pix - imgmin)/Δ, pix_norm -> convert(T, pix_norm*Δ + imgmin), @@ -189,21 +256,44 @@ function Images.normedview(img::AstroImage{T}) where T end export normedview +""" + clampednormedview(arr, (min, max)) + +Given an AbstractArray and limits `min,max` return a view of the array +where data between [min, max] are scaled to [0, 1] and datat outside that +range are clamped to [0, 1]. -function normedclampedview(img::AbstractArray{T}, lims) where T +See also: normedview +""" +function clampednormedview(img::AbstractArray{T}, lims) where T imgmin, imgmax = lims Δ = abs(imgmax - imgmin) normeddata = mappedarray( - pix -> clamp((pix - imgmin)/Δ, 0, 1), + pix -> clamp((pix - imgmin)/Δ, zero(T), one(T)), pix_norm -> convert(T, pix_norm*Δ + imgmin), img ) return maybe_shareheaders(img, normeddata) end -function normedclampedview(img::AbstractArray{Bool}, lims) +function clampednormedview(img::AbstractArray{T}, lims) where T <: Normed + # If the data is in a Normed type and the limits are [0,1] then + # it already lies in that range. + if lims[1] == 0 && lims[2] == 1 + return img + end + imgmin, imgmax = lims + Δ = abs(imgmax - imgmin) + normeddata = mappedarray( + pix -> clamp((pix - imgmin)/Δ, zero(T), one(T)), + pix_norm -> pix_norm*Δ + imgmin, + img + ) + return maybe_shareheaders(img, normeddata) +end +function clampednormedview(img::AbstractArray{Bool}, lims) return img end -export normedclampedview +export clampednormedview # Lazily reinterpret the AstroImage as a Matrix{Color}, upon request. # By itself, Images.colorview works fine on AstroImages. But From 747b748bd067dffd71668db490b3456dbb9679d4 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Thu, 30 Dec 2021 09:58:58 -0800 Subject: [PATCH 018/178] Remove duplicate recipe --- src/plot-recipes.jl | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index a9bace70..87b08b97 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -9,15 +9,6 @@ using RecipesBase arraydata(img) end -@recipe function f(img::AstroImage) - seriestype := :heatmap - aspect_ratio := :equal - # Right now we only support single frame images, - # gray scale is a good choice. - color := :grays - arraydata(img) -end - @recipe function f(img::AstroImage, wcs::WCSTransform) seriestype := :heatmap aspect_ratio := :equal From 0c3cf72052870379954afbc3e44e474433c1dc6d Mon Sep 17 00:00:00 2001 From: William Thompson Date: Thu, 30 Dec 2021 10:01:41 -0800 Subject: [PATCH 019/178] export load and save from FileIO --- src/AstroImages.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 88e34209..390e7c88 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -28,7 +28,7 @@ function FileIO.load(f::File{format"FITS"}, ext::Int=1) AstroImage(fits, ext) end end - +export load, save # function FileIO.load(f::File{format"FITS"}, ext::NTuple{N,Int}) where {N} # fits = FITS(f.filename) From f86fcc71255fede2fa7ec577a827ceb5bb1d8193 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 2 Jan 2022 07:35:15 -0800 Subject: [PATCH 020/178] Typo --- src/showmime.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/showmime.jl b/src/showmime.jl index 0c2742e2..f729adc0 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -99,7 +99,7 @@ For a list of convenient stretch functions, see: Finally the data is mapped to RGB values according to `cmap`. If cmap is `nothing`, grayscale is used. ColorSchemes.jl defines hundreds of colormaps. A few nice ones for -images innclude: `:viridis`, `:magma`, `:plasma`, `:thermal`, and `:turbo`. +images include: `:viridis`, `:magma`, `:plasma`, `:thermal`, and `:turbo`. Crucially, this function returns a view over the underlying data. If `img` is updated then those changes will be reflected by this view with the exception of `clims` which From 8d237d6ead5c7cd879e072154d479afe6b8129ae Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 3 Jan 2022 08:36:22 -0800 Subject: [PATCH 021/178] Move from accessing fields directly to getfield to allow future header syntax --- src/AstroImages.jl | 174 ++++++++++++++++++++++++++------------------- 1 file changed, 101 insertions(+), 73 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 390e7c88..7ec79c65 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -115,44 +115,17 @@ mutable struct AstroImage{T, N, TDat} <: AbstractArray{T,N} end -# Think about re-creating an image wrapper type. -# This can have properties about the stretch that can -# be modified -# struct AstroImageColorView1 <: Ast -# end -# What do we want? Indexable just like it is currently -# Not settable though -# Same headers -# Would need to store: -# * clims -# * stretch -# * mappedarray -# * properties of stretches? -struct AstroImageView{T,N,TImg,TMap,#=P<:Properties=#} <: AbstractArray{T,N} - image::TImg - mapper::TMap - # properties::P -end -AstroImageView( - img::AbstractArray{T1,N}, - mapper::AbstractArray{T2,N}, -) where {T1,T2,N} = AstroImageView{T2,N,typeof(img),typeof(mapper)}(img,mapper) - - """ Images.arraydata(img::AstroImage) """ -Images.arraydata(img::AstroImage) = img.data -Images.arraydata(img::AstroImageView) = img.image -Images.arraydata(img::AstroImageView{T,N,AstroImage}) where {T,N} = arraydata(img.image) -headers(img::AstroImage) = img.headers -headers(img::AstroImageView) = headers(img.image) +Images.arraydata(img::AstroImage) = getfield(img, :data) +headers(img::AstroImage) = getfield(img, :headers) function wcs(img::AstroImage) - if img.wcs_stale - img.wcs = only(WCS.from_header(string(headers(img)), ignore_rejected=true)) - img.wcs_stale = false + if getfield(img, :wcs_stale) + setfield(img, :wcs, wcsfromheaders(img)) + setfield(img, :wcs_stale, false) end - return img.wcs + return getfield(img, :wcs) end export arraydata, headers, wcs @@ -163,11 +136,10 @@ export Comment struct History end export History -AstroImageOrView = Union{AstroImage,AstroImageView} # extending the AbstractArray interface -Base.size(img::AstroImageOrView) = size(arraydata(img)) -Base.length(img::AstroImageOrView) = length(arraydata(img)) +Base.size(img::AstroImage) = size(arraydata(img)) +Base.length(img::AstroImage) = length(arraydata(img)) # Getting and setting data is forwarded to the underlying array # Accessing a single value or a vector returns just the data. @@ -182,22 +154,17 @@ function Base.getindex(img::AstroImage, inds...) return copyheaders(img, dat) end end -Base.getindex(img::AstroImageView, inds...) = getindex(img.mapper, inds...) # default fallback for operations on Array Base.setindex!(img::AstroImage, v, inds...) = setindex!(arraydata(img), v, inds...) # default fallback for operations on Array # Getting and setting comments Base.getindex(img::AstroImage, inds::AbstractString...) = getindex(headers(img), inds...) # accesing header using strings -Base.getindex(img::AstroImageView, inds::AbstractString...) = getindex(headers(img), inds...) # accesing header using strings function Base.setindex!(img::AstroImage, v, ind::AbstractString) # modifying header using a string setindex!(headers(img), v, ind) # Mark the WCS object as beign out of date if this was a WCS header keyword - if ind ∈ WCS_HEADERS_2 - img.wcs_stale = true + if ind ∈ WCS_HEADERS + setfield(img, :wcs_stale, true) end end -function Base.setindex!(aview::AstroImageView, v, ind::AbstractString) # modifying header using a string - setindex!(aview.image, v, ind) -end Base.getindex(img::AstroImage, inds::Symbol...) = getindex(img, string.(inds)...) # accessing header using symbol Base.setindex!(img::AstroImage, v, ind::Symbol) = setindex!(img, v, string(ind)) Base.getindex(img::AstroImage, ind::AbstractString, ::Type{Comment}) = get_comment(headers(img), ind) # accesing header comment using strings @@ -217,7 +184,7 @@ function Base.getindex(img::AstroImage, ::Type{Comment}) return view(hdr.comments, ii) end # Adding new history entries -function Base.push!(img::AstroImageOrView, ::Type{History}, history::AbstractString) +function Base.push!(img::AstroImage, ::Type{History}, history::AbstractString) hdr = headers(img) push!(hdr.keys, "HISTORY") push!(hdr.values, nothing) @@ -233,7 +200,8 @@ headers of `imgnew` does not affect the headers of `img`. See also: [`shareheaders`](@ref). """ copyheaders(img::AstroImage, data::AbstractArray) = - AstroImage(data, deepcopy(headers(img)), img.wcs) + AstroImage(data, deepcopy(headers(img)), getfield(img, :wcs)) +export copyheaders """ shareheaders(img::AstroImage, data) -> imgnew @@ -242,51 +210,64 @@ using the data of the AbstractArray `data`. The two images have synchronized headers; modifying one also affects the other. See also: [`copyheaders`](@ref). """ -shareheaders(img::AstroImage, data::AbstractArray) = AstroImage(data, headers(img), img.wcs) +shareheaders(img::AstroImage, data::AbstractArray) = AstroImage(data, headers(img), getfield(img, :wcs)) +export shareheaders +# Share headers if an AstroImage, do nothing if AbstractArray maybe_shareheaders(img::AstroImage, data) = shareheaders(img, data) maybe_shareheaders(::AbstractArray, data) = data # Iteration # Defer to the array object in case it has special iteration defined -Base.iterate(img::AstroImageOrView) = Base.iterate(arraydata(img)) -Base.iterate(img::AstroImageOrView, s) = Base.iterate(arraydata(img), s) +Base.iterate(img::AstroImage) = Base.iterate(arraydata(img)) +Base.iterate(img::AstroImage, s) = Base.iterate(arraydata(img), s) + +# Delegate axes to the backing array +Base.axes(img::AstroImage) = Base.axes(arraydata(img)) # Restrict downsizes images by roughly a factor of two. # We want to keep the wrapper but downsize the underlying array -Images.restrict(img::AstroImageOrView, ::Tuple{}) = img +Images.restrict(img::AstroImage, ::Tuple{}) = img Images.restrict(img::AstroImage, region::Dims) = shareheaders(img, restrict(arraydata(img), region)) -Images.restrict(imgview::AstroImageView, region::Dims) = restrict(imgview.mapper, region) +Images.restrict(imgview::AstroImage, region::Dims) = restrict(imgview.mapper, region) # TODO: use WCS info # ImageCore.pixelspacing(img::ImageMeta) = pixelspacing(arraydata(img)) Base.promote_rule(::Type{AstroImage{T}}, ::Type{AstroImage{V}}) where {T,V} = AstroImage{promote_type{T,V}} -# function Base.similar(img::AstroImage) where T -# dat = similar(arraydata(img)) -# _,_,C,P = TNCP(img) -# T2 = eltype(dat) -# N = length(size(dat)) -# return AstroImage{T2,N,C,P}( -# dat, -# (zero(dat),one(dat)), -# true, -# # TODO: -# # similar(img.wcs), -# img.wcs, -# Properties{Float64}(), -# FITSHeader(String[],[],String[]), -# ) -# end -# Broadcasting +function Base.similar(img::AstroImage) where T + dat = similar(arraydata(img)) + return AstroImage( + dat, + deepcopy(headers(img)), + getfield(img, :wcs), + ) +end +# Getting a similar AstroImage with specific indices will typyically +# return an OffsetArray +function Base.similar(img::AstroImage, dims::Tuple) where T + dat = similar(arraydata(img), dims) + # Similar creates a new AstroImage with a similar array. + # We start with empty headers, except we copy any + # WCS headers from the original image. + # The idea being we get an array that represents the same patch + # of the sky in the same coordinate system. + return AstroImage( + dat, + deepcopy(headers(img)), + getfield(img, :wcs) + ) +end + + Base.copy(img::AstroImage) = AstroImage( copy(arraydata(img)), deepcopy(headers(img)), # We copy the headers but share the WCS object. # If the headers change such that wcs is now out of date, # a new wcs will be generated when needed. - img.wcs + getfield(img, :wcs) ) Base.convert(::Type{AstroImage}, A::AstroImage) = A Base.convert(::Type{AstroImage}, A::AbstractArray) = AstroImage(A) @@ -295,7 +276,10 @@ Base.convert(::Type{AstroImage{T}}, A::AstroImage) where {T} = shareheaders(A, c Base.convert(::Type{AstroImage{T}}, A::AbstractArray{T}) where {T} = AstroImage(A) Base.convert(::Type{AstroImage{T}}, A::AbstractArray) where {T} = AstroImage(convert(AbstractArray{T}, A)) +# TODO: offset arrays Base.view(img::AstroImage, inds...) = shareheaders(img, view(arraydata(img), inds...)) + +# Broadcasting # Base.selectdim(img::AstroImage, d::Integer, idxs) = AstroImage(selectdim(arraydata(img), d, idxs), headers(img)) # broadcast mechanics Base.BroadcastStyle(::Type{<:AstroImage}) = Broadcast.ArrayStyle{AstroImage}() @@ -310,8 +294,8 @@ function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{AstroImage} return AstroImage{T2,N,typeof(dat)}( dat, deepcopy(headers(img)), - img.wcs, - false + getfield(img, :wcs), + getfield(img, :wcs_stale) ) end "`A = find_img(As)` returns the first AstroImage among the arguments." @@ -353,6 +337,7 @@ AstroImage(img::AstroImage) = img Convenience function to create a FITSHeader with no keywords set. """ emptyheaders() = FITSHeader(String[],[],String[]) + """ emptywcs() @@ -361,6 +346,23 @@ dimensionality. """ emptywcs(data::AbstractArray) = WCSTransform(ndims(data)) + +""" + filterwcsheaders(hdrs::FITSHeader) + +Return a new FITSHeader containing WCS headers from `hdrs`. +This is useful for creating a new image with the same coordinates +as another. +""" +function filterwcsheaders(hdrs::FITSHeader) + include_keys = intersect(keys(hdrs), WCS_HEADERS) + return FITSHeader( + include_keys, + map(key -> hdrs[key], include_keys), + map(key -> get_comment(hdrs, key), include_keys), + ) +end + """ AstroImage(data::AbstractArray, [headers::FITSHeader,] [wcs::WCSTransform,]) @@ -395,8 +397,11 @@ AstroImage(data::AbstractArray, wcs::WCSTransform) = AstroImage(data, emptyheade Helper function to create a WCSTransform from an array and FITSHeaders. """ -function wcsfromheaders(data::AbstractArray, head::FITSHeader) - wcsout = WCS.from_header(string(head), ignore_rejected=true) +function wcsfromheaders(img::AstroImage) + # We only need to stringify WCS headers. This might just be 4-10 header keywords + # out of thousands. + # wcsout = WCS.from_header(string(filterwcsheaders(headers(img))), ignore_rejected=true) + wcsout = WCS.from_header(string(headers(img)), ignore_rejected=true) if length(wcsout) == 1 return only(wcsout) elseif length(wcsout) == 0 @@ -533,6 +538,29 @@ include("wcs_headers.jl") include("showmime.jl") include("plot-recipes.jl") include("ccd2rgb.jl") -include("reproject.jl") + +include("patches.jl") end # module + + +#= +TODO: +* properties? +* contrast/bias? +* interactive (Jupyter) +* Plots & Makie recipes +* indexing +* cubes +* RGB and other composites +* tests + +* histogram equaization + +* fileio + +* FITSIO PR/issue (performance) +* PlotUtils PR/issue (zscale with iteratble) +* WCS PR/issue (locking) + +=# \ No newline at end of file From 18df410a0f96a570c1b8b79fea36f86d4fff6ad3 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 3 Jan 2022 08:37:15 -0800 Subject: [PATCH 022/178] Add missing WCS-relevant headers --- src/wcs_headers.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/wcs_headers.jl b/src/wcs_headers.jl index a35d26bc..9bb3548f 100644 --- a/src/wcs_headers.jl +++ b/src/wcs_headers.jl @@ -1,4 +1,8 @@ const WCS_HEADERS_TEMPLATES = [ + "DATE", + "MJD", + + "WCSAXESa", "WCAXna", "WCSTna", @@ -204,7 +208,7 @@ const WCS_HEADERS_TEMPLATES = [ # Expand the headers containing lower case specifers into N copies Is = [""; string.(1:4)] # Find all lower case templates -const WCS_HEADERS_2 = Set(mapreduce(vcat, WCS_HEADERS_TEMPLATES) do template +const WCS_HEADERS = Set(mapreduce(vcat, WCS_HEADERS_TEMPLATES) do template if any(islowercase, template) template_chars = Vector{Char}(template) chars = template_chars[islowercase.(template_chars)] From 3ef9cd0cff4a4c5bfcae788cbba5004cb9460b4f Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 3 Jan 2022 08:38:11 -0800 Subject: [PATCH 023/178] Temporary patches to other packages for smoother usage --- src/{reproject.jl => patches.jl} | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) rename src/{reproject.jl => patches.jl} (68%) diff --git a/src/reproject.jl b/src/patches.jl similarity index 68% rename from src/reproject.jl rename to src/patches.jl index 87adf9b9..bd9f2f38 100644 --- a/src/reproject.jl +++ b/src/patches.jl @@ -1,3 +1,25 @@ + +#= +ImageContrastAdjustment +=# +function Images.ImageContrastAdjustment.adjust_histogram(::Type{T}, + img::AstroImage, + f::Images.ImageContrastAdjustment.AbstractHistogramAdjustmentAlgorithm, + args...; kwargs...) where T + out = similar(img, axes(img)) + adjust_histogram!(out, img, f, args...; kwargs...) + return out +end + + +#= +ImageTransformations +=# +# function warp(img::AstroImage, args...; kwargs...) +# out = warp(arraydatat(img), args...; kwargs...) +# return copyheaders(img, out) +# end + #= Additional methods to allow Reproject to work. =# From ade420689f2f4b938902e6c3cc563a10019a028c Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 3 Jan 2022 08:38:32 -0800 Subject: [PATCH 024/178] Verify cmap is valid before setting it as default --- src/showmime.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/showmime.jl b/src/showmime.jl index f729adc0..53750594 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -41,6 +41,9 @@ Alter the default color map used to display images when using `imview` or displaying an AstroImage. """ function set_cmap!(cmap) + if cmap ∉ keys(ColorSchemes.colorschemes) + throw(KeyError("$cmap not found in ColorSchemes.colorschemes")) + end _default_cmap[] = cmap end """ From 5920089439208c26957f5bbc9abc38bf7471579d Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 3 Jan 2022 12:45:46 -0800 Subject: [PATCH 025/178] WIP on plot recipes --- src/AstroImages.jl | 6 +- src/plot-recipes.jl | 293 ++++++++++++++++++++++++++++++++++++++++---- src/showmime.jl | 17 ++- 3 files changed, 285 insertions(+), 31 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 7ec79c65..c05985bd 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -122,8 +122,8 @@ Images.arraydata(img::AstroImage) = getfield(img, :data) headers(img::AstroImage) = getfield(img, :headers) function wcs(img::AstroImage) if getfield(img, :wcs_stale) - setfield(img, :wcs, wcsfromheaders(img)) - setfield(img, :wcs_stale, false) + setfield!(img, :wcs, wcsfromheaders(img)) + setfield!(img, :wcs_stale, false) end return getfield(img, :wcs) end @@ -162,7 +162,7 @@ function Base.setindex!(img::AstroImage, v, ind::AbstractString) # modifying he setindex!(headers(img), v, ind) # Mark the WCS object as beign out of date if this was a WCS header keyword if ind ∈ WCS_HEADERS - setfield(img, :wcs_stale, true) + setfield!(img, :wcs_stale, true) end end Base.getindex(img::AstroImage, inds::Symbol...) = getindex(img, string.(inds)...) # accessing header using symbol diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 87b08b97..366aff6b 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -1,44 +1,285 @@ using RecipesBase -@recipe function f(img::AstroImage) - seriestype := :heatmap - aspect_ratio := :equal - # Right now we only support single frame images, - # gray scale is a good choice. - color := :grays - arraydata(img) -end +using AstroAngles + +""" + plot(img::AstroImage; clims=extrema, stretch=identity, cmap=nothing) + +Create a read only view of an array or AstroImage mapping its data values +to Colors according to `clims`, `stretch`, and `cmap`. + +The data is first clamped to `clims`, which can either be a tuple of (min, max) +values or a function accepting an iterator of pixel values that returns (min, max). +By default, `clims=extrema` i.e. the minimum and maximum of `img`. +Convenient functions to use for `clims` are: +`extrema`, `zscale`, and `percent(p)` + +Next, the data is rescaled to [0,1] and remapped according to the function `stretch`. +Stretch can be any monotonic fuction mapping values in the range [0,1] to some range [a,b]. +Note that `log(0)` is not defined so is not directly supported. +For a list of convenient stretch functions, see: +`logstretch`, `powstretch`, `squarestretch`, `asinhstretch`, `sinhstretch`, `powerdiststretch` + +Finally the data is mapped to RGB values according to `cmap`. If cmap is `nothing`, +grayscale is used. ColorSchemes.jl defines hundreds of colormaps. A few nice ones for +images include: `:viridis`, `:magma`, `:plasma`, `:thermal`, and `:turbo`. -@recipe function f(img::AstroImage, wcs::WCSTransform) +Crucially, this function returns a view over the underlying data. If `img` is updated +then those changes will be reflected by this view with the exception of `clims` which +is not recalculated. + +Note: if clims or stretch is a function, the pixel values passed in are first filtered +to remove non-finite or missing values. + +### Defaults +The default values of `clims`, `stretch`, and `cmap` are `extrema`, `identity`, and `nothing` +respectively. +You may alter these defaults using `AstroImages.set_clims!`, `AstroImages.set_stretch!`, and +`AstroImages.set_cmap!`. + +### Automatic Display +Arrays wrapped by `AstroImage()` get displayed as images automatically by calling +`imview` on them with the default settings when using displays that support showing PNG images. + +### Missing data +Pixels that are `NaN` or `missing` will be displayed as transparent when `cmap` is set +or black if. ++/- Inf will be displayed as black or white respectively. + +### Exporting Images +The view returned by `imview` can be saved using general `FileIO.save` methods. +Example: +```julia +v = imview(data, cmap=:magma, stretch=asinhstretch, clims=percent(95)) +save("output.png", v) +``` +""" +@recipe function f( + img::AstroImage{T}; + clims=_default_clims[], + stretch=_default_stretch[], + cmap=_default_cmap[], + wcs=true + ) where T seriestype := :heatmap aspect_ratio := :equal - color := :grays - xformatter := x -> pix2world_xformatter(x, wcs) - yformatter := y -> pix2world_yformatter(y, wcs) - xlabel := labler_x(wcs) - ylabel := labler_y(wcs) - arraydata(img) + + if isnothing(cmap) + cmap = :grays + end + color := cmap + + + # TODO: apply same `restrict` logic as in Images.jl to downsize + # very large images. + + # We use the same pipeline as imview: normalize the image data according to clims + # then stretch, then plot in the new stretched range + + # Users can pass clims as an array or tuple containing the minimum and maximum values + if typeof(clims) <: AbstractArray || typeof(clims) <: Tuple + if length(clims) != 2 + error("clims must have exactly two values if provided.") + end + imgmin = first(clims) + imgmax = last(clims) + # Or as a callable that computes them given an iterator + else + imgmin, imgmax = clims(skipmissingnan(img)) + end + + img_flipped = img[end:-1:begin,:] + + normed = clampednormedview(img_flipped, (imgmin, imgmax)) + + if T <: Union{Missing,<:Number} + TT = typeof(first(skipmissing(normed))) + else + TT = T + end + if TT == Bool + TT = N0f8 + end + + stretchmin = stretch(zero(TT)) + stretchmax = stretch(one(TT)) + mapper = mappedarray(img_flipped, normed) do pixr, pixn + if ismissing(pixr) || !isfinite(pixr) || ismissing(pixn) || !isfinite(pixn) + # We check pixr in addition to pixn because we want to preserve if the pixels + # are +-Inf + stretched = pixr + else + stretched = stretch(pixn) + end + end + + # The output range may not be [0,1] depending on the stretch function + clims := (stretchmin,stretchmax) + + # Calculate tick labels for the colorbar. + # These may not have linear spacing depending on stretch function. + # We can't know the inverse of the user's stretch function in general, so we have to + # map in the forwards direction. + # cbticklabels = range( + # imgmin, + # imgmax, + # length=9 + # ) + # cbtickpos = stretch.(cbticklabels) + + # By default, disable the colorbar. + # Plots.jl does no give us sufficient control to make sure the range and ticks + # are correct after applying a non-linear stretch + colorbar := false + + # we have a wcs flag (true by default) so that users can skip over + # plotting in physical coordinates. This is especially important + # if the WCS headers are mallformed in some way. + if wcs + + # We want to avoid having the same coordinate repeated many times + # in narrow fields of view (e.g. 150.1, 150.1, 150.1). + # We attempt to decect this and switch to a coordinate + Δ format + w = AstroImages.wcs(img) + + # TODO: Is this really the x min and max? What if the image is rotated? + x1, y1 = pix_to_world(w, [float(minimum(axes(img,1))), float(minimum(axes(img,2)))]) + x2, y2 = pix_to_world(w, [float(maximum(axes(img,1))), float(maximum(axes(img,2)))]) + # Image indices will often be reversed vs. the physical coordinates + xmin, xmax = minmax(x1,x2) + ymin, ymax = minmax(y1,y2) + + # X + if xmax - xmin < 1 + start_x = xmin + start_x_d, start_x_m, start_x_s = deg2dms(xmin) + diff_x_d, diff_x_m, diff_x_s = deg2dms(xmax) .- deg2dms(xmin) + xunit = w.cunit[1] + tickdiv, tickunit = 1.0, xunit + # Determine which coordinates to use along the axis. + @show start_x + start_x = floor(start_x_d) + @show start_x + if diff_x_d <= 1 + start_x = dms2deg(start_x_d, start_x_m, 0) + @show start_x + tickdiv, tickunit = nextunit(tickdiv, tickunit) + end + if diff_x_m <= 1 + start_x = dms2deg(start_x_d, start_x_m, ceil(diff_x_m)) + @show start_x + tickdiv, tickunit = nextunit(tickdiv, tickunit) + end + + # TODO: adaptive unit switching + xlabel := labler_x(w, (start_x_d, start_x_m, start_x_s)) + + # tickdiv, tickunit = nextunit(w.cunit[1]) + xformatter := x -> pix2world_xformatter(x, w, start_x) + else + xformatter := x -> pix2world_xformatter(x, w) + xlabel := labler_x(w) + end + + # # Y + # if ymax - ymin < 1 + # # Switch to Δ labeling + + # # TODO: adaptive unit switching + # starty = round(ymin, RoundToZero, digits=0) + # ylabel := labler_y(w, starty) + + # tickdiv, tickunit = nextunit(w.cunit[2]) + # yformatter := y -> pix2world_yformatter(y, w, starty, tickdiv, tickunit) + # else + yformatter := y -> pix2world_yformatter(y, w) + ylabel := labler_y(w) + # end + + # TODO: also disable equal aspect ratio if the scales are totally different + end + + return mapper end function pix2world_xformatter(x, wcs::WCSTransform) - res = round(pix_to_world(wcs, [float(x), float(x)])[1], digits = 2) - if wcs.cunit[1] == "deg" # TODO: add symbols for more units - return string(res)*"°" + res = round(pix_to_world(wcs, [float(x), float(x)])[1][1], digits=2) + return string(res, unit2sym(wcs.cunit[1])) +end +function pix2world_xformatter(x, wcs::WCSTransform, start) + x = pix_to_world(wcs, [float(x), float(x)])[1][1] + + pm = sign(start - x) >= 0 ? '+' : '-' + + + @show start x + diff_x_d, diff_x_m, diff_x_s = deg2dms(x) .- deg2dms(start) + @show diff_x_d diff_x_m diff_x_s + + if abs(diff_x_d) > 1 + return string(pm, abs(round(Int, diff_x_d)), unit2sym("deg")) + elseif abs(diff_x_m) > 1 + @show diff_x_m + return string(pm, abs(round(Int, diff_x_m)), unit2sym("am")) + elseif abs(diff_x_s) > 1 + return string(pm, abs(round(Int, diff_x_s)), unit2sym("as")) + elseif abs(diff_x_s) > 1e-3 + return string(pm, abs(round(Int, diff_x_s*1e3)), unit2sym("mas")) + elseif abs(diff_x_s) > 1e-6 + return string(pm, abs(round(Int, diff_x_s*1e3)), unit2sym("μas")) else - return res[1] + return string(pm, abs(diff_x_s), unit2sym("as")) end + + + + # return string(pm, diff_x_d, diff_x_m, diff_x_s, unit2sym(tickunit)) + return string(pm, abs(round(Int, diff_x_s)), unit2sym(tickunit)) end -function pix2world_yformatter(x, wcs::WCSTransform) - res = round(pix_to_world(wcs, [float(x), float(x)])[2], digits = 2) - if wcs.cunit[2] == "deg" # TODO: add symbols for more units - return string(res)*"°" +function pix2world_yformatter(y, wcs::WCSTransform) + res = round(pix_to_world(wcs, [float(y), float(y)])[2][1], digits=2) + return string(res, unit2sym(wcs.cunit[1])) +end +function pix2world_yformatter(y, wcs::WCSTransform, start, tickdiv, tickunit) + y = pix_to_world(wcs, [float(y), float(y)])[2][1] + pm = sign(res) >= 0 ? '+' : '-' + return string(pm, abs(round(res,digits=2)), unit2sym(tickunit)) +end + + +labler_x(wcs) = ctype_x(wcs) +labler_y(wcs) = ctype_y(wcs) + +labler_x(wcs, start) = string(ctype_x(wcs), " ", start, unit2sym(wcs.cunit[1])) +labler_y(wcs, start) = string(ctype_y(wcs), " ", start, unit2sym(wcs.cunit[2])) + +function unit2sym(unit) + if unit == "deg" # TODO: add symbols for more units + "°" + elseif unit == "am" # TODO: add symbols for more units + "'" + elseif unit == "as" # TODO: add symbols for more units + "\"" else - return res[1] + string(unit) end end -function labler_x(wcs::WCSTransform) +function nextunit(div, unit) + if unit == "deg" # TODO: add symbols for more units + return div*60, "am" + elseif unit == "am" # TODO: add symbols for more units + return div*60, "as" + elseif unit == "as" # TODO: add symbols for more units + return div*60, "mas" + else + div, string(unit) + end +end + + +function ctype_x(wcs::WCSTransform) if length(wcs.ctype[1]) == 0 return wcs.radesys elseif wcs.ctype[1][1:2] == "RA" @@ -52,7 +293,7 @@ function labler_x(wcs::WCSTransform) end end -function labler_y(wcs::WCSTransform) +function ctype_y(wcs::WCSTransform) if length(wcs.ctype[2]) == 0 return wcs.radesys elseif wcs.ctype[2][1:3] == "DEC" diff --git a/src/showmime.jl b/src/showmime.jl index 53750594..e356affd 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -20,6 +20,13 @@ # show(io, mime, brightness_contrast(img), kwargs...) # This is used in VSCode and others + +# If the user displays a AstroImage of colors (e.g. one created with imview) +# fal through and display the data as an image +Base.show(io::IO, mime::MIME"image/png", img::AstroImage{T,2}; kwargs...) where {T<:Colorant} = + show(io, mime, arraydata(img), kwargs...) + +# Otherwise, call imview with the default settings. Base.show(io::IO, mime::MIME"image/png", img::AstroImage{T,2}; kwargs...) where {T} = show(io, mime, imview(img), kwargs...) @@ -152,6 +159,11 @@ function imview( if !nonempty return end + + # TODO: Images.jl has logic to downsize huge images before displaying them. + # We should use that here before applying all this processing instead of + # letting Images.jl handle it after. + # Users can pass clims as an array or tuple containing the minimum and maximum values if typeof(clims) <: AbstractArray || typeof(clims) <: Tuple if length(clims) != 2 @@ -167,6 +179,7 @@ function imview( return _imview(img, normed, stretch, cmap) end function _imview(img, normed::AbstractArray{T}, stretch, cmap) where T + if T <: Union{Missing,<:Number} TT = typeof(first(skipmissing(normed))) else @@ -217,8 +230,8 @@ function _imview(img, normed::AbstractArray{T}, stretch, cmap) where T end end - # return AstroImageView(img, mapper) - return mapper + return shareheaders(img, mapper) + # return mapper end export imview From b02caccf52591c72b8d807e3e65b5ef12431d8d5 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 3 Jan 2022 12:50:23 -0800 Subject: [PATCH 026/178] Fix getindex for AstroImage of non numerical data --- src/AstroImages.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index c05985bd..66bb37a7 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -148,7 +148,7 @@ function Base.getindex(img::AstroImage, inds...) dat = getindex(arraydata(img), inds...) # ndims is defined for Numbers but not Missing. # This check is therefore necessary for img[1,1]->missing to work. - if ismissing(dat) || ndims(dat) <= 1 + if !(typeof(dat) <: Number) || ndims(dat) <= 1 return dat else return copyheaders(img, dat) From b4f0bc8d83aa58c5da1ea2019a6d9f51439248ce Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 3 Jan 2022 12:57:27 -0800 Subject: [PATCH 027/178] RGB images should also stay as AstroImages and share headers --- src/ccd2rgb.jl | 68 +++++++++++++++++++++++++++++++++++++++++++++---- src/showmime.jl | 3 +-- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/ccd2rgb.jl b/src/ccd2rgb.jl index 8ecf15a7..973a79ce 100644 --- a/src/ccd2rgb.jl +++ b/src/ccd2rgb.jl @@ -23,11 +23,16 @@ julia> ccd2rgb(r, b, g, shape_out = (1000,1000), stretch = sqrt) julia> ccd2rgb(r, b, g, shape_out = (1000,1000), stretch = asinh) ``` """ -function ccd2rgb(red::Tuple{AbstractMatrix, WCSTransform}, green::Tuple{AbstractMatrix, WCSTransform}, - blue::Tuple{AbstractMatrix, WCSTransform}; stretch = identity, shape_out = size(red[1])) - red_rp = reproject(red, red[2], shape_out = shape_out)[1] - green_rp = reproject(green, red[2], shape_out = shape_out)[1] - blue_rp = reproject(blue, red[2], shape_out = shape_out)[1] +function ccd2rgb( + red::AstroImage, + green::AstroImage, + blue::AstroImage; + stretch = identity, + shape_out = size(red[1]) +) + red_rp = reproject(red, red, shape_out = shape_out)[1] + green_rp = reproject(green, red, shape_out = shape_out)[1] + blue_rp = reproject(blue, red, shape_out = shape_out)[1] I = (red_rp .+ green_rp .+ blue_rp) ./ 3 I .= (x -> stretch(x)/x).(I) @@ -46,3 +51,56 @@ ccd2rgb(red::ImageHDU, green::ImageHDU, blue::ImageHDU; stretch = identity, shap ccd2rgb((read(red), WCS.from_header(read_header(red, String))[1]), (read(green), WCS.from_header(read_header(green, String))[1]), (read(blue), WCS.from_header(read_header(blue, String))[1]), stretch = stretch, shape_out = shape_out) + + +function composechannels( + images, + multipliers=ones(size(images)), # 0.299 * R + 0.587 * G + 0.114 * B + channels=["#F00", "#0F0", "#00F"]; + clims=extrema, + stretch=identity, + # reproject = all(==(wcs(first(images))), wcs(img) for img in images) ? false : wcs(first(images)), + reproject = false, + shape_out = size(first(images)), +) + if reproject == false + reprojected = images + else + reprojected = map(images) do image + Reproject.reproject(image, reproject; shape_out)[1] + end + end + I = broadcast(+, reprojected...) ./ length(reprojected) + I .= (x -> stretch(x)/x).(I) + + colors = parse.(Colorant, channels) + # @show colors + + # red_rp .*= I + # green_rp .*= I + # blue_rp .*= I + + # m1 = maximum(x->isnan(x) ? -Inf : x, red_rp) + # m2 = maximum(x->isnan(x) ? -Inf : x, green_rp) + # m3 = maximum(x->isnan(x) ? -Inf : x, blue_rp) + # return colorview(RGB, red_rp./m1 , green_rp./m2, blue_rp./m3) + + # return colorview(RGB, (reprojected .* multipliers)...) + + colorized = map(eachindex(reprojected)) do i + arraydata(reprojected[i]) .* multipliers[i] .* colors[i] + end + mapped = (+).(colorized...) ./ length(reprojected) + + return maybe_shareheaders(first(images), mapped) + + # return (reprojected .* multipliers .* colors) + + # cube = cat(reprojected .* multipliers..., dims=3) + # out = Matrix{eltype(first(reprojected))}(shape_out) + # map(CartesianIndices(first(reprojected))) do I + # i,j = Tuple(I) + # out[i,j] = cube[i,j,:] + # end +end +export composechannels \ No newline at end of file diff --git a/src/showmime.jl b/src/showmime.jl index e356affd..bf271a91 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -230,9 +230,8 @@ function _imview(img, normed::AbstractArray{T}, stretch, cmap) where T end end - return shareheaders(img, mapper) + return maybe_shareheaders(img, mapper) # return mapper - end export imview From 5d1453c566fd303490105b49f914dc9b4e656d6c Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 4 Jan 2022 13:04:43 -0800 Subject: [PATCH 028/178] Forward wcs_stale in calls to similar --- src/AstroImages.jl | 7 +- src/plot-recipes.jl | 235 +++++++++++++++++++++++++++++++------------- 2 files changed, 169 insertions(+), 73 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 66bb37a7..90d9775c 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -113,6 +113,7 @@ mutable struct AstroImage{T, N, TDat} <: AbstractArray{T,N} wcs::WCSTransform wcs_stale::Bool end +AstroImage(data::AbstractArray{T,N}, headers, wcs, wcs_stale) where {T,N} = AstroImage{T,N,typeof(data)}(data,headers,wcs,wcs_stale) """ @@ -210,7 +211,7 @@ using the data of the AbstractArray `data`. The two images have synchronized headers; modifying one also affects the other. See also: [`copyheaders`](@ref). """ -shareheaders(img::AstroImage, data::AbstractArray) = AstroImage(data, headers(img), getfield(img, :wcs)) +shareheaders(img::AstroImage, data::AbstractArray) = AstroImage(data, headers(img), getfield(img, :wcs), getfield(img, :wcs_stale)) export shareheaders # Share headers if an AstroImage, do nothing if AbstractArray maybe_shareheaders(img::AstroImage, data) = shareheaders(img, data) @@ -242,6 +243,7 @@ function Base.similar(img::AstroImage) where T dat, deepcopy(headers(img)), getfield(img, :wcs), + getfield(img, :wcs_stale), ) end # Getting a similar AstroImage with specific indices will typyically @@ -256,7 +258,8 @@ function Base.similar(img::AstroImage, dims::Tuple) where T return AstroImage( dat, deepcopy(headers(img)), - getfield(img, :wcs) + getfield(img, :wcs), + getfield(img, :wcs_stale) ) end diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 366aff6b..5bff3ceb 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -54,87 +54,179 @@ v = imview(data, cmap=:magma, stretch=asinhstretch, clims=percent(95)) save("output.png", v) ``` """ -@recipe function f( - img::AstroImage{T}; - clims=_default_clims[], - stretch=_default_stretch[], - cmap=_default_cmap[], - wcs=true - ) where T - seriestype := :heatmap - aspect_ratio := :equal - - if isnothing(cmap) - cmap = :grays - end - color := cmap - - - # TODO: apply same `restrict` logic as in Images.jl to downsize - # very large images. - - # We use the same pipeline as imview: normalize the image data according to clims - # then stretch, then plot in the new stretched range - - # Users can pass clims as an array or tuple containing the minimum and maximum values - if typeof(clims) <: AbstractArray || typeof(clims) <: Tuple - if length(clims) != 2 - error("clims must have exactly two values if provided.") - end - imgmin = first(clims) - imgmax = last(clims) - # Or as a callable that computes them given an iterator - else - imgmin, imgmax = clims(skipmissingnan(img)) - end - - img_flipped = img[end:-1:begin,:] - - normed = clampednormedview(img_flipped, (imgmin, imgmax)) - - if T <: Union{Missing,<:Number} - TT = typeof(first(skipmissing(normed))) - else - TT = T - end - if TT == Bool - TT = N0f8 - end +# @recipe function f( +# img::AstroImage{T}; +# clims=_default_clims[], +# stretch=_default_stretch[], +# cmap=_default_cmap[], +# wcs=true +# ) where {T<:Number} +# seriestype := :heatmap +# aspect_ratio := :equal + +# if isnothing(cmap) +# cmap = :grays +# end +# color := cmap + + +# # TODO: apply same `restrict` logic as in Images.jl to downsize +# # very large images. + +# # We use the same pipeline as imview: normalize the image data according to clims +# # then stretch, then plot in the new stretched range + +# # Users can pass clims as an array or tuple containing the minimum and maximum values +# if typeof(clims) <: AbstractArray || typeof(clims) <: Tuple +# if length(clims) != 2 +# error("clims must have exactly two values if provided.") +# end +# imgmin = first(clims) +# imgmax = last(clims) +# # Or as a callable that computes them given an iterator +# else +# imgmin, imgmax = clims(skipmissingnan(img)) +# end + +# img_flipped = img[end:-1:begin,:] + +# normed = clampednormedview(img_flipped, (imgmin, imgmax)) + +# if T <: Union{Missing,<:Number} +# TT = typeof(first(skipmissing(normed))) +# else +# TT = T +# end +# if TT == Bool +# TT = N0f8 +# end + +# stretchmin = stretch(zero(TT)) +# stretchmax = stretch(one(TT)) +# mapper = mappedarray(img_flipped, normed) do pixr, pixn +# if ismissing(pixr) || !isfinite(pixr) || ismissing(pixn) || !isfinite(pixn) +# # We check pixr in addition to pixn because we want to preserve if the pixels +# # are +-Inf +# stretched = pixr +# else +# stretched = stretch(pixn) +# end +# end + +# # The output range may not be [0,1] depending on the stretch function +# clims := (stretchmin,stretchmax) + +# # Calculate tick labels for the colorbar. +# # These may not have linear spacing depending on stretch function. +# # We can't know the inverse of the user's stretch function in general, so we have to +# # map in the forwards direction. +# # cbticklabels = range( +# # imgmin, +# # imgmax, +# # length=9 +# # ) +# # cbtickpos = stretch.(cbticklabels) + +# # By default, disable the colorbar. +# # Plots.jl does no give us sufficient control to make sure the range and ticks +# # are correct after applying a non-linear stretch +# colorbar := false + +# # we have a wcs flag (true by default) so that users can skip over +# # plotting in physical coordinates. This is especially important +# # if the WCS headers are mallformed in some way. +# if wcs + +# # We want to avoid having the same coordinate repeated many times +# # in narrow fields of view (e.g. 150.1, 150.1, 150.1). +# # We attempt to decect this and switch to a coordinate + Δ format +# w = AstroImages.wcs(img) + +# # TODO: Is this really the x min and max? What if the image is rotated? +# x1, y1 = pix_to_world(w, [float(minimum(axes(img,1))), float(minimum(axes(img,2)))]) +# x2, y2 = pix_to_world(w, [float(maximum(axes(img,1))), float(maximum(axes(img,2)))]) +# # Image indices will often be reversed vs. the physical coordinates +# xmin, xmax = minmax(x1,x2) +# ymin, ymax = minmax(y1,y2) + +# # X +# if xmax - xmin < 1 +# start_x = xmin +# start_x_d, start_x_m, start_x_s = deg2dms(xmin) +# diff_x_d, diff_x_m, diff_x_s = deg2dms(xmax) .- deg2dms(xmin) +# xunit = w.cunit[1] +# tickdiv, tickunit = 1.0, xunit +# # Determine which coordinates to use along the axis. +# @show start_x +# start_x = floor(start_x_d) +# @show start_x +# if diff_x_d <= 1 +# start_x = dms2deg(start_x_d, start_x_m, 0) +# @show start_x +# tickdiv, tickunit = nextunit(tickdiv, tickunit) +# end +# if diff_x_m <= 1 +# start_x = dms2deg(start_x_d, start_x_m, ceil(diff_x_m)) +# @show start_x +# tickdiv, tickunit = nextunit(tickdiv, tickunit) +# end + +# # TODO: adaptive unit switching +# xlabel := labler_x(w, (start_x_d, start_x_m, start_x_s)) + +# # tickdiv, tickunit = nextunit(w.cunit[1]) +# xformatter := x -> pix2world_xformatter(x, w, start_x) +# else +# xformatter := x -> pix2world_xformatter(x, w) +# xlabel := labler_x(w) +# end + +# # # Y +# # if ymax - ymin < 1 +# # # Switch to Δ labeling + +# # # TODO: adaptive unit switching +# # starty = round(ymin, RoundToZero, digits=0) +# # ylabel := labler_y(w, starty) + +# # tickdiv, tickunit = nextunit(w.cunit[2]) +# # yformatter := y -> pix2world_yformatter(y, w, starty, tickdiv, tickunit) +# # else +# yformatter := y -> pix2world_yformatter(y, w) +# ylabel := labler_y(w) +# # end + +# # TODO: also disable equal aspect ratio if the scales are totally different +# end - stretchmin = stretch(zero(TT)) - stretchmax = stretch(one(TT)) - mapper = mappedarray(img_flipped, normed) do pixr, pixn - if ismissing(pixr) || !isfinite(pixr) || ismissing(pixn) || !isfinite(pixn) - # We check pixr in addition to pixn because we want to preserve if the pixels - # are +-Inf - stretched = pixr - else - stretched = stretch(pixn) - end - end +# return mapper +# end - # The output range may not be [0,1] depending on the stretch function - clims := (stretchmin,stretchmax) +@recipe function f( + img::AstroImage{T}; + clims=_default_clims[], + stretch=_default_stretch[], + cmap=_default_cmap[], + wcs=true +) where {T<:Number} + iv = imview(img; clims, stretch, cmap) + return iv +end - # Calculate tick labels for the colorbar. - # These may not have linear spacing depending on stretch function. - # We can't know the inverse of the user's stretch function in general, so we have to - # map in the forwards direction. - # cbticklabels = range( - # imgmin, - # imgmax, - # length=9 - # ) - # cbtickpos = stretch.(cbticklabels) +@recipe function f( + img::AstroImage{T}; + wcs=true +) where {T<:Colorant} # By default, disable the colorbar. # Plots.jl does no give us sufficient control to make sure the range and ticks # are correct after applying a non-linear stretch - colorbar := false + # colorbar := false # we have a wcs flag (true by default) so that users can skip over # plotting in physical coordinates. This is especially important # if the WCS headers are mallformed in some way. + @show wcs if wcs # We want to avoid having the same coordinate repeated many times @@ -142,6 +234,7 @@ save("output.png", v) # We attempt to decect this and switch to a coordinate + Δ format w = AstroImages.wcs(img) + # TODO: Is this really the x min and max? What if the image is rotated? x1, y1 = pix_to_world(w, [float(minimum(axes(img,1))), float(minimum(axes(img,2)))]) x2, y2 = pix_to_world(w, [float(maximum(axes(img,1))), float(maximum(axes(img,2)))]) @@ -199,7 +292,7 @@ save("output.png", v) # TODO: also disable equal aspect ratio if the scales are totally different end - return mapper + return LinRange(xmin,xmax,size(img,1)), LinRange(xmin,xmax,size(img,2)), arraydata(img) end function pix2world_xformatter(x, wcs::WCSTransform) From 4161bdddb575794d3d76922e3e223704555d0ca7 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 4 Jan 2022 13:10:48 -0800 Subject: [PATCH 029/178] WIP plot recipes --- Project.toml | 2 + src/AstroImages.jl | 38 ++- src/ccd2rgb.jl | 21 +- src/plot-recipes.jl | 563 ++++++++++++++++++++++---------------------- src/showmime.jl | 56 ++--- 5 files changed, 355 insertions(+), 325 deletions(-) diff --git a/Project.toml b/Project.toml index f3e43d66..415f47f7 100644 --- a/Project.toml +++ b/Project.toml @@ -4,6 +4,7 @@ authors = ["Mosè Giordano", "Rohit Kumar"] version = "0.2.0" [deps] +AstroAngles = "5c4adb95-c1fc-4c53-b4ea-2a94080c53d2" ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" FITSIO = "525bcba6-941b-5504-bd06-fd0dc1a4d2eb" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" @@ -14,6 +15,7 @@ Interact = "c601a237-2ae4-5e1e-952c-7a85b0c7eef1" MappedArrays = "dbb5928d-eab1-5f90-85c2-b9b0edb7c900" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" Reproject = "d1dcc2e6-806e-11e9-2897-3f99785db2ae" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 90d9775c..a904911b 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -139,8 +139,22 @@ export History # extending the AbstractArray interface -Base.size(img::AstroImage) = size(arraydata(img)) -Base.length(img::AstroImage) = length(arraydata(img)) +# and delegating calls to the wrapped array + +# Simple delegation +for f in [ + :(Base.size), + :(Base.length), +] + @eval ($f)(img::AstroImage) = $f(arraydata(img)) +end +# Return result wrapped in array +for f in [ + :(Base.adjoint), + :(Base.transpose) +] + @eval ($f)(img::AstroImage) = shareheaders(img, $f(arraydata(img))) +end # Getting and setting data is forwarded to the underlying array # Accessing a single value or a vector returns just the data. @@ -201,7 +215,7 @@ headers of `imgnew` does not affect the headers of `img`. See also: [`shareheaders`](@ref). """ copyheaders(img::AstroImage, data::AbstractArray) = - AstroImage(data, deepcopy(headers(img)), getfield(img, :wcs)) + AstroImage(data, deepcopy(headers(img)), getfield(img, :wcs), getfield(img, :wcs_stale)) export copyheaders """ @@ -216,6 +230,8 @@ export shareheaders # Share headers if an AstroImage, do nothing if AbstractArray maybe_shareheaders(img::AstroImage, data) = shareheaders(img, data) maybe_shareheaders(::AbstractArray, data) = data +maybe_copyheaders(img::AstroImage, data) = copyheaders(img, data) +maybe_copyheaders(::AbstractArray, data) = data # Iteration # Defer to the array object in case it has special iteration defined @@ -229,7 +245,6 @@ Base.axes(img::AstroImage) = Base.axes(arraydata(img)) # We want to keep the wrapper but downsize the underlying array Images.restrict(img::AstroImage, ::Tuple{}) = img Images.restrict(img::AstroImage, region::Dims) = shareheaders(img, restrict(arraydata(img), region)) -Images.restrict(imgview::AstroImage, region::Dims) = restrict(imgview.mapper, region) # TODO: use WCS info # ImageCore.pixelspacing(img::ImageMeta) = pixelspacing(arraydata(img)) @@ -270,7 +285,8 @@ Base.copy(img::AstroImage) = AstroImage( # We copy the headers but share the WCS object. # If the headers change such that wcs is now out of date, # a new wcs will be generated when needed. - getfield(img, :wcs) + getfield(img, :wcs), + getfield(img, :wcs_stale) ) Base.convert(::Type{AstroImage}, A::AstroImage) = A Base.convert(::Type{AstroImage}, A::AbstractArray) = AstroImage(A) @@ -395,20 +411,24 @@ AstroImage(data::AbstractArray, wcs::WCSTransform) = AstroImage(data, emptyheade """ - wcsfromheaders(data::AbstractArray, headers::FITSHeader) + wcsfromheaders(img::AstroImage; relax=WCS.HDR_ALL, ignore_rejected=true) Helper function to create a WCSTransform from an array and FITSHeaders. """ -function wcsfromheaders(img::AstroImage) +function wcsfromheaders(img::AstroImage; relax=WCS.HDR_ALL, ignore_rejected=true) # We only need to stringify WCS headers. This might just be 4-10 header keywords # out of thousands. # wcsout = WCS.from_header(string(filterwcsheaders(headers(img))), ignore_rejected=true) - wcsout = WCS.from_header(string(headers(img)), ignore_rejected=true) + wcsout = WCS.from_header( + string(headers(img)); + ignore_rejected, + relax + ) if length(wcsout) == 1 return only(wcsout) elseif length(wcsout) == 0 - return emptywcs(data) + return emptywcs(arraydata(img)) else error("Mutiple WCSTransform returned from headers") end diff --git a/src/ccd2rgb.jl b/src/ccd2rgb.jl index 973a79ce..6a758d8a 100644 --- a/src/ccd2rgb.jl +++ b/src/ccd2rgb.jl @@ -66,6 +66,9 @@ function composechannels( if reproject == false reprojected = images else + if reproject == true + reproject = first(images) + end reprojected = map(images) do image Reproject.reproject(image, reproject; shape_out)[1] end @@ -87,20 +90,22 @@ function composechannels( # return colorview(RGB, (reprojected .* multipliers)...) + ## TODO: this all needs to be lazy + colorized = map(eachindex(reprojected)) do i arraydata(reprojected[i]) .* multipliers[i] .* colors[i] end mapped = (+).(colorized...) ./ length(reprojected) + T = coloralpha(eltype(mapped)) + mapped = T.(mapped) + mapped[isnan.(mapped)] .= RGBA(0,0,0,0) - return maybe_shareheaders(first(images), mapped) - + return maybe_copyheaders(first(images), mapped') # return (reprojected .* multipliers .* colors) - # cube = cat(reprojected .* multipliers..., dims=3) - # out = Matrix{eltype(first(reprojected))}(shape_out) - # map(CartesianIndices(first(reprojected))) do I - # i,j = Tuple(I) - # out[i,j] = cube[i,j,:] - # end + + # TODO: more flexible blending + # ColorBlnding + # missing/NaN handling end export composechannels \ No newline at end of file diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 5bff3ceb..fb1d8dd7 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -1,6 +1,7 @@ using RecipesBase - using AstroAngles +using Printf +using PlotUtils: optimize_ticks """ plot(img::AstroImage; clims=extrema, stretch=identity, cmap=nothing) @@ -54,154 +55,8 @@ v = imview(data, cmap=:magma, stretch=asinhstretch, clims=percent(95)) save("output.png", v) ``` """ -# @recipe function f( -# img::AstroImage{T}; -# clims=_default_clims[], -# stretch=_default_stretch[], -# cmap=_default_cmap[], -# wcs=true -# ) where {T<:Number} -# seriestype := :heatmap -# aspect_ratio := :equal - -# if isnothing(cmap) -# cmap = :grays -# end -# color := cmap - - -# # TODO: apply same `restrict` logic as in Images.jl to downsize -# # very large images. - -# # We use the same pipeline as imview: normalize the image data according to clims -# # then stretch, then plot in the new stretched range - -# # Users can pass clims as an array or tuple containing the minimum and maximum values -# if typeof(clims) <: AbstractArray || typeof(clims) <: Tuple -# if length(clims) != 2 -# error("clims must have exactly two values if provided.") -# end -# imgmin = first(clims) -# imgmax = last(clims) -# # Or as a callable that computes them given an iterator -# else -# imgmin, imgmax = clims(skipmissingnan(img)) -# end - -# img_flipped = img[end:-1:begin,:] - -# normed = clampednormedview(img_flipped, (imgmin, imgmax)) - -# if T <: Union{Missing,<:Number} -# TT = typeof(first(skipmissing(normed))) -# else -# TT = T -# end -# if TT == Bool -# TT = N0f8 -# end - -# stretchmin = stretch(zero(TT)) -# stretchmax = stretch(one(TT)) -# mapper = mappedarray(img_flipped, normed) do pixr, pixn -# if ismissing(pixr) || !isfinite(pixr) || ismissing(pixn) || !isfinite(pixn) -# # We check pixr in addition to pixn because we want to preserve if the pixels -# # are +-Inf -# stretched = pixr -# else -# stretched = stretch(pixn) -# end -# end - -# # The output range may not be [0,1] depending on the stretch function -# clims := (stretchmin,stretchmax) - -# # Calculate tick labels for the colorbar. -# # These may not have linear spacing depending on stretch function. -# # We can't know the inverse of the user's stretch function in general, so we have to -# # map in the forwards direction. -# # cbticklabels = range( -# # imgmin, -# # imgmax, -# # length=9 -# # ) -# # cbtickpos = stretch.(cbticklabels) - -# # By default, disable the colorbar. -# # Plots.jl does no give us sufficient control to make sure the range and ticks -# # are correct after applying a non-linear stretch -# colorbar := false - -# # we have a wcs flag (true by default) so that users can skip over -# # plotting in physical coordinates. This is especially important -# # if the WCS headers are mallformed in some way. -# if wcs - -# # We want to avoid having the same coordinate repeated many times -# # in narrow fields of view (e.g. 150.1, 150.1, 150.1). -# # We attempt to decect this and switch to a coordinate + Δ format -# w = AstroImages.wcs(img) - -# # TODO: Is this really the x min and max? What if the image is rotated? -# x1, y1 = pix_to_world(w, [float(minimum(axes(img,1))), float(minimum(axes(img,2)))]) -# x2, y2 = pix_to_world(w, [float(maximum(axes(img,1))), float(maximum(axes(img,2)))]) -# # Image indices will often be reversed vs. the physical coordinates -# xmin, xmax = minmax(x1,x2) -# ymin, ymax = minmax(y1,y2) - -# # X -# if xmax - xmin < 1 -# start_x = xmin -# start_x_d, start_x_m, start_x_s = deg2dms(xmin) -# diff_x_d, diff_x_m, diff_x_s = deg2dms(xmax) .- deg2dms(xmin) -# xunit = w.cunit[1] -# tickdiv, tickunit = 1.0, xunit -# # Determine which coordinates to use along the axis. -# @show start_x -# start_x = floor(start_x_d) -# @show start_x -# if diff_x_d <= 1 -# start_x = dms2deg(start_x_d, start_x_m, 0) -# @show start_x -# tickdiv, tickunit = nextunit(tickdiv, tickunit) -# end -# if diff_x_m <= 1 -# start_x = dms2deg(start_x_d, start_x_m, ceil(diff_x_m)) -# @show start_x -# tickdiv, tickunit = nextunit(tickdiv, tickunit) -# end - -# # TODO: adaptive unit switching -# xlabel := labler_x(w, (start_x_d, start_x_m, start_x_s)) - -# # tickdiv, tickunit = nextunit(w.cunit[1]) -# xformatter := x -> pix2world_xformatter(x, w, start_x) -# else -# xformatter := x -> pix2world_xformatter(x, w) -# xlabel := labler_x(w) -# end - -# # # Y -# # if ymax - ymin < 1 -# # # Switch to Δ labeling - -# # # TODO: adaptive unit switching -# # starty = round(ymin, RoundToZero, digits=0) -# # ylabel := labler_y(w, starty) - -# # tickdiv, tickunit = nextunit(w.cunit[2]) -# # yformatter := y -> pix2world_yformatter(y, w, starty, tickdiv, tickunit) -# # else -# yformatter := y -> pix2world_yformatter(y, w) -# ylabel := labler_y(w) -# # end - -# # TODO: also disable equal aspect ratio if the scales are totally different -# end - -# return mapper -# end - +# This recipe promotes AstroImages of numerical data into full color using +# imview(). @recipe function f( img::AstroImage{T}; clims=_default_clims[], @@ -209,13 +64,19 @@ save("output.png", v) cmap=_default_cmap[], wcs=true ) where {T<:Number} + # We currently use the AstroImages defaults. If unset, we could + # instead follow the plot theme. iv = imview(img; clims, stretch, cmap) return iv end +# TODO: the wcs parameter is not getting forwardded correctly. Use plot recipe system for this. + +# This recipe plots as AstroImage of color data as an image series (not heatmap). +# This lets us also plot color composites e.g. in WCS coordinates. @recipe function f( img::AstroImage{T}; - wcs=true + wcs=AstroImages.wcs(img) ) where {T<:Colorant} # By default, disable the colorbar. @@ -223,153 +84,303 @@ end # are correct after applying a non-linear stretch # colorbar := false - # we have a wcs flag (true by default) so that users can skip over + # we have a wcs flag (from the image by default) so that users can skip over # plotting in physical coordinates. This is especially important # if the WCS headers are mallformed in some way. - @show wcs - if wcs - - # We want to avoid having the same coordinate repeated many times - # in narrow fields of view (e.g. 150.1, 150.1, 150.1). - # We attempt to decect this and switch to a coordinate + Δ format - w = AstroImages.wcs(img) - - - # TODO: Is this really the x min and max? What if the image is rotated? - x1, y1 = pix_to_world(w, [float(minimum(axes(img,1))), float(minimum(axes(img,2)))]) - x2, y2 = pix_to_world(w, [float(maximum(axes(img,1))), float(maximum(axes(img,2)))]) - # Image indices will often be reversed vs. the physical coordinates - xmin, xmax = minmax(x1,x2) - ymin, ymax = minmax(y1,y2) - - # X - if xmax - xmin < 1 - start_x = xmin - start_x_d, start_x_m, start_x_s = deg2dms(xmin) - diff_x_d, diff_x_m, diff_x_s = deg2dms(xmax) .- deg2dms(xmin) - xunit = w.cunit[1] - tickdiv, tickunit = 1.0, xunit - # Determine which coordinates to use along the axis. - @show start_x - start_x = floor(start_x_d) - @show start_x - if diff_x_d <= 1 - start_x = dms2deg(start_x_d, start_x_m, 0) - @show start_x - tickdiv, tickunit = nextunit(tickdiv, tickunit) - end - if diff_x_m <= 1 - start_x = dms2deg(start_x_d, start_x_m, ceil(diff_x_m)) - @show start_x - tickdiv, tickunit = nextunit(tickdiv, tickunit) - end - - # TODO: adaptive unit switching - xlabel := labler_x(w, (start_x_d, start_x_m, start_x_s)) - - # tickdiv, tickunit = nextunit(w.cunit[1]) - xformatter := x -> pix2world_xformatter(x, w, start_x) - else - xformatter := x -> pix2world_xformatter(x, w) - xlabel := labler_x(w) - end + if !isnothing(wcs) - # # Y - # if ymax - ymin < 1 - # # Switch to Δ labeling + yguide, yticks = prepare_label_ticks(img, 2) + yguide := yguide + yticks := yticks - # # TODO: adaptive unit switching - # starty = round(ymin, RoundToZero, digits=0) - # ylabel := labler_y(w, starty) + xguide, xticks = prepare_label_ticks(img, 1) + xguide := xguide + xticks := xticks - # tickdiv, tickunit = nextunit(w.cunit[2]) - # yformatter := y -> pix2world_yformatter(y, w, starty, tickdiv, tickunit) - # else - yformatter := y -> pix2world_yformatter(y, w) - ylabel := labler_y(w) - # end - - # TODO: also disable equal aspect ratio if the scales are totally different end - return LinRange(xmin,xmax,size(img,1)), LinRange(xmin,xmax,size(img,2)), arraydata(img) -end + # TODO: also disable equal aspect ratio if the scales are totally different + aspect_ratio := :equal -function pix2world_xformatter(x, wcs::WCSTransform) - res = round(pix_to_world(wcs, [float(x), float(x)])[1][1], digits=2) - return string(res, unit2sym(wcs.cunit[1])) + # We have to do a lot of flipping to keep the orientation corect + yflip := false + return axes(img,1), axes(img,2), view(arraydata(img), reverse(axes(img,1)),:) end -function pix2world_xformatter(x, wcs::WCSTransform, start) - x = pix_to_world(wcs, [float(x), float(x)])[1][1] - - pm = sign(start - x) >= 0 ? '+' : '-' - - - @show start x - diff_x_d, diff_x_m, diff_x_s = deg2dms(x) .- deg2dms(start) - @show diff_x_d diff_x_m diff_x_s - - if abs(diff_x_d) > 1 - return string(pm, abs(round(Int, diff_x_d)), unit2sym("deg")) - elseif abs(diff_x_m) > 1 - @show diff_x_m - return string(pm, abs(round(Int, diff_x_m)), unit2sym("am")) - elseif abs(diff_x_s) > 1 - return string(pm, abs(round(Int, diff_x_s)), unit2sym("as")) - elseif abs(diff_x_s) > 1e-3 - return string(pm, abs(round(Int, diff_x_s*1e3)), unit2sym("mas")) - elseif abs(diff_x_s) > 1e-6 - return string(pm, abs(round(Int, diff_x_s*1e3)), unit2sym("μas")) + + +function prepare_label_ticks(img, axnum) + + if axnum == 1 + label = ctype_x(wcs(img)) + if wcs(img).cunit[1] == "deg" + + else + converter = x->(x,) + end + elseif axnum == 2 + label = ctype_y(wcs(img)) else - return string(pm, abs(diff_x_s), unit2sym("as")) + label = wcs(img).ctype[axnum] + converter = deg2dmsmμ + units = dmsmμ_units + end + if wcs(img).cunit[axnum] == "deg" + # TODO: detect RA vs dec + if axnum == 1 + converter = deg2hms + units = hms_units + else + converter = deg2dmsmμ + units = dmsmμ_units + end + else + converter = x->(x,) + units = ("",) end - + # w denotes world coordinates along this axis; x denotes pixel coordinates. + # wᵀ denotes world coordinates along the opposite axis. + axnumᵀ = axnum == 1 ? 2 : 1 + # TODO: Is this really the x min and max? What if the image is rotated? + w1, wᵀ1 = pix_to_world(wcs(img), Float64[minimum(axes(img,1)), minimum(axes(img,2))])[[axnum, axnumᵀ]] + w2, wᵀ2 = pix_to_world(wcs(img), Float64[minimum(axes(img,1)), maximum(axes(img,2))])[[axnum, axnumᵀ]] + w3, wᵀ3 = pix_to_world(wcs(img), Float64[maximum(axes(img,1)), minimum(axes(img,2))])[[axnum, axnumᵀ]] + w4, wᵀ4 = pix_to_world(wcs(img), Float64[maximum(axes(img,1)), maximum(axes(img,2))])[[axnum, axnumᵀ]] + minw, maxw = extrema((w1,w2,w3,w4)) + minwᵀ, maxwᵀ = extrema((wᵀ1,wᵀ2,wᵀ3,wᵀ4)) + + + # Use PlotUtils.optimize_ticks to find good tick positions in world coordinates + tickpos_w = optimize_ticks(minw*60*60, maxw*60*60; k_min=6, k_ideal=6)[1] + # Then convert back to pixel coordinates along the axis + tickpos = map(tickpos_w) do w + x = world_to_pix(wcs(img), Float64[w/60/60, minwᵀ][[axnum,axnumᵀ]])[axnum] + return x + end - # return string(pm, diff_x_d, diff_x_m, diff_x_s, unit2sym(tickunit)) - return string(pm, abs(round(Int, diff_x_s)), unit2sym(tickunit)) -end + minxᵀ, maxxᵀ = first(axes(img,axnumᵀ)), last(axes(img,axnumᵀ)) -function pix2world_yformatter(y, wcs::WCSTransform) - res = round(pix_to_world(wcs, [float(y), float(y)])[2][1], digits=2) - return string(res, unit2sym(wcs.cunit[1])) -end -function pix2world_yformatter(y, wcs::WCSTransform, start, tickdiv, tickunit) - y = pix_to_world(wcs, [float(y), float(y)])[2][1] - pm = sign(res) >= 0 ? '+' : '-' - return string(pm, abs(round(res,digits=2)), unit2sym(tickunit)) -end + # Format inital ticklabel + ticklabels = fill("", length(tickpos)) + # We only include the part of the label that has changed since the last time. + # Split up coordinates into e.g. sexagesimal + parts = map(tickpos) do x + w = pix_to_world(wcs(img), Float64[x, minxᵀ][[axnum,axnumᵀ]])[axnum] + vals = converter(w) + return vals + end + # Start with something impossible of the same size: + last_coord = Inf .* converter(minw) + zero_coords_i = maximum(map(parts) do vals + changing_coord_i = findfirst(vals .!= last_coord) + last_coord = vals + return changing_coord_i + end) + + # Loop through using only the relevant part of the label + # Start with something impossible of the same size: + last_coord = Inf .* converter(minw) + for (i,vals) in enumerate(parts) + changing_coord_i = findfirst(vals .!= last_coord) + + ticklabels[i] = mapreduce(*, zip(vals[changing_coord_i:zero_coords_i],units[changing_coord_i:zero_coords_i])) do (val,unit) + @sprintf("%d%s", val, unit) + end -labler_x(wcs) = ctype_x(wcs) -labler_y(wcs) = ctype_y(wcs) + last_coord = vals + end -labler_x(wcs, start) = string(ctype_x(wcs), " ", start, unit2sym(wcs.cunit[1])) -labler_y(wcs, start) = string(ctype_y(wcs), " ", start, unit2sym(wcs.cunit[2])) + return label, (tickpos, ticklabels) -function unit2sym(unit) - if unit == "deg" # TODO: add symbols for more units - "°" - elseif unit == "am" # TODO: add symbols for more units - "'" - elseif unit == "as" # TODO: add symbols for more units - "\"" - else - string(unit) - end end -function nextunit(div, unit) - if unit == "deg" # TODO: add symbols for more units - return div*60, "am" - elseif unit == "am" # TODO: add symbols for more units - return div*60, "as" - elseif unit == "as" # TODO: add symbols for more units - return div*60, "mas" - else - div, string(unit) - end + +# # We need a function that takes a WCSTransform, axis number, and physical coordinate start annd stop +# # And returns (if units are degrees): +# # * Starting position in sexagesimal (truncated to show static parts) +# # * formatter in sexagesimal (truncated to show changing parts) +# function prepare_formatter(w::WCSTransform, axnum, xmin, xmax, len) + +# if axnum == 1 +# label = ctype_x(w) +# converter = deg2hms +# units = hms_units +# elseif axnum == 2 +# label = ctype_y(w) +# converter = deg2dmsmμ +# units = dmsmμ_units +# else +# label = w.ctype[axnum] +# converter = deg2dmsmμ +# units = dmsmμ_units +# end + +# if w.cunit[axnum] != "deg" +# formatter = string +# else +# diff = converter(xmax) .- converter(xmin) +# changing_coord_i = findfirst(d->abs(d)>0, diff) + +# start = vcat( +# converter(xmin)[1:changing_coord_i-1]..., +# zeros(length(units)-changing_coord_i+1)... +# ) + +# # Add the starting coordinate to the label. +# # These are the components that are the same for every tick +# label *= " " * mapreduce(*, zip(start,units[1:changing_coord_i-1])) do (val,unit) +# @sprintf("%d%s", val, unit) +# end * "+" + +# # TODO: also need a tick positioner??? Ottherwise the ticks aren'tt properly posjtions + +# formatter = function (x) +# # TODO: pixels are already in physical coordinates... +# # This seems like not a good assumption +# # res = x +# # Todo, both x? Doesn't seem right +# # res = pix_to_world(w, [float(x), float(x)])[axnum] +# if axnum ==1 +# res = pix_to_world(w, [float(x), float(zero(x))])[axnum] +# else +# res = pix_to_world(w, [float(zero(x)), float(x)])[axnum] +# end + +# vals = converter(res) .- start +# if changing_coord_i < length(units) +# ticklabel = " " * mapreduce(*, zip(vals[changing_coord_i:min(changing_coord_i+1,end)],units[changing_coord_i:min(changing_coord_i+1,end)])) do (val,unit) +# @sprintf("%d%s", val, unit) +# end +# else +# ticklabel = @sprintf( +# "%.2f%s", +# vals[changing_coord_i], +# units[changing_coord_i] +# ) +# end +# return ticklabel +# end +# end +# return label, formatter +# end + +# # Extended form of deg2dms that further returns mas, microas. +function deg2dmsmμ(deg) + d,m,s = deg2dms(deg) + s_f = floor(s) + mas = (s - s_f)*1e3 + mas_f = floor(mas) + μas = (mas - mas_f)*1e3 + return (d,m,s_f,mas_f,μas) end +const dmsmμ_units = [ + "°", + "'", + "\"", + "mas", + "μas", +] + +# function deg2hmsmμ(deg) +# h,m,s = deg2hms(deg) +# s_f = floor(s) +# mas = (s - s_f)*1e3 +# mas_f = floor(mas) +# μas = (mas - mas_f)*1e3 +# return (h,m,s_f,mas_f,μas) +# end +# const hmsmμ_units = [ +# "ʰ", +# "ᵐ", +# "ˢ", +# # "mas", +# # "μas", +# ] +# const hms_units = [ +# "ʰ", +# "ᵐ", +# "ˢ", +# ] + + + + + + + +# function pix2world_xformatter(x, wcs::WCSTransform) +# res = round(pix_to_world(wcs, [float(x), float(x)])[1][1], digits=2) +# return string(res, unit2sym(wcs.cunit[1])) +# end +# function pix2world_xformatter(x, wcs::WCSTransform, start) +# x = pix_to_world(wcs, [float(x), float(x)])[1][1] + +# pm = sign(start - x) >= 0 ? '+' : '-' + +# diff_x_d, diff_x_m, diff_x_s = deg2dms(x) .- deg2dms(start) + +# if abs(diff_x_d) > 1 +# return string(pm, abs(round(Int, diff_x_d)), unit2sym("deg")) +# elseif abs(diff_x_m) > 1 +# @show diff_x_m +# return string(pm, abs(round(Int, diff_x_m)), unit2sym("am")) +# elseif abs(diff_x_s) > 1 +# return string(pm, abs(round(Int, diff_x_s)), unit2sym("as")) +# elseif abs(diff_x_s) > 1e-3 +# return string(pm, abs(round(Int, diff_x_s*1e3)), unit2sym("mas")) +# elseif abs(diff_x_s) > 1e-6 +# return string(pm, abs(round(Int, diff_x_s*1e3)), unit2sym("μas")) +# else +# return string(pm, abs(diff_x_s), unit2sym("as")) +# end + + + +# # return string(pm, diff_x_d, diff_x_m, diff_x_s, unit2sym(tickunit)) +# return string(pm, abs(round(Int, diff_x_s)), unit2sym(tickunit)) +# end + +# function pix2world_yformatter(y, wcs::WCSTransform) +# res = round(pix_to_world(wcs, [float(y), float(y)])[2][1], digits=2) +# return string(res, unit2sym(wcs.cunit[1])) +# end +# function pix2world_yformatter(y, wcs::WCSTransform, start, tickdiv, tickunit) +# y = pix_to_world(wcs, [float(y), float(y)])[2][1] +# pm = sign(res) >= 0 ? '+' : '-' +# return string(pm, abs(round(res,digits=2)), unit2sym(tickunit)) +# end + + +# labler_x(wcs) = ctype_x(wcs) +# labler_y(wcs) = ctype_y(wcs) + +# labler_x(wcs, start) = string(ctype_x(wcs), " ", start, unit2sym(wcs.cunit[1])) +# labler_y(wcs, start) = string(ctype_y(wcs), " ", start, unit2sym(wcs.cunit[2])) + +# function unit2sym(unit) +# if unit == "deg" # TODO: add symbols for more units +# "°" +# elseif unit == "am" # TODO: add symbols for more units +# "'" +# elseif unit == "as" # TODO: add symbols for more units +# "\"" +# else +# string(unit) +# end +# end + +# function nextunit(div, unit) +# if unit == "deg" # TODO: add symbols for more units +# return div*60, "am" +# elseif unit == "am" # TODO: add symbols for more units +# return div*60, "as" +# elseif unit == "as" # TODO: add symbols for more units +# return div*60, "mas" +# else +# div, string(unit) +# end +# end function ctype_x(wcs::WCSTransform) diff --git a/src/showmime.jl b/src/showmime.jl index bf271a91..db4a1ebf 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -194,44 +194,36 @@ function _imview(img, normed::AbstractArray{T}, stretch, cmap) where T # No color map: use Gray if isnothing(cmap) - f = scaleminmax(stretchmin, stretchmax) - mapper = mappedarray(normed) do pix - if ismissing(pix) - return Gray{TT}(0) - else - stretched = isfinite(pix) ? stretch(pix) : pix - return Gray{TT}(f(stretched)) - end + cmap = :grays + end + cscheme = ColorSchemes.colorschemes[cmap] + mapper = mappedarray(img, normed) do pixr, pixn + if ismissing(pixr) || !isfinite(pixr) || ismissing(pixn) || !isfinite(pixn) + # We check pixr in addition to pixn because we want to preserve if the pixels + # are +-Inf + stretched = pixr + else + stretched = stretch(pixn) end - # Monochromatic image using a colormap via ColorSchemes - else - cscheme = ColorSchemes.colorschemes[cmap] - mapper = mappedarray(img, normed) do pixr, pixn - if ismissing(pixr) || !isfinite(pixr) || ismissing(pixn) || !isfinite(pixn) - # We check pixr in addition to pixn because we want to preserve if the pixels - # are +-Inf - stretched = pixr + # We treat NaN/missing values as transparent + return if ismissing(stretched) || isnan(stretched) + RGBA{TT}(0,0,0,0) + # We treat Inf values as white / -Inf as black + elseif isinf(stretched) + if stretched > 0 + RGBA{TT}(1,1,1,1) else - stretched = stretch(pixn) - end - # We treat NaN/missing values as transparent - return if ismissing(stretched) || isnan(stretched) - RGBA{TT}(0,0,0,0) - # We treat Inf values as white / -Inf as black - elseif isinf(stretched) - if stretched > 0 - RGBA{TT}(1,1,1,1) - else - RGBA{TT}(0,0,0,1) - end - else - RGBA{TT}(get(cscheme::ColorScheme, stretched, (stretchmin, stretchmax))) + RGBA{TT}(0,0,0,1) end + else + RGBA{TT}(get(cscheme::ColorScheme, stretched, (stretchmin, stretchmax))) end end - return maybe_shareheaders(img, mapper) - # return mapper + # Flip image to match conventions of other programs + flipped_view = view(mapper', reverse(axes(mapper,1)),:) + + return maybe_copyheaders(img, flipped_view) end export imview From 72edafc6901dac98af76255eb63cb8f8a5b85e8b Mon Sep 17 00:00:00 2001 From: William Thompson Date: Wed, 5 Jan 2022 12:55:51 -0800 Subject: [PATCH 030/178] Allow compose to work with regular arrays in addition to AstroImages --- src/ccd2rgb.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ccd2rgb.jl b/src/ccd2rgb.jl index 6a758d8a..99980661 100644 --- a/src/ccd2rgb.jl +++ b/src/ccd2rgb.jl @@ -93,7 +93,7 @@ function composechannels( ## TODO: this all needs to be lazy colorized = map(eachindex(reprojected)) do i - arraydata(reprojected[i]) .* multipliers[i] .* colors[i] + reprojected[i] .* multipliers[i] .* colors[i] end mapped = (+).(colorized...) ./ length(reprojected) T = coloralpha(eltype(mapped)) From 38887ad1272a59a3b52f0620b6203b29d405d2f7 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Wed, 5 Jan 2022 12:56:25 -0800 Subject: [PATCH 031/178] Improvements to plot recipes --- src/plot-recipes.jl | 115 ++++++++++++++++++++++++++++++++------------ 1 file changed, 85 insertions(+), 30 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index fb1d8dd7..f71b4793 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -104,7 +104,7 @@ end # We have to do a lot of flipping to keep the orientation corect yflip := false - return axes(img,1), axes(img,2), view(arraydata(img), reverse(axes(img,1)),:) + return axes(img,2), axes(img,1), view(arraydata(img), reverse(axes(img,1)),:) end @@ -112,24 +112,18 @@ function prepare_label_ticks(img, axnum) if axnum == 1 label = ctype_x(wcs(img)) - if wcs(img).cunit[1] == "deg" - - else - converter = x->(x,) - end elseif axnum == 2 label = ctype_y(wcs(img)) else label = wcs(img).ctype[axnum] - converter = deg2dmsmμ - units = dmsmμ_units end if wcs(img).cunit[axnum] == "deg" - # TODO: detect RA vs dec - if axnum == 1 + if startswith(uppercase(wcs(img).ctype[axnum]), "RA") + @info "using deg2hms" converter = deg2hms units = hms_units else + @info "using deg2dms" converter = deg2dmsmμ units = dmsmμ_units end @@ -141,31 +135,77 @@ function prepare_label_ticks(img, axnum) # w denotes world coordinates along this axis; x denotes pixel coordinates. # wᵀ denotes world coordinates along the opposite axis. axnumᵀ = axnum == 1 ? 2 : 1 - # TODO: Is this really the x min and max? What if the image is rotated? - w1, wᵀ1 = pix_to_world(wcs(img), Float64[minimum(axes(img,1)), minimum(axes(img,2))])[[axnum, axnumᵀ]] - w2, wᵀ2 = pix_to_world(wcs(img), Float64[minimum(axes(img,1)), maximum(axes(img,2))])[[axnum, axnumᵀ]] - w3, wᵀ3 = pix_to_world(wcs(img), Float64[maximum(axes(img,1)), minimum(axes(img,2))])[[axnum, axnumᵀ]] - w4, wᵀ4 = pix_to_world(wcs(img), Float64[maximum(axes(img,1)), maximum(axes(img,2))])[[axnum, axnumᵀ]] - minw, maxw = extrema((w1,w2,w3,w4)) - minwᵀ, maxwᵀ = extrema((wᵀ1,wᵀ2,wᵀ3,wᵀ4)) + # TODO: Is this really the x min and max? What if the image is rotated? + # TODO: what about viewing a slice? + # TODO: wrapped around axes... + # Mabye we can detect this by comparing with the minpoint. If they're not monatonic, + # there is a wrap around somewhere. But then what do we do... + + @info "axes" axnum axnumᵀ + minx = first(axes(img,axnum)) + maxx = last(axes(img,axnum)) + minxᵀ = first(axes(img,axnumᵀ)) + maxxᵀ = last(axes(img,axnumᵀ)) + @info "pixel extrema" minx maxx minxᵀ maxxᵀ + @show [axnum, axnumᵀ] + + w1, wᵀ1 = pix_to_world(wcs(img), Float64[minx, minxᵀ][[axnum, axnumᵀ]])[[axnum, axnumᵀ]] + w2, wᵀ2 = pix_to_world(wcs(img), Float64[minx, maxxᵀ][[axnum, axnumᵀ]])[[axnum, axnumᵀ]] + w3, wᵀ3 = pix_to_world(wcs(img), Float64[maxx, minxᵀ][[axnum, axnumᵀ]])[[axnum, axnumᵀ]] + w4, wᵀ4 = pix_to_world(wcs(img), Float64[maxx, maxxᵀ][[axnum, axnumᵀ]])[[axnum, axnumᵀ]] + # minw, maxw = extrema((w1,w2,w3,w4)) + # minwᵀ, maxwᵀ = extrema((wᵀ1,wᵀ2,wᵀ3,wᵀ4)) + # @show w1 w2 w3 w4 + minw, maxw = extrema((w1,w3)) + minwᵀ, maxwᵀ = extrema((wᵀ1,wᵀ3)) + @info "world extrema" w1 w2 w3 w4 wᵀ1 wᵀ2 wᵀ3 wᵀ4 minw maxw + # @show w1 w3 wᵀ3 wᵀ3 + + # TODO: May need to rethink this approach in light of coordinates that can wrap around + # Perhaps we can instead choose a phyically relevant step size # Use PlotUtils.optimize_ticks to find good tick positions in world coordinates - tickpos_w = optimize_ticks(minw*60*60, maxw*60*60; k_min=6, k_ideal=6)[1] + Q=[(1.0,1.0), (2.0, 0.7), (5.0, 0.5), (3.0, 0.2)] + tickpos_w = optimize_ticks(minw*6, maxw*6; Q, k_min=3, k_ideal=6)[1] + if w1 > w3 + tickpos_w = reverse(tickpos_w) + end # Then convert back to pixel coordinates along the axis tickpos = map(tickpos_w) do w - x = world_to_pix(wcs(img), Float64[w/60/60, minwᵀ][[axnum,axnumᵀ]])[axnum] + # TODO: naxis, slices + x = world_to_pix(wcs(img), Float64[w/6, minwᵀ, 0][[axnum,axnumᵀ,3]])[axnum] return x end - minxᵀ, maxxᵀ = first(axes(img,axnumᵀ)), last(axes(img,axnumᵀ)) + + # tickpos_w = optimize_ticks(minw*60*60, maxw*60*60; k_min=6, k_ideal=6)[1] + # @show tickpos_w + # phys_tick_spacing = mean(diff(tickpos_w))/60/60 + # @show phys_tick_spacing + # pix_spacing = abs( + # world_to_pix(wcs(img), Float64[minw+phys_tick_spacing, minwᵀ, 0][[axnum,axnumᵀ,3]])[axnum] - + # world_to_pix(wcs(img), Float64[minw, minwᵀ, 0][[axnum,axnumᵀ,3]])[axnum] + + # ) + # @show pix_spacing + + # Q = [ + # (1.0, 1.0), + # (pix_spacing, 0.9), + # (pix_spacing/2, 0.8), + # (pix_spacing/3, 0.7), + # (pix_spacing/5, 0.6), + # ] + # tickpos = optimize_ticks(minx, maxx; Q, k_min=6, k_ideal=6)[1] # Format inital ticklabel ticklabels = fill("", length(tickpos)) # We only include the part of the label that has changed since the last time. # Split up coordinates into e.g. sexagesimal parts = map(tickpos) do x - w = pix_to_world(wcs(img), Float64[x, minxᵀ][[axnum,axnumᵀ]])[axnum] + # TODO: naxis + w = pix_to_world(wcs(img), Float64[x, minxᵀ, 0][[axnum,axnumᵀ,3]])[axnum] vals = converter(w) return vals end @@ -178,18 +218,33 @@ function prepare_label_ticks(img, axnum) return changing_coord_i end) + # deubg override: + # zero_coords_i = length(last_coord) + # Loop through using only the relevant part of the label # Start with something impossible of the same size: last_coord = Inf .* converter(minw) for (i,vals) in enumerate(parts) changing_coord_i = findfirst(vals .!= last_coord) - ticklabels[i] = mapreduce(*, zip(vals[changing_coord_i:zero_coords_i],units[changing_coord_i:zero_coords_i])) do (val,unit) - @sprintf("%d%s", val, unit) + val_unit_zip = zip(vals[changing_coord_i:zero_coords_i],units[changing_coord_i:zero_coords_i]) + ticklabels[i] = mapreduce(*, enumerate(val_unit_zip)) do (coord_i,(val,unit)) + # Last coordinate always gets decimal places + # if coord_i == zero_coords_i && zero_coords_i == length(vals) + if coord_i + changing_coord_i - 1== length(vals) + str = @sprintf("%.2f", val) + while endswith(str, r"0|\.") + str = chop(str) + end + else + str = @sprintf("%d", val) + end + return str * unit end last_coord = vals end + @show ticklabels return label, (tickpos, ticklabels) @@ -297,11 +352,11 @@ const dmsmμ_units = [ # # "mas", # # "μas", # ] -# const hms_units = [ -# "ʰ", -# "ᵐ", -# "ˢ", -# ] +const hms_units = [ + "ʰ", + "ᵐ", + "ˢ", +] @@ -389,7 +444,7 @@ function ctype_x(wcs::WCSTransform) elseif wcs.ctype[1][1:2] == "RA" return "Right Ascension (ICRS)" elseif wcs.ctype[1][1:4] == "GLON" - return "Galactic Coordinate" + return "Galactic Longitude" elseif wcs.ctype[1][1:4] == "TLON" return "ITRS" else @@ -403,7 +458,7 @@ function ctype_y(wcs::WCSTransform) elseif wcs.ctype[2][1:3] == "DEC" return "Declination (ICRS)" elseif wcs.ctype[2][1:4] == "GLAT" - return "Galactic Coordinate" + return "Galactic Latitude" elseif wcs.ctype[2][1:4] == "TLAT" return "ITRS" else From 65bf3ce8de2c290766fc8515a815fad9aa8a2ca8 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Thu, 6 Jan 2022 08:52:56 -0800 Subject: [PATCH 032/178] WIP wcs axes in plots --- src/AstroImages.jl | 4 +- src/plot-recipes.jl | 325 ++++++++++---------------------------------- src/showmime.jl | 2 +- 3 files changed, 75 insertions(+), 256 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index a904911b..fdc7aeb3 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -156,6 +156,8 @@ for f in [ @eval ($f)(img::AstroImage) = shareheaders(img, $f(arraydata(img))) end +Base.parent(img::AstroImage) = arraydata(img) + # Getting and setting data is forwarded to the underlying array # Accessing a single value or a vector returns just the data. # Accering a 2+D slice copies the headers and re-wraps the data. @@ -163,7 +165,7 @@ function Base.getindex(img::AstroImage, inds...) dat = getindex(arraydata(img), inds...) # ndims is defined for Numbers but not Missing. # This check is therefore necessary for img[1,1]->missing to work. - if !(typeof(dat) <: Number) || ndims(dat) <= 1 + if !(eltype(dat) <: Number) || ndims(dat) <= 1 return dat else return copyheaders(img, dat) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index f71b4793..beac1723 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -62,7 +62,7 @@ save("output.png", v) clims=_default_clims[], stretch=_default_stretch[], cmap=_default_cmap[], - wcs=true + wcs=AstroImages.wcs(img) ) where {T<:Number} # We currently use the AstroImages defaults. If unset, we could # instead follow the plot theme. @@ -89,41 +89,61 @@ end # if the WCS headers are mallformed in some way. if !isnothing(wcs) - yguide, yticks = prepare_label_ticks(img, 2) - yguide := yguide - yticks := yticks - - xguide, xticks = prepare_label_ticks(img, 1) + # TODO: fill out coordinates array considering offset indices and slices + # out of cubes (tricky!) + + xguide, xticks = prepare_label_ticks(img, 1, ones(wcs.naxis)) xguide := xguide xticks := xticks + yguide, yticks = prepare_label_ticks(img, 2, ones(wcs.naxis)) + yguide := yguide + yticks := yticks end # TODO: also disable equal aspect ratio if the scales are totally different - aspect_ratio := :equal + # aspect_ratio := :equal # We have to do a lot of flipping to keep the orientation corect yflip := false + # return axes(img,2), axes(img,1), view(arraydata(img), reverse(axes(img,1)),:) + xflip := false return axes(img,2), axes(img,1), view(arraydata(img), reverse(axes(img,1)),:) end -function prepare_label_ticks(img, axnum) +""" +Calculate good tick positions and nice labels for them + +INPUT +img: an AstroImage +axnum: the index of the axis we want ticks for +axnumᵀ: the index of the axis we are plotting against +coords: the position in all coordinates for this plot. The value a axnum and axnumᵀ is igored. + +`coords` is important for showing 2D coords of a 3+D cube as we need to know +our position along the other axes for accurate tick positions. + +OUTPUT +tickpos: tick positions in pixels for this axis +ticklabels: tick labels for each position +""" +function prepare_label_ticks(img, axnum, coords) + + naxis = wcs(img).naxis + coordsx = convert(Vector{Float64}, coords) + + # coordsw = zeros(eltype(coordsx), size(coordsx)) + coordsw = pix_to_world(wcs(img), coordsx) + + + label = ctype_label(wcs(img).ctype[axnum], wcs(img).radesys) - if axnum == 1 - label = ctype_x(wcs(img)) - elseif axnum == 2 - label = ctype_y(wcs(img)) - else - label = wcs(img).ctype[axnum] - end if wcs(img).cunit[axnum] == "deg" if startswith(uppercase(wcs(img).ctype[axnum]), "RA") - @info "using deg2hms" converter = deg2hms units = hms_units else - @info "using deg2dms" converter = deg2dmsmμ units = dmsmμ_units end @@ -134,78 +154,49 @@ function prepare_label_ticks(img, axnum) # w denotes world coordinates along this axis; x denotes pixel coordinates. # wᵀ denotes world coordinates along the opposite axis. - axnumᵀ = axnum == 1 ? 2 : 1 - # TODO: Is this really the x min and max? What if the image is rotated? - # TODO: what about viewing a slice? # TODO: wrapped around axes... # Mabye we can detect this by comparing with the minpoint. If they're not monatonic, # there is a wrap around somewhere. But then what do we do... - @info "axes" axnum axnumᵀ minx = first(axes(img,axnum)) maxx = last(axes(img,axnum)) - minxᵀ = first(axes(img,axnumᵀ)) - maxxᵀ = last(axes(img,axnumᵀ)) - @info "pixel extrema" minx maxx minxᵀ maxxᵀ - @show [axnum, axnumᵀ] - - w1, wᵀ1 = pix_to_world(wcs(img), Float64[minx, minxᵀ][[axnum, axnumᵀ]])[[axnum, axnumᵀ]] - w2, wᵀ2 = pix_to_world(wcs(img), Float64[minx, maxxᵀ][[axnum, axnumᵀ]])[[axnum, axnumᵀ]] - w3, wᵀ3 = pix_to_world(wcs(img), Float64[maxx, minxᵀ][[axnum, axnumᵀ]])[[axnum, axnumᵀ]] - w4, wᵀ4 = pix_to_world(wcs(img), Float64[maxx, maxxᵀ][[axnum, axnumᵀ]])[[axnum, axnumᵀ]] - # minw, maxw = extrema((w1,w2,w3,w4)) - # minwᵀ, maxwᵀ = extrema((wᵀ1,wᵀ2,wᵀ3,wᵀ4)) - # @show w1 w2 w3 w4 + + posx = copy(coordsx) + posx[axnum] = minx + w1 = pix_to_world(wcs(img), posx)[axnum] + posx[axnum] = maxx + w3 = pix_to_world(wcs(img), posx)[axnum] minw, maxw = extrema((w1,w3)) - minwᵀ, maxwᵀ = extrema((wᵀ1,wᵀ3)) - @info "world extrema" w1 w2 w3 w4 wᵀ1 wᵀ2 wᵀ3 wᵀ4 minw maxw - # @show w1 w3 wᵀ3 wᵀ3 # TODO: May need to rethink this approach in light of coordinates that can wrap around # Perhaps we can instead choose a phyically relevant step size # Use PlotUtils.optimize_ticks to find good tick positions in world coordinates - Q=[(1.0,1.0), (2.0, 0.7), (5.0, 0.5), (3.0, 0.2)] + Q=[(1.0,1.0), (3.0, 0.8), (2.0, 0.7), (5.0, 0.5)] tickpos_w = optimize_ticks(minw*6, maxw*6; Q, k_min=3, k_ideal=6)[1] if w1 > w3 tickpos_w = reverse(tickpos_w) end + # Then convert back to pixel coordinates along the axis tickpos = map(tickpos_w) do w - # TODO: naxis, slices - x = world_to_pix(wcs(img), Float64[w/6, minwᵀ, 0][[axnum,axnumᵀ,3]])[axnum] + posw = copy(coordsw) + posw[axnum] = w/6 + # pos[axnumᵀ] = minwᵀ # TODO: should this instead be wᵀ1? + x = world_to_pix(wcs(img), posw)[axnum] return x end - - # tickpos_w = optimize_ticks(minw*60*60, maxw*60*60; k_min=6, k_ideal=6)[1] - # @show tickpos_w - # phys_tick_spacing = mean(diff(tickpos_w))/60/60 - # @show phys_tick_spacing - # pix_spacing = abs( - # world_to_pix(wcs(img), Float64[minw+phys_tick_spacing, minwᵀ, 0][[axnum,axnumᵀ,3]])[axnum] - - # world_to_pix(wcs(img), Float64[minw, minwᵀ, 0][[axnum,axnumᵀ,3]])[axnum] - - # ) - # @show pix_spacing - - # Q = [ - # (1.0, 1.0), - # (pix_spacing, 0.9), - # (pix_spacing/2, 0.8), - # (pix_spacing/3, 0.7), - # (pix_spacing/5, 0.6), - # ] - # tickpos = optimize_ticks(minx, maxx; Q, k_min=6, k_ideal=6)[1] - # Format inital ticklabel ticklabels = fill("", length(tickpos)) # We only include the part of the label that has changed since the last time. # Split up coordinates into e.g. sexagesimal parts = map(tickpos) do x - # TODO: naxis - w = pix_to_world(wcs(img), Float64[x, minxᵀ, 0][[axnum,axnumᵀ,3]])[axnum] + posx = copy(coordsx) + posx[axnum] = x + # pos[axnumᵀ] = minxᵀ + w = pix_to_world(wcs(img), posx)[axnum] vals = converter(w) return vals end @@ -218,9 +209,6 @@ function prepare_label_ticks(img, axnum) return changing_coord_i end) - # deubg override: - # zero_coords_i = length(last_coord) - # Loop through using only the relevant part of the label # Start with something impossible of the same size: last_coord = Inf .* converter(minw) @@ -239,88 +227,22 @@ function prepare_label_ticks(img, axnum) else str = @sprintf("%d", val) end - return str * unit + if length(str) > 0 + return str * unit + else + return str + end end last_coord = vals end - @show ticklabels return label, (tickpos, ticklabels) end -# # We need a function that takes a WCSTransform, axis number, and physical coordinate start annd stop -# # And returns (if units are degrees): -# # * Starting position in sexagesimal (truncated to show static parts) -# # * formatter in sexagesimal (truncated to show changing parts) -# function prepare_formatter(w::WCSTransform, axnum, xmin, xmax, len) - -# if axnum == 1 -# label = ctype_x(w) -# converter = deg2hms -# units = hms_units -# elseif axnum == 2 -# label = ctype_y(w) -# converter = deg2dmsmμ -# units = dmsmμ_units -# else -# label = w.ctype[axnum] -# converter = deg2dmsmμ -# units = dmsmμ_units -# end - -# if w.cunit[axnum] != "deg" -# formatter = string -# else -# diff = converter(xmax) .- converter(xmin) -# changing_coord_i = findfirst(d->abs(d)>0, diff) - -# start = vcat( -# converter(xmin)[1:changing_coord_i-1]..., -# zeros(length(units)-changing_coord_i+1)... -# ) - -# # Add the starting coordinate to the label. -# # These are the components that are the same for every tick -# label *= " " * mapreduce(*, zip(start,units[1:changing_coord_i-1])) do (val,unit) -# @sprintf("%d%s", val, unit) -# end * "+" - -# # TODO: also need a tick positioner??? Ottherwise the ticks aren'tt properly posjtions - -# formatter = function (x) -# # TODO: pixels are already in physical coordinates... -# # This seems like not a good assumption -# # res = x -# # Todo, both x? Doesn't seem right -# # res = pix_to_world(w, [float(x), float(x)])[axnum] -# if axnum ==1 -# res = pix_to_world(w, [float(x), float(zero(x))])[axnum] -# else -# res = pix_to_world(w, [float(zero(x)), float(x)])[axnum] -# end - -# vals = converter(res) .- start -# if changing_coord_i < length(units) -# ticklabel = " " * mapreduce(*, zip(vals[changing_coord_i:min(changing_coord_i+1,end)],units[changing_coord_i:min(changing_coord_i+1,end)])) do (val,unit) -# @sprintf("%d%s", val, unit) -# end -# else -# ticklabel = @sprintf( -# "%.2f%s", -# vals[changing_coord_i], -# units[changing_coord_i] -# ) -# end -# return ticklabel -# end -# end -# return label, formatter -# end - -# # Extended form of deg2dms that further returns mas, microas. +# Extended form of deg2dms that further returns mas, microas. function deg2dmsmμ(deg) d,m,s = deg2dms(deg) s_f = floor(s) @@ -336,132 +258,27 @@ const dmsmμ_units = [ "mas", "μas", ] - -# function deg2hmsmμ(deg) -# h,m,s = deg2hms(deg) -# s_f = floor(s) -# mas = (s - s_f)*1e3 -# mas_f = floor(mas) -# μas = (mas - mas_f)*1e3 -# return (h,m,s_f,mas_f,μas) -# end -# const hmsmμ_units = [ -# "ʰ", -# "ᵐ", -# "ˢ", -# # "mas", -# # "μas", -# ] const hms_units = [ "ʰ", "ᵐ", "ˢ", ] - - - - - - -# function pix2world_xformatter(x, wcs::WCSTransform) -# res = round(pix_to_world(wcs, [float(x), float(x)])[1][1], digits=2) -# return string(res, unit2sym(wcs.cunit[1])) -# end -# function pix2world_xformatter(x, wcs::WCSTransform, start) -# x = pix_to_world(wcs, [float(x), float(x)])[1][1] - -# pm = sign(start - x) >= 0 ? '+' : '-' - -# diff_x_d, diff_x_m, diff_x_s = deg2dms(x) .- deg2dms(start) - -# if abs(diff_x_d) > 1 -# return string(pm, abs(round(Int, diff_x_d)), unit2sym("deg")) -# elseif abs(diff_x_m) > 1 -# @show diff_x_m -# return string(pm, abs(round(Int, diff_x_m)), unit2sym("am")) -# elseif abs(diff_x_s) > 1 -# return string(pm, abs(round(Int, diff_x_s)), unit2sym("as")) -# elseif abs(diff_x_s) > 1e-3 -# return string(pm, abs(round(Int, diff_x_s*1e3)), unit2sym("mas")) -# elseif abs(diff_x_s) > 1e-6 -# return string(pm, abs(round(Int, diff_x_s*1e3)), unit2sym("μas")) -# else -# return string(pm, abs(diff_x_s), unit2sym("as")) -# end - - - -# # return string(pm, diff_x_d, diff_x_m, diff_x_s, unit2sym(tickunit)) -# return string(pm, abs(round(Int, diff_x_s)), unit2sym(tickunit)) -# end - -# function pix2world_yformatter(y, wcs::WCSTransform) -# res = round(pix_to_world(wcs, [float(y), float(y)])[2][1], digits=2) -# return string(res, unit2sym(wcs.cunit[1])) -# end -# function pix2world_yformatter(y, wcs::WCSTransform, start, tickdiv, tickunit) -# y = pix_to_world(wcs, [float(y), float(y)])[2][1] -# pm = sign(res) >= 0 ? '+' : '-' -# return string(pm, abs(round(res,digits=2)), unit2sym(tickunit)) -# end - - -# labler_x(wcs) = ctype_x(wcs) -# labler_y(wcs) = ctype_y(wcs) - -# labler_x(wcs, start) = string(ctype_x(wcs), " ", start, unit2sym(wcs.cunit[1])) -# labler_y(wcs, start) = string(ctype_y(wcs), " ", start, unit2sym(wcs.cunit[2])) - -# function unit2sym(unit) -# if unit == "deg" # TODO: add symbols for more units -# "°" -# elseif unit == "am" # TODO: add symbols for more units -# "'" -# elseif unit == "as" # TODO: add symbols for more units -# "\"" -# else -# string(unit) -# end -# end - -# function nextunit(div, unit) -# if unit == "deg" # TODO: add symbols for more units -# return div*60, "am" -# elseif unit == "am" # TODO: add symbols for more units -# return div*60, "as" -# elseif unit == "as" # TODO: add symbols for more units -# return div*60, "mas" -# else -# div, string(unit) -# end -# end - - -function ctype_x(wcs::WCSTransform) - if length(wcs.ctype[1]) == 0 - return wcs.radesys - elseif wcs.ctype[1][1:2] == "RA" - return "Right Ascension (ICRS)" - elseif wcs.ctype[1][1:4] == "GLON" +function ctype_label(ctype,radesys) + if length(ctype) == 0 + return radesys + elseif startswith(ctype, "RA") + return "Right Ascension ($(radesys))" + elseif startswith(ctype, "GLON") return "Galactic Longitude" - elseif wcs.ctype[1][1:4] == "TLON" + elseif startswith(ctype, "TLON") return "ITRS" - else - return wcs.ctype[1] - end -end - -function ctype_y(wcs::WCSTransform) - if length(wcs.ctype[2]) == 0 - return wcs.radesys - elseif wcs.ctype[2][1:3] == "DEC" - return "Declination (ICRS)" - elseif wcs.ctype[2][1:4] == "GLAT" + elseif startswith(ctype, "DEC") + return "Declination ($(radesys))" + elseif startswith(ctype, "GLAT") return "Galactic Latitude" - elseif wcs.ctype[2][1:4] == "TLAT" - return "ITRS" + elseif startswith(ctype, "TLAT") else - return wcs.ctype[2] + return ctype end end diff --git a/src/showmime.jl b/src/showmime.jl index db4a1ebf..b8f18f23 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -221,7 +221,7 @@ function _imview(img, normed::AbstractArray{T}, stretch, cmap) where T end # Flip image to match conventions of other programs - flipped_view = view(mapper', reverse(axes(mapper,1)),:) + flipped_view = view(mapper', reverse(axes(mapper,2)),:) return maybe_copyheaders(img, flipped_view) end From 9e9bd1acffb4db83b5e0e91c87ba4a3a18c93aa6 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 7 Jan 2022 12:23:04 -0800 Subject: [PATCH 033/178] Overhaul of WCS ticks --- src/plot-recipes.jl | 346 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 281 insertions(+), 65 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index beac1723..f92af428 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -84,6 +84,8 @@ end # are correct after applying a non-linear stretch # colorbar := false + # TODO: this wcs flag is currently less than useless. + # we have a wcs flag (from the image by default) so that users can skip over # plotting in physical coordinates. This is especially important # if the WCS headers are mallformed in some way. @@ -91,14 +93,30 @@ end # TODO: fill out coordinates array considering offset indices and slices # out of cubes (tricky!) + + # Note: if the axes are on unusual sides (e.g. y-axis at right, x-axis at top) + # then these coordinates are not correct. They are only correct exactly + # along the axis. + # In astropy, the ticks are actually tilted to reflect this, though in general + # the transformation from pixel to coordinates can be non-linear and curved. + + (;tickpos1x, tickpos1w, tickpos2x, tickpos2w, ) = gen_wcs_grid_ticks(img, (1,2), ones(wcs.naxis)) - xguide, xticks = prepare_label_ticks(img, 1, ones(wcs.naxis)) - xguide := xguide - xticks := xticks + xticks := (tickpos1x, prepare_tick_labels(img, 1, tickpos1x, tickpos1w)) + xguide := ctype_label(wcs.ctype[1], wcs.radesys) + + yticks := (tickpos2x, prepare_tick_labels(img, 2, tickpos2x, tickpos2w)) + yguide := ctype_label(wcs.ctype[2], wcs.radesys) - yguide, yticks = prepare_label_ticks(img, 2, ones(wcs.naxis)) - yguide := yguide - yticks := yticks + # To ensure the physical axis tick labels are correct the axes must be + # tight to the image + xlims := first(axes(img,1)), last(axes(img,1)) + ylims := first(axes(img,2)), last(axes(img,2)) + + # The grid lines are likely to be confusing since they do not follow + # the possibly tilted axes + grid --> false + tickdirection --> :none end # TODO: also disable equal aspect ratio if the scales are totally different @@ -106,14 +124,14 @@ end # We have to do a lot of flipping to keep the orientation corect yflip := false - # return axes(img,2), axes(img,1), view(arraydata(img), reverse(axes(img,1)),:) xflip := false + return axes(img,2), axes(img,1), view(arraydata(img), reverse(axes(img,1)),:) end """ -Calculate good tick positions and nice labels for them +Calculate good tick positions and nice labels for them. INPUT img: an AstroImage @@ -128,16 +146,11 @@ OUTPUT tickpos: tick positions in pixels for this axis ticklabels: tick labels for each position """ -function prepare_label_ticks(img, axnum, coords) - - naxis = wcs(img).naxis - coordsx = convert(Vector{Float64}, coords) - - # coordsw = zeros(eltype(coordsx), size(coordsx)) - coordsw = pix_to_world(wcs(img), coordsx) - - - label = ctype_label(wcs(img).ctype[axnum], wcs(img).radesys) +# Most of the complexity of this function is making sure everything +# generalizes to N different, possiby skewed axes, where a change in +# the opposite coordinate or even an unplotted coordinate affects +# the tick labels. +function prepare_tick_labels(img, axnum, tickposx, tickposw) if wcs(img).cunit[axnum] == "deg" if startswith(uppercase(wcs(img).ctype[axnum]), "RA") @@ -152,57 +165,17 @@ function prepare_label_ticks(img, axnum, coords) units = ("",) end - # w denotes world coordinates along this axis; x denotes pixel coordinates. - # wᵀ denotes world coordinates along the opposite axis. - - # TODO: wrapped around axes... - # Mabye we can detect this by comparing with the minpoint. If they're not monatonic, - # there is a wrap around somewhere. But then what do we do... - - minx = first(axes(img,axnum)) - maxx = last(axes(img,axnum)) - - posx = copy(coordsx) - posx[axnum] = minx - w1 = pix_to_world(wcs(img), posx)[axnum] - posx[axnum] = maxx - w3 = pix_to_world(wcs(img), posx)[axnum] - minw, maxw = extrema((w1,w3)) - - # TODO: May need to rethink this approach in light of coordinates that can wrap around - # Perhaps we can instead choose a phyically relevant step size - - # Use PlotUtils.optimize_ticks to find good tick positions in world coordinates - Q=[(1.0,1.0), (3.0, 0.8), (2.0, 0.7), (5.0, 0.5)] - tickpos_w = optimize_ticks(minw*6, maxw*6; Q, k_min=3, k_ideal=6)[1] - if w1 > w3 - tickpos_w = reverse(tickpos_w) - end - - # Then convert back to pixel coordinates along the axis - tickpos = map(tickpos_w) do w - posw = copy(coordsw) - posw[axnum] = w/6 - # pos[axnumᵀ] = minwᵀ # TODO: should this instead be wᵀ1? - x = world_to_pix(wcs(img), posw)[axnum] - return x - end - # Format inital ticklabel - ticklabels = fill("", length(tickpos)) + ticklabels = fill("", length(tickposx)) # We only include the part of the label that has changed since the last time. # Split up coordinates into e.g. sexagesimal - parts = map(tickpos) do x - posx = copy(coordsx) - posx[axnum] = x - # pos[axnumᵀ] = minxᵀ - w = pix_to_world(wcs(img), posx)[axnum] + parts = map(tickposw) do w vals = converter(w) return vals end # Start with something impossible of the same size: - last_coord = Inf .* converter(minw) + last_coord = Inf .* converter(first(tickposw)) zero_coords_i = maximum(map(parts) do vals changing_coord_i = findfirst(vals .!= last_coord) last_coord = vals @@ -211,10 +184,9 @@ function prepare_label_ticks(img, axnum, coords) # Loop through using only the relevant part of the label # Start with something impossible of the same size: - last_coord = Inf .* converter(minw) + last_coord = Inf .* converter(first(tickposw)) for (i,vals) in enumerate(parts) changing_coord_i = findfirst(vals .!= last_coord) - val_unit_zip = zip(vals[changing_coord_i:zero_coords_i],units[changing_coord_i:zero_coords_i]) ticklabels[i] = mapreduce(*, enumerate(val_unit_zip)) do (coord_i,(val,unit)) # Last coordinate always gets decimal places @@ -233,11 +205,10 @@ function prepare_label_ticks(img, axnum, coords) return str end end - last_coord = vals end - return label, (tickpos, ticklabels) + return ticklabels end @@ -282,3 +253,248 @@ function ctype_label(ctype,radesys) return ctype end end + + +""" + gen_wcs_grid_ticks(img::AstroImage, ax=(1,2), coords=(first(axes(img,ax[1])),first(axes(img,ax[2])))) + +Given an AstroImage, return information necessary to plot WCS gridlines in physical +coordinates against the image's pixel coordinates. +This function has to work on both plotted axes at once to handle rotation and general +curvature of the WCS grid projected on the image coordinates. + +""" +function gen_wcs_grid_ticks(img, ax=(1,2), coords=(first(axes(img,ax[1])),first(axes(img,ax[2]))); color=:black, label="", p=false, line_kwargs...) #pscing in deg + + # x and y denote pixel coordinates (along `ax`), u and v are world coordinates along same? + ax = collect(ax) + coordsx = convert(Vector{Float64}, collect(coords)) + + minx = first(axes(img,ax[1])) + maxx = last(axes(img,ax[1])) + miny = first(axes(img,ax[2])) + maxy = last(axes(img,ax[2])) + + # Find the extent of this slice in world coordinates + posxy = repeat(coordsx, 1, 4) + posxy[ax,1] .= (minx,miny) + posxy[ax,2] .= (minx,maxy) + posxy[ax,3] .= (maxx,miny) + posxy[ax,4] .= (maxx,maxy) + posuv = pix_to_world(wcs(img), posxy) + (minu, maxu), (minv, maxv) = extrema(posuv, dims=2) + + # Find nice grid spacings + Q=[(1.0,1.0), (3.0, 0.8), (2.0, 0.7), (5.0, 0.5)] # dms2deg(0, 0, 20) + tickposu = optimize_ticks(6minu, 6maxu; Q, k_min=5, k_ideal=8, k_max=20)[1]./6 + tickposv = optimize_ticks(6minv, 6maxv; Q, k_min=5, k_ideal=8, k_max=20)[1]./6 + + # In general, grid can be curved when plotted back against the image. + # So we will need to sample multiple points along the grid. + # TODO: find a good heuristic for this based on the curvature. + N_points = 15 + urange = range(minu, maxu, length=N_points) + vrange = range(minv, maxv, length=N_points) + + # # Hold one coordinate constant at a time while tracing the other + # for ticku in tickposu + # # Make sure we handle unplotted slices correctly. + # griduv = repeat(posuv[:,1], 1, N_points) + # griduv[1,:] .= ticku + # griduv[2,:] .= vrange + # posxy = world_to_pix(wcs(img), griduv) + # p = Main.plot!( + # posxy[1,:], + # posxy[2,:]; + # color, + # label, + # line_kwargs... + # ) + # end + + tickpos1x = Float64[] + tickpos1w = Float64[] + gridlinesxy1 = map(tickposu) do ticku + # Make sure we handle unplotted slices correctly. + griduv = repeat(posuv[:,1], 1, N_points) + griduv[ax[1],:] .= ticku + griduv[ax[2],:] .= vrange + posxy = world_to_pix(wcs(img), griduv) + + # Now that we have the grid in pixel coordinates, + # if we find out where the grid intersects the axes we can put + # the labels in the correct spot + + # Find the first and last indices of the grid line that are within the + # plot bounds + in_axes = (minx .<= posxy[ax[1],:] .<= maxx) .& (miny .<= posxy[ax[2],:] .<= maxy) + entered_axes_i = findfirst(in_axes) + exitted_axes_i = findlast(in_axes) + + # From here, do a linear fit to find the intersection with the axis. + # This should be accurate enough as long as N_points is high enough + # that the curvature of the grid is smooth by eye. + # y=mx+b + m1 = (posxy[ax[2],entered_axes_i+1] - posxy[ax[2],entered_axes_i])/ + (posxy[ax[1],entered_axes_i+1] - posxy[ax[1],entered_axes_i]) + b1 = posxy[ax[2],entered_axes_i] - m1*posxy[ax[1],entered_axes_i] + # Find the coordinate of maxy so that we don't run over the top axis + x_maxy = (maxy-b1)/m1 + if x_maxy > maxx + # We never hit the axis + x1 = maxx + else + x1 = x_maxy + end + # Now extrapolate the line + y = m1*(x1)+b1 + point_entered = [ + x1 + y + ] + + # Now do where the lines exit the plot + m2 = (posxy[ax[2],exitted_axes_i] - posxy[ax[2],exitted_axes_i-1])/ + (posxy[ax[1],exitted_axes_i] - posxy[ax[1],exitted_axes_i-1]) + b2 = posxy[ax[2],exitted_axes_i] - m2*posxy[ax[1],exitted_axes_i] + # Find the coordinate of maxy so that we don't run below the bottom axis + x_miny = (miny-b2)/m2 + if x_miny < minx + # We never hit the axis + x2 = minx + else + x2 = x_miny + end + # Now extrapolate the line + y = m2*(x2)+b2 + + point_exitted = [ + x2 + y + ] + if minx <= x_miny <= maxx + push!(tickpos1x, x2) + push!(tickpos1w, ticku) + end + # Chop off the lines to be inside the plot and then put + # out new intercept points back in + + posxy_neat = [point_entered posxy[:,entered_axes_i:exitted_axes_i] point_exitted] + # TODO: other axes also need a fit? + + if p + Main.plot!( + posxy_neat[ax[1],:], + posxy_neat[ax[2],:]; + color, + label, + line_kwargs... + ) + end + + # TODO: one option could be to place grid labels where they exit the + # plot. This gets around the tilted ticks issue + + gridlinexy = ( + posxy_neat[ax[1],:], + posxy_neat[ax[2],:] + ) + return gridlinexy + end + # Then do the opposite coordinate + + tickpos2x = Float64[] + tickpos2w = Float64[] + gridlinesxy2 = map(tickposv) do tickv + # Make sure we handle unplotted slices correctly. + griduv = repeat(posuv[:,1], 1, N_points) + griduv[ax[1],:] .= urange + griduv[ax[2],:] .= tickv + posxy = world_to_pix(wcs(img), griduv) + + # Now that we have the grid in pixel coordinates, + # if we find out where the grid intersects the axes we can put + # the labels in the correct spot + + # Find the first and last indices of the grid line that are within the + # plot bounds + in_axes = (minx .<= posxy[ax[1],:] .<= maxx) .& (miny .<= posxy[ax[2],:] .<= maxy) + entered_axes_i = findfirst(in_axes) + exitted_axes_i = findlast(in_axes) + + # From here, do a linear fit to find the intersection with the axis. + # This should be accurate enough as long as N_points is high enough + # that the curvature of the grid is smooth by eye. + # y=mx+b + m1 = (posxy[ax[2],entered_axes_i+1] - posxy[ax[2],entered_axes_i])/ + (posxy[ax[1],entered_axes_i+1] - posxy[ax[1],entered_axes_i]) + b1 = posxy[ax[2],entered_axes_i] - m1*posxy[ax[1],entered_axes_i] + # Find the coordinate of maxy so that we don't run over the top axis + x_maxy = (maxy-b1)/m1 + # TODO: both side comparison + if x_maxy > minx + # We never hit the axis + x1 = x_maxy + else + x1 = minx + end + # Now extrapolate the line + y = m1*(x1)+b1 + if x_maxy < minx + push!(tickpos2x, y) + push!(tickpos2w, tickv) + end + point_entered = [ + x1 + y + ] + + # Now do where the lines exit the plot + m2 = (posxy[ax[2],exitted_axes_i] - posxy[ax[2],exitted_axes_i-1])/ + (posxy[ax[1],exitted_axes_i] - posxy[ax[1],exitted_axes_i-1]) + b2 = posxy[ax[2],exitted_axes_i] - m2*posxy[ax[1],exitted_axes_i] + # Find the coordinate of maxy so that we don't run below the bottom axis + x_miny = (miny-b2)/m2 + if x_miny > maxx + # We never hit the axis + x2 = maxx + else + x2 = x_miny + end + # Now extrapolate the line + y = m2*(x2)+b2 + + point_exitted = [ + x2 + y + ] + # Chop off the lines to be inside the plot and then put + # out new intercept points back in + + posxy_neat = [point_entered posxy[:,entered_axes_i:exitted_axes_i] point_exitted] + # TODO: other axes also need a fit? + + if p + Main.plot!( + posxy_neat[ax[1],:], + posxy_neat[ax[2],:]; + color, + label, + line_kwargs... + ) + end + + # TODO: one option could be to place grid labels where they exit the + # plot. This gets around the tilted ticks issue + + gridlinexy = ( + posxy_neat[ax[1],:], + posxy_neat[ax[2],:] + ) + return gridlinexy + end + + return (;gridlinesxy1, gridlinesxy2, tickpos1x, tickpos1w, tickpos2x, tickpos2w) + +end +export gen_wcs_grid_ticks \ No newline at end of file From 07fdbe052fcd137de881c71cea60130175c507d5 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 7 Jan 2022 16:54:03 -0800 Subject: [PATCH 034/178] Refactor to create WCS Grid struct --- src/AstroImages.jl | 49 ++++++++-- src/ccd2rgb.jl | 5 +- src/plot-recipes.jl | 223 ++++++++++++++++++++++++++++++-------------- src/showmime.jl | 70 +++++++------- 4 files changed, 234 insertions(+), 113 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index fdc7aeb3..56aaba88 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -3,9 +3,20 @@ __precompile__() module AstroImages using FITSIO, FileIO, Images, Interact, Reproject, WCS, MappedArrays +using Statistics +using MappedArrays +using ColorSchemes +using PlotUtils: zscale export load, AstroImage, ccd2rgb, set_brightness!, set_contrast!, add_label!, reset! +export zscale, percent +export logstretch, powstretch, sqrtstretch, squarestretch, asinhstretch, sinhstretch, powerdiststretch + +export imview +export clampednormedview + + _load(fits::FITS, ext::Int) = read(fits[ext]) # _load(fits::FITS, ext::NTuple{N, Int}) where {N} = ntuple(i-> read(fits[ext[i]]), N) # # _load(fits::NTuple{N, FITS}, ext::NTuple{N, Int}) where {N} = ntuple(i -> _load(fits[i], ext[i]), N) @@ -30,6 +41,16 @@ function FileIO.load(f::File{format"FITS"}, ext::Int=1) end export load, save +# using UUIDs +# del_format(format"FITS") +# add_format(format"FITS", +# # See https://www.loc.gov/preservation/digital/formats/fdd/fdd000317.shtml#sign +# [0x53,0x49,0x4d,0x50,0x4c,0x45,0x20,0x20,0x3d,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x54], +# [".fit", ".fits", ".fts", ".FIT", ".FITS", ".FTS"], +# [:FITSIO => UUID("525bcba6-941b-5504-bd06-fd0dc1a4d2eb")], +# [:AstroImages => UUID("fe3fc30c-9b16-11e9-1c73-17dabf39f4ad")] +# ) + # function FileIO.load(f::File{format"FITS"}, ext::NTuple{N,Int}) where {N} # fits = FITS(f.filename) # out = _load(fits, ext) @@ -171,6 +192,7 @@ function Base.getindex(img::AstroImage, inds...) return copyheaders(img, dat) end end +Base.getindex(img::AstroImage{T}, inds...) where {T<:Colorant} = getindex(arraydata(img), inds...) Base.setindex!(img::AstroImage, v, inds...) = setindex!(arraydata(img), v, inds...) # default fallback for operations on Array # Getting and setting comments @@ -418,15 +440,30 @@ AstroImage(data::AbstractArray, wcs::WCSTransform) = AstroImage(data, emptyheade Helper function to create a WCSTransform from an array and FITSHeaders. """ -function wcsfromheaders(img::AstroImage; relax=WCS.HDR_ALL, ignore_rejected=true) +function wcsfromheaders(img::AstroImage; relax=WCS.HDR_ALL) # We only need to stringify WCS headers. This might just be 4-10 header keywords # out of thousands. # wcsout = WCS.from_header(string(filterwcsheaders(headers(img))), ignore_rejected=true) - wcsout = WCS.from_header( - string(headers(img)); - ignore_rejected, - relax - ) + local wcsout + # Load the headers without ignoring rejected to get error messages + try + wcsout = WCS.from_header( + string(headers(img)); + ignore_rejected=false, + relax + ) + catch err + # Load them again ignoring error messages + wcsout = WCS.from_header( + string(headers(img)); + ignore_rejected=true, + relax + ) + # If that still fails, the use gets the stack trace here + # If not, print a warning about rejected headers + @warn "WCSTransform was generated by ignoring rejected headers. It may not be valid." exception=err + end + if length(wcsout) == 1 return only(wcsout) elseif length(wcsout) == 0 diff --git a/src/ccd2rgb.jl b/src/ccd2rgb.jl index 99980661..15cbe528 100644 --- a/src/ccd2rgb.jl +++ b/src/ccd2rgb.jl @@ -100,7 +100,10 @@ function composechannels( mapped = T.(mapped) mapped[isnan.(mapped)] .= RGBA(0,0,0,0) - return maybe_copyheaders(first(images), mapped') + # Flip image to match conventions of other programs + flipped_view = view(mapped', reverse(axes(mapped,2)),:) + + return maybe_copyheaders(first(images), flipped_view) # return (reprojected .* multipliers .* colors) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index f92af428..ec587236 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -100,23 +100,26 @@ end # In astropy, the ticks are actually tilted to reflect this, though in general # the transformation from pixel to coordinates can be non-linear and curved. - (;tickpos1x, tickpos1w, tickpos2x, tickpos2w, ) = gen_wcs_grid_ticks(img, (1,2), ones(wcs.naxis)) + # (;tickpos1x, tickpos1w, tickpos2x, tickpos2w, ) + wcsax = WCSGrid6(img, (1,2)) + + gridspec = wcsgridspec(wcsax) - xticks := (tickpos1x, prepare_tick_labels(img, 1, tickpos1x, tickpos1w)) - xguide := ctype_label(wcs.ctype[1], wcs.radesys) + xticks --> (gridspec.tickpos1x, prepare_tick_labels(wcs, 1, gridspec)) + xguide --> ctype_label(wcs.ctype[1], wcs.radesys) - yticks := (tickpos2x, prepare_tick_labels(img, 2, tickpos2x, tickpos2w)) - yguide := ctype_label(wcs.ctype[2], wcs.radesys) + yticks --> (gridspec.tickpos2x, prepare_tick_labels(wcs, 2, gridspec)) + yguide --> ctype_label(wcs.ctype[2], wcs.radesys) # To ensure the physical axis tick labels are correct the axes must be # tight to the image - xlims := first(axes(img,1)), last(axes(img,1)) - ylims := first(axes(img,2)), last(axes(img,2)) + xlims := first(axes(img,2)), last(axes(img,2)) + ylims := first(axes(img,1)), last(axes(img,1)) # The grid lines are likely to be confusing since they do not follow # the possibly tilted axes - grid --> false - tickdirection --> :none + grid := false + tickdirection := :none end # TODO: also disable equal aspect ratio if the scales are totally different @@ -131,10 +134,10 @@ end """ -Calculate good tick positions and nice labels for them. +Generate nice labels from a WCSTransform, axis, and known positions. INPUT -img: an AstroImage +w: a WCSTransform axnum: the index of the axis we want ticks for axnumᵀ: the index of the axis we are plotting against coords: the position in all coordinates for this plot. The value a axnum and axnumᵀ is igored. @@ -150,10 +153,21 @@ ticklabels: tick labels for each position # generalizes to N different, possiby skewed axes, where a change in # the opposite coordinate or even an unplotted coordinate affects # the tick labels. -function prepare_tick_labels(img, axnum, tickposx, tickposw) +function prepare_tick_labels(w::WCSTransform, axnum, gridspec)#tickposx, tickposw) + + # TODO: sort out axnum stuff + tickposx = axnum == 1 ? gridspec.tickpos1x : gridspec.tickpos2x + tickposw = axnum == 1 ? gridspec.tickpos1w : gridspec.tickpos2w - if wcs(img).cunit[axnum] == "deg" - if startswith(uppercase(wcs(img).ctype[axnum]), "RA") + if length(tickposw) != length(tickposx) + error("Tick position vectors are of different length") + end + if length(tickposx) == 0 + return String[] + end + + if w.cunit[axnum] == "deg" + if startswith(uppercase(w.ctype[axnum]), "RA") converter = deg2hms units = hms_units else @@ -255,8 +269,27 @@ function ctype_label(ctype,radesys) end +# struct WCSGrid6 +# w +# extent +# gridlinesxy1 +# gridlinesxy2 +# tickpos1x +# tickpos1w +# tickslopes1x +# tickpos2x +# tickpos2w +# tickslopes2x +# end +struct WCSGrid6 + w + extent + ax + coords +end + """ - gen_wcs_grid_ticks(img::AstroImage, ax=(1,2), coords=(first(axes(img,ax[1])),first(axes(img,ax[2])))) + WCSGrid6(img::AstroImage, ax=(1,2), coords=(first(axes(img,ax[1])),first(axes(img,ax[2])))) Given an AstroImage, return information necessary to plot WCS gridlines in physical coordinates against the image's pixel coordinates. @@ -264,16 +297,25 @@ This function has to work on both plotted axes at once to handle rotation and ge curvature of the WCS grid projected on the image coordinates. """ -function gen_wcs_grid_ticks(img, ax=(1,2), coords=(first(axes(img,ax[1])),first(axes(img,ax[2]))); color=:black, label="", p=false, line_kwargs...) #pscing in deg - - # x and y denote pixel coordinates (along `ax`), u and v are world coordinates along same? - ax = collect(ax) - coordsx = convert(Vector{Float64}, collect(coords)) +function WCSGrid6(img::AstroImage, ax=(1,2)) minx = first(axes(img,ax[1])) maxx = last(axes(img,ax[1])) miny = first(axes(img,ax[2])) maxy = last(axes(img,ax[2])) + extent = (minx, maxx, miny, maxy) + + return WCSGrid6(wcs(img), extent, ax, (extent[1], extent[3])) +end + +function wcsgridspec(wsg::WCSGrid6) +# function wcsgridspec(w::WCSTransform, extent, ax=(1,2), coords=(extent[1], extent[3]))#coords=(first(axes(img,ax[1])),first(axes(img,ax[2])))) + + # x and y denote pixel coordinates (along `ax`), u and v are world coordinates along same? + ax = collect(wsg.ax) + coordsx = convert(Vector{Float64}, collect(wsg.coords)) + + minx, maxx, miny, maxy = wsg.extent # Find the extent of this slice in world coordinates posxy = repeat(coordsx, 1, 4) @@ -281,13 +323,17 @@ function gen_wcs_grid_ticks(img, ax=(1,2), coords=(first(axes(img,ax[1])),first( posxy[ax,2] .= (minx,maxy) posxy[ax,3] .= (maxx,miny) posxy[ax,4] .= (maxx,maxy) - posuv = pix_to_world(wcs(img), posxy) + posuv = pix_to_world(wsg.w, posxy) (minu, maxu), (minv, maxv) = extrema(posuv, dims=2) # Find nice grid spacings + # These heuristics can probably be improved Q=[(1.0,1.0), (3.0, 0.8), (2.0, 0.7), (5.0, 0.5)] # dms2deg(0, 0, 20) - tickposu = optimize_ticks(6minu, 6maxu; Q, k_min=5, k_ideal=8, k_max=20)[1]./6 - tickposv = optimize_ticks(6minv, 6maxv; Q, k_min=5, k_ideal=8, k_max=20)[1]./6 + k_min = 4 + k_ideal = 8 + k_max = 20 + tickposu = optimize_ticks(6minu, 6maxu; Q, k_min, k_ideal, k_max)[1]./6 + tickposv = optimize_ticks(6minv, 6maxv; Q, k_min, k_ideal, k_max)[1]./6 # In general, grid can be curved when plotted back against the image. # So we will need to sample multiple points along the grid. @@ -296,30 +342,15 @@ function gen_wcs_grid_ticks(img, ax=(1,2), coords=(first(axes(img,ax[1])),first( urange = range(minu, maxu, length=N_points) vrange = range(minv, maxv, length=N_points) - # # Hold one coordinate constant at a time while tracing the other - # for ticku in tickposu - # # Make sure we handle unplotted slices correctly. - # griduv = repeat(posuv[:,1], 1, N_points) - # griduv[1,:] .= ticku - # griduv[2,:] .= vrange - # posxy = world_to_pix(wcs(img), griduv) - # p = Main.plot!( - # posxy[1,:], - # posxy[2,:]; - # color, - # label, - # line_kwargs... - # ) - # end - tickpos1x = Float64[] tickpos1w = Float64[] + tickslopes1x = Float64[] gridlinesxy1 = map(tickposu) do ticku # Make sure we handle unplotted slices correctly. griduv = repeat(posuv[:,1], 1, N_points) griduv[ax[1],:] .= ticku griduv[ax[2],:] .= vrange - posxy = world_to_pix(wcs(img), griduv) + posxy = world_to_pix(wsg.w, griduv) # Now that we have the grid in pixel coordinates, # if we find out where the grid intersects the axes we can put @@ -355,7 +386,7 @@ function gen_wcs_grid_ticks(img, ax=(1,2), coords=(first(axes(img,ax[1])),first( # Now do where the lines exit the plot m2 = (posxy[ax[2],exitted_axes_i] - posxy[ax[2],exitted_axes_i-1])/ - (posxy[ax[1],exitted_axes_i] - posxy[ax[1],exitted_axes_i-1]) + (posxy[ax[1],exitted_axes_i] - posxy[ax[1],exitted_axes_i-1]) b2 = posxy[ax[2],exitted_axes_i] - m2*posxy[ax[1],exitted_axes_i] # Find the coordinate of maxy so that we don't run below the bottom axis x_miny = (miny-b2)/m2 @@ -375,25 +406,13 @@ function gen_wcs_grid_ticks(img, ax=(1,2), coords=(first(axes(img,ax[1])),first( if minx <= x_miny <= maxx push!(tickpos1x, x2) push!(tickpos1w, ticku) + push!(tickslopes1x, m2) end # Chop off the lines to be inside the plot and then put # out new intercept points back in posxy_neat = [point_entered posxy[:,entered_axes_i:exitted_axes_i] point_exitted] - # TODO: other axes also need a fit? - - if p - Main.plot!( - posxy_neat[ax[1],:], - posxy_neat[ax[2],:]; - color, - label, - line_kwargs... - ) - end - - # TODO: one option could be to place grid labels where they exit the - # plot. This gets around the tilted ticks issue + # TODO: do unplotted other axes also need a fit? gridlinexy = ( posxy_neat[ax[1],:], @@ -405,12 +424,13 @@ function gen_wcs_grid_ticks(img, ax=(1,2), coords=(first(axes(img,ax[1])),first( tickpos2x = Float64[] tickpos2w = Float64[] + tickslopes2x = Float64[] gridlinesxy2 = map(tickposv) do tickv # Make sure we handle unplotted slices correctly. griduv = repeat(posuv[:,1], 1, N_points) griduv[ax[1],:] .= urange griduv[ax[2],:] .= tickv - posxy = world_to_pix(wcs(img), griduv) + posxy = world_to_pix(wsg.w, griduv) # Now that we have the grid in pixel coordinates, # if we find out where the grid intersects the axes we can put @@ -443,6 +463,7 @@ function gen_wcs_grid_ticks(img, ax=(1,2), coords=(first(axes(img,ax[1])),first( if x_maxy < minx push!(tickpos2x, y) push!(tickpos2w, tickv) + push!(tickslopes2x, m1) end point_entered = [ x1 @@ -473,16 +494,6 @@ function gen_wcs_grid_ticks(img, ax=(1,2), coords=(first(axes(img,ax[1])),first( posxy_neat = [point_entered posxy[:,entered_axes_i:exitted_axes_i] point_exitted] # TODO: other axes also need a fit? - - if p - Main.plot!( - posxy_neat[ax[1],:], - posxy_neat[ax[2],:]; - color, - label, - line_kwargs... - ) - end # TODO: one option could be to place grid labels where they exit the # plot. This gets around the tilted ticks issue @@ -494,7 +505,83 @@ function gen_wcs_grid_ticks(img, ax=(1,2), coords=(first(axes(img,ax[1])),first( return gridlinexy end - return (;gridlinesxy1, gridlinesxy2, tickpos1x, tickpos1w, tickpos2x, tickpos2w) + # return WCSGrid6(w, extent, gridlinesxy1, gridlinesxy2, tickpos1x, tickpos1w, tickslopes1x, tickpos2x, tickpos2w, tickslopes2x) + return (;gridlinesxy1, gridlinesxy2, tickpos1x, tickpos1w, tickslopes1x, tickpos2x, tickpos2w, tickslopes2x) +end +export WCSGrid6 + + +# TODO: the wcs parameter is not getting forwardded correctly. Use plot recipe system for this. + +# This recipe plots as AstroImage of color data as an image series (not heatmap). +# This lets us also plot color composites e.g. in WCS coordinates. +@recipe function f(wcsax::WCSGrid6) + color --> :black # Is there a way to get the foreground color automatically? + label --> "" + + gridspec = wcsgridspec(wcsax) + + # Unroll grid lines into a single series separated by NaNs + xs1 = mapreduce(vcat, gridspec.gridlinesxy1) do gridline + return vcat(gridline[1], NaN) + end + ys1 = mapreduce(vcat, gridspec.gridlinesxy1) do gridline + return vcat(gridline[2], NaN) + end + xs2 = mapreduce(vcat, gridspec.gridlinesxy2) do gridline + return vcat(gridline[1], NaN) + end + ys2 = mapreduce(vcat, gridspec.gridlinesxy2) do gridline + return vcat(gridline[2], NaN) + end + + xs = vcat(xs1, NaN, xs2) + ys = vcat(ys1, NaN, ys2) + + # We can optionally annotate the grid with their coordinates + + # @series begin + # ticklabels = prepare_tick_labels(gridspec.w, 2, gridspec.tickpos2x, gridspec.tickpos2w) + # rotations = atand.(gridspec.tickslopes2x, 1) + # series_annotations := [ + # Main.Plots.text(" $l", :left, :bottom, :white, 8; rotation) + # for (l, rotation) in zip(ticklabels, rotations) + # ] + # ones(length(gridspec.tickpos2x)), gridspec.tickpos2x + # end + + # @series begin + # ticklabels = prepare_tick_labels(gridspec.w, 1, gridspec.tickpos1x, gridspec.tickpos1w) + # rotations = atand.(gridspec.tickslopes1x, 1) + # series_annotations := [ + # Main.Plots.text(" $l", :left, :bottom, :white, 8; rotation) + # for (l, rotation) in zip(ticklabels, rotations) + # ] + # gridspec.tickpos1x, ones(length(gridspec.tickpos1x)) + # end + + if haskey(plotattributes, :annotategrid) && plotattributes[:annotategrid] + @series begin + ticklabels = prepare_tick_labels(gridspec.w, 2, gridspec.tickpos2x, gridspec.tickpos2w) + rotations = atand.(gridspec.tickslopes2x, 1) + series_annotations := [ + Main.Plots.text(" $l", :left, :bottom, :white, 8; rotation) + for (l, rotation) in zip(ticklabels, rotations) + ] + ones(length(gridspec.tickpos2x)), gridspec.tickpos2x + end + + @series begin + ticklabels = prepare_tick_labels(gridspec.w, 1, gridspec.tickpos1x, gridspec.tickpos1w) + rotations = atand.(gridspec.tickslopes1x, 1) + series_annotations := [ + Main.Plots.text(" $l", :left, :bottom, :white, 8; rotation) + for (l, rotation) in zip(ticklabels, rotations) + ] + gridspec.tickpos1x, ones(length(gridspec.tickpos1x)) + end + end + + return xs, ys end -export gen_wcs_grid_ticks \ No newline at end of file diff --git a/src/showmime.jl b/src/showmime.jl index b8f18f23..e91e5440 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -30,14 +30,40 @@ Base.show(io::IO, mime::MIME"image/png", img::AstroImage{T,2}; kwargs...) where Base.show(io::IO, mime::MIME"image/png", img::AstroImage{T,2}; kwargs...) where {T} = show(io, mime, imview(img), kwargs...) -using Statistics -using MappedArrays -using ColorSchemes -using PlotUtils: zscale -export zscale + + + +# These reproduce the behaviour of DS9 according to http://ds9.si.edu/doc/ref/how.html +logstretch(x,a=1000) = log(a*x+1)/log(a) +powstretch(x,a=1000) = (a^x - 1)/a +sqrtstretch = sqrt +squarestretch(x) = x^2 +asinhstretch(x) = asinh(10x)/3 +sinhstretch(x) = sinh(3x)/10 +# These additional stretches reproduce behaviour from astropy +powerdiststretch(x, a=1000) = (a^x - 1) / (a - 1) + +""" + percent(99.5) + +Returns a function that calculates display limits that include the given +percent of the image data. + +Example: +```julia +julia> imview(img, clims=percent(90)) +``` +This will set the limits to be the 5th percentile to the 95th percentile. +""" +function percent(perc::Number) + trim = (1 - perc/100)/2 + clims(data) = quantile(data, (trim, 1-trim)) + clims(data::AbstractMatrix) = quantile(vec(data), (trim, 1-trim)) + return clims +end const _default_cmap = Ref{Union{Symbol,Nothing}}(nothing) -const _default_clims = Ref{Any}(extrema) +const _default_clims = Ref{Any}(percent(99.5)) const _default_stretch = Ref{Any}(identity) """ @@ -78,17 +104,6 @@ Helper to iterate over data skipping missing and non-finite values. """ skipmissingnan(itr) = Iterators.filter(el->!ismissing(el) && isfinite(el), itr) -# These reproduce the behaviour of DS9 according to http://ds9.si.edu/doc/ref/how.html -logstretch(x,a=1000) = log(a*x+1)/log(a) -powstretch(x,a=1000) = (a^x - 1)/a -sqrtstretch = sqrt -squarestretch(x) = x^2 -asinhstretch(x) = asinh(10x)/3 -sinhstretch(x) = sinh(3x)/10 -# These additional stretches reproduce behaviour from astropy -powerdiststretch(x, a=1000) = (a^x - 1) / (a - 1) -export logstretch, powstretch, sqrtstretch, squarestretch, asinhstretch, sinhstretch, powerdiststretch - """ imview(img; clims=extrema, stretch=identity, cmap=nothing) @@ -225,27 +240,8 @@ function _imview(img, normed::AbstractArray{T}, stretch, cmap) where T return maybe_copyheaders(img, flipped_view) end -export imview -""" - percent(99.5) -Returns a function that calculates display limits that include the given -percent of the image data. - -Example: -```julia -julia> imview(img, clims=percent(90)) -``` -This will set the limits to be the 5th percentile to the 95th percentile. -""" -function percent(perc::Number) - trim = (1 - perc/100)/2 - clims(data) = quantile(data, (trim, 1-trim)) - clims(data::AbstractMatrix) = quantile(vec(data), (trim, 1-trim)) - return clims -end -export percent # TODO: is this the correct function to extend? # Instead of using a datatype like N0f32 to interpret integers as fixed point values in [0,1], @@ -261,7 +257,6 @@ function Images.normedview(img::AstroImage{T}) where T ) return shareheaders(img, normeddata) end -export normedview """ clampednormedview(arr, (min, max)) @@ -300,7 +295,6 @@ end function clampednormedview(img::AbstractArray{Bool}, lims) return img end -export clampednormedview # Lazily reinterpret the AstroImage as a Matrix{Color}, upon request. # By itself, Images.colorview works fine on AstroImages. But From 747af1a196233ef6c3cb8a6288df4107932a9732 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sat, 8 Jan 2022 08:24:12 -0800 Subject: [PATCH 035/178] edge cases 1 --- src/plot-recipes.jl | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index ec587236..51c89ee3 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -89,7 +89,7 @@ end # we have a wcs flag (from the image by default) so that users can skip over # plotting in physical coordinates. This is especially important # if the WCS headers are mallformed in some way. - if !isnothing(wcs) + if !haskey(plotattributes, :wcs) || plotattributes[:wcs] # TODO: fill out coordinates array considering offset indices and slices # out of cubes (tricky!) @@ -345,7 +345,8 @@ function wcsgridspec(wsg::WCSGrid6) tickpos1x = Float64[] tickpos1w = Float64[] tickslopes1x = Float64[] - gridlinesxy1 = map(tickposu) do ticku + gridlinesxy1 = NTuple{2,Vector{Float64}}[] + for ticku in tickposu # Make sure we handle unplotted slices correctly. griduv = repeat(posuv[:,1], 1, N_points) griduv[ax[1],:] .= ticku @@ -361,6 +362,9 @@ function wcsgridspec(wsg::WCSGrid6) in_axes = (minx .<= posxy[ax[1],:] .<= maxx) .& (miny .<= posxy[ax[2],:] .<= maxy) entered_axes_i = findfirst(in_axes) exitted_axes_i = findlast(in_axes) + if count(in_axes) <= 1 || isnothing(entered_axes_i) || isnothing(exitted_axes_i) + continue + end # From here, do a linear fit to find the intersection with the axis. # This should be accurate enough as long as N_points is high enough @@ -418,14 +422,16 @@ function wcsgridspec(wsg::WCSGrid6) posxy_neat[ax[1],:], posxy_neat[ax[2],:] ) - return gridlinexy + push!(gridlinesxy1, gridlinexy) end # Then do the opposite coordinate tickpos2x = Float64[] tickpos2w = Float64[] tickslopes2x = Float64[] - gridlinesxy2 = map(tickposv) do tickv + gridlinesxy2 = NTuple{2,Vector{Float64}}[] + for tickv in tickposv + # Make sure we handle unplotted slices correctly. griduv = repeat(posuv[:,1], 1, N_points) griduv[ax[1],:] .= urange @@ -441,6 +447,9 @@ function wcsgridspec(wsg::WCSGrid6) in_axes = (minx .<= posxy[ax[1],:] .<= maxx) .& (miny .<= posxy[ax[2],:] .<= maxy) entered_axes_i = findfirst(in_axes) exitted_axes_i = findlast(in_axes) + if isnothing(entered_axes_i) || isnothing(exitted_axes_i) + continue + end # From here, do a linear fit to find the intersection with the axis. # This should be accurate enough as long as N_points is high enough @@ -502,7 +511,7 @@ function wcsgridspec(wsg::WCSGrid6) posxy_neat[ax[1],:], posxy_neat[ax[2],:] ) - return gridlinexy + push!(gridlinesxy2, gridlinexy) end # return WCSGrid6(w, extent, gridlinesxy1, gridlinesxy2, tickpos1x, tickpos1w, tickslopes1x, tickpos2x, tickpos2w, tickslopes2x) From 4be5a04dfd352ee698dc454d43733a60a28463e6 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sat, 8 Jan 2022 10:16:29 -0800 Subject: [PATCH 036/178] corner cases 2 --- src/plot-recipes.jl | 126 ++++++++++++++++++++++---------------------- 1 file changed, 64 insertions(+), 62 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 51c89ee3..b351111b 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -16,7 +16,7 @@ Convenient functions to use for `clims` are: `extrema`, `zscale`, and `percent(p)` Next, the data is rescaled to [0,1] and remapped according to the function `stretch`. -Stretch can be any monotonic fuction mapping values in the range [0,1] to some range [a,b]. +Stretch can be any monotonic function mapping values in the range [0,1] to some range [a,b]. Note that `log(0)` is not defined so is not directly supported. For a list of convenient stretch functions, see: `logstretch`, `powstretch`, `squarestretch`, `asinhstretch`, `sinhstretch`, `powerdiststretch` @@ -62,7 +62,6 @@ save("output.png", v) clims=_default_clims[], stretch=_default_stretch[], cmap=_default_cmap[], - wcs=AstroImages.wcs(img) ) where {T<:Number} # We currently use the AstroImages defaults. If unset, we could # instead follow the plot theme. @@ -76,7 +75,6 @@ end # This lets us also plot color composites e.g. in WCS coordinates. @recipe function f( img::AstroImage{T}; - wcs=AstroImages.wcs(img) ) where {T<:Colorant} # By default, disable the colorbar. @@ -84,8 +82,6 @@ end # are correct after applying a non-linear stretch # colorbar := false - # TODO: this wcs flag is currently less than useless. - # we have a wcs flag (from the image by default) so that users can skip over # plotting in physical coordinates. This is especially important # if the WCS headers are mallformed in some way. @@ -101,18 +97,20 @@ end # the transformation from pixel to coordinates can be non-linear and curved. # (;tickpos1x, tickpos1w, tickpos2x, tickpos2w, ) - wcsax = WCSGrid6(img, (1,2)) + wcsg = WCSGrid6(img, (1,2)) - gridspec = wcsgridspec(wcsax) + gridspec = wcsgridspec(wcsg) - xticks --> (gridspec.tickpos1x, prepare_tick_labels(wcs, 1, gridspec)) - xguide --> ctype_label(wcs.ctype[1], wcs.radesys) + # xticks --> (gridspec.tickpos1x, wcsticks(wcs(img), 1, gridspec)) + xticks --> wcsticks(wcsg, 1, gridspec) + xguide --> ctype_label(wcs(img).ctype[1], wcs(img).radesys) - yticks --> (gridspec.tickpos2x, prepare_tick_labels(wcs, 2, gridspec)) - yguide --> ctype_label(wcs.ctype[2], wcs.radesys) + # yticks --> (gridspec.tickpos2x, wcsticks(wcs(img), 2, gridspec)) + yticks --> wcsticks(wcsg, 2, gridspec) + yguide --> ctype_label(wcs(img).ctype[2], wcs(img).radesys) # To ensure the physical axis tick labels are correct the axes must be - # tight to the image + # # tight to the image xlims := first(axes(img,2)), last(axes(img,2)) ylims := first(axes(img,1)), last(axes(img,1)) @@ -153,7 +151,9 @@ ticklabels: tick labels for each position # generalizes to N different, possiby skewed axes, where a change in # the opposite coordinate or even an unplotted coordinate affects # the tick labels. -function prepare_tick_labels(w::WCSTransform, axnum, gridspec)#tickposx, tickposw) +wcsticks(img::AstroImage, axnum) = wcsticks(WCSGrid6(img), axnum) +function wcsticks(wcsg::WCSGrid6, axnum, gridspec=wcsgridspec(wcsg))#tickposx, tickposw) + w = wcsg.w # TODO: sort out axnum stuff tickposx = axnum == 1 ? gridspec.tickpos1x : gridspec.tickpos2x @@ -163,7 +163,7 @@ function prepare_tick_labels(w::WCSTransform, axnum, gridspec)#tickposx, tickpos error("Tick position vectors are of different length") end if length(tickposx) == 0 - return String[] + return similar(tickposx, 0), String[] end if w.cunit[axnum] == "deg" @@ -222,10 +222,9 @@ function prepare_tick_labels(w::WCSTransform, axnum, gridspec)#tickposx, tickpos last_coord = vals end - return ticklabels - + return tickposx, ticklabels end - +export wcsticks # Extended form of deg2dms that further returns mas, microas. function deg2dmsmμ(deg) @@ -338,7 +337,7 @@ function wcsgridspec(wsg::WCSGrid6) # In general, grid can be curved when plotted back against the image. # So we will need to sample multiple points along the grid. # TODO: find a good heuristic for this based on the curvature. - N_points = 15 + N_points = 30 urange = range(minu, maxu, length=N_points) vrange = range(minv, maxv, length=N_points) @@ -416,6 +415,7 @@ function wcsgridspec(wsg::WCSGrid6) # out new intercept points back in posxy_neat = [point_entered posxy[:,entered_axes_i:exitted_axes_i] point_exitted] + # posxy_neat = posxy # TODO: do unplotted other axes also need a fit? gridlinexy = ( @@ -459,17 +459,22 @@ function wcsgridspec(wsg::WCSGrid6) (posxy[ax[1],entered_axes_i+1] - posxy[ax[1],entered_axes_i]) b1 = posxy[ax[2],entered_axes_i] - m1*posxy[ax[1],entered_axes_i] # Find the coordinate of maxy so that we don't run over the top axis - x_maxy = (maxy-b1)/m1 - # TODO: both side comparison - if x_maxy > minx - # We never hit the axis - x1 = x_maxy + if (posxy[ax[1],entered_axes_i+1]-minx)^2 < (posxy[ax[1],entered_axes_i+1]-maxx)^2 + x1 = (maxy-b1)/m1 else - x1 = minx + x1 = (miny-b1)/m1 end + println() + @show x1 + should_label = false + if x1 <= miny + should_label = true + end + x1 = clamp(x1, miny, maxy) # Now extrapolate the line y = m1*(x1)+b1 - if x_maxy < minx + @show x1 y m1 should_label + if should_label push!(tickpos2x, y) push!(tickpos2w, tickv) push!(tickslopes2x, m1) @@ -481,13 +486,19 @@ function wcsgridspec(wsg::WCSGrid6) # Now do where the lines exit the plot m2 = (posxy[ax[2],exitted_axes_i] - posxy[ax[2],exitted_axes_i-1])/ - (posxy[ax[1],exitted_axes_i] - posxy[ax[1],exitted_axes_i-1]) + (posxy[ax[1],exitted_axes_i] - posxy[ax[1],exitted_axes_i-1]) b2 = posxy[ax[2],exitted_axes_i] - m2*posxy[ax[1],exitted_axes_i] # Find the coordinate of maxy so that we don't run below the bottom axis + + + # Maybe just find which of minx//maxx are closer? x_miny = (miny-b2)/m2 - if x_miny > maxx - # We never hit the axis - x2 = maxx + if !(minx <= x_miny <= maxx) + if (minx-posxy[ax[1],exitted_axes_i])^2 < (maxx - posxy[ax[1],exitted_axes_i]) + x2 = minx + else + x2 = maxx + end else x2 = x_miny end @@ -501,12 +512,11 @@ function wcsgridspec(wsg::WCSGrid6) # Chop off the lines to be inside the plot and then put # out new intercept points back in + # posxy_neat = [point_entered posxy[:,entered_axes_i:exitted_axes_i] point_exitted] posxy_neat = [point_entered posxy[:,entered_axes_i:exitted_axes_i] point_exitted] + # posxy_neat = posxy # TODO: other axes also need a fit? - # TODO: one option could be to place grid labels where they exit the - # plot. This gets around the tilted ticks issue - gridlinexy = ( posxy_neat[ax[1],:], posxy_neat[ax[2],:] @@ -515,7 +525,16 @@ function wcsgridspec(wsg::WCSGrid6) end # return WCSGrid6(w, extent, gridlinesxy1, gridlinesxy2, tickpos1x, tickpos1w, tickslopes1x, tickpos2x, tickpos2w, tickslopes2x) - return (;gridlinesxy1, gridlinesxy2, tickpos1x, tickpos1w, tickslopes1x, tickpos2x, tickpos2w, tickslopes2x) + return (; + gridlinesxy1, + gridlinesxy2, + tickpos1x, + tickpos1w, + tickslopes1x, + tickpos2x, + tickpos2w, + tickslopes2x + ) end export WCSGrid6 @@ -524,11 +543,11 @@ export WCSGrid6 # This recipe plots as AstroImage of color data as an image series (not heatmap). # This lets us also plot color composites e.g. in WCS coordinates. -@recipe function f(wcsax::WCSGrid6) +@recipe function f(wcsg::WCSGrid6) color --> :black # Is there a way to get the foreground color automatically? label --> "" - gridspec = wcsgridspec(wcsax) + gridspec = wcsgridspec(wcsg) # Unroll grid lines into a single series separated by NaNs xs1 = mapreduce(vcat, gridspec.gridlinesxy1) do gridline @@ -548,46 +567,29 @@ export WCSGrid6 ys = vcat(ys1, NaN, ys2) # We can optionally annotate the grid with their coordinates - - # @series begin - # ticklabels = prepare_tick_labels(gridspec.w, 2, gridspec.tickpos2x, gridspec.tickpos2w) - # rotations = atand.(gridspec.tickslopes2x, 1) - # series_annotations := [ - # Main.Plots.text(" $l", :left, :bottom, :white, 8; rotation) - # for (l, rotation) in zip(ticklabels, rotations) - # ] - # ones(length(gridspec.tickpos2x)), gridspec.tickpos2x - # end - - # @series begin - # ticklabels = prepare_tick_labels(gridspec.w, 1, gridspec.tickpos1x, gridspec.tickpos1w) - # rotations = atand.(gridspec.tickslopes1x, 1) - # series_annotations := [ - # Main.Plots.text(" $l", :left, :bottom, :white, 8; rotation) - # for (l, rotation) in zip(ticklabels, rotations) - # ] - # gridspec.tickpos1x, ones(length(gridspec.tickpos1x)) - # end - if haskey(plotattributes, :annotategrid) && plotattributes[:annotategrid] + tickpos, ticklabels = wcsticks(wcsg, 2, gridspec) @series begin - ticklabels = prepare_tick_labels(gridspec.w, 2, gridspec.tickpos2x, gridspec.tickpos2w) rotations = atand.(gridspec.tickslopes2x, 1) + seriestype := :line + linewidth := 0 series_annotations := [ - Main.Plots.text(" $l", :left, :bottom, :white, 8; rotation) + Main.Plots.text(" $l", :left, :bottom, :white, 8; rotation) for (l, rotation) in zip(ticklabels, rotations) ] - ones(length(gridspec.tickpos2x)), gridspec.tickpos2x + ones(length(gridspec.tickpos2x)), tickpos end + tickpos, ticklabels = wcsticks(wcsg, 1, gridspec) @series begin - ticklabels = prepare_tick_labels(gridspec.w, 1, gridspec.tickpos1x, gridspec.tickpos1w) rotations = atand.(gridspec.tickslopes1x, 1) + seriestype := :line + linewidth := 0 series_annotations := [ - Main.Plots.text(" $l", :left, :bottom, :white, 8; rotation) + Main.Plots.text(" $l", :left, :bottom, :white, 8; rotation) for (l, rotation) in zip(ticklabels, rotations) ] - gridspec.tickpos1x, ones(length(gridspec.tickpos1x)) + tickpos, ones(length(gridspec.tickpos1x)) end end From b8454c766a0c2748a7f94fa9ba803edf618247ae Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 9 Jan 2022 11:08:21 -0800 Subject: [PATCH 037/178] WCSGrid improvements --- src/plot-recipes.jl | 502 ++++++++++++++++++++++++++------------------ 1 file changed, 295 insertions(+), 207 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index b351111b..ce19ca96 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -97,7 +97,7 @@ end # the transformation from pixel to coordinates can be non-linear and curved. # (;tickpos1x, tickpos1w, tickpos2x, tickpos2w, ) - wcsg = WCSGrid6(img, (1,2)) + wcsg = WCSGrid(img, (1,2)) gridspec = wcsgridspec(wcsg) @@ -131,6 +131,15 @@ end end +struct WCSGrid + w + extent + ax + coords +end +WCSGrid(w,extent,ax) = WCSGrid(w,extent,ax,ones(length(ax))) + + """ Generate nice labels from a WCSTransform, axis, and known positions. @@ -151,8 +160,8 @@ ticklabels: tick labels for each position # generalizes to N different, possiby skewed axes, where a change in # the opposite coordinate or even an unplotted coordinate affects # the tick labels. -wcsticks(img::AstroImage, axnum) = wcsticks(WCSGrid6(img), axnum) -function wcsticks(wcsg::WCSGrid6, axnum, gridspec=wcsgridspec(wcsg))#tickposx, tickposw) +wcsticks(img::AstroImage, axnum) = wcsticks(WCSGrid(img), axnum) +function wcsticks(wcsg::WCSGrid, axnum, gridspec=wcsgridspec(wcsg))#tickposx, tickposw) w = wcsg.w # TODO: sort out axnum stuff @@ -268,27 +277,9 @@ function ctype_label(ctype,radesys) end -# struct WCSGrid6 -# w -# extent -# gridlinesxy1 -# gridlinesxy2 -# tickpos1x -# tickpos1w -# tickslopes1x -# tickpos2x -# tickpos2w -# tickslopes2x -# end -struct WCSGrid6 - w - extent - ax - coords -end """ - WCSGrid6(img::AstroImage, ax=(1,2), coords=(first(axes(img,ax[1])),first(axes(img,ax[2])))) + WCSGrid(img::AstroImage, ax=(1,2), coords=(first(axes(img,ax[1])),first(axes(img,ax[2])))) Given an AstroImage, return information necessary to plot WCS gridlines in physical coordinates against the image's pixel coordinates. @@ -296,7 +287,7 @@ This function has to work on both plotted axes at once to handle rotation and ge curvature of the WCS grid projected on the image coordinates. """ -function WCSGrid6(img::AstroImage, ax=(1,2)) +function WCSGrid(img::AstroImage, ax=(1,2)) minx = first(axes(img,ax[1])) maxx = last(axes(img,ax[1])) @@ -304,16 +295,74 @@ function WCSGrid6(img::AstroImage, ax=(1,2)) maxy = last(axes(img,ax[2])) extent = (minx, maxx, miny, maxy) - return WCSGrid6(wcs(img), extent, ax, (extent[1], extent[3])) + return WCSGrid(wcs(img), extent, ax, (extent[1], extent[3])) +end + + + +# TODO: the wcs parameter is not getting forwardded correctly. Use plot recipe system for this. + +# This recipe plots as AstroImage of color data as an image series (not heatmap). +# This lets us also plot color composites e.g. in WCS coordinates. +@recipe function f(wcsg::WCSGrid) + color --> :black # Is there a way to get the foreground color automatically? + label --> "" + + gridspec = wcsgridspec(wcsg) + xs, ys = wcsgridlines(gridspec) + + color = plotattributes[:color] + + # We can optionally annotate the grid with their coordinates + if haskey(plotattributes, :annotategrid) && plotattributes[:annotategrid] + tickpos, ticklabels = wcsticks(wcsg, 2, gridspec) + @series begin + rotations = atand.(gridspec.tickslopes2x, 1) + seriestype := :line + linewidth := 0 + series_annotations := [ + Main.Plots.text(" $l", :left, :bottom, color, 8; rotation) + for (l, rotation) in zip(ticklabels, rotations) + ] + ones(length(gridspec.tickpos2x)), tickpos + end + + tickpos, ticklabels = wcsticks(wcsg, 1, gridspec) + @series begin + rotations = atand.(gridspec.tickslopes1x, 1) + seriestype := :line + linewidth := 0 + series_annotations := [ + Main.Plots.text(" $l", :left, :bottom, color, 8; rotation) + for (l, rotation) in zip(ticklabels, rotations) + ] + tickpos, ones(length(gridspec.tickpos1x)) + end + end + + xticks --> wcsticks(wcsg, 1, gridspec) + xguide --> ctype_label(wcsg.w.ctype[wcsg.ax[1]], wcsg.w.radesys) + + yticks --> wcsticks(wcsg, 2, gridspec) + yguide --> ctype_label(wcsg.w.ctype[wcsg.ax[2]], wcsg.w.radesys) + + xlims := wcsg.extent[1], wcsg.extent[2] + ylims := wcsg.extent[3], wcsg.extent[4] + + grid := false + tickdirection := :none + + return xs, ys + end -function wcsgridspec(wsg::WCSGrid6) + +function wcsgridspec(wsg::WCSGrid) # function wcsgridspec(w::WCSTransform, extent, ax=(1,2), coords=(extent[1], extent[3]))#coords=(first(axes(img,ax[1])),first(axes(img,ax[2])))) # x and y denote pixel coordinates (along `ax`), u and v are world coordinates along same? ax = collect(wsg.ax) coordsx = convert(Vector{Float64}, collect(wsg.coords)) - minx, maxx, miny, maxy = wsg.extent # Find the extent of this slice in world coordinates @@ -324,6 +373,8 @@ function wcsgridspec(wsg::WCSGrid6) posxy[ax,4] .= (maxx,maxy) posuv = pix_to_world(wsg.w, posxy) (minu, maxu), (minv, maxv) = extrema(posuv, dims=2) + # Δu = abs(maxu-minu) + # Δv = abs(maxv-minv) # Find nice grid spacings # These heuristics can probably be improved @@ -337,14 +388,15 @@ function wcsgridspec(wsg::WCSGrid6) # In general, grid can be curved when plotted back against the image. # So we will need to sample multiple points along the grid. # TODO: find a good heuristic for this based on the curvature. - N_points = 30 + N_points = 20 + # TODO: this does not handle coordinates that wrap arounds urange = range(minu, maxu, length=N_points) vrange = range(minv, maxv, length=N_points) - tickpos1x = Float64[] - tickpos1w = Float64[] - tickslopes1x = Float64[] - gridlinesxy1 = NTuple{2,Vector{Float64}}[] + tickpos2x = Float64[] + tickpos2w = Float64[] + tickslopes2x = Float64[] + gridlinesxy2 = NTuple{2,Vector{Float64}}[] for ticku in tickposu # Make sure we handle unplotted slices correctly. griduv = repeat(posuv[:,1], 1, N_points) @@ -356,65 +408,107 @@ function wcsgridspec(wsg::WCSGrid6) # if we find out where the grid intersects the axes we can put # the labels in the correct spot - # Find the first and last indices of the grid line that are within the - # plot bounds - in_axes = (minx .<= posxy[ax[1],:] .<= maxx) .& (miny .<= posxy[ax[2],:] .<= maxy) - entered_axes_i = findfirst(in_axes) - exitted_axes_i = findlast(in_axes) - if count(in_axes) <= 1 || isnothing(entered_axes_i) || isnothing(exitted_axes_i) + # We can use these masks to determine where, and in what direction + # the gridlines leave the plot extent + in_horz_ax = minx .<= posxy[ax[1],:] .<= maxx + in_vert_ax = miny .<= posxy[ax[2],:] .<= maxy + in_axes = in_horz_ax .& in_vert_ax + if count(in_axes) < 2 continue - end - - # From here, do a linear fit to find the intersection with the axis. - # This should be accurate enough as long as N_points is high enough - # that the curvature of the grid is smooth by eye. - # y=mx+b - m1 = (posxy[ax[2],entered_axes_i+1] - posxy[ax[2],entered_axes_i])/ - (posxy[ax[1],entered_axes_i+1] - posxy[ax[1],entered_axes_i]) - b1 = posxy[ax[2],entered_axes_i] - m1*posxy[ax[1],entered_axes_i] - # Find the coordinate of maxy so that we don't run over the top axis - x_maxy = (maxy-b1)/m1 - if x_maxy > maxx - # We never hit the axis - x1 = maxx - else - x1 = x_maxy - end - # Now extrapolate the line - y = m1*(x1)+b1 - point_entered = [ - x1 - y - ] - - # Now do where the lines exit the plot - m2 = (posxy[ax[2],exitted_axes_i] - posxy[ax[2],exitted_axes_i-1])/ - (posxy[ax[1],exitted_axes_i] - posxy[ax[1],exitted_axes_i-1]) - b2 = posxy[ax[2],exitted_axes_i] - m2*posxy[ax[1],exitted_axes_i] - # Find the coordinate of maxy so that we don't run below the bottom axis - x_miny = (miny-b2)/m2 - if x_miny < minx - # We never hit the axis - x2 = minx + # Vertical grid line + elseif all(in_horz_ax) + point_entered = [ + posxy[ax[1],1] + miny + ] + point_exitted = [ + posxy[ax[1],1] + maxy + ] + push!(tickpos2x, posxy[ax[1],1]) + push!(tickpos2w, ticku) + push!(tickslopes2x, π/2) else - x2 = x_miny - end - # Now extrapolate the line - y = m2*(x2)+b2 - - point_exitted = [ - x2 - y - ] - if minx <= x_miny <= maxx - push!(tickpos1x, x2) - push!(tickpos1w, ticku) - push!(tickslopes1x, m2) + + # Use the masks to pick an x,y point inside the axes and an + # x,y point outside the axes. + i = findfirst(in_axes) + x1 = posxy[ax[1],i] + y1 = posxy[ax[2],i] + x2 = posxy[ax[1],i+1] + y2 = posxy[ax[2],i+1] + if x2-x1 ≈ 0 + @show "undef slope A" + end + + # Fit a line where we cross the axis + m1 = (y2-y1)/(x2-x1) + b1 = y1-m1*x1 + # If the line enters via the vertical axes... + if findfirst(in_vert_ax) <= findfirst(in_horz_ax) + # Then we simply evaluate it at that axis + x = abs(x1-maxx) < abs(x1-minx) ? maxx : minx + x = clamp(x,minx,maxx) + y = m1*x+b1 + if abs(x-minx) < abs(x-maxx) + push!(tickpos2x, y) + push!(tickpos2w, ticku) + push!(tickslopes2x, atan(m1, 1)) + end + else + # We must find where it enters the plot from + # bottom or top + x = abs(y1-maxy) < abs(y1-miny) ? (maxy-b1)/m1 : (miny-b1)/m1 + x = clamp(x,minx,maxx) + y = m1*x+b1 + end + + # From here, do a linear fit to find the intersection with the axis. + point_entered = [ + x + y + ] + + # Use the masks to pick an x,y point inside the axes and an + # x,y point outside the axes. + i = findlast(in_axes) + x1 = posxy[ax[1],i-1] + y1 = posxy[ax[2],i-1] + x2 = posxy[ax[1],i] + y2 = posxy[ax[2],i] + if x2-x1 ≈ 0 + @show "undef slope B" + end + + # Fit a line where we cross the axis + m2 = (y2-y1)/(x2-x1) + b2 = y2-m2*x2 + if findlast(in_vert_ax) > findlast(in_horz_ax) + # Then we simply evaluate it at that axis + x = abs(x1-maxx) < abs(x1-minx) ? maxx : minx + x = clamp(x,minx,maxx) + y = m2*x+b2 + if abs(x-minx) < abs(x-maxx) + push!(tickpos2x, y) + push!(tickpos2w, ticku) + push!(tickslopes2x, atan(m2, 1)) + end + else + # We must find where it enters the plot from + # bottom or top + x = abs(y1-maxy) < abs(y1-miny) ? (maxy-b2)/m2 : (miny-b2)/m2 + x = clamp(x,minx,maxx) + y = m2*x+b2 + end + + # From here, do a linear fit to find the intersection with the axis. + point_exitted = [ + x + y + ] end - # Chop off the lines to be inside the plot and then put - # out new intercept points back in - posxy_neat = [point_entered posxy[:,entered_axes_i:exitted_axes_i] point_exitted] + posxy_neat = [point_entered posxy[[ax[1],ax[2]],in_axes] point_exitted] # posxy_neat = posxy # TODO: do unplotted other axes also need a fit? @@ -422,16 +516,15 @@ function wcsgridspec(wsg::WCSGrid6) posxy_neat[ax[1],:], posxy_neat[ax[2],:] ) - push!(gridlinesxy1, gridlinexy) + push!(gridlinesxy2, gridlinexy) end - # Then do the opposite coordinate - tickpos2x = Float64[] - tickpos2w = Float64[] - tickslopes2x = Float64[] - gridlinesxy2 = NTuple{2,Vector{Float64}}[] + # Then do the opposite coordinate + tickpos1x = Float64[] + tickpos1w = Float64[] + tickslopes1x = Float64[] + gridlinesxy1 = NTuple{2,Vector{Float64}}[] for tickv in tickposv - # Make sure we handle unplotted slices correctly. griduv = repeat(posuv[:,1], 1, N_points) griduv[ax[1],:] .= urange @@ -442,89 +535,117 @@ function wcsgridspec(wsg::WCSGrid6) # if we find out where the grid intersects the axes we can put # the labels in the correct spot - # Find the first and last indices of the grid line that are within the - # plot bounds - in_axes = (minx .<= posxy[ax[1],:] .<= maxx) .& (miny .<= posxy[ax[2],:] .<= maxy) - entered_axes_i = findfirst(in_axes) - exitted_axes_i = findlast(in_axes) - if isnothing(entered_axes_i) || isnothing(exitted_axes_i) + # We can use these masks to determine where, and in what direction + # the gridlines leave the plot extent + in_horz_ax = minx .<= posxy[ax[1],:] .<= maxx + in_vert_ax = miny .<= posxy[ax[2],:] .<= maxy + in_axes = in_horz_ax .& in_vert_ax + if count(in_axes) < 2 continue - end - - # From here, do a linear fit to find the intersection with the axis. - # This should be accurate enough as long as N_points is high enough - # that the curvature of the grid is smooth by eye. - # y=mx+b - m1 = (posxy[ax[2],entered_axes_i+1] - posxy[ax[2],entered_axes_i])/ - (posxy[ax[1],entered_axes_i+1] - posxy[ax[1],entered_axes_i]) - b1 = posxy[ax[2],entered_axes_i] - m1*posxy[ax[1],entered_axes_i] - # Find the coordinate of maxy so that we don't run over the top axis - if (posxy[ax[1],entered_axes_i+1]-minx)^2 < (posxy[ax[1],entered_axes_i+1]-maxx)^2 - x1 = (maxy-b1)/m1 + # Vertical grid line + elseif all(in_vert_ax) + point_entered = [ + maxx + posxy[ax[2],1] + ] + point_exitted = [ + maxx + posxy[ax[2],2] + ] + push!(tickpos1x, posxy[ax[2],1]) + push!(tickpos1w, tickv) + push!(tickslopes1x, π/2) else - x1 = (miny-b1)/m1 - end - println() - @show x1 - should_label = false - if x1 <= miny - should_label = true - end - x1 = clamp(x1, miny, maxy) - # Now extrapolate the line - y = m1*(x1)+b1 - @show x1 y m1 should_label - if should_label - push!(tickpos2x, y) - push!(tickpos2w, tickv) - push!(tickslopes2x, m1) - end - point_entered = [ - x1 - y - ] - - # Now do where the lines exit the plot - m2 = (posxy[ax[2],exitted_axes_i] - posxy[ax[2],exitted_axes_i-1])/ - (posxy[ax[1],exitted_axes_i] - posxy[ax[1],exitted_axes_i-1]) - b2 = posxy[ax[2],exitted_axes_i] - m2*posxy[ax[1],exitted_axes_i] - # Find the coordinate of maxy so that we don't run below the bottom axis + + # Use the masks to pick an x,y point inside the axes and an + # x,y point outside the axes. + i = findfirst(in_axes) + x1 = posxy[ax[1],i] + y1 = posxy[ax[2],i] + x2 = posxy[ax[1],i+1] + y2 = posxy[ax[2],i+1] + if x2-x1 ≈ 0 + @show "undef slope C" + end + + # Fit a line where we cross the axis + m1 = (y2-y1)/(x2-x1) + b1 = y1-m1*x1 + # If the line enters via the vertical axes... + if findfirst(in_vert_ax) < findfirst(in_horz_ax) + # Then we simply evaluate it at that axis + x = abs(x1-maxx) < abs(x1-minx) ? maxx : minx + x = clamp(x,minx,maxx) + y = m1*x+b1 + else + # We must find where it enters the plot from + # bottom or top + x = abs(y1-maxy) < abs(y1-miny) ? (maxy-b1)/m1 : (miny-b1)/m1 + # x = clamp(x,minx,maxx) + y = m1*x+b1 + if abs(y-miny) < abs(y-maxy) + push!(tickpos1x, x) + push!(tickpos1w, tickv) + push!(tickslopes1x, atan(m1, 1)) + end + end + # From here, do a linear fit to find the intersection with the axis. + point_entered = [ + x + y + ] - # Maybe just find which of minx//maxx are closer? - x_miny = (miny-b2)/m2 - if !(minx <= x_miny <= maxx) - if (minx-posxy[ax[1],exitted_axes_i])^2 < (maxx - posxy[ax[1],exitted_axes_i]) - x2 = minx + # Use the masks to pick an x,y point inside the axes and an + # x,y point outside the axes. + i = findlast(in_axes) + x1 = posxy[ax[1],i-1] + y1 = posxy[ax[2],i-1] + x2 = posxy[ax[1],i] + y2 = posxy[ax[2],i] + if x2-x1 ≈ 0 + @show "undef slope D" + end + + # Fit a line where we cross the axis + m2 = (y2-y1)/(x2-x1) + b2 = y2-m2*x2 + if findlast(in_vert_ax) > findlast(in_horz_ax) + # Then we simply evaluate it at that axis + x = abs(x1-maxx) < abs(x1-minx) ? maxx : minx + x = clamp(x,minx,maxx) + y = m2*x+b2 else - x2 = maxx + # We must find where it enters the plot from + # bottom or top + x = abs(y1-maxy) < abs(y1-miny) ? (maxy-b2)/m2 : (miny-b2)/m2 + x = clamp(x,minx,maxx) + y = m2*x+b2 + if abs(y-miny) < abs(y-maxy) + push!(tickpos1x, x) + push!(tickpos1w, tickv) + push!(tickslopes1x, atan(m2, 1)) + end end - else - x2 = x_miny + + # From here, do a linear fit to find the intersection with the axis. + point_exitted = [ + x + y + ] end - # Now extrapolate the line - y = m2*(x2)+b2 - - point_exitted = [ - x2 - y - ] - # Chop off the lines to be inside the plot and then put - # out new intercept points back in - - # posxy_neat = [point_entered posxy[:,entered_axes_i:exitted_axes_i] point_exitted] - posxy_neat = [point_entered posxy[:,entered_axes_i:exitted_axes_i] point_exitted] - # posxy_neat = posxy - # TODO: other axes also need a fit? + + posxy_neat = [point_entered posxy[[ax[1],ax[2]],in_axes] point_exitted] + # TODO: do unplotted other axes also need a fit? gridlinexy = ( posxy_neat[ax[1],:], posxy_neat[ax[2],:] ) - push!(gridlinesxy2, gridlinexy) + push!(gridlinesxy1, gridlinexy) end - # return WCSGrid6(w, extent, gridlinesxy1, gridlinesxy2, tickpos1x, tickpos1w, tickslopes1x, tickpos2x, tickpos2w, tickslopes2x) + return (; gridlinesxy1, gridlinesxy2, @@ -536,63 +657,30 @@ function wcsgridspec(wsg::WCSGrid6) tickslopes2x ) end -export WCSGrid6 - -# TODO: the wcs parameter is not getting forwardded correctly. Use plot recipe system for this. - -# This recipe plots as AstroImage of color data as an image series (not heatmap). -# This lets us also plot color composites e.g. in WCS coordinates. -@recipe function f(wcsg::WCSGrid6) - color --> :black # Is there a way to get the foreground color automatically? - label --> "" - - gridspec = wcsgridspec(wcsg) - +function wcsgridlines(wcsg::WCSGrid) + return wcsgridlines(wcsgridspec(wcsg)) +end +function wcsgridlines(gridspec::NamedTuple) # Unroll grid lines into a single series separated by NaNs - xs1 = mapreduce(vcat, gridspec.gridlinesxy1) do gridline + xs1 = mapreduce(vcat, gridspec.gridlinesxy1, init=Float64[]) do gridline return vcat(gridline[1], NaN) end - ys1 = mapreduce(vcat, gridspec.gridlinesxy1) do gridline + ys1 = mapreduce(vcat, gridspec.gridlinesxy1, init=Float64[]) do gridline return vcat(gridline[2], NaN) end - xs2 = mapreduce(vcat, gridspec.gridlinesxy2) do gridline + xs2 = mapreduce(vcat, gridspec.gridlinesxy2, init=Float64[]) do gridline return vcat(gridline[1], NaN) end - ys2 = mapreduce(vcat, gridspec.gridlinesxy2) do gridline + ys2 = mapreduce(vcat, gridspec.gridlinesxy2, init=Float64[]) do gridline return vcat(gridline[2], NaN) end xs = vcat(xs1, NaN, xs2) ys = vcat(ys1, NaN, ys2) - - # We can optionally annotate the grid with their coordinates - if haskey(plotattributes, :annotategrid) && plotattributes[:annotategrid] - tickpos, ticklabels = wcsticks(wcsg, 2, gridspec) - @series begin - rotations = atand.(gridspec.tickslopes2x, 1) - seriestype := :line - linewidth := 0 - series_annotations := [ - Main.Plots.text(" $l", :left, :bottom, :white, 8; rotation) - for (l, rotation) in zip(ticklabels, rotations) - ] - ones(length(gridspec.tickpos2x)), tickpos - end - - tickpos, ticklabels = wcsticks(wcsg, 1, gridspec) - @series begin - rotations = atand.(gridspec.tickslopes1x, 1) - seriestype := :line - linewidth := 0 - series_annotations := [ - Main.Plots.text(" $l", :left, :bottom, :white, 8; rotation) - for (l, rotation) in zip(ticklabels, rotations) - ] - tickpos, ones(length(gridspec.tickpos1x)) - end - end - return xs, ys - end +export wcsgridlines + +export WCSGrid + From e48625cf102d35653492392298310372ed952ad7 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 10 Jan 2022 09:21:31 -0800 Subject: [PATCH 038/178] Address more corner cases --- src/plot-recipes.jl | 50 ++++++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index ce19ca96..ce0b3e0c 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -69,7 +69,6 @@ save("output.png", v) return iv end -# TODO: the wcs parameter is not getting forwardded correctly. Use plot recipe system for this. # This recipe plots as AstroImage of color data as an image series (not heatmap). # This lets us also plot color composites e.g. in WCS coordinates. @@ -197,6 +196,13 @@ function wcsticks(wcsg::WCSGrid, axnum, gridspec=wcsgridspec(wcsg))#tickposx, ti return vals end + # if sign(last(tickposx) - first(tickposx)) != sign(last(tickposw) - first(tickposw)) + # if abs(last(tickposw)) < abs(first(tickposw)) + # tickposx = reverse(tickposx) + # tickposw = reverse(tickposw) + # parts = reverse(parts) + # end + # Start with something impossible of the same size: last_coord = Inf .* converter(first(tickposw)) zero_coords_i = maximum(map(parts) do vals @@ -210,17 +216,21 @@ function wcsticks(wcsg::WCSGrid, axnum, gridspec=wcsgridspec(wcsg))#tickposx, ti last_coord = Inf .* converter(first(tickposw)) for (i,vals) in enumerate(parts) changing_coord_i = findfirst(vals .!= last_coord) + # Don't display just e.g. 00" when we could display 50'00" + if changing_coord_i > 1 && vals[changing_coord_i] == 0 + changing_coord_i = changing_coord_i -1 + end val_unit_zip = zip(vals[changing_coord_i:zero_coords_i],units[changing_coord_i:zero_coords_i]) ticklabels[i] = mapreduce(*, enumerate(val_unit_zip)) do (coord_i,(val,unit)) # Last coordinate always gets decimal places # if coord_i == zero_coords_i && zero_coords_i == length(vals) if coord_i + changing_coord_i - 1== length(vals) str = @sprintf("%.2f", val) - while endswith(str, r"0|\.") - str = chop(str) - end + # while endswith(str, r"0|\.") + # str = chop(str) + # end else - str = @sprintf("%d", val) + str = @sprintf("%02d", val) end if length(str) > 0 return str * unit @@ -397,11 +407,11 @@ function wcsgridspec(wsg::WCSGrid) tickpos2w = Float64[] tickslopes2x = Float64[] gridlinesxy2 = NTuple{2,Vector{Float64}}[] - for ticku in tickposu + for tickv in tickposv # Make sure we handle unplotted slices correctly. griduv = repeat(posuv[:,1], 1, N_points) - griduv[ax[1],:] .= ticku - griduv[ax[2],:] .= vrange + griduv[ax[1],:] .= urange + griduv[ax[2],:] .= tickv posxy = world_to_pix(wsg.w, griduv) # Now that we have the grid in pixel coordinates, @@ -416,7 +426,7 @@ function wcsgridspec(wsg::WCSGrid) if count(in_axes) < 2 continue # Vertical grid line - elseif all(in_horz_ax) + elseif posxy[ax[1],findfirst(in_axes)] - posxy[ax[1],findlast(in_axes)] ≈ 0 point_entered = [ posxy[ax[1],1] miny @@ -425,8 +435,9 @@ function wcsgridspec(wsg::WCSGrid) posxy[ax[1],1] maxy ] + # TODO: this logic isn't quite right! push!(tickpos2x, posxy[ax[1],1]) - push!(tickpos2w, ticku) + push!(tickpos2w, tickv) push!(tickslopes2x, π/2) else @@ -452,7 +463,7 @@ function wcsgridspec(wsg::WCSGrid) y = m1*x+b1 if abs(x-minx) < abs(x-maxx) push!(tickpos2x, y) - push!(tickpos2w, ticku) + push!(tickpos2w, tickv) push!(tickslopes2x, atan(m1, 1)) end else @@ -469,6 +480,7 @@ function wcsgridspec(wsg::WCSGrid) y ] + # Use the masks to pick an x,y point inside the axes and an # x,y point outside the axes. i = findlast(in_axes) @@ -490,7 +502,7 @@ function wcsgridspec(wsg::WCSGrid) y = m2*x+b2 if abs(x-minx) < abs(x-maxx) push!(tickpos2x, y) - push!(tickpos2w, ticku) + push!(tickpos2w, tickv) push!(tickslopes2x, atan(m2, 1)) end else @@ -524,11 +536,11 @@ function wcsgridspec(wsg::WCSGrid) tickpos1w = Float64[] tickslopes1x = Float64[] gridlinesxy1 = NTuple{2,Vector{Float64}}[] - for tickv in tickposv + for ticku in tickposu # Make sure we handle unplotted slices correctly. griduv = repeat(posuv[:,1], 1, N_points) - griduv[ax[1],:] .= urange - griduv[ax[2],:] .= tickv + griduv[ax[1],:] .= ticku + griduv[ax[2],:] .= vrange posxy = world_to_pix(wsg.w, griduv) # Now that we have the grid in pixel coordinates, @@ -543,7 +555,7 @@ function wcsgridspec(wsg::WCSGrid) if count(in_axes) < 2 continue # Vertical grid line - elseif all(in_vert_ax) + elseif posxy[ax[2],findfirst(in_axes)] - posxy[ax[2],findlast(in_axes)] ≈ 0 point_entered = [ maxx posxy[ax[2],1] @@ -553,7 +565,7 @@ function wcsgridspec(wsg::WCSGrid) posxy[ax[2],2] ] push!(tickpos1x, posxy[ax[2],1]) - push!(tickpos1w, tickv) + push!(tickpos1w, ticku) push!(tickslopes1x, π/2) else @@ -585,7 +597,7 @@ function wcsgridspec(wsg::WCSGrid) y = m1*x+b1 if abs(y-miny) < abs(y-maxy) push!(tickpos1x, x) - push!(tickpos1w, tickv) + push!(tickpos1w, ticku) push!(tickslopes1x, atan(m1, 1)) end end @@ -623,7 +635,7 @@ function wcsgridspec(wsg::WCSGrid) y = m2*x+b2 if abs(y-miny) < abs(y-maxy) push!(tickpos1x, x) - push!(tickpos1w, tickv) + push!(tickpos1w, ticku) push!(tickslopes1x, atan(m2, 1)) end end From 2d9d3788e9b2ef86819324e484c9684bc4c77c1c Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 10 Jan 2022 09:50:57 -0800 Subject: [PATCH 039/178] More corner cases --- src/plot-recipes.jl | 71 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 13 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index ce0b3e0c..8331623b 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -425,18 +425,45 @@ function wcsgridspec(wsg::WCSGrid) in_axes = in_horz_ax .& in_vert_ax if count(in_axes) < 2 continue - # Vertical grid line - elseif posxy[ax[1],findfirst(in_axes)] - posxy[ax[1],findlast(in_axes)] ≈ 0 + # # Vertical grid line + # elseif posxy[ax[1],findfirst(in_axes)] - posxy[ax[1],findlast(in_axes)] ≈ 0 + # point_entered = [ + # posxy[ax[1],1] + # miny + # ] + # point_exitted = [ + # posxy[ax[1],1] + # maxy + # ] + # # TODO: this logic isn't quite right! + # push!(tickpos2x, posxy[ax[1],1]) + # push!(tickpos2w, tickv) + # push!(tickslopes2x, π/2) + + + elseif posxy[ax[2],findfirst(in_axes)] - posxy[ax[2],findlast(in_axes)] ≈ 0 point_entered = [ - posxy[ax[1],1] - miny + minx + posxy[ax[2],findfirst(in_axes)] ] point_exitted = [ - posxy[ax[1],1] - maxy + maxx + posxy[ax[2],findlast(in_axes)] + ] + push!(tickpos2x, posxy[ax[2],findfirst(in_axes)]) + push!(tickpos2w, tickv) + push!(tickslopes2x, 0) + # Vertical grid lines + elseif posxy[ax[2],findfirst(in_axes)] - posxy[ax[2],findlast(in_axes)] ≈ 0 + point_entered = [ + minx + posxy[ax[2],findfirst(in_axes)] + ] + point_exitted = [ + maxx + posxy[ax[2],findfirst(in_axes)] ] - # TODO: this logic isn't quite right! - push!(tickpos2x, posxy[ax[1],1]) + push!(tickpos2x, posxy[ax[2],1]) push!(tickpos2w, tickv) push!(tickslopes2x, π/2) else @@ -546,23 +573,41 @@ function wcsgridspec(wsg::WCSGrid) # Now that we have the grid in pixel coordinates, # if we find out where the grid intersects the axes we can put # the labels in the correct spot - + # We can use these masks to determine where, and in what direction # the gridlines leave the plot extent in_horz_ax = minx .<= posxy[ax[1],:] .<= maxx in_vert_ax = miny .<= posxy[ax[2],:] .<= maxy in_axes = in_horz_ax .& in_vert_ax + + + # @show posxy[ax[1],findfirst(in_axes)] - posxy[ax[1],findlast(in_axes)] ≈ 0 + # @show posxy[ax[2],findfirst(in_axes)] - posxy[ax[2],findlast(in_axes)] ≈ 0 + if count(in_axes) < 2 continue - # Vertical grid line + # Horizontal grid lines + elseif posxy[ax[1],findfirst(in_axes)] - posxy[ax[1],findlast(in_axes)] ≈ 0 + point_entered = [ + posxy[ax[1],findfirst(in_axes)] + miny + ] + point_exitted = [ + posxy[ax[1],findlast(in_axes)] + maxy + ] + push!(tickpos1x, posxy[ax[1],findfirst(in_axes)]) + push!(tickpos1w, ticku) + push!(tickslopes1x, 0) + # Vertical grid lines elseif posxy[ax[2],findfirst(in_axes)] - posxy[ax[2],findlast(in_axes)] ≈ 0 point_entered = [ - maxx - posxy[ax[2],1] + minx + posxy[ax[2],findfirst(in_axes)] ] point_exitted = [ maxx - posxy[ax[2],2] + posxy[ax[2],findfirst(in_axes)] ] push!(tickpos1x, posxy[ax[2],1]) push!(tickpos1w, ticku) From a7b00ae974806362a7dae3ac1326ebff2b4e536c Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 10 Jan 2022 10:58:41 -0800 Subject: [PATCH 040/178] Adaptively add more grid lines Add lines until we have at least 2 ticks so that users can always know the scale of their images --- src/plot-recipes.jl | 570 ++++++++++++++++++++++---------------------- 1 file changed, 288 insertions(+), 282 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 8331623b..d90fc186 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -383,326 +383,332 @@ function wcsgridspec(wsg::WCSGrid) posxy[ax,4] .= (maxx,maxy) posuv = pix_to_world(wsg.w, posxy) (minu, maxu), (minv, maxv) = extrema(posuv, dims=2) - # Δu = abs(maxu-minu) - # Δv = abs(maxv-minv) - - # Find nice grid spacings - # These heuristics can probably be improved - Q=[(1.0,1.0), (3.0, 0.8), (2.0, 0.7), (5.0, 0.5)] # dms2deg(0, 0, 20) - k_min = 4 - k_ideal = 8 - k_max = 20 - tickposu = optimize_ticks(6minu, 6maxu; Q, k_min, k_ideal, k_max)[1]./6 - tickposv = optimize_ticks(6minv, 6maxv; Q, k_min, k_ideal, k_max)[1]./6 # In general, grid can be curved when plotted back against the image. # So we will need to sample multiple points along the grid. # TODO: find a good heuristic for this based on the curvature. N_points = 20 - # TODO: this does not handle coordinates that wrap arounds urange = range(minu, maxu, length=N_points) vrange = range(minv, maxv, length=N_points) + # Find nice grid spacings + # These heuristics can probably be improved + # TODO: this does not handle coordinates that wrap arounds + Q=[(1.0,1.0), (3.0, 0.8), (2.0, 0.7), (5.0, 0.5)] # dms2deg(0, 0, 20) + k_min = 3 + k_ideal = 5 + k_max = 10 + tickpos2x = Float64[] tickpos2w = Float64[] tickslopes2x = Float64[] gridlinesxy2 = NTuple{2,Vector{Float64}}[] - for tickv in tickposv - # Make sure we handle unplotted slices correctly. - griduv = repeat(posuv[:,1], 1, N_points) - griduv[ax[1],:] .= urange - griduv[ax[2],:] .= tickv - posxy = world_to_pix(wsg.w, griduv) - - # Now that we have the grid in pixel coordinates, - # if we find out where the grid intersects the axes we can put - # the labels in the correct spot - - # We can use these masks to determine where, and in what direction - # the gridlines leave the plot extent - in_horz_ax = minx .<= posxy[ax[1],:] .<= maxx - in_vert_ax = miny .<= posxy[ax[2],:] .<= maxy - in_axes = in_horz_ax .& in_vert_ax - if count(in_axes) < 2 - continue - # # Vertical grid line - # elseif posxy[ax[1],findfirst(in_axes)] - posxy[ax[1],findlast(in_axes)] ≈ 0 - # point_entered = [ - # posxy[ax[1],1] - # miny - # ] - # point_exitted = [ - # posxy[ax[1],1] - # maxy - # ] - # # TODO: this logic isn't quite right! - # push!(tickpos2x, posxy[ax[1],1]) - # push!(tickpos2w, tickv) - # push!(tickslopes2x, π/2) - - - elseif posxy[ax[2],findfirst(in_axes)] - posxy[ax[2],findlast(in_axes)] ≈ 0 - point_entered = [ - minx - posxy[ax[2],findfirst(in_axes)] - ] - point_exitted = [ - maxx - posxy[ax[2],findlast(in_axes)] - ] - push!(tickpos2x, posxy[ax[2],findfirst(in_axes)]) - push!(tickpos2w, tickv) - push!(tickslopes2x, 0) - # Vertical grid lines - elseif posxy[ax[2],findfirst(in_axes)] - posxy[ax[2],findlast(in_axes)] ≈ 0 - point_entered = [ - minx - posxy[ax[2],findfirst(in_axes)] - ] - point_exitted = [ - maxx - posxy[ax[2],findfirst(in_axes)] - ] - push!(tickpos2x, posxy[ax[2],1]) - push!(tickpos2w, tickv) - push!(tickslopes2x, π/2) - else - - # Use the masks to pick an x,y point inside the axes and an - # x,y point outside the axes. - i = findfirst(in_axes) - x1 = posxy[ax[1],i] - y1 = posxy[ax[2],i] - x2 = posxy[ax[1],i+1] - y2 = posxy[ax[2],i+1] - if x2-x1 ≈ 0 - @show "undef slope A" - end - - # Fit a line where we cross the axis - m1 = (y2-y1)/(x2-x1) - b1 = y1-m1*x1 - # If the line enters via the vertical axes... - if findfirst(in_vert_ax) <= findfirst(in_horz_ax) - # Then we simply evaluate it at that axis - x = abs(x1-maxx) < abs(x1-minx) ? maxx : minx - x = clamp(x,minx,maxx) - y = m1*x+b1 - if abs(x-minx) < abs(x-maxx) - push!(tickpos2x, y) - push!(tickpos2w, tickv) - push!(tickslopes2x, atan(m1, 1)) - end + while length(tickpos2x) < 2 + k_min += 2 + k_ideal += 2 + k_max += 2 + + tickposv = optimize_ticks(6minv, 6maxv; Q, k_min, k_ideal, k_max)[1]./6 + + empty!(tickpos2x) + empty!(tickpos2w) + empty!(tickslopes2x) + empty!(gridlinesxy2) + for tickv in tickposv + # Make sure we handle unplotted slices correctly. + griduv = repeat(posuv[:,1], 1, N_points) + griduv[ax[1],:] .= urange + griduv[ax[2],:] .= tickv + posxy = world_to_pix(wsg.w, griduv) + + # Now that we have the grid in pixel coordinates, + # if we find out where the grid intersects the axes we can put + # the labels in the correct spot + + # We can use these masks to determine where, and in what direction + # the gridlines leave the plot extent + in_horz_ax = minx .<= posxy[ax[1],:] .<= maxx + in_vert_ax = miny .<= posxy[ax[2],:] .<= maxy + in_axes = in_horz_ax .& in_vert_ax + if count(in_axes) < 2 + continue + elseif posxy[ax[2],findfirst(in_axes)] - posxy[ax[2],findlast(in_axes)] ≈ 0 + point_entered = [ + minx + posxy[ax[2],findfirst(in_axes)] + ] + point_exitted = [ + maxx + posxy[ax[2],findlast(in_axes)] + ] + push!(tickpos2x, posxy[ax[2],findfirst(in_axes)]) + push!(tickpos2w, tickv) + push!(tickslopes2x, 0) + # Vertical grid lines + elseif posxy[ax[2],findfirst(in_axes)] - posxy[ax[2],findlast(in_axes)] ≈ 0 + point_entered = [ + minx + posxy[ax[2],findfirst(in_axes)] + ] + point_exitted = [ + maxx + posxy[ax[2],findfirst(in_axes)] + ] + push!(tickpos2x, posxy[ax[2],1]) + push!(tickpos2w, tickv) + push!(tickslopes2x, π/2) else - # We must find where it enters the plot from - # bottom or top - x = abs(y1-maxy) < abs(y1-miny) ? (maxy-b1)/m1 : (miny-b1)/m1 - x = clamp(x,minx,maxx) - y = m1*x+b1 - end - - # From here, do a linear fit to find the intersection with the axis. - point_entered = [ - x - y - ] + # Use the masks to pick an x,y point inside the axes and an + # x,y point outside the axes. + i = findfirst(in_axes) + x1 = posxy[ax[1],i] + y1 = posxy[ax[2],i] + x2 = posxy[ax[1],i+1] + y2 = posxy[ax[2],i+1] + if x2-x1 ≈ 0 + @show "undef slope A" + end - # Use the masks to pick an x,y point inside the axes and an - # x,y point outside the axes. - i = findlast(in_axes) - x1 = posxy[ax[1],i-1] - y1 = posxy[ax[2],i-1] - x2 = posxy[ax[1],i] - y2 = posxy[ax[2],i] - if x2-x1 ≈ 0 - @show "undef slope B" - end + # Fit a line where we cross the axis + m1 = (y2-y1)/(x2-x1) + b1 = y1-m1*x1 + # If the line enters via the vertical axes... + if findfirst(in_vert_ax) <= findfirst(in_horz_ax) + # Then we simply evaluate it at that axis + x = abs(x1-maxx) < abs(x1-minx) ? maxx : minx + x = clamp(x,minx,maxx) + y = m1*x+b1 + if abs(x-minx) < abs(x-maxx) + push!(tickpos2x, y) + push!(tickpos2w, tickv) + push!(tickslopes2x, atan(m1, 1)) + end + else + # We must find where it enters the plot from + # bottom or top + x = abs(y1-maxy) < abs(y1-miny) ? (maxy-b1)/m1 : (miny-b1)/m1 + x = clamp(x,minx,maxx) + y = m1*x+b1 + end + + # From here, do a linear fit to find the intersection with the axis. + point_entered = [ + x + y + ] + + + # Use the masks to pick an x,y point inside the axes and an + # x,y point outside the axes. + i = findlast(in_axes) + x1 = posxy[ax[1],i-1] + y1 = posxy[ax[2],i-1] + x2 = posxy[ax[1],i] + y2 = posxy[ax[2],i] + if x2-x1 ≈ 0 + @show "undef slope B" + end - # Fit a line where we cross the axis - m2 = (y2-y1)/(x2-x1) - b2 = y2-m2*x2 - if findlast(in_vert_ax) > findlast(in_horz_ax) - # Then we simply evaluate it at that axis - x = abs(x1-maxx) < abs(x1-minx) ? maxx : minx - x = clamp(x,minx,maxx) - y = m2*x+b2 - if abs(x-minx) < abs(x-maxx) - push!(tickpos2x, y) - push!(tickpos2w, tickv) - push!(tickslopes2x, atan(m2, 1)) + # Fit a line where we cross the axis + m2 = (y2-y1)/(x2-x1) + b2 = y2-m2*x2 + if findlast(in_vert_ax) > findlast(in_horz_ax) + # Then we simply evaluate it at that axis + x = abs(x1-maxx) < abs(x1-minx) ? maxx : minx + x = clamp(x,minx,maxx) + y = m2*x+b2 + if abs(x-minx) < abs(x-maxx) + push!(tickpos2x, y) + push!(tickpos2w, tickv) + push!(tickslopes2x, atan(m2, 1)) + end + else + # We must find where it enters the plot from + # bottom or top + x = abs(y1-maxy) < abs(y1-miny) ? (maxy-b2)/m2 : (miny-b2)/m2 + x = clamp(x,minx,maxx) + y = m2*x+b2 end - else - # We must find where it enters the plot from - # bottom or top - x = abs(y1-maxy) < abs(y1-miny) ? (maxy-b2)/m2 : (miny-b2)/m2 - x = clamp(x,minx,maxx) - y = m2*x+b2 + + # From here, do a linear fit to find the intersection with the axis. + point_exitted = [ + x + y + ] end - - # From here, do a linear fit to find the intersection with the axis. - point_exitted = [ - x - y - ] - end - posxy_neat = [point_entered posxy[[ax[1],ax[2]],in_axes] point_exitted] - # posxy_neat = posxy - # TODO: do unplotted other axes also need a fit? + posxy_neat = [point_entered posxy[[ax[1],ax[2]],in_axes] point_exitted] + # posxy_neat = posxy + # TODO: do unplotted other axes also need a fit? - gridlinexy = ( - posxy_neat[ax[1],:], - posxy_neat[ax[2],:] - ) - push!(gridlinesxy2, gridlinexy) + gridlinexy = ( + posxy_neat[ax[1],:], + posxy_neat[ax[2],:] + ) + push!(gridlinesxy2, gridlinexy) + end end # Then do the opposite coordinate + k_min = 3 + k_ideal = 5 + k_max = 10 tickpos1x = Float64[] tickpos1w = Float64[] tickslopes1x = Float64[] gridlinesxy1 = NTuple{2,Vector{Float64}}[] - for ticku in tickposu - # Make sure we handle unplotted slices correctly. - griduv = repeat(posuv[:,1], 1, N_points) - griduv[ax[1],:] .= ticku - griduv[ax[2],:] .= vrange - posxy = world_to_pix(wsg.w, griduv) - - # Now that we have the grid in pixel coordinates, - # if we find out where the grid intersects the axes we can put - # the labels in the correct spot - - # We can use these masks to determine where, and in what direction - # the gridlines leave the plot extent - in_horz_ax = minx .<= posxy[ax[1],:] .<= maxx - in_vert_ax = miny .<= posxy[ax[2],:] .<= maxy - in_axes = in_horz_ax .& in_vert_ax - - - # @show posxy[ax[1],findfirst(in_axes)] - posxy[ax[1],findlast(in_axes)] ≈ 0 - # @show posxy[ax[2],findfirst(in_axes)] - posxy[ax[2],findlast(in_axes)] ≈ 0 - - if count(in_axes) < 2 - continue - # Horizontal grid lines - elseif posxy[ax[1],findfirst(in_axes)] - posxy[ax[1],findlast(in_axes)] ≈ 0 - point_entered = [ - posxy[ax[1],findfirst(in_axes)] - miny - ] - point_exitted = [ - posxy[ax[1],findlast(in_axes)] - maxy - ] - push!(tickpos1x, posxy[ax[1],findfirst(in_axes)]) - push!(tickpos1w, ticku) - push!(tickslopes1x, 0) - # Vertical grid lines - elseif posxy[ax[2],findfirst(in_axes)] - posxy[ax[2],findlast(in_axes)] ≈ 0 - point_entered = [ - minx - posxy[ax[2],findfirst(in_axes)] - ] - point_exitted = [ - maxx - posxy[ax[2],findfirst(in_axes)] - ] - push!(tickpos1x, posxy[ax[2],1]) - push!(tickpos1w, ticku) - push!(tickslopes1x, π/2) - else + while length(tickpos1x) < 2 + k_min += 2 + k_ideal += 2 + k_max += 2 + + tickposu = optimize_ticks(6minu, 6maxu; Q, k_min, k_ideal, k_max)[1]./6 + + empty!(tickpos1x) + empty!(tickpos1w) + empty!(tickslopes1x) + empty!(gridlinesxy1) + for ticku in tickposu + # Make sure we handle unplotted slices correctly. + griduv = repeat(posuv[:,1], 1, N_points) + griduv[ax[1],:] .= ticku + griduv[ax[2],:] .= vrange + posxy = world_to_pix(wsg.w, griduv) + + # Now that we have the grid in pixel coordinates, + # if we find out where the grid intersects the axes we can put + # the labels in the correct spot + + # We can use these masks to determine where, and in what direction + # the gridlines leave the plot extent + in_horz_ax = minx .<= posxy[ax[1],:] .<= maxx + in_vert_ax = miny .<= posxy[ax[2],:] .<= maxy + in_axes = in_horz_ax .& in_vert_ax + + + # @show posxy[ax[1],findfirst(in_axes)] - posxy[ax[1],findlast(in_axes)] ≈ 0 + # @show posxy[ax[2],findfirst(in_axes)] - posxy[ax[2],findlast(in_axes)] ≈ 0 - # Use the masks to pick an x,y point inside the axes and an - # x,y point outside the axes. - i = findfirst(in_axes) - x1 = posxy[ax[1],i] - y1 = posxy[ax[2],i] - x2 = posxy[ax[1],i+1] - y2 = posxy[ax[2],i+1] - if x2-x1 ≈ 0 - @show "undef slope C" - end - - # Fit a line where we cross the axis - m1 = (y2-y1)/(x2-x1) - b1 = y1-m1*x1 - # If the line enters via the vertical axes... - if findfirst(in_vert_ax) < findfirst(in_horz_ax) - # Then we simply evaluate it at that axis - x = abs(x1-maxx) < abs(x1-minx) ? maxx : minx - x = clamp(x,minx,maxx) - y = m1*x+b1 + if count(in_axes) < 2 + continue + # Horizontal grid lines + elseif posxy[ax[1],findfirst(in_axes)] - posxy[ax[1],findlast(in_axes)] ≈ 0 + point_entered = [ + posxy[ax[1],findfirst(in_axes)] + miny + ] + point_exitted = [ + posxy[ax[1],findlast(in_axes)] + maxy + ] + push!(tickpos1x, posxy[ax[1],findfirst(in_axes)]) + push!(tickpos1w, ticku) + push!(tickslopes1x, 0) + # Vertical grid lines + elseif posxy[ax[2],findfirst(in_axes)] - posxy[ax[2],findlast(in_axes)] ≈ 0 + point_entered = [ + minx + posxy[ax[2],findfirst(in_axes)] + ] + point_exitted = [ + maxx + posxy[ax[2],findfirst(in_axes)] + ] + push!(tickpos1x, posxy[ax[2],1]) + push!(tickpos1w, ticku) + push!(tickslopes1x, π/2) else - # We must find where it enters the plot from - # bottom or top - x = abs(y1-maxy) < abs(y1-miny) ? (maxy-b1)/m1 : (miny-b1)/m1 - # x = clamp(x,minx,maxx) - y = m1*x+b1 - if abs(y-miny) < abs(y-maxy) - push!(tickpos1x, x) - push!(tickpos1w, ticku) - push!(tickslopes1x, atan(m1, 1)) + + # Use the masks to pick an x,y point inside the axes and an + # x,y point outside the axes. + i = findfirst(in_axes) + x1 = posxy[ax[1],i] + y1 = posxy[ax[2],i] + x2 = posxy[ax[1],i+1] + y2 = posxy[ax[2],i+1] + if x2-x1 ≈ 0 + @show "undef slope C" end - end - - # From here, do a linear fit to find the intersection with the axis. - point_entered = [ - x - y - ] - # Use the masks to pick an x,y point inside the axes and an - # x,y point outside the axes. - i = findlast(in_axes) - x1 = posxy[ax[1],i-1] - y1 = posxy[ax[2],i-1] - x2 = posxy[ax[1],i] - y2 = posxy[ax[2],i] - if x2-x1 ≈ 0 - @show "undef slope D" - end + # Fit a line where we cross the axis + m1 = (y2-y1)/(x2-x1) + b1 = y1-m1*x1 + # If the line enters via the vertical axes... + if findfirst(in_vert_ax) < findfirst(in_horz_ax) + # Then we simply evaluate it at that axis + x = abs(x1-maxx) < abs(x1-minx) ? maxx : minx + x = clamp(x,minx,maxx) + y = m1*x+b1 + else + # We must find where it enters the plot from + # bottom or top + x = abs(y1-maxy) < abs(y1-miny) ? (maxy-b1)/m1 : (miny-b1)/m1 + # x = clamp(x,minx,maxx) + y = m1*x+b1 + if abs(y-miny) < abs(y-maxy) + push!(tickpos1x, x) + push!(tickpos1w, ticku) + push!(tickslopes1x, atan(m1, 1)) + end + end + + # From here, do a linear fit to find the intersection with the axis. + point_entered = [ + x + y + ] + + # Use the masks to pick an x,y point inside the axes and an + # x,y point outside the axes. + i = findlast(in_axes) + x1 = posxy[ax[1],i-1] + y1 = posxy[ax[2],i-1] + x2 = posxy[ax[1],i] + y2 = posxy[ax[2],i] + if x2-x1 ≈ 0 + @show "undef slope D" + end - # Fit a line where we cross the axis - m2 = (y2-y1)/(x2-x1) - b2 = y2-m2*x2 - if findlast(in_vert_ax) > findlast(in_horz_ax) - # Then we simply evaluate it at that axis - x = abs(x1-maxx) < abs(x1-minx) ? maxx : minx - x = clamp(x,minx,maxx) - y = m2*x+b2 - else - # We must find where it enters the plot from - # bottom or top - x = abs(y1-maxy) < abs(y1-miny) ? (maxy-b2)/m2 : (miny-b2)/m2 - x = clamp(x,minx,maxx) - y = m2*x+b2 - if abs(y-miny) < abs(y-maxy) - push!(tickpos1x, x) - push!(tickpos1w, ticku) - push!(tickslopes1x, atan(m2, 1)) + # Fit a line where we cross the axis + m2 = (y2-y1)/(x2-x1) + b2 = y2-m2*x2 + if findlast(in_vert_ax) > findlast(in_horz_ax) + # Then we simply evaluate it at that axis + x = abs(x1-maxx) < abs(x1-minx) ? maxx : minx + x = clamp(x,minx,maxx) + y = m2*x+b2 + else + # We must find where it enters the plot from + # bottom or top + x = abs(y1-maxy) < abs(y1-miny) ? (maxy-b2)/m2 : (miny-b2)/m2 + x = clamp(x,minx,maxx) + y = m2*x+b2 + if abs(y-miny) < abs(y-maxy) + push!(tickpos1x, x) + push!(tickpos1w, ticku) + push!(tickslopes1x, atan(m2, 1)) + end end + + # From here, do a linear fit to find the intersection with the axis. + point_exitted = [ + x + y + ] end - - # From here, do a linear fit to find the intersection with the axis. - point_exitted = [ - x - y - ] - end - posxy_neat = [point_entered posxy[[ax[1],ax[2]],in_axes] point_exitted] - # TODO: do unplotted other axes also need a fit? + posxy_neat = [point_entered posxy[[ax[1],ax[2]],in_axes] point_exitted] + # TODO: do unplotted other axes also need a fit? - gridlinexy = ( - posxy_neat[ax[1],:], - posxy_neat[ax[2],:] - ) - push!(gridlinesxy1, gridlinexy) + gridlinexy = ( + posxy_neat[ax[1],:], + posxy_neat[ax[2],:] + ) + push!(gridlinesxy1, gridlinexy) + end end - return (; gridlinesxy1, gridlinesxy2, From 52b78b011dd69a140b99563a0a8090cd019a9abc Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 11 Jan 2022 08:28:55 -0800 Subject: [PATCH 041/178] Add easy `plot(img, grid=true)` to show WCSGrid --- src/plot-recipes.jl | 135 +++++++++++++++++++++++++------------------- 1 file changed, 77 insertions(+), 58 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index d90fc186..8ef36752 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -95,8 +95,9 @@ end # In astropy, the ticks are actually tilted to reflect this, though in general # the transformation from pixel to coordinates can be non-linear and curved. - # (;tickpos1x, tickpos1w, tickpos2x, tickpos2w, ) - wcsg = WCSGrid(img, (1,2)) + ax = haskey(plotattributes, :axes) ? plotattributes[:axes] : (1,2) + slice = haskey(plotattributes, :slice) ? plotattributes[:slice] : ones(wcs(img).naxis) + wcsg = WCSGrid(img, ax, slice) gridspec = wcsgridspec(wcsg) @@ -112,11 +113,6 @@ end # # tight to the image xlims := first(axes(img,2)), last(axes(img,2)) ylims := first(axes(img,1)), last(axes(img,1)) - - # The grid lines are likely to be confusing since they do not follow - # the possibly tilted axes - grid := false - tickdirection := :none end # TODO: also disable equal aspect ratio if the scales are totally different @@ -126,7 +122,28 @@ end yflip := false xflip := false - return axes(img,2), axes(img,1), view(arraydata(img), reverse(axes(img,1)),:) + @series begin + axes(img,2), axes(img,1), view(arraydata(img), reverse(axes(img,1)),:) + end + + # If wcs=true (default) and grid=true (not default), overplot a WCS + # grid. + if (!haskey(plotattributes, :wcs) || plotattributes[:wcs]) + if haskey(plotattributes, :xgrid) && plotattributes[:xgrid] && + haskey(plotattributes, :ygrid) && plotattributes[:ygrid] + + # Plot the WCSGrid as a second series (actually just lines) + @series begin + wcsg + end + end + + # The actual grid lines are likely to be confusing since they do not follow + # the possibly tilted axes. Always hide them and the ticks. + grid := false + tickdirection := :none + end + return end @@ -163,7 +180,6 @@ wcsticks(img::AstroImage, axnum) = wcsticks(WCSGrid(img), axnum) function wcsticks(wcsg::WCSGrid, axnum, gridspec=wcsgridspec(wcsg))#tickposx, tickposw) w = wcsg.w - # TODO: sort out axnum stuff tickposx = axnum == 1 ? gridspec.tickpos1x : gridspec.tickpos2x tickposw = axnum == 1 ? gridspec.tickpos1w : gridspec.tickpos2w @@ -174,6 +190,7 @@ function wcsticks(wcsg::WCSGrid, axnum, gridspec=wcsgridspec(wcsg))#tickposx, ti return similar(tickposx, 0), String[] end + # Select a unit converter (e.g. 12.12 -> (a,b,c,d)) and list of units if w.cunit[axnum] == "deg" if startswith(uppercase(w.ctype[axnum]), "RA") converter = deg2hms @@ -196,13 +213,6 @@ function wcsticks(wcsg::WCSGrid, axnum, gridspec=wcsgridspec(wcsg))#tickposx, ti return vals end - # if sign(last(tickposx) - first(tickposx)) != sign(last(tickposw) - first(tickposw)) - # if abs(last(tickposw)) < abs(first(tickposw)) - # tickposx = reverse(tickposx) - # tickposw = reverse(tickposw) - # parts = reverse(parts) - # end - # Start with something impossible of the same size: last_coord = Inf .* converter(first(tickposw)) zero_coords_i = maximum(map(parts) do vals @@ -216,19 +226,16 @@ function wcsticks(wcsg::WCSGrid, axnum, gridspec=wcsgridspec(wcsg))#tickposx, ti last_coord = Inf .* converter(first(tickposw)) for (i,vals) in enumerate(parts) changing_coord_i = findfirst(vals .!= last_coord) - # Don't display just e.g. 00" when we could display 50'00" + # Don't display just e.g. 00" when we should display 50'00" if changing_coord_i > 1 && vals[changing_coord_i] == 0 changing_coord_i = changing_coord_i -1 end val_unit_zip = zip(vals[changing_coord_i:zero_coords_i],units[changing_coord_i:zero_coords_i]) ticklabels[i] = mapreduce(*, enumerate(val_unit_zip)) do (coord_i,(val,unit)) - # Last coordinate always gets decimal places - # if coord_i == zero_coords_i && zero_coords_i == length(vals) + # If the last coordinate we print if the last coordinate we have available, + # display it with decimal places if coord_i + changing_coord_i - 1== length(vals) str = @sprintf("%.2f", val) - # while endswith(str, r"0|\.") - # str = chop(str) - # end else str = @sprintf("%02d", val) end @@ -280,7 +287,7 @@ function ctype_label(ctype,radesys) return "Declination ($(radesys))" elseif startswith(ctype, "GLAT") return "Galactic Latitude" - elseif startswith(ctype, "TLAT") + # elseif startswith(ctype, "TLAT") else return ctype end @@ -297,7 +304,7 @@ This function has to work on both plotted axes at once to handle rotation and ge curvature of the WCS grid projected on the image coordinates. """ -function WCSGrid(img::AstroImage, ax=(1,2)) +function WCSGrid(img::AstroImage, ax=(1,2), coords=ones(wcs(img).naxis)) minx = first(axes(img,ax[1])) maxx = last(axes(img,ax[1])) @@ -305,7 +312,7 @@ function WCSGrid(img::AstroImage, ax=(1,2)) maxy = last(axes(img,ax[2])) extent = (minx, maxx, miny, maxy) - return WCSGrid(wcs(img), extent, ax, (extent[1], extent[3])) + return WCSGrid(wcs(img), extent, ax, coords) end @@ -315,23 +322,48 @@ end # This recipe plots as AstroImage of color data as an image series (not heatmap). # This lets us also plot color composites e.g. in WCS coordinates. @recipe function f(wcsg::WCSGrid) - color --> :black # Is there a way to get the foreground color automatically? label --> "" gridspec = wcsgridspec(wcsg) xs, ys = wcsgridlines(gridspec) - color = plotattributes[:color] + if haskey(plotattributes, :foreground_color_grid) + color --> plotattributes[:foreground_color_grid] + elseif haskey(plotattributes, :foreground_color) + color --> plotattributes[:foreground_color] + else + color --> :black + end + if haskey(plotattributes, :foreground_color_text) + textcolor = plotattributes[:foreground_color_text] + else + textcolor = plotattributes[:color] + end + + + xticks --> wcsticks(wcsg, 1, gridspec) + xguide --> ctype_label(wcsg.w.ctype[wcsg.ax[1]], wcsg.w.radesys) + + yticks --> wcsticks(wcsg, 2, gridspec) + yguide --> ctype_label(wcsg.w.ctype[wcsg.ax[2]], wcsg.w.radesys) + + xlims := wcsg.extent[1], wcsg.extent[2] + ylims := wcsg.extent[3], wcsg.extent[4] + + grid := false + tickdirection := :none + + @series xs, ys # We can optionally annotate the grid with their coordinates if haskey(plotattributes, :annotategrid) && plotattributes[:annotategrid] tickpos, ticklabels = wcsticks(wcsg, 2, gridspec) @series begin - rotations = atand.(gridspec.tickslopes2x, 1) + rotations = rad2deg.(gridspec.tickslopes2x) seriestype := :line linewidth := 0 series_annotations := [ - Main.Plots.text(" $l", :left, :bottom, color, 8; rotation) + Main.Plots.text(" $l", :left, :bottom, textcolor, 8; rotation) for (l, rotation) in zip(ticklabels, rotations) ] ones(length(gridspec.tickpos2x)), tickpos @@ -339,31 +371,18 @@ end tickpos, ticklabels = wcsticks(wcsg, 1, gridspec) @series begin - rotations = atand.(gridspec.tickslopes1x, 1) + rotations = rad2deg.(gridspec.tickslopes1x) seriestype := :line linewidth := 0 series_annotations := [ - Main.Plots.text(" $l", :left, :bottom, color, 8; rotation) + Main.Plots.text(" $l", :right, :bottom, textcolor, 8; rotation) for (l, rotation) in zip(ticklabels, rotations) ] tickpos, ones(length(gridspec.tickpos1x)) end end - xticks --> wcsticks(wcsg, 1, gridspec) - xguide --> ctype_label(wcsg.w.ctype[wcsg.ax[1]], wcsg.w.radesys) - - yticks --> wcsticks(wcsg, 2, gridspec) - yguide --> ctype_label(wcsg.w.ctype[wcsg.ax[2]], wcsg.w.radesys) - - xlims := wcsg.extent[1], wcsg.extent[2] - ylims := wcsg.extent[3], wcsg.extent[4] - - grid := false - tickdirection := :none - - return xs, ys - + return end @@ -393,8 +412,8 @@ function wcsgridspec(wsg::WCSGrid) # Find nice grid spacings # These heuristics can probably be improved - # TODO: this does not handle coordinates that wrap arounds - Q=[(1.0,1.0), (3.0, 0.8), (2.0, 0.7), (5.0, 0.5)] # dms2deg(0, 0, 20) + # TODO: this does not handle coordinates that wrap around + Q=[(1.0,1.0), (3.0, 0.8), (2.0, 0.7), (5.0, 0.5)] k_min = 3 k_ideal = 5 k_max = 10 @@ -403,10 +422,12 @@ function wcsgridspec(wsg::WCSGrid) tickpos2w = Float64[] tickslopes2x = Float64[] gridlinesxy2 = NTuple{2,Vector{Float64}}[] - while length(tickpos2x) < 2 + i = 3 + while length(tickpos2x) < 2 && i > 0 k_min += 2 k_ideal += 2 k_max += 2 + i -= 1 tickposv = optimize_ticks(6minv, 6maxv; Q, k_min, k_ideal, k_max)[1]./6 @@ -467,7 +488,7 @@ function wcsgridspec(wsg::WCSGrid) x2 = posxy[ax[1],i+1] y2 = posxy[ax[2],i+1] if x2-x1 ≈ 0 - @show "undef slope A" + @warn "undef slope" end # Fit a line where we cross the axis @@ -507,7 +528,7 @@ function wcsgridspec(wsg::WCSGrid) x2 = posxy[ax[1],i] y2 = posxy[ax[2],i] if x2-x1 ≈ 0 - @show "undef slope B" + @warn "undef slope" end # Fit a line where we cross the axis @@ -521,7 +542,7 @@ function wcsgridspec(wsg::WCSGrid) if abs(x-minx) < abs(x-maxx) push!(tickpos2x, y) push!(tickpos2w, tickv) - push!(tickslopes2x, atan(m2, 1)) + push!(tickslopes2x, atan(m2, 0)) end else # We must find where it enters the plot from @@ -558,10 +579,12 @@ function wcsgridspec(wsg::WCSGrid) tickpos1w = Float64[] tickslopes1x = Float64[] gridlinesxy1 = NTuple{2,Vector{Float64}}[] - while length(tickpos1x) < 2 + i = 3 + while length(tickpos1x) < 2 && i > 0 k_min += 2 k_ideal += 2 k_max += 2 + i -= 1 tickposu = optimize_ticks(6minu, 6maxu; Q, k_min, k_ideal, k_max)[1]./6 @@ -586,10 +609,6 @@ function wcsgridspec(wsg::WCSGrid) in_vert_ax = miny .<= posxy[ax[2],:] .<= maxy in_axes = in_horz_ax .& in_vert_ax - - # @show posxy[ax[1],findfirst(in_axes)] - posxy[ax[1],findlast(in_axes)] ≈ 0 - # @show posxy[ax[2],findfirst(in_axes)] - posxy[ax[2],findlast(in_axes)] ≈ 0 - if count(in_axes) < 2 continue # Horizontal grid lines @@ -628,7 +647,7 @@ function wcsgridspec(wsg::WCSGrid) x2 = posxy[ax[1],i+1] y2 = posxy[ax[2],i+1] if x2-x1 ≈ 0 - @show "undef slope C" + @warn "undef slope" end # Fit a line where we cross the axis @@ -667,7 +686,7 @@ function wcsgridspec(wsg::WCSGrid) x2 = posxy[ax[1],i] y2 = posxy[ax[2],i] if x2-x1 ≈ 0 - @show "undef slope D" + @warn "undef slope" end # Fit a line where we cross the axis From bbaf300dbd265edc56574625311168af7c4b9d84 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 11 Jan 2022 09:09:00 -0800 Subject: [PATCH 042/178] Plot formatting improvements --- src/plot-recipes.jl | 61 +++++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 8ef36752..9a968535 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -63,6 +63,17 @@ save("output.png", v) stretch=_default_stretch[], cmap=_default_cmap[], ) where {T<:Number} + + # TODO: we often plot an AstroImage{<:Number} which hasn't yet had + # its wcs cached (wcs_stale=true) and we make an image view here. + # That means we may have to keep recomputing the WCS on each plot call + # since the result is stored in the imview instead of original image. + # Call wcs(img) here if we are later going to plot with wcs coordinates + # to ensure this gets cached beween calls. + if !haskey(plotattributes, :wcs) || plotattributes[:wcs] + wcs(img) + end + # We currently use the AstroImages defaults. If unset, we could # instead follow the plot theme. iv = imview(img; clims, stretch, cmap) @@ -110,9 +121,14 @@ end yguide --> ctype_label(wcs(img).ctype[2], wcs(img).radesys) # To ensure the physical axis tick labels are correct the axes must be - # # tight to the image + # tight to the image xlims := first(axes(img,2)), last(axes(img,2)) ylims := first(axes(img,1)), last(axes(img,1)) + + # The actual grid lines are likely to be confusing since they do not follow + # the possibly tilted axes. Always hide them and the ticks. + grid := false + tickdirection := :none end # TODO: also disable equal aspect ratio if the scales are totally different @@ -128,22 +144,16 @@ end # If wcs=true (default) and grid=true (not default), overplot a WCS # grid. - if (!haskey(plotattributes, :wcs) || plotattributes[:wcs]) - if haskey(plotattributes, :xgrid) && plotattributes[:xgrid] && - haskey(plotattributes, :ygrid) && plotattributes[:ygrid] + if (!haskey(plotattributes, :wcs) || plotattributes[:wcs]) && + haskey(plotattributes, :xgrid) && plotattributes[:xgrid] && + haskey(plotattributes, :ygrid) && plotattributes[:ygrid] - # Plot the WCSGrid as a second series (actually just lines) - @series begin - wcsg - end + # Plot the WCSGrid as a second series (actually just lines) + @series begin + wcsg end - - # The actual grid lines are likely to be confusing since they do not follow - # the possibly tilted axes. Always hide them and the ticks. - grid := false - tickdirection := :none - end - return + end + return end @@ -217,6 +227,9 @@ function wcsticks(wcsg::WCSGrid, axnum, gridspec=wcsgridspec(wcsg))#tickposx, ti last_coord = Inf .* converter(first(tickposw)) zero_coords_i = maximum(map(parts) do vals changing_coord_i = findfirst(vals .!= last_coord) + if isnothing(changing_coord_i) + changing_coord_i = 1 + end last_coord = vals return changing_coord_i end) @@ -226,6 +239,9 @@ function wcsticks(wcsg::WCSGrid, axnum, gridspec=wcsgridspec(wcsg))#tickposx, ti last_coord = Inf .* converter(first(tickposw)) for (i,vals) in enumerate(parts) changing_coord_i = findfirst(vals .!= last_coord) + if isnothing(changing_coord_i) + changing_coord_i = 1 + end # Don't display just e.g. 00" when we should display 50'00" if changing_coord_i > 1 && vals[changing_coord_i] == 0 changing_coord_i = changing_coord_i -1 @@ -339,12 +355,9 @@ end else textcolor = plotattributes[:color] end + annotate = haskey(plotattributes, :annotategrid) && plotattributes[:annotategrid] - - xticks --> wcsticks(wcsg, 1, gridspec) xguide --> ctype_label(wcsg.w.ctype[wcsg.ax[1]], wcsg.w.radesys) - - yticks --> wcsticks(wcsg, 2, gridspec) yguide --> ctype_label(wcsg.w.ctype[wcsg.ax[2]], wcsg.w.radesys) xlims := wcsg.extent[1], wcsg.extent[2] @@ -353,10 +366,13 @@ end grid := false tickdirection := :none + xticks --> wcsticks(wcsg, 1, gridspec) + yticks --> wcsticks(wcsg, 2, gridspec) + @series xs, ys # We can optionally annotate the grid with their coordinates - if haskey(plotattributes, :annotategrid) && plotattributes[:annotategrid] + if annotate tickpos, ticklabels = wcsticks(wcsg, 2, gridspec) @series begin rotations = rad2deg.(gridspec.tickslopes2x) @@ -374,9 +390,10 @@ end rotations = rad2deg.(gridspec.tickslopes1x) seriestype := :line linewidth := 0 + direction = ifelse.(rotations .> 0, :left, :right) series_annotations := [ - Main.Plots.text(" $l", :right, :bottom, textcolor, 8; rotation) - for (l, rotation) in zip(ticklabels, rotations) + Main.Plots.text(" $l", dir, :bottom, textcolor, 8; rotation) + for (l, rotation, dir) in zip(ticklabels, rotations, direction) ] tickpos, ones(length(gridspec.tickpos1x)) end From 1d629c0e35f3308b1fd18521dd3df434c2a6c771 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 11 Jan 2022 09:23:55 -0800 Subject: [PATCH 043/178] Organize exports --- src/AstroImages.jl | 34 +++++++++++++++++++++++++++++----- src/plot-recipes.jl | 5 +---- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 56aaba88..9ccf7f5c 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -8,13 +8,32 @@ using MappedArrays using ColorSchemes using PlotUtils: zscale -export load, AstroImage, ccd2rgb, set_brightness!, set_contrast!, add_label!, reset! +export load, + save, + AstroImage, + WCSGrid, + ccd2rgb, + composechannels, + set_brightness!, + set_contrast!, + add_label!, + reset!, + zscale, + percent, + logstretch, + powstretch, + sqrtstretch, + squarestretch, + asinhstretch, + sinhstretch, + powerdiststretch, + clampednormedview, + imview, + clampednormedview, + wcsticks, + wcsgridlines -export zscale, percent -export logstretch, powstretch, sqrtstretch, squarestretch, asinhstretch, sinhstretch, powerdiststretch -export imview -export clampednormedview _load(fits::FITS, ext::Int) = read(fits[ext]) @@ -179,6 +198,11 @@ end Base.parent(img::AstroImage) = arraydata(img) +# We might want property access for headers in future. +function Base.getproperty(img::AstroImage, ::Symbol) + error("getproperty reserved for future use.") +end + # Getting and setting data is forwarded to the underlying array # Accessing a single value or a vector returns just the data. # Accering a 2+D slice copies the headers and re-wraps the data. diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 9a968535..4d2ef583 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -234,6 +234,7 @@ function wcsticks(wcsg::WCSGrid, axnum, gridspec=wcsgridspec(wcsg))#tickposx, ti return changing_coord_i end) + # Loop through using only the relevant part of the label # Start with something impossible of the same size: last_coord = Inf .* converter(first(tickposw)) @@ -266,7 +267,6 @@ function wcsticks(wcsg::WCSGrid, axnum, gridspec=wcsgridspec(wcsg))#tickposx, ti return tickposx, ticklabels end -export wcsticks # Extended form of deg2dms that further returns mas, microas. function deg2dmsmμ(deg) @@ -779,7 +779,4 @@ function wcsgridlines(gridspec::NamedTuple) ys = vcat(ys1, NaN, ys2) return xs, ys end -export wcsgridlines - -export WCSGrid From 0fa711b499539c123fad0a9f9dbb6f4c30a50647 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 14 Jan 2022 11:22:47 -0800 Subject: [PATCH 044/178] Fixed plot recipes for images with unequal sized axes --- src/plot-recipes.jl | 65 ++++++++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 4d2ef583..eeda67d2 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -108,8 +108,14 @@ end ax = haskey(plotattributes, :axes) ? plotattributes[:axes] : (1,2) slice = haskey(plotattributes, :slice) ? plotattributes[:slice] : ones(wcs(img).naxis) - wcsg = WCSGrid(img, ax, slice) + minx = first(axes(img,ax[2])) + maxx = last(axes(img,ax[2])) + miny = first(axes(img,ax[1])) + maxy = last(axes(img,ax[1])) + extent = (minx, maxx, miny, maxy) + + wcsg = WCSGrid(wcs(img), extent, ax, slice) gridspec = wcsgridspec(wcsg) # xticks --> (gridspec.tickpos1x, wcsticks(wcs(img), 1, gridspec)) @@ -124,11 +130,6 @@ end # tight to the image xlims := first(axes(img,2)), last(axes(img,2)) ylims := first(axes(img,1)), last(axes(img,1)) - - # The actual grid lines are likely to be confusing since they do not follow - # the possibly tilted axes. Always hide them and the ticks. - grid := false - tickdirection := :none end # TODO: also disable equal aspect ratio if the scales are totally different @@ -139,7 +140,7 @@ end xflip := false @series begin - axes(img,2), axes(img,1), view(arraydata(img), reverse(axes(img,1)),:) + view(arraydata(img), reverse(axes(img,1)),:) end # If wcs=true (default) and grid=true (not default), overplot a WCS @@ -404,7 +405,6 @@ end function wcsgridspec(wsg::WCSGrid) -# function wcsgridspec(w::WCSTransform, extent, ax=(1,2), coords=(extent[1], extent[3]))#coords=(first(axes(img,ax[1])),first(axes(img,ax[2])))) # x and y denote pixel coordinates (along `ax`), u and v are world coordinates along same? ax = collect(wsg.ax) @@ -423,7 +423,7 @@ function wcsgridspec(wsg::WCSGrid) # In general, grid can be curved when plotted back against the image. # So we will need to sample multiple points along the grid. # TODO: find a good heuristic for this based on the curvature. - N_points = 20 + N_points = 50 urange = range(minu, maxu, length=N_points) vrange = range(minv, maxv, length=N_points) @@ -439,12 +439,12 @@ function wcsgridspec(wsg::WCSGrid) tickpos2w = Float64[] tickslopes2x = Float64[] gridlinesxy2 = NTuple{2,Vector{Float64}}[] - i = 3 - while length(tickpos2x) < 2 && i > 0 + j = 3 + while length(tickpos2x) < 2 && j > 0 k_min += 2 k_ideal += 2 k_max += 2 - i -= 1 + j -= 1 tickposv = optimize_ticks(6minv, 6maxv; Q, k_min, k_ideal, k_max)[1]./6 @@ -470,7 +470,16 @@ function wcsgridspec(wsg::WCSGrid) in_axes = in_horz_ax .& in_vert_ax if count(in_axes) < 2 continue - elseif posxy[ax[2],findfirst(in_axes)] - posxy[ax[2],findlast(in_axes)] ≈ 0 + # elseif all(in_axes) + # point_entered = [ + # posxy[ax[1],begin] + # posxy[ax[2],begin] + # ] + # point_exitted = [ + # posxy[ax[1],end] + # posxy[ax[2],end] + # ] + elseif foldl(==, posxy[ax[1],findfirst(in_axes):findlast(in_axes)], init=true) point_entered = [ minx posxy[ax[2],findfirst(in_axes)] @@ -483,7 +492,7 @@ function wcsgridspec(wsg::WCSGrid) push!(tickpos2w, tickv) push!(tickslopes2x, 0) # Vertical grid lines - elseif posxy[ax[2],findfirst(in_axes)] - posxy[ax[2],findlast(in_axes)] ≈ 0 + elseif foldl(==, posxy[ax[2],findfirst(in_axes):findlast(in_axes)], init=true) point_entered = [ minx posxy[ax[2],findfirst(in_axes)] @@ -505,7 +514,7 @@ function wcsgridspec(wsg::WCSGrid) x2 = posxy[ax[1],i+1] y2 = posxy[ax[2],i+1] if x2-x1 ≈ 0 - @warn "undef slope" + # @warn "undef slope" end # Fit a line where we cross the axis @@ -545,7 +554,7 @@ function wcsgridspec(wsg::WCSGrid) x2 = posxy[ax[1],i] y2 = posxy[ax[2],i] if x2-x1 ≈ 0 - @warn "undef slope" + # @warn "undef slope" end # Fit a line where we cross the axis @@ -596,12 +605,12 @@ function wcsgridspec(wsg::WCSGrid) tickpos1w = Float64[] tickslopes1x = Float64[] gridlinesxy1 = NTuple{2,Vector{Float64}}[] - i = 3 - while length(tickpos1x) < 2 && i > 0 + j = 3 + while length(tickpos1x) < 2 && j > 0 k_min += 2 k_ideal += 2 k_max += 2 - i -= 1 + j -= 1 tickposu = optimize_ticks(6minu, 6maxu; Q, k_min, k_ideal, k_max)[1]./6 @@ -628,8 +637,17 @@ function wcsgridspec(wsg::WCSGrid) if count(in_axes) < 2 continue + # elseif all(in_axes) + # point_entered = [ + # posxy[ax[1],begin] + # posxy[ax[2],begin] + # ] + # point_exitted = [ + # posxy[ax[1],end] + # posxy[ax[2],end] + # ] # Horizontal grid lines - elseif posxy[ax[1],findfirst(in_axes)] - posxy[ax[1],findlast(in_axes)] ≈ 0 + elseif foldl(==, posxy[ax[1],findfirst(in_axes):findlast(in_axes)], init=true) point_entered = [ posxy[ax[1],findfirst(in_axes)] miny @@ -642,7 +660,7 @@ function wcsgridspec(wsg::WCSGrid) push!(tickpos1w, ticku) push!(tickslopes1x, 0) # Vertical grid lines - elseif posxy[ax[2],findfirst(in_axes)] - posxy[ax[2],findlast(in_axes)] ≈ 0 + elseif foldl(==, posxy[ax[2],findfirst(in_axes):findlast(in_axes)], init=true) point_entered = [ minx posxy[ax[2],findfirst(in_axes)] @@ -655,7 +673,6 @@ function wcsgridspec(wsg::WCSGrid) push!(tickpos1w, ticku) push!(tickslopes1x, π/2) else - # Use the masks to pick an x,y point inside the axes and an # x,y point outside the axes. i = findfirst(in_axes) @@ -664,7 +681,7 @@ function wcsgridspec(wsg::WCSGrid) x2 = posxy[ax[1],i+1] y2 = posxy[ax[2],i+1] if x2-x1 ≈ 0 - @warn "undef slope" + # @warn "undef slope" end # Fit a line where we cross the axis @@ -703,7 +720,7 @@ function wcsgridspec(wsg::WCSGrid) x2 = posxy[ax[1],i] y2 = posxy[ax[2],i] if x2-x1 ≈ 0 - @warn "undef slope" + # @warn "undef slope" end # Fit a line where we cross the axis From ba8729d81eaf8712ad203da5b782cfa7639b22a4 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 14 Jan 2022 15:27:10 -0800 Subject: [PATCH 045/178] Improved grid annotations --- src/plot-recipes.jl | 174 ++++++++++++++++++++++++++++++++------------ 1 file changed, 126 insertions(+), 48 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index eeda67d2..acec516d 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -74,6 +74,11 @@ save("output.png", v) wcs(img) end + # We don't to override e.g. heatmaps and histograms + if haskey(plotattributes, :seriestype) + return arraydata(img) + end + # We currently use the AstroImages defaults. If unset, we could # instead follow the plot theme. iv = imview(img; clims, stretch, cmap) @@ -107,7 +112,7 @@ end # the transformation from pixel to coordinates can be non-linear and curved. ax = haskey(plotattributes, :axes) ? plotattributes[:axes] : (1,2) - slice = haskey(plotattributes, :slice) ? plotattributes[:slice] : ones(wcs(img).naxis) + coords = haskey(plotattributes, :coords) ? plotattributes[:coords] : ones(wcs(img).naxis) minx = first(axes(img,ax[2])) maxx = last(axes(img,ax[2])) @@ -115,15 +120,15 @@ end maxy = last(axes(img,ax[1])) extent = (minx, maxx, miny, maxy) - wcsg = WCSGrid(wcs(img), extent, ax, slice) + wcsg = WCSGrid(wcs(img), extent, ax, coords) gridspec = wcsgridspec(wcsg) - # xticks --> (gridspec.tickpos1x, wcsticks(wcs(img), 1, gridspec)) - xticks --> wcsticks(wcsg, 1, gridspec) + xticks --> (gridspec.tickpos1x, wcslabels(wcs(img), 1, gridspec.tickpos1w)) + # xticks --> wcsticks(wcsg, 1, gridspec) xguide --> ctype_label(wcs(img).ctype[1], wcs(img).radesys) - # yticks --> (gridspec.tickpos2x, wcsticks(wcs(img), 2, gridspec)) - yticks --> wcsticks(wcsg, 2, gridspec) + yticks --> (gridspec.tickpos2x, wcslabels(wcs(img), 2, gridspec.tickpos2w)) + # yticks --> wcsticks(wcsg, 2, gridspec) yguide --> ctype_label(wcs(img).ctype[2], wcs(img).radesys) # To ensure the physical axis tick labels are correct the axes must be @@ -187,18 +192,20 @@ ticklabels: tick labels for each position # generalizes to N different, possiby skewed axes, where a change in # the opposite coordinate or even an unplotted coordinate affects # the tick labels. -wcsticks(img::AstroImage, axnum) = wcsticks(WCSGrid(img), axnum) -function wcsticks(wcsg::WCSGrid, axnum, gridspec=wcsgridspec(wcsg))#tickposx, tickposw) - w = wcsg.w +function wcslabels(img::AstroImage, axnum) + gs = wcsgridspec(WCSGrid(img)) + tickposw = axnum == 1 ? gs.tickpos1w : gs.tickpos2w + return wcslabels( + wcs(img), + axnum, + tickposw + ) +end - tickposx = axnum == 1 ? gridspec.tickpos1x : gridspec.tickpos2x - tickposw = axnum == 1 ? gridspec.tickpos1w : gridspec.tickpos2w +function wcslabels(w::WCSTransform, axnum, tickposw) - if length(tickposw) != length(tickposx) - error("Tick position vectors are of different length") - end - if length(tickposx) == 0 - return similar(tickposx, 0), String[] + if length(tickposw) == 0 + return String[] end # Select a unit converter (e.g. 12.12 -> (a,b,c,d)) and list of units @@ -216,7 +223,7 @@ function wcsticks(wcsg::WCSGrid, axnum, gridspec=wcsgridspec(wcsg))#tickposx, ti end # Format inital ticklabel - ticklabels = fill("", length(tickposx)) + ticklabels = fill("", length(tickposw)) # We only include the part of the label that has changed since the last time. # Split up coordinates into e.g. sexagesimal parts = map(tickposw) do w @@ -266,7 +273,7 @@ function wcsticks(wcsg::WCSGrid, axnum, gridspec=wcsgridspec(wcsg))#tickposx, ti last_coord = vals end - return tickposx, ticklabels + return ticklabels end # Extended form of deg2dms that further returns mas, microas. @@ -374,35 +381,35 @@ end # We can optionally annotate the grid with their coordinates if annotate - tickpos, ticklabels = wcsticks(wcsg, 2, gridspec) @series begin - rotations = rad2deg.(gridspec.tickslopes2x) + rotations = rad2deg.(gridspec.annotations1θ) + ticklabels = wcslabels(wcsg.w, 1, gridspec.annotations1w) seriestype := :line linewidth := 0 series_annotations := [ - Main.Plots.text(" $l", :left, :bottom, textcolor, 8; rotation) - for (l, rotation) in zip(ticklabels, rotations) + Main.Plots.text(" $l", :center, :bottom, textcolor, 8, rotation=(-90 <= r <= 90) ? r : r+180) + for (l, r) in zip(ticklabels, rotations) ] - ones(length(gridspec.tickpos2x)), tickpos + gridspec.annotations1x, gridspec.annotations1y end - - tickpos, ticklabels = wcsticks(wcsg, 1, gridspec) @series begin - rotations = rad2deg.(gridspec.tickslopes1x) + rotations = rad2deg.(gridspec.annotations2θ) + ticklabels = wcslabels(wcsg.w, 2, gridspec.annotations2w) seriestype := :line linewidth := 0 - direction = ifelse.(rotations .> 0, :left, :right) series_annotations := [ - Main.Plots.text(" $l", dir, :bottom, textcolor, 8; rotation) - for (l, rotation, dir) in zip(ticklabels, rotations, direction) + Main.Plots.text(" $l", :center, :bottom, textcolor, 8, rotation=(-90 <= r <= 90) ? r : r+180) + for (l, r) in zip(ticklabels, rotations) ] - tickpos, ones(length(gridspec.tickpos1x)) + gridspec.annotations2x, gridspec.annotations2y end + end return end +allequal(itr) = all(==(first(itr)), itr) function wcsgridspec(wsg::WCSGrid) @@ -439,6 +446,7 @@ function wcsgridspec(wsg::WCSGrid) tickpos2w = Float64[] tickslopes2x = Float64[] gridlinesxy2 = NTuple{2,Vector{Float64}}[] + local tickposv j = 3 while length(tickpos2x) < 2 && j > 0 k_min += 2 @@ -479,33 +487,34 @@ function wcsgridspec(wsg::WCSGrid) # posxy[ax[1],end] # posxy[ax[2],end] # ] - elseif foldl(==, posxy[ax[1],findfirst(in_axes):findlast(in_axes)], init=true) + elseif allequal(posxy[ax[1],findfirst(in_axes):findlast(in_axes)]) point_entered = [ - minx - posxy[ax[2],findfirst(in_axes)] + posxy[ax[1],max(begin,findfirst(in_axes)-1)] + # posxy[ax[2],max(begin,findfirst(in_axes)-1)] + miny ] point_exitted = [ - maxx - posxy[ax[2],findlast(in_axes)] + posxy[ax[1],min(end,findlast(in_axes)+1)] + # posxy[ax[2],min(end,findlast(in_axes)+1)] + maxy ] push!(tickpos2x, posxy[ax[2],findfirst(in_axes)]) push!(tickpos2w, tickv) push!(tickslopes2x, 0) # Vertical grid lines - elseif foldl(==, posxy[ax[2],findfirst(in_axes):findlast(in_axes)], init=true) + elseif allequal(posxy[ax[2],findfirst(in_axes):findlast(in_axes)]) point_entered = [ - minx - posxy[ax[2],findfirst(in_axes)] + minx #posxy[ax[1],max(begin,findfirst(in_axes)-1)] + posxy[ax[2],max(begin,findfirst(in_axes)-1)] ] point_exitted = [ - maxx - posxy[ax[2],findfirst(in_axes)] + maxx #posxy[ax[1],min(end,findlast(in_axes)+1)] + posxy[ax[2],min(end,findlast(in_axes)+1)] ] push!(tickpos2x, posxy[ax[2],1]) push!(tickpos2w, tickv) push!(tickslopes2x, π/2) else - # Use the masks to pick an x,y point inside the axes and an # x,y point outside the axes. i = findfirst(in_axes) @@ -514,7 +523,7 @@ function wcsgridspec(wsg::WCSGrid) x2 = posxy[ax[1],i+1] y2 = posxy[ax[2],i+1] if x2-x1 ≈ 0 - # @warn "undef slope" + @warn "undef slope" end # Fit a line where we cross the axis @@ -554,7 +563,7 @@ function wcsgridspec(wsg::WCSGrid) x2 = posxy[ax[1],i] y2 = posxy[ax[2],i] if x2-x1 ≈ 0 - # @warn "undef slope" + @warn "undef slope" end # Fit a line where we cross the axis @@ -605,6 +614,7 @@ function wcsgridspec(wsg::WCSGrid) tickpos1w = Float64[] tickslopes1x = Float64[] gridlinesxy1 = NTuple{2,Vector{Float64}}[] + local tickposu j = 3 while length(tickpos1x) < 2 && j > 0 k_min += 2 @@ -635,6 +645,7 @@ function wcsgridspec(wsg::WCSGrid) in_vert_ax = miny .<= posxy[ax[2],:] .<= maxy in_axes = in_horz_ax .& in_vert_ax + if count(in_axes) < 2 continue # elseif all(in_axes) @@ -647,7 +658,7 @@ function wcsgridspec(wsg::WCSGrid) # posxy[ax[2],end] # ] # Horizontal grid lines - elseif foldl(==, posxy[ax[1],findfirst(in_axes):findlast(in_axes)], init=true) + elseif allequal(posxy[ax[1],findfirst(in_axes):findlast(in_axes)]) point_entered = [ posxy[ax[1],findfirst(in_axes)] miny @@ -660,7 +671,7 @@ function wcsgridspec(wsg::WCSGrid) push!(tickpos1w, ticku) push!(tickslopes1x, 0) # Vertical grid lines - elseif foldl(==, posxy[ax[2],findfirst(in_axes):findlast(in_axes)], init=true) + elseif allequal(posxy[ax[2],findfirst(in_axes):findlast(in_axes)]) point_entered = [ minx posxy[ax[2],findfirst(in_axes)] @@ -681,7 +692,7 @@ function wcsgridspec(wsg::WCSGrid) x2 = posxy[ax[1],i+1] y2 = posxy[ax[2],i+1] if x2-x1 ≈ 0 - # @warn "undef slope" + @warn "undef slope" end # Fit a line where we cross the axis @@ -720,7 +731,7 @@ function wcsgridspec(wsg::WCSGrid) x2 = posxy[ax[1],i] y2 = posxy[ax[2],i] if x2-x1 ≈ 0 - # @warn "undef slope" + @warn "undef slope" end # Fit a line where we cross the axis @@ -762,6 +773,63 @@ function wcsgridspec(wsg::WCSGrid) end end + # Grid annotations + annotations1w = Float64[] + annotations1x = Float64[] + annotations1y = Float64[] + annotations1θ = Float64[] + for ticku in tickposu + # Make sure we handle unplotted slices correctly. + griduv = posuv[:,1] + griduv[ax[1]] = ticku + griduv[ax[2]] = mean(vrange) + posxy = world_to_pix(wsg.w, griduv) + if !(minx < posxy[1] < maxx) || + !(miny < posxy[2] < maxy) + continue + end + push!(annotations1w, ticku) + push!(annotations1x, posxy[ax[1]]) + push!(annotations1y, posxy[ax[2]]) + + # Now find slope (TODO: stepsize) + griduv[ax[2]] -= 1 + posxy2 = world_to_pix(wsg.w, griduv) + θ = atan( + posxy2[ax[2]] - posxy[ax[2]], + posxy2[ax[1]] - posxy[ax[1]], + ) + push!(annotations1θ, θ) + end + annotations2w = Float64[] + annotations2x = Float64[] + annotations2y = Float64[] + annotations2θ = Float64[] + for tickv in tickposv + # Make sure we handle unplotted slices correctly. + griduv = posuv[:,1] + griduv[ax[1]] = mean(urange) + griduv[ax[2]] = tickv + posxy = world_to_pix(wsg.w, griduv) + if !(minx < posxy[1] < maxx) || + !(miny < posxy[2] < maxy) + continue + end + push!(annotations2w, tickv) + push!(annotations2x, posxy[ax[1]]) + push!(annotations2y, posxy[ax[2]]) + + # Now find slope (TODO: stepsize) + griduv[ax[1]] += 1 + posxy2 = world_to_pix(wsg.w, griduv) + θ = atan( + posxy2[ax[2]] - posxy[ax[2]], + posxy2[ax[1]] - posxy[ax[1]], + ) + push!(annotations2θ, θ) + end + + return (; gridlinesxy1, gridlinesxy2, @@ -770,7 +838,17 @@ function wcsgridspec(wsg::WCSGrid) tickslopes1x, tickpos2x, tickpos2w, - tickslopes2x + tickslopes2x, + + annotations1w, + annotations1x, + annotations1y, + annotations1θ, + + annotations2w, + annotations2x, + annotations2y, + annotations2θ, ) end From e7f478add50fe2d17aa4b0b9c6afb4f98d3c9065 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 14 Jan 2022 15:43:47 -0800 Subject: [PATCH 046/178] RIght align gridlabels --- src/plot-recipes.jl | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index acec516d..d968b505 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -363,7 +363,7 @@ end else textcolor = plotattributes[:color] end - annotate = haskey(plotattributes, :annotategrid) && plotattributes[:annotategrid] + annotate = haskey(plotattributes, :gridlabels) && plotattributes[:gridlabels] xguide --> ctype_label(wcsg.w.ctype[wcsg.ax[1]], wcsg.w.radesys) yguide --> ctype_label(wcsg.w.ctype[wcsg.ax[2]], wcsg.w.radesys) @@ -382,12 +382,13 @@ end # We can optionally annotate the grid with their coordinates if annotate @series begin - rotations = rad2deg.(gridspec.annotations1θ) + # TODO: why is this reverse necessary? + rotations = reverse(rad2deg.(gridspec.annotations1θ)) ticklabels = wcslabels(wcsg.w, 1, gridspec.annotations1w) seriestype := :line linewidth := 0 series_annotations := [ - Main.Plots.text(" $l", :center, :bottom, textcolor, 8, rotation=(-90 <= r <= 90) ? r : r+180) + Main.Plots.text(" $l", :right, :bottom, textcolor, 8, rotation=(-90 <= r <= 90) ? r : r+180) for (l, r) in zip(ticklabels, rotations) ] gridspec.annotations1x, gridspec.annotations1y @@ -398,7 +399,7 @@ end seriestype := :line linewidth := 0 series_annotations := [ - Main.Plots.text(" $l", :center, :bottom, textcolor, 8, rotation=(-90 <= r <= 90) ? r : r+180) + Main.Plots.text(" $l", :right, :bottom, textcolor, 8, rotation=(-90 <= r <= 90) ? r : r+180) for (l, r) in zip(ticklabels, rotations) ] gridspec.annotations2x, gridspec.annotations2y @@ -793,7 +794,8 @@ function wcsgridspec(wsg::WCSGrid) push!(annotations1y, posxy[ax[2]]) # Now find slope (TODO: stepsize) - griduv[ax[2]] -= 1 + # griduv[ax[2]] -= 1 + griduv[ax[2]] += 0.1step(vrange) posxy2 = world_to_pix(wsg.w, griduv) θ = atan( posxy2[ax[2]] - posxy[ax[2]], @@ -819,8 +821,7 @@ function wcsgridspec(wsg::WCSGrid) push!(annotations2x, posxy[ax[1]]) push!(annotations2y, posxy[ax[2]]) - # Now find slope (TODO: stepsize) - griduv[ax[1]] += 1 + griduv[ax[1]] += 0.1step(urange) posxy2 = world_to_pix(wsg.w, griduv) θ = atan( posxy2[ax[2]] - posxy[ax[2]], From 21a5299912cc605b2753e3db5c5398f9e25c3882 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 16 Jan 2022 07:50:22 -0800 Subject: [PATCH 047/178] Add error hint for imview --- src/AstroImages.jl | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 9ccf7f5c..988f13e6 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -1,5 +1,3 @@ -__precompile__() - module AstroImages using FITSIO, FileIO, Images, Interact, Reproject, WCS, MappedArrays @@ -627,6 +625,19 @@ include("ccd2rgb.jl") include("patches.jl") +function __init__() + + # You can only `imview` 2D slices. Add an error hint if the user + # tries to display a cube. + if isdefined(Base.Experimental, :register_error_hint) + Base.Experimental.register_error_hint(MethodError) do io, exc, argtypes, kwargs + if exc.f == imview && first(argtypes) <: AbstractArray && ndims(first(argtypes)) != 2 + print(io, "\n`imview` is only supported on 2D arrays.\nIf you have a cube, try viewing one slice at a time.") + end + end + end +end + end # module From 2209269813287a1e8a4b3d4e1d88a2ef1b30018a Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 16 Jan 2022 07:58:16 -0800 Subject: [PATCH 048/178] Organize code --- src/AstroImages.jl | 55 ++-------- src/showmime.jl | 263 --------------------------------------------- 2 files changed, 9 insertions(+), 309 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 988f13e6..bc31d4a2 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -29,20 +29,13 @@ export load, imview, clampednormedview, wcsticks, - wcsgridlines + wcsgridlines, + arraydata, + headers, + wcs, + Comment, + History - - - -_load(fits::FITS, ext::Int) = read(fits[ext]) -# _load(fits::FITS, ext::NTuple{N, Int}) where {N} = ntuple(i-> read(fits[ext[i]]), N) -# # _load(fits::NTuple{N, FITS}, ext::NTuple{N, Int}) where {N} = ntuple(i -> _load(fits[i], ext[i]), N) - -# _header(fits::FITS, ext::Int) = WCS.from_header(read_header(fits[ext], String))[1] -# _header(fits::FITS, ext::NTuple{N, Int}) where {N} = -# ntuple(i -> WCS.from_header(read_header(fits[ext[i]], String))[1], N) -# _header(fits::NTuple{N, FITS}, ext::NTuple{N, Int}) where {N} = -# ntuple(i -> _header(fits[i], ext[i]), N) """ load(fitsfile::String, n=1) @@ -167,13 +160,9 @@ function wcs(img::AstroImage) return getfield(img, :wcs) end -export arraydata, headers, wcs - struct Comment end -export Comment - struct History end -export History + # extending the AbstractArray interface @@ -378,23 +367,6 @@ find_img(::Any, rest) = find_img(rest) Construct an `AstroImage` object of `data`, using `color` as color map, `Gray` by default. """ AstroImage(img::AstroImage) = img -# AstroImage(data::AbstractArray{T,N}, headers::FITSHeader, wcs::WCSTransform) where {T,N} = -# AstroImage{T,N,typeof(data)}(data, headers, wcs) - -# AstroImage(color::Type{<:Color}, data::AbstractArray{T,N}, wcs::WCSTransform) where {T<:Real,N<:Int} = -# AstroImage{T, N, color, Float64}(data, extrema(data), false, wcs, Properties{Float64}()) -# function AstroImage(color::Type{<:AbstractRGB}, data::NTuple{N, Matrix{T}}, wcs::NTuple{N, WCSTransform}) where {T <: Union{AbstractFloat, FixedPoint}, N} -# if N == 3 -# img = ccd2rgb((data[1], wcs[1]),(data[2], wcs[2]),(data[3], wcs[3])) -# return AstroImage{T,color,N, widen(T)}(data, ntuple(i -> extrema(data[i]), N), wcs, Properties{widen(T)}(rgb_image = img)) -# end -# end -# function AstroImage(color::Type{<:AbstractRGB}, data::NTuple{N, Matrix{T}}, wcs::NTuple{N, WCSTransform}) where {T<:Real, N} -# if N == 3 -# img = ccd2rgb((data[1], wcs[1]),(data[2], wcs[2]),(data[3], wcs[3])) -# return AstroImage{T,color,N, Float64}(data, ntuple(i -> extrema(data[i]), N), wcs, Properties{Float64}(rgb_image = img)) -# end -# end """ emptyheaders() @@ -465,7 +437,6 @@ FITSHeaders. function wcsfromheaders(img::AstroImage; relax=WCS.HDR_ALL) # We only need to stringify WCS headers. This might just be 4-10 header keywords # out of thousands. - # wcsout = WCS.from_header(string(filterwcsheaders(headers(img))), ignore_rejected=true) local wcsout # Load the headers without ignoring rejected to get error messages try @@ -510,15 +481,6 @@ AstroImage(fits::FITS, ext::Int=1) = AstroImage(fits[ext], read_header(fits[ext] Given an open FITS HDU, load it as an AstroImage. """ AstroImage(hdu::HDU) = AstroImage(read(hdu), read_header(hdu)) -# AstroImage(color::Type{<:Color}, fits::FITS, ext::NTuple{N, Int}) where {N} = -# AstroImage(color, _load(fits, ext), ntuple(i -> WCS.from_header(read_header(fits[ext[i]], String))[1], N)) -# AstroImage(color::Type{<:Color}, fits::NTuple{N, FITS}, ext::NTuple{N, Int}) where {N} = -# AstroImage(color, ntuple(i -> _load(fits[i], ext[i]), N), ntuple(i -> WCS.from_header(read_header(fits[i][ext[i]], String))[1], N)) - -# AstroImage(files::NTuple{N,String}) where {N} = -# AstroImage(Gray, load(files)...) -# AstroImage(color::Type{<:Color}, files::NTuple{N,String}) where {N} = -# AstroImage(color, load(files)...) """ img = AstroImage(filename::AbstractString, ext::Integer=1) @@ -619,10 +581,11 @@ function reset!(img::AstroImage{T,N}) where {T,N} end include("wcs_headers.jl") +include("imview.jl") include("showmime.jl") include("plot-recipes.jl") -include("ccd2rgb.jl") +include("ccd2rgb.jl") include("patches.jl") function __init__() diff --git a/src/showmime.jl b/src/showmime.jl index e91e5440..7bc6709f 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -33,269 +33,6 @@ Base.show(io::IO, mime::MIME"image/png", img::AstroImage{T,2}; kwargs...) where -# These reproduce the behaviour of DS9 according to http://ds9.si.edu/doc/ref/how.html -logstretch(x,a=1000) = log(a*x+1)/log(a) -powstretch(x,a=1000) = (a^x - 1)/a -sqrtstretch = sqrt -squarestretch(x) = x^2 -asinhstretch(x) = asinh(10x)/3 -sinhstretch(x) = sinh(3x)/10 -# These additional stretches reproduce behaviour from astropy -powerdiststretch(x, a=1000) = (a^x - 1) / (a - 1) - -""" - percent(99.5) - -Returns a function that calculates display limits that include the given -percent of the image data. - -Example: -```julia -julia> imview(img, clims=percent(90)) -``` -This will set the limits to be the 5th percentile to the 95th percentile. -""" -function percent(perc::Number) - trim = (1 - perc/100)/2 - clims(data) = quantile(data, (trim, 1-trim)) - clims(data::AbstractMatrix) = quantile(vec(data), (trim, 1-trim)) - return clims -end - -const _default_cmap = Ref{Union{Symbol,Nothing}}(nothing) -const _default_clims = Ref{Any}(percent(99.5)) -const _default_stretch = Ref{Any}(identity) - -""" - set_cmap!(cmap::Symbol) - set_cmap!(cmap::Nothing) - -Alter the default color map used to display images when using -`imview` or displaying an AstroImage. -""" -function set_cmap!(cmap) - if cmap ∉ keys(ColorSchemes.colorschemes) - throw(KeyError("$cmap not found in ColorSchemes.colorschemes")) - end - _default_cmap[] = cmap -end -""" - set_clims!(clims::Tuple) - set_clims!(clims::Function) - -Alter the default limits used to display images when using -`imview` or displaying an AstroImage. -""" -function set_clims!(clims) - _default_clims[] = clims -end -""" - set_stretch!(stretch::Function) - -Alter the default value stretch functio used to display images when using -`imview` or displaying an AstroImage. -""" -function set_stretch!(stretch) - _default_stretch[] = stretch -end - -""" -Helper to iterate over data skipping missing and non-finite values. -""" -skipmissingnan(itr) = Iterators.filter(el->!ismissing(el) && isfinite(el), itr) - -""" - imview(img; clims=extrema, stretch=identity, cmap=nothing) - -Create a read only view of an array or AstroImage mapping its data values -to Colors according to `clims`, `stretch`, and `cmap`. - -The data is first clamped to `clims`, which can either be a tuple of (min, max) -values or a function accepting an iterator of pixel values that returns (min, max). -By default, `clims=extrema` i.e. the minimum and maximum of `img`. -Convenient functions to use for `clims` are: -`extrema`, `zscale`, and `percent(p)` - -Next, the data is rescaled to [0,1] and remapped according to the function `stretch`. -Stretch can be any monotonic fuction mapping values in the range [0,1] to some range [a,b]. -Note that `log(0)` is not defined so is not directly supported. -For a list of convenient stretch functions, see: -`logstretch`, `powstretch`, `squarestretch`, `asinhstretch`, `sinhstretch`, `powerdiststretch` - -Finally the data is mapped to RGB values according to `cmap`. If cmap is `nothing`, -grayscale is used. ColorSchemes.jl defines hundreds of colormaps. A few nice ones for -images include: `:viridis`, `:magma`, `:plasma`, `:thermal`, and `:turbo`. - -Crucially, this function returns a view over the underlying data. If `img` is updated -then those changes will be reflected by this view with the exception of `clims` which -is not recalculated. - -Note: if clims or stretch is a function, the pixel values passed in are first filtered -to remove non-finite or missing values. - -### Defaults -The default values of `clims`, `stretch`, and `cmap` are `extrema`, `identity`, and `nothing` -respectively. -You may alter these defaults using `AstroImages.set_clims!`, `AstroImages.set_stretch!`, and -`AstroImages.set_cmap!`. - -### Automatic Display -Arrays wrapped by `AstroImage()` get displayed as images automatically by calling -`imview` on them with the default settings when using displays that support showing PNG images. - -### Missing data -Pixels that are `NaN` or `missing` will be displayed as transparent when `cmap` is set -or black if. -+/- Inf will be displayed as black or white respectively. - -### Exporting Images -The view returned by `imview` can be saved using general `FileIO.save` methods. -Example: -```julia -v = imview(data, cmap=:magma, stretch=asinhstretch, clims=percent(95)) -save("output.png", v) -``` -""" -function imview( - img::AbstractMatrix{T}; - clims=_default_clims[], - stretch=_default_stretch[], - cmap=_default_cmap[], -) where {T} - - # TODO: catch this in `show` instead of here. - isempt = isempty(img) - if isempt - return - end - # Users will occaisionally pass in data that is 0D, filled with NaN, or filled with missing. - # We still need to do something reasonable in those caes. - nonempty = any(x-> !ismissing(x) && isfinite(x), img) - if !nonempty - return - end - - # TODO: Images.jl has logic to downsize huge images before displaying them. - # We should use that here before applying all this processing instead of - # letting Images.jl handle it after. - - # Users can pass clims as an array or tuple containing the minimum and maximum values - if typeof(clims) <: AbstractArray || typeof(clims) <: Tuple - if length(clims) != 2 - error("clims must have exactly two values if provided.") - end - imgmin = first(clims) - imgmax = last(clims) - # Or as a callable that computes them given an iterator - else - imgmin, imgmax = clims(skipmissingnan(img)) - end - normed = clampednormedview(img, (imgmin, imgmax)) - return _imview(img, normed, stretch, cmap) -end -function _imview(img, normed::AbstractArray{T}, stretch, cmap) where T - - if T <: Union{Missing,<:Number} - TT = typeof(first(skipmissing(normed))) - else - TT = T - end - if TT == Bool - TT = N0f8 - end - - stretchmin = stretch(zero(TT)) - stretchmax = stretch(one(TT)) - - # No color map: use Gray - if isnothing(cmap) - cmap = :grays - end - cscheme = ColorSchemes.colorschemes[cmap] - mapper = mappedarray(img, normed) do pixr, pixn - if ismissing(pixr) || !isfinite(pixr) || ismissing(pixn) || !isfinite(pixn) - # We check pixr in addition to pixn because we want to preserve if the pixels - # are +-Inf - stretched = pixr - else - stretched = stretch(pixn) - end - # We treat NaN/missing values as transparent - return if ismissing(stretched) || isnan(stretched) - RGBA{TT}(0,0,0,0) - # We treat Inf values as white / -Inf as black - elseif isinf(stretched) - if stretched > 0 - RGBA{TT}(1,1,1,1) - else - RGBA{TT}(0,0,0,1) - end - else - RGBA{TT}(get(cscheme::ColorScheme, stretched, (stretchmin, stretchmax))) - end - end - - # Flip image to match conventions of other programs - flipped_view = view(mapper', reverse(axes(mapper,2)),:) - - return maybe_copyheaders(img, flipped_view) -end - - - -# TODO: is this the correct function to extend? -# Instead of using a datatype like N0f32 to interpret integers as fixed point values in [0,1], -# we use a mappedarray to map the native data range (regardless of type) to [0,1] -Images.normedview(img::AstroImage{<:FixedPoint}) = img -function Images.normedview(img::AstroImage{T}) where T - imgmin, imgmax = extrema(skipmissingnan(img)) - Δ = abs(imgmax - imgmin) - normeddata = mappedarray( - pix -> (pix - imgmin)/Δ, - pix_norm -> convert(T, pix_norm*Δ + imgmin), - img - ) - return shareheaders(img, normeddata) -end - -""" - clampednormedview(arr, (min, max)) - -Given an AbstractArray and limits `min,max` return a view of the array -where data between [min, max] are scaled to [0, 1] and datat outside that -range are clamped to [0, 1]. - -See also: normedview -""" -function clampednormedview(img::AbstractArray{T}, lims) where T - imgmin, imgmax = lims - Δ = abs(imgmax - imgmin) - normeddata = mappedarray( - pix -> clamp((pix - imgmin)/Δ, zero(T), one(T)), - pix_norm -> convert(T, pix_norm*Δ + imgmin), - img - ) - return maybe_shareheaders(img, normeddata) -end -function clampednormedview(img::AbstractArray{T}, lims) where T <: Normed - # If the data is in a Normed type and the limits are [0,1] then - # it already lies in that range. - if lims[1] == 0 && lims[2] == 1 - return img - end - imgmin, imgmax = lims - Δ = abs(imgmax - imgmin) - normeddata = mappedarray( - pix -> clamp((pix - imgmin)/Δ, zero(T), one(T)), - pix_norm -> pix_norm*Δ + imgmin, - img - ) - return maybe_shareheaders(img, normeddata) -end -function clampednormedview(img::AbstractArray{Bool}, lims) - return img -end - # Lazily reinterpret the AstroImage as a Matrix{Color}, upon request. # By itself, Images.colorview works fine on AstroImages. But # AstroImages are not normalized to be between [0,1]. So we override From e8eb678e4d40011c9eb9c20d82d8895f3e877ca4 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 31 Jan 2022 10:30:30 -0800 Subject: [PATCH 049/178] Experiment on tracking axes --- src/AstroImages.jl | 72 ++++++++++++++++++++++++++++++++------------- src/plot-recipes.jl | 14 +++++++-- 2 files changed, 64 insertions(+), 22 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index bc31d4a2..59d3b161 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -5,6 +5,7 @@ using Statistics using MappedArrays using ColorSchemes using PlotUtils: zscale +using OffsetArrays export load, save, @@ -138,13 +139,19 @@ end Provides access to a FITS image along with its accompanying headers and WCS information, if applicable. """ -mutable struct AstroImage{T, N, TDat} <: AbstractArray{T,N} +struct AstroImage{T, N, TDat} <: AbstractArray{T,N} data::TDat headers::FITSHeader - wcs::WCSTransform - wcs_stale::Bool + wcs::Ref{WCSTransform} + wcs_stale::Ref{Bool} + wcs_axes::NTuple{N,Union{Int,Colon}} where N end -AstroImage(data::AbstractArray{T,N}, headers, wcs, wcs_stale) where {T,N} = AstroImage{T,N,typeof(data)}(data,headers,wcs,wcs_stale) +# Provide a type alias for a 1D version of our data structure. This is useful when extracting e.g. a spectrum from a data cube and +# retaining the headers and spectral axis information. +const AstroVec{T,TDat} = AstroImage{T,1,TDat} where {T,TDat} +export AstroVec + +AstroImage(data::AbstractArray{T,N}, headers, wcs, wcs_stale, wcs_axes) where {T,N} = AstroImage{T,N,typeof(data)}(data,headers,Ref(wcs),Ref(wcs_stale),wcs_axes) """ @@ -153,11 +160,11 @@ AstroImage(data::AbstractArray{T,N}, headers, wcs, wcs_stale) where {T,N} = Astr Images.arraydata(img::AstroImage) = getfield(img, :data) headers(img::AstroImage) = getfield(img, :headers) function wcs(img::AstroImage) - if getfield(img, :wcs_stale) - setfield!(img, :wcs, wcsfromheaders(img)) - setfield!(img, :wcs_stale, false) + if getfield(img, :wcs_stale)[] + getfield(img, :wcs)[] = wcsfromheaders(img) + getfield(img, :wcs_stale)[] = false end - return getfield(img, :wcs) + return getfield(img, :wcs)[] end struct Comment end @@ -197,12 +204,32 @@ function Base.getindex(img::AstroImage, inds...) dat = getindex(arraydata(img), inds...) # ndims is defined for Numbers but not Missing. # This check is therefore necessary for img[1,1]->missing to work. - if !(eltype(dat) <: Number) || ndims(dat) <= 1 + if !(eltype(dat) <: Number) || ndims(dat) == 0 return dat else - return copyheaders(img, dat) + ax_in = collect(getfield(img, :wcs_axes)) + ax_mask = ax_in .=== (:) + ax_out = Vector{Union{Int,Colon}}(ax_in) + ax_out[ax_mask] .= _filter_inds(inds) + @show ax_out + @show _ranges(inds) + @show typeof(dat) size(dat) + return AstroImage( + OffsetArray(dat, _ranges(inds)...), + deepcopy(headers(img)), + getfield(img, :wcs)[], + getfield(img, :wcs_stale)[], + tuple(ax_out...) + ) + # return copyheaders(img, dat) end end +_filter_inds(inds) = tuple(( + typeof(ind) <: Union{AbstractRange,Colon} ? (:) : ind + for ind in inds +)...) +_ranges(args) = filter(arg -> typeof(arg) <: Union{AbstractRange,Colon}, args) + Base.getindex(img::AstroImage{T}, inds...) where {T<:Colorant} = getindex(arraydata(img), inds...) Base.setindex!(img::AstroImage, v, inds...) = setindex!(arraydata(img), v, inds...) # default fallback for operations on Array @@ -210,9 +237,9 @@ Base.setindex!(img::AstroImage, v, inds...) = setindex!(arraydata(img), v, inds. Base.getindex(img::AstroImage, inds::AbstractString...) = getindex(headers(img), inds...) # accesing header using strings function Base.setindex!(img::AstroImage, v, ind::AbstractString) # modifying header using a string setindex!(headers(img), v, ind) - # Mark the WCS object as beign out of date if this was a WCS header keyword + # Mark the WCS object as being out of date if this was a WCS header keyword if ind ∈ WCS_HEADERS - setfield!(img, :wcs_stale, true) + getfield(img, :wcs_stale)[] = true end end Base.getindex(img::AstroImage, inds::Symbol...) = getindex(img, string.(inds)...) # accessing header using symbol @@ -241,7 +268,6 @@ function Base.push!(img::AstroImage, ::Type{History}, history::AbstractString) push!(hdr.comments, history) end -# TODO: do we need to adjust CRPIX_ when selecting a subset of the image? """ copyheaders(img::AstroImage, data) -> imgnew Create a new image copying the headers of `img` but @@ -250,7 +276,7 @@ headers of `imgnew` does not affect the headers of `img`. See also: [`shareheaders`](@ref). """ copyheaders(img::AstroImage, data::AbstractArray) = - AstroImage(data, deepcopy(headers(img)), getfield(img, :wcs), getfield(img, :wcs_stale)) + AstroImage(data, deepcopy(headers(img)), getfield(img, :wcs), getfield(img, :wcs_stale), getfield(img, :wcs_axes)) export copyheaders """ @@ -260,7 +286,7 @@ using the data of the AbstractArray `data`. The two images have synchronized headers; modifying one also affects the other. See also: [`copyheaders`](@ref). """ -shareheaders(img::AstroImage, data::AbstractArray) = AstroImage(data, headers(img), getfield(img, :wcs), getfield(img, :wcs_stale)) +shareheaders(img::AstroImage, data::AbstractArray) = AstroImage(data, headers(img), getfield(img, :wcs)[], getfield(img, :wcs_stale)[], getfield(img, :wcs_axes)) export shareheaders # Share headers if an AstroImage, do nothing if AbstractArray maybe_shareheaders(img::AstroImage, data) = shareheaders(img, data) @@ -294,6 +320,7 @@ function Base.similar(img::AstroImage) where T deepcopy(headers(img)), getfield(img, :wcs), getfield(img, :wcs_stale), + getfield(img, :wcs_axes), ) end # Getting a similar AstroImage with specific indices will typyically @@ -309,7 +336,8 @@ function Base.similar(img::AstroImage, dims::Tuple) where T dat, deepcopy(headers(img)), getfield(img, :wcs), - getfield(img, :wcs_stale) + getfield(img, :wcs_stale), + getfield(img, :wcs_axes) ) end @@ -349,7 +377,8 @@ function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{AstroImage} dat, deepcopy(headers(img)), getfield(img, :wcs), - getfield(img, :wcs_stale) + getfield(img, :wcs_stale), + getfield(img, :wcs_axes) ) end "`A = find_img(As)` returns the first AstroImage among the arguments." @@ -382,6 +411,8 @@ Given an AbstractArray, return a blank WCSTransform of the appropriate dimensionality. """ emptywcs(data::AbstractArray) = WCSTransform(ndims(data)) +emptywcs(img::AstroImage) = WCSTransform(length(getfield(img, :wcs_axes))) + """ @@ -423,7 +454,7 @@ function AstroImage( # This avoids those computations if the WCS transform is not needed. # It also allows us to create images with invalid WCS headers, # only erroring when/if they are used. - return AstroImage{T,N,typeof(data)}(data, header, wcs, wcs_stale) + return AstroImage{T,N,typeof(data)}(data, header, wcs, wcs_stale, tuple(((:) for _ in 1:N)...)) end AstroImage(data::AbstractArray, wcs::WCSTransform) = AstroImage(data, emptyheaders(), wcs) @@ -460,7 +491,7 @@ function wcsfromheaders(img::AstroImage; relax=WCS.HDR_ALL) if length(wcsout) == 1 return only(wcsout) elseif length(wcsout) == 0 - return emptywcs(arraydata(img)) + return emptywcs(img) else error("Mutiple WCSTransform returned from headers") end @@ -581,7 +612,8 @@ function reset!(img::AstroImage{T,N}) where {T,N} end include("wcs_headers.jl") -include("imview.jl") +# include("imview.jl") +imview(args...;kwargs...) = nothing include("showmime.jl") include("plot-recipes.jl") diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index d968b505..a3d650ec 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -111,8 +111,18 @@ end # In astropy, the ticks are actually tilted to reflect this, though in general # the transformation from pixel to coordinates can be non-linear and curved. - ax = haskey(plotattributes, :axes) ? plotattributes[:axes] : (1,2) - coords = haskey(plotattributes, :coords) ? plotattributes[:coords] : ones(wcs(img).naxis) + # ax = haskey(plotattributes, :axes) ? plotattributes[:axes] : (1,2) + # coords = haskey(plotattributes, :coords) ? plotattributes[:coords] : ones(wcs(img).naxis) + ax = findall(==(:), getfield(img, :wcs_axes)) + j = 0 + coords = map(getfield(img, :wcs_axes)) do coord + j += 1 + if coord === (:) + first(axes(img,j)) + else + coord + end + end minx = first(axes(img,ax[2])) maxx = last(axes(img,ax[2])) From 3a4a0613e89c3e7876c17d9cc2258d241ce1d37f Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 31 Jan 2022 10:33:37 -0800 Subject: [PATCH 050/178] Improving wcs grid calculation code --- Project.toml | 1 + src/AstroImages.jl | 15 ++- src/imview.jl | 284 ++++++++++++++++++++++++++++++++++++++++++++ src/plot-recipes.jl | 219 ++++++++++++++++++---------------- src/showmime.jl | 1 - 5 files changed, 412 insertions(+), 108 deletions(-) create mode 100644 src/imview.jl diff --git a/Project.toml b/Project.toml index 415f47f7..7e294144 100644 --- a/Project.toml +++ b/Project.toml @@ -13,6 +13,7 @@ Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" InlineStrings = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48" Interact = "c601a237-2ae4-5e1e-952c-7a85b0c7eef1" MappedArrays = "dbb5928d-eab1-5f90-85c2-b9b0edb7c900" +OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" diff --git a/src/AstroImages.jl b/src/AstroImages.jl index bc31d4a2..10d33451 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -6,6 +6,8 @@ using MappedArrays using ColorSchemes using PlotUtils: zscale +using OffsetArrays + export load, save, AstroImage, @@ -233,7 +235,13 @@ function Base.getindex(img::AstroImage, ::Type{Comment}) ii = findall(==("COMMENT"), hdr.keys) return view(hdr.comments, ii) end -# Adding new history entries +# Adding new comment and history entries +function Base.push!(img::AstroImage, ::Type{Comment}, history::AbstractString) + hdr = headers(img) + push!(hdr.keys, "HISTORY") + push!(hdr.values, nothing) + push!(hdr.comments, history) +end function Base.push!(img::AstroImage, ::Type{History}, history::AbstractString) hdr = headers(img) push!(hdr.keys, "HISTORY") @@ -595,7 +603,7 @@ function __init__() if isdefined(Base.Experimental, :register_error_hint) Base.Experimental.register_error_hint(MethodError) do io, exc, argtypes, kwargs if exc.f == imview && first(argtypes) <: AbstractArray && ndims(first(argtypes)) != 2 - print(io, "\n`imview` is only supported on 2D arrays.\nIf you have a cube, try viewing one slice at a time.") + print(io, "\nThe `imview` function only supports 2D images. If you have a cube, try viewing one slice at a time.\n") end end end @@ -610,7 +618,9 @@ TODO: * contrast/bias? * interactive (Jupyter) * Plots & Makie recipes +* Plots: vertical/horizotat axes from m106 * indexing +* recenter that updates indexes and CRPIX * cubes * RGB and other composites * tests @@ -621,6 +631,5 @@ TODO: * FITSIO PR/issue (performance) * PlotUtils PR/issue (zscale with iteratble) -* WCS PR/issue (locking) =# \ No newline at end of file diff --git a/src/imview.jl b/src/imview.jl new file mode 100644 index 00000000..b934da08 --- /dev/null +++ b/src/imview.jl @@ -0,0 +1,284 @@ +# These reproduce the behaviour of DS9 according to http://ds9.si.edu/doc/ref/how.html +logstretch(x,a=1000) = log(a*x+1)/log(a) +powstretch(x,a=1000) = (a^x - 1)/a +sqrtstretch = sqrt +squarestretch(x) = x^2 +asinhstretch(x) = asinh(10x)/3 +sinhstretch(x) = sinh(3x)/10 +# These additional stretches reproduce behaviour from astropy +powerdiststretch(x, a=1000) = (a^x - 1) / (a - 1) + +""" + percent(99.5) + +Returns a function that calculates display limits that include the given +percent of the image data. + +Example: +```julia +julia> imview(img, clims=percent(90)) +``` +This will set the limits to be the 5th percentile to the 95th percentile. +""" +function percent(perc::Number) + trim = (1 - perc/100)/2 + clims(data) = quantile(data, (trim, 1-trim)) + clims(data::AbstractMatrix) = quantile(vec(data), (trim, 1-trim)) + return clims +end + +const _default_cmap = Ref{Union{Symbol,Nothing}}(nothing) +const _default_clims = Ref{Any}(percent(99.5)) +const _default_stretch = Ref{Any}(identity) + +""" + set_cmap!(cmap::Symbol) + set_cmap!(cmap::Nothing) + +Alter the default color map used to display images when using +`imview` or displaying an AstroImage. +""" +function set_cmap!(cmap) + if cmap ∉ keys(ColorSchemes.colorschemes) + throw(KeyError("$cmap not found in ColorSchemes.colorschemes")) + end + _default_cmap[] = cmap +end +""" + set_clims!(clims::Tuple) + set_clims!(clims::Function) + +Alter the default limits used to display images when using +`imview` or displaying an AstroImage. +""" +function set_clims!(clims) + _default_clims[] = clims +end +""" + set_stretch!(stretch::Function) + +Alter the default value stretch functio used to display images when using +`imview` or displaying an AstroImage. +""" +function set_stretch!(stretch) + _default_stretch[] = stretch +end + + + +""" +Helper to iterate over data skipping missing and non-finite values. +""" +skipmissingnan(itr) = Iterators.filter(el->!ismissing(el) && isfinite(el), itr) + +""" + imview(img; clims=extrema, stretch=identity, cmap=nothing) + +Create a read only view of an array or AstroImage mapping its data values +to Colors according to `clims`, `stretch`, and `cmap`. + +The data is first clamped to `clims`, which can either be a tuple of (min, max) +values or a function accepting an iterator of pixel values that returns (min, max). +By default, `clims=extrema` i.e. the minimum and maximum of `img`. +Convenient functions to use for `clims` are: +`extrema`, `zscale`, and `percent(p)` + +Next, the data is rescaled to [0,1] and remapped according to the function `stretch`. +Stretch can be any monotonic fuction mapping values in the range [0,1] to some range [a,b]. +Note that `log(0)` is not defined so is not directly supported. +For a list of convenient stretch functions, see: +`logstretch`, `powstretch`, `squarestretch`, `asinhstretch`, `sinhstretch`, `powerdiststretch` + +Finally the data is mapped to RGB values according to `cmap`. If cmap is `nothing`, +grayscale is used. ColorSchemes.jl defines hundreds of colormaps. A few nice ones for +images include: `:viridis`, `:magma`, `:plasma`, `:thermal`, and `:turbo`. + +Crucially, this function returns a view over the underlying data. If `img` is updated +then those changes will be reflected by this view with the exception of `clims` which +is not recalculated. + +Note: if clims or stretch is a function, the pixel values passed in are first filtered +to remove non-finite or missing values. + +### Defaults +The default values of `clims`, `stretch`, and `cmap` are `extrema`, `identity`, and `nothing` +respectively. +You may alter these defaults using `AstroImages.set_clims!`, `AstroImages.set_stretch!`, and +`AstroImages.set_cmap!`. + +### Automatic Display +Arrays wrapped by `AstroImage()` get displayed as images automatically by calling +`imview` on them with the default settings when using displays that support showing PNG images. + +### Missing data +Pixels that are `NaN` or `missing` will be displayed as transparent when `cmap` is set +or black if. ++/- Inf will be displayed as black or white respectively. + +### Exporting Images +The view returned by `imview` can be saved using general `FileIO.save` methods. +Example: +```julia +v = imview(data, cmap=:magma, stretch=asinhstretch, clims=percent(95)) +save("output.png", v) +``` +""" +function imview( + img::AbstractMatrix{T}; + clims=_default_clims[], + stretch=_default_stretch[], + cmap=_default_cmap[], +) where {T} + + # TODO: catch this in `show` instead of here. + isempt = isempty(img) + if isempt + return + end + # Users will occaisionally pass in data that is 0D, filled with NaN, or filled with missing. + # We still need to do something reasonable in those caes. + nonempty = any(x-> !ismissing(x) && isfinite(x), img) + if !nonempty + return + end + + # TODO: Images.jl has logic to downsize huge images before displaying them. + # We should use that here before applying all this processing instead of + # letting Images.jl handle it after. + + # Users can pass clims as an array or tuple containing the minimum and maximum values + if typeof(clims) <: AbstractArray || typeof(clims) <: Tuple + if length(clims) != 2 + error("clims must have exactly two values if provided.") + end + imgmin = first(clims) + imgmax = last(clims) + # Or as a callable that computes them given an iterator + else + imgmin, imgmax = clims(skipmissingnan(img)) + end + normed = clampednormedview(img, (imgmin, imgmax)) + return _imview(img, normed, stretch, cmap) +end +function _imview(img, normed::AbstractArray{T}, stretch, cmap) where T + + if T <: Union{Missing,<:Number} + TT = typeof(first(skipmissing(normed))) + else + TT = T + end + if TT == Bool + TT = N0f8 + end + + stretchmin = stretch(zero(TT)) + stretchmax = stretch(one(TT)) + + # Peviously no colormap would fall back to Gray, but + # it's simpler to keep a single codepath and use the :grays + # color scheme. + if isnothing(cmap) + cmap = :grays + end + cscheme = ColorSchemes.colorschemes[cmap] + img_no = OffsetArrays.no_offset_view(img) + normed_no = OffsetArrays.no_offset_view(normed) + mapper = mappedarray(img_no, normed_no) do pixr, pixn + if ismissing(pixr) || !isfinite(pixr) || ismissing(pixn) || !isfinite(pixn) + # We check pixr in addition to pixn because we want to preserve if the pixels + # are +-Inf + stretched = pixr + else + stretched = stretch(pixn) + end + # We treat NaN/missing values as transparent + return if ismissing(stretched) || isnan(stretched) + RGBA{TT}(0,0,0,0) + # We treat Inf values as white / -Inf as black + elseif isinf(stretched) + if stretched > 0 + RGBA{TT}(1,1,1,1) + else + RGBA{TT}(0,0,0,1) + end + else + RGBA{TT}(get(cscheme::ColorScheme, stretched, (stretchmin, stretchmax))) + end + end + + # Flip image to match conventions of other programs + # flipped_view = view(mapper', reverse(axes(mapper,2)),:) + # return maybe_copyheaders(img, flipped_view) + # return maybe_copyheaders(img, mapper) + + # flipped_view = OffsetArray( + # view( + # mapper', + # reverse(axes(mapper,1)), + # :, + # ), + # axes(img)... + # ) + flipped_view = view( + mapper', + reverse(axes(mapper,2)), + :, + ) + + return maybe_copyheaders(img, OffsetArray(flipped_view, axes(img,2), axes(img,1))) +end + + + +# TODO: is this the correct function to extend? +# Instead of using a datatype like N0f32 to interpret integers as fixed point values in [0,1], +# we use a mappedarray to map the native data range (regardless of type) to [0,1] +Images.normedview(img::AstroImage{<:FixedPoint}) = img +function Images.normedview(img::AstroImage{T}) where T + imgmin, imgmax = extrema(skipmissingnan(img)) + Δ = abs(imgmax - imgmin) + normeddata = mappedarray( + pix -> (pix - imgmin)/Δ, + pix_norm -> convert(T, pix_norm*Δ + imgmin), + img + ) + return shareheaders(img, normeddata) +end + +""" + clampednormedview(arr, (min, max)) + +Given an AbstractArray and limits `min,max` return a view of the array +where data between [min, max] are scaled to [0, 1] and datat outside that +range are clamped to [0, 1]. + +See also: normedview +""" +function clampednormedview(img::AbstractArray{T}, lims) where T + imgmin, imgmax = lims + Δ = abs(imgmax - imgmin) + normeddata = mappedarray( + pix -> clamp((pix - imgmin)/Δ, zero(T), one(T)), + pix_norm -> convert(T, pix_norm*Δ + imgmin), + img + ) + return maybe_shareheaders(img, normeddata) +end +function clampednormedview(img::AbstractArray{T}, lims) where T <: Normed + # If the data is in a Normed type and the limits are [0,1] then + # it already lies in that range. + if lims[1] == 0 && lims[2] == 1 + return img + end + imgmin, imgmax = lims + Δ = abs(imgmax - imgmin) + normeddata = mappedarray( + pix -> clamp((pix - imgmin)/Δ, zero(T), one(T)), + pix_norm -> pix_norm*Δ + imgmin, + img + ) + return maybe_shareheaders(img, normeddata) +end +function clampednormedview(img::AbstractArray{Bool}, lims) + return img +end \ No newline at end of file diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index d968b505..81442159 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -124,28 +124,30 @@ end gridspec = wcsgridspec(wcsg) xticks --> (gridspec.tickpos1x, wcslabels(wcs(img), 1, gridspec.tickpos1w)) - # xticks --> wcsticks(wcsg, 1, gridspec) xguide --> ctype_label(wcs(img).ctype[1], wcs(img).radesys) yticks --> (gridspec.tickpos2x, wcslabels(wcs(img), 2, gridspec.tickpos2w)) - # yticks --> wcsticks(wcsg, 2, gridspec) yguide --> ctype_label(wcs(img).ctype[2], wcs(img).radesys) # To ensure the physical axis tick labels are correct the axes must be # tight to the image - xlims := first(axes(img,2)), last(axes(img,2)) - ylims := first(axes(img,1)), last(axes(img,1)) + xl = first(axes(img,2)), last(axes(img,2)) + yl = first(axes(img,1)), last(axes(img,1)) + ylims --> yl + xlims --> xl end - # TODO: also disable equal aspect ratio if the scales are totally different - # aspect_ratio := :equal + # Disable equal aspect ratios if the scales are totally different + if max(size(img)...)/min(size(img)...) >= 7 + aspect_ratio --> :none + end # We have to do a lot of flipping to keep the orientation corect yflip := false xflip := false @series begin - view(arraydata(img), reverse(axes(img,1)),:) + axes(img,2), axes(img,1), view(arraydata(img), reverse(axes(img,1)),:) end # If wcs=true (default) and grid=true (not default), overplot a WCS @@ -156,7 +158,7 @@ end # Plot the WCSGrid as a second series (actually just lines) @series begin - wcsg + wcsg, gridspec end end return @@ -173,35 +175,28 @@ WCSGrid(w,extent,ax) = WCSGrid(w,extent,ax,ones(length(ax))) """ -Generate nice labels from a WCSTransform, axis, and known positions. - -INPUT -w: a WCSTransform -axnum: the index of the axis we want ticks for -axnumᵀ: the index of the axis we are plotting against -coords: the position in all coordinates for this plot. The value a axnum and axnumᵀ is igored. + wcsticks(img, axnum) -`coords` is important for showing 2D coords of a 3+D cube as we need to know -our position along the other axes for accurate tick positions. +Generate nice tick labels for an AstroImage along axis `axnum` +Returns a vector of pixel positions and a vector of strings. -OUTPUT -tickpos: tick positions in pixels for this axis -ticklabels: tick labels for each position +Example: +plot(img, xticks=wcsticks(img, 1), yticks=wcsticks(img, 2)) """ -# Most of the complexity of this function is making sure everything -# generalizes to N different, possiby skewed axes, where a change in -# the opposite coordinate or even an unplotted coordinate affects -# the tick labels. -function wcslabels(img::AstroImage, axnum) +function wcsticks(img::AstroImage, axnum) gs = wcsgridspec(WCSGrid(img)) + tickposx = axnum == 1 ? gs.tickpos1x : gs.tickpos2x tickposw = axnum == 1 ? gs.tickpos1w : gs.tickpos2w - return wcslabels( + return tickposx, wcslabels( wcs(img), axnum, tickposw ) end +# Function to generate nice string coordinate labels given a WCSTransform, axis number, +# and a vector of tick positions in world coordinates. +# This is used for labelling ticks and for annotating grid lines. function wcslabels(w::WCSTransform, axnum, tickposw) if length(tickposw) == 0 @@ -341,14 +336,11 @@ end -# TODO: the wcs parameter is not getting forwardded correctly. Use plot recipe system for this. - -# This recipe plots as AstroImage of color data as an image series (not heatmap). -# This lets us also plot color composites e.g. in WCS coordinates. -@recipe function f(wcsg::WCSGrid) +# Recipe for a WCSGrid with lines, optional ticks (on by default), +# and optional grid labels (off by defaut). +# The AstroImage plotrecipe uses this recipe for grid lines if `grid=true`. +@recipe function f(wcsg::WCSGrid, gridspec=wcsgridspec(wcsg)) label --> "" - - gridspec = wcsgridspec(wcsg) xs, ys = wcsgridlines(gridspec) if haskey(plotattributes, :foreground_color_grid) @@ -368,8 +360,8 @@ end xguide --> ctype_label(wcsg.w.ctype[wcsg.ax[1]], wcsg.w.radesys) yguide --> ctype_label(wcsg.w.ctype[wcsg.ax[2]], wcsg.w.radesys) - xlims := wcsg.extent[1], wcsg.extent[2] - ylims := wcsg.extent[3], wcsg.extent[4] + xlims --> wcsg.extent[1], wcsg.extent[2] + ylims --> wcsg.extent[3], wcsg.extent[4] grid := false tickdirection := :none @@ -379,7 +371,8 @@ end @series xs, ys - # We can optionally annotate the grid with their coordinates + # We can optionally annotate the grid with their coordinates. + # These come after the grid lines so they appear overtop. if annotate @series begin # TODO: why is this reverse necessary? @@ -388,7 +381,7 @@ end seriestype := :line linewidth := 0 series_annotations := [ - Main.Plots.text(" $l", :right, :bottom, textcolor, 8, rotation=(-90 <= r <= 90) ? r : r+180) + Main.Plots.text(" $l", :right, :bottom, textcolor, 8, rotation=(-95 <= r <= 95) ? r : r+180) for (l, r) in zip(ticklabels, rotations) ] gridspec.annotations1x, gridspec.annotations1y @@ -399,7 +392,7 @@ end seriestype := :line linewidth := 0 series_annotations := [ - Main.Plots.text(" $l", :right, :bottom, textcolor, 8, rotation=(-90 <= r <= 90) ? r : r+180) + Main.Plots.text(" $l", :right, :bottom, textcolor, 8, rotation=(-95 <= r <= 95) ? r : r+180) for (l, r) in zip(ticklabels, rotations) ] gridspec.annotations2x, gridspec.annotations2y @@ -410,32 +403,52 @@ end return end +# Helper: true if all elements in vector are equal to each other. allequal(itr) = all(==(first(itr)), itr) +# This function is responsible for actually laying out grid lines for a WCSGrid, +# ensuring they don't exceed the plot bounds, finding where they intersect the axes, +# and picking tick locations at the appropriate intersections with the left and +# bottom axes. function wcsgridspec(wsg::WCSGrid) + # Most of the complexity of this function is making sure everything + # generalizes to N different, possiby skewed axes, where a change in + # the opposite coordinate or even an unplotted coordinate affects + # the grid. - # x and y denote pixel coordinates (along `ax`), u and v are world coordinates along same? + # x and y denote pixel coordinates (along `ax`), u and v are world coordinates roughly along same. ax = collect(wsg.ax) coordsx = convert(Vector{Float64}, collect(wsg.coords)) minx, maxx, miny, maxy = wsg.extent + @show wsg.extent + # Find the extent of this slice in world coordinates - posxy = repeat(coordsx, 1, 4) + # posxy = repeat(coordsx, 1, 4) + # posxy[ax,1] .= (minx,miny) + # posxy[ax,2] .= (minx,maxy) + # posxy[ax,3] .= (maxx,miny) + # posxy[ax,4] .= (maxx,maxy) + # posuv = pix_to_world(wsg.w, posxy) + # (minu, maxu), (minv, maxv) = extrema(posuv, dims=2) + posxy = repeat(coordsx, 1, 6) posxy[ax,1] .= (minx,miny) posxy[ax,2] .= (minx,maxy) - posxy[ax,3] .= (maxx,miny) - posxy[ax,4] .= (maxx,maxy) + posxy[ax,3] .= (minx,miny+(maxy-miny)/2) + posxy[ax,4] .= (minx+(maxx-minx)/2,miny) + posxy[ax,5] .= (maxx,miny) + posxy[ax,6] .= (maxx,maxy) posuv = pix_to_world(wsg.w, posxy) (minu, maxu), (minv, maxv) = extrema(posuv, dims=2) - # In general, grid can be curved when plotted back against the image. - # So we will need to sample multiple points along the grid. + # In general, grid can be curved when plotted back against the image, + # so we will need to sample multiple points along the grid. # TODO: find a good heuristic for this based on the curvature. N_points = 50 urange = range(minu, maxu, length=N_points) vrange = range(minv, maxv, length=N_points) - # Find nice grid spacings + # Find nice grid spacings using PlotUtils.optimize_ticks # These heuristics can probably be improved # TODO: this does not handle coordinates that wrap around Q=[(1.0,1.0), (3.0, 0.8), (2.0, 0.7), (5.0, 0.5)] @@ -445,8 +458,10 @@ function wcsgridspec(wsg::WCSGrid) tickpos2x = Float64[] tickpos2w = Float64[] - tickslopes2x = Float64[] gridlinesxy2 = NTuple{2,Vector{Float64}}[] + # Not all grid lines will intersect the x & y axes nicely. + # If we don't get enough valid tick marks (at least 2) loop again + # requesting more locations up to three times. local tickposv j = 3 while length(tickpos2x) < 2 && j > 0 @@ -456,10 +471,11 @@ function wcsgridspec(wsg::WCSGrid) j -= 1 tickposv = optimize_ticks(6minv, 6maxv; Q, k_min, k_ideal, k_max)[1]./6 + # tickposv = [10:60:360;] + # tickposv = [-13.834999999999999, -13.83, -13.825000000000001, -13.82, -13.815, -13.81] empty!(tickpos2x) empty!(tickpos2w) - empty!(tickslopes2x) empty!(gridlinesxy2) for tickv in tickposv # Make sure we handle unplotted slices correctly. @@ -479,15 +495,15 @@ function wcsgridspec(wsg::WCSGrid) in_axes = in_horz_ax .& in_vert_ax if count(in_axes) < 2 continue - # elseif all(in_axes) - # point_entered = [ - # posxy[ax[1],begin] - # posxy[ax[2],begin] - # ] - # point_exitted = [ - # posxy[ax[1],end] - # posxy[ax[2],end] - # ] + elseif all(in_axes) + point_entered = [ + posxy[ax[1],begin] + posxy[ax[2],begin] + ] + point_exitted = [ + posxy[ax[1],end] + posxy[ax[2],end] + ] elseif allequal(posxy[ax[1],findfirst(in_axes):findlast(in_axes)]) point_entered = [ posxy[ax[1],max(begin,findfirst(in_axes)-1)] @@ -499,9 +515,6 @@ function wcsgridspec(wsg::WCSGrid) # posxy[ax[2],min(end,findlast(in_axes)+1)] maxy ] - push!(tickpos2x, posxy[ax[2],findfirst(in_axes)]) - push!(tickpos2w, tickv) - push!(tickslopes2x, 0) # Vertical grid lines elseif allequal(posxy[ax[2],findfirst(in_axes):findlast(in_axes)]) point_entered = [ @@ -512,9 +525,6 @@ function wcsgridspec(wsg::WCSGrid) maxx #posxy[ax[1],min(end,findlast(in_axes)+1)] posxy[ax[2],min(end,findlast(in_axes)+1)] ] - push!(tickpos2x, posxy[ax[2],1]) - push!(tickpos2w, tickv) - push!(tickslopes2x, π/2) else # Use the masks to pick an x,y point inside the axes and an # x,y point outside the axes. @@ -536,11 +546,6 @@ function wcsgridspec(wsg::WCSGrid) x = abs(x1-maxx) < abs(x1-minx) ? maxx : minx x = clamp(x,minx,maxx) y = m1*x+b1 - if abs(x-minx) < abs(x-maxx) - push!(tickpos2x, y) - push!(tickpos2w, tickv) - push!(tickslopes2x, atan(m1, 1)) - end else # We must find where it enters the plot from # bottom or top @@ -575,11 +580,6 @@ function wcsgridspec(wsg::WCSGrid) x = abs(x1-maxx) < abs(x1-minx) ? maxx : minx x = clamp(x,minx,maxx) y = m2*x+b2 - if abs(x-minx) < abs(x-maxx) - push!(tickpos2x, y) - push!(tickpos2w, tickv) - push!(tickslopes2x, atan(m2, 0)) - end else # We must find where it enters the plot from # bottom or top @@ -595,6 +595,18 @@ function wcsgridspec(wsg::WCSGrid) ] end + + if point_entered[ax[1]] == minx + push!(tickpos2x, point_entered[ax[2]]) + push!(tickpos2w, tickv) + end + if point_exitted[ax[1]] == minx + push!(tickpos2x, point_exitted[ax[2]]) + push!(tickpos2w, tickv) + end + @show point_entered minx maxx miny maxy + + posxy_neat = [point_entered posxy[[ax[1],ax[2]],in_axes] point_exitted] # posxy_neat = posxy # TODO: do unplotted other axes also need a fit? @@ -613,8 +625,10 @@ function wcsgridspec(wsg::WCSGrid) k_max = 10 tickpos1x = Float64[] tickpos1w = Float64[] - tickslopes1x = Float64[] gridlinesxy1 = NTuple{2,Vector{Float64}}[] + # Not all grid lines will intersect the x & y axes nicely. + # If we don't get enough valid tick marks (at least 2) loop again + # requesting more locations up to three times. local tickposu j = 3 while length(tickpos1x) < 2 && j > 0 @@ -625,9 +639,11 @@ function wcsgridspec(wsg::WCSGrid) tickposu = optimize_ticks(6minu, 6maxu; Q, k_min, k_ideal, k_max)[1]./6 + # tickposu = [274.7, 274.705, 274.71, 274.715, 274.71999999999997, 274.72499999999997, 274.72999999999996] + # tickposu = [10:60:360;] + empty!(tickpos1x) empty!(tickpos1w) - empty!(tickslopes1x) empty!(gridlinesxy1) for ticku in tickposu # Make sure we handle unplotted slices correctly. @@ -649,15 +665,15 @@ function wcsgridspec(wsg::WCSGrid) if count(in_axes) < 2 continue - # elseif all(in_axes) - # point_entered = [ - # posxy[ax[1],begin] - # posxy[ax[2],begin] - # ] - # point_exitted = [ - # posxy[ax[1],end] - # posxy[ax[2],end] - # ] + elseif all(in_axes) + point_entered = [ + posxy[ax[1],begin] + posxy[ax[2],begin] + ] + point_exitted = [ + posxy[ax[1],end] + posxy[ax[2],end] + ] # Horizontal grid lines elseif allequal(posxy[ax[1],findfirst(in_axes):findlast(in_axes)]) point_entered = [ @@ -668,9 +684,8 @@ function wcsgridspec(wsg::WCSGrid) posxy[ax[1],findlast(in_axes)] maxy ] - push!(tickpos1x, posxy[ax[1],findfirst(in_axes)]) - push!(tickpos1w, ticku) - push!(tickslopes1x, 0) + # push!(tickpos1x, posxy[ax[1],findfirst(in_axes)]) + # push!(tickpos1w, ticku) # Vertical grid lines elseif allequal(posxy[ax[2],findfirst(in_axes):findlast(in_axes)]) point_entered = [ @@ -681,9 +696,6 @@ function wcsgridspec(wsg::WCSGrid) maxx posxy[ax[2],findfirst(in_axes)] ] - push!(tickpos1x, posxy[ax[2],1]) - push!(tickpos1w, ticku) - push!(tickslopes1x, π/2) else # Use the masks to pick an x,y point inside the axes and an # x,y point outside the axes. @@ -709,13 +721,8 @@ function wcsgridspec(wsg::WCSGrid) # We must find where it enters the plot from # bottom or top x = abs(y1-maxy) < abs(y1-miny) ? (maxy-b1)/m1 : (miny-b1)/m1 - # x = clamp(x,minx,maxx) + x = clamp(x,minx,maxx) y = m1*x+b1 - if abs(y-miny) < abs(y-maxy) - push!(tickpos1x, x) - push!(tickpos1w, ticku) - push!(tickslopes1x, atan(m1, 1)) - end end # From here, do a linear fit to find the intersection with the axis. @@ -749,11 +756,6 @@ function wcsgridspec(wsg::WCSGrid) x = abs(y1-maxy) < abs(y1-miny) ? (maxy-b2)/m2 : (miny-b2)/m2 x = clamp(x,minx,maxx) y = m2*x+b2 - if abs(y-miny) < abs(y-maxy) - push!(tickpos1x, x) - push!(tickpos1w, ticku) - push!(tickslopes1x, atan(m2, 1)) - end end # From here, do a linear fit to find the intersection with the axis. @@ -766,6 +768,15 @@ function wcsgridspec(wsg::WCSGrid) posxy_neat = [point_entered posxy[[ax[1],ax[2]],in_axes] point_exitted] # TODO: do unplotted other axes also need a fit? + if point_entered[ax[2]] == miny + push!(tickpos1x, point_entered[ax[1]]) + push!(tickpos1w, ticku) + end + if point_exitted[ax[2]] == miny + push!(tickpos1x, point_exitted[ax[1]]) + push!(tickpos1w, ticku) + end + gridlinexy = ( posxy_neat[ax[1],:], posxy_neat[ax[2],:] @@ -773,8 +784,9 @@ function wcsgridspec(wsg::WCSGrid) push!(gridlinesxy1, gridlinexy) end end + @show tickpos1x - # Grid annotations + # Grid annotations are simpler: annotations1w = Float64[] annotations1x = Float64[] annotations1y = Float64[] @@ -830,16 +842,13 @@ function wcsgridspec(wsg::WCSGrid) push!(annotations2θ, θ) end - return (; gridlinesxy1, gridlinesxy2, tickpos1x, tickpos1w, - tickslopes1x, tickpos2x, tickpos2w, - tickslopes2x, annotations1w, annotations1x, @@ -853,6 +862,8 @@ function wcsgridspec(wsg::WCSGrid) ) end +# From a WCSGrid, return just the grid lines as a single pair of x & y coordinates +# suitable for plotting. function wcsgridlines(wcsg::WCSGrid) return wcsgridlines(wcsgridspec(wcsg)) end diff --git a/src/showmime.jl b/src/showmime.jl index 7bc6709f..237ac451 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -32,7 +32,6 @@ Base.show(io::IO, mime::MIME"image/png", img::AstroImage{T,2}; kwargs...) where - # Lazily reinterpret the AstroImage as a Matrix{Color}, upon request. # By itself, Images.colorview works fine on AstroImages. But # AstroImages are not normalized to be between [0,1]. So we override From 0fc7d9edd31a585784aa79c323b093c9dd09a176 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Wed, 9 Mar 2022 14:01:59 -0800 Subject: [PATCH 051/178] Prelim work on tracking indices and slices --- src/AstroImages.jl | 5 +- src/plot-recipes.jl | 326 +++++++++++++++++++++++++++++--------------- 2 files changed, 219 insertions(+), 112 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index f437e31c..2f53316c 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -284,7 +284,7 @@ headers of `imgnew` does not affect the headers of `img`. See also: [`shareheaders`](@ref). """ copyheaders(img::AstroImage, data::AbstractArray) = - AstroImage(data, deepcopy(headers(img)), getfield(img, :wcs), getfield(img, :wcs_stale), getfield(img, :wcs_axes)) + AstroImage(data, deepcopy(headers(img)), getfield(img, :wcs)[], getfield(img, :wcs_stale)[], getfield(img, :wcs_axes)) export copyheaders """ @@ -620,8 +620,7 @@ function reset!(img::AstroImage{T,N}) where {T,N} end include("wcs_headers.jl") -# include("imview.jl") -imview(args...;kwargs...) = nothing +include("imview.jl") include("showmime.jl") include("plot-recipes.jl") diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 3694706d..cba71f02 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -58,13 +58,13 @@ save("output.png", v) # This recipe promotes AstroImages of numerical data into full color using # imview(). @recipe function f( - img::AstroImage{T}; + img::AstroImage{T,2}; clims=_default_clims[], stretch=_default_stretch[], cmap=_default_cmap[], ) where {T<:Number} - # TODO: we often plot an AstroImage{<:Number} which hasn't yet had + # We often plot an AstroImage{<:Number} which hasn't yet had # its wcs cached (wcs_stale=true) and we make an image view here. # That means we may have to keep recomputing the WCS on each plot call # since the result is stored in the imview instead of original image. @@ -74,104 +74,191 @@ save("output.png", v) wcs(img) end - # We don't to override e.g. heatmaps and histograms + # We don't to override e.g. histograms if haskey(plotattributes, :seriestype) return arraydata(img) - end - - # We currently use the AstroImages defaults. If unset, we could - # instead follow the plot theme. - iv = imview(img; clims, stretch, cmap) - return iv -end + else + # We currently use the AstroImages defaults. If unset, we could + # instead follow the plot theme. + imgv = imview(img; clims, stretch, cmap) + + xgrid --> true + ygrid --> true + + # By default, disable the colorbar. + # Plots.jl does no give us sufficient control to make sure the range and ticks + # are correct after applying a non-linear stretch + # colorbar := false + + # we have a wcs flag (from the image by default) so that users can skip over + # plotting in physical coordinates. This is especially important + # if the WCS headers are mallformed in some way. + if !haskey(plotattributes, :wcs) || plotattributes[:wcs] + + # TODO: fill out coordinates array considering offset indices and slices + # out of cubes (tricky!) + + # Note: if the axes are on unusual sides (e.g. y-axis at right, x-axis at top) + # then these coordinates are not correct. They are only correct exactly + # along the axis. + # In astropy, the ticks are actually tilted to reflect this, though in general + # the transformation from pixel to coordinates can be non-linear and curved. + + # ax = haskey(plotattributes, :axes) ? plotattributes[:axes] : (1,2) + # coords = haskey(plotattributes, :coords) ? plotattributes[:coords] : ones(wcs(img).naxis) + ax = findall(==(:), getfield(imgv, :wcs_axes)) + j = 0 + coords = map(getfield(imgv, :wcs_axes)) do coord + j += 1 + if coord === (:) + first(axes(imgv,j)) + else + coord + end + end + minx = first(axes(imgv,2)) + maxx = last(axes(imgv,2)) + miny = first(axes(imgv,1)) + maxy = last(axes(imgv,1)) + extent = (minx, maxx, miny, maxy) + + wcsg = WCSGrid(wcs(imgv), extent, ax, coords) + gridspec = wcsgridspec(wcsg) + + xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), ax[1], gridspec.tickpos1w)) + xguide --> ctype_label(wcs(imgv).ctype[ax[1]], wcs(imgv).radesys) + + yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), ax[2], gridspec.tickpos2w)) + yguide --> ctype_label(wcs(imgv).ctype[ax[2]], wcs(imgv).radesys) + + # To ensure the physical axis tick labels are correct the axes must be + # tight to the image + xl = first(axes(imgv,2)), last(axes(imgv,2)) + yl = first(axes(imgv,1)), last(axes(imgv,1)) + ylims --> yl + xlims --> xl + end + # Disable equal aspect ratios if the scales are totally different + if max(size(imgv)...)/min(size(imgv)...) >= 7 + aspect_ratio --> :none + end -# This recipe plots as AstroImage of color data as an image series (not heatmap). -# This lets us also plot color composites e.g. in WCS coordinates. -@recipe function f( - img::AstroImage{T}; -) where {T<:Colorant} + # We have to do a lot of flipping to keep the orientation corect + yflip := false + xflip := false - # By default, disable the colorbar. - # Plots.jl does no give us sufficient control to make sure the range and ticks - # are correct after applying a non-linear stretch - # colorbar := false + @series begin + # axes(imgv,2), axes(imgv,1), view(arraydata(imgv), reverse(axes(imgv,1)),:) + # axes(imgv,2) .- 0.5, axes(imgv,1) .- 0.5, + # @show size(view(arraydata(imgv), reverse(axes(imgv,1)),:)) + view(arraydata(imgv), reverse(axes(imgv,1)),:) + end - # we have a wcs flag (from the image by default) so that users can skip over - # plotting in physical coordinates. This is especially important - # if the WCS headers are mallformed in some way. - if !haskey(plotattributes, :wcs) || plotattributes[:wcs] + # If wcs=true (default) and grid=true (not default), overplot a WCS + # grid. + if (!haskey(plotattributes, :wcs) || plotattributes[:wcs]) && + haskey(plotattributes, :xgrid) && plotattributes[:xgrid] && + haskey(plotattributes, :ygrid) && plotattributes[:ygrid] - # TODO: fill out coordinates array considering offset indices and slices - # out of cubes (tricky!) - - # Note: if the axes are on unusual sides (e.g. y-axis at right, x-axis at top) - # then these coordinates are not correct. They are only correct exactly - # along the axis. - # In astropy, the ticks are actually tilted to reflect this, though in general - # the transformation from pixel to coordinates can be non-linear and curved. - - # ax = haskey(plotattributes, :axes) ? plotattributes[:axes] : (1,2) - # coords = haskey(plotattributes, :coords) ? plotattributes[:coords] : ones(wcs(img).naxis) - ax = findall(==(:), getfield(img, :wcs_axes)) - j = 0 - coords = map(getfield(img, :wcs_axes)) do coord - j += 1 - if coord === (:) - first(axes(img,j)) - else - coord + # Plot the WCSGrid as a second series (actually just lines) + @series begin + wcsg, gridspec end end - - minx = first(axes(img,ax[2])) - maxx = last(axes(img,ax[2])) - miny = first(axes(img,ax[1])) - maxy = last(axes(img,ax[1])) - extent = (minx, maxx, miny, maxy) - - wcsg = WCSGrid(wcs(img), extent, ax, coords) - gridspec = wcsgridspec(wcsg) - - xticks --> (gridspec.tickpos1x, wcslabels(wcs(img), 1, gridspec.tickpos1w)) - xguide --> ctype_label(wcs(img).ctype[1], wcs(img).radesys) - - yticks --> (gridspec.tickpos2x, wcslabels(wcs(img), 2, gridspec.tickpos2w)) - yguide --> ctype_label(wcs(img).ctype[2], wcs(img).radesys) - - # To ensure the physical axis tick labels are correct the axes must be - # tight to the image - xl = first(axes(img,2)), last(axes(img,2)) - yl = first(axes(img,1)), last(axes(img,1)) - ylims --> yl - xlims --> xl + return end +end - # Disable equal aspect ratios if the scales are totally different - if max(size(img)...)/min(size(img)...) >= 7 - aspect_ratio --> :none - end - # We have to do a lot of flipping to keep the orientation corect - yflip := false - xflip := false - @series begin - axes(img,2), axes(img,1), view(arraydata(img), reverse(axes(img,1)),:) - end +@recipe function f( + img::AstroVec{T}; +) where {T<:Number} + + # We don't to override e.g. histograms + if haskey(plotattributes, :seriestype) + return arraydata(img) + + else + + # we have a wcs flag (from the image by default) so that users can skip over + # plotting in physical coordinates. This is especially important + # if the WCS headers are mallformed in some way. + if !haskey(plotattributes, :wcs) || plotattributes[:wcs] + + # Note: if the axes are on unusual sides (e.g. y-axis at right, x-axis at top) + # then these coordinates are not correct. They are only correct exactly + # along the axis. + + # ax = haskey(plotattributes, :axes) ? plotattributes[:axes] : (1,2) + # coords = haskey(plotattributes, :coords) ? plotattributes[:coords] : ones(wcs(img).naxis) + ax = findall(==(:), getfield(img, :wcs_axes)) + j = 0 + coords = map(getfield(img, :wcs_axes)) do coord + j += 1 + if coord === (:) + first(axes(img,j)) + else + coord + end + end + + l = ctype_label(wcs(img).ctype[only(ax)], wcs(img).radesys) + xguide --> l - # If wcs=true (default) and grid=true (not default), overplot a WCS - # grid. - if (!haskey(plotattributes, :wcs) || plotattributes[:wcs]) && - haskey(plotattributes, :xgrid) && plotattributes[:xgrid] && - haskey(plotattributes, :ygrid) && plotattributes[:ygrid] + # minx = first(axes(imgv,ax[2])) + # maxx = last(axes(imgv,ax[2])) + # miny = first(axes(imgv,ax[1])) + # maxy = last(axes(imgv,ax[1])) + # extent = (minx, maxx, miny, maxy) + + # wcsg = WCSGrid(wcs(imgv), extent, ax, coords) + # gridspec = wcsgridspec(wcsg) + + # xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), 1, gridspec.tickpos1w)) + # xguide --> ctype_label(wcs(imgv).ctype[1], wcs(imgv).radesys) + + # yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), 2, gridspec.tickpos2w)) + # yguide --> ctype_label(wcs(imgv).ctype[2], wcs(imgv).radesys) + + # # To ensure the physical axis tick labels are correct the axes must be + # # tight to the image + # xl = first(axes(imgv,2)), last(axes(imgv,2)) + # yl = first(axes(imgv,1)), last(axes(imgv,1)) + # ylims --> yl + # xlims --> xl + end - # Plot the WCSGrid as a second series (actually just lines) + # # Disable equal aspect ratios if the scales are totally different + # if max(size(imgv)...)/min(size(imgv)...) >= 7 + # aspect_ratio --> :none + # end + + # # We have to do a lot of flipping to keep the orientation corect + # yflip := false + # xflip := false + + # @series begin + # axes(imgv,2), axes(imgv,1), view(arraydata(imgv), reverse(axes(imgv,1)),:) + # end + + # If wcs=true (default) and grid=true (not default), overplot a WCS + # grid. + # if (!haskey(plotattributes, :wcs) || plotattributes[:wcs]) && + # haskey(plotattributes, :xgrid) && plotattributes[:xgrid] && + # haskey(plotattributes, :ygrid) && plotattributes[:ygrid] + + # # Plot the WCSGrid as a second series (actually just lines) + # @series begin + # wcsg, gridspec + # end + # end @series begin - wcsg, gridspec + arraydata(img) end + return end - return end @@ -390,6 +477,7 @@ end ticklabels = wcslabels(wcsg.w, 1, gridspec.annotations1w) seriestype := :line linewidth := 0 + # TODO: we need to use requires to load in Plots for the necessary text control. Future versions of RecipesBase might fix this. series_annotations := [ Main.Plots.text(" $l", :right, :bottom, textcolor, 8, rotation=(-95 <= r <= 95) ? r : r+180) for (l, r) in zip(ticklabels, rotations) @@ -430,26 +518,17 @@ function wcsgridspec(wsg::WCSGrid) ax = collect(wsg.ax) coordsx = convert(Vector{Float64}, collect(wsg.coords)) minx, maxx, miny, maxy = wsg.extent - @show wsg.extent - + # @show wsg.extent # Find the extent of this slice in world coordinates - # posxy = repeat(coordsx, 1, 4) - # posxy[ax,1] .= (minx,miny) - # posxy[ax,2] .= (minx,maxy) - # posxy[ax,3] .= (maxx,miny) - # posxy[ax,4] .= (maxx,maxy) - # posuv = pix_to_world(wsg.w, posxy) - # (minu, maxu), (minv, maxv) = extrema(posuv, dims=2) - posxy = repeat(coordsx, 1, 6) + posxy = repeat(coordsx, 1, 4) posxy[ax,1] .= (minx,miny) posxy[ax,2] .= (minx,maxy) - posxy[ax,3] .= (minx,miny+(maxy-miny)/2) - posxy[ax,4] .= (minx+(maxx-minx)/2,miny) - posxy[ax,5] .= (maxx,miny) - posxy[ax,6] .= (maxx,maxy) + posxy[ax,3] .= (maxx,miny) + posxy[ax,4] .= (maxx,maxy) posuv = pix_to_world(wsg.w, posxy) - (minu, maxu), (minv, maxv) = extrema(posuv, dims=2) + (minu, maxu), (minv, maxv) = extrema(posuv, dims=2)[[ax[1],ax[2]],:] + # In general, grid can be curved when plotted back against the image, # so we will need to sample multiple points along the grid. @@ -606,15 +685,15 @@ function wcsgridspec(wsg::WCSGrid) end - if point_entered[ax[1]] == minx - push!(tickpos2x, point_entered[ax[2]]) + if point_entered[1] == minx + push!(tickpos2x, point_entered[2]) push!(tickpos2w, tickv) end - if point_exitted[ax[1]] == minx - push!(tickpos2x, point_exitted[ax[2]]) + if point_exitted[1] == minx + push!(tickpos2x, point_exitted[2]) push!(tickpos2w, tickv) end - @show point_entered minx maxx miny maxy + # @show point_entered minx maxx miny maxy posxy_neat = [point_entered posxy[[ax[1],ax[2]],in_axes] point_exitted] @@ -622,8 +701,8 @@ function wcsgridspec(wsg::WCSGrid) # TODO: do unplotted other axes also need a fit? gridlinexy = ( - posxy_neat[ax[1],:], - posxy_neat[ax[2],:] + posxy_neat[1,:], + posxy_neat[2,:] ) push!(gridlinesxy2, gridlinexy) end @@ -778,23 +857,23 @@ function wcsgridspec(wsg::WCSGrid) posxy_neat = [point_entered posxy[[ax[1],ax[2]],in_axes] point_exitted] # TODO: do unplotted other axes also need a fit? - if point_entered[ax[2]] == miny + if point_entered[2] == miny push!(tickpos1x, point_entered[ax[1]]) push!(tickpos1w, ticku) end - if point_exitted[ax[2]] == miny + if point_exitted[2] == miny push!(tickpos1x, point_exitted[ax[1]]) push!(tickpos1w, ticku) end gridlinexy = ( - posxy_neat[ax[1],:], - posxy_neat[ax[2],:] + posxy_neat[1,:], + posxy_neat[2,:] ) push!(gridlinesxy1, gridlinexy) end end - @show tickpos1x + # @show tickpos1x # Grid annotations are simpler: annotations1w = Float64[] @@ -897,3 +976,32 @@ function wcsgridlines(gridspec::NamedTuple) return xs, ys end + + +function wcsvecticks(w,coords,ax,minx,maxx) + # x and y denote pixel coordinates (along `ax`), u and v are world coordinates roughly along same. + coordsx = convert(Vector{Float64}, collect(coords)) + + # Find the extent of this slice in world coordinates + posxy = repeat(coordsx, 1, 2) + posxy[ax,1] = minx + posxy[ax,2] = maxx + posuv = pix_to_world(w, posxy) + minu, maxu = extrema(posuv[ax,:]) + + # Find nice grid spacings using PlotUtils.optimize_ticks + # These heuristics can probably be improved + # TODO: this does not handle coordinates that wrap around + Q=[(1.0,1.0), (3.0, 0.8), (2.0, 0.7), (5.0, 0.5)] + k_min = 3 + k_ideal = 5 + k_max = 10 + + tickpos2x = Float64[] + tickpos2w = Float64[] + tickposv = optimize_ticks(6minv, 6maxv; Q, k_min, k_ideal, k_max)[1]./6 + griduv = posuv[:,1] + griduv[ax,:] .= urange + posxy = world_to_pix(wsg.w, griduv) + +end \ No newline at end of file From 80967967c16e82f3fd1672d4d1bb828e331bbd45 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 11 Mar 2022 11:58:16 -0800 Subject: [PATCH 052/178] Fix copy & similar --- src/AstroImages.jl | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 2f53316c..1ae97cb0 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -144,8 +144,8 @@ headers and WCS information, if applicable. struct AstroImage{T, N, TDat} <: AbstractArray{T,N} data::TDat headers::FITSHeader - wcs::Ref{WCSTransform} - wcs_stale::Ref{Bool} + wcs::Base.RefValue{WCSTransform} + wcs_stale::Base.RefValue{Bool} wcs_axes::NTuple{N,Union{Int,Colon}} where N end # Provide a type alias for a 1D version of our data structure. This is useful when extracting e.g. a spectrum from a data cube and @@ -213,9 +213,6 @@ function Base.getindex(img::AstroImage, inds...) ax_mask = ax_in .=== (:) ax_out = Vector{Union{Int,Colon}}(ax_in) ax_out[ax_mask] .= _filter_inds(inds) - @show ax_out - @show _ranges(inds) - @show typeof(dat) size(dat) return AstroImage( OffsetArray(dat, _ranges(inds)...), deepcopy(headers(img)), @@ -326,8 +323,8 @@ function Base.similar(img::AstroImage) where T return AstroImage( dat, deepcopy(headers(img)), - getfield(img, :wcs), - getfield(img, :wcs_stale), + getfield(img, :wcs)[], + getfield(img, :wcs_stale)[], getfield(img, :wcs_axes), ) end @@ -343,8 +340,8 @@ function Base.similar(img::AstroImage, dims::Tuple) where T return AstroImage( dat, deepcopy(headers(img)), - getfield(img, :wcs), - getfield(img, :wcs_stale), + getfield(img, :wcs)[], + getfield(img, :wcs_stale)[], getfield(img, :wcs_axes) ) end @@ -356,8 +353,9 @@ Base.copy(img::AstroImage) = AstroImage( # We copy the headers but share the WCS object. # If the headers change such that wcs is now out of date, # a new wcs will be generated when needed. - getfield(img, :wcs), - getfield(img, :wcs_stale) + getfield(img, :wcs)[], + getfield(img, :wcs_stale)[], + getfield(img, :wcs_axes) ) Base.convert(::Type{AstroImage}, A::AstroImage) = A Base.convert(::Type{AstroImage}, A::AbstractArray) = AstroImage(A) From fbb0d3d35efbc6d0d12a9b6d5a974eea5af2bb94 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 11 Mar 2022 16:34:32 -0800 Subject: [PATCH 053/178] WIP in adapting DimensionalData --- Project.toml | 2 +- src/AstroImages.jl | 485 ++++++++++++++++++++++++-------------------- src/imview.jl | 6 +- src/plot-recipes.jl | 156 +++++++------- src/showmime.jl | 4 +- src/wcs.jl | 5 + 6 files changed, 349 insertions(+), 309 deletions(-) create mode 100644 src/wcs.jl diff --git a/Project.toml b/Project.toml index 7e294144..64f11deb 100644 --- a/Project.toml +++ b/Project.toml @@ -6,6 +6,7 @@ version = "0.2.0" [deps] AstroAngles = "5c4adb95-c1fc-4c53-b4ea-2a94080c53d2" ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" +DimensionalData = "0703355e-b756-11e9-17c0-8b28908087d0" FITSIO = "525bcba6-941b-5504-bd06-fd0dc1a4d2eb" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" @@ -13,7 +14,6 @@ Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" InlineStrings = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48" Interact = "c601a237-2ae4-5e1e-952c-7a85b0c7eef1" MappedArrays = "dbb5928d-eab1-5f90-85c2-b9b0edb7c900" -OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 1ae97cb0..439ea2d5 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -5,13 +5,11 @@ using Statistics using MappedArrays using ColorSchemes using PlotUtils: zscale -using OffsetArrays - -using OffsetArrays +using DimensionalData export load, save, - AstroImage, + AstroArray, WCSGrid, ccd2rgb, composechannels, @@ -49,7 +47,7 @@ tuple with the data of each corresponding extension is returned. """ function FileIO.load(f::File{format"FITS"}, ext::Int=1) return FITS(f.filename) do fits - AstroImage(fits, ext) + AstroArray(fits, ext) end end export load, save @@ -61,7 +59,7 @@ export load, save # [0x53,0x49,0x4d,0x50,0x4c,0x45,0x20,0x20,0x3d,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x54], # [".fit", ".fits", ".fts", ".FIT", ".FITS", ".FTS"], # [:FITSIO => UUID("525bcba6-941b-5504-bd06-fd0dc1a4d2eb")], -# [:AstroImages => UUID("fe3fc30c-9b16-11e9-1c73-17dabf39f4ad")] +# [:AstroArrays => UUID("fe3fc30c-9b16-11e9-1c73-17dabf39f4ad")] # ) # function FileIO.load(f::File{format"FITS"}, ext::NTuple{N,Int}) where {N} @@ -119,49 +117,59 @@ for n in (8, 16, 32, 64) end -mutable struct Properties{P <: Union{AbstractFloat, FixedPoint}} - rgb_image::MappedArrays.MultiMappedArray{RGB{P},2,Tuple{Array{P,2},Array{P,2},Array{P,2}},Type{RGB{P}},typeof(ImageCore.extractchannels)} - contrast::Float64 - brightness::Float64 - label::Array{Tuple{Tuple{Float64,Float64},String},1} - function Properties{P}(;kvs...) where P - obj = new{P}() - obj.contrast = 1.0 - obj.brightness = 0.0 - obj.label = Array{Tuple{Tuple{Float64,Float64},String}}(undef,0) - for (k,v) in kvs - setproperty!(obj, k, v) - end - return obj - end -end +# mutable struct Properties{P <: Union{AbstractFloat, FixedPoint}} +# rgb_image::MappedArrays.MultiMappedArray{RGB{P},2,Tuple{Array{P,2},Array{P,2},Array{P,2}},Type{RGB{P}},typeof(ImageCore.extractchannels)} +# contrast::Float64 +# brightness::Float64 +# label::Array{Tuple{Tuple{Float64,Float64},String},1} +# function Properties{P}(;kvs...) where P +# obj = new{P}() +# obj.contrast = 1.0 +# obj.brightness = 0.0 +# obj.label = Array{Tuple{Tuple{Float64,Float64},String}}(undef,0) +# for (k,v) in kvs +# setproperty!(obj, k, v) +# end +# return obj +# end +# end """ Provides access to a FITS image along with its accompanying headers and WCS information, if applicable. """ -struct AstroImage{T, N, TDat} <: AbstractArray{T,N} - data::TDat +# struct AstroArray{T, N, TDat} <: AbstractArray{T,N} +struct AstroArray{T,N,D<:Tuple,R<:Tuple,A<:AbstractArray{T,N}} <: AbstractDimArray{T,N,D,A} + # Parent array we are wrapping + data::A + # Fields for DimensionalData + dims::D + refdims::R + # FITS Heads beloning to this image, if any headers::FITSHeader + # A cached WCSTransform object for this data wcs::Base.RefValue{WCSTransform} + # A flag that is set when a user modifies a WCS header. + # The next access to the wcs object will regenerate from + # the new headers on demand. wcs_stale::Base.RefValue{Bool} - wcs_axes::NTuple{N,Union{Int,Colon}} where N end # Provide a type alias for a 1D version of our data structure. This is useful when extracting e.g. a spectrum from a data cube and # retaining the headers and spectral axis information. -const AstroVec{T,TDat} = AstroImage{T,1,TDat} where {T,TDat} +const AstroVec{T,D,R,A} = AstroArray{T,1,D,R,A} where {T,D,R,A} +const AstroImage{T,D,R,A} = AstroArray{T,2,D,R,A} where {T,D,R,A} export AstroVec -AstroImage(data::AbstractArray{T,N}, headers, wcs, wcs_stale, wcs_axes) where {T,N} = AstroImage{T,N,typeof(data)}(data,headers,Ref(wcs),Ref(wcs_stale),wcs_axes) +# AstroArray(data::AbstractArray{T,N}, headers, wcs, wcs_stale, wcs_axes) where {T,N} = AstroArray{T,N,typeof(data)}(data,headers,Ref(wcs),Ref(wcs_stale),wcs_axes) """ - Images.arraydata(img::AstroImage) + Images.arraydata(img::AstroArray) """ -Images.arraydata(img::AstroImage) = getfield(img, :data) -headers(img::AstroImage) = getfield(img, :headers) -function wcs(img::AstroImage) +Images.arraydata(img::AstroArray) = getfield(img, :data) +headers(img::AstroArray) = getfield(img, :headers) +function wcs(img::AstroArray) if getfield(img, :wcs_stale)[] getfield(img, :wcs)[] = wcsfromheaders(img) getfield(img, :wcs_stale)[] = false @@ -169,6 +177,52 @@ function wcs(img::AstroImage) return getfield(img, :wcs)[] end +# Implement DimensionalData interface +DimensionalData.dims(A::AstroArray) = getfield(A, :dims) +DimensionalData.refdims(A::AstroArray) = getfield(A, :refdims) +DimensionalData.data(A::AstroArray) = getfield(A, :data) +DimensionalData.name(A::AstroArray) = DimensionalData.NoName() +DimensionalData.metadata(A::AstroArray) = DimensionalData.Dimensions.LookupArrays.NoMetadata() + + +@inline function DimensionalData.rebuild( + img::AstroArray, + data, + # Fields for DimensionalData + dims::Tuple=DimensionalData.dims(img), + refdims=DimensionalData.refdims(img), + # FITS Header beloning to this image, if any + headers::FITSHeader=deepcopy(headers(img)), + # A cached WCSTransform object for this data + wcs::WCSTransform=getfield(img, :wcs)[], + wcs_stale::Bool=getfield(img, :wcs_stale)[], +) + return AstroArray(data, dims, refdims, headers, Ref(wcs), Ref(wcs_stale)) +end +# Stub for when a name is passed along (we don't implement the name functionality) +@inline function DimensionalData.rebuild( + img::AstroArray, + data, + dims::Tuple, + refdims, + name::Symbol, + args... +) + # name is dropped + return DimensionalData.rebuild(img, data, dims, refdims, args...) +end +@inline DimensionalData.rebuildsliced( + f::Function, + img::AstroArray, + data, + I, + headers=deepcopy(headers(img)), + wcs=getfield(img, :wcs)[], + wcs_stale=getfield(img, :wcs_stale)[], +) = rebuild(img, data, DimensionalData.slicedims(f, img, I)..., headers, wcs, wcs_stale) + + + struct Comment end struct History end @@ -182,91 +236,93 @@ for f in [ :(Base.size), :(Base.length), ] - @eval ($f)(img::AstroImage) = $f(arraydata(img)) + @eval ($f)(img::AstroArray) = $f(arraydata(img)) end # Return result wrapped in array for f in [ :(Base.adjoint), :(Base.transpose) ] - @eval ($f)(img::AstroImage) = shareheaders(img, $f(arraydata(img))) + @eval ($f)(img::AstroArray) = shareheaders(img, $f(arraydata(img))) end -Base.parent(img::AstroImage) = arraydata(img) +Base.parent(img::AstroArray) = arraydata(img) # We might want property access for headers in future. -function Base.getproperty(img::AstroImage, ::Symbol) +function Base.getproperty(img::AstroArray, ::Symbol) error("getproperty reserved for future use.") end # Getting and setting data is forwarded to the underlying array # Accessing a single value or a vector returns just the data. # Accering a 2+D slice copies the headers and re-wraps the data. -function Base.getindex(img::AstroImage, inds...) - dat = getindex(arraydata(img), inds...) - # ndims is defined for Numbers but not Missing. - # This check is therefore necessary for img[1,1]->missing to work. - if !(eltype(dat) <: Number) || ndims(dat) == 0 - return dat - else - ax_in = collect(getfield(img, :wcs_axes)) - ax_mask = ax_in .=== (:) - ax_out = Vector{Union{Int,Colon}}(ax_in) - ax_out[ax_mask] .= _filter_inds(inds) - return AstroImage( - OffsetArray(dat, _ranges(inds)...), - deepcopy(headers(img)), - getfield(img, :wcs)[], - getfield(img, :wcs_stale)[], - tuple(ax_out...) - ) - # return copyheaders(img, dat) - end -end +Base.getindex(img::AstroArray, ind::Int) = getindex(parent(img), ind) + +# function Base.getindex(img::AstroArray, inds...) +# dat = getindex(arraydata(img), inds...) +# # ndims is defined for Numbers but not Missing. +# # This check is therefore necessary for img[1,1]->missing to work. +# if !(eltype(dat) <: Number) || ndims(dat) == 0 +# return dat +# else +# ax_in = collect(getfield(img, :wcs_axes)) +# ax_mask = ax_in .=== (:) +# ax_out = Vector{Union{Int,Colon}}(ax_in) +# ax_out[ax_mask] .= _filter_inds(inds) +# return AstroArray( +# OffsetArray(dat, _ranges(inds)...), +# deepcopy(headers(img)), +# getfield(img, :wcs)[], +# getfield(img, :wcs_stale)[], +# tuple(ax_out...) +# ) +# # return copyheaders(img, dat) +# end +# end _filter_inds(inds) = tuple(( typeof(ind) <: Union{AbstractRange,Colon} ? (:) : ind for ind in inds )...) _ranges(args) = filter(arg -> typeof(arg) <: Union{AbstractRange,Colon}, args) -Base.getindex(img::AstroImage{T}, inds...) where {T<:Colorant} = getindex(arraydata(img), inds...) -Base.setindex!(img::AstroImage, v, inds...) = setindex!(arraydata(img), v, inds...) # default fallback for operations on Array +Base.getindex(img::AstroArray{T}, inds...) where {T<:Colorant} = getindex(arraydata(img), inds...) +Base.setindex!(img::AstroArray, v, inds...) = setindex!(arraydata(img), v, inds...) # default fallback for operations on Array # Getting and setting comments -Base.getindex(img::AstroImage, inds::AbstractString...) = getindex(headers(img), inds...) # accesing header using strings -function Base.setindex!(img::AstroImage, v, ind::AbstractString) # modifying header using a string +Base.getindex(img::AstroArray, inds::AbstractString...) = getindex(headers(img), inds...) # accesing header using strings +function Base.setindex!(img::AstroArray, v, ind::AbstractString) # modifying header using a string setindex!(headers(img), v, ind) # Mark the WCS object as being out of date if this was a WCS header keyword if ind ∈ WCS_HEADERS getfield(img, :wcs_stale)[] = true end end -Base.getindex(img::AstroImage, inds::Symbol...) = getindex(img, string.(inds)...) # accessing header using symbol -Base.setindex!(img::AstroImage, v, ind::Symbol) = setindex!(img, v, string(ind)) -Base.getindex(img::AstroImage, ind::AbstractString, ::Type{Comment}) = get_comment(headers(img), ind) # accesing header comment using strings -Base.setindex!(img::AstroImage, v, ind::AbstractString, ::Type{Comment}) = set_comment!(headers(img), ind, v) # modifying header comment using strings -Base.getindex(img::AstroImage, ind::Symbol, ::Type{Comment}) = get_comment(headers(img), string(ind)) # accessing header comment using symbol -Base.setindex!(img::AstroImage, v, ind::Symbol, ::Type{Comment}) = set_comment!(headers(img), string(ind), v) # modifying header comment using Symbol +Base.getindex(img::AstroArray, inds::Symbol...) = getindex(img, string.(inds)...) # accessing header using symbol +Base.setindex!(img::AstroArray, v, ind::Symbol) = setindex!(img, v, string(ind)) +Base.getindex(img::AstroArray, ind::AbstractString, ::Type{Comment}) = get_comment(headers(img), ind) # accesing header comment using strings +Base.setindex!(img::AstroArray, v, ind::AbstractString, ::Type{Comment}) = set_comment!(headers(img), ind, v) # modifying header comment using strings +Base.getindex(img::AstroArray, ind::Symbol, ::Type{Comment}) = get_comment(headers(img), string(ind)) # accessing header comment using symbol +Base.setindex!(img::AstroArray, v, ind::Symbol, ::Type{Comment}) = set_comment!(headers(img), string(ind), v) # modifying header comment using Symbol # Support for special HISTORY and COMMENT entries -function Base.getindex(img::AstroImage, ::Type{History}) +function Base.getindex(img::AstroArray, ::Type{History}) hdr = headers(img) ii = findall(==("HISTORY"), hdr.keys) return view(hdr.comments, ii) end -function Base.getindex(img::AstroImage, ::Type{Comment}) +function Base.getindex(img::AstroArray, ::Type{Comment}) hdr = headers(img) ii = findall(==("COMMENT"), hdr.keys) return view(hdr.comments, ii) end # Adding new comment and history entries -function Base.push!(img::AstroImage, ::Type{Comment}, history::AbstractString) +function Base.push!(img::AstroArray, ::Type{Comment}, history::AbstractString) hdr = headers(img) push!(hdr.keys, "HISTORY") push!(hdr.values, nothing) push!(hdr.comments, history) end -function Base.push!(img::AstroImage, ::Type{History}, history::AbstractString) +function Base.push!(img::AstroArray, ::Type{History}, history::AbstractString) hdr = headers(img) push!(hdr.keys, "HISTORY") push!(hdr.values, nothing) @@ -274,53 +330,53 @@ function Base.push!(img::AstroImage, ::Type{History}, history::AbstractString) end """ - copyheaders(img::AstroImage, data) -> imgnew + copyheaders(img::AstroArray, data) -> imgnew Create a new image copying the headers of `img` but using the data of the AbstractArray `data`. Note that changing the headers of `imgnew` does not affect the headers of `img`. See also: [`shareheaders`](@ref). """ -copyheaders(img::AstroImage, data::AbstractArray) = - AstroImage(data, deepcopy(headers(img)), getfield(img, :wcs)[], getfield(img, :wcs_stale)[], getfield(img, :wcs_axes)) +copyheaders(img::AstroArray, data::AbstractArray) = + AstroArray(data, dims(img), refdims(img), deepcopy(headers(img)), Ref(getfield(img, :wcs)[]), Ref(getfield(img, :wcs_stale)[])) export copyheaders """ - shareheaders(img::AstroImage, data) -> imgnew + shareheaders(img::AstroArray, data) -> imgnew Create a new image reusing the headers dictionary of `img` but using the data of the AbstractArray `data`. The two images have synchronized headers; modifying one also affects the other. See also: [`copyheaders`](@ref). """ -shareheaders(img::AstroImage, data::AbstractArray) = AstroImage(data, headers(img), getfield(img, :wcs)[], getfield(img, :wcs_stale)[], getfield(img, :wcs_axes)) +shareheaders(img::AstroArray, data::AbstractArray) = AstroArray(data, dims(img), refdims(img), headers(img), Ref(getfield(img, :wcs)[]), Ref(getfield(img, :wcs_stale)[])) export shareheaders -# Share headers if an AstroImage, do nothing if AbstractArray -maybe_shareheaders(img::AstroImage, data) = shareheaders(img, data) +# Share headers if an AstroArray, do nothing if AbstractArray +maybe_shareheaders(img::AstroArray, data) = shareheaders(img, data) maybe_shareheaders(::AbstractArray, data) = data -maybe_copyheaders(img::AstroImage, data) = copyheaders(img, data) +maybe_copyheaders(img::AstroArray, data) = copyheaders(img, data) maybe_copyheaders(::AbstractArray, data) = data # Iteration # Defer to the array object in case it has special iteration defined -Base.iterate(img::AstroImage) = Base.iterate(arraydata(img)) -Base.iterate(img::AstroImage, s) = Base.iterate(arraydata(img), s) +Base.iterate(img::AstroArray) = Base.iterate(arraydata(img)) +Base.iterate(img::AstroArray, s) = Base.iterate(arraydata(img), s) # Delegate axes to the backing array -Base.axes(img::AstroImage) = Base.axes(arraydata(img)) +Base.axes(img::AstroArray) = Base.axes(arraydata(img)) # Restrict downsizes images by roughly a factor of two. # We want to keep the wrapper but downsize the underlying array -Images.restrict(img::AstroImage, ::Tuple{}) = img -Images.restrict(img::AstroImage, region::Dims) = shareheaders(img, restrict(arraydata(img), region)) +Images.restrict(img::AstroArray, ::Tuple{}) = img +Images.restrict(img::AstroArray, region::Dims) = shareheaders(img, restrict(arraydata(img), region)) # TODO: use WCS info # ImageCore.pixelspacing(img::ImageMeta) = pixelspacing(arraydata(img)) -Base.promote_rule(::Type{AstroImage{T}}, ::Type{AstroImage{V}}) where {T,V} = AstroImage{promote_type{T,V}} +Base.promote_rule(::Type{AstroArray{T}}, ::Type{AstroArray{V}}) where {T,V} = AstroArray{promote_type{T,V}} -function Base.similar(img::AstroImage) where T +function Base.similar(img::AstroArray) where T dat = similar(arraydata(img)) - return AstroImage( + return AstroArray( dat, deepcopy(headers(img)), getfield(img, :wcs)[], @@ -328,16 +384,16 @@ function Base.similar(img::AstroImage) where T getfield(img, :wcs_axes), ) end -# Getting a similar AstroImage with specific indices will typyically +# Getting a similar AstroArray with specific indices will typyically # return an OffsetArray -function Base.similar(img::AstroImage, dims::Tuple) where T +function Base.similar(img::AstroArray, dims::Tuple) where T dat = similar(arraydata(img), dims) - # Similar creates a new AstroImage with a similar array. + # Similar creates a new AstroArray with a similar array. # We start with empty headers, except we copy any # WCS headers from the original image. # The idea being we get an array that represents the same patch # of the sky in the same coordinate system. - return AstroImage( + return AstroArray( dat, deepcopy(headers(img)), getfield(img, :wcs)[], @@ -347,61 +403,52 @@ function Base.similar(img::AstroImage, dims::Tuple) where T end -Base.copy(img::AstroImage) = AstroImage( - copy(arraydata(img)), - deepcopy(headers(img)), - # We copy the headers but share the WCS object. - # If the headers change such that wcs is now out of date, - # a new wcs will be generated when needed. - getfield(img, :wcs)[], - getfield(img, :wcs_stale)[], - getfield(img, :wcs_axes) -) -Base.convert(::Type{AstroImage}, A::AstroImage) = A -Base.convert(::Type{AstroImage}, A::AbstractArray) = AstroImage(A) -Base.convert(::Type{AstroImage{T}}, A::AstroImage{T}) where {T} = A -Base.convert(::Type{AstroImage{T}}, A::AstroImage) where {T} = shareheaders(A, convert(AbstractArray{T}, arraydata(A))) -Base.convert(::Type{AstroImage{T}}, A::AbstractArray{T}) where {T} = AstroImage(A) -Base.convert(::Type{AstroImage{T}}, A::AbstractArray) where {T} = AstroImage(convert(AbstractArray{T}, A)) +Base.copy(img::AstroArray) = rebuild(img, copy(parent(img))) +Base.convert(::Type{AstroArray}, A::AstroArray) = A +Base.convert(::Type{AstroArray}, A::AbstractArray) = AstroArray(A) +Base.convert(::Type{AstroArray{T}}, A::AstroArray{T}) where {T} = A +Base.convert(::Type{AstroArray{T}}, A::AstroArray) where {T} = shareheaders(A, convert(AbstractArray{T}, arraydata(A))) +Base.convert(::Type{AstroArray{T}}, A::AbstractArray{T}) where {T} = AstroArray(A) +Base.convert(::Type{AstroArray{T}}, A::AbstractArray) where {T} = AstroArray(convert(AbstractArray{T}, A)) # TODO: offset arrays -Base.view(img::AstroImage, inds...) = shareheaders(img, view(arraydata(img), inds...)) +Base.view(img::AstroArray, inds...) = shareheaders(img, view(arraydata(img), inds...)) # Broadcasting -# Base.selectdim(img::AstroImage, d::Integer, idxs) = AstroImage(selectdim(arraydata(img), d, idxs), headers(img)) +# Base.selectdim(img::AstroArray, d::Integer, idxs) = AstroArray(selectdim(arraydata(img), d, idxs), headers(img)) # broadcast mechanics -Base.BroadcastStyle(::Type{<:AstroImage}) = Broadcast.ArrayStyle{AstroImage}() -function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{AstroImage}}, ::Type{T}) where T - img = find_img(bc) - dat = similar(arraydata(img), T, axes(bc)) - T2 = eltype(dat) - N = ndims(dat) - # We copy the headers but share the WCS object. - # If the headers change such that wcs is now out of date, - # a new wcs will be generated when needed. - return AstroImage{T2,N,typeof(dat)}( - dat, - deepcopy(headers(img)), - getfield(img, :wcs), - getfield(img, :wcs_stale), - getfield(img, :wcs_axes) - ) -end -"`A = find_img(As)` returns the first AstroImage among the arguments." -find_img(bc::Base.Broadcast.Broadcasted) = find_img(bc.args) -find_img(args::Tuple) = find_img(find_img(args[1]), Base.tail(args)) -find_img(x) = x -find_img(::Tuple{}) = nothing -find_img(a::AstroImage, rest) = a -find_img(::Any, rest) = find_img(rest) +# Base.BroadcastStyle(::Type{<:AstroArray}) = Broadcast.ArrayStyle{AstroArray}() +# function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{AstroArray}}, ::Type{T}) where T +# img = find_img(bc) +# dat = similar(arraydata(img), T, axes(bc)) +# T2 = eltype(dat) +# N = ndims(dat) +# # We copy the headers but share the WCS object. +# # If the headers change such that wcs is now out of date, +# # a new wcs will be generated when needed. +# return AstroArray{T2,N,typeof(dat)}( +# dat, +# deepcopy(headers(img)), +# getfield(img, :wcs), +# getfield(img, :wcs_stale), +# getfield(img, :wcs_axes) +# ) +# end +# "`A = find_img(As)` returns the first AstroArray among the arguments." +# find_img(bc::Base.Broadcast.Broadcasted) = find_img(bc.args) +# find_img(args::Tuple) = find_img(find_img(args[1]), Base.tail(args)) +# find_img(x) = x +# find_img(::Tuple{}) = nothing +# find_img(a::AstroArray, rest) = a +# find_img(::Any, rest) = find_img(rest) """ - AstroImage([color=Gray,] data::Matrix{Real}) - AstroImage(color::Type{<:Color}, data::NTuple{N, Matrix{T}}) where {T<:Real, N} + AstroArray([color=Gray,] data::Matrix{Real}) + AstroArray(color::Type{<:Color}, data::NTuple{N, Matrix{T}}) where {T<:Real, N} -Construct an `AstroImage` object of `data`, using `color` as color map, `Gray` by default. +Construct an `AstroArray` object of `data`, using `color` as color map, `Gray` by default. """ -AstroImage(img::AstroImage) = img +AstroArray(img::AstroArray) = img """ emptyheaders() @@ -417,7 +464,7 @@ Given an AbstractArray, return a blank WCSTransform of the appropriate dimensionality. """ emptywcs(data::AbstractArray) = WCSTransform(ndims(data)) -emptywcs(img::AstroImage) = WCSTransform(length(getfield(img, :wcs_axes))) +emptywcs(img::AstroArray) = WCSTransform(length(getfield(img, :wcs_axes))) @@ -438,12 +485,12 @@ function filterwcsheaders(hdrs::FITSHeader) end """ - AstroImage(data::AbstractArray, [headers::FITSHeader,] [wcs::WCSTransform,]) + AstroArray(data::AbstractArray, [headers::FITSHeader,] [wcs::WCSTransform,]) -Create an AstroImage from an array, and optionally headers or headers and a +Create an AstroArray from an array, and optionally headers or headers and a WCSTransform. """ -function AstroImage( +function AstroArray( data::AbstractArray{T,N}, header::FITSHeader=emptyheaders(), wcs::Union{WCSTransform,Nothing}=nothing @@ -460,18 +507,18 @@ function AstroImage( # This avoids those computations if the WCS transform is not needed. # It also allows us to create images with invalid WCS headers, # only erroring when/if they are used. - return AstroImage{T,N,typeof(data)}(data, header, wcs, wcs_stale, tuple(((:) for _ in 1:N)...)) + return AstroArray{T,N,typeof(data)}(data, header, wcs, wcs_stale, tuple(((:) for _ in 1:N)...)) end -AstroImage(data::AbstractArray, wcs::WCSTransform) = AstroImage(data, emptyheaders(), wcs) +AstroArray(data::AbstractArray, wcs::WCSTransform) = AstroArray(data, emptyheaders(), wcs) """ - wcsfromheaders(img::AstroImage; relax=WCS.HDR_ALL, ignore_rejected=true) + wcsfromheaders(img::AstroArray; relax=WCS.HDR_ALL, ignore_rejected=true) Helper function to create a WCSTransform from an array and FITSHeaders. """ -function wcsfromheaders(img::AstroImage; relax=WCS.HDR_ALL) +function wcsfromheaders(img::AstroArray; relax=WCS.HDR_ALL) # We only need to stringify WCS headers. This might just be 4-10 header keywords # out of thousands. local wcsout @@ -505,125 +552,125 @@ end """ - AstroImage(fits::FITS, ext::Int=1) + AstroArray(fits::FITS, ext::Int=1) Given an open FITS file from the FITSIO library, -load the HDU number `ext` as an AstroImage. +load the HDU number `ext` as an AstroArray. """ -AstroImage(fits::FITS, ext::Int=1) = AstroImage(fits[ext], read_header(fits[ext])) +AstroArray(fits::FITS, ext::Int=1) = AstroArray(fits[ext], read_header(fits[ext])) """ - AstroImage(hdu::HDU) + AstroArray(hdu::HDU) -Given an open FITS HDU, load it as an AstroImage. +Given an open FITS HDU, load it as an AstroArray. """ -AstroImage(hdu::HDU) = AstroImage(read(hdu), read_header(hdu)) +AstroArray(hdu::HDU) = AstroArray(read(hdu), read_header(hdu)) """ - img = AstroImage(filename::AbstractString, ext::Integer=1) + img = AstroArray(filename::AbstractString, ext::Integer=1) -Load an image HDU `ext` from the FITS file at `filename` as an AstroImage. +Load an image HDU `ext` from the FITS file at `filename` as an AstroArray. """ -function AstroImage(filename::AbstractString, ext::Integer=1) +function AstroArray(filename::AbstractString, ext::Integer=1) return FITS(filename,"r") do fits - return AstroImage(fits[ext]) + return AstroArray(fits[ext]) end end """ - img1, img2 = AstroImage(filename::AbstractString, exts) + img1, img2 = AstroArray(filename::AbstractString, exts) -Load multiple image HDUs `exts` from an FITS file at `filename` as an AstroImage. +Load multiple image HDUs `exts` from an FITS file at `filename` as an AstroArray. `exts` must be a tuple, range, :, or array of Integers. All listed HDUs in `exts` must be image HDUs or an error will occur. Example: ```julia -img1, img2 = AstroImage("abc.fits", (1,3)) # loads the first and third HDU as images. -imgs = AstroImage("abc.fits", 1:3) # loads the first three HDUs as images. -imgs = AstroImage("abc.fits", :) # loads all HDUs as images. +img1, img2 = AstroArray("abc.fits", (1,3)) # loads the first and third HDU as images. +imgs = AstroArray("abc.fits", 1:3) # loads the first three HDUs as images. +imgs = AstroArray("abc.fits", :) # loads all HDUs as images. ``` """ -function AstroImage(filename::AbstractString, exts::Union{NTuple{N, <:Integer},AbstractArray{<:Integer}}) where {N} +function AstroArray(filename::AbstractString, exts::Union{NTuple{N, <:Integer},AbstractArray{<:Integer}}) where {N} return FITS(filename,"r") do fits return map(exts) do ext - return AstroImage(fits[ext]) + return AstroArray(fits[ext]) end end end -function AstroImage(filename::AbstractString, ::Colon) where {N} +function AstroArray(filename::AbstractString, ::Colon) where {N} return FITS(filename,"r") do fits return map(fits) do hdu - return AstroImage(hdu) + return AstroArray(hdu) end end end -""" - set_brightness!(img::AstroImage, value::AbstractFloat) - -Sets brightness of `rgb_image` to value. -""" -function set_brightness!(img::AstroImage, value::AbstractFloat) - if isdefined(img.property, :rgb_image) - diff = value - img.property.brightness - img.property.brightness = value - img.property.rgb_image .+= RGB{typeof(value)}(diff, diff, diff) - else - throw(DomainError(value, "Can't apply operation. AstroImage dosen't contain :rgb_image")) - end -end - -""" - set_contrast!(img::AstroImage, value::AbstractFloat) - -Sets contrast of rgb_image to value. -""" -function set_contrast!(img::AstroImage, value::AbstractFloat) - if isdefined(img.property, :rgb_image) - diff = (value / img.property.contrast) - img.property.contrast = value - img.property.rgb_image = colorview(RGB, red.(img.property.rgb_image) .* diff, green.(img.property.rgb_image) .* diff, - blue.(img.property.rgb_image) .* diff) - else - throw(DomainError(value, "Can't apply operation. AstroImage dosen't contain :rgb_image")) - end -end - -""" - add_label!(img::AstroImage, x::Real, y::Real, label::String) +# """ +# set_brightness!(img::AstroArray, value::AbstractFloat) + +# Sets brightness of `rgb_image` to value. +# """ +# function set_brightness!(img::AstroArray, value::AbstractFloat) +# if isdefined(img.property, :rgb_image) +# diff = value - img.property.brightness +# img.property.brightness = value +# img.property.rgb_image .+= RGB{typeof(value)}(diff, diff, diff) +# else +# throw(DomainError(value, "Can't apply operation. AstroArray dosen't contain :rgb_image")) +# end +# end -Stores label to coordinates (x,y) in AstroImage's property label. -""" -function add_label!(img::AstroImage, x::Real, y::Real, label::String) - push!(img.property.label, ((x,y), label)) -end +# """ +# set_contrast!(img::AstroArray, value::AbstractFloat) + +# Sets contrast of rgb_image to value. +# """ +# function set_contrast!(img::AstroArray, value::AbstractFloat) +# if isdefined(img.property, :rgb_image) +# diff = (value / img.property.contrast) +# img.property.contrast = value +# img.property.rgb_image = colorview(RGB, red.(img.property.rgb_image) .* diff, green.(img.property.rgb_image) .* diff, +# blue.(img.property.rgb_image) .* diff) +# else +# throw(DomainError(value, "Can't apply operation. AstroArray dosen't contain :rgb_image")) +# end +# end -""" - reset!(img::AstroImage) +# """ +# add_label!(img::AstroArray, x::Real, y::Real, label::String) -Resets AstroImage property fields. +# Stores label to coordinates (x,y) in AstroArray's property label. +# """ +# function add_label!(img::AstroArray, x::Real, y::Real, label::String) +# push!(img.property.label, ((x,y), label)) +# end -Sets brightness to 0.0, contrast to 1.0, empties label -and form a fresh rgb_image without any brightness, contrast operations on it. -""" -function reset!(img::AstroImage{T,N}) where {T,N} - img.property.contrast = 1.0 - img.property.brightness = 0.0 - img.property.label = [] - if N == 3 && C == RGB - shape_out = size(img.property.rgb_image) - img.property.rgb_image = ccd2rgb((img.data[1], img.wcs[1]),(img.data[2], img.wcs[2]),(img.data[3], img.wcs[3]), - shape_out = shape_out) - end -end +# """ +# reset!(img::AstroArray) + +# Resets AstroArray property fields. + +# Sets brightness to 0.0, contrast to 1.0, empties label +# and form a fresh rgb_image without any brightness, contrast operations on it. +# """ +# function reset!(img::AstroArray{T,N}) where {T,N} +# img.property.contrast = 1.0 +# img.property.brightness = 0.0 +# img.property.label = [] +# if N == 3 && C == RGB +# shape_out = size(img.property.rgb_image) +# img.property.rgb_image = ccd2rgb((img.data[1], img.wcs[1]),(img.data[2], img.wcs[2]),(img.data[3], img.wcs[3]), +# shape_out = shape_out) +# end +# end include("wcs_headers.jl") include("imview.jl") include("showmime.jl") include("plot-recipes.jl") -include("ccd2rgb.jl") -include("patches.jl") +# include("ccd2rgb.jl") +# include("patches.jl") function __init__() diff --git a/src/imview.jl b/src/imview.jl index b934da08..9add8072 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -181,9 +181,7 @@ function _imview(img, normed::AbstractArray{T}, stretch, cmap) where T cmap = :grays end cscheme = ColorSchemes.colorschemes[cmap] - img_no = OffsetArrays.no_offset_view(img) - normed_no = OffsetArrays.no_offset_view(normed) - mapper = mappedarray(img_no, normed_no) do pixr, pixn + mapper = mappedarray(img, normed) do pixr, pixn if ismissing(pixr) || !isfinite(pixr) || ismissing(pixn) || !isfinite(pixn) # We check pixr in addition to pixn because we want to preserve if the pixels # are +-Inf @@ -225,7 +223,7 @@ function _imview(img, normed::AbstractArray{T}, stretch, cmap) where T :, ) - return maybe_copyheaders(img, OffsetArray(flipped_view, axes(img,2), axes(img,1))) + return maybe_copyheaders(img, flipped_view) end diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index cba71f02..f49e1831 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -58,11 +58,13 @@ save("output.png", v) # This recipe promotes AstroImages of numerical data into full color using # imview(). @recipe function f( - img::AstroImage{T,2}; + ::DimensionalData.HeatMapLike, + img::AstroImage{T}; clims=_default_clims[], stretch=_default_stretch[], cmap=_default_cmap[], ) where {T<:Number} + println("Hit AstroImage recipe") # We often plot an AstroImage{<:Number} which hasn't yet had # its wcs cached (wcs_stale=true) and we make an image view here. @@ -74,100 +76,88 @@ save("output.png", v) wcs(img) end - # We don't to override e.g. histograms - if haskey(plotattributes, :seriestype) - return arraydata(img) - else - # We currently use the AstroImages defaults. If unset, we could - # instead follow the plot theme. - imgv = imview(img; clims, stretch, cmap) + # We currently use the AstroImages defaults. If unset, we could + # instead follow the plot theme. + imgv = imview(img; clims, stretch, cmap) - xgrid --> true - ygrid --> true + xgrid --> true + ygrid --> true - # By default, disable the colorbar. - # Plots.jl does no give us sufficient control to make sure the range and ticks - # are correct after applying a non-linear stretch - # colorbar := false + # By default, disable the colorbar. + # Plots.jl does no give us sufficient control to make sure the range and ticks + # are correct after applying a non-linear stretch + # colorbar := false - # we have a wcs flag (from the image by default) so that users can skip over - # plotting in physical coordinates. This is especially important - # if the WCS headers are mallformed in some way. - if !haskey(plotattributes, :wcs) || plotattributes[:wcs] + # we have a wcs flag (from the image by default) so that users can skip over + # plotting in physical coordinates. This is especially important + # if the WCS headers are mallformed in some way. + if !haskey(plotattributes, :wcs) || plotattributes[:wcs] - # TODO: fill out coordinates array considering offset indices and slices - # out of cubes (tricky!) + # TODO: fill out coordinates array considering offset indices and slices + # out of cubes (tricky!) + + # Note: if the axes are on unusual sides (e.g. y-axis at right, x-axis at top) + # then these coordinates are not correct. They are only correct exactly + # along the axis. + # In astropy, the ticks are actually tilted to reflect this, though in general + # the transformation from pixel to coordinates can be non-linear and curved. + + # ax = haskey(plotattributes, :axes) ? plotattributes[:axes] : (1,2) + # coords = haskey(plotattributes, :coords) ? plotattributes[:coords] : ones(wcs(img).naxis) + ax = [1,1] + dax = [X(),Y()] + minx = first(axes(imgv,2)) + maxx = last(axes(imgv,2)) + miny = first(axes(imgv,1)) + maxy = last(axes(imgv,1)) + extent = (minx, maxx, miny, maxy) + + wcsg = WCSGrid(wcs(imgv), extent, ax, coords) + gridspec = wcsgridspec(wcsg) + + xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), ax[1], gridspec.tickpos1w)) + xguide --> ctype_label(wcs(imgv).ctype[ax[1]], wcs(imgv).radesys) + + yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), ax[2], gridspec.tickpos2w)) + yguide --> ctype_label(wcs(imgv).ctype[ax[2]], wcs(imgv).radesys) + + # To ensure the physical axis tick labels are correct the axes must be + # tight to the image + xl = first(axes(imgv,2)), last(axes(imgv,2)) + yl = first(axes(imgv,1)), last(axes(imgv,1)) + ylims --> yl + xlims --> xl + end - # Note: if the axes are on unusual sides (e.g. y-axis at right, x-axis at top) - # then these coordinates are not correct. They are only correct exactly - # along the axis. - # In astropy, the ticks are actually tilted to reflect this, though in general - # the transformation from pixel to coordinates can be non-linear and curved. + # Disable equal aspect ratios if the scales are totally different + if max(size(imgv)...)/min(size(imgv)...) >= 7 + aspect_ratio --> :none + end - # ax = haskey(plotattributes, :axes) ? plotattributes[:axes] : (1,2) - # coords = haskey(plotattributes, :coords) ? plotattributes[:coords] : ones(wcs(img).naxis) - ax = findall(==(:), getfield(imgv, :wcs_axes)) - j = 0 - coords = map(getfield(imgv, :wcs_axes)) do coord - j += 1 - if coord === (:) - first(axes(imgv,j)) - else - coord - end - end - minx = first(axes(imgv,2)) - maxx = last(axes(imgv,2)) - miny = first(axes(imgv,1)) - maxy = last(axes(imgv,1)) - extent = (minx, maxx, miny, maxy) - - wcsg = WCSGrid(wcs(imgv), extent, ax, coords) - gridspec = wcsgridspec(wcsg) - - xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), ax[1], gridspec.tickpos1w)) - xguide --> ctype_label(wcs(imgv).ctype[ax[1]], wcs(imgv).radesys) - - yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), ax[2], gridspec.tickpos2w)) - yguide --> ctype_label(wcs(imgv).ctype[ax[2]], wcs(imgv).radesys) - - # To ensure the physical axis tick labels are correct the axes must be - # tight to the image - xl = first(axes(imgv,2)), last(axes(imgv,2)) - yl = first(axes(imgv,1)), last(axes(imgv,1)) - ylims --> yl - xlims --> xl - end + # We have to do a lot of flipping to keep the orientation corect + yflip := false + xflip := false - # Disable equal aspect ratios if the scales are totally different - if max(size(imgv)...)/min(size(imgv)...) >= 7 - aspect_ratio --> :none - end + println("In plot recipe") + @series begin + # axes(imgv,2), axes(imgv,1), view(arraydata(imgv), reverse(axes(imgv,1)),:) + # axes(imgv,2) .- 0.5, axes(imgv,1) .- 0.5, + # @show size(view(arraydata(imgv), reverse(axes(imgv,1)),:)) + view(arraydata(imgv), reverse(axes(imgv,1)),:) + end - # We have to do a lot of flipping to keep the orientation corect - yflip := false - xflip := false + # If wcs=true (default) and grid=true (not default), overplot a WCS + # grid. + if (!haskey(plotattributes, :wcs) || plotattributes[:wcs]) && + haskey(plotattributes, :xgrid) && plotattributes[:xgrid] && + haskey(plotattributes, :ygrid) && plotattributes[:ygrid] + # Plot the WCSGrid as a second series (actually just lines) @series begin - # axes(imgv,2), axes(imgv,1), view(arraydata(imgv), reverse(axes(imgv,1)),:) - # axes(imgv,2) .- 0.5, axes(imgv,1) .- 0.5, - # @show size(view(arraydata(imgv), reverse(axes(imgv,1)),:)) - view(arraydata(imgv), reverse(axes(imgv,1)),:) + wcsg, gridspec end - - # If wcs=true (default) and grid=true (not default), overplot a WCS - # grid. - if (!haskey(plotattributes, :wcs) || plotattributes[:wcs]) && - haskey(plotattributes, :xgrid) && plotattributes[:xgrid] && - haskey(plotattributes, :ygrid) && plotattributes[:ygrid] - - # Plot the WCSGrid as a second series (actually just lines) - @series begin - wcsg, gridspec - end - end - return end + return end diff --git a/src/showmime.jl b/src/showmime.jl index 237ac451..daaa0ecc 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -23,11 +23,11 @@ # If the user displays a AstroImage of colors (e.g. one created with imview) # fal through and display the data as an image -Base.show(io::IO, mime::MIME"image/png", img::AstroImage{T,2}; kwargs...) where {T<:Colorant} = +Base.show(io::IO, mime::MIME"image/png", img::AstroImage{T}; kwargs...) where {T<:Colorant} = show(io, mime, arraydata(img), kwargs...) # Otherwise, call imview with the default settings. -Base.show(io::IO, mime::MIME"image/png", img::AstroImage{T,2}; kwargs...) where {T} = +Base.show(io::IO, mime::MIME"image/png", img::AstroImage{T}; kwargs...) where {T} = show(io, mime, imview(img), kwargs...) diff --git a/src/wcs.jl b/src/wcs.jl new file mode 100644 index 00000000..309dcf17 --- /dev/null +++ b/src/wcs.jl @@ -0,0 +1,5 @@ +# Smart versions of pix_to_world and world_to_pix + +function WCS.pix_to_world(img::AstroArray, pixcoords) + @show dims(img) +end \ No newline at end of file From b6a21647810972bf1f5389acffe65a306f5d7e74 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 13 Mar 2022 11:29:35 -0700 Subject: [PATCH 054/178] Continued WIP migrating to DimensionalData --- Project.toml | 2 + README.md | 32 +- src/AstroImages.jl | 834 ++++++++++++++++++-------------------------- src/ccd2rgb.jl | 6 +- src/imview.jl | 28 +- src/patches.jl | 18 +- src/plot-recipes.jl | 26 +- src/showmime.jl | 18 +- src/wcs.jl | 329 ++++++++++++++++- src/wcs_headers.jl | 231 ------------ test/ccd2rgb.jl | 6 +- test/plots.jl | 2 +- test/runtests.jl | 46 +-- 13 files changed, 756 insertions(+), 822 deletions(-) diff --git a/Project.toml b/Project.toml index 64f11deb..2aa285d0 100644 --- a/Project.toml +++ b/Project.toml @@ -20,6 +20,8 @@ Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" Reproject = "d1dcc2e6-806e-11e9-2897-3f99785db2ae" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" +UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" WCS = "15f3aee2-9e10-537f-b834-a6fb8bdb944d" [compat] diff --git a/README.md b/README.md index a0ef7e19..d3fa5539 100644 --- a/README.md +++ b/README.md @@ -8,17 +8,17 @@ Introduction ------------ -`AstroImage.jl` allows you to plot images from an +`AstroImageMat.jl` allows you to plot images from an astronomical [`FITS`](https://en.wikipedia.org/wiki/FITS) file using the popular [`Images.jl`](https://github.com/JuliaImages/Images.jl) and [`Plots.jl`](https://github.com/JuliaPlots/Plots.jl) Julia packages. -`AstroImage.jl` uses [`FITSIO.jl`](https://github.com/JuliaAstro/FITSIO.jl) to +`AstroImageMat.jl` uses [`FITSIO.jl`](https://github.com/JuliaAstro/FITSIO.jl) to read FITS files. Installation ------------ -`AstroImage.jl` is available for Julia 1.0 and later versions, and can be +`AstroImageMat.jl` is available for Julia 1.0 and later versions, and can be installed with [Julia built-in package manager](https://docs.julialang.org/en/v1/stdlib/Pkg/). @@ -55,44 +55,44 @@ julia> load("file.fits", 3) [...] ``` -## AstroImage type +## AstroImageMat type -The package provides a new type, `AstroImage` to integrate FITS images with -Julia packages for plotting and image processing. The `AstroImage` function has +The package provides a new type, `AstroImageMat` to integrate FITS images with +Julia packages for plotting and image processing. The `AstroImageMat` function has the same syntax as `load`. This command: ```julia -julia> img = AstroImage("file.fits") -AstroImages.AstroImage{UInt16,ColorTypes.Gray,1,Float64}[...] +julia> img = AstroImageMat("file.fits") +AstroImages.AstroImageMat{UInt16,ColorTypes.Gray,1,Float64}[...] ``` will read the first valid extension from the `file.fits` file and wrap its content in a `NTuple{N, Matrix{Gray}}`, that can be easily used with `Images.jl` and related packages. -If you are working in a Jupyter notebook, an `AstroImage` object is +If you are working in a Jupyter notebook, an `AstroImageMat` object is automatically rendered as a PNG image. -`AstroImage` automatically extracts and store `wcs` information of images in a `NTuple{N, WCSTransform}`. +`AstroImageMat` automatically extracts and store `wcs` information of images in a `NTuple{N, WCSTransform}`. ## Forming RGB image -`AstroImage` can automatically construct a RGB image if 3 different colour band data is given. +`AstroImageMat` can automatically construct a RGB image if 3 different colour band data is given. ```julia -julia> img = AstroImage(RGB, ("file1.fits","file2.fits", "file3.fits")) +julia> img = AstroImageMat(RGB, ("file1.fits","file2.fits", "file3.fits")) ``` Where 1st index of `file1.fits`, `file2.fits`, `file3.fits` contains band data of red, blue and green channels respectively. -Optionally, `ccd2rgb` method can be used to form a coloured image from 3 bands without creating an `AstroImage`. +Optionally, `ccd2rgb` method can be used to form a coloured image from 3 bands without creating an `AstroImageMat`. The formed image can be accessed using `img.property.rgb_image`. `set_brightness!` and `set_contrast!` methods can be used to change brightness and contrast of formed `rgb_image`. -`add_label!` method can be used to add/store Astronomical labels in an `AstroImage`. +`add_label!` method can be used to add/store Astronomical labels in an `AstroImageMat`. `reset!` method resets `brightness`, `contrast` and `label` fields to defaults and construct a fresh `rgb_image` without any brightness, contrast operations. -## Plotting an AstroImage +## Plotting an AstroImageMat -An `AstroImage` object can be plotted with `Plots.jl` package. Just use +An `AstroImageMat` object can be plotted with `Plots.jl` package. Just use ```julia julia> using Plots diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 439ea2d5..70ade58e 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -1,15 +1,21 @@ module AstroImages -using FITSIO, FileIO, Images, Interact, Reproject, WCS, MappedArrays +using FITSIO +using FileIO +using Images +using Interact +using Reproject +using WCS using Statistics using MappedArrays using ColorSchemes using PlotUtils: zscale using DimensionalData +using Tables export load, save, - AstroArray, + AstroImage, WCSGrid, ccd2rgb, composechannels, @@ -32,71 +38,11 @@ export load, wcsticks, wcsgridlines, arraydata, - headers, + header, wcs, Comment, History -""" - load(fitsfile::String, n=1) - -Read and return the data from `n`-th extension of the FITS file. - -Second argument can also be a tuple of integers, in which case a -tuple with the data of each corresponding extension is returned. -""" -function FileIO.load(f::File{format"FITS"}, ext::Int=1) - return FITS(f.filename) do fits - AstroArray(fits, ext) - end -end -export load, save - -# using UUIDs -# del_format(format"FITS") -# add_format(format"FITS", -# # See https://www.loc.gov/preservation/digital/formats/fdd/fdd000317.shtml#sign -# [0x53,0x49,0x4d,0x50,0x4c,0x45,0x20,0x20,0x3d,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x54], -# [".fit", ".fits", ".fts", ".FIT", ".FITS", ".FTS"], -# [:FITSIO => UUID("525bcba6-941b-5504-bd06-fd0dc1a4d2eb")], -# [:AstroArrays => UUID("fe3fc30c-9b16-11e9-1c73-17dabf39f4ad")] -# ) - -# function FileIO.load(f::File{format"FITS"}, ext::NTuple{N,Int}) where {N} -# fits = FITS(f.filename) -# out = _load(fits, ext) -# header = _header(fits, ext) -# close(fits) -# return out, header -# end - -# function FileIO.load(f::NTuple{N, String}) where {N} -# fits = ntuple(i-> FITS(f[i]), N) -# ext = indexer(fits) -# out = _load(fits, ext) -# header = _header(fits, ext) -# for i in 1:N -# close(fits[i]) -# end -# return out, header -# end - -# function indexer(fits::FITS) -# ext = 0 -# for (i, hdu) in enumerate(fits) -# if hdu isa ImageHDU && length(size(hdu)) >= 2 # check if Image is atleast 2D -# ext = i -# break -# end -# end -# if ext > 1 -# @info "Image was loaded from HDU $ext" -# elseif ext == 0 -# error("There are no ImageHDU extensions in '$(fits.filename)'") -# end -# return ext -# end -# indexer(fits::NTuple{N, FITS}) where {N} = ntuple(i -> indexer(fits[i]), N) # Images.jl expects data to be either a float or a fixed-point number. Here we define some # utilities to convert all data types supported by FITS format to float or fixed-point: @@ -117,382 +63,177 @@ for n in (8, 16, 32, 64) end -# mutable struct Properties{P <: Union{AbstractFloat, FixedPoint}} -# rgb_image::MappedArrays.MultiMappedArray{RGB{P},2,Tuple{Array{P,2},Array{P,2},Array{P,2}},Type{RGB{P}},typeof(ImageCore.extractchannels)} -# contrast::Float64 -# brightness::Float64 -# label::Array{Tuple{Tuple{Float64,Float64},String},1} -# function Properties{P}(;kvs...) where P -# obj = new{P}() -# obj.contrast = 1.0 -# obj.brightness = 0.0 -# obj.label = Array{Tuple{Tuple{Float64,Float64},String}}(undef,0) -# for (k,v) in kvs -# setproperty!(obj, k, v) -# end -# return obj -# end -# end - - """ Provides access to a FITS image along with its accompanying -headers and WCS information, if applicable. +header and WCS information, if applicable. """ -# struct AstroArray{T, N, TDat} <: AbstractArray{T,N} -struct AstroArray{T,N,D<:Tuple,R<:Tuple,A<:AbstractArray{T,N}} <: AbstractDimArray{T,N,D,A} +struct AstroImage{T,N,D<:Tuple,R<:Tuple,A<:AbstractArray{T,N}} <: AbstractDimArray{T,N,D,A} # Parent array we are wrapping data::A # Fields for DimensionalData dims::D refdims::R # FITS Heads beloning to this image, if any - headers::FITSHeader + header::FITSHeader # A cached WCSTransform object for this data wcs::Base.RefValue{WCSTransform} # A flag that is set when a user modifies a WCS header. # The next access to the wcs object will regenerate from - # the new headers on demand. + # the new header on demand. wcs_stale::Base.RefValue{Bool} end -# Provide a type alias for a 1D version of our data structure. This is useful when extracting e.g. a spectrum from a data cube and -# retaining the headers and spectral axis information. -const AstroVec{T,D,R,A} = AstroArray{T,1,D,R,A} where {T,D,R,A} -const AstroImage{T,D,R,A} = AstroArray{T,2,D,R,A} where {T,D,R,A} -export AstroVec - -# AstroArray(data::AbstractArray{T,N}, headers, wcs, wcs_stale, wcs_axes) where {T,N} = AstroArray{T,N,typeof(data)}(data,headers,Ref(wcs),Ref(wcs_stale),wcs_axes) - +# Provide type aliases for 1D and 2D versions of our data structure. +const AstroImageVec{T,D,R,A} = AstroImage{T,1,D,R,A} where {T,D,R,A} +const AstroImageMat{T,D,R,A} = AstroImage{T,2,D,R,A} where {T,D,R,A} +export AstroImage, AstroImageVec, AstroImageMat +# Accessors """ - Images.arraydata(img::AstroArray) + Images.arraydata(img::AstroImage) """ -Images.arraydata(img::AstroArray) = getfield(img, :data) -headers(img::AstroArray) = getfield(img, :headers) -function wcs(img::AstroArray) +Images.arraydata(img::AstroImage) = getfield(img, :data) +header(img::AstroImage) = getfield(img, :header) +function wcs(img::AstroImage) if getfield(img, :wcs_stale)[] - getfield(img, :wcs)[] = wcsfromheaders(img) + getfield(img, :wcs)[] = wcsfromheader(img) getfield(img, :wcs_stale)[] = false end return getfield(img, :wcs)[] end -# Implement DimensionalData interface -DimensionalData.dims(A::AstroArray) = getfield(A, :dims) -DimensionalData.refdims(A::AstroArray) = getfield(A, :refdims) -DimensionalData.data(A::AstroArray) = getfield(A, :data) -DimensionalData.name(A::AstroArray) = DimensionalData.NoName() -DimensionalData.metadata(A::AstroArray) = DimensionalData.Dimensions.LookupArrays.NoMetadata() +# Implement DimensionalData interface +Base.parent(img::AstroImage) = arraydata(img) +DimensionalData.dims(A::AstroImage) = getfield(A, :dims) +DimensionalData.refdims(A::AstroImage) = getfield(A, :refdims) +DimensionalData.data(A::AstroImage) = getfield(A, :data) +DimensionalData.name(::AstroImage) = DimensionalData.NoName() +DimensionalData.metadata(::AstroImage) = DimensionalData.Dimensions.LookupArrays.NoMetadata() + @inline function DimensionalData.rebuild( - img::AstroArray, + img::AstroImage, data, # Fields for DimensionalData dims::Tuple=DimensionalData.dims(img), - refdims=DimensionalData.refdims(img), + refdims::Tuple=DimensionalData.refdims(img), + name::Union{Symbol,DimensionalData.AbstractName,Nothing}=nothing, + metadata::Union{DimensionalData.LookupArrays.AbstractMetadata,Nothing}=nothing, # FITS Header beloning to this image, if any - headers::FITSHeader=deepcopy(headers(img)), + header::FITSHeader=deepcopy(header(img)), # A cached WCSTransform object for this data wcs::WCSTransform=getfield(img, :wcs)[], wcs_stale::Bool=getfield(img, :wcs_stale)[], ) - return AstroArray(data, dims, refdims, headers, Ref(wcs), Ref(wcs_stale)) -end -# Stub for when a name is passed along (we don't implement the name functionality) -@inline function DimensionalData.rebuild( - img::AstroArray, - data, - dims::Tuple, - refdims, - name::Symbol, - args... -) - # name is dropped - return DimensionalData.rebuild(img, data, dims, refdims, args...) + return AstroImage(data, dims, refdims, header, Ref(wcs), Ref(wcs_stale)) end +# Stub for when a name or metadata are passed along (we don't implement that functionality) +# @inline function DimensionalData.rebuild( +# img::AstroImage, +# data, +# dims::Tuple, +# refdims::Tuple, +# name::Union{Symbol,DimensionalData.AbstractName}, +# metadata::Union{DimensionalData.LookupArrays.AbstractMetadata,Nothing}, +# ) +# # name and metadata are dropped +# return DimensionalData.rebuild(img, data, dims, refdims, name, metadata) +# end @inline DimensionalData.rebuildsliced( f::Function, - img::AstroArray, + img::AstroImage, data, I, - headers=deepcopy(headers(img)), + header=deepcopy(header(img)), wcs=getfield(img, :wcs)[], wcs_stale=getfield(img, :wcs_stale)[], -) = rebuild(img, data, DimensionalData.slicedims(f, img, I)..., headers, wcs, wcs_stale) - - - -struct Comment end -struct History end - - +) = rebuild(img, data, DimensionalData.slicedims(f, img, I)..., nothing, nothing, header, wcs, wcs_stale) -# extending the AbstractArray interface -# and delegating calls to the wrapped array - -# Simple delegation -for f in [ - :(Base.size), - :(Base.length), -] - @eval ($f)(img::AstroArray) = $f(arraydata(img)) -end +# For these functions that return lazy wrappers, we want to +# share header # Return result wrapped in array for f in [ :(Base.adjoint), - :(Base.transpose) + :(Base.transpose), + :(Base.view) + # TODO: check view works ] - @eval ($f)(img::AstroArray) = shareheaders(img, $f(arraydata(img))) -end - -Base.parent(img::AstroArray) = arraydata(img) - -# We might want property access for headers in future. -function Base.getproperty(img::AstroArray, ::Symbol) - error("getproperty reserved for future use.") -end - -# Getting and setting data is forwarded to the underlying array -# Accessing a single value or a vector returns just the data. -# Accering a 2+D slice copies the headers and re-wraps the data. -Base.getindex(img::AstroArray, ind::Int) = getindex(parent(img), ind) - -# function Base.getindex(img::AstroArray, inds...) -# dat = getindex(arraydata(img), inds...) -# # ndims is defined for Numbers but not Missing. -# # This check is therefore necessary for img[1,1]->missing to work. -# if !(eltype(dat) <: Number) || ndims(dat) == 0 -# return dat -# else -# ax_in = collect(getfield(img, :wcs_axes)) -# ax_mask = ax_in .=== (:) -# ax_out = Vector{Union{Int,Colon}}(ax_in) -# ax_out[ax_mask] .= _filter_inds(inds) -# return AstroArray( -# OffsetArray(dat, _ranges(inds)...), -# deepcopy(headers(img)), -# getfield(img, :wcs)[], -# getfield(img, :wcs_stale)[], -# tuple(ax_out...) -# ) -# # return copyheaders(img, dat) -# end -# end -_filter_inds(inds) = tuple(( - typeof(ind) <: Union{AbstractRange,Colon} ? (:) : ind - for ind in inds -)...) -_ranges(args) = filter(arg -> typeof(arg) <: Union{AbstractRange,Colon}, args) - -Base.getindex(img::AstroArray{T}, inds...) where {T<:Colorant} = getindex(arraydata(img), inds...) -Base.setindex!(img::AstroArray, v, inds...) = setindex!(arraydata(img), v, inds...) # default fallback for operations on Array - -# Getting and setting comments -Base.getindex(img::AstroArray, inds::AbstractString...) = getindex(headers(img), inds...) # accesing header using strings -function Base.setindex!(img::AstroArray, v, ind::AbstractString) # modifying header using a string - setindex!(headers(img), v, ind) - # Mark the WCS object as being out of date if this was a WCS header keyword - if ind ∈ WCS_HEADERS - getfield(img, :wcs_stale)[] = true - end -end -Base.getindex(img::AstroArray, inds::Symbol...) = getindex(img, string.(inds)...) # accessing header using symbol -Base.setindex!(img::AstroArray, v, ind::Symbol) = setindex!(img, v, string(ind)) -Base.getindex(img::AstroArray, ind::AbstractString, ::Type{Comment}) = get_comment(headers(img), ind) # accesing header comment using strings -Base.setindex!(img::AstroArray, v, ind::AbstractString, ::Type{Comment}) = set_comment!(headers(img), ind, v) # modifying header comment using strings -Base.getindex(img::AstroArray, ind::Symbol, ::Type{Comment}) = get_comment(headers(img), string(ind)) # accessing header comment using symbol -Base.setindex!(img::AstroArray, v, ind::Symbol, ::Type{Comment}) = set_comment!(headers(img), string(ind), v) # modifying header comment using Symbol - -# Support for special HISTORY and COMMENT entries -function Base.getindex(img::AstroArray, ::Type{History}) - hdr = headers(img) - ii = findall(==("HISTORY"), hdr.keys) - return view(hdr.comments, ii) -end -function Base.getindex(img::AstroArray, ::Type{Comment}) - hdr = headers(img) - ii = findall(==("COMMENT"), hdr.keys) - return view(hdr.comments, ii) -end -# Adding new comment and history entries -function Base.push!(img::AstroArray, ::Type{Comment}, history::AbstractString) - hdr = headers(img) - push!(hdr.keys, "HISTORY") - push!(hdr.values, nothing) - push!(hdr.comments, history) -end -function Base.push!(img::AstroArray, ::Type{History}, history::AbstractString) - hdr = headers(img) - push!(hdr.keys, "HISTORY") - push!(hdr.values, nothing) - push!(hdr.comments, history) + @eval ($f)(img::AstroImage) = shareheader(img, $f(arraydata(img))) end """ - copyheaders(img::AstroArray, data) -> imgnew -Create a new image copying the headers of `img` but -using the data of the AbstractArray `data`. Note that changing the -headers of `imgnew` does not affect the headers of `img`. -See also: [`shareheaders`](@ref). -""" -copyheaders(img::AstroArray, data::AbstractArray) = - AstroArray(data, dims(img), refdims(img), deepcopy(headers(img)), Ref(getfield(img, :wcs)[]), Ref(getfield(img, :wcs_stale)[])) -export copyheaders + AstroImage(fits::FITS, ext::Int=1) +Given an open FITS file from the FITSIO library, +load the HDU number `ext` as an AstroImage. """ - shareheaders(img::AstroArray, data) -> imgnew -Create a new image reusing the headers dictionary of `img` but -using the data of the AbstractArray `data`. The two images have -synchronized headers; modifying one also affects the other. -See also: [`copyheaders`](@ref). -""" -shareheaders(img::AstroArray, data::AbstractArray) = AstroArray(data, dims(img), refdims(img), headers(img), Ref(getfield(img, :wcs)[]), Ref(getfield(img, :wcs_stale)[])) -export shareheaders -# Share headers if an AstroArray, do nothing if AbstractArray -maybe_shareheaders(img::AstroArray, data) = shareheaders(img, data) -maybe_shareheaders(::AbstractArray, data) = data -maybe_copyheaders(img::AstroArray, data) = copyheaders(img, data) -maybe_copyheaders(::AbstractArray, data) = data - -# Iteration -# Defer to the array object in case it has special iteration defined -Base.iterate(img::AstroArray) = Base.iterate(arraydata(img)) -Base.iterate(img::AstroArray, s) = Base.iterate(arraydata(img), s) - -# Delegate axes to the backing array -Base.axes(img::AstroArray) = Base.axes(arraydata(img)) - -# Restrict downsizes images by roughly a factor of two. -# We want to keep the wrapper but downsize the underlying array -Images.restrict(img::AstroArray, ::Tuple{}) = img -Images.restrict(img::AstroArray, region::Dims) = shareheaders(img, restrict(arraydata(img), region)) - -# TODO: use WCS info -# ImageCore.pixelspacing(img::ImageMeta) = pixelspacing(arraydata(img)) - -Base.promote_rule(::Type{AstroArray{T}}, ::Type{AstroArray{V}}) where {T,V} = AstroArray{promote_type{T,V}} - - -function Base.similar(img::AstroArray) where T - dat = similar(arraydata(img)) - return AstroArray( - dat, - deepcopy(headers(img)), - getfield(img, :wcs)[], - getfield(img, :wcs_stale)[], - getfield(img, :wcs_axes), - ) -end -# Getting a similar AstroArray with specific indices will typyically -# return an OffsetArray -function Base.similar(img::AstroArray, dims::Tuple) where T - dat = similar(arraydata(img), dims) - # Similar creates a new AstroArray with a similar array. - # We start with empty headers, except we copy any - # WCS headers from the original image. - # The idea being we get an array that represents the same patch - # of the sky in the same coordinate system. - return AstroArray( - dat, - deepcopy(headers(img)), - getfield(img, :wcs)[], - getfield(img, :wcs_stale)[], - getfield(img, :wcs_axes) - ) -end - - -Base.copy(img::AstroArray) = rebuild(img, copy(parent(img))) -Base.convert(::Type{AstroArray}, A::AstroArray) = A -Base.convert(::Type{AstroArray}, A::AbstractArray) = AstroArray(A) -Base.convert(::Type{AstroArray{T}}, A::AstroArray{T}) where {T} = A -Base.convert(::Type{AstroArray{T}}, A::AstroArray) where {T} = shareheaders(A, convert(AbstractArray{T}, arraydata(A))) -Base.convert(::Type{AstroArray{T}}, A::AbstractArray{T}) where {T} = AstroArray(A) -Base.convert(::Type{AstroArray{T}}, A::AbstractArray) where {T} = AstroArray(convert(AbstractArray{T}, A)) - -# TODO: offset arrays -Base.view(img::AstroArray, inds...) = shareheaders(img, view(arraydata(img), inds...)) - -# Broadcasting -# Base.selectdim(img::AstroArray, d::Integer, idxs) = AstroArray(selectdim(arraydata(img), d, idxs), headers(img)) -# broadcast mechanics -# Base.BroadcastStyle(::Type{<:AstroArray}) = Broadcast.ArrayStyle{AstroArray}() -# function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{AstroArray}}, ::Type{T}) where T -# img = find_img(bc) -# dat = similar(arraydata(img), T, axes(bc)) -# T2 = eltype(dat) -# N = ndims(dat) -# # We copy the headers but share the WCS object. -# # If the headers change such that wcs is now out of date, -# # a new wcs will be generated when needed. -# return AstroArray{T2,N,typeof(dat)}( -# dat, -# deepcopy(headers(img)), -# getfield(img, :wcs), -# getfield(img, :wcs_stale), -# getfield(img, :wcs_axes) -# ) -# end -# "`A = find_img(As)` returns the first AstroArray among the arguments." -# find_img(bc::Base.Broadcast.Broadcasted) = find_img(bc.args) -# find_img(args::Tuple) = find_img(find_img(args[1]), Base.tail(args)) -# find_img(x) = x -# find_img(::Tuple{}) = nothing -# find_img(a::AstroArray, rest) = a -# find_img(::Any, rest) = find_img(rest) +AstroImage(fits::FITS, ext::Int=1) = AstroImage(fits[ext]) """ - AstroArray([color=Gray,] data::Matrix{Real}) - AstroArray(color::Type{<:Color}, data::NTuple{N, Matrix{T}}) where {T<:Real, N} + AstroImage(hdu::HDU) -Construct an `AstroArray` object of `data`, using `color` as color map, `Gray` by default. +Given an open FITS HDU, load it as an AstroImage. """ -AstroArray(img::AstroArray) = img +AstroImage(hdu::HDU) = AstroImage(read(hdu), read_header(hdu)) """ - emptyheaders() + img = AstroImage(filename::AbstractString, ext::Integer=1) -Convenience function to create a FITSHeader with no keywords set. +Load an image HDU `ext` from the FITS file at `filename` as an AstroImage. """ -emptyheaders() = FITSHeader(String[],[],String[]) - +function AstroImage(filename::AbstractString, ext::Integer=1) + return FITS(filename,"r") do fits + return AstroImage(fits[ext]) + end +end """ - emptywcs() + img1, img2 = AstroImage(filename::AbstractString, exts) -Given an AbstractArray, return a blank WCSTransform of the appropriate -dimensionality. -""" -emptywcs(data::AbstractArray) = WCSTransform(ndims(data)) -emptywcs(img::AstroArray) = WCSTransform(length(getfield(img, :wcs_axes))) +Load multiple image HDUs `exts` from an FITS file at `filename` as an AstroImage. +`exts` must be a tuple, range, :, or array of Integers. +All listed HDUs in `exts` must be image HDUs or an error will occur. +Example: +```julia +img1, img2 = AstroImage("abc.fits", (1,3)) # loads the first and third HDU as images. +imgs = AstroImage("abc.fits", 1:3) # loads the first three HDUs as images. +imgs = AstroImage("abc.fits", :) # loads all HDUs as images. +``` +""" +function AstroImage(filename::AbstractString, exts::Union{NTuple{N, <:Integer},AbstractArray{<:Integer}}) where {N} + return FITS(filename,"r") do fits + return map(exts) do ext + return AstroImage(fits[ext]) + end + end +end +function AstroImage(filename::AbstractString, ::Colon) where {N} + return FITS(filename,"r") do fits + return map(fits) do hdu + return AstroImage(hdu) + end + end +end """ - filterwcsheaders(hdrs::FITSHeader) + AstroImage([color=Gray,] data::Matrix{Real}) + AstroImage(color::Type{<:Color}, data::NTuple{N, Matrix{T}}) where {T<:Real, N} -Return a new FITSHeader containing WCS headers from `hdrs`. -This is useful for creating a new image with the same coordinates -as another. +Construct an `AstroImage` object of `data`, using `color` as color map, `Gray` by default. """ -function filterwcsheaders(hdrs::FITSHeader) - include_keys = intersect(keys(hdrs), WCS_HEADERS) - return FITSHeader( - include_keys, - map(key -> hdrs[key], include_keys), - map(key -> get_comment(hdrs, key), include_keys), - ) -end +AstroImage(img::AstroImage) = img + """ - AstroArray(data::AbstractArray, [headers::FITSHeader,] [wcs::WCSTransform,]) + AstroImage(data::AbstractArray, [header::FITSHeader,] [wcs::WCSTransform,]) -Create an AstroArray from an array, and optionally headers or headers and a +Create an AstroImage from an array, and optionally header or header and a WCSTransform. """ -function AstroArray( +function AstroImage( data::AbstractArray{T,N}, - header::FITSHeader=emptyheaders(), + header::FITSHeader=emptyheader(), wcs::Union{WCSTransform,Nothing}=nothing ) where {T, N} wcs_stale = isnothing(wcs) @@ -501,173 +242,270 @@ function AstroArray( end # If the user passes in a WCSTransform of their own, we use it and mark # wcs_stale=false. It will be kept unless they manually change a WCS header. - # If they don't pass anythin, we start with empty WCS information regardless - # of what's in the headers but we mark it as stale. + # If they don't pass anything, we start with empty WCS information regardless + # of what's in the header but we mark it as stale. # If/when the WCS info is accessed via `wcs(img)` it will be computed and cached. # This avoids those computations if the WCS transform is not needed. - # It also allows us to create images with invalid WCS headers, + # It also allows us to create images with invalid WCS header, # only erroring when/if they are used. - return AstroArray{T,N,typeof(data)}(data, header, wcs, wcs_stale, tuple(((:) for _ in 1:N)...)) -end -AstroArray(data::AbstractArray, wcs::WCSTransform) = AstroArray(data, emptyheaders(), wcs) - - -""" - wcsfromheaders(img::AstroArray; relax=WCS.HDR_ALL, ignore_rejected=true) -Helper function to create a WCSTransform from an array and -FITSHeaders. -""" -function wcsfromheaders(img::AstroArray; relax=WCS.HDR_ALL) - # We only need to stringify WCS headers. This might just be 4-10 header keywords - # out of thousands. - local wcsout - # Load the headers without ignoring rejected to get error messages - try - wcsout = WCS.from_header( - string(headers(img)); - ignore_rejected=false, - relax - ) - catch err - # Load them again ignoring error messages - wcsout = WCS.from_header( - string(headers(img)); - ignore_rejected=true, - relax + # Fields for DimensionalData. + # Name dimensions always as X,Y,Z, then Dim{4}, Dim{5}, etc. + # If we wanted to do something smarter e.g. time axes we would have + # to look at the WCSTransform, and we want to avoid doing this on construction + # for the reasons described above. + dimnames = ( + X, Y, Z + )[1:min(3,N)] + if N > 3 + dimnames = ( + dimnames..., + (Dim{i} for i in 4:N)... ) - # If that still fails, the use gets the stack trace here - # If not, print a warning about rejected headers - @warn "WCSTransform was generated by ignoring rejected headers. It may not be valid." exception=err end - - if length(wcsout) == 1 - return only(wcsout) - elseif length(wcsout) == 0 - return emptywcs(img) - else - error("Mutiple WCSTransform returned from headers") + dimaxes = map(dimnames, axes(data)) do dim, ax + dim(ax) end -end + dims = DimensionalData.format(dimaxes, data) + refdims = () + return AstroImage(data, dims, refdims, header, Ref(wcs), Ref(wcs_stale)) +end +AstroImage(data::AbstractArray, wcs::WCSTransform) = AstroImage(data, emptyheader(), wcs) + + +using UUIDs +# TODO: This should be registered correctly with FileIO +del_format(format"FITS") +add_format(format"FITS", + # See https://www.loc.gov/preservation/digital/formats/fdd/fdd000317.shtml#sign + [0x53,0x49,0x4d,0x50,0x4c,0x45,0x20,0x20,0x3d,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x54], + [".fit", ".fits", ".fts", ".FIT", ".FITS", ".FTS"], + [:FITSIO => UUID("525bcba6-941b-5504-bd06-fd0dc1a4d2eb")], + [:AstroImages => UUID("fe3fc30c-9b16-11e9-1c73-17dabf39f4ad")] +) """ - AstroArray(fits::FITS, ext::Int=1) + load(fitsfile::String) -Given an open FITS file from the FITSIO library, -load the HDU number `ext` as an AstroArray. -""" -AstroArray(fits::FITS, ext::Int=1) = AstroArray(fits[ext], read_header(fits[ext])) +Read and return the data from the first ImageHDU in a FITS file +as an AstroImage. If no ImageHDUs are present, an error is returned. -""" - AstroArray(hdu::HDU) + load(fitsfile::String, ext::Int) -Given an open FITS HDU, load it as an AstroArray. -""" -AstroArray(hdu::HDU) = AstroArray(read(hdu), read_header(hdu)) +Read and return the data from the HDU `ext`. If it is an ImageHDU, +as AstroImage is returned. If it is a TableHDU, a plain Julia +column table is returned. -""" - img = AstroArray(filename::AbstractString, ext::Integer=1) + load(fitsfile::String, :) -Load an image HDU `ext` from the FITS file at `filename` as an AstroArray. -""" -function AstroArray(filename::AbstractString, ext::Integer=1) - return FITS(filename,"r") do fits - return AstroArray(fits[ext]) - end -end -""" - img1, img2 = AstroArray(filename::AbstractString, exts) +Read and return the data from each HDU in an FITS file. ImageHDUs are +returned as AstroImage, and TableHDUs are returned as column tables. -Load multiple image HDUs `exts` from an FITS file at `filename` as an AstroArray. -`exts` must be a tuple, range, :, or array of Integers. -All listed HDUs in `exts` must be image HDUs or an error will occur. + load(fitsfile::String, exts::Union{NTuple, AbstractArray}) -Example: -```julia -img1, img2 = AstroArray("abc.fits", (1,3)) # loads the first and third HDU as images. -imgs = AstroArray("abc.fits", 1:3) # loads the first three HDUs as images. -imgs = AstroArray("abc.fits", :) # loads all HDUs as images. -``` +Read and return the data from the HDUs given by `exts`. ImageHDUs are +returned as AstroImage, and TableHDUs are returned as column tables. """ -function AstroArray(filename::AbstractString, exts::Union{NTuple{N, <:Integer},AbstractArray{<:Integer}}) where {N} - return FITS(filename,"r") do fits - return map(exts) do ext - return AstroArray(fits[ext]) +function fileio_load(f::File{format"FITS"}, ext::Union{Int,Nothing}=nothing) where N + return FITS(f.filename, "r") do fits + if isnothing(ext) + ext = indexer(fits) end + _loadhdu(fits[ext]) end end -function AstroArray(filename::AbstractString, ::Colon) where {N} - return FITS(filename,"r") do fits - return map(fits) do hdu - return AstroArray(hdu) +function fileio_load(f::File{format"FITS"}, exts::Union{NTuple{N, <:Integer},AbstractArray{<:Integer}}) where N + return FITS(f.filename, "r") do fits + map(exts) do ext + _loadhdu(fits[ext]) + end + end +end +function fileio_load(f::File{format"FITS"}, exts::Colon) where N + return FITS(f.filename, "r") do fits + exts_resolved = 1:length(fits) + map(exts_resolved) do ext + _loadhdu(fits[ext]) end end end +_loadhdu(hdu::FITSIO.ImageHDU) = AstroImage(hdu) +_loadhdu(hdu::FITSIO.TableHDU) = Tables.columntable(hdu) +export load, save -# """ -# set_brightness!(img::AstroArray, value::AbstractFloat) - -# Sets brightness of `rgb_image` to value. -# """ -# function set_brightness!(img::AstroArray, value::AbstractFloat) -# if isdefined(img.property, :rgb_image) -# diff = value - img.property.brightness -# img.property.brightness = value -# img.property.rgb_image .+= RGB{typeof(value)}(diff, diff, diff) -# else -# throw(DomainError(value, "Can't apply operation. AstroArray dosen't contain :rgb_image")) -# end +# function fileio_load(f::File{format"FITS"}, ext::NTuple{N,Int}) where {N} +# fits = FITS(f.filename) +# out = _load(fits, ext) +# header = _header(fits, ext) +# close(fits) +# return out, header # end -# """ -# set_contrast!(img::AstroArray, value::AbstractFloat) - -# Sets contrast of rgb_image to value. -# """ -# function set_contrast!(img::AstroArray, value::AbstractFloat) -# if isdefined(img.property, :rgb_image) -# diff = (value / img.property.contrast) -# img.property.contrast = value -# img.property.rgb_image = colorview(RGB, red.(img.property.rgb_image) .* diff, green.(img.property.rgb_image) .* diff, -# blue.(img.property.rgb_image) .* diff) -# else -# throw(DomainError(value, "Can't apply operation. AstroArray dosen't contain :rgb_image")) +# function fileio_load(f::NTuple{N, String}) where {N} +# fits = ntuple(i-> FITS(f[i]), N) +# ext = indexer(fits) +# out = _load(fits, ext) +# header = _header(fits, ext) +# for i in 1:N +# close(fits[i]) # end +# return out, header # end -# """ -# add_label!(img::AstroArray, x::Real, y::Real, label::String) +function indexer(fits::FITS) + ext = 0 + for (i, hdu) in enumerate(fits) + if hdu isa ImageHDU && length(size(hdu)) >= 2 # check if Image is atleast 2D + ext = i + break + end + end + if ext > 1 + @info "Image was loaded from HDU $ext" + elseif ext == 0 + error("There are no ImageHDU extensions in '$(fits.filename)'") + end + return ext +end +indexer(fits::NTuple{N, FITS}) where {N} = ntuple(i -> indexer(fits[i]), N) + -# Stores label to coordinates (x,y) in AstroArray's property label. -# """ -# function add_label!(img::AstroArray, x::Real, y::Real, label::String) -# push!(img.property.label, ((x,y), label)) -# end -# """ -# reset!(img::AstroArray) - -# Resets AstroArray property fields. - -# Sets brightness to 0.0, contrast to 1.0, empties label -# and form a fresh rgb_image without any brightness, contrast operations on it. -# """ -# function reset!(img::AstroArray{T,N}) where {T,N} -# img.property.contrast = 1.0 -# img.property.brightness = 0.0 -# img.property.label = [] -# if N == 3 && C == RGB -# shape_out = size(img.property.rgb_image) -# img.property.rgb_image = ccd2rgb((img.data[1], img.wcs[1]),(img.data[2], img.wcs[2]),(img.data[3], img.wcs[3]), -# shape_out = shape_out) -# end -# end -include("wcs_headers.jl") +struct Comment end +struct History end + + +# We might want getproperty for header access in future. +function Base.getproperty(img::AstroImage, ::Symbol) + error("getproperty reserved for future use.") +end + +# All data indexing is handled by DimensionalData. +# We add overloads for String and Symbol indexing to +# access the FITS header instead. +# _filter_inds(inds) = tuple(( +# typeof(ind) <: Union{AbstractRange,Colon} ? (:) : ind +# for ind in inds +# )...) +# _ranges(args) = filter(arg -> typeof(arg) <: Union{AbstractRange,Colon}, args) + +# Base.getindex(img::AstroImage{T}, inds...) where {T<:Colorant} = getindex(arraydata(img), inds...) +# Base.setindex!(img::AstroImage, v, inds...) = setindex!(arraydata(img), v, inds...) # default fallback for operations on Array + +# Getting and setting comments +Base.getindex(img::AstroImage, inds::AbstractString...) = getindex(header(img), inds...) # accesing header using strings +function Base.setindex!(img::AstroImage, v, ind::AbstractString) # modifying header using a string + setindex!(header(img), v, ind) + # Mark the WCS object as being out of date if this was a WCS header keyword + if ind ∈ WCS_HEADERS + getfield(img, :wcs_stale)[] = true + end +end +Base.getindex(img::AstroImage, inds::Symbol...) = getindex(img, string.(inds)...) # accessing header using symbol +Base.setindex!(img::AstroImage, v, ind::Symbol) = setindex!(img, v, string(ind)) +Base.getindex(img::AstroImage, ind::AbstractString, ::Type{Comment}) = get_comment(header(img), ind) # accesing header comment using strings +Base.setindex!(img::AstroImage, v, ind::AbstractString, ::Type{Comment}) = set_comment!(header(img), ind, v) # modifying header comment using strings +Base.getindex(img::AstroImage, ind::Symbol, ::Type{Comment}) = get_comment(header(img), string(ind)) # accessing header comment using symbol +Base.setindex!(img::AstroImage, v, ind::Symbol, ::Type{Comment}) = set_comment!(header(img), string(ind), v) # modifying header comment using Symbol + +# Support for special HISTORY and COMMENT entries +function Base.getindex(img::AstroImage, ::Type{History}) + hdr = header(img) + ii = findall(==("HISTORY"), hdr.keys) + return view(hdr.comments, ii) +end +function Base.getindex(img::AstroImage, ::Type{Comment}) + hdr = header(img) + ii = findall(==("COMMENT"), hdr.keys) + return view(hdr.comments, ii) +end +# Adding new comment and history entries +function Base.push!(img::AstroImage, ::Type{Comment}, history::AbstractString) + hdr = header(img) + push!(hdr.keys, "HISTORY") + push!(hdr.values, nothing) + push!(hdr.comments, history) +end +function Base.push!(img::AstroImage, ::Type{History}, history::AbstractString) + hdr = header(img) + push!(hdr.keys, "HISTORY") + push!(hdr.values, nothing) + push!(hdr.comments, history) +end + +""" + copyheader(img::AstroImage, data) -> imgnew +Create a new image copying the header of `img` but +using the data of the AbstractArray `data`. Note that changing the +header of `imgnew` does not affect the header of `img`. +See also: [`shareheader`](@ref). +""" +copyheader(img::AstroImage, data::AbstractArray) = + AstroImage(data, dims(img), refdims(img), deepcopy(header(img)), Ref(getfield(img, :wcs)[]), Ref(getfield(img, :wcs_stale)[])) +export copyheader + +""" + shareheader(img::AstroImage, data) -> imgnew +Create a new image reusing the header dictionary of `img` but +using the data of the AbstractArray `data`. The two images have +synchronized header; modifying one also affects the other. +See also: [`copyheader`](@ref). +""" +shareheader(img::AstroImage, data::AbstractArray) = AstroImage(data, dims(img), refdims(img), header(img), Ref(getfield(img, :wcs)[]), Ref(getfield(img, :wcs_stale)[])) +export shareheader +# Share header if an AstroImage, do nothing if AbstractArray +maybe_shareheader(img::AstroImage, data) = shareheader(img, data) +maybe_shareheader(::AbstractArray, data) = data +maybe_copyheader(img::AstroImage, data) = copyheader(img, data) +maybe_copyheader(::AbstractArray, data) = data + + +# Restrict downsizes images by roughly a factor of two. +# We want to keep the wrapper but downsize the underlying array +# TODO: correct dimensions after restrict. +Images.restrict(img::AstroImage, ::Tuple{}) = img +Images.restrict(img::AstroImage, region::Dims) = shareheader(img, restrict(arraydata(img), region)) + +# TODO: use WCS info +# ImageCore.pixelspacing(img::ImageMeta) = pixelspacing(arraydata(img)) + +Base.promote_rule(::Type{AstroImage{T}}, ::Type{AstroImage{V}}) where {T,V} = AstroImage{promote_type{T,V}} + + + +Base.copy(img::AstroImage) = rebuild(img, copy(parent(img))) +Base.convert(::Type{AstroImage}, A::AstroImage) = A +Base.convert(::Type{AstroImage}, A::AbstractArray) = AstroImage(A) +Base.convert(::Type{AstroImage{T}}, A::AstroImage{T}) where {T} = A +Base.convert(::Type{AstroImage{T}}, A::AstroImage) where {T} = shareheader(A, convert(AbstractArray{T}, arraydata(A))) +Base.convert(::Type{AstroImage{T}}, A::AbstractArray{T}) where {T} = AstroImage(A) +Base.convert(::Type{AstroImage{T}}, A::AbstractArray) where {T} = AstroImage(convert(AbstractArray{T}, A)) + +# TODO: share headers in View. Needs support from DimensionalData. + +# "`A = find_img(As)` returns the first AstroImage among the arguments." +# find_img(bc::Base.Broadcast.Broadcasted) = find_img(bc.args) +# find_img(args::Tuple) = find_img(find_img(args[1]), Base.tail(args)) +# find_img(x) = x +# find_img(::Tuple{}) = nothing +# find_img(a::AstroImage, rest) = a +# find_img(::Any, rest) = find_img(rest) + + +""" + emptyheader() + +Convenience function to create a FITSHeader with no keywords set. +""" +emptyheader() = FITSHeader(String[],[],String[]) + + +include("wcs.jl") include("imview.jl") include("showmime.jl") -include("plot-recipes.jl") +# include("plot-recipes.jl") # include("ccd2rgb.jl") # include("patches.jl") @@ -679,7 +517,7 @@ function __init__() if isdefined(Base.Experimental, :register_error_hint) Base.Experimental.register_error_hint(MethodError) do io, exc, argtypes, kwargs if exc.f == imview && first(argtypes) <: AbstractArray && ndims(first(argtypes)) != 2 - print(io, "\nThe `imview` function only supports 2D images. If you have a cube, try viewing one slice at a time.\n") + print(io, "\nThe `imview` function only supports 2D images. If you have a cube, try viewing one slice at a time: imview(cube[:,:,1])\n") end end end diff --git a/src/ccd2rgb.jl b/src/ccd2rgb.jl index 15cbe528..7cfee4a9 100644 --- a/src/ccd2rgb.jl +++ b/src/ccd2rgb.jl @@ -24,9 +24,9 @@ julia> ccd2rgb(r, b, g, shape_out = (1000,1000), stretch = asinh) ``` """ function ccd2rgb( - red::AstroImage, - green::AstroImage, - blue::AstroImage; + red::AstroImageMat, + green::AstroImageMat, + blue::AstroImageMat; stretch = identity, shape_out = size(red[1]) ) diff --git a/src/imview.jl b/src/imview.jl index 9add8072..ef96b1c6 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -27,7 +27,7 @@ function percent(perc::Number) return clims end -const _default_cmap = Ref{Union{Symbol,Nothing}}(nothing) +const _default_cmap = Ref{Union{Symbol,Nothing}}(:magma)#nothing) const _default_clims = Ref{Any}(percent(99.5)) const _default_stretch = Ref{Any}(identity) @@ -36,7 +36,7 @@ const _default_stretch = Ref{Any}(identity) set_cmap!(cmap::Nothing) Alter the default color map used to display images when using -`imview` or displaying an AstroImage. +`imview` or displaying an AstroImageMat. """ function set_cmap!(cmap) if cmap ∉ keys(ColorSchemes.colorschemes) @@ -49,7 +49,7 @@ end set_clims!(clims::Function) Alter the default limits used to display images when using -`imview` or displaying an AstroImage. +`imview` or displaying an AstroImageMat. """ function set_clims!(clims) _default_clims[] = clims @@ -58,7 +58,7 @@ end set_stretch!(stretch::Function) Alter the default value stretch functio used to display images when using -`imview` or displaying an AstroImage. +`imview` or displaying an AstroImageMat. """ function set_stretch!(stretch) _default_stretch[] = stretch @@ -74,7 +74,7 @@ skipmissingnan(itr) = Iterators.filter(el->!ismissing(el) && isfinite(el), itr) """ imview(img; clims=extrema, stretch=identity, cmap=nothing) -Create a read only view of an array or AstroImage mapping its data values +Create a read only view of an array or AstroImageMat mapping its data values to Colors according to `clims`, `stretch`, and `cmap`. The data is first clamped to `clims`, which can either be a tuple of (min, max) @@ -107,7 +107,7 @@ You may alter these defaults using `AstroImages.set_clims!`, `AstroImages.set_s `AstroImages.set_cmap!`. ### Automatic Display -Arrays wrapped by `AstroImage()` get displayed as images automatically by calling +Arrays wrapped by `AstroImageMat()` get displayed as images automatically by calling `imview` on them with the default settings when using displays that support showing PNG images. ### Missing data @@ -206,8 +206,8 @@ function _imview(img, normed::AbstractArray{T}, stretch, cmap) where T # Flip image to match conventions of other programs # flipped_view = view(mapper', reverse(axes(mapper,2)),:) - # return maybe_copyheaders(img, flipped_view) - # return maybe_copyheaders(img, mapper) + # return maybe_copyheader(img, flipped_view) + # return maybe_copyheader(img, mapper) # flipped_view = OffsetArray( # view( @@ -223,7 +223,7 @@ function _imview(img, normed::AbstractArray{T}, stretch, cmap) where T :, ) - return maybe_copyheaders(img, flipped_view) + return maybe_copyheader(img, flipped_view) end @@ -231,8 +231,8 @@ end # TODO: is this the correct function to extend? # Instead of using a datatype like N0f32 to interpret integers as fixed point values in [0,1], # we use a mappedarray to map the native data range (regardless of type) to [0,1] -Images.normedview(img::AstroImage{<:FixedPoint}) = img -function Images.normedview(img::AstroImage{T}) where T +Images.normedview(img::AstroImageMat{<:FixedPoint}) = img +function Images.normedview(img::AstroImageMat{T}) where T imgmin, imgmax = extrema(skipmissingnan(img)) Δ = abs(imgmax - imgmin) normeddata = mappedarray( @@ -240,7 +240,7 @@ function Images.normedview(img::AstroImage{T}) where T pix_norm -> convert(T, pix_norm*Δ + imgmin), img ) - return shareheaders(img, normeddata) + return shareheader(img, normeddata) end """ @@ -260,7 +260,7 @@ function clampednormedview(img::AbstractArray{T}, lims) where T pix_norm -> convert(T, pix_norm*Δ + imgmin), img ) - return maybe_shareheaders(img, normeddata) + return maybe_shareheader(img, normeddata) end function clampednormedview(img::AbstractArray{T}, lims) where T <: Normed # If the data is in a Normed type and the limits are [0,1] then @@ -275,7 +275,7 @@ function clampednormedview(img::AbstractArray{T}, lims) where T <: Normed pix_norm -> pix_norm*Δ + imgmin, img ) - return maybe_shareheaders(img, normeddata) + return maybe_shareheader(img, normeddata) end function clampednormedview(img::AbstractArray{Bool}, lims) return img diff --git a/src/patches.jl b/src/patches.jl index bd9f2f38..7e33ab02 100644 --- a/src/patches.jl +++ b/src/patches.jl @@ -3,7 +3,7 @@ ImageContrastAdjustment =# function Images.ImageContrastAdjustment.adjust_histogram(::Type{T}, - img::AstroImage, + img::AstroImageMat, f::Images.ImageContrastAdjustment.AbstractHistogramAdjustmentAlgorithm, args...; kwargs...) where T out = similar(img, axes(img)) @@ -15,7 +15,7 @@ end #= ImageTransformations =# -# function warp(img::AstroImage, args...; kwargs...) +# function warp(img::AstroImageMat, args...; kwargs...) # out = warp(arraydatat(img), args...; kwargs...) # return copyheaders(img, out) # end @@ -27,30 +27,30 @@ Additional methods to allow Reproject to work. using Reproject """ - img_proj, mask = reproject(img_in::AstroImage, img_out::AstroImage) + img_proj, mask = reproject(img_in::AstroImageMat, img_out::AstroImageMat) -Reprojects the AstroImage `img_in` to the coordinates of `img_out` +Reprojects the AstroImageMat `img_in` to the coordinates of `img_out` according to the WCS information/headers using interpolation. """ -function Reproject.reproject(img_in::AstroImage, img_out::AstroImage) +function Reproject.reproject(img_in::AstroImageMat, img_out::AstroImageMat) data_out, mask = reproject(img_in, img_out) # TODO: should copy the WCS headers from img_out and the remaining # headers from img_in. return copyheaders(img_in, data_out) end -function Reproject.parse_input_data(input_data::AstroImage, hdu) +function Reproject.parse_input_data(input_data::AstroImageMat, hdu) input_data, input_data.wcs end -function Reproject.parse_output_projection(output_data::AstroImage, hdu) +function Reproject.parse_output_projection(output_data::AstroImageMat, hdu) output_data.wcs, size(output_data) end -function Reproject.pad_edges(array_in::AstroImage{T}) where {T} +function Reproject.pad_edges(array_in::AstroImageMat{T}) where {T} image = Matrix{T}(undef, size(array_in)[1] + 2, size(array_in)[2] + 2) image[2:end-1,2:end-1] = array_in image[2:end-1,1] = array_in[:,1] image[2:end-1,end] = array_in[:,end] image[1,:] = image[2,:] image[end,:] = image[end-1,:] - return AstroImage(image, headers(array_in), wcs(array_in)) + return AstroImageMat(image, headers(array_in), wcs(array_in)) end \ No newline at end of file diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index f49e1831..ec29ffea 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -4,9 +4,9 @@ using Printf using PlotUtils: optimize_ticks """ - plot(img::AstroImage; clims=extrema, stretch=identity, cmap=nothing) + plot(img::AstroImageMat; clims=extrema, stretch=identity, cmap=nothing) -Create a read only view of an array or AstroImage mapping its data values +Create a read only view of an array or AstroImageMat mapping its data values to Colors according to `clims`, `stretch`, and `cmap`. The data is first clamped to `clims`, which can either be a tuple of (min, max) @@ -39,7 +39,7 @@ You may alter these defaults using `AstroImages.set_clims!`, `AstroImages.set_s `AstroImages.set_cmap!`. ### Automatic Display -Arrays wrapped by `AstroImage()` get displayed as images automatically by calling +Arrays wrapped by `AstroImageMat()` get displayed as images automatically by calling `imview` on them with the default settings when using displays that support showing PNG images. ### Missing data @@ -59,14 +59,14 @@ save("output.png", v) # imview(). @recipe function f( ::DimensionalData.HeatMapLike, - img::AstroImage{T}; + img::AstroImageMat{T}; clims=_default_clims[], stretch=_default_stretch[], cmap=_default_cmap[], ) where {T<:Number} - println("Hit AstroImage recipe") + println("Hit AstroImageMat recipe") - # We often plot an AstroImage{<:Number} which hasn't yet had + # We often plot an AstroImageMat{<:Number} which hasn't yet had # its wcs cached (wcs_stale=true) and we make an image view here. # That means we may have to keep recomputing the WCS on each plot call # since the result is stored in the imview instead of original image. @@ -163,7 +163,7 @@ end @recipe function f( - img::AstroVec{T}; + img::AstroImageVec{T}; ) where {T<:Number} # We don't to override e.g. histograms @@ -264,13 +264,13 @@ WCSGrid(w,extent,ax) = WCSGrid(w,extent,ax,ones(length(ax))) """ wcsticks(img, axnum) -Generate nice tick labels for an AstroImage along axis `axnum` +Generate nice tick labels for an AstroImageMat along axis `axnum` Returns a vector of pixel positions and a vector of strings. Example: plot(img, xticks=wcsticks(img, 1), yticks=wcsticks(img, 2)) """ -function wcsticks(img::AstroImage, axnum) +function wcsticks(img::AstroImageMat, axnum) gs = wcsgridspec(WCSGrid(img)) tickposx = axnum == 1 ? gs.tickpos1x : gs.tickpos2x tickposw = axnum == 1 ? gs.tickpos1w : gs.tickpos2w @@ -402,15 +402,15 @@ end """ - WCSGrid(img::AstroImage, ax=(1,2), coords=(first(axes(img,ax[1])),first(axes(img,ax[2])))) + WCSGrid(img::AstroImageMat, ax=(1,2), coords=(first(axes(img,ax[1])),first(axes(img,ax[2])))) -Given an AstroImage, return information necessary to plot WCS gridlines in physical +Given an AstroImageMat, return information necessary to plot WCS gridlines in physical coordinates against the image's pixel coordinates. This function has to work on both plotted axes at once to handle rotation and general curvature of the WCS grid projected on the image coordinates. """ -function WCSGrid(img::AstroImage, ax=(1,2), coords=ones(wcs(img).naxis)) +function WCSGrid(img::AstroImageMat, ax=(1,2), coords=ones(wcs(img).naxis)) minx = first(axes(img,ax[1])) maxx = last(axes(img,ax[1])) @@ -425,7 +425,7 @@ end # Recipe for a WCSGrid with lines, optional ticks (on by default), # and optional grid labels (off by defaut). -# The AstroImage plotrecipe uses this recipe for grid lines if `grid=true`. +# The AstroImageMat plotrecipe uses this recipe for grid lines if `grid=true`. @recipe function f(wcsg::WCSGrid, gridspec=wcsgridspec(wcsg)) label --> "" xs, ys = wcsgridlines(gridspec) diff --git a/src/showmime.jl b/src/showmime.jl index daaa0ecc..7d05debe 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -2,13 +2,13 @@ # @. color(matrix / 255 * T(contrast) + T(brightness) / 255) # """ -# brightness_contrast(image::AstroImage; brightness_range = 0:255, contrast_range = 1:1000, header_number = 1) +# brightness_contrast(image::AstroImageMat; brightness_range = 0:255, contrast_range = 1:1000, header_number = 1) # Visualize the fits image by changing the brightness and contrast of image. # Users can also provide their own range as keyword arguments. # """ -# function brightness_contrast(img::AstroImage{T,N}; brightness_range = 0:255, +# function brightness_contrast(img::AstroImageMat{T,N}; brightness_range = 0:255, # contrast_range = 1:1000, header_number = 1) where {T,N} # @manipulate for brightness in brightness_range, contrast in contrast_range # _brightness_contrast(C, img.data[header_number], brightness, contrast) @@ -16,27 +16,27 @@ # end # This is used in Jupyter notebooks -# Base.show(io::IO, mime::MIME"text/html", img::AstroImage; kwargs...) = +# Base.show(io::IO, mime::MIME"text/html", img::AstroImageMat; kwargs...) = # show(io, mime, brightness_contrast(img), kwargs...) # This is used in VSCode and others -# If the user displays a AstroImage of colors (e.g. one created with imview) +# If the user displays a AstroImageMat of colors (e.g. one created with imview) # fal through and display the data as an image -Base.show(io::IO, mime::MIME"image/png", img::AstroImage{T}; kwargs...) where {T<:Colorant} = +Base.show(io::IO, mime::MIME"image/png", img::AstroImageMat{T}; kwargs...) where {T<:Colorant} = show(io, mime, arraydata(img), kwargs...) # Otherwise, call imview with the default settings. -Base.show(io::IO, mime::MIME"image/png", img::AstroImage{T}; kwargs...) where {T} = +Base.show(io::IO, mime::MIME"image/png", img::AstroImageMat{T}; kwargs...) where {T} = show(io, mime, imview(img), kwargs...) -# Lazily reinterpret the AstroImage as a Matrix{Color}, upon request. +# Lazily reinterpret the AstroImageMat as a Matrix{Color}, upon request. # By itself, Images.colorview works fine on AstroImages. But # AstroImages are not normalized to be between [0,1]. So we override # colorview to first normalize the data using scaleminmax -function render(img::AstroImage{T,N}) where {T,N} +function render(img::AstroImageMat{T,N}) where {T,N} # imgmin, imgmax = img.minmax imgmin, imgmax = extrema(img) # Add one to maximum to work around this issue: @@ -44,4 +44,4 @@ function render(img::AstroImage{T,N}) where {T,N} f = scaleminmax(_float(imgmin), _float(max(imgmax, imgmax + one(T)))) return colorview(Gray, f.(_float.(img.data))) end -Images.colorview(img::AstroImage) = render(img) +Images.colorview(img::AstroImageMat) = render(img) diff --git a/src/wcs.jl b/src/wcs.jl index 309dcf17..ada7a147 100644 --- a/src/wcs.jl +++ b/src/wcs.jl @@ -1,5 +1,330 @@ +const WCS_HEADERS_TEMPLATES = [ + "DATE", + "MJD", + + + "WCSAXESa", + "WCAXna", + "WCSTna", + "WCSXna", + "CRPIXja", + "jCRPna", + "jCRPXn", + "TCRPna", + "TCRPXn", + "PCi_ja", + "ijPCna", + "TPn_ka", + "TPCn_ka", + "CDi_ja", + "ijCDna", + "TCn_ka", + "TCDn_ka", + "CDELTia", + "iCDEna", + "iCDLTn", + "TCDEna", + "TCDLTn", + "CROTAi", + "iCROTn", + "TCROTn", + "CUNITia", + "iCUNna", + "iCUNIn", + "TCUNna", + "TCUNIn", + "CTYPEia", + "iCTYna", + "iCTYPn", + "TCTYna", + "TCTYPn", + "CRVALia", + "iCRVna", + "iCRVLn", + "TCRVna", + "TCRVLn", + "LONPOLEa", + "LONPna", + "LATPOLEa", + "LATPna", + "RESTFREQ", + "RESTFRQa", + "RFRQna", + "RESTWAVa", + "RWAVna", + "PVi_ma", + "iVn_ma", + "iPVn_ma", + "TVn_ma", + "TPVn_ma", + "PROJPm", + "PSi_ma", + "iSn_ma", + "iPSn_ma", + "TSn_ma", + "TPSn_ma", + "VELREF", + "CNAMEia", + "iCNAna", + "iCNAMn", + "TCNAna", + "TCNAMn", + "CRDERia", + "iCRDna", + "iCRDEn", + "TCRDna", + "TCRDEn", + "CSYERia", + "iCSYna", + "iCSYEn", + "TCSYna", + "TCSYEn", + "CZPHSia", + "iCZPna", + "iCZPHn", + "TCZPna", + "TCZPHn", + "CPERIia", + "iCPRna", + "iCPERn", + "TCPRna", + "TCPERn", + "WCSNAMEa", + "WCSNna", + "TWCSna", + "TIMESYS", + "TREFPOS", + "TRPOSn", + "TREFDIR", + "TRDIRn", + "PLEPHEM", + "TIMEUNIT", + "DATEREF", + "MJDREF", + "MJDREFI", + "MJDREFF", + "JDREF", + "JDREFI", + "JDREFF", + "TIMEOFFS", + "DATE-OBS", + "DOBSn", + "DATE-BEG", + "DATE-AVG", + "DAVGn", + "DATE-END", + "MJD-OBS", + "MJDOBn", + "MJD-BEG", + "MJD-AVG", + "MJDAn", + "MJD-END", + "JEPOCH", + "BEPOCH", + "TSTART", + "TSTOP", + "XPOSURE", + "TELAPSE", + "TIMSYER", + "TIMRDER", + "TIMEDEL", + "TIMEPIXR", + "OBSGEO-X", + "OBSGXn", + "OBSGEO-Y", + "OBSGYn", + "OBSGEO-Z", + "OBSGZn", + "OBSGEO-L", + "OBSGLn", + "OBSGEO-B", + "OBSGBn", + "OBSGEO-H", + "OBSGHn", + "OBSORBIT", + "RADESYSa", + "RADEna", + "RADECSYS", + "EPOCH", + "EQUINOXa", + "EQUIna", + "SPECSYSa", + "SPECna", + "SSYSOBSa", + "SOBSna", + "VELOSYSa", + "VSYSna", + "VSOURCEa", + "VSOUna", + "ZSOURCEa", + "ZSOUna", + "SSYSSRCa", + "SSRCna", + "VELANGLa", + "VANGna", + "RSUN_REF", + "DSUN_OBS", + "CRLN_OBS", + "HGLN_OBS", + "HGLT_OBS", + "NAXISn", + "CROTAn", + "PROJPn", + "CPDISja", + "CQDISia", + "DPja", + "DQia", + "CPERRja", + "CQERRia", + "DVERRa", + "A_ORDER", + "B_ORDER", + "AP_ORDER", + "BP_ORDER", + "A_DMAX", + "B_DMAX", + "A_p_q", + "B_p_q", + "AP_p_q", + "BP_p_q", + "CNPIX1", + "PPO3", + "PPO6", + "XPIXELSZ", + "YPIXELSZ", + "PLTRAH", + "PLTRAM", + "PLTRAS", + "PLTDECSN", + "PLTDECD", + "PLTDECM", + "PLTDECS", + "PLATEID", + "AMDXm", + "AMDYm", + "WATi_m" +] + +# Expand the headers containing lower case specifers into N copies +Is = [""; string.(1:4)] +# Find all lower case templates +const WCS_HEADERS = Set(mapreduce(vcat, WCS_HEADERS_TEMPLATES) do template + if any(islowercase, template) + template_chars = Vector{Char}(template) + chars = template_chars[islowercase.(template_chars)] + out = String[template] + for replace_target in chars + newout = String[] + for template in out + for i in Is + push!(newout, replace(template, replace_target=>i)) + end + end + append!(out, newout) + end + out + else + template + end +end) + + + +""" + emptywcs() + +Given an AbstractArray, return a blank WCSTransform of the appropriate +dimensionality. +""" +emptywcs(data::AbstractArray) = WCSTransform(ndims(data)) +emptywcs(img::AstroImage) = WCSTransform(length(getfield(img, :wcs_axes))) + + + +# """ +# filterwcsheader(hdrs::FITSHeader) + +# Return a new FITSHeader containing WCS header from `hdrs`. +# This is useful for creating a new image with the same coordinates +# as another. +# """ +# function filterwcsheader(hdrs::FITSHeader) +# include_keys = intersect(keys(hdrs), WCS_HEADERS) +# return FITSHeader( +# include_keys, +# map(key -> hdrs[key], include_keys), +# map(key -> get_comment(hdrs, key), include_keys), +# ) +# end + +""" + wcsfromheader(img::AstroImage; relax=WCS.HDR_ALL, ignore_rejected=true) + +Helper function to create a WCSTransform from an array and +FITSHeaders. +""" +function wcsfromheader(img::AstroImage; relax=WCS.HDR_ALL) + # We only need to stringify WCS header. This might just be 4-10 header keywords + # out of thousands. + local wcsout + # Load the header without ignoring rejected to get error messages + try + wcsout = WCS.from_header( + string(header(img)); + ignore_rejected=false, + relax + ) + catch err + # Load them again ignoring error messages + wcsout = WCS.from_header( + string(header(img)); + ignore_rejected=true, + relax + ) + # If that still fails, the use gets the stack trace here + # If not, print a warning about rejected header + @warn "WCSTransform was generated by ignoring rejected header. It may not be valid." exception=err + end + + if length(wcsout) == 1 + return only(wcsout) + elseif length(wcsout) == 0 + return emptywcs(img) + else + @warn "Mutiple WCSTransform returned from header, using first and ignoring the rest." + return first(wcsout) + end +end +# TODO: wcsfromheader(::FITSHeader,) + + # Smart versions of pix_to_world and world_to_pix +function WCS.pix_to_world!(world_coords_out, img::AstroImage, pixcoords) + # Find the coordinates in the parent array. + # Dimensional data + pixcoords_floored = floor.(Int, pixcoords) + pixcoords_frac = (pixcoords .- pixcoords_floored) .* step.(dims(img)) + parentcoords = getindex.(dims(img), pixcoords_floored) .+ pixcoords_frac + # WCS.jl is very restrictive. We need to supply a Vector{Float64} + # as input, not any other kind of collection. + if parentcoords isa Array{Float64} + parentcoords_prepared = parentcoords + else + parentcoords_prepared = [Float64(c) for c in parentcoords] + end + + # TODO: we need to pass in ref dims locations as well, and then filter the + # output to only include the dims of the current slice? + # out = zeros(Float64, length(dims(img))+length(refdims(img)), size(pixcoords,2)) -function WCS.pix_to_world(img::AstroArray, pixcoords) - @show dims(img) + return WCS.pix_to_world!(wcs(img), parentcoords_prepared, world_coords_out) +end +function WCS.pix_to_world(img::AstroImage, pixcoords) + if pixcoords isa Array{Float64} + pixcoords_prepared = pixcoords + else + pixcoords_prepared = [Float64(c) for c in pixcoords] + end + out = similar(pixcoords_prepared, Float64) + return WCS.pix_to_world!(out, img, pixcoords_prepared) end \ No newline at end of file diff --git a/src/wcs_headers.jl b/src/wcs_headers.jl index 9bb3548f..e69de29b 100644 --- a/src/wcs_headers.jl +++ b/src/wcs_headers.jl @@ -1,231 +0,0 @@ -const WCS_HEADERS_TEMPLATES = [ - "DATE", - "MJD", - - - "WCSAXESa", - "WCAXna", - "WCSTna", - "WCSXna", - "CRPIXja", - "jCRPna", - "jCRPXn", - "TCRPna", - "TCRPXn", - "PCi_ja", - "ijPCna", - "TPn_ka", - "TPCn_ka", - "CDi_ja", - "ijCDna", - "TCn_ka", - "TCDn_ka", - "CDELTia", - "iCDEna", - "iCDLTn", - "TCDEna", - "TCDLTn", - "CROTAi", - "iCROTn", - "TCROTn", - "CUNITia", - "iCUNna", - "iCUNIn", - "TCUNna", - "TCUNIn", - "CTYPEia", - "iCTYna", - "iCTYPn", - "TCTYna", - "TCTYPn", - "CRVALia", - "iCRVna", - "iCRVLn", - "TCRVna", - "TCRVLn", - "LONPOLEa", - "LONPna", - "LATPOLEa", - "LATPna", - "RESTFREQ", - "RESTFRQa", - "RFRQna", - "RESTWAVa", - "RWAVna", - "PVi_ma", - "iVn_ma", - "iPVn_ma", - "TVn_ma", - "TPVn_ma", - "PROJPm", - "PSi_ma", - "iSn_ma", - "iPSn_ma", - "TSn_ma", - "TPSn_ma", - "VELREF", - "CNAMEia", - "iCNAna", - "iCNAMn", - "TCNAna", - "TCNAMn", - "CRDERia", - "iCRDna", - "iCRDEn", - "TCRDna", - "TCRDEn", - "CSYERia", - "iCSYna", - "iCSYEn", - "TCSYna", - "TCSYEn", - "CZPHSia", - "iCZPna", - "iCZPHn", - "TCZPna", - "TCZPHn", - "CPERIia", - "iCPRna", - "iCPERn", - "TCPRna", - "TCPERn", - "WCSNAMEa", - "WCSNna", - "TWCSna", - "TIMESYS", - "TREFPOS", - "TRPOSn", - "TREFDIR", - "TRDIRn", - "PLEPHEM", - "TIMEUNIT", - "DATEREF", - "MJDREF", - "MJDREFI", - "MJDREFF", - "JDREF", - "JDREFI", - "JDREFF", - "TIMEOFFS", - "DATE-OBS", - "DOBSn", - "DATE-BEG", - "DATE-AVG", - "DAVGn", - "DATE-END", - "MJD-OBS", - "MJDOBn", - "MJD-BEG", - "MJD-AVG", - "MJDAn", - "MJD-END", - "JEPOCH", - "BEPOCH", - "TSTART", - "TSTOP", - "XPOSURE", - "TELAPSE", - "TIMSYER", - "TIMRDER", - "TIMEDEL", - "TIMEPIXR", - "OBSGEO-X", - "OBSGXn", - "OBSGEO-Y", - "OBSGYn", - "OBSGEO-Z", - "OBSGZn", - "OBSGEO-L", - "OBSGLn", - "OBSGEO-B", - "OBSGBn", - "OBSGEO-H", - "OBSGHn", - "OBSORBIT", - "RADESYSa", - "RADEna", - "RADECSYS", - "EPOCH", - "EQUINOXa", - "EQUIna", - "SPECSYSa", - "SPECna", - "SSYSOBSa", - "SOBSna", - "VELOSYSa", - "VSYSna", - "VSOURCEa", - "VSOUna", - "ZSOURCEa", - "ZSOUna", - "SSYSSRCa", - "SSRCna", - "VELANGLa", - "VANGna", - "RSUN_REF", - "DSUN_OBS", - "CRLN_OBS", - "HGLN_OBS", - "HGLT_OBS", - "NAXISn", - "CROTAn", - "PROJPn", - "CPDISja", - "CQDISia", - "DPja", - "DQia", - "CPERRja", - "CQERRia", - "DVERRa", - "A_ORDER", - "B_ORDER", - "AP_ORDER", - "BP_ORDER", - "A_DMAX", - "B_DMAX", - "A_p_q", - "B_p_q", - "AP_p_q", - "BP_p_q", - "CNPIX1", - "PPO3", - "PPO6", - "XPIXELSZ", - "YPIXELSZ", - "PLTRAH", - "PLTRAM", - "PLTRAS", - "PLTDECSN", - "PLTDECD", - "PLTDECM", - "PLTDECS", - "PLATEID", - "AMDXm", - "AMDYm", - "WATi_m" -] - -# Expand the headers containing lower case specifers into N copies -Is = [""; string.(1:4)] -# Find all lower case templates -const WCS_HEADERS = Set(mapreduce(vcat, WCS_HEADERS_TEMPLATES) do template - if any(islowercase, template) - template_chars = Vector{Char}(template) - chars = template_chars[islowercase.(template_chars)] - out = String[template] - for replace_target in chars - newout = String[] - for template in out - for i in Is - push!(newout, replace(template, replace_target=>i)) - end - end - append!(out, newout) - end - out - else - template - end -end) - -## diff --git a/test/ccd2rgb.jl b/test/ccd2rgb.jl index 70ee6739..f65fccd4 100644 --- a/test/ccd2rgb.jl +++ b/test/ccd2rgb.jl @@ -45,8 +45,8 @@ end @test isapprox(blue.(asinh_res), blue.(asinh_ans), nans = true, rtol = 3e-5) @test isapprox(green.(asinh_res), green.(asinh_ans), nans = true, rtol = 3e-5) - @testset "AstroImage using ccd2rgb and properties" begin - img = AstroImage(RGB, (joinpath("data","casa_0.5-1.5keV.fits"), joinpath("data","casa_1.5-3.0keV.fits"), + @testset "AstroImageMat using ccd2rgb and properties" begin + img = AstroImageMat(RGB, (joinpath("data","casa_0.5-1.5keV.fits"), joinpath("data","casa_1.5-3.0keV.fits"), joinpath("data","casa_4.0-6.0keV.fits"))) @test RGB.(img.property.rgb_image) isa Array{RGB{Float64},2} @@ -85,7 +85,7 @@ end @test isapprox(blue.(img.property.rgb_image), blue.(ans), nans = true) - img = AstroImage(joinpath("data","casa_0.5-1.5keV.fits")) + img = AstroImageMat(joinpath("data","casa_0.5-1.5keV.fits")) @test_throws DomainError set_brightness!(img, 1.2) @test_throws DomainError set_contrast!(img, 1.2) end diff --git a/test/plots.jl b/test/plots.jl index 1ea2bcba..8678272c 100644 --- a/test/plots.jl +++ b/test/plots.jl @@ -3,7 +3,7 @@ using AstroImages: pix2world_xformatter, pix2world_yformatter @testset "Plot recipes" begin data = randn(10, 10) - img = AstroImage(data) + img = AstroImageMat(data) wcs1 = WCSTransform(2; ctype = ["RA---AIR", "DEC--AIR"]) wcs2 = WCSTransform(2; ctype = ["GLON--", "GLAT--"]) wcs3 = WCSTransform(2; ctype = ["TLON--", "TLAT--"]) diff --git a/test/runtests.jl b/test/runtests.jl index 98545eaf..ceb8ea86 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -39,7 +39,7 @@ end end @test load(fname, 1)[1] == data @test load(fname, (1, 1))[1] == (data, data) - img = AstroImage(fname) + img = AstroImageMat(fname) rendered_img = colorview(img) @test iszero(minimum(rendered_img)) end @@ -57,7 +57,7 @@ end @test @inferred(_brightness_contrast(Gray, M, 0, 255)) == Gray.(M) @test @inferred(_brightness_contrast(Gray, M, 255, 0)) == Gray.(ones(size(M))) @test @inferred(_brightness_contrast(Gray, M, 0, 0)) == Gray.(zeros(size(M))) - @test brightness_contrast(AstroImage(M)) isa Widgets.Widget{:manipulate,Any} + @test brightness_contrast(AstroImageMat(M)) isa Widgets.Widget{:manipulate,Any} end @testset "default handler" begin @@ -67,7 +67,7 @@ end FITS(fname, "w") do f write(f, data) end - @test_throws ErrorException AstroImage(fname) + @test_throws ErrorException AstroImageMat(fname) end @testset "no ImageHDU" begin @@ -86,23 +86,23 @@ end FITS(fname, "w") do f write(f, indata; varcols=["vcol", "VCOL"]) - @test_throws MethodError AstroImage(f) + @test_throws MethodError AstroImageMat(f) end end - @testset "Opening AstroImage in different ways" begin + @testset "Opening AstroImageMat in different ways" begin data = rand(2,2) wcs = WCSTransform(2;) FITS(fname, "w") do f write(f, data) end f = FITS(fname) - @test AstroImage(fname, 1) isa AstroImage - @test AstroImage(Gray ,fname, 1) isa AstroImage - @test AstroImage(Gray, f, 1) isa AstroImage - @test AstroImage(data, wcs) isa AstroImage - @test AstroImage((data,data), (wcs,wcs)) isa AstroImage - @test AstroImage(Gray, data, wcs) isa AstroImage + @test AstroImageMat(fname, 1) isa AstroImageMat + @test AstroImageMat(Gray ,fname, 1) isa AstroImageMat + @test AstroImageMat(Gray, f, 1) isa AstroImageMat + @test AstroImageMat(data, wcs) isa AstroImageMat + @test AstroImageMat((data,data), (wcs,wcs)) isa AstroImageMat + @test AstroImageMat(Gray, data, wcs) isa AstroImageMat close(f) end @@ -125,17 +125,17 @@ end write(f, rand(2, 2)) end - @test @test_logs (:info, "Image was loaded from HDU 3") AstroImage(fname) isa AstroImage + @test @test_logs (:info, "Image was loaded from HDU 3") AstroImageMat(fname) isa AstroImageMat end rm(fname, force = true) end @testset "Utility functions" begin - @test size(AstroImage((rand(10,10), rand(10,10)))) == ((10,10), (10,10)) - @test length(AstroImage((rand(10,10), rand(10,10)))) == 2 + @test size(AstroImageMat((rand(10,10), rand(10,10)))) == ((10,10), (10,10)) + @test length(AstroImageMat((rand(10,10), rand(10,10)))) == 2 end -@testset "multi image AstroImage" begin +@testset "multi image AstroImageMat" begin data1 = rand(10,10) data2 = rand(10,10) fname = tempname() * ".fits" @@ -144,20 +144,20 @@ end write(f, data2) end - img = AstroImage(fname, (1,2)) + img = AstroImageMat(fname, (1,2)) @test length(img.data) == 2 @test img.data[1] == data1 @test img.data[2] == data2 f = FITS(fname) - img = AstroImage(Gray, f, (1,2)) + img = AstroImageMat(Gray, f, (1,2)) @test length(img.data) == 2 @test img.data[1] == data1 @test img.data[2] == data2 close(f) end -@testset "multi wcs AstroImage" begin +@testset "multi wcs AstroImageMat" begin fname = tempname() * ".fits" f = FITS(fname, "w") inhdr = FITSHeader(["CTYPE1", "CTYPE2", "RADESYS", "FLTKEY", "INTKEY", "BOOLKEY", "STRKEY", "COMMENT", @@ -178,20 +178,20 @@ end write(f, indata; header=inhdr) close(f) - img = AstroImage(fname, (1,2)) + img = AstroImageMat(fname, (1,2)) f = FITS(fname) @test length(img.wcs) == 2 @test WCS.to_header(img.wcs[1]) === WCS.to_header(WCS.from_header(read_header(f[1], String))[1]) @test WCS.to_header(img.wcs[2]) === WCS.to_header(WCS.from_header(read_header(f[2], String))[1]) - img = AstroImage(Gray, f, (1,2)) + img = AstroImageMat(Gray, f, (1,2)) @test length(img.wcs) == 2 @test WCS.to_header(img.wcs[1]) === WCS.to_header(WCS.from_header(read_header(f[1], String))[1]) @test WCS.to_header(img.wcs[2]) === WCS.to_header(WCS.from_header(read_header(f[2], String))[1]) close(f) end -@testset "multi file AstroImage" begin +@testset "multi file AstroImageMat" begin fname1 = tempname() * ".fits" f = FITS(fname1, "w") inhdr = FITSHeader(["CTYPE1", "CTYPE2", "RADESYS", "FLTKEY", "INTKEY", "BOOLKEY", "STRKEY", "COMMENT", @@ -223,7 +223,7 @@ end write(f, indata3; header=inhdr) close(f) - img = AstroImage((fname1, fname2, fname3)) + img = AstroImageMat((fname1, fname2, fname3)) f1 = FITS(fname1) f2 = FITS(fname2) f3 = FITS(fname3) @@ -236,7 +236,7 @@ end WCS.to_header(img.wcs[3]) == WCS.to_header(WCS.from_header(read_header(f1[1], String))[1]) @test eltype(eltype(img.data)) == Int - img = AstroImage(Gray, (f1, f2, f3), (1,1,1)) + img = AstroImageMat(Gray, (f1, f2, f3), (1,1,1)) @test length(img.data) == length(img.wcs) == 3 @test img.data[1] == indata1 @test img.data[2] == indata2 From a1e3d54662cf89f34921bc2080fd524a2bc60544 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 13 Mar 2022 11:29:56 -0700 Subject: [PATCH 055/178] Delete empty file --- src/wcs_headers.jl | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/wcs_headers.jl diff --git a/src/wcs_headers.jl b/src/wcs_headers.jl deleted file mode 100644 index e69de29b..00000000 From 4177b7b3b3026c43f2a15014454d246e206f7e4a Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 13 Mar 2022 12:05:31 -0700 Subject: [PATCH 056/178] Export some symbols from DimensionalData --- src/AstroImages.jl | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 70ade58e..f2d23392 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -87,6 +87,12 @@ const AstroImageVec{T,D,R,A} = AstroImage{T,1,D,R,A} where {T,D,R,A} const AstroImageMat{T,D,R,A} = AstroImage{T,2,D,R,A} where {T,D,R,A} export AstroImage, AstroImageVec, AstroImageMat +# Re-export symbols from DimensionalData that users will need +# for indexing. +export X, Y, Z, Dim +export At, Near, Between, .. +export dims, refdims + # Accessors """ Images.arraydata(img::AstroImage) @@ -102,7 +108,6 @@ function wcs(img::AstroImage) end - # Implement DimensionalData interface Base.parent(img::AstroImage) = arraydata(img) DimensionalData.dims(A::AstroImage) = getfield(A, :dims) @@ -274,16 +279,6 @@ end AstroImage(data::AbstractArray, wcs::WCSTransform) = AstroImage(data, emptyheader(), wcs) -using UUIDs -# TODO: This should be registered correctly with FileIO -del_format(format"FITS") -add_format(format"FITS", - # See https://www.loc.gov/preservation/digital/formats/fdd/fdd000317.shtml#sign - [0x53,0x49,0x4d,0x50,0x4c,0x45,0x20,0x20,0x3d,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x54], - [".fit", ".fits", ".fts", ".FIT", ".FITS", ".FTS"], - [:FITSIO => UUID("525bcba6-941b-5504-bd06-fd0dc1a4d2eb")], - [:AstroImages => UUID("fe3fc30c-9b16-11e9-1c73-17dabf39f4ad")] -) """ load(fitsfile::String) @@ -509,6 +504,7 @@ include("showmime.jl") # include("ccd2rgb.jl") # include("patches.jl") +using UUIDs function __init__() @@ -521,6 +517,16 @@ function __init__() end end end + + # TODO: This should be registered correctly with FileIO + del_format(format"FITS") + add_format(format"FITS", + # See https://www.loc.gov/preservation/digital/formats/fdd/fdd000317.shtml#sign + [0x53,0x49,0x4d,0x50,0x4c,0x45,0x20,0x20,0x3d,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x54], + [".fit", ".fits", ".fts", ".FIT", ".FITS", ".FTS"], + [:FITSIO => UUID("525bcba6-941b-5504-bd06-fd0dc1a4d2eb")], + [:AstroImages => UUID("fe3fc30c-9b16-11e9-1c73-17dabf39f4ad")] + ) end end # module From 522ebd097cd28e3a11e170c0df114321d28d9056 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 13 Mar 2022 12:05:47 -0700 Subject: [PATCH 057/178] Store settings in RefValue not Ref (typestability) --- src/imview.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/imview.jl b/src/imview.jl index ef96b1c6..ca00189a 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -27,9 +27,9 @@ function percent(perc::Number) return clims end -const _default_cmap = Ref{Union{Symbol,Nothing}}(:magma)#nothing) -const _default_clims = Ref{Any}(percent(99.5)) -const _default_stretch = Ref{Any}(identity) +const _default_cmap = Base.RefValue{Union{Symbol,Nothing}}(:magma)#nothing) +const _default_clims = Base.RefValue{Any}(percent(99.5)) +const _default_stretch = Base.RefValue{Any}(identity) """ set_cmap!(cmap::Symbol) From f6f7a28dab30d2c64db085cf79c95f5fb8d62f97 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 13 Mar 2022 12:06:05 -0700 Subject: [PATCH 058/178] Prototype automatic display of complex images --- src/showmime.jl | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/showmime.jl b/src/showmime.jl index 7d05debe..39145ec5 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -27,10 +27,33 @@ Base.show(io::IO, mime::MIME"image/png", img::AstroImageMat{T}; kwargs...) where show(io, mime, arraydata(img), kwargs...) # Otherwise, call imview with the default settings. -Base.show(io::IO, mime::MIME"image/png", img::AstroImageMat{T}; kwargs...) where {T} = +Base.show(io::IO, mime::MIME"image/png", img::AstroImageMat{T}; kwargs...) where {T<:Real} = show(io, mime, imview(img), kwargs...) +# Special handling for complex images +function Base.show(io::IO, mime::MIME"image/png", img::AstroImageMat{T}; kwargs...) where {T<:Complex} + # Not sure we really want to support this functionality, but we will allow it for + # now with a warning. + @warn "Displaying complex image as magnitude and phase (maxlog=1)" maxlog=1 + mag_view = imview(abs.(img)) + angle_view = imview(angle.(img), clims=(-pi, pi), cmap=:turbo) + show(io, mime, vcat(mag_view,angle_view), kwargs...) +end + +# const _autoshow = Base.RefValue{Bool}(true) +# """ +# set_autoshow!(autoshow::Bool) +# By default, `display`ing a 2D AstroImage e.g. at the REPL or in a notebook +# shows it as a PNG image using the `imview` function and user's default +# colormap, stretch, etc. +# If set to false, displaying an image will just show a textual representation. +# You can still visualize images using `imview`. +# """ +# function set_autoshow!(autoshow::Bool) +# _autoshow[] = autoshow +# end +# TODO: for this to work, we need to actually add and remove a show method. TBD how. # Lazily reinterpret the AstroImageMat as a Matrix{Color}, upon request. # By itself, Images.colorview works fine on AstroImages. But From 2d62d965af01808a2f13105746be1fb9e6ffb2de Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 14 Mar 2022 08:15:36 -0700 Subject: [PATCH 059/178] Fleshed out pix_to_world implementation --- src/AstroImages.jl | 15 +++++++- src/showmime.jl | 2 +- src/wcs.jl | 87 ++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 87 insertions(+), 17 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index f2d23392..62f637c9 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -93,6 +93,19 @@ export X, Y, Z, Dim export At, Near, Between, .. export dims, refdims +# We need to keep a canonical order of dimensions to match back with WCS +# dimension numbers. E.g. if we see Z(), we need to know this is WCSTransform(..).ctype[3]. +# Currently this is supported up to dimension 10, but this feels arbitrary. +# In future, let's just hardcode X,Y,Z and then use the dimension number itself +# after that. +const dimnames = ( + X, Y, Z, + (Dim{i} for i in 4:10)... +) + +# Export WCS coordinate conversion functions +export pix_to_world, pix_to_world! + # Accessors """ Images.arraydata(img::AstroImage) @@ -161,8 +174,8 @@ for f in [ :(Base.adjoint), :(Base.transpose), :(Base.view) - # TODO: check view works ] + # TODO: these functions are copying headers @eval ($f)(img::AstroImage) = shareheader(img, $f(arraydata(img))) end diff --git a/src/showmime.jl b/src/showmime.jl index 39145ec5..754534f1 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -36,7 +36,7 @@ function Base.show(io::IO, mime::MIME"image/png", img::AstroImageMat{T}; kwargs. # now with a warning. @warn "Displaying complex image as magnitude and phase (maxlog=1)" maxlog=1 mag_view = imview(abs.(img)) - angle_view = imview(angle.(img), clims=(-pi, pi), cmap=:turbo) + angle_view = imview(angle.(img), clims=(-pi, pi), cmap=:cyclic_mygbm_30_95_c78_n256_s25) show(io, mime, vcat(mag_view,angle_view), kwargs...) end diff --git a/src/wcs.jl b/src/wcs.jl index ada7a147..1ba396b6 100644 --- a/src/wcs.jl +++ b/src/wcs.jl @@ -237,7 +237,7 @@ Given an AbstractArray, return a blank WCSTransform of the appropriate dimensionality. """ emptywcs(data::AbstractArray) = WCSTransform(ndims(data)) -emptywcs(img::AstroImage) = WCSTransform(length(getfield(img, :wcs_axes))) +emptywcs(img::AstroImage) = WCSTransform(length(dims(img))+length(refdims(img))) @@ -299,6 +299,61 @@ end # Smart versions of pix_to_world and world_to_pix +""" + pix_to_world(img::AstroImage, pixcoords) + +Given an astro image, look up the world coordinates of the pixels given +by `pixcoords`. World coordinates are resolved using WCS.jl and a +WCSTransform calculated from any FITS header present in `img`. If +no WCS information is in the header, or the axes are all linear, this will +just return pixel coordinates. + +`pixcoords` should be the coordinates in your current selection +of the image. For example, if you select a slice like this: +```julia +julia> cube = load("some-3d-cube.fits") +julia> slice = cube[10:20, 30:40, 5] +``` + +Then to look up the coordinates of the pixel in the bottom left corner of +`slice`, run: +```julia +julia> world_coords = pix_to_world(img, (1, 1)) +[10, 30, 5] +``` +If WCS information was present in the header of `cube`, then those coordinates +would be resolved using axis 1, 2, and 3 respectively. + +!! Coordinates must be provided in the order of `dims(img)`. If you transpose +an image, the order you pass the coordinates should not change. +""" +function WCS.pix_to_world(img::AstroImage, pixcoords) + if pixcoords isa Array{Float64} + pixcoords_prepared = pixcoords + else + pixcoords_prepared = [Float64(c) for c in pixcoords] + end + D_out = length(dims(img))+length(refdims(img)) + if ndims(pixcoords_prepared) > 1 + out = similar(pixcoords_prepared, Float64, D_out, size(pixcoords_prepared,2)) + else + out = similar(pixcoords_prepared, Float64, D_out) + end + return WCS.pix_to_world!(out, img, pixcoords_prepared) +end +function WCS.pix_to_world(img::AstroImage, pixcoords::NTuple{N,DimensionalData.Dimension}) where N + pixcoords_prepared = zeros(Float64, length(pixcoords)) + for dim in pixcoords + j = findfirst(dimnames) do dim_candidate + name(dim_candidate) == name(dim) + end + pixcoords_prepared[j] = dim[] + end + D_out = length(dims(img))+length(refdims(img)) + out = zeros(Float64, D_out) + return WCS.pix_to_world!(out, img, pixcoords_prepared) +end +WCS.pix_to_world(img::AstroImage, pixcoords::DimensionalData.Dimension...) = WCS.pix_to_world(img, pixcoords) function WCS.pix_to_world!(world_coords_out, img::AstroImage, pixcoords) # Find the coordinates in the parent array. # Dimensional data @@ -307,24 +362,26 @@ function WCS.pix_to_world!(world_coords_out, img::AstroImage, pixcoords) parentcoords = getindex.(dims(img), pixcoords_floored) .+ pixcoords_frac # WCS.jl is very restrictive. We need to supply a Vector{Float64} # as input, not any other kind of collection. - if parentcoords isa Array{Float64} - parentcoords_prepared = parentcoords - else - parentcoords_prepared = [Float64(c) for c in parentcoords] - end + # TODO: avoid allocation in case where refdims=() and pixcoords isa Array{Float64} + parentcoords_prepared = zeros(length(dims(img))+length(refdims(img))) # TODO: we need to pass in ref dims locations as well, and then filter the # output to only include the dims of the current slice? # out = zeros(Float64, length(dims(img))+length(refdims(img)), size(pixcoords,2)) + for (i, dim) in enumerate(dims(img)) + j = findfirst(dimnames) do dim_candidate + name(dim_candidate) == name(dim) + end + parentcoords_prepared[j] = parentcoords[i] + end + for dim in refdims(img) + j = findfirst(dimnames) do dim_candidate + name(dim_candidate) == name(dim) + end + parentcoords_prepared[j] = dim[1] + end + @show parentcoords_prepared + return WCS.pix_to_world!(wcs(img), parentcoords_prepared, world_coords_out) -end -function WCS.pix_to_world(img::AstroImage, pixcoords) - if pixcoords isa Array{Float64} - pixcoords_prepared = pixcoords - else - pixcoords_prepared = [Float64(c) for c in pixcoords] - end - out = similar(pixcoords_prepared, Float64) - return WCS.pix_to_world!(out, img, pixcoords_prepared) end \ No newline at end of file From 2c7eeef1db154bf5f73c372c9a957f1c6a4822e2 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 14 Mar 2022 08:27:15 -0700 Subject: [PATCH 060/178] WIP adapting plot recipes --- src/AstroImages.jl | 2 +- src/plot-recipes.jl | 62 ++++++++++++++++++++++++++------------------- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 62f637c9..15e5abdd 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -513,7 +513,7 @@ emptyheader() = FITSHeader(String[],[],String[]) include("wcs.jl") include("imview.jl") include("showmime.jl") -# include("plot-recipes.jl") +include("plot-recipes.jl") # include("ccd2rgb.jl") # include("patches.jl") diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index ec29ffea..ea2757ea 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -58,7 +58,7 @@ save("output.png", v) # This recipe promotes AstroImages of numerical data into full color using # imview(). @recipe function f( - ::DimensionalData.HeatMapLike, + s::DimensionalData.HeatMapLike, img::AstroImageMat{T}; clims=_default_clims[], stretch=_default_stretch[], @@ -102,24 +102,23 @@ save("output.png", v) # In astropy, the ticks are actually tilted to reflect this, though in general # the transformation from pixel to coordinates can be non-linear and curved. - # ax = haskey(plotattributes, :axes) ? plotattributes[:axes] : (1,2) - # coords = haskey(plotattributes, :coords) ? plotattributes[:coords] : ones(wcs(img).naxis) ax = [1,1] - dax = [X(),Y()] - minx = first(axes(imgv,2)) - maxx = last(axes(imgv,2)) - miny = first(axes(imgv,1)) - maxy = last(axes(imgv,1)) + minx = first(dims(imgv,2)) + maxx = last(dims(imgv,2)) + miny = first(dims(imgv,1)) + maxy = last(dims(imgv,1)) extent = (minx, maxx, miny, maxy) - wcsg = WCSGrid(wcs(imgv), extent, ax, coords) - gridspec = wcsgridspec(wcsg) + @show extent + + # wcsg = WCSGrid(wcs(imgv), extent, ax, coords) + # gridspec = wcsgridspec(wcsg) - xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), ax[1], gridspec.tickpos1w)) - xguide --> ctype_label(wcs(imgv).ctype[ax[1]], wcs(imgv).radesys) + # xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), ax[1], gridspec.tickpos1w)) + # xguide --> ctype_label(wcs(imgv).ctype[ax[1]], wcs(imgv).radesys) - yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), ax[2], gridspec.tickpos2w)) - yguide --> ctype_label(wcs(imgv).ctype[ax[2]], wcs(imgv).radesys) + # yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), ax[2], gridspec.tickpos2w)) + # yguide --> ctype_label(wcs(imgv).ctype[ax[2]], wcs(imgv).radesys) # To ensure the physical axis tick labels are correct the axes must be # tight to the image @@ -144,20 +143,31 @@ save("output.png", v) # axes(imgv,2) .- 0.5, axes(imgv,1) .- 0.5, # @show size(view(arraydata(imgv), reverse(axes(imgv,1)),:)) view(arraydata(imgv), reverse(axes(imgv,1)),:) + + # imgv = permutedims(imgv, DimensionalData.commondims(>:, (DimensionalData.ZDim, DimensionalData.YDim, DimensionalData.XDim, DimensionalData.TimeDim, DimensionalData.Dimension, DimensionalData.Dimension), dims(imgv))) + # y, x = dims(imgv) + # :xguide --> DimensionalData.label(x) + # :yguide --> DimensionalData.label(y) + # :zguide --> DimensionalData.label(imgv) + # :colorbar_title --> DimensionalData.label(imgv) + # DimensionalData._xticks!(plotattributes, s, x) + # DimensionalData._yticks!(plotattributes, s, y) + # DimensionalData._withaxes(x, y, imgv) + # arraydata(imgv) end - # If wcs=true (default) and grid=true (not default), overplot a WCS - # grid. - if (!haskey(plotattributes, :wcs) || plotattributes[:wcs]) && - haskey(plotattributes, :xgrid) && plotattributes[:xgrid] && - haskey(plotattributes, :ygrid) && plotattributes[:ygrid] - - # Plot the WCSGrid as a second series (actually just lines) - @series begin - wcsg, gridspec - end - end - return + # # If wcs=true (default) and grid=true (not default), overplot a WCS + # # grid. + # if (!haskey(plotattributes, :wcs) || plotattributes[:wcs]) && + # haskey(plotattributes, :xgrid) && plotattributes[:xgrid] && + # haskey(plotattributes, :ygrid) && plotattributes[:ygrid] + + # # Plot the WCSGrid as a second series (actually just lines) + # @series begin + # wcsg, gridspec + # end + # end + # return end From 399847736eda265caefcf4f89d53dc9904621ea9 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 20 Mar 2022 14:45:17 -0700 Subject: [PATCH 061/178] WIP on world_to_pix --- src/AstroImages.jl | 2 +- src/wcs.jl | 54 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 15e5abdd..c8633795 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -104,7 +104,7 @@ const dimnames = ( ) # Export WCS coordinate conversion functions -export pix_to_world, pix_to_world! +export pix_to_world, pix_to_world!, world_to_pix, world!_to_pix # Accessors """ diff --git a/src/wcs.jl b/src/wcs.jl index 1ba396b6..01a3b790 100644 --- a/src/wcs.jl +++ b/src/wcs.jl @@ -354,7 +354,7 @@ function WCS.pix_to_world(img::AstroImage, pixcoords::NTuple{N,DimensionalData.D return WCS.pix_to_world!(out, img, pixcoords_prepared) end WCS.pix_to_world(img::AstroImage, pixcoords::DimensionalData.Dimension...) = WCS.pix_to_world(img, pixcoords) -function WCS.pix_to_world!(world_coords_out, img::AstroImage, pixcoords) +function WCS.pix_to_world!(worldcoords_out, img::AstroImage, pixcoords) # Find the coordinates in the parent array. # Dimensional data pixcoords_floored = floor.(Int, pixcoords) @@ -380,8 +380,56 @@ function WCS.pix_to_world!(world_coords_out, img::AstroImage, pixcoords) end parentcoords_prepared[j] = dim[1] end - @show parentcoords_prepared + return WCS.pix_to_world!(wcs(img), parentcoords_prepared, worldcoords_out) +end + + +## +function WCS.world_to_pix(img::AstroImage, worldcoords) + if worldcoords isa Array{Float64} + worldcoords_prepared = worldcoords + else + worldcoords_prepared = [Float64(c) for c in worldcoords] + end + D_out = length(dims(img))+length(refdims(img)) + if ndims(worldcoords_prepared) > 1 + out = similar(worldcoords_prepared, Float64, D_out, size(worldcoords_prepared,2)) + else + out = similar(worldcoords_prepared, Float64, D_out) + end + return WCS.world_to_pix!(out, img, worldcoords_prepared) +end +function WCS.world_to_pix!(pixcoords_out, img::AstroImage, worldcoords) + # # Find the coordinates in the parent array. + # # Dimensional data + # worldcoords_floored = floor.(Int, worldcoords) + # worldcoords_frac = (worldcoords .- worldcoords_floored) .* step.(dims(img)) + # parentcoords = getindex.(dims(img), worldcoords_floored) .+ worldcoords_frac + # WCS.jl is very restrictive. We need to supply a Vector{Float64} + # as input, not any other kind of collection. + # TODO: avoid allocation in case where refdims=() and worldcoords isa Array{Float64} + worldcoords_prepared = zeros(length(dims(img))+length(refdims(img))) + + # TODO: we need to pass in ref dims locations as well, and then filter the + # output to only include the dims of the current slice? + # out = zeros(Float64, length(dims(img))+length(refdims(img)), size(worldcoords,2)) + for (i, dim) in enumerate(dims(img)) + j = findfirst(dimnames) do dim_candidate + name(dim_candidate) == name(dim) + end + worldcoords_prepared[j] = worldcoords[i] + end + for dim in refdims(img) + j = findfirst(dimnames) do dim_candidate + name(dim_candidate) == name(dim) + end + worldcoords_prepared[j] = dim[1] + end + + # This returns the parent pixel coordinates. + WCS.world_to_pix!(wcs(img), worldcoords_prepared, pixcoords_out) - return WCS.pix_to_world!(wcs(img), parentcoords_prepared, world_coords_out) + pixcoords_out .-= first.(dims(img)) + pixcoords_out .= pixcoords_out ./ step.(dims(img)) .+ 1 end \ No newline at end of file From 32f0a12aa0bc0f3b85ff171e71146e6c7ebb057d Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 20 Mar 2022 14:45:49 -0700 Subject: [PATCH 062/178] WIP move to implot recipe --- src/plot-recipes.jl | 319 ++++++++++++++++++++++++-------------------- 1 file changed, 173 insertions(+), 146 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index ea2757ea..8b594beb 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -3,68 +3,21 @@ using AstroAngles using Printf using PlotUtils: optimize_ticks -""" - plot(img::AstroImageMat; clims=extrema, stretch=identity, cmap=nothing) - -Create a read only view of an array or AstroImageMat mapping its data values -to Colors according to `clims`, `stretch`, and `cmap`. - -The data is first clamped to `clims`, which can either be a tuple of (min, max) -values or a function accepting an iterator of pixel values that returns (min, max). -By default, `clims=extrema` i.e. the minimum and maximum of `img`. -Convenient functions to use for `clims` are: -`extrema`, `zscale`, and `percent(p)` - -Next, the data is rescaled to [0,1] and remapped according to the function `stretch`. -Stretch can be any monotonic function mapping values in the range [0,1] to some range [a,b]. -Note that `log(0)` is not defined so is not directly supported. -For a list of convenient stretch functions, see: -`logstretch`, `powstretch`, `squarestretch`, `asinhstretch`, `sinhstretch`, `powerdiststretch` - -Finally the data is mapped to RGB values according to `cmap`. If cmap is `nothing`, -grayscale is used. ColorSchemes.jl defines hundreds of colormaps. A few nice ones for -images include: `:viridis`, `:magma`, `:plasma`, `:thermal`, and `:turbo`. - -Crucially, this function returns a view over the underlying data. If `img` is updated -then those changes will be reflected by this view with the exception of `clims` which -is not recalculated. - -Note: if clims or stretch is a function, the pixel values passed in are first filtered -to remove non-finite or missing values. - -### Defaults -The default values of `clims`, `stretch`, and `cmap` are `extrema`, `identity`, and `nothing` -respectively. -You may alter these defaults using `AstroImages.set_clims!`, `AstroImages.set_stretch!`, and -`AstroImages.set_cmap!`. -### Automatic Display -Arrays wrapped by `AstroImageMat()` get displayed as images automatically by calling -`imview` on them with the default settings when using displays that support showing PNG images. +@userplot ImPlot +@recipe function f(h::ImPlot) + if length(h.args) != 1 || !(typeof(h.args[1]) <: AbstractArray) + error("Image plots require an arugment that is a subtype of AbstractArray. Got: $(typeof(h.args))") + end + img = only(h.args) + if !(typeof(img) <: AstroImage) + img = AstroImage(only(h.args)) + end + T = eltype(img) + if ndims(img) != 2 + error("Image passed to `implot` must be two-dimensional. Got ndims(img)=$(ndims(img))") + end -### Missing data -Pixels that are `NaN` or `missing` will be displayed as transparent when `cmap` is set -or black if. -+/- Inf will be displayed as black or white respectively. - -### Exporting Images -The view returned by `imview` can be saved using general `FileIO.save` methods. -Example: -```julia -v = imview(data, cmap=:magma, stretch=asinhstretch, clims=percent(95)) -save("output.png", v) -``` -""" -# This recipe promotes AstroImages of numerical data into full color using -# imview(). -@recipe function f( - s::DimensionalData.HeatMapLike, - img::AstroImageMat{T}; - clims=_default_clims[], - stretch=_default_stretch[], - cmap=_default_cmap[], -) where {T<:Number} - println("Hit AstroImageMat recipe") # We often plot an AstroImageMat{<:Number} which hasn't yet had # its wcs cached (wcs_stale=true) and we make an image view here. @@ -76,26 +29,48 @@ save("output.png", v) wcs(img) end + # Use package defaults if not user provided. + clims --> _default_clims[] + stretch --> _default_stretch[] + cmap --> _default_cmap[] + # We currently use the AstroImages defaults. If unset, we could # instead follow the plot theme. - imgv = imview(img; clims, stretch, cmap) + if T <: Colorant + imgv = img + else + clims = plotattributes[:clims] + stretch = plotattributes[:stretch] + cmap = plotattributes[:cmap] + imgv = imview(img; clims, stretch, cmap) + end xgrid --> true ygrid --> true # By default, disable the colorbar. # Plots.jl does no give us sufficient control to make sure the range and ticks - # are correct after applying a non-linear stretch - # colorbar := false + # are correct after applying a non-linear stretch. + colorbar := false + # We may be able to make our own colorbar in future using a second image plot + # off to the side using something like: + # if typeof(clims) <: AbstractArray || typeof(clims) <: Tuple + # if length(clims) != 2 + # error("clims must have exactly two values if provided.") + # end + # imgmin = first(clims) + # imgmax = last(clims) + # # Or as a callable that computes them given an iterator + # else + # imgmin, imgmax = clims(skipmissingnan(img)) + # end + # imview(repeat(range(imgmin, imgmax,length=100), 1,10)'; clims=(imgmin,imgmax), stretch, cmap) # we have a wcs flag (from the image by default) so that users can skip over # plotting in physical coordinates. This is especially important # if the WCS headers are mallformed in some way. if !haskey(plotattributes, :wcs) || plotattributes[:wcs] - # TODO: fill out coordinates array considering offset indices and slices - # out of cubes (tricky!) - # Note: if the axes are on unusual sides (e.g. y-axis at right, x-axis at top) # then these coordinates are not correct. They are only correct exactly # along the axis. @@ -137,7 +112,6 @@ save("output.png", v) yflip := false xflip := false - println("In plot recipe") @series begin # axes(imgv,2), axes(imgv,1), view(arraydata(imgv), reverse(axes(imgv,1)),:) # axes(imgv,2) .- 0.5, axes(imgv,1) .- 0.5, @@ -171,95 +145,148 @@ save("output.png", v) end +""" + implot(img::AstroImageMat; clims=extrema, stretch=identity, cmap=nothing) -@recipe function f( - img::AstroImageVec{T}; -) where {T<:Number} - - # We don't to override e.g. histograms - if haskey(plotattributes, :seriestype) - return arraydata(img) - - else +Create a read only view of an array or AstroImageMat mapping its data values +to Colors according to `clims`, `stretch`, and `cmap`. - # we have a wcs flag (from the image by default) so that users can skip over - # plotting in physical coordinates. This is especially important - # if the WCS headers are mallformed in some way. - if !haskey(plotattributes, :wcs) || plotattributes[:wcs] - - # Note: if the axes are on unusual sides (e.g. y-axis at right, x-axis at top) - # then these coordinates are not correct. They are only correct exactly - # along the axis. - - # ax = haskey(plotattributes, :axes) ? plotattributes[:axes] : (1,2) - # coords = haskey(plotattributes, :coords) ? plotattributes[:coords] : ones(wcs(img).naxis) - ax = findall(==(:), getfield(img, :wcs_axes)) - j = 0 - coords = map(getfield(img, :wcs_axes)) do coord - j += 1 - if coord === (:) - first(axes(img,j)) - else - coord - end - end +The data is first clamped to `clims`, which can either be a tuple of (min, max) +values or a function accepting an iterator of pixel values that returns (min, max). +By default, `clims=extrema` i.e. the minimum and maximum of `img`. +Convenient functions to use for `clims` are: +`extrema`, `zscale`, and `percent(p)` - l = ctype_label(wcs(img).ctype[only(ax)], wcs(img).radesys) - xguide --> l +Next, the data is rescaled to [0,1] and remapped according to the function `stretch`. +Stretch can be any monotonic function mapping values in the range [0,1] to some range [a,b]. +Note that `log(0)` is not defined so is not directly supported. +For a list of convenient stretch functions, see: +`logstretch`, `powstretch`, `squarestretch`, `asinhstretch`, `sinhstretch`, `powerdiststretch` - # minx = first(axes(imgv,ax[2])) - # maxx = last(axes(imgv,ax[2])) - # miny = first(axes(imgv,ax[1])) - # maxy = last(axes(imgv,ax[1])) - # extent = (minx, maxx, miny, maxy) +Finally the data is mapped to RGB values according to `cmap`. If cmap is `nothing`, +grayscale is used. ColorSchemes.jl defines hundreds of colormaps. A few nice ones for +images include: `:viridis`, `:magma`, `:plasma`, `:thermal`, and `:turbo`. - # wcsg = WCSGrid(wcs(imgv), extent, ax, coords) - # gridspec = wcsgridspec(wcsg) - - # xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), 1, gridspec.tickpos1w)) - # xguide --> ctype_label(wcs(imgv).ctype[1], wcs(imgv).radesys) - - # yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), 2, gridspec.tickpos2w)) - # yguide --> ctype_label(wcs(imgv).ctype[2], wcs(imgv).radesys) - - # # To ensure the physical axis tick labels are correct the axes must be - # # tight to the image - # xl = first(axes(imgv,2)), last(axes(imgv,2)) - # yl = first(axes(imgv,1)), last(axes(imgv,1)) - # ylims --> yl - # xlims --> xl - end +Crucially, this function returns a view over the underlying data. If `img` is updated +then those changes will be reflected by this view with the exception of `clims` which +is not recalculated. - # # Disable equal aspect ratios if the scales are totally different - # if max(size(imgv)...)/min(size(imgv)...) >= 7 - # aspect_ratio --> :none - # end +Note: if clims or stretch is a function, the pixel values passed in are first filtered +to remove non-finite or missing values. - # # We have to do a lot of flipping to keep the orientation corect - # yflip := false - # xflip := false +### Defaults +The default values of `clims`, `stretch`, and `cmap` are `extrema`, `identity`, and `nothing` +respectively. +You may alter these defaults using `AstroImages.set_clims!`, `AstroImages.set_stretch!`, and +`AstroImages.set_cmap!`. - # @series begin - # axes(imgv,2), axes(imgv,1), view(arraydata(imgv), reverse(axes(imgv,1)),:) - # end +### Automatic Display +Arrays wrapped by `AstroImageMat()` get displayed as images automatically by calling +`imview` on them with the default settings when using displays that support showing PNG images. - # If wcs=true (default) and grid=true (not default), overplot a WCS - # grid. - # if (!haskey(plotattributes, :wcs) || plotattributes[:wcs]) && - # haskey(plotattributes, :xgrid) && plotattributes[:xgrid] && - # haskey(plotattributes, :ygrid) && plotattributes[:ygrid] +### Missing data +Pixels that are `NaN` or `missing` will be displayed as transparent when `cmap` is set +or black if. ++/- Inf will be displayed as black or white respectively. - # # Plot the WCSGrid as a second series (actually just lines) - # @series begin - # wcsg, gridspec - # end - # end - @series begin - arraydata(img) - end - return - end -end +### Exporting Images +The view returned by `imview` can be saved using general `FileIO.save` methods. +Example: +```julia +v = imview(data, cmap=:magma, stretch=asinhstretch, clims=percent(95)) +save("output.png", v) +``` +""" +implot + +# @recipe function f( +# img::AstroImageVec{T}; +# ) where {T<:Number} + +# # We don't to override e.g. histograms +# if haskey(plotattributes, :seriestype) +# return arraydata(img) + +# else + +# # we have a wcs flag (from the image by default) so that users can skip over +# # plotting in physical coordinates. This is especially important +# # if the WCS headers are mallformed in some way. +# if !haskey(plotattributes, :wcs) || plotattributes[:wcs] + +# # Note: if the axes are on unusual sides (e.g. y-axis at right, x-axis at top) +# # then these coordinates are not correct. They are only correct exactly +# # along the axis. + +# # ax = haskey(plotattributes, :axes) ? plotattributes[:axes] : (1,2) +# # coords = haskey(plotattributes, :coords) ? plotattributes[:coords] : ones(wcs(img).naxis) +# ax = findall(==(:), getfield(img, :wcs_axes)) +# j = 0 +# coords = map(getfield(img, :wcs_axes)) do coord +# j += 1 +# if coord === (:) +# first(axes(img,j)) +# else +# coord +# end +# end + +# l = ctype_label(wcs(img).ctype[only(ax)], wcs(img).radesys) +# xguide --> l + +# # minx = first(axes(imgv,ax[2])) +# # maxx = last(axes(imgv,ax[2])) +# # miny = first(axes(imgv,ax[1])) +# # maxy = last(axes(imgv,ax[1])) +# # extent = (minx, maxx, miny, maxy) + +# # wcsg = WCSGrid(wcs(imgv), extent, ax, coords) +# # gridspec = wcsgridspec(wcsg) + +# # xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), 1, gridspec.tickpos1w)) +# # xguide --> ctype_label(wcs(imgv).ctype[1], wcs(imgv).radesys) + +# # yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), 2, gridspec.tickpos2w)) +# # yguide --> ctype_label(wcs(imgv).ctype[2], wcs(imgv).radesys) + +# # # To ensure the physical axis tick labels are correct the axes must be +# # # tight to the image +# # xl = first(axes(imgv,2)), last(axes(imgv,2)) +# # yl = first(axes(imgv,1)), last(axes(imgv,1)) +# # ylims --> yl +# # xlims --> xl +# end + +# # # Disable equal aspect ratios if the scales are totally different +# # if max(size(imgv)...)/min(size(imgv)...) >= 7 +# # aspect_ratio --> :none +# # end + +# # # We have to do a lot of flipping to keep the orientation corect +# # yflip := false +# # xflip := false + +# # @series begin +# # axes(imgv,2), axes(imgv,1), view(arraydata(imgv), reverse(axes(imgv,1)),:) +# # end + +# # If wcs=true (default) and grid=true (not default), overplot a WCS +# # grid. +# # if (!haskey(plotattributes, :wcs) || plotattributes[:wcs]) && +# # haskey(plotattributes, :xgrid) && plotattributes[:xgrid] && +# # haskey(plotattributes, :ygrid) && plotattributes[:ygrid] + +# # # Plot the WCSGrid as a second series (actually just lines) +# # @series begin +# # wcsg, gridspec +# # end +# # end +# @series begin +# arraydata(img) +# end +# return +# end +# end struct WCSGrid From 43cc672d5931614e20df743728e69bb605061d30 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 21 Mar 2022 08:05:29 -0700 Subject: [PATCH 063/178] Fixes for WCS plotting with new DimensionalData approach --- src/plot-recipes.jl | 366 ++++++++++++++++---------------------------- src/wcs.jl | 139 ++++++++++++----- 2 files changed, 232 insertions(+), 273 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 8b594beb..fc67fddd 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -25,7 +25,7 @@ using PlotUtils: optimize_ticks # since the result is stored in the imview instead of original image. # Call wcs(img) here if we are later going to plot with wcs coordinates # to ensure this gets cached beween calls. - if !haskey(plotattributes, :wcs) || plotattributes[:wcs] + if !haskey(plotattributes, :wcsticks) || plotattributes[:wcsticks] wcs(img) end @@ -69,7 +69,7 @@ using PlotUtils: optimize_ticks # we have a wcs flag (from the image by default) so that users can skip over # plotting in physical coordinates. This is especially important # if the WCS headers are mallformed in some way. - if !haskey(plotattributes, :wcs) || plotattributes[:wcs] + if !haskey(plotattributes, :wcsticks) || plotattributes[:wcsticks] # Note: if the axes are on unusual sides (e.g. y-axis at right, x-axis at top) # then these coordinates are not correct. They are only correct exactly @@ -77,28 +77,26 @@ using PlotUtils: optimize_ticks # In astropy, the ticks are actually tilted to reflect this, though in general # the transformation from pixel to coordinates can be non-linear and curved. - ax = [1,1] - minx = first(dims(imgv,2)) - maxx = last(dims(imgv,2)) - miny = first(dims(imgv,1)) - maxy = last(dims(imgv,1)) - extent = (minx, maxx, miny, maxy) + minx = first(axes(imgv,2)) + maxx = last(axes(imgv,2)) + miny = first(axes(imgv,1)) + maxy = last(axes(imgv,1)) + extent = (minx-0.5, maxx+0.5, miny-0.5, maxy+0.5) - @show extent - # wcsg = WCSGrid(wcs(imgv), extent, ax, coords) - # gridspec = wcsgridspec(wcsg) + wcsg = WCSGrid(imgv, extent) + gridspec = wcsgridspec(wcsg) - # xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), ax[1], gridspec.tickpos1w)) - # xguide --> ctype_label(wcs(imgv).ctype[ax[1]], wcs(imgv).radesys) + xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), dimindex(img,1), gridspec.tickpos1w)) + xguide --> ctype_label(wcs(imgv).ctype[dimindex(img,1)], wcs(imgv).radesys) - # yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), ax[2], gridspec.tickpos2w)) - # yguide --> ctype_label(wcs(imgv).ctype[ax[2]], wcs(imgv).radesys) + yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), dimindex(img,2), gridspec.tickpos2w)) + yguide --> ctype_label(wcs(imgv).ctype[dimindex(img,2)], wcs(imgv).radesys) # To ensure the physical axis tick labels are correct the axes must be # tight to the image - xl = first(axes(imgv,2)), last(axes(imgv,2)) - yl = first(axes(imgv,1)), last(axes(imgv,1)) + xl = first(axes(imgv,2))-0.5, last(axes(imgv,2))+0.5 + yl = first(axes(imgv,1))-0.5, last(axes(imgv,1))+0.5 ylims --> yl xlims --> xl end @@ -113,9 +111,6 @@ using PlotUtils: optimize_ticks xflip := false @series begin - # axes(imgv,2), axes(imgv,1), view(arraydata(imgv), reverse(axes(imgv,1)),:) - # axes(imgv,2) .- 0.5, axes(imgv,1) .- 0.5, - # @show size(view(arraydata(imgv), reverse(axes(imgv,1)),:)) view(arraydata(imgv), reverse(axes(imgv,1)),:) # imgv = permutedims(imgv, DimensionalData.commondims(>:, (DimensionalData.ZDim, DimensionalData.YDim, DimensionalData.XDim, DimensionalData.TimeDim, DimensionalData.Dimension, DimensionalData.Dimension), dims(imgv))) @@ -130,18 +125,18 @@ using PlotUtils: optimize_ticks # arraydata(imgv) end - # # If wcs=true (default) and grid=true (not default), overplot a WCS - # # grid. - # if (!haskey(plotattributes, :wcs) || plotattributes[:wcs]) && - # haskey(plotattributes, :xgrid) && plotattributes[:xgrid] && - # haskey(plotattributes, :ygrid) && plotattributes[:ygrid] - - # # Plot the WCSGrid as a second series (actually just lines) - # @series begin - # wcsg, gridspec - # end - # end - # return + # If wcs=true (default) and grid=true (not default), overplot a WCS + # grid. + if (!haskey(plotattributes, :wcsticks) || plotattributes[:wcsticks]) && + haskey(plotattributes, :xgrid) && plotattributes[:xgrid] && + haskey(plotattributes, :ygrid) && plotattributes[:ygrid] + + # Plot the WCSGrid as a second series (actually just lines) + @series begin + wcsg, gridspec + end + end + return end @@ -199,103 +194,10 @@ save("output.png", v) """ implot -# @recipe function f( -# img::AstroImageVec{T}; -# ) where {T<:Number} - -# # We don't to override e.g. histograms -# if haskey(plotattributes, :seriestype) -# return arraydata(img) - -# else - -# # we have a wcs flag (from the image by default) so that users can skip over -# # plotting in physical coordinates. This is especially important -# # if the WCS headers are mallformed in some way. -# if !haskey(plotattributes, :wcs) || plotattributes[:wcs] - -# # Note: if the axes are on unusual sides (e.g. y-axis at right, x-axis at top) -# # then these coordinates are not correct. They are only correct exactly -# # along the axis. - -# # ax = haskey(plotattributes, :axes) ? plotattributes[:axes] : (1,2) -# # coords = haskey(plotattributes, :coords) ? plotattributes[:coords] : ones(wcs(img).naxis) -# ax = findall(==(:), getfield(img, :wcs_axes)) -# j = 0 -# coords = map(getfield(img, :wcs_axes)) do coord -# j += 1 -# if coord === (:) -# first(axes(img,j)) -# else -# coord -# end -# end - -# l = ctype_label(wcs(img).ctype[only(ax)], wcs(img).radesys) -# xguide --> l - -# # minx = first(axes(imgv,ax[2])) -# # maxx = last(axes(imgv,ax[2])) -# # miny = first(axes(imgv,ax[1])) -# # maxy = last(axes(imgv,ax[1])) -# # extent = (minx, maxx, miny, maxy) - -# # wcsg = WCSGrid(wcs(imgv), extent, ax, coords) -# # gridspec = wcsgridspec(wcsg) - -# # xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), 1, gridspec.tickpos1w)) -# # xguide --> ctype_label(wcs(imgv).ctype[1], wcs(imgv).radesys) - -# # yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), 2, gridspec.tickpos2w)) -# # yguide --> ctype_label(wcs(imgv).ctype[2], wcs(imgv).radesys) - -# # # To ensure the physical axis tick labels are correct the axes must be -# # # tight to the image -# # xl = first(axes(imgv,2)), last(axes(imgv,2)) -# # yl = first(axes(imgv,1)), last(axes(imgv,1)) -# # ylims --> yl -# # xlims --> xl -# end - -# # # Disable equal aspect ratios if the scales are totally different -# # if max(size(imgv)...)/min(size(imgv)...) >= 7 -# # aspect_ratio --> :none -# # end - -# # # We have to do a lot of flipping to keep the orientation corect -# # yflip := false -# # xflip := false - -# # @series begin -# # axes(imgv,2), axes(imgv,1), view(arraydata(imgv), reverse(axes(imgv,1)),:) -# # end - -# # If wcs=true (default) and grid=true (not default), overplot a WCS -# # grid. -# # if (!haskey(plotattributes, :wcs) || plotattributes[:wcs]) && -# # haskey(plotattributes, :xgrid) && plotattributes[:xgrid] && -# # haskey(plotattributes, :ygrid) && plotattributes[:ygrid] - -# # # Plot the WCSGrid as a second series (actually just lines) -# # @series begin -# # wcsg, gridspec -# # end -# # end -# @series begin -# arraydata(img) -# end -# return -# end -# end - - struct WCSGrid - w - extent - ax - coords + img::AstroImage + extent::NTuple{4,Float64} end -WCSGrid(w,extent,ax) = WCSGrid(w,extent,ax,ones(length(ax))) """ @@ -307,8 +209,7 @@ Returns a vector of pixel positions and a vector of strings. Example: plot(img, xticks=wcsticks(img, 1), yticks=wcsticks(img, 2)) """ -function wcsticks(img::AstroImageMat, axnum) - gs = wcsgridspec(WCSGrid(img)) +function wcsticks(img::AstroImageMat, axnum, gs = wcsgridspec(WCSGrid(img))) tickposx = axnum == 1 ? gs.tickpos1x : gs.tickpos2x tickposw = axnum == 1 ? gs.tickpos1w : gs.tickpos2w return tickposx, wcslabels( @@ -447,15 +348,16 @@ This function has to work on both plotted axes at once to handle rotation and ge curvature of the WCS grid projected on the image coordinates. """ -function WCSGrid(img::AstroImageMat, ax=(1,2), coords=ones(wcs(img).naxis)) - - minx = first(axes(img,ax[1])) - maxx = last(axes(img,ax[1])) - miny = first(axes(img,ax[2])) - maxy = last(axes(img,ax[2])) - extent = (minx, maxx, miny, maxy) - - return WCSGrid(wcs(img), extent, ax, coords) +function WCSGrid(img::AstroImageMat) + minx = first(axes(img,1)) + maxx = last(axes(img,1)) + miny = first(axes(img,2)) + maxy = last(axes(img,2)) + # extent = (minx, maxx, miny, maxy) + extent = (minx-0.5, maxx+0.5, miny-0.5, maxy+0.5) + # extent = (minx-2, maxx+2, miny-2, maxy+2) + + return WCSGrid(img, extent) end @@ -481,8 +383,8 @@ end end annotate = haskey(plotattributes, :gridlabels) && plotattributes[:gridlabels] - xguide --> ctype_label(wcsg.w.ctype[wcsg.ax[1]], wcsg.w.radesys) - yguide --> ctype_label(wcsg.w.ctype[wcsg.ax[2]], wcsg.w.radesys) + xguide --> ctype_label(wcs(wcsg.img).ctype[dimindex(wcsg.img,1)], wcs(wcsg.img).radesys) + yguide --> ctype_label(wcs(wcsg.img).ctype[dimindex(wcsg.img,2)], wcs(wcsg.img).radesys) xlims --> wcsg.extent[1], wcsg.extent[2] ylims --> wcsg.extent[3], wcsg.extent[4] @@ -490,8 +392,8 @@ end grid := false tickdirection := :none - xticks --> wcsticks(wcsg, 1, gridspec) - yticks --> wcsticks(wcsg, 2, gridspec) + xticks --> wcsticks(wcsg.img, 1, gridspec) + yticks --> wcsticks(wcsg.img, 2, gridspec) @series xs, ys @@ -501,7 +403,7 @@ end @series begin # TODO: why is this reverse necessary? rotations = reverse(rad2deg.(gridspec.annotations1θ)) - ticklabels = wcslabels(wcsg.w, 1, gridspec.annotations1w) + ticklabels = wcslabels(wcs(wcsg.img), 1, gridspec.annotations1w) seriestype := :line linewidth := 0 # TODO: we need to use requires to load in Plots for the necessary text control. Future versions of RecipesBase might fix this. @@ -513,7 +415,7 @@ end end @series begin rotations = rad2deg.(gridspec.annotations2θ) - ticklabels = wcslabels(wcsg.w, 2, gridspec.annotations2w) + ticklabels = wcslabels(wcs(wcsg.img), 2, gridspec.annotations2w) seriestype := :line linewidth := 0 series_annotations := [ @@ -542,20 +444,15 @@ function wcsgridspec(wsg::WCSGrid) # the grid. # x and y denote pixel coordinates (along `ax`), u and v are world coordinates roughly along same. - ax = collect(wsg.ax) - coordsx = convert(Vector{Float64}, collect(wsg.coords)) minx, maxx, miny, maxy = wsg.extent - # @show wsg.extent # Find the extent of this slice in world coordinates - posxy = repeat(coordsx, 1, 4) - posxy[ax,1] .= (minx,miny) - posxy[ax,2] .= (minx,maxy) - posxy[ax,3] .= (maxx,miny) - posxy[ax,4] .= (maxx,maxy) - posuv = pix_to_world(wsg.w, posxy) - (minu, maxu), (minv, maxv) = extrema(posuv, dims=2)[[ax[1],ax[2]],:] - + posxy = [ + minx minx maxx maxx + miny maxy miny maxy + ] + posuv = pix_to_world(wsg.img, posxy) + (minu, maxu), (minv, maxv) = extrema(posuv, dims=2) # In general, grid can be curved when plotted back against the image, # so we will need to sample multiple points along the grid. @@ -579,7 +476,8 @@ function wcsgridspec(wsg::WCSGrid) # If we don't get enough valid tick marks (at least 2) loop again # requesting more locations up to three times. local tickposv - j = 3 + # j = 3 + j = 1 while length(tickpos2x) < 2 && j > 0 k_min += 2 k_ideal += 2 @@ -587,8 +485,6 @@ function wcsgridspec(wsg::WCSGrid) j -= 1 tickposv = optimize_ticks(6minv, 6maxv; Q, k_min, k_ideal, k_max)[1]./6 - # tickposv = [10:60:360;] - # tickposv = [-13.834999999999999, -13.83, -13.825000000000001, -13.82, -13.815, -13.81] empty!(tickpos2x) empty!(tickpos2w) @@ -596,9 +492,9 @@ function wcsgridspec(wsg::WCSGrid) for tickv in tickposv # Make sure we handle unplotted slices correctly. griduv = repeat(posuv[:,1], 1, N_points) - griduv[ax[1],:] .= urange - griduv[ax[2],:] .= tickv - posxy = world_to_pix(wsg.w, griduv) + griduv[1,:] .= urange + griduv[2,:] .= tickv + posxy = world_to_pix(wsg.img, griduv) # Now that we have the grid in pixel coordinates, # if we find out where the grid intersects the axes we can put @@ -606,49 +502,49 @@ function wcsgridspec(wsg::WCSGrid) # We can use these masks to determine where, and in what direction # the gridlines leave the plot extent - in_horz_ax = minx .<= posxy[ax[1],:] .<= maxx - in_vert_ax = miny .<= posxy[ax[2],:] .<= maxy + in_horz_ax = minx .<= posxy[1,:] .<= maxx + in_vert_ax = miny .<= posxy[2,:] .<= maxy in_axes = in_horz_ax .& in_vert_ax if count(in_axes) < 2 continue elseif all(in_axes) point_entered = [ - posxy[ax[1],begin] - posxy[ax[2],begin] + posxy[1,begin] + posxy[2,begin] ] point_exitted = [ - posxy[ax[1],end] - posxy[ax[2],end] + posxy[1,end] + posxy[2,end] ] - elseif allequal(posxy[ax[1],findfirst(in_axes):findlast(in_axes)]) + elseif allequal(posxy[1,findfirst(in_axes):findlast(in_axes)]) point_entered = [ - posxy[ax[1],max(begin,findfirst(in_axes)-1)] - # posxy[ax[2],max(begin,findfirst(in_axes)-1)] + posxy[1,max(begin,findfirst(in_axes)-1)] + # posxy[2,max(begin,findfirst(in_axes)-1)] miny ] point_exitted = [ - posxy[ax[1],min(end,findlast(in_axes)+1)] - # posxy[ax[2],min(end,findlast(in_axes)+1)] + posxy[1,min(end,findlast(in_axes)+1)] + # posxy[2,min(end,findlast(in_axes)+1)] maxy ] # Vertical grid lines - elseif allequal(posxy[ax[2],findfirst(in_axes):findlast(in_axes)]) + elseif allequal(posxy[2,findfirst(in_axes):findlast(in_axes)]) point_entered = [ - minx #posxy[ax[1],max(begin,findfirst(in_axes)-1)] - posxy[ax[2],max(begin,findfirst(in_axes)-1)] + minx #posxy[1,max(begin,findfirst(in_axes)-1)] + posxy[2,max(begin,findfirst(in_axes)-1)] ] point_exitted = [ - maxx #posxy[ax[1],min(end,findlast(in_axes)+1)] - posxy[ax[2],min(end,findlast(in_axes)+1)] + maxx #posxy[1,min(end,findlast(in_axes)+1)] + posxy[2,min(end,findlast(in_axes)+1)] ] else # Use the masks to pick an x,y point inside the axes and an # x,y point outside the axes. i = findfirst(in_axes) - x1 = posxy[ax[1],i] - y1 = posxy[ax[2],i] - x2 = posxy[ax[1],i+1] - y2 = posxy[ax[2],i+1] + x1 = posxy[1,i] + y1 = posxy[2,i] + x2 = posxy[1,i+1] + y2 = posxy[2,i+1] if x2-x1 ≈ 0 @warn "undef slope" end @@ -680,10 +576,10 @@ function wcsgridspec(wsg::WCSGrid) # Use the masks to pick an x,y point inside the axes and an # x,y point outside the axes. i = findlast(in_axes) - x1 = posxy[ax[1],i-1] - y1 = posxy[ax[2],i-1] - x2 = posxy[ax[1],i] - y2 = posxy[ax[2],i] + x1 = posxy[1,i-1] + y1 = posxy[2,i-1] + x2 = posxy[1,i] + y2 = posxy[2,i] if x2-x1 ≈ 0 @warn "undef slope" end @@ -720,10 +616,9 @@ function wcsgridspec(wsg::WCSGrid) push!(tickpos2x, point_exitted[2]) push!(tickpos2w, tickv) end - # @show point_entered minx maxx miny maxy - posxy_neat = [point_entered posxy[[ax[1],ax[2]],in_axes] point_exitted] + posxy_neat = [point_entered posxy[[1,2],in_axes] point_exitted] # posxy_neat = posxy # TODO: do unplotted other axes also need a fit? @@ -746,7 +641,8 @@ function wcsgridspec(wsg::WCSGrid) # If we don't get enough valid tick marks (at least 2) loop again # requesting more locations up to three times. local tickposu - j = 3 + # j = 3 + j = 1 while length(tickpos1x) < 2 && j > 0 k_min += 2 k_ideal += 2 @@ -755,18 +651,15 @@ function wcsgridspec(wsg::WCSGrid) tickposu = optimize_ticks(6minu, 6maxu; Q, k_min, k_ideal, k_max)[1]./6 - # tickposu = [274.7, 274.705, 274.71, 274.715, 274.71999999999997, 274.72499999999997, 274.72999999999996] - # tickposu = [10:60:360;] - empty!(tickpos1x) empty!(tickpos1w) empty!(gridlinesxy1) for ticku in tickposu # Make sure we handle unplotted slices correctly. griduv = repeat(posuv[:,1], 1, N_points) - griduv[ax[1],:] .= ticku - griduv[ax[2],:] .= vrange - posxy = world_to_pix(wsg.w, griduv) + griduv[1,:] .= ticku + griduv[2,:] .= vrange + posxy = world_to_pix(wsg.img, griduv) # Now that we have the grid in pixel coordinates, # if we find out where the grid intersects the axes we can put @@ -774,8 +667,8 @@ function wcsgridspec(wsg::WCSGrid) # We can use these masks to determine where, and in what direction # the gridlines leave the plot extent - in_horz_ax = minx .<= posxy[ax[1],:] .<= maxx - in_vert_ax = miny .<= posxy[ax[2],:] .<= maxy + in_horz_ax = minx .<= posxy[1,:] .<= maxx + in_vert_ax = miny .<= posxy[2,:] .<= maxy in_axes = in_horz_ax .& in_vert_ax @@ -783,43 +676,43 @@ function wcsgridspec(wsg::WCSGrid) continue elseif all(in_axes) point_entered = [ - posxy[ax[1],begin] - posxy[ax[2],begin] + posxy[1,begin] + posxy[2,begin] ] point_exitted = [ - posxy[ax[1],end] - posxy[ax[2],end] + posxy[1,end] + posxy[2,end] ] # Horizontal grid lines - elseif allequal(posxy[ax[1],findfirst(in_axes):findlast(in_axes)]) + elseif allequal(posxy[1,findfirst(in_axes):findlast(in_axes)]) point_entered = [ - posxy[ax[1],findfirst(in_axes)] + posxy[1,findfirst(in_axes)] miny ] point_exitted = [ - posxy[ax[1],findlast(in_axes)] + posxy[1,findlast(in_axes)] maxy ] - # push!(tickpos1x, posxy[ax[1],findfirst(in_axes)]) + # push!(tickpos1x, posxy[1,findfirst(in_axes)]) # push!(tickpos1w, ticku) # Vertical grid lines - elseif allequal(posxy[ax[2],findfirst(in_axes):findlast(in_axes)]) + elseif allequal(posxy[2,findfirst(in_axes):findlast(in_axes)]) point_entered = [ minx - posxy[ax[2],findfirst(in_axes)] + posxy[2,findfirst(in_axes)] ] point_exitted = [ maxx - posxy[ax[2],findfirst(in_axes)] + posxy[2,findfirst(in_axes)] ] else # Use the masks to pick an x,y point inside the axes and an # x,y point outside the axes. i = findfirst(in_axes) - x1 = posxy[ax[1],i] - y1 = posxy[ax[2],i] - x2 = posxy[ax[1],i+1] - y2 = posxy[ax[2],i+1] + x1 = posxy[1,i] + y1 = posxy[2,i] + x2 = posxy[1,i+1] + y2 = posxy[2,i+1] if x2-x1 ≈ 0 @warn "undef slope" end @@ -850,10 +743,10 @@ function wcsgridspec(wsg::WCSGrid) # Use the masks to pick an x,y point inside the axes and an # x,y point outside the axes. i = findlast(in_axes) - x1 = posxy[ax[1],i-1] - y1 = posxy[ax[2],i-1] - x2 = posxy[ax[1],i] - y2 = posxy[ax[2],i] + x1 = posxy[1,i-1] + y1 = posxy[2,i-1] + x2 = posxy[1,i] + y2 = posxy[2,i] if x2-x1 ≈ 0 @warn "undef slope" end @@ -881,15 +774,15 @@ function wcsgridspec(wsg::WCSGrid) ] end - posxy_neat = [point_entered posxy[[ax[1],ax[2]],in_axes] point_exitted] + posxy_neat = [point_entered posxy[[1,2],in_axes] point_exitted] # TODO: do unplotted other axes also need a fit? if point_entered[2] == miny - push!(tickpos1x, point_entered[ax[1]]) + push!(tickpos1x, point_entered[1]) push!(tickpos1w, ticku) end if point_exitted[2] == miny - push!(tickpos1x, point_exitted[ax[1]]) + push!(tickpos1x, point_exitted[1]) push!(tickpos1w, ticku) end @@ -900,7 +793,6 @@ function wcsgridspec(wsg::WCSGrid) push!(gridlinesxy1, gridlinexy) end end - # @show tickpos1x # Grid annotations are simpler: annotations1w = Float64[] @@ -910,24 +802,24 @@ function wcsgridspec(wsg::WCSGrid) for ticku in tickposu # Make sure we handle unplotted slices correctly. griduv = posuv[:,1] - griduv[ax[1]] = ticku - griduv[ax[2]] = mean(vrange) - posxy = world_to_pix(wsg.w, griduv) + griduv[1] = ticku + griduv[2] = mean(vrange) + posxy = world_to_pix(wsg.img, griduv) if !(minx < posxy[1] < maxx) || !(miny < posxy[2] < maxy) continue end push!(annotations1w, ticku) - push!(annotations1x, posxy[ax[1]]) - push!(annotations1y, posxy[ax[2]]) + push!(annotations1x, posxy[1]) + push!(annotations1y, posxy[2]) # Now find slope (TODO: stepsize) # griduv[ax[2]] -= 1 - griduv[ax[2]] += 0.1step(vrange) - posxy2 = world_to_pix(wsg.w, griduv) + griduv[2] += 0.1step(vrange) + posxy2 = world_to_pix(wsg.img, griduv) θ = atan( - posxy2[ax[2]] - posxy[ax[2]], - posxy2[ax[1]] - posxy[ax[1]], + posxy2[2] - posxy[2], + posxy2[1] - posxy[1], ) push!(annotations1θ, θ) end @@ -938,22 +830,22 @@ function wcsgridspec(wsg::WCSGrid) for tickv in tickposv # Make sure we handle unplotted slices correctly. griduv = posuv[:,1] - griduv[ax[1]] = mean(urange) - griduv[ax[2]] = tickv - posxy = world_to_pix(wsg.w, griduv) + griduv[1] = mean(urange) + griduv[2] = tickv + posxy = world_to_pix(wsg.img, griduv) if !(minx < posxy[1] < maxx) || !(miny < posxy[2] < maxy) continue end push!(annotations2w, tickv) - push!(annotations2x, posxy[ax[1]]) - push!(annotations2y, posxy[ax[2]]) + push!(annotations2x, posxy[1]) + push!(annotations2y, posxy[2]) - griduv[ax[1]] += 0.1step(urange) - posxy2 = world_to_pix(wsg.w, griduv) + griduv[1] += 0.1step(urange) + posxy2 = world_to_pix(wsg.img, griduv) θ = atan( - posxy2[ax[2]] - posxy[ax[2]], - posxy2[ax[1]] - posxy[ax[1]], + posxy2[2] - posxy[2], + posxy2[1] - posxy[1], ) push!(annotations2θ, θ) end diff --git a/src/wcs.jl b/src/wcs.jl index 01a3b790..7a9aa90b 100644 --- a/src/wcs.jl +++ b/src/wcs.jl @@ -300,7 +300,7 @@ end # Smart versions of pix_to_world and world_to_pix """ - pix_to_world(img::AstroImage, pixcoords) + pix_to_world(img::AstroImage, pixcoords; all=false) Given an astro image, look up the world coordinates of the pixels given by `pixcoords`. World coordinates are resolved using WCS.jl and a @@ -318,16 +318,36 @@ julia> slice = cube[10:20, 30:40, 5] Then to look up the coordinates of the pixel in the bottom left corner of `slice`, run: ```julia -julia> world_coords = pix_to_world(img, (1, 1)) -[10, 30, 5] +julia> world_coords = pix_to_world(img, [1, 1]) +[10, 30] ``` + If WCS information was present in the header of `cube`, then those coordinates would be resolved using axis 1, 2, and 3 respectively. +To include world coordinates in all axes, pass `all=true` +```julia +julia> world_coords = pix_to_world(img, [1, 1], all=true) +[10, 30, 5] +``` + !! Coordinates must be provided in the order of `dims(img)`. If you transpose an image, the order you pass the coordinates should not change. """ -function WCS.pix_to_world(img::AstroImage, pixcoords) +# function WCS.pix_to_world(img::AstroImage, pixcoords::NTuple{N,DimensionalData.Dimension}) where N +# pixcoords_prepared = zeros(Float64, length(pixcoords)) +# for dim in pixcoords +# j = findfirst(dimnames) do dim_candidate +# name(dim_candidate) == name(dim) +# end +# pixcoords_prepared[j] = dim[] +# end +# D_out = length(dims(img))+length(refdims(img)) +# out = zeros(Float64, D_out) +# return WCS.pix_to_world!(out, img, pixcoords_prepared) +# end +# WCS.pix_to_world(img::AstroImage, pixcoords::DimensionalData.Dimension...) = WCS.pix_to_world(img, pixcoords) +function WCS.pix_to_world(img::AstroImage, pixcoords; all=false) if pixcoords isa Array{Float64} pixcoords_prepared = pixcoords else @@ -335,35 +355,25 @@ function WCS.pix_to_world(img::AstroImage, pixcoords) end D_out = length(dims(img))+length(refdims(img)) if ndims(pixcoords_prepared) > 1 - out = similar(pixcoords_prepared, Float64, D_out, size(pixcoords_prepared,2)) + worldcoords_out = similar(pixcoords_prepared, Float64, D_out, size(pixcoords_prepared,2)) else - out = similar(pixcoords_prepared, Float64, D_out) + worldcoords_out = similar(pixcoords_prepared, Float64, D_out) end - return WCS.pix_to_world!(out, img, pixcoords_prepared) -end -function WCS.pix_to_world(img::AstroImage, pixcoords::NTuple{N,DimensionalData.Dimension}) where N - pixcoords_prepared = zeros(Float64, length(pixcoords)) - for dim in pixcoords - j = findfirst(dimnames) do dim_candidate - name(dim_candidate) == name(dim) - end - pixcoords_prepared[j] = dim[] - end - D_out = length(dims(img))+length(refdims(img)) - out = zeros(Float64, D_out) - return WCS.pix_to_world!(out, img, pixcoords_prepared) -end -WCS.pix_to_world(img::AstroImage, pixcoords::DimensionalData.Dimension...) = WCS.pix_to_world(img, pixcoords) -function WCS.pix_to_world!(worldcoords_out, img::AstroImage, pixcoords) + # Find the coordinates in the parent array. # Dimensional data - pixcoords_floored = floor.(Int, pixcoords) - pixcoords_frac = (pixcoords .- pixcoords_floored) .* step.(dims(img)) - parentcoords = getindex.(dims(img), pixcoords_floored) .+ pixcoords_frac + # pixcoords_floored = floor.(Int, pixcoords) + # pixcoords_frac = (pixcoords .- pixcoords_floored) .* step.(dims(img)) + # parentcoords = getindex.(dims(img), pixcoords_floored) .+ pixcoords_frac + parentcoords = pixcoords .* step.(dims(img)) .+ first.(dims(img)) # WCS.jl is very restrictive. We need to supply a Vector{Float64} # as input, not any other kind of collection. # TODO: avoid allocation in case where refdims=() and pixcoords isa Array{Float64} - parentcoords_prepared = zeros(length(dims(img))+length(refdims(img))) + if ndims(pixcoords_prepared) > 1 + parentcoords_prepared = zeros(length(dims(img))+length(refdims(img)), size(pixcoords_prepared,2)) + else + parentcoords_prepared = zeros(length(dims(img))+length(refdims(img))) + end # TODO: we need to pass in ref dims locations as well, and then filter the # output to only include the dims of the current slice? @@ -372,16 +382,38 @@ function WCS.pix_to_world!(worldcoords_out, img::AstroImage, pixcoords) j = findfirst(dimnames) do dim_candidate name(dim_candidate) == name(dim) end - parentcoords_prepared[j] = parentcoords[i] + parentcoords_prepared[j,:] .= parentcoords[i,:] end for dim in refdims(img) j = findfirst(dimnames) do dim_candidate name(dim_candidate) == name(dim) end - parentcoords_prepared[j] = dim[1] + parentcoords_prepared[j,:] .= dim[1] + end + + # Get world coordinates along all slices + WCS.pix_to_world!(wcs(img), parentcoords_prepared, worldcoords_out) + + # If user requested world coordinates in all dims, not just selected + # dims of img + if all + return worldcoords_out + end + + # Otherwise filter to only return coordinates along selected dims. + if ndims(pixcoords_prepared) > 1 + world_coords_of_these_axes = zeros(length(dims(img)), size(pixcoords_prepared,2)) + else + world_coords_of_these_axes = zeros(length(dims(img))) + end + for (i, dim) in enumerate(dims(img)) + j = findfirst(dimnames) do dim_candidate + name(dim_candidate) == name(dim) + end + world_coords_of_these_axes[i,:] .= worldcoords_out[j,:] end - return WCS.pix_to_world!(wcs(img), parentcoords_prepared, worldcoords_out) + return world_coords_of_these_axes end @@ -409,8 +441,11 @@ function WCS.world_to_pix!(pixcoords_out, img::AstroImage, worldcoords) # WCS.jl is very restrictive. We need to supply a Vector{Float64} # as input, not any other kind of collection. # TODO: avoid allocation in case where refdims=() and worldcoords isa Array{Float64} - worldcoords_prepared = zeros(length(dims(img))+length(refdims(img))) - + if ndims(worldcoords) > 1 + worldcoords_prepared = zeros(length(dims(img))+length(refdims(img)),size(worldcoords,2)) + else + worldcoords_prepared = zeros(length(dims(img))+length(refdims(img))) + end # TODO: we need to pass in ref dims locations as well, and then filter the # output to only include the dims of the current slice? # out = zeros(Float64, length(dims(img))+length(refdims(img)), size(worldcoords,2)) @@ -418,18 +453,50 @@ function WCS.world_to_pix!(pixcoords_out, img::AstroImage, worldcoords) j = findfirst(dimnames) do dim_candidate name(dim_candidate) == name(dim) end - worldcoords_prepared[j] = worldcoords[i] + worldcoords_prepared[j,:] = worldcoords[i,:] end for dim in refdims(img) j = findfirst(dimnames) do dim_candidate name(dim_candidate) == name(dim) end - worldcoords_prepared[j] = dim[1] + worldcoords_prepared[j,:] .= dim[1] end # This returns the parent pixel coordinates. - WCS.world_to_pix!(wcs(img), worldcoords_prepared, pixcoords_out) + # WCS.world_to_pix!(wcs(img), worldcoords_prepared, pixcoords_out) + pixcoords_out = WCS.world_to_pix(wcs(img), worldcoords_prepared) + + + coordoffsets = zeros(length(dims(img))+length(refdims(img))) + coordsteps = zeros(length(dims(img))+length(refdims(img))) + for (i, dim) in enumerate(dims(img)) + j = findfirst(dimnames) do dim_candidate + name(dim_candidate) == name(dim) + end + coordoffsets[j] = first(dims(img)[i]) + coordsteps[j] = step(dims(img)[i]) + end + for dim in refdims(img) + j = findfirst(dimnames) do dim_candidate + name(dim_candidate) == name(dim) + end + coordoffsets[j] = first(dim) + coordsteps[j] = step(dim) + end - pixcoords_out .-= first.(dims(img)) - pixcoords_out .= pixcoords_out ./ step.(dims(img)) .+ 1 + pixcoords_out .-= coordoffsets + pixcoords_out .= pixcoords_out ./ coordsteps .+ 1 +end + + + + +## Helpers +function dimindex(img::AstroImage, ind::Int) + return dimindex(dims(img), ind) +end +function dimindex(imgdims, ind::Int) + findfirst(dimnames) do dim_candidate + name(dim_candidate) == name(imgdims[ind]) + end end \ No newline at end of file From 886d1fdae47dadf38d54b0d0cb2777b2438c12fa Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 21 Mar 2022 08:10:47 -0700 Subject: [PATCH 064/178] Allow grid to adapt to fit enough ticks --- src/plot-recipes.jl | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index fc67fddd..139b089c 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -476,8 +476,7 @@ function wcsgridspec(wsg::WCSGrid) # If we don't get enough valid tick marks (at least 2) loop again # requesting more locations up to three times. local tickposv - # j = 3 - j = 1 + j = 3 while length(tickpos2x) < 2 && j > 0 k_min += 2 k_ideal += 2 @@ -641,8 +640,7 @@ function wcsgridspec(wsg::WCSGrid) # If we don't get enough valid tick marks (at least 2) loop again # requesting more locations up to three times. local tickposu - # j = 3 - j = 1 + j = 3 while length(tickpos1x) < 2 && j > 0 k_min += 2 k_ideal += 2 From a90b27b5d4a4372a55c0c94377533a8cea29e211 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 21 Mar 2022 08:59:18 -0700 Subject: [PATCH 065/178] Better handling of empty or all non-finite/missing images --- src/AstroImages.jl | 20 ++++---------------- src/imview.jl | 6 ++++-- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index c8633795..f178b9d1 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -386,21 +386,9 @@ struct History end # We might want getproperty for header access in future. -function Base.getproperty(img::AstroImage, ::Symbol) - error("getproperty reserved for future use.") -end - -# All data indexing is handled by DimensionalData. -# We add overloads for String and Symbol indexing to -# access the FITS header instead. -# _filter_inds(inds) = tuple(( -# typeof(ind) <: Union{AbstractRange,Colon} ? (:) : ind -# for ind in inds -# )...) -# _ranges(args) = filter(arg -> typeof(arg) <: Union{AbstractRange,Colon}, args) - -# Base.getindex(img::AstroImage{T}, inds...) where {T<:Colorant} = getindex(arraydata(img), inds...) -# Base.setindex!(img::AstroImage, v, inds...) = setindex!(arraydata(img), v, inds...) # default fallback for operations on Array +# function Base.getproperty(img::AstroImage, ::Symbol) +# error("getproperty reserved for future use.") +# end # Getting and setting comments Base.getindex(img::AstroImage, inds::AbstractString...) = getindex(header(img), inds...) # accesing header using strings @@ -536,7 +524,7 @@ function __init__() add_format(format"FITS", # See https://www.loc.gov/preservation/digital/formats/fdd/fdd000317.shtml#sign [0x53,0x49,0x4d,0x50,0x4c,0x45,0x20,0x20,0x3d,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x54], - [".fit", ".fits", ".fts", ".FIT", ".FITS", ".FTS"], + [".fit", ".fits", ".fts", ".FIT", ".FITS", ".FTS", ".fit", ".fits.gz", ".fts.gz", ".FIT.gz", ".FITS.gz", ".FTS.gz"], [:FITSIO => UUID("525bcba6-941b-5504-bd06-fd0dc1a4d2eb")], [:AstroImages => UUID("fe3fc30c-9b16-11e9-1c73-17dabf39f4ad")] ) diff --git a/src/imview.jl b/src/imview.jl index ca00189a..13b68f24 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -133,13 +133,15 @@ function imview( # TODO: catch this in `show` instead of here. isempt = isempty(img) if isempt - return + @warn "imview called with empty argument" + return fill(RGBA{N0f8}(0,0,0,0), 0,0) end # Users will occaisionally pass in data that is 0D, filled with NaN, or filled with missing. # We still need to do something reasonable in those caes. nonempty = any(x-> !ismissing(x) && isfinite(x), img) if !nonempty - return + @warn "imview called with all missing or non-finite values" + return map(px->RGBA{N0f8}(0,0,0,0), img) end # TODO: Images.jl has logic to downsize huge images before displaying them. From 2a3f28c1859d5ff0d0eae55d0bddcdb16bfd3b0d Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 21 Mar 2022 08:59:41 -0700 Subject: [PATCH 066/178] Set title using refdims --- src/plot-recipes.jl | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 139b089c..4b82c03f 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -48,6 +48,12 @@ using PlotUtils: optimize_ticks xgrid --> true ygrid --> true + # Use a default grid color that shows up across more + # color maps + if !haskey(plotattributes, :xforeground_color_grid) && !haskey(plotattributes, :yforeground_color_grid) + gridcolor --> :lightgray + end + # By default, disable the colorbar. # Plots.jl does no give us sufficient control to make sure the range and ticks # are correct after applying a non-linear stretch. @@ -110,7 +116,26 @@ using PlotUtils: optimize_ticks yflip := false xflip := false + if length(refdims(imgv)) > 0 + if !haskey(plotattributes, :wcsticks) || plotattributes[:wcsticks] + refdimslabel = join(map(refdims(imgv)) do d + # match dimension with the wcs axis number + i = findfirst(dimnames) do dim_candidate + name(dim_candidate) == name(d) + end + label = ctype_label(wcs(imgv).ctype[i], wcs(imgv).radesys) + value = pix_to_world(imgv, [1,1], all=true)[i] + unit = wcs(imgv).cunit[i] + return @sprintf("%s = %.5g %s", label, value, unit) + end) + else + refdimslabel = join(map(d->"$(name(d))= $(d[1])", refdims(imgv))) + end + title --> refdimslabel + end + @series begin + view(arraydata(imgv), reverse(axes(imgv,1)),:) # imgv = permutedims(imgv, DimensionalData.commondims(>:, (DimensionalData.ZDim, DimensionalData.YDim, DimensionalData.XDim, DimensionalData.TimeDim, DimensionalData.Dimension, DimensionalData.Dimension), dims(imgv))) @@ -476,7 +501,7 @@ function wcsgridspec(wsg::WCSGrid) # If we don't get enough valid tick marks (at least 2) loop again # requesting more locations up to three times. local tickposv - j = 3 + j = 5 while length(tickpos2x) < 2 && j > 0 k_min += 2 k_ideal += 2 @@ -640,7 +665,7 @@ function wcsgridspec(wsg::WCSGrid) # If we don't get enough valid tick marks (at least 2) loop again # requesting more locations up to three times. local tickposu - j = 3 + j = 5 while length(tickpos1x) < 2 && j > 0 k_min += 2 k_ideal += 2 From c2a3826389dba664a4fb79c73abb95429592275f Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 22 Mar 2022 07:08:49 -0700 Subject: [PATCH 067/178] Improved WCS header support --- src/AstroImages.jl | 28 +++++++++------------ src/wcs.jl | 62 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 20 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index f178b9d1..2f5b578f 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -19,9 +19,6 @@ export load, WCSGrid, ccd2rgb, composechannels, - set_brightness!, - set_contrast!, - add_label!, reset!, zscale, percent, @@ -32,11 +29,10 @@ export load, asinhstretch, sinhstretch, powerdiststretch, - clampednormedview, imview, clampednormedview, - wcsticks, - wcsgridlines, + # wcsticks, + # wcsgridlines, arraydata, header, wcs, @@ -481,15 +477,6 @@ Base.convert(::Type{AstroImage{T}}, A::AbstractArray) where {T} = AstroImage(con # TODO: share headers in View. Needs support from DimensionalData. -# "`A = find_img(As)` returns the first AstroImage among the arguments." -# find_img(bc::Base.Broadcast.Broadcasted) = find_img(bc.args) -# find_img(args::Tuple) = find_img(find_img(args[1]), Base.tail(args)) -# find_img(x) = x -# find_img(::Tuple{}) = nothing -# find_img(a::AstroImage, rest) = a -# find_img(::Any, rest) = find_img(rest) - - """ emptyheader() @@ -524,10 +511,19 @@ function __init__() add_format(format"FITS", # See https://www.loc.gov/preservation/digital/formats/fdd/fdd000317.shtml#sign [0x53,0x49,0x4d,0x50,0x4c,0x45,0x20,0x20,0x3d,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x54], - [".fit", ".fits", ".fts", ".FIT", ".FITS", ".FTS", ".fit", ".fits.gz", ".fts.gz", ".FIT.gz", ".FITS.gz", ".FTS.gz"], + [".fit", ".fits", ".fts", ".FIT", ".FITS", ".FTS", ".fit",], [:FITSIO => UUID("525bcba6-941b-5504-bd06-fd0dc1a4d2eb")], [:AstroImages => UUID("fe3fc30c-9b16-11e9-1c73-17dabf39f4ad")] ) + # TODO: How to add FileIO support for fits.gz files? We can open these + # with AstroImage("...fits.gz") but not load, since the .gz takes precedence. + # add_format(format"FITS.GZ", + # [0x1f, 0x8b, 0x08], + # [".fits.gz", ".fts.gz", ".FIT.gz", ".FITS.gz", ".FTS.gz"], + # [:FITSIO => UUID("525bcba6-941b-5504-bd06-fd0dc1a4d2eb")], + # [:AstroImages => UUID("fe3fc30c-9b16-11e9-1c73-17dabf39f4ad")] + # ) + end end # module diff --git a/src/wcs.jl b/src/wcs.jl index 7a9aa90b..efb62096 100644 --- a/src/wcs.jl +++ b/src/wcs.jl @@ -267,17 +267,23 @@ function wcsfromheader(img::AstroImage; relax=WCS.HDR_ALL) # We only need to stringify WCS header. This might just be 4-10 header keywords # out of thousands. local wcsout - # Load the header without ignoring rejected to get error messages + + io = IOBuffer() + serializeheader(io, header(img)) + hdrstr = String(take!(io)) + + + # Load the headers without ignoring rejected to get error messages try wcsout = WCS.from_header( - string(header(img)); + hdrstr; ignore_rejected=false, relax ) catch err # Load them again ignoring error messages wcsout = WCS.from_header( - string(header(img)); + hdrstr; ignore_rejected=true, relax ) @@ -295,7 +301,6 @@ function wcsfromheader(img::AstroImage; relax=WCS.HDR_ALL) return first(wcsout) end end -# TODO: wcsfromheader(::FITSHeader,) # Smart versions of pix_to_world and world_to_pix @@ -499,4 +504,53 @@ function dimindex(imgdims, ind::Int) findfirst(dimnames) do dim_candidate name(dim_candidate) == name(imgdims[ind]) end +end + + + + +## For now, we use a copied version of FITSIO's show method for FITSHeader. +# We have to be careful to format things in a way WCSLib will like. +# In particular, we can't put newlines after each 80 characters. +# FITSIO has to do this so users can see the header. + +# functions for displaying header values in show(io, header) +hdrval_repr(v::Bool) = v ? "T" : "F" +hdrval_repr(v::String) = @sprintf "'%-8s'" v +hdrval_repr(v::Union{AbstractFloat, Integer}) = string(v) + +function serializeheader(io, hdr::FITSHeader) + n = length(hdr) + for i=1:n + if hdr.keys[i] == "COMMENT" || hdr.keys[i] == "HISTORY" + lastc = min(72, lastindex(hdr.comments[i])) + @printf io "%s %s" hdr.keys[i] hdr.comments[i][1:lastc] + print(io, " "^(72-lastc)) + else + @printf io "%-8s" hdr.keys[i] + if hdr.values[i] === nothing + print(io, " ") + rc = 50 # remaining characters on line + elseif hdr.values[i] isa String + val = hdrval_repr(hdr.values[i]) + @printf io "= %-20s" val + rc = length(val) <= 20 ? 50 : 70 - length(val) + else + val = hdrval_repr(hdr.values[i]) + @printf io "= %20s" val + rc = length(val) <= 20 ? 50 : 70 - length(val) + end + if length(hdr.comments[i]) > 0 + lastc = min(rc-3, lastindex(hdr.comments[i])) + @printf io " / %s" hdr.comments[i][1:lastc] + rc -= lastc + 3 + end + print(io, " "^rc) + end + if i == n + print(io, "\nEND"*(" "^77)) + else + print(io) + end + end end \ No newline at end of file From ddaaf064920917e2774a832358838b424449aef0 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 22 Mar 2022 07:21:29 -0700 Subject: [PATCH 068/178] Better handling of empty HDUs --- src/AstroImages.jl | 34 ++++++++++++---------------------- src/imview.jl | 3 +-- 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 2f5b578f..def03659 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -334,29 +334,18 @@ function fileio_load(f::File{format"FITS"}, exts::Colon) where N end end end -_loadhdu(hdu::FITSIO.ImageHDU) = AstroImage(hdu) -_loadhdu(hdu::FITSIO.TableHDU) = Tables.columntable(hdu) -export load, save - -# function fileio_load(f::File{format"FITS"}, ext::NTuple{N,Int}) where {N} -# fits = FITS(f.filename) -# out = _load(fits, ext) -# header = _header(fits, ext) -# close(fits) -# return out, header -# end - -# function fileio_load(f::NTuple{N, String}) where {N} -# fits = ntuple(i-> FITS(f[i]), N) -# ext = indexer(fits) -# out = _load(fits, ext) -# header = _header(fits, ext) -# for i in 1:N -# close(fits[i]) -# end -# return out, header -# end +_loadhdu(hdu::FITSIO.TableHDU) = Tables.columntable(hdu) +function _loadhdu(hdu::FITSIO.ImageHDU) + if size(hdu) != () + return AstroImage(hdu) + else + # Sometimes files have an empty data HDU that shows up as an image HDU but has headers. + # Fallback to creating an empty AstroImage with those headers. + emptydata = fill(0, (0,0)) + return AstroImage(emptydata, (), (), read_header(hdu), Ref(emptywcs(emptydata)), Ref(false)) + end +end function indexer(fits::FITS) ext = 0 for (i, hdu) in enumerate(fits) @@ -373,6 +362,7 @@ function indexer(fits::FITS) return ext end indexer(fits::NTuple{N, FITS}) where {N} = ntuple(i -> indexer(fits[i]), N) +export load, save diff --git a/src/imview.jl b/src/imview.jl index 13b68f24..2de333aa 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -130,11 +130,10 @@ function imview( cmap=_default_cmap[], ) where {T} - # TODO: catch this in `show` instead of here. isempt = isempty(img) if isempt @warn "imview called with empty argument" - return fill(RGBA{N0f8}(0,0,0,0), 0,0) + return fill(RGBA{N0f8}(0,0,0,0), 1,1) end # Users will occaisionally pass in data that is 0D, filled with NaN, or filled with missing. # We still need to do something reasonable in those caes. From 8ec98bbfb1e9952344b95d8043f51716ccffbd8a Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 22 Mar 2022 07:33:28 -0700 Subject: [PATCH 069/178] Cleanup imports --- src/AstroImages.jl | 8 +++++++- src/plot-recipes.jl | 8 -------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index def03659..3db7778c 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -2,7 +2,7 @@ module AstroImages using FITSIO using FileIO -using Images +using Images # TODO: maybe this can be ImagesCore using Interact using Reproject using WCS @@ -12,6 +12,12 @@ using ColorSchemes using PlotUtils: zscale using DimensionalData using Tables +using RecipesBase +using AstroAngles +using Printf +using PlotUtils: optimize_ticks + + export load, save, diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 4b82c03f..2e59b97a 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -1,9 +1,3 @@ -using RecipesBase -using AstroAngles -using Printf -using PlotUtils: optimize_ticks - - @userplot ImPlot @recipe function f(h::ImPlot) if length(h.args) != 1 || !(typeof(h.args[1]) <: AbstractArray) @@ -34,8 +28,6 @@ using PlotUtils: optimize_ticks stretch --> _default_stretch[] cmap --> _default_cmap[] - # We currently use the AstroImages defaults. If unset, we could - # instead follow the plot theme. if T <: Colorant imgv = img else From 44441f2c93ed5ea4ce35a9eab1b0ee823f84e3de Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 22 Mar 2022 08:45:29 -0700 Subject: [PATCH 070/178] Support for saving AstroImages, arrays, and tables to FITS. --- src/AstroImages.jl | 58 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 3db7778c..bc2e2ffd 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -237,10 +237,10 @@ end """ - AstroImage([color=Gray,] data::Matrix{Real}) - AstroImage(color::Type{<:Color}, data::NTuple{N, Matrix{T}}) where {T<:Real, N} + AstroImage(img::AstroImage) -Construct an `AstroImage` object of `data`, using `color` as color map, `Gray` by default. +Returns its argument. Useful to ensure an argument is converted to an +AstroImage before proceeding. """ AstroImage(img::AstroImage) = img @@ -316,6 +316,8 @@ returned as AstroImage, and TableHDUs are returned as column tables. Read and return the data from the HDUs given by `exts`. ImageHDUs are returned as AstroImage, and TableHDUs are returned as column tables. + +!! Currently comments on TableHDUs are not supported and are ignored. """ function fileio_load(f::File{format"FITS"}, ext::Union{Int,Nothing}=nothing) where N return FITS(f.filename, "r") do fits @@ -368,6 +370,41 @@ function indexer(fits::FITS) return ext end indexer(fits::NTuple{N, FITS}) where {N} = ntuple(i -> indexer(fits[i]), N) + + +# Fallback for saving arbitrary arrays +function fileio_save(f::File{format"FITS"}, args...) + FITS(f.filename, "w") do fits + for arg in args + writearg(fits, arg) + end + end +end +writearg(fits, img::AstroImage) = write(fits, arraydata(img), header=header(img)) +# Fallback for writing plain arrays +writearg(fits, arr::AbstractArray) = write(fits, arr) +# For table compatible data. +# This allows users to round trip: dat = load("abc.fits", :); write("abc", dat) +# when it contains FITS tables. +function writearg(fits, table) + if !Tables.istable(table) + error("Cannot save argument to FITS file. Value is not an AbstractArray or table.") + end + # FITSIO has fairly restrictive input types for writing tables (assertions for documentation only) + colname_strings = string.(collect(Tables.columnnames(table)))::Vector{String} + columns = collect(Tables.columns(table))::Vector + write( + fits, + colname_strings, + columns; + hdutype=TableHDU, + # TODO: In future, we want to be able to access and round-trip coments + # on table HDUs + # header=nothing + ) +end + + export load, save @@ -412,7 +449,7 @@ end # Adding new comment and history entries function Base.push!(img::AstroImage, ::Type{Comment}, history::AbstractString) hdr = header(img) - push!(hdr.keys, "HISTORY") + push!(hdr.keys, "COMMENT") push!(hdr.values, nothing) push!(hdr.comments, history) end @@ -527,20 +564,21 @@ end # module #= TODO: + + * properties? * contrast/bias? * interactive (Jupyter) * Plots & Makie recipes -* Plots: vertical/horizotat axes from m106 -* indexing -* recenter that updates indexes and CRPIX -* cubes * RGB and other composites * tests - * histogram equaization -* fileio +* FileIO Registration. +* fits.gz support +* Table wrapper for TableHDUs that preserves comment access, units. +* Reading/writing subbarrays +* Specifying what kind of table, ASCII or TableHDU when wriring. * FITSIO PR/issue (performance) * PlotUtils PR/issue (zscale with iteratble) From 2476fada0314e45986bec33af947dfa7340508ac Mon Sep 17 00:00:00 2001 From: William Thompson Date: Wed, 23 Mar 2022 10:28:26 -0700 Subject: [PATCH 071/178] Interactive cube manipulation --- src/AstroImages.jl | 2 +- src/imview.jl | 14 ++++++---- src/showmime.jl | 69 ++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 67 insertions(+), 18 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index bc2e2ffd..fa44ff80 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -523,7 +523,7 @@ include("imview.jl") include("showmime.jl") include("plot-recipes.jl") -# include("ccd2rgb.jl") +include("ccd2rgb.jl") # include("patches.jl") using UUIDs diff --git a/src/imview.jl b/src/imview.jl index 2de333aa..e2187d28 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -39,10 +39,7 @@ Alter the default color map used to display images when using `imview` or displaying an AstroImageMat. """ function set_cmap!(cmap) - if cmap ∉ keys(ColorSchemes.colorschemes) - throw(KeyError("$cmap not found in ColorSchemes.colorschemes")) - end - _default_cmap[] = cmap + _default_cmap[] = _lookup_cmap(cmap) end """ set_clims!(clims::Tuple) @@ -71,6 +68,13 @@ Helper to iterate over data skipping missing and non-finite values. """ skipmissingnan(itr) = Iterators.filter(el->!ismissing(el) && isfinite(el), itr) + +function _lookup_cmap(cmap) + if cmap ∉ keys(ColorSchemes.colorschemes) + error("$cmap not found in ColorSchemes.colorschemes. See: https://juliagraphics.github.io/ColorSchemes.jl/stable/catalogue/") + end +end + """ imview(img; clims=extrema, stretch=identity, cmap=nothing) @@ -159,7 +163,7 @@ function imview( imgmin, imgmax = clims(skipmissingnan(img)) end normed = clampednormedview(img, (imgmin, imgmax)) - return _imview(img, normed, stretch, cmap) + return _imview(img, normed, stretch, _lookup_cmap(cmap)) end function _imview(img, normed::AbstractArray{T}, stretch, cmap) where T diff --git a/src/showmime.jl b/src/showmime.jl index 754534f1..c3d6e11e 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -6,18 +6,6 @@ # Visualize the fits image by changing the brightness and contrast of image. -# Users can also provide their own range as keyword arguments. -# """ -# function brightness_contrast(img::AstroImageMat{T,N}; brightness_range = 0:255, -# contrast_range = 1:1000, header_number = 1) where {T,N} -# @manipulate for brightness in brightness_range, contrast in contrast_range -# _brightness_contrast(C, img.data[header_number], brightness, contrast) -# end -# end - -# This is used in Jupyter notebooks -# Base.show(io::IO, mime::MIME"text/html", img::AstroImageMat; kwargs...) = -# show(io, mime, brightness_contrast(img), kwargs...) # This is used in VSCode and others @@ -55,6 +43,7 @@ end # end # TODO: for this to work, we need to actually add and remove a show method. TBD how. +# TODO: ensure this still works and is backwards compatible # Lazily reinterpret the AstroImageMat as a Matrix{Color}, upon request. # By itself, Images.colorview works fine on AstroImages. But # AstroImages are not normalized to be between [0,1]. So we override @@ -68,3 +57,59 @@ function render(img::AstroImageMat{T,N}) where {T,N} return colorview(Gray, f.(_float.(img.data))) end Images.colorview(img::AstroImageMat) = render(img) + + +using Base64 + + +""" + interact_cube(cube::AbstractArray, initial_slices=) +If running in an interactive environment like IJulia, allow scrolling through +the slices of a cube interactively using `imview`. +Accepts the same keyword arguments as `imview`, with one exception. Here, +if `clims` is a function, it is applied once to all the finite pixels in the cube +to determine the color limits rather than just the currently displayed slice. +""" +function interact_cube( + cube::Union{AbstractArray{T,3},AbstractArray{T,4},AbstractArray{T,5}}, + initial_slices=first.(axes.(Ref(cube),3:ndims(cube))); + clims=_default_clims[], + imview_kwargs... +) where T + # Create a single view that updates + buf = cube[:,:,initial_slices...] + + # If not provided, calculate clims by applying to the whole cube + # rather than just one slice + # Users can pass clims as an array or tuple containing the minimum and maximum values + if typeof(clims) <: AbstractArray || typeof(clims) <: Tuple + if length(clims) != 2 + error("clims must have exactly two values if provided.") + end + clims = (first(clims), last(clims)) + # Or as a callable that computes them given an iterator + else + clims = clims(skipmissingnan(cube)) + end + + v = imview(buf; clims, imview_kwargs...) + + cubesliders = map(3:ndims(cube)) do ax_i + ax = axes(cube, ax_i) + return Interact.slider(ax, initial_slices[ax_i-2], label=string(dimnames[ax_i])); + end + + function viz(sliderindexes) + buf .= view(cube,:,:,sliderindexes...) + b64 = Base64.base64encode() do io + show(io, MIME("image/png"), v) + end + HTML("
") + end + + return vbox(cubesliders..., map(viz, cubesliders...)) +end + +# This is used in Jupyter notebooks +Base.show(io::IO, mime::MIME"text/html", cube::Union{AstroImage{T,3},AstroImage{T,4},AstroImage{T,5}}; kwargs...) where T = + show(io, mime, interact_cube(cube), kwargs...) \ No newline at end of file From bc0be85183d901b8c8eacb203c2999933d526959 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 25 Mar 2022 12:18:56 -0700 Subject: [PATCH 072/178] Fix cmap --- src/imview.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/imview.jl b/src/imview.jl index e2187d28..3bbb5898 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -73,6 +73,7 @@ function _lookup_cmap(cmap) if cmap ∉ keys(ColorSchemes.colorschemes) error("$cmap not found in ColorSchemes.colorschemes. See: https://juliagraphics.github.io/ColorSchemes.jl/stable/catalogue/") end + return cmap end """ From 991a1c9ecd3d3d1016be4d4ef4690b31dd5acd6e Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 25 Mar 2022 12:19:06 -0700 Subject: [PATCH 073/178] Detect additional WCS headers --- src/wcs.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wcs.jl b/src/wcs.jl index efb62096..e7003ae3 100644 --- a/src/wcs.jl +++ b/src/wcs.jl @@ -206,7 +206,7 @@ const WCS_HEADERS_TEMPLATES = [ ] # Expand the headers containing lower case specifers into N copies -Is = [""; string.(1:4)] +Is = [""; string.(1:4); string.('a':'d')] # Find all lower case templates const WCS_HEADERS = Set(mapreduce(vcat, WCS_HEADERS_TEMPLATES) do template if any(islowercase, template) From 5740516f8dfb9038b8a842722325c99d7cc9420e Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 25 Mar 2022 12:19:16 -0700 Subject: [PATCH 074/178] Auto display images with missing data --- src/showmime.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/showmime.jl b/src/showmime.jl index c3d6e11e..3d36bdc8 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -15,7 +15,7 @@ Base.show(io::IO, mime::MIME"image/png", img::AstroImageMat{T}; kwargs...) where show(io, mime, arraydata(img), kwargs...) # Otherwise, call imview with the default settings. -Base.show(io::IO, mime::MIME"image/png", img::AstroImageMat{T}; kwargs...) where {T<:Real} = +Base.show(io::IO, mime::MIME"image/png", img::AstroImageMat{T}; kwargs...) where {T<:Union{Real,Missing}} = show(io, mime, imview(img), kwargs...) # Special handling for complex images From a4ca80485919e8b0fc0f534ba1c895fc296eff39 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 25 Mar 2022 12:23:23 -0700 Subject: [PATCH 075/178] Remove interactive cube functionality --- Project.toml | 1 - src/AstroImages.jl | 1 - src/showmime.jl | 106 ++++++++++++++++++++++----------------------- 3 files changed, 53 insertions(+), 55 deletions(-) diff --git a/Project.toml b/Project.toml index 2aa285d0..6ad2ee19 100644 --- a/Project.toml +++ b/Project.toml @@ -12,7 +12,6 @@ FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" InlineStrings = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48" -Interact = "c601a237-2ae4-5e1e-952c-7a85b0c7eef1" MappedArrays = "dbb5928d-eab1-5f90-85c2-b9b0edb7c900" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043" diff --git a/src/AstroImages.jl b/src/AstroImages.jl index fa44ff80..4db9f2ee 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -3,7 +3,6 @@ module AstroImages using FITSIO using FileIO using Images # TODO: maybe this can be ImagesCore -using Interact using Reproject using WCS using Statistics diff --git a/src/showmime.jl b/src/showmime.jl index 3d36bdc8..ce7cb532 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -59,57 +59,57 @@ end Images.colorview(img::AstroImageMat) = render(img) -using Base64 - - -""" - interact_cube(cube::AbstractArray, initial_slices=) -If running in an interactive environment like IJulia, allow scrolling through -the slices of a cube interactively using `imview`. -Accepts the same keyword arguments as `imview`, with one exception. Here, -if `clims` is a function, it is applied once to all the finite pixels in the cube -to determine the color limits rather than just the currently displayed slice. -""" -function interact_cube( - cube::Union{AbstractArray{T,3},AbstractArray{T,4},AbstractArray{T,5}}, - initial_slices=first.(axes.(Ref(cube),3:ndims(cube))); - clims=_default_clims[], - imview_kwargs... -) where T - # Create a single view that updates - buf = cube[:,:,initial_slices...] - - # If not provided, calculate clims by applying to the whole cube - # rather than just one slice - # Users can pass clims as an array or tuple containing the minimum and maximum values - if typeof(clims) <: AbstractArray || typeof(clims) <: Tuple - if length(clims) != 2 - error("clims must have exactly two values if provided.") - end - clims = (first(clims), last(clims)) - # Or as a callable that computes them given an iterator - else - clims = clims(skipmissingnan(cube)) - end - - v = imview(buf; clims, imview_kwargs...) - - cubesliders = map(3:ndims(cube)) do ax_i - ax = axes(cube, ax_i) - return Interact.slider(ax, initial_slices[ax_i-2], label=string(dimnames[ax_i])); - end - - function viz(sliderindexes) - buf .= view(cube,:,:,sliderindexes...) - b64 = Base64.base64encode() do io - show(io, MIME("image/png"), v) - end - HTML("
") - end - - return vbox(cubesliders..., map(viz, cubesliders...)) -end +# using Base64 + + +# """ +# interact_cube(cube::AbstractArray, initial_slices=) +# If running in an interactive environment like IJulia, allow scrolling through +# the slices of a cube interactively using `imview`. +# Accepts the same keyword arguments as `imview`, with one exception. Here, +# if `clims` is a function, it is applied once to all the finite pixels in the cube +# to determine the color limits rather than just the currently displayed slice. +# """ +# function interact_cube( +# cube::Union{AbstractArray{T,3},AbstractArray{T,4},AbstractArray{T,5}}, +# initial_slices=first.(axes.(Ref(cube),3:ndims(cube))); +# clims=_default_clims[], +# imview_kwargs... +# ) where T +# # Create a single view that updates +# buf = cube[:,:,initial_slices...] + +# # If not provided, calculate clims by applying to the whole cube +# # rather than just one slice +# # Users can pass clims as an array or tuple containing the minimum and maximum values +# if typeof(clims) <: AbstractArray || typeof(clims) <: Tuple +# if length(clims) != 2 +# error("clims must have exactly two values if provided.") +# end +# clims = (first(clims), last(clims)) +# # Or as a callable that computes them given an iterator +# else +# clims = clims(skipmissingnan(cube)) +# end + +# v = imview(buf; clims, imview_kwargs...) + +# cubesliders = map(3:ndims(cube)) do ax_i +# ax = axes(cube, ax_i) +# return Interact.slider(ax, initial_slices[ax_i-2], label=string(dimnames[ax_i])); +# end + +# function viz(sliderindexes) +# buf .= view(cube,:,:,sliderindexes...) +# b64 = Base64.base64encode() do io +# show(io, MIME("image/png"), v) +# end +# HTML("
") +# end + +# return vbox(cubesliders..., map(viz, cubesliders...)) +# end -# This is used in Jupyter notebooks -Base.show(io::IO, mime::MIME"text/html", cube::Union{AstroImage{T,3},AstroImage{T,4},AstroImage{T,5}}; kwargs...) where T = - show(io, mime, interact_cube(cube), kwargs...) \ No newline at end of file +# # This is used in Jupyter notebooks +# Base.show(io::IO, mime::MIME"text/html", cube::Union{AstroImage{T,3},AstroImage{T,4},AstroImage{T,5}}; kwargs...) where T = +# show(io, mime, interact_cube(cube), kwargs...) \ No newline at end of file From 75e37891ae58e767d37800adc1343d5409c8f2a6 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 25 Mar 2022 13:39:58 -0700 Subject: [PATCH 076/178] Reduce dependencies --- Project.toml | 6 +++++- src/AstroImages.jl | 14 +++++++++----- src/imview.jl | 26 ++++++++++++++++++++++++-- src/showmime.jl | 22 +++++++++++----------- 4 files changed, 49 insertions(+), 19 deletions(-) diff --git a/Project.toml b/Project.toml index 6ad2ee19..99326a42 100644 --- a/Project.toml +++ b/Project.toml @@ -9,8 +9,12 @@ ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" DimensionalData = "0703355e-b756-11e9-17c0-8b28908087d0" FITSIO = "525bcba6-941b-5504-bd06-fd0dc1a4d2eb" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +ImageAxes = "2803e5a7-5153-5ecf-9a86-9b4c37f5f5ac" +ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534" ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" -Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" +ImageMetadata = "bc367c6b-8a6b-528e-b4bd-a4b897500b49" +ImageShow = "4e3cecfd-b093-5904-9786-8bbb286a6a31" +ImageTransformations = "02fcd773-0e25-5acc-982a-7f6622650795" InlineStrings = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48" MappedArrays = "dbb5928d-eab1-5f90-85c2-b9b0edb7c900" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 4db9f2ee..111fe1ca 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -2,7 +2,11 @@ module AstroImages using FITSIO using FileIO -using Images # TODO: maybe this can be ImagesCore +# Rather than pulling in all of Images.jl, just grab the packages +# we need to extend to our basic functionality. +# We also need ImageShow so that user's images appear automatically. +using ImageCore, ImageShow, ImageMetadata, ImageAxes, ImageTransformations # TODO: maybe this can be ImagesCore + using Reproject using WCS using Statistics @@ -109,9 +113,9 @@ export pix_to_world, pix_to_world!, world_to_pix, world!_to_pix # Accessors """ - Images.arraydata(img::AstroImage) + ImageMetadata.arraydata(img::AstroImage) """ -Images.arraydata(img::AstroImage) = getfield(img, :data) +ImageMetadata.arraydata(img::AstroImage) = getfield(img, :data) header(img::AstroImage) = getfield(img, :header) function wcs(img::AstroImage) if getfield(img, :wcs_stale)[] @@ -489,8 +493,8 @@ maybe_copyheader(::AbstractArray, data) = data # Restrict downsizes images by roughly a factor of two. # We want to keep the wrapper but downsize the underlying array # TODO: correct dimensions after restrict. -Images.restrict(img::AstroImage, ::Tuple{}) = img -Images.restrict(img::AstroImage, region::Dims) = shareheader(img, restrict(arraydata(img), region)) +ImageTransformations.restrict(img::AstroImage, ::Tuple{}) = img +ImageTransformations.restrict(img::AstroImage, region::Dims) = shareheader(img, restrict(arraydata(img), region)) # TODO: use WCS info # ImageCore.pixelspacing(img::ImageMeta) = pixelspacing(arraydata(img)) diff --git a/src/imview.jl b/src/imview.jl index 3bbb5898..7e807a3e 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -166,6 +166,28 @@ function imview( normed = clampednormedview(img, (imgmin, imgmax)) return _imview(img, normed, stretch, _lookup_cmap(cmap)) end +# Special handling for complex images +""" + imview(img::AbstractArray{<:Complex}; ...) + +When applied to an image with complex values, display the magnitude +of the pixels using `imview` and display the phase angle as a panel below +using a cyclical color map. +For more customatization, you can create a view like this yourself: +```julia +vcat( + imview(abs.(img)), + imview(angle.(img)), +) +``` +""" +function imview(img::AbstractArray{T}; kwargs...) where {T<:Complex} + + mag_view = imview(abs.(img), kwargs...) + angle_view = imview(angle.(img), clims=(-pi, pi), cmap=:cyclic_mygbm_30_95_c78_n256_s25) + vcat(mag_view,angle_view) +end + function _imview(img, normed::AbstractArray{T}, stretch, cmap) where T if T <: Union{Missing,<:Number} @@ -237,8 +259,8 @@ end # TODO: is this the correct function to extend? # Instead of using a datatype like N0f32 to interpret integers as fixed point values in [0,1], # we use a mappedarray to map the native data range (regardless of type) to [0,1] -Images.normedview(img::AstroImageMat{<:FixedPoint}) = img -function Images.normedview(img::AstroImageMat{T}) where T +ImageCore.normedview(img::AstroImageMat{<:FixedPoint}) = img +function ImageCore.normedview(img::AstroImageMat{T}) where T imgmin, imgmax = extrema(skipmissingnan(img)) Δ = abs(imgmax - imgmin) normeddata = mappedarray( diff --git a/src/showmime.jl b/src/showmime.jl index ce7cb532..426c0645 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -15,18 +15,18 @@ Base.show(io::IO, mime::MIME"image/png", img::AstroImageMat{T}; kwargs...) where show(io, mime, arraydata(img), kwargs...) # Otherwise, call imview with the default settings. -Base.show(io::IO, mime::MIME"image/png", img::AstroImageMat{T}; kwargs...) where {T<:Union{Real,Missing}} = +Base.show(io::IO, mime::MIME"image/png", img::AstroImageMat{T}; kwargs...) where {T<:Union{Number,Missing}} = show(io, mime, imview(img), kwargs...) -# Special handling for complex images -function Base.show(io::IO, mime::MIME"image/png", img::AstroImageMat{T}; kwargs...) where {T<:Complex} - # Not sure we really want to support this functionality, but we will allow it for - # now with a warning. - @warn "Displaying complex image as magnitude and phase (maxlog=1)" maxlog=1 - mag_view = imview(abs.(img)) - angle_view = imview(angle.(img), clims=(-pi, pi), cmap=:cyclic_mygbm_30_95_c78_n256_s25) - show(io, mime, vcat(mag_view,angle_view), kwargs...) -end +# # Special handling for complex images +# function Base.show(io::IO, mime::MIME"image/png", img::AstroImageMat{T}; kwargs...) where {T<:Complex} +# # Not sure we really want to support this functionality, but we will allow it for +# # now with a warning. +# @warn "Displaying complex image as magnitude and phase (maxlog=1)" maxlog=1 +# mag_view = imview(abs.(img)) +# angle_view = imview(angle.(img), clims=(-pi, pi), cmap=:cyclic_mygbm_30_95_c78_n256_s25) +# show(io, mime, vcat(mag_view,angle_view), kwargs...) +# end # const _autoshow = Base.RefValue{Bool}(true) # """ @@ -56,7 +56,7 @@ function render(img::AstroImageMat{T,N}) where {T,N} f = scaleminmax(_float(imgmin), _float(max(imgmax, imgmax + one(T)))) return colorview(Gray, f.(_float.(img.data))) end -Images.colorview(img::AstroImageMat) = render(img) +ImageCore.colorview(img::AstroImageMat) = render(img) # using Base64 From 0936a47cb9cee3f28004f6bb9d4c3869eda173fc Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 25 Mar 2022 15:30:23 -0700 Subject: [PATCH 077/178] Fix imview of complex images --- src/imview.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/imview.jl b/src/imview.jl index 7e807a3e..f3be4fda 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -181,9 +181,9 @@ vcat( ) ``` """ -function imview(img::AbstractArray{T}; kwargs...) where {T<:Complex} +function imview(img::AbstractMatrix{T}; kwargs...) where {T<:Complex} - mag_view = imview(abs.(img), kwargs...) + mag_view = imview(abs.(img); kwargs...) angle_view = imview(angle.(img), clims=(-pi, pi), cmap=:cyclic_mygbm_30_95_c78_n256_s25) vcat(mag_view,angle_view) end From 2973ba5c5ebb3123591a2dfe8fc91ac275a75770 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 25 Mar 2022 16:35:55 -0700 Subject: [PATCH 078/178] Support cmap=nothing again --- src/imview.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/imview.jl b/src/imview.jl index f3be4fda..230201fd 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -75,6 +75,7 @@ function _lookup_cmap(cmap) end return cmap end +_lookup_cmap(cmap::Nothing) = nothing """ imview(img; clims=extrema, stretch=identity, cmap=nothing) From 9c9db60c66bcf1f260d10782f06299dc8d9907bc Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sat, 26 Mar 2022 20:00:18 -0700 Subject: [PATCH 079/178] Begin work on docs --- .gitignore | 4 ++++ docs/Project.toml | 5 ++++ docs/make.jl | 22 +++++++++++++++++ docs/src/api.md | 48 +++++++++++++++++++++++++++++++++++++ docs/src/getting-started.md | 1 + docs/src/index.md | 5 ++++ docs/src/tour.md | 21 ++++++++++++++++ 7 files changed, 106 insertions(+) create mode 100644 docs/Project.toml create mode 100644 docs/make.jl create mode 100644 docs/src/api.md create mode 100644 docs/src/getting-started.md create mode 100644 docs/src/index.md create mode 100644 docs/src/tour.md diff --git a/.gitignore b/.gitignore index 8d931fb1..28387fd8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ Manifest.toml # test files /test/data + + +# built docs +/docs/build diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 00000000..fb315d18 --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,5 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" + +[compat] +Documenter = "0.27" \ No newline at end of file diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 00000000..4c879704 --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,22 @@ +using Documenter, AstroImages +makedocs( + sitename="AstroImages.jl", + pages = [ + "Home" => "index.md", + "Tour" => "tour.md", + "Tutorials" => [ + "Getting Started" => "getting-started.md" + ], + "API" => "api.md", + ], + format = Documenter.HTML( + prettyurls = get(ENV, "CI", nothing) == "true" + ), + workdir=".." +) + + +# deploydocs( +# repo = "github.com/sefffal/AstroImages.jl.git", +# devbranch = "main" +# ) diff --git a/docs/src/api.md b/docs/src/api.md new file mode 100644 index 00000000..37dd6741 --- /dev/null +++ b/docs/src/api.md @@ -0,0 +1,48 @@ +# API Documentation + + +```@docs +load +save +AstroImage +AstroImageVec +AstroImageMat +arraydata +header +Comment +History +wcs +pix_to_world +pix_to_world! +world_to_pix +world!_to_pix +X +Y +Z +Dim +At +Near +Between +.. +dims +refdims +clampednormedview +WCSGrid +ccd2rgb +composechannels +reset! +zscale +percent +logstretch +powstretch +sqrtstretch +squarestretch +asinhstretch +sinhstretch +powerdiststretch +imview +implot +copyheader +shareheader +ImageCore.normedview +``` \ No newline at end of file diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md new file mode 100644 index 00000000..bad55622 --- /dev/null +++ b/docs/src/getting-started.md @@ -0,0 +1 @@ +# Getting Started diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 00000000..72cbe334 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,5 @@ +# Home + +AstroImages.jl is a Julia package for loading, manipulating, visualizing, and saving astronomical images. + + diff --git a/docs/src/tour.md b/docs/src/tour.md new file mode 100644 index 00000000..7c3efe2d --- /dev/null +++ b/docs/src/tour.md @@ -0,0 +1,21 @@ +# Package Tour + +To follow along, download the images from the [Fits Liberator](https://esahubble.org/projects/fits_liberator/eagledata/) page and unzip them. + +```@meta +DocTestSetup = quote + using AstroImages +end +``` + +```@repl +using AstroImages +``` + +Let's start by loading a FITS file. +```@repl +using AstroImages +eagle_656 = load("fits/656nmos.fits") +save("eagle-1.png") # hide +``` +![eagle](eagle-1.png) From abeff6fd243b46f8a414d4fc404546e9a46cf92e Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 27 Mar 2022 08:48:19 -0700 Subject: [PATCH 080/178] Restructure code layout for methods that extend other packages --- Project.toml | 1 + src/AstroImages.jl | 150 +++-------------------- src/contrib/abstract-ffts.jl | 19 +++ src/contrib/images.jl | 85 +++++++++++++ src/{patches.jl => contrib/reproject.jl} | 21 +--- src/io.jl | 108 ++++++++++++++++ 6 files changed, 229 insertions(+), 155 deletions(-) create mode 100644 src/contrib/abstract-ffts.jl create mode 100644 src/contrib/images.jl rename src/{patches.jl => contrib/reproject.jl} (69%) create mode 100644 src/io.jl diff --git a/Project.toml b/Project.toml index 99326a42..c66995a5 100644 --- a/Project.toml +++ b/Project.toml @@ -4,6 +4,7 @@ authors = ["Mosè Giordano", "Rohit Kumar"] version = "0.2.0" [deps] +AbstractFFTs = "621f4979-c628-5d54-868e-fcf4e3e8185c" AstroAngles = "5c4adb95-c1fc-4c53-b4ea-2a94080c53d2" ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" DimensionalData = "0703355e-b756-11e9-17c0-8b28908087d0" diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 111fe1ca..e3e82ce1 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -25,6 +25,8 @@ using PlotUtils: optimize_ticks export load, save, AstroImage, + AstroImageVec, + AstroImageMat, WCSGrid, ccd2rgb, composechannels, @@ -46,7 +48,13 @@ export load, header, wcs, Comment, - History + History, + pix_to_world, + pix_to_world!, + world_to_pix, + world_to_pix!, + x + # Images.jl expects data to be either a float or a fixed-point number. Here we define some @@ -90,7 +98,6 @@ end # Provide type aliases for 1D and 2D versions of our data structure. const AstroImageVec{T,D,R,A} = AstroImage{T,1,D,R,A} where {T,D,R,A} const AstroImageMat{T,D,R,A} = AstroImage{T,2,D,R,A} where {T,D,R,A} -export AstroImage, AstroImageVec, AstroImageMat # Re-export symbols from DimensionalData that users will need # for indexing. @@ -108,8 +115,6 @@ const dimnames = ( (Dim{i} for i in 4:10)... ) -# Export WCS coordinate conversion functions -export pix_to_world, pix_to_world!, world_to_pix, world!_to_pix # Accessors """ @@ -150,18 +155,6 @@ DimensionalData.metadata(::AstroImage) = DimensionalData.Dimensions.LookupArrays ) return AstroImage(data, dims, refdims, header, Ref(wcs), Ref(wcs_stale)) end -# Stub for when a name or metadata are passed along (we don't implement that functionality) -# @inline function DimensionalData.rebuild( -# img::AstroImage, -# data, -# dims::Tuple, -# refdims::Tuple, -# name::Union{Symbol,DimensionalData.AbstractName}, -# metadata::Union{DimensionalData.LookupArrays.AbstractMetadata,Nothing}, -# ) -# # name and metadata are dropped -# return DimensionalData.rebuild(img, data, dims, refdims, name, metadata) -# end @inline DimensionalData.rebuildsliced( f::Function, img::AstroImage, @@ -298,117 +291,7 @@ AstroImage(data::AbstractArray, wcs::WCSTransform) = AstroImage(data, emptyheade -""" - load(fitsfile::String) - -Read and return the data from the first ImageHDU in a FITS file -as an AstroImage. If no ImageHDUs are present, an error is returned. - - load(fitsfile::String, ext::Int) - -Read and return the data from the HDU `ext`. If it is an ImageHDU, -as AstroImage is returned. If it is a TableHDU, a plain Julia -column table is returned. - - load(fitsfile::String, :) - -Read and return the data from each HDU in an FITS file. ImageHDUs are -returned as AstroImage, and TableHDUs are returned as column tables. - - load(fitsfile::String, exts::Union{NTuple, AbstractArray}) - -Read and return the data from the HDUs given by `exts`. ImageHDUs are -returned as AstroImage, and TableHDUs are returned as column tables. - -!! Currently comments on TableHDUs are not supported and are ignored. -""" -function fileio_load(f::File{format"FITS"}, ext::Union{Int,Nothing}=nothing) where N - return FITS(f.filename, "r") do fits - if isnothing(ext) - ext = indexer(fits) - end - _loadhdu(fits[ext]) - end -end -function fileio_load(f::File{format"FITS"}, exts::Union{NTuple{N, <:Integer},AbstractArray{<:Integer}}) where N - return FITS(f.filename, "r") do fits - map(exts) do ext - _loadhdu(fits[ext]) - end - end -end -function fileio_load(f::File{format"FITS"}, exts::Colon) where N - return FITS(f.filename, "r") do fits - exts_resolved = 1:length(fits) - map(exts_resolved) do ext - _loadhdu(fits[ext]) - end - end -end - -_loadhdu(hdu::FITSIO.TableHDU) = Tables.columntable(hdu) -function _loadhdu(hdu::FITSIO.ImageHDU) - if size(hdu) != () - return AstroImage(hdu) - else - # Sometimes files have an empty data HDU that shows up as an image HDU but has headers. - # Fallback to creating an empty AstroImage with those headers. - emptydata = fill(0, (0,0)) - return AstroImage(emptydata, (), (), read_header(hdu), Ref(emptywcs(emptydata)), Ref(false)) - end -end -function indexer(fits::FITS) - ext = 0 - for (i, hdu) in enumerate(fits) - if hdu isa ImageHDU && length(size(hdu)) >= 2 # check if Image is atleast 2D - ext = i - break - end - end - if ext > 1 - @info "Image was loaded from HDU $ext" - elseif ext == 0 - error("There are no ImageHDU extensions in '$(fits.filename)'") - end - return ext -end -indexer(fits::NTuple{N, FITS}) where {N} = ntuple(i -> indexer(fits[i]), N) - - -# Fallback for saving arbitrary arrays -function fileio_save(f::File{format"FITS"}, args...) - FITS(f.filename, "w") do fits - for arg in args - writearg(fits, arg) - end - end -end -writearg(fits, img::AstroImage) = write(fits, arraydata(img), header=header(img)) -# Fallback for writing plain arrays -writearg(fits, arr::AbstractArray) = write(fits, arr) -# For table compatible data. -# This allows users to round trip: dat = load("abc.fits", :); write("abc", dat) -# when it contains FITS tables. -function writearg(fits, table) - if !Tables.istable(table) - error("Cannot save argument to FITS file. Value is not an AbstractArray or table.") - end - # FITSIO has fairly restrictive input types for writing tables (assertions for documentation only) - colname_strings = string.(collect(Tables.columnnames(table)))::Vector{String} - columns = collect(Tables.columns(table))::Vector - write( - fits, - colname_strings, - columns; - hdutype=TableHDU, - # TODO: In future, we want to be able to access and round-trip coments - # on table HDUs - # header=nothing - ) -end - -export load, save @@ -490,15 +373,6 @@ maybe_copyheader(img::AstroImage, data) = copyheader(img, data) maybe_copyheader(::AbstractArray, data) = data -# Restrict downsizes images by roughly a factor of two. -# We want to keep the wrapper but downsize the underlying array -# TODO: correct dimensions after restrict. -ImageTransformations.restrict(img::AstroImage, ::Tuple{}) = img -ImageTransformations.restrict(img::AstroImage, region::Dims) = shareheader(img, restrict(arraydata(img), region)) - -# TODO: use WCS info -# ImageCore.pixelspacing(img::ImageMeta) = pixelspacing(arraydata(img)) - Base.promote_rule(::Type{AstroImage{T}}, ::Type{AstroImage{V}}) where {T,V} = AstroImage{promote_type{T,V}} @@ -510,6 +384,7 @@ Base.convert(::Type{AstroImage{T}}, A::AstroImage{T}) where {T} = A Base.convert(::Type{AstroImage{T}}, A::AstroImage) where {T} = shareheader(A, convert(AbstractArray{T}, arraydata(A))) Base.convert(::Type{AstroImage{T}}, A::AbstractArray{T}) where {T} = AstroImage(A) Base.convert(::Type{AstroImage{T}}, A::AbstractArray) where {T} = AstroImage(convert(AbstractArray{T}, A)) +Base.convert(::Type{AstroImage{T,N,D,R,AT}}, A::AbstractArray{T,N}) where {T,N,D,R,AT} = AstroImage(convert(AbstractArray{T}, A)) # TODO: share headers in View. Needs support from DimensionalData. @@ -522,10 +397,15 @@ emptyheader() = FITSHeader(String[],[],String[]) include("wcs.jl") +include("io.jl") include("imview.jl") include("showmime.jl") include("plot-recipes.jl") +include("contrib/images.jl") +include("contrib/abstract-ffts.jl") +include("contrib/reproject.jl") + include("ccd2rgb.jl") # include("patches.jl") using UUIDs diff --git a/src/contrib/abstract-ffts.jl b/src/contrib/abstract-ffts.jl new file mode 100644 index 00000000..39dcddc5 --- /dev/null +++ b/src/contrib/abstract-ffts.jl @@ -0,0 +1,19 @@ +using AbstractFFTs + +for f in [ + :(AbstractFFTs.plan_fft), + :(AbstractFFTs.plan_bfft), + :(AbstractFFTs.plan_ifft), + :(AbstractFFTs.plan_fft!), + :(AbstractFFTs.plan_bfft!), + :(AbstractFFTs.plan_ifft!), + :(AbstractFFTs.plan_rfft), + :(AbstractFFTs.fftshift), +] + # TODO: should we try to alter the image headers to change the units? + @eval ($f)(img::AstroImage, args...; kwargs...) = copyheader(img, $f(arraydata(img))) +end + + +# AbstractFFTs.complexfloat(img::AstroImage{T,N,D,R,StridedArray{T}}) where {T<:Complex{<:BlasReal}} = img +# AbstractFFTs.realfloat(img::AstroImage{T,N,D,R,StridedArray{T}}) where {T<:BlasReal} = img diff --git a/src/contrib/images.jl b/src/contrib/images.jl new file mode 100644 index 00000000..db41eb88 --- /dev/null +++ b/src/contrib/images.jl @@ -0,0 +1,85 @@ +#= +ImageTransformations +=# +# function warp(img::AstroImageMat, args...; kwargs...) +# out = warp(arraydatat(img), args...; kwargs...) +# return copyheaders(img, out) +# end + + + +# Instead of using a datatype like N0f32 to interpret integers as fixed point values in [0,1], +# we use a mappedarray to map the native data range (regardless of type) to [0,1] +ImageCore.normedview(img::AstroImageMat{<:FixedPoint}) = img +function ImageCore.normedview(img::AstroImageMat{T}) where T + imgmin, imgmax = extrema(skipmissingnan(img)) + Δ = abs(imgmax - imgmin) + normeddata = mappedarray( + pix -> (pix - imgmin)/Δ, + pix_norm -> convert(T, pix_norm*Δ + imgmin), + img + ) + return shareheader(img, normeddata) +end + +""" + clampednormedview(arr, (min, max)) + +Given an AbstractArray and limits `min,max` return a view of the array +where data between [min, max] are scaled to [0, 1] and datat outside that +range are clamped to [0, 1]. + +See also: normedview +""" +function clampednormedview(img::AbstractArray{T}, lims) where T + imgmin, imgmax = lims + Δ = abs(imgmax - imgmin) + normeddata = mappedarray( + pix -> clamp((pix - imgmin)/Δ, zero(T), one(T)), + pix_norm -> convert(T, pix_norm*Δ + imgmin), + img + ) + return maybe_shareheader(img, normeddata) +end +function clampednormedview(img::AbstractArray{T}, lims) where T <: Normed + # If the data is in a Normed type and the limits are [0,1] then + # it already lies in that range. + if lims[1] == 0 && lims[2] == 1 + return img + end + imgmin, imgmax = lims + Δ = abs(imgmax - imgmin) + normeddata = mappedarray( + pix -> clamp((pix - imgmin)/Δ, zero(T), one(T)), + pix_norm -> pix_norm*Δ + imgmin, + img + ) + return maybe_shareheader(img, normeddata) +end +function clampednormedview(img::AbstractArray{Bool}, lims) + return img +end + + +# Restrict downsizes images by roughly a factor of two. +# We want to keep the wrapper but downsize the underlying array +# TODO: correct dimensions after restrict. +ImageTransformations.restrict(img::AstroImage, ::Tuple{}) = img +ImageTransformations.restrict(img::AstroImage, region::Dims) = shareheader(img, restrict(arraydata(img), region)) + +# TODO: use WCS info +# ImageCore.pixelspacing(img::ImageMeta) = pixelspacing(arraydata(img)) + + +# ImageContrastAdjustment +# function ImageContrastAdjustment.adjust_histogram(::Type{T}, +# img::AstroImageMat, +# f::Images.ImageContrastAdjustment.AbstractHistogramAdjustmentAlgorithm, +# args...; kwargs...) where T +# out = similar(img, axes(img)) +# adjust_histogram!(out, img, f, args...; kwargs...) +# return out +# end + + + diff --git a/src/patches.jl b/src/contrib/reproject.jl similarity index 69% rename from src/patches.jl rename to src/contrib/reproject.jl index 7e33ab02..a4179cbd 100644 --- a/src/patches.jl +++ b/src/contrib/reproject.jl @@ -1,25 +1,6 @@ -#= -ImageContrastAdjustment -=# -function Images.ImageContrastAdjustment.adjust_histogram(::Type{T}, - img::AstroImageMat, - f::Images.ImageContrastAdjustment.AbstractHistogramAdjustmentAlgorithm, - args...; kwargs...) where T - out = similar(img, axes(img)) - adjust_histogram!(out, img, f, args...; kwargs...) - return out -end -#= -ImageTransformations -=# -# function warp(img::AstroImageMat, args...; kwargs...) -# out = warp(arraydatat(img), args...; kwargs...) -# return copyheaders(img, out) -# end - #= Additional methods to allow Reproject to work. =# @@ -53,4 +34,4 @@ function Reproject.pad_edges(array_in::AstroImageMat{T}) where {T} image[1,:] = image[2,:] image[end,:] = image[end-1,:] return AstroImageMat(image, headers(array_in), wcs(array_in)) -end \ No newline at end of file +end diff --git a/src/io.jl b/src/io.jl new file mode 100644 index 00000000..0e257069 --- /dev/null +++ b/src/io.jl @@ -0,0 +1,108 @@ +""" +load(fitsfile::String) + +Read and return the data from the first ImageHDU in a FITS file +as an AstroImage. If no ImageHDUs are present, an error is returned. + +load(fitsfile::String, ext::Int) + +Read and return the data from the HDU `ext`. If it is an ImageHDU, +as AstroImage is returned. If it is a TableHDU, a plain Julia +column table is returned. + +load(fitsfile::String, :) + +Read and return the data from each HDU in an FITS file. ImageHDUs are +returned as AstroImage, and TableHDUs are returned as column tables. + +load(fitsfile::String, exts::Union{NTuple, AbstractArray}) + +Read and return the data from the HDUs given by `exts`. ImageHDUs are +returned as AstroImage, and TableHDUs are returned as column tables. + +!! Currently comments on TableHDUs are not supported and are ignored. +""" +function fileio_load(f::File{format"FITS"}, ext::Union{Int,Nothing}=nothing) where N +return FITS(f.filename, "r") do fits + if isnothing(ext) + ext = indexer(fits) + end + _loadhdu(fits[ext]) +end +end +function fileio_load(f::File{format"FITS"}, exts::Union{NTuple{N, <:Integer},AbstractArray{<:Integer}}) where N +return FITS(f.filename, "r") do fits + map(exts) do ext + _loadhdu(fits[ext]) + end +end +end +function fileio_load(f::File{format"FITS"}, exts::Colon) where N +return FITS(f.filename, "r") do fits + exts_resolved = 1:length(fits) + map(exts_resolved) do ext + _loadhdu(fits[ext]) + end +end +end + +_loadhdu(hdu::FITSIO.TableHDU) = Tables.columntable(hdu) +function _loadhdu(hdu::FITSIO.ImageHDU) +if size(hdu) != () + return AstroImage(hdu) +else + # Sometimes files have an empty data HDU that shows up as an image HDU but has headers. + # Fallback to creating an empty AstroImage with those headers. + emptydata = fill(0, (0,0)) + return AstroImage(emptydata, (), (), read_header(hdu), Ref(emptywcs(emptydata)), Ref(false)) +end +end +function indexer(fits::FITS) +ext = 0 +for (i, hdu) in enumerate(fits) + if hdu isa ImageHDU && length(size(hdu)) >= 2 # check if Image is atleast 2D + ext = i + break + end +end +if ext > 1 + @info "Image was loaded from HDU $ext" +elseif ext == 0 + error("There are no ImageHDU extensions in '$(fits.filename)'") +end +return ext +end +indexer(fits::NTuple{N, FITS}) where {N} = ntuple(i -> indexer(fits[i]), N) + + +# Fallback for saving arbitrary arrays +function fileio_save(f::File{format"FITS"}, args...) +FITS(f.filename, "w") do fits + for arg in args + writearg(fits, arg) + end +end +end +writearg(fits, img::AstroImage) = write(fits, arraydata(img), header=header(img)) +# Fallback for writing plain arrays +writearg(fits, arr::AbstractArray) = write(fits, arr) +# For table compatible data. +# This allows users to round trip: dat = load("abc.fits", :); write("abc", dat) +# when it contains FITS tables. +function writearg(fits, table) + if !Tables.istable(table) + error("Cannot save argument to FITS file. Value is not an AbstractArray or table.") + end + # FITSIO has fairly restrictive input types for writing tables (assertions for documentation only) + colname_strings = string.(collect(Tables.columnnames(table)))::Vector{String} + columns = collect(Tables.columns(table))::Vector + write( + fits, + colname_strings, + columns; + hdutype=TableHDU, + # TODO: In future, we want to be able to access and round-trip coments + # on table HDUs + # header=nothing + ) +end \ No newline at end of file From 34729123f3b404b197c1619017afea06162a3a48 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 27 Mar 2022 08:48:30 -0700 Subject: [PATCH 081/178] Colorbar support --- src/imview.jl | 118 +++++++++++++++----------- src/plot-recipes.jl | 197 ++++++++++++++++++++++---------------------- 2 files changed, 168 insertions(+), 147 deletions(-) diff --git a/src/imview.jl b/src/imview.jl index 230201fd..23e5d39c 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -22,8 +22,8 @@ This will set the limits to be the 5th percentile to the 95th percentile. """ function percent(perc::Number) trim = (1 - perc/100)/2 - clims(data) = quantile(data, (trim, 1-trim)) - clims(data::AbstractMatrix) = quantile(vec(data), (trim, 1-trim)) + clims(data::AbstractVector) = quantile(data, (trim, 1-trim)) + clims(data::AbstractArray) = clims(vec(data)) return clims end @@ -77,6 +77,22 @@ function _lookup_cmap(cmap) end _lookup_cmap(cmap::Nothing) = nothing +function _resolve_clims(img, clims) + # Tuple or abstract array + if typeof(clims) <: AbstractArray || typeof(clims) <: Tuple + if length(clims) != 2 + error("clims must have exactly two values if provided.") + end + imgmin = first(clims) + imgmax = last(clims) + # Or as a callable that computes them given an iterator + else + imgmin, imgmax = clims(skipmissingnan(img)) + end + return imgmin, imgmax +end + + """ imview(img; clims=extrema, stretch=identity, cmap=nothing) @@ -256,56 +272,58 @@ function _imview(img, normed::AbstractArray{T}, stretch, cmap) where T end - -# TODO: is this the correct function to extend? -# Instead of using a datatype like N0f32 to interpret integers as fixed point values in [0,1], -# we use a mappedarray to map the native data range (regardless of type) to [0,1] -ImageCore.normedview(img::AstroImageMat{<:FixedPoint}) = img -function ImageCore.normedview(img::AstroImageMat{T}) where T - imgmin, imgmax = extrema(skipmissingnan(img)) - Δ = abs(imgmax - imgmin) - normeddata = mappedarray( - pix -> (pix - imgmin)/Δ, - pix_norm -> convert(T, pix_norm*Δ + imgmin), - img - ) - return shareheader(img, normeddata) -end - """ - clampednormedview(arr, (min, max)) + imview_colorbar(img; orientation=:vertical) +Create a colorbar for a given image matching how it is displayed by +`imview`. Returns an image. -Given an AbstractArray and limits `min,max` return a view of the array -where data between [min, max] are scaled to [0, 1] and datat outside that -range are clamped to [0, 1]. - -See also: normedview +`orientation` can be `:vertical` or `:horizontal`. """ -function clampednormedview(img::AbstractArray{T}, lims) where T - imgmin, imgmax = lims - Δ = abs(imgmax - imgmin) - normeddata = mappedarray( - pix -> clamp((pix - imgmin)/Δ, zero(T), one(T)), - pix_norm -> convert(T, pix_norm*Δ + imgmin), - img - ) - return maybe_shareheader(img, normeddata) -end -function clampednormedview(img::AbstractArray{T}, lims) where T <: Normed - # If the data is in a Normed type and the limits are [0,1] then - # it already lies in that range. - if lims[1] == 0 && lims[2] == 1 - return img +function imview_colorbar( + img::AbstractMatrix; + orientation=:vertical, + clims=_default_clims[], + stretch=_default_stretch[], + cmap=_default_cmap[], +) + imgmin, imgmax = _resolve_clims(img, clims) + cbpixlen = 100 + data = repeat(range(imgmin, imgmax, length=cbpixlen), 1,10) + if orientation == :vertical + data = data' + elseif orientation == :horizontal + data = data + else + error("Unsupported orientation for colorbar \"$orientation\"") end - imgmin, imgmax = lims - Δ = abs(imgmax - imgmin) - normeddata = mappedarray( - pix -> clamp((pix - imgmin)/Δ, zero(T), one(T)), - pix_norm -> pix_norm*Δ + imgmin, - img - ) - return maybe_shareheader(img, normeddata) + + # # Stretch the colors: + # # Construct the image to use as a colorbar + # cbimg = imview(data; clims=(imgmin,imgmax), stretch, cmap) + # # And the colorbar tick locations & labels + # ticks, cbmin, cbmax = optimize_ticks(imgmin, imgmax) + # # Now map these to pixel locations through streching and colorlimits: + # stretchmin = stretch(zero(eltype(data))) + # stretchmax = stretch(one(eltype(data))) + # normedticks = clampednormedview(ticks, (imgmin, imgmax)) + # ticksloc = map(ticks,normedticks) do tick, tickn + # return cbpixlen * tickn + # end + + # Strech the ticks + # Construct the image to use as a colorbar + cbimg = imview(data; clims=(imgmin,imgmax), stretch=identity, cmap) + # And the colorbar tick locations & labels + ticks, cbmin, cbmax = optimize_ticks(imgmin, imgmax) + # Now map these to pixel locations through streching and colorlimits: + stretchmin = stretch(zero(eltype(data))) + stretchmax = stretch(one(eltype(data))) + normedticks = clampednormedview(ticks, (imgmin, imgmax)) + ticksloc = map(ticks,normedticks) do tick, tickn + stretched = stretch(tickn) + stretchednormed = (stretched - stretchmin) * (stretchmax - stretchmin) + return cbpixlen * stretchednormed + end + + return cbimg, (ticksloc, string.(ticks)) end -function clampednormedview(img::AbstractArray{Bool}, lims) - return img -end \ No newline at end of file diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 2e59b97a..6f821d93 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -13,14 +13,19 @@ end - # We often plot an AstroImageMat{<:Number} which hasn't yet had - # its wcs cached (wcs_stale=true) and we make an image view here. - # That means we may have to keep recomputing the WCS on each plot call - # since the result is stored in the imview instead of original image. - # Call wcs(img) here if we are later going to plot with wcs coordinates - # to ensure this gets cached beween calls. - if !haskey(plotattributes, :wcsticks) || plotattributes[:wcsticks] - wcs(img) + # Show WCS coordinates if wcsticks is true or unspecified, and has at least one WCS axis present. + showwcsticks = (!haskey(plotattributes, :wcsticks) || plotattributes[:wcsticks]) && + !all(==(""), wcs(img).ctype) + if showwcsticks + + minx = first(axes(img,2)) + maxx = last(axes(img,2)) + miny = first(axes(img,1)) + maxy = last(axes(img,1)) + extent = (minx-0.5, maxx+0.5, miny-0.5, maxy+0.5) + + wcsg = WCSGrid(img, extent) + gridspec = wcsgridspec(wcsg) end # Use package defaults if not user provided. @@ -37,122 +42,120 @@ imgv = imview(img; clims, stretch, cmap) end - xgrid --> true - ygrid --> true - # Use a default grid color that shows up across more - # color maps - if !haskey(plotattributes, :xforeground_color_grid) && !haskey(plotattributes, :yforeground_color_grid) - gridcolor --> :lightgray - end - - # By default, disable the colorbar. - # Plots.jl does no give us sufficient control to make sure the range and ticks - # are correct after applying a non-linear stretch. - colorbar := false - # We may be able to make our own colorbar in future using a second image plot - # off to the side using something like: - # if typeof(clims) <: AbstractArray || typeof(clims) <: Tuple - # if length(clims) != 2 - # error("clims must have exactly two values if provided.") - # end - # imgmin = first(clims) - # imgmax = last(clims) - # # Or as a callable that computes them given an iterator - # else - # imgmin, imgmax = clims(skipmissingnan(img)) - # end - # imview(repeat(range(imgmin, imgmax,length=100), 1,10)'; clims=(imgmin,imgmax), stretch, cmap) + # We have to do a lot of flipping to keep the orientation corect + yflip := false + xflip := false # we have a wcs flag (from the image by default) so that users can skip over # plotting in physical coordinates. This is especially important # if the WCS headers are mallformed in some way. - if !haskey(plotattributes, :wcsticks) || plotattributes[:wcsticks] + showgrid = (!haskey(plotattributes, :xgrid) || haskey(plotattributes, :xgrid)) && + (!haskey(plotattributes, :ygrid) || haskey(plotattributes, :ygrid)) + + # Actual image series (RGB pixels by this point) + @series begin + subplot := 1 + colorbar := false + + + # Disable equal aspect ratios if the scales are totally different + if max(size(imgv)...)/min(size(imgv)...) >= 7 + aspect_ratio --> :none + end # Note: if the axes are on unusual sides (e.g. y-axis at right, x-axis at top) # then these coordinates are not correct. They are only correct exactly # along the axis. # In astropy, the ticks are actually tilted to reflect this, though in general # the transformation from pixel to coordinates can be non-linear and curved. - - minx = first(axes(imgv,2)) - maxx = last(axes(imgv,2)) - miny = first(axes(imgv,1)) - maxy = last(axes(imgv,1)) - extent = (minx-0.5, maxx+0.5, miny-0.5, maxy+0.5) - - - wcsg = WCSGrid(imgv, extent) - gridspec = wcsgridspec(wcsg) - xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), dimindex(img,1), gridspec.tickpos1w)) - xguide --> ctype_label(wcs(imgv).ctype[dimindex(img,1)], wcs(imgv).radesys) - - yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), dimindex(img,2), gridspec.tickpos2w)) - yguide --> ctype_label(wcs(imgv).ctype[dimindex(img,2)], wcs(imgv).radesys) - - # To ensure the physical axis tick labels are correct the axes must be - # tight to the image - xl = first(axes(imgv,2))-0.5, last(axes(imgv,2))+0.5 - yl = first(axes(imgv,1))-0.5, last(axes(imgv,1))+0.5 - ylims --> yl - xlims --> xl - end - - # Disable equal aspect ratios if the scales are totally different - if max(size(imgv)...)/min(size(imgv)...) >= 7 - aspect_ratio --> :none - end - - # We have to do a lot of flipping to keep the orientation corect - yflip := false - xflip := false + if showwcsticks + xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), dimindex(img,1), gridspec.tickpos1w)) + xguide --> ctype_label(wcs(imgv).ctype[dimindex(img,1)], wcs(imgv).radesys) - if length(refdims(imgv)) > 0 - if !haskey(plotattributes, :wcsticks) || plotattributes[:wcsticks] - refdimslabel = join(map(refdims(imgv)) do d - # match dimension with the wcs axis number - i = findfirst(dimnames) do dim_candidate - name(dim_candidate) == name(d) - end - label = ctype_label(wcs(imgv).ctype[i], wcs(imgv).radesys) - value = pix_to_world(imgv, [1,1], all=true)[i] - unit = wcs(imgv).cunit[i] - return @sprintf("%s = %.5g %s", label, value, unit) - end) - else - refdimslabel = join(map(d->"$(name(d))= $(d[1])", refdims(imgv))) + yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), dimindex(img,2), gridspec.tickpos2w)) + yguide --> ctype_label(wcs(imgv).ctype[dimindex(img,2)], wcs(imgv).radesys) end - title --> refdimslabel - end - @series begin + # Display a title giving our position along unplotted dimensions + if length(refdims(imgv)) > 0 + if showwcsticks + refdimslabel = join(map(refdims(imgv)) do d + # match dimension with the wcs axis number + i = findfirst(dimnames) do dim_candidate + name(dim_candidate) == name(d) + end + label = ctype_label(wcs(imgv).ctype[i], wcs(imgv).radesys) + value = pix_to_world(imgv, [1,1], all=true)[i] + unit = wcs(imgv).cunit[i] + return @sprintf("%s = %.5g %s", label, value, unit) + end) + else + refdimslabel = join(map(d->"$(name(d))= $(d[1])", refdims(imgv))) + end + title --> refdimslabel + end view(arraydata(imgv), reverse(axes(imgv,1)),:) - - # imgv = permutedims(imgv, DimensionalData.commondims(>:, (DimensionalData.ZDim, DimensionalData.YDim, DimensionalData.XDim, DimensionalData.TimeDim, DimensionalData.Dimension, DimensionalData.Dimension), dims(imgv))) - # y, x = dims(imgv) - # :xguide --> DimensionalData.label(x) - # :yguide --> DimensionalData.label(y) - # :zguide --> DimensionalData.label(imgv) - # :colorbar_title --> DimensionalData.label(imgv) - # DimensionalData._xticks!(plotattributes, s, x) - # DimensionalData._yticks!(plotattributes, s, y) - # DimensionalData._withaxes(x, y, imgv) - # arraydata(imgv) end # If wcs=true (default) and grid=true (not default), overplot a WCS # grid. - if (!haskey(plotattributes, :wcsticks) || plotattributes[:wcsticks]) && - haskey(plotattributes, :xgrid) && plotattributes[:xgrid] && - haskey(plotattributes, :ygrid) && plotattributes[:ygrid] + if showgrid && showwcsticks # Plot the WCSGrid as a second series (actually just lines) @series begin + subplot := 1 + # Use a default grid color that shows up across more + # color maps + if !haskey(plotattributes, :xforeground_color_grid) && !haskey(plotattributes, :yforeground_color_grid) + gridcolor --> :lightgray + end + + # To ensure the physical axis tick labels are correct the axes must be + # tight to the image + xl = first(axes(imgv,2))-0.5, last(axes(imgv,2))+0.5 + yl = first(axes(imgv,1))-0.5, last(axes(imgv,1))+0.5 + ylims := yl + xlims := xl + wcsg, gridspec end end + + # Disable the colorbar. + # Plots.jl does not give us sufficient control to make sure the range and ticks + # are correct after applying a non-linear stretch. + # We attempt to make our own colorbar using a second plot. + showcolorbar = !(T <: Colorant) && get(plotattributes, :colorbar, true) + if showcolorbar + layout := @layout [ + img{0.95w} colorbar + ] + colorbartitle = get(plotattributes, :colorbartitle, "") + if !haskey(plotattributes, :colorbartitle) && haskey(header(img), "UNIT") + colorbartitle = string(img[:UNIT]) + end + + @series begin + subplot := 2 + aspect_ratio := :none + colorbar := false + cbimg, cbticks = imview_colorbar(img; clims, stretch, cmap) + xticks := [] + ymirror := true + yticks := cbticks + ylabel := colorbartitle + xlabel := "" + xlims := Tuple(axes(cbimg, 2)) + ylims := Tuple(axes(cbimg, 2)) + view(cbimg, reverse(axes(cbimg,1)),:) + end + end + + + return end From 6f93c3545c39180f2809881a72bb4c606940a4c1 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 27 Mar 2022 09:07:38 -0700 Subject: [PATCH 082/178] Keep AstroImage wrapper after FFT operations --- src/contrib/abstract-ffts.jl | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/contrib/abstract-ffts.jl b/src/contrib/abstract-ffts.jl index 39dcddc5..cc286bbf 100644 --- a/src/contrib/abstract-ffts.jl +++ b/src/contrib/abstract-ffts.jl @@ -1,19 +1,27 @@ using AbstractFFTs for f in [ - :(AbstractFFTs.plan_fft), - :(AbstractFFTs.plan_bfft), - :(AbstractFFTs.plan_ifft), - :(AbstractFFTs.plan_fft!), - :(AbstractFFTs.plan_bfft!), - :(AbstractFFTs.plan_ifft!), - :(AbstractFFTs.plan_rfft), - :(AbstractFFTs.fftshift), + :(AbstractFFTs.fft), + :(AbstractFFTs.bfft), + :(AbstractFFTs.ifft), + :(AbstractFFTs.fft!), + :(AbstractFFTs.bfft!), + :(AbstractFFTs.ifft!), + :(AbstractFFTs.rfft), ] # TODO: should we try to alter the image headers to change the units? @eval ($f)(img::AstroImage, args...; kwargs...) = copyheader(img, $f(arraydata(img))) end +for f in [ + :(AbstractFFTs.fftshift), +] + # TODO: should we try to alter the image headers to change the units? + @eval ($f)(img::AstroImage, args...; kwargs...) = shareheader(img, $f(arraydata(img))) +end + + + # AbstractFFTs.complexfloat(img::AstroImage{T,N,D,R,StridedArray{T}}) where {T<:Complex{<:BlasReal}} = img # AbstractFFTs.realfloat(img::AstroImage{T,N,D,R,StridedArray{T}}) where {T<:BlasReal} = img From 86e1c559b94a8009b93d42ccd6f2a2eb09e9aaed Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 27 Mar 2022 09:07:52 -0700 Subject: [PATCH 083/178] Improved plotting support with complex images to match imview --- src/imview.jl | 4 +- src/plot-recipes.jl | 114 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 102 insertions(+), 16 deletions(-) diff --git a/src/imview.jl b/src/imview.jl index 23e5d39c..a1936934 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -201,7 +201,7 @@ vcat( function imview(img::AbstractMatrix{T}; kwargs...) where {T<:Complex} mag_view = imview(abs.(img); kwargs...) - angle_view = imview(angle.(img), clims=(-pi, pi), cmap=:cyclic_mygbm_30_95_c78_n256_s25) + angle_view = imview(angle.(img), clims=(-pi, pi), stretch=identity, cmap=:cyclic_mygbm_30_95_c78_n256_s25) vcat(mag_view,angle_view) end @@ -314,7 +314,7 @@ function imview_colorbar( # Construct the image to use as a colorbar cbimg = imview(data; clims=(imgmin,imgmax), stretch=identity, cmap) # And the colorbar tick locations & labels - ticks, cbmin, cbmax = optimize_ticks(imgmin, imgmax) + ticks, cbmin, cbmax = optimize_ticks(1imgmin, 1imgmax) # Now map these to pixel locations through streching and colorlimits: stretchmin = stretch(zero(eltype(data))) stretchmax = stretch(one(eltype(data))) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 6f821d93..ca2ed2c3 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -3,28 +3,28 @@ if length(h.args) != 1 || !(typeof(h.args[1]) <: AbstractArray) error("Image plots require an arugment that is a subtype of AbstractArray. Got: $(typeof(h.args))") end - img = only(h.args) - if !(typeof(img) <: AstroImage) - img = AstroImage(only(h.args)) + data = only(h.args) + if !(typeof(data) <: AstroImage) + data = AstroImage(only(h.args)) end - T = eltype(img) - if ndims(img) != 2 + T = eltype(data) + if ndims(data) != 2 error("Image passed to `implot` must be two-dimensional. Got ndims(img)=$(ndims(img))") end # Show WCS coordinates if wcsticks is true or unspecified, and has at least one WCS axis present. showwcsticks = (!haskey(plotattributes, :wcsticks) || plotattributes[:wcsticks]) && - !all(==(""), wcs(img).ctype) + !all(==(""), wcs(data).ctype) if showwcsticks - minx = first(axes(img,2)) - maxx = last(axes(img,2)) - miny = first(axes(img,1)) - maxy = last(axes(img,1)) + minx = first(axes(data,2)) + maxx = last(axes(data,2)) + miny = first(axes(data,1)) + maxy = last(axes(data,1)) extent = (minx-0.5, maxx+0.5, miny-0.5, maxy+0.5) - wcsg = WCSGrid(img, extent) + wcsg = WCSGrid(data, extent) gridspec = wcsgridspec(wcsg) end @@ -34,11 +34,16 @@ cmap --> _default_cmap[] if T <: Colorant - imgv = img + imgv = data else clims = plotattributes[:clims] stretch = plotattributes[:stretch] cmap = plotattributes[:cmap] + if T <: Complex + img = abs.(data) + else + img = data + end imgv = imview(img; clims, stretch, cmap) end @@ -128,7 +133,7 @@ # Plots.jl does not give us sufficient control to make sure the range and ticks # are correct after applying a non-linear stretch. # We attempt to make our own colorbar using a second plot. - showcolorbar = !(T <: Colorant) && get(plotattributes, :colorbar, true) + showcolorbar = !(T <: Colorant) && get(plotattributes, :colorbar, true) != :none if showcolorbar layout := @layout [ img{0.95w} colorbar @@ -155,6 +160,87 @@ end + # TODO: refactor to reduce duplication + if T <: Complex + img = angle.(data) + imgv = imview(img, clims=(-1pi, 1pi),stretch=identity, cmap=:cyclic_mygbm_30_95_c78_n256_s25) + layout := @layout [ + imgmag{0.95w, 0.5h} colorbar{0.5h} + imgangle{0.95w, 0.5h} _ + ] + @series begin + subplot := 3 + colorbar := false + + # Disable equal aspect ratios if the scales are totally different + if max(size(imgv)...)/min(size(imgv)...) >= 7 + aspect_ratio --> :none + end + + # Note: if the axes are on unusual sides (e.g. y-axis at right, x-axis at top) + # then these coordinates are not correct. They are only correct exactly + # along the axis. + # In astropy, the ticks are actually tilted to reflect this, though in general + # the transformation from pixel to coordinates can be non-linear and curved. + + if showwcsticks + xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), dimindex(img,1), gridspec.tickpos1w)) + xguide --> ctype_label(wcs(imgv).ctype[dimindex(img,1)], wcs(imgv).radesys) + + yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), dimindex(img,2), gridspec.tickpos2w)) + yguide --> ctype_label(wcs(imgv).ctype[dimindex(img,2)], wcs(imgv).radesys) + end + + # Display a title giving our position along unplotted dimensions + if length(refdims(imgv)) > 0 + if showwcsticks + refdimslabel = join(map(refdims(imgv)) do d + # match dimension with the wcs axis number + i = findfirst(dimnames) do dim_candidate + name(dim_candidate) == name(d) + end + label = ctype_label(wcs(imgv).ctype[i], wcs(imgv).radesys) + value = pix_to_world(imgv, [1,1], all=true)[i] + unit = wcs(imgv).cunit[i] + return @sprintf("%s = %.5g %s", label, value, unit) + end) + else + refdimslabel = join(map(d->"$(name(d))= $(d[1])", refdims(imgv))) + end + title --> refdimslabel + end + + view(arraydata(imgv), reverse(axes(imgv,1)),:) + end + + if showcolorbar + layout := @layout [ + imgmag{0.95w, 0.5h} colorbar{0.5h} + imgangle{0.95w, 0.5h} colorbarangle{0.5h} + ] + colorbartitle = get(plotattributes, :colorbartitle, "") + if !haskey(plotattributes, :colorbartitle) && haskey(header(img), "UNIT") + colorbartitle = string(img[:UNIT]) + end + + @series begin + subplot := 4 + aspect_ratio := :none + colorbar := false + cbimg, _ = imview_colorbar(img; stretch=identity, clims=(-pi, pi), cmap=:cyclic_mygbm_30_95_c78_n256_s25) + xticks := [] + ymirror := true + ax = axes(cbimg,1) + yticks := ([first(ax), mean(ax), last(ax)], ["-π", "0", "π"]) + ylabel := colorbartitle + xlabel := "" + xlims := Tuple(axes(cbimg, 2)) + ylims := Tuple(axes(cbimg, 2)) + view(cbimg, reverse(axes(cbimg,1)),:) + end + end + end + return end @@ -941,4 +1027,4 @@ function wcsvecticks(w,coords,ax,minx,maxx) griduv[ax,:] .= urange posxy = world_to_pix(wsg.w, griduv) -end \ No newline at end of file +end From 6b33e54e80269f4aae7682e9cdac217ebf1d0c8c Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 27 Mar 2022 09:14:32 -0700 Subject: [PATCH 084/178] Default to more ticks --- src/imview.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/imview.jl b/src/imview.jl index a1936934..601fbef9 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -299,7 +299,7 @@ function imview_colorbar( # # Stretch the colors: # # Construct the image to use as a colorbar - # cbimg = imview(data; clims=(imgmin,imgmax), stretch, cmap) + # cbimg = imview(data; clims=(imgmin,imgmax), stretch, cmap, k_min=3) # # And the colorbar tick locations & labels # ticks, cbmin, cbmax = optimize_ticks(imgmin, imgmax) # # Now map these to pixel locations through streching and colorlimits: @@ -314,7 +314,7 @@ function imview_colorbar( # Construct the image to use as a colorbar cbimg = imview(data; clims=(imgmin,imgmax), stretch=identity, cmap) # And the colorbar tick locations & labels - ticks, cbmin, cbmax = optimize_ticks(1imgmin, 1imgmax) + ticks, cbmin, cbmax = optimize_ticks(Float64(imgmin), Float64(imgmax), k_min=3) # Now map these to pixel locations through streching and colorlimits: stretchmin = stretch(zero(eltype(data))) stretchmax = stretch(one(eltype(data))) From 52f254e7d429b1a2b8372db0a04190602140b0e8 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 27 Mar 2022 09:19:44 -0700 Subject: [PATCH 085/178] default complex colorbar titles --- src/imview.jl | 4 ++-- src/plot-recipes.jl | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/imview.jl b/src/imview.jl index 601fbef9..238cf889 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -314,12 +314,12 @@ function imview_colorbar( # Construct the image to use as a colorbar cbimg = imview(data; clims=(imgmin,imgmax), stretch=identity, cmap) # And the colorbar tick locations & labels - ticks, cbmin, cbmax = optimize_ticks(Float64(imgmin), Float64(imgmax), k_min=3) + ticks, _, _ = optimize_ticks(Float64(imgmin), Float64(imgmax), k_min=3) # Now map these to pixel locations through streching and colorlimits: stretchmin = stretch(zero(eltype(data))) stretchmax = stretch(one(eltype(data))) normedticks = clampednormedview(ticks, (imgmin, imgmax)) - ticksloc = map(ticks,normedticks) do tick, tickn + ticksloc = map(normedticks) do tickn stretched = stretch(tickn) stretchednormed = (stretched - stretchmin) * (stretchmax - stretchmin) return cbpixlen * stretchednormed diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index ca2ed2c3..e4371a45 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -41,6 +41,7 @@ cmap = plotattributes[:cmap] if T <: Complex img = abs.(data) + img["UNIT"] = "magnitude" else img = data end @@ -163,6 +164,7 @@ # TODO: refactor to reduce duplication if T <: Complex img = angle.(data) + img["UNIT"] = "angle (rad)" imgv = imview(img, clims=(-1pi, 1pi),stretch=identity, cmap=:cyclic_mygbm_30_95_c78_n256_s25) layout := @layout [ imgmag{0.95w, 0.5h} colorbar{0.5h} From f4c2934ed3dc0c013d85c812ac3ddc72f92757b7 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 27 Mar 2022 09:36:43 -0700 Subject: [PATCH 086/178] Sorted out layouts for combinations of colorbar={true,false} and eltype={complex,real} --- src/plot-recipes.jl | 103 +++++++++++++++++++++----------------------- 1 file changed, 50 insertions(+), 53 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index e4371a45..3ef3b215 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -16,6 +16,8 @@ # Show WCS coordinates if wcsticks is true or unspecified, and has at least one WCS axis present. showwcsticks = (!haskey(plotattributes, :wcsticks) || plotattributes[:wcsticks]) && !all(==(""), wcs(data).ctype) + showwcstitle = (!haskey(plotattributes, :wcstitle) || plotattributes[:wcstitle]) && + !all(==(""), wcs(data).ctype) if showwcsticks minx = first(axes(data,2)) @@ -41,6 +43,7 @@ cmap = plotattributes[:cmap] if T <: Complex img = abs.(data) + showwcsticks = false img["UNIT"] = "magnitude" else img = data @@ -59,9 +62,30 @@ showgrid = (!haskey(plotattributes, :xgrid) || haskey(plotattributes, :xgrid)) && (!haskey(plotattributes, :ygrid) || haskey(plotattributes, :ygrid)) + # Display a title giving our position along unplotted dimensions + if length(refdims(imgv)) > 0 + if showwcstitle + refdimslabel = join(map(refdims(imgv)) do d + # match dimension with the wcs axis number + i = findfirst(dimnames) do dim_candidate + name(dim_candidate) == name(d) + end + label = ctype_label(wcs(imgv).ctype[i], wcs(imgv).radesys) + value = pix_to_world(imgv, [1,1], all=true)[i] + unit = wcs(imgv).cunit[i] + return @sprintf("%s = %.5g %s", label, value, unit) + end) + else + refdimslabel = join(map(d->"$(name(d))= $(d[1])", refdims(imgv))) + end + title --> refdimslabel + end + + subplot_i = 0 # Actual image series (RGB pixels by this point) @series begin - subplot := 1 + subplot_i += 1 + subplot := subplot_i colorbar := false @@ -84,25 +108,6 @@ yguide --> ctype_label(wcs(imgv).ctype[dimindex(img,2)], wcs(imgv).radesys) end - # Display a title giving our position along unplotted dimensions - if length(refdims(imgv)) > 0 - if showwcsticks - refdimslabel = join(map(refdims(imgv)) do d - # match dimension with the wcs axis number - i = findfirst(dimnames) do dim_candidate - name(dim_candidate) == name(d) - end - label = ctype_label(wcs(imgv).ctype[i], wcs(imgv).radesys) - value = pix_to_world(imgv, [1,1], all=true)[i] - unit = wcs(imgv).cunit[i] - return @sprintf("%s = %.5g %s", label, value, unit) - end) - else - refdimslabel = join(map(d->"$(name(d))= $(d[1])", refdims(imgv))) - end - title --> refdimslabel - end - view(arraydata(imgv), reverse(axes(imgv,1)),:) end @@ -129,23 +134,38 @@ wcsg, gridspec end end + # Disable the colorbar. # Plots.jl does not give us sufficient control to make sure the range and ticks # are correct after applying a non-linear stretch. # We attempt to make our own colorbar using a second plot. showcolorbar = !(T <: Colorant) && get(plotattributes, :colorbar, true) != :none - if showcolorbar + if T <: Complex layout := @layout [ - img{0.95w} colorbar + imgmag{0.5h} + imgangle{0.5h} ] + end + if showcolorbar + if T <: Complex + layout := @layout [ + imgmag{0.95w, 0.5h} colorbar{0.5h} + imgangle{0.95w, 0.5h} colorbarangle{0.5h} + ] + else + layout := @layout [ + img{0.95w} colorbar + ] + end colorbartitle = get(plotattributes, :colorbartitle, "") if !haskey(plotattributes, :colorbartitle) && haskey(header(img), "UNIT") colorbartitle = string(img[:UNIT]) end + subplot_i += 1 @series begin - subplot := 2 + subplot := subplot_i aspect_ratio := :none colorbar := false cbimg, cbticks = imview_colorbar(img; clims, stretch, cmap) @@ -156,6 +176,7 @@ xlabel := "" xlims := Tuple(axes(cbimg, 2)) ylims := Tuple(axes(cbimg, 2)) + title := "" view(cbimg, reverse(axes(cbimg,1)),:) end end @@ -166,13 +187,11 @@ img = angle.(data) img["UNIT"] = "angle (rad)" imgv = imview(img, clims=(-1pi, 1pi),stretch=identity, cmap=:cyclic_mygbm_30_95_c78_n256_s25) - layout := @layout [ - imgmag{0.95w, 0.5h} colorbar{0.5h} - imgangle{0.95w, 0.5h} _ - ] @series begin - subplot := 3 + subplot_i += 1 + subplot := subplot_i colorbar := false + title := "" # Disable equal aspect ratios if the scales are totally different if max(size(imgv)...)/min(size(imgv)...) >= 7 @@ -192,41 +211,18 @@ yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), dimindex(img,2), gridspec.tickpos2w)) yguide --> ctype_label(wcs(imgv).ctype[dimindex(img,2)], wcs(imgv).radesys) end - - # Display a title giving our position along unplotted dimensions - if length(refdims(imgv)) > 0 - if showwcsticks - refdimslabel = join(map(refdims(imgv)) do d - # match dimension with the wcs axis number - i = findfirst(dimnames) do dim_candidate - name(dim_candidate) == name(d) - end - label = ctype_label(wcs(imgv).ctype[i], wcs(imgv).radesys) - value = pix_to_world(imgv, [1,1], all=true)[i] - unit = wcs(imgv).cunit[i] - return @sprintf("%s = %.5g %s", label, value, unit) - end) - else - refdimslabel = join(map(d->"$(name(d))= $(d[1])", refdims(imgv))) - end - title --> refdimslabel - end - view(arraydata(imgv), reverse(axes(imgv,1)),:) end if showcolorbar - layout := @layout [ - imgmag{0.95w, 0.5h} colorbar{0.5h} - imgangle{0.95w, 0.5h} colorbarangle{0.5h} - ] colorbartitle = get(plotattributes, :colorbartitle, "") if !haskey(plotattributes, :colorbartitle) && haskey(header(img), "UNIT") colorbartitle = string(img[:UNIT]) end @series begin - subplot := 4 + subplot_i += 1 + subplot := subplot_i aspect_ratio := :none colorbar := false cbimg, _ = imview_colorbar(img; stretch=identity, clims=(-pi, pi), cmap=:cyclic_mygbm_30_95_c78_n256_s25) @@ -238,6 +234,7 @@ xlabel := "" xlims := Tuple(axes(cbimg, 2)) ylims := Tuple(axes(cbimg, 2)) + title := "" view(cbimg, reverse(axes(cbimg,1)),:) end end From dbb93f1b0c35d630fd44d3f896bcb165acac995e Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 27 Mar 2022 09:56:35 -0700 Subject: [PATCH 087/178] Fixed logic for colorbar title --- src/plot-recipes.jl | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 3ef3b215..8ffa0f13 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -43,7 +43,6 @@ cmap = plotattributes[:cmap] if T <: Complex img = abs.(data) - showwcsticks = false img["UNIT"] = "magnitude" else img = data @@ -158,9 +157,9 @@ img{0.95w} colorbar ] end - colorbartitle = get(plotattributes, :colorbartitle, "") - if !haskey(plotattributes, :colorbartitle) && haskey(header(img), "UNIT") - colorbartitle = string(img[:UNIT]) + colorbar_title = get(plotattributes, :colorbar_title, "") + if !haskey(plotattributes, :colorbar_title) && haskey(header(img), "UNIT") + colorbar_title = string(img[:UNIT]) end subplot_i += 1 @@ -172,7 +171,7 @@ xticks := [] ymirror := true yticks := cbticks - ylabel := colorbartitle + ylabel := colorbar_title xlabel := "" xlims := Tuple(axes(cbimg, 2)) ylims := Tuple(axes(cbimg, 2)) @@ -215,9 +214,9 @@ end if showcolorbar - colorbartitle = get(plotattributes, :colorbartitle, "") - if !haskey(plotattributes, :colorbartitle) && haskey(header(img), "UNIT") - colorbartitle = string(img[:UNIT]) + colorbar_title = get(plotattributes, :colorbar_title, "") + if !haskey(plotattributes, :colorbar_title) && haskey(header(img), "UNIT") + colorbar_title = string(img[:UNIT]) end @series begin @@ -230,7 +229,7 @@ ymirror := true ax = axes(cbimg,1) yticks := ([first(ax), mean(ax), last(ax)], ["-π", "0", "π"]) - ylabel := colorbartitle + ylabel := colorbar_title xlabel := "" xlims := Tuple(axes(cbimg, 2)) ylims := Tuple(axes(cbimg, 2)) @@ -238,6 +237,7 @@ view(cbimg, reverse(axes(cbimg,1)),:) end end + end From 41804d4cb87fa2a328535e94a18ed1ab3ba3d685 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 27 Mar 2022 10:09:31 -0700 Subject: [PATCH 088/178] Allow grid to adapt to fill user's xlims and ylims --- src/plot-recipes.jl | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 8ffa0f13..d4a99caa 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -26,7 +26,14 @@ maxy = last(axes(data,1)) extent = (minx-0.5, maxx+0.5, miny-0.5, maxy+0.5) - wcsg = WCSGrid(data, extent) + if haskey(plotattributes, :xlims) + extent = (plotattributes[:xlims]..., extent[3:4]...) + end + if haskey(plotattributes, :ylims) + extent = (extent[1:2]..., plotattributes[:ylims]...) + end + + wcsg = WCSGrid(data, Float64.(extent)) gridspec = wcsgridspec(wcsg) end @@ -34,6 +41,7 @@ clims --> _default_clims[] stretch --> _default_stretch[] cmap --> _default_cmap[] + grid := false if T <: Colorant imgv = data @@ -127,8 +135,8 @@ # tight to the image xl = first(axes(imgv,2))-0.5, last(axes(imgv,2))+0.5 yl = first(axes(imgv,1))-0.5, last(axes(imgv,1))+0.5 - ylims := yl - xlims := xl + ylims --> yl + xlims --> xl wcsg, gridspec end From 30d464a13946917b67b69a423960a6d50efa8325 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 27 Mar 2022 10:16:26 -0700 Subject: [PATCH 089/178] Disable equal aspectratios when... ... displayed limits have a natural aspect ratio > 7. --- src/plot-recipes.jl | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index d4a99caa..795551fb 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -18,21 +18,20 @@ !all(==(""), wcs(data).ctype) showwcstitle = (!haskey(plotattributes, :wcstitle) || plotattributes[:wcstitle]) && !all(==(""), wcs(data).ctype) - if showwcsticks - - minx = first(axes(data,2)) - maxx = last(axes(data,2)) - miny = first(axes(data,1)) - maxy = last(axes(data,1)) - extent = (minx-0.5, maxx+0.5, miny-0.5, maxy+0.5) - if haskey(plotattributes, :xlims) - extent = (plotattributes[:xlims]..., extent[3:4]...) - end - if haskey(plotattributes, :ylims) - extent = (extent[1:2]..., plotattributes[:ylims]...) - end + minx = first(axes(data,2)) + maxx = last(axes(data,2)) + miny = first(axes(data,1)) + maxy = last(axes(data,1)) + extent = (minx-0.5, maxx+0.5, miny-0.5, maxy+0.5) + if haskey(plotattributes, :xlims) + extent = (plotattributes[:xlims]..., extent[3:4]...) + end + if haskey(plotattributes, :ylims) + extent = (extent[1:2]..., plotattributes[:ylims]...) + end + if showwcsticks wcsg = WCSGrid(data, Float64.(extent)) gridspec = wcsgridspec(wcsg) end @@ -63,6 +62,14 @@ yflip := false xflip := false + + # Disable equal aspect ratios if the scales are totally different + displayed_data_ratio = (extent[2]-extent[1])/(extent[4]-extent[3]) + if displayed_data_ratio >= 7 + aspect_ratio --> :none + end + + # we have a wcs flag (from the image by default) so that users can skip over # plotting in physical coordinates. This is especially important # if the WCS headers are mallformed in some way. @@ -96,10 +103,7 @@ colorbar := false - # Disable equal aspect ratios if the scales are totally different - if max(size(imgv)...)/min(size(imgv)...) >= 7 - aspect_ratio --> :none - end + # Note: if the axes are on unusual sides (e.g. y-axis at right, x-axis at top) # then these coordinates are not correct. They are only correct exactly @@ -200,10 +204,6 @@ colorbar := false title := "" - # Disable equal aspect ratios if the scales are totally different - if max(size(imgv)...)/min(size(imgv)...) >= 7 - aspect_ratio --> :none - end # Note: if the axes are on unusual sides (e.g. y-axis at right, x-axis at top) # then these coordinates are not correct. They are only correct exactly From 69f132581b71b5dee3838fbb19386b26fd6a2403 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 27 Mar 2022 10:36:59 -0700 Subject: [PATCH 090/178] Fix `percent` on matrices --- src/imview.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/imview.jl b/src/imview.jl index 238cf889..1b260912 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -22,8 +22,8 @@ This will set the limits to be the 5th percentile to the 95th percentile. """ function percent(perc::Number) trim = (1 - perc/100)/2 - clims(data::AbstractVector) = quantile(data, (trim, 1-trim)) - clims(data::AbstractArray) = clims(vec(data)) + clims(data::AbstractMatrix) = quantile(vec(data), (trim, 1-trim)) + clims(data) = quantile(data, (trim, 1-trim)) return clims end From 519b98bf3c3f136ce480877250000fff6485c8f6 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 28 Mar 2022 16:55:19 -0700 Subject: [PATCH 091/178] Support for standalone labelled dims... autonaming of Wcs dims first crack at polarization plotting --- src/AstroImages.jl | 207 ++++++++++++++++++++++++++++---------------- src/imview.jl | 6 +- src/io.jl | 152 ++++++++++++++++++++++---------- src/plot-recipes.jl | 117 +++++++++++++++---------- src/wcs.jl | 39 ++------- 5 files changed, 319 insertions(+), 202 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index e3e82ce1..d6255ddf 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -27,6 +27,7 @@ export load, AstroImage, AstroImageVec, AstroImageMat, + Wcs, WCSGrid, ccd2rgb, composechannels, @@ -115,13 +116,24 @@ const dimnames = ( (Dim{i} for i in 4:10)... ) +const Spec = Dim{:Spec} +const Pol = Dim{:Pol} +struct Wcs{N,T} <: DimensionalData.Dimension{T} + val::T +end +Wcs{N}(val::T) where {N,T} = Wcs{N,T}(val) +Wcs{N}() where N = Wcs{N}(:) +DimensionalData.name(::Type{<:Wcs{N}}) where N = Symbol("Wcs$N") +DimensionalData.basetypeof(::Type{<:Wcs{N}}) where N = Wcs{N} +# DimensionalData.key2dim(::Val{N}) where N<:Integer = Wcs{N}() +DimensionalData.dim2key(::Type{D}) where D<:Wcs{N} where N = Symbol("Wcs$N") +wcsax(::Wcs{N}) where N = N +export Spec, Pol, Wcs +# TODO: Sep? # Accessors -""" - ImageMetadata.arraydata(img::AstroImage) -""" -ImageMetadata.arraydata(img::AstroImage) = getfield(img, :data) header(img::AstroImage) = getfield(img, :header) +header(::AbstractArray) = emptyheader() function wcs(img::AstroImage) if getfield(img, :wcs_stale)[] getfield(img, :wcs)[] = wcsfromheader(img) @@ -129,9 +141,16 @@ function wcs(img::AstroImage) end return getfield(img, :wcs)[] end +wcs(arr::AbstractArray) = emptywcs(arr) +""" + ImageMetadata.arraydata(img::AstroImage) + +Returns the underlying wrapped array of `img`. +""" +ImageMetadata.arraydata(img::AstroImage) = getfield(img, :data) -# Implement DimensionalData interface +# Implement DimensionalData interface Base.parent(img::AstroImage) = arraydata(img) DimensionalData.dims(A::AstroImage) = getfield(A, :dims) DimensionalData.refdims(A::AstroImage) = getfield(A, :refdims) @@ -177,60 +196,6 @@ for f in [ @eval ($f)(img::AstroImage) = shareheader(img, $f(arraydata(img))) end -""" - AstroImage(fits::FITS, ext::Int=1) - -Given an open FITS file from the FITSIO library, -load the HDU number `ext` as an AstroImage. -""" -AstroImage(fits::FITS, ext::Int=1) = AstroImage(fits[ext]) - -""" - AstroImage(hdu::HDU) - -Given an open FITS HDU, load it as an AstroImage. -""" -AstroImage(hdu::HDU) = AstroImage(read(hdu), read_header(hdu)) - -""" - img = AstroImage(filename::AbstractString, ext::Integer=1) - -Load an image HDU `ext` from the FITS file at `filename` as an AstroImage. -""" -function AstroImage(filename::AbstractString, ext::Integer=1) - return FITS(filename,"r") do fits - return AstroImage(fits[ext]) - end -end -""" - img1, img2 = AstroImage(filename::AbstractString, exts) - -Load multiple image HDUs `exts` from an FITS file at `filename` as an AstroImage. -`exts` must be a tuple, range, :, or array of Integers. -All listed HDUs in `exts` must be image HDUs or an error will occur. - -Example: -```julia -img1, img2 = AstroImage("abc.fits", (1,3)) # loads the first and third HDU as images. -imgs = AstroImage("abc.fits", 1:3) # loads the first three HDUs as images. -imgs = AstroImage("abc.fits", :) # loads all HDUs as images. -``` -""" -function AstroImage(filename::AbstractString, exts::Union{NTuple{N, <:Integer},AbstractArray{<:Integer}}) where {N} - return FITS(filename,"r") do fits - return map(exts) do ext - return AstroImage(fits[ext]) - end - end -end -function AstroImage(filename::AbstractString, ::Colon) where {N} - return FITS(filename,"r") do fits - return map(fits) do hdu - return AstroImage(hdu) - end - end -end - """ AstroImage(img::AstroImage) @@ -241,6 +206,53 @@ AstroImage before proceeding. AstroImage(img::AstroImage) = img +# """ +# AstroImage(data::AbstractArray, [header::FITSHeader,] [wcs::WCSTransform,]) + +# Create an AstroImage from an array, and optionally header or header and a +# WCSTransform. +# """ +# function AstroImage( +# data::AbstractArray{T,N}, +# header::FITSHeader=emptyheader(), +# wcs::Union{WCSTransform,Nothing}=nothing +# ) where {T, N} +# wcs_stale = isnothing(wcs) +# if isnothing(wcs) +# wcs = emptywcs(data) +# end +# # If the user passes in a WCSTransform of their own, we use it and mark +# # wcs_stale=false. It will be kept unless they manually change a WCS header. +# # If they don't pass anything, we start with empty WCS information regardless +# # of what's in the header but we mark it as stale. +# # If/when the WCS info is accessed via `wcs(img)` it will be computed and cached. +# # This avoids those computations if the WCS transform is not needed. +# # It also allows us to create images with invalid WCS header, +# # only erroring when/if they are used. + +# # Fields for DimensionalData. +# # Name dimensions always as X,Y,Z, then Dim{4}, Dim{5}, etc. +# # If we wanted to do something smarter e.g. time axes we would have +# # to look at the WCSTransform, and we want to avoid doing this on construction +# # for the reasons described above. +# dimnames = ( +# X, Y, Z +# )[1:min(3,N)] +# if N > 3 +# dimnames = ( +# dimnames..., +# (Dim{i} for i in 4:N)... +# ) +# end +# dimaxes = map(dimnames, axes(data)) do dim, ax +# dim(ax) +# end +# dims = DimensionalData.format(dimaxes, data) +# refdims = () + +# return AstroImage(data, dims, refdims, header, Ref(wcs), Ref(wcs_stale)) +# end + """ AstroImage(data::AbstractArray, [header::FITSHeader,] [wcs::WCSTransform,]) @@ -249,8 +261,11 @@ WCSTransform. """ function AstroImage( data::AbstractArray{T,N}, + dims::Union{Tuple,NamedTuple}=(), + refdims::Union{Tuple,NamedTuple}=(), header::FITSHeader=emptyheader(), - wcs::Union{WCSTransform,Nothing}=nothing + wcs::Union{WCSTransform,Nothing}=nothing; + wcsdims=false ) where {T, N} wcs_stale = isnothing(wcs) if isnothing(wcs) @@ -266,27 +281,71 @@ function AstroImage( # only erroring when/if they are used. # Fields for DimensionalData. + if dims == () + if wcsdims + ourdims = Tuple(Wcs{i} for i in 1:ndims(data)) + else + ourdims = dimnames[1:ndims(data)] + end + dimaxes = map(ourdims, axes(data)) do dim, ax + dim(ax) + end + dims = DimensionalData.format(dimaxes, data) + else + dims = DimensionalData.format(dims, data) + end + + + if length(dims) != ndims(data) + error("Number of dims does not match the shape of the data") + end + # Name dimensions always as X,Y,Z, then Dim{4}, Dim{5}, etc. # If we wanted to do something smarter e.g. time axes we would have # to look at the WCSTransform, and we want to avoid doing this on construction # for the reasons described above. - dimnames = ( - X, Y, Z - )[1:min(3,N)] - if N > 3 - dimnames = ( - dimnames..., - (Dim{i} for i in 4:N)... - ) - end - dimaxes = map(dimnames, axes(data)) do dim, ax - dim(ax) - end - dims = DimensionalData.format(dimaxes, data) - refdims = () + # dimnames = ( + # X, Y, Z + # )[1:min(3,N)] + # if N > 3 + # dimnames = ( + # dimnames..., + # (Dim{i} for i in 4:N)... + # ) + # end + # dimaxes = map(dimnames, axes(data)) do dim, ax + # dim(ax) + # end + # dims = DimensionalData.format(dimaxes, data) + # refdims = () return AstroImage(data, dims, refdims, header, Ref(wcs), Ref(wcs_stale)) end +function AstroImage( + darr::AbstractDimArray, + header::FITSHeader=emptyheader(), + wcs::Union{WCSTransform,Nothing}=nothing; + wcsdims=false +) + wcs_stale = isnothing(wcs) + if isnothing(wcs) + wcs = emptywcs(darr) + end + return AstroImage(parent(darr), dims(darr), refdims(darr), header, Ref(wcs), Ref(wcs_stale)) +end +AstroImage( + data::AbstractArray, + header::FITSHeader, + wcs::Union{WCSTransform,Nothing}=nothing; + wcsdims=false +) = AstroImage(data, (), (), header, wcs; wcsdims) +# AstroImage( +# data::AbstractArray, +# header::FITSHeader=emptyheader(), +# wcs::Union{WCSTransform,Nothing}=nothing; +# wcsdims=false +# ) = AstroImage(data, dimnames[begin:begin+ndims(data)], (), header, wcs; wcsdims) +# TODO: ensure this gets WCS dims. AstroImage(data::AbstractArray, wcs::WCSTransform) = AstroImage(data, emptyheader(), wcs) diff --git a/src/imview.jl b/src/imview.jl index 1b260912..058c7f6f 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -324,6 +324,8 @@ function imview_colorbar( stretchednormed = (stretched - stretchmin) * (stretchmax - stretchmin) return cbpixlen * stretchednormed end - - return cbimg, (ticksloc, string.(ticks)) + ticklabels = map(ticks) do t + @sprintf("%4g", t) + end + return cbimg, (ticksloc, ticklabels) end diff --git a/src/io.jl b/src/io.jl index 0e257069..b3d6860c 100644 --- a/src/io.jl +++ b/src/io.jl @@ -1,88 +1,144 @@ + +""" +AstroImage(fits::FITS, ext::Int=1) + +Given an open FITS file from the FITSIO library, +load the HDU number `ext` as an AstroImage. +""" +AstroImage(fits::FITS, ext::Int=1; wcsdims=false) = AstroImage(fits[ext]; wcsdims) + +""" +AstroImage(hdu::HDU) + +Given an open FITS HDU, load it as an AstroImage. +""" +AstroImage(hdu::HDU; wcsdims=false) = AstroImage(read(hdu), read_header(hdu); wcsdims) + """ -load(fitsfile::String) +img = AstroImage(filename::AbstractString, ext::Integer=1) + +Load an image HDU `ext` from the FITS file at `filename` as an AstroImage. +""" +function AstroImage(filename::AbstractString, ext::Integer=1; wcsdims=false) + return FITS(filename, "r") do fits + return AstroImage(fits[ext]; wcsdims) + end +end +""" +img1, img2 = AstroImage(filename::AbstractString, exts) + +Load multiple image HDUs `exts` from an FITS file at `filename` as an AstroImage. +`exts` must be a tuple, range, :, or array of Integers. +All listed HDUs in `exts` must be image HDUs or an error will occur. + +Example: +```julia +img1, img2 = AstroImage("abc.fits", (1,3)) # loads the first and third HDU as images. +imgs = AstroImage("abc.fits", 1:3) # loads the first three HDUs as images. +imgs = AstroImage("abc.fits", :) # loads all HDUs as images. +``` +""" +function AstroImage(filename::AbstractString, exts::Union{NTuple{N,<:Integer},AbstractArray{<:Integer}}; wcsdims=false) where {N} + return FITS(filename, "r") do fits + return map(exts) do ext + return AstroImage(fits[ext]; wcsdims) + end + end +end +function AstroImage(filename::AbstractString, ::Colon; wcsdims=false) where {N} + return FITS(filename, "r") do fits + return map(fits) do hdu + return AstroImage(hdu; wcsdims) + end + end +end + + +""" +load(fitsfile::String; wcsdims=false) Read and return the data from the first ImageHDU in a FITS file as an AstroImage. If no ImageHDUs are present, an error is returned. -load(fitsfile::String, ext::Int) +load(fitsfile::String, ext::Int; wcsdims=false) Read and return the data from the HDU `ext`. If it is an ImageHDU, as AstroImage is returned. If it is a TableHDU, a plain Julia column table is returned. -load(fitsfile::String, :) +load(fitsfile::String, :; wcsdims=false) Read and return the data from each HDU in an FITS file. ImageHDUs are returned as AstroImage, and TableHDUs are returned as column tables. -load(fitsfile::String, exts::Union{NTuple, AbstractArray}) +load(fitsfile::String, exts::Union{NTuple, AbstractArray}; wcsdims=false) Read and return the data from the HDUs given by `exts`. ImageHDUs are returned as AstroImage, and TableHDUs are returned as column tables. !! Currently comments on TableHDUs are not supported and are ignored. """ -function fileio_load(f::File{format"FITS"}, ext::Union{Int,Nothing}=nothing) where N -return FITS(f.filename, "r") do fits - if isnothing(ext) - ext = indexer(fits) +function fileio_load(f::File{format"FITS"}, ext::Union{Int,Nothing}=nothing; wcsdims=false) where {N} + return FITS(f.filename, "r") do fits + if isnothing(ext) + ext = indexer(fits) + end + _loadhdu(fits[ext]; wcsdims) end - _loadhdu(fits[ext]) -end end -function fileio_load(f::File{format"FITS"}, exts::Union{NTuple{N, <:Integer},AbstractArray{<:Integer}}) where N -return FITS(f.filename, "r") do fits - map(exts) do ext - _loadhdu(fits[ext]) +function fileio_load(f::File{format"FITS"}, exts::Union{NTuple{N,<:Integer},AbstractArray{<:Integer}}; wcsdims=false) where {N} + return FITS(f.filename, "r") do fits + map(exts) do ext + _loadhdu(fits[ext]; wcsdims) + end end end -end -function fileio_load(f::File{format"FITS"}, exts::Colon) where N -return FITS(f.filename, "r") do fits - exts_resolved = 1:length(fits) - map(exts_resolved) do ext - _loadhdu(fits[ext]) +function fileio_load(f::File{format"FITS"}, ::Colon; wcsdims=false) where {N} + return FITS(f.filename, "r") do fits + exts_resolved = 1:length(fits) + map(exts_resolved) do ext + _loadhdu(fits[ext]; wcsdims) + end end end -end _loadhdu(hdu::FITSIO.TableHDU) = Tables.columntable(hdu) -function _loadhdu(hdu::FITSIO.ImageHDU) -if size(hdu) != () - return AstroImage(hdu) -else - # Sometimes files have an empty data HDU that shows up as an image HDU but has headers. - # Fallback to creating an empty AstroImage with those headers. - emptydata = fill(0, (0,0)) - return AstroImage(emptydata, (), (), read_header(hdu), Ref(emptywcs(emptydata)), Ref(false)) -end +function _loadhdu(hdu::FITSIO.ImageHDU; wcsdims=false) + if size(hdu) != () + return AstroImage(hdu; wcsdims) + else + # Sometimes files have an empty data HDU that shows up as an image HDU but has headers. + # Fallback to creating an empty AstroImage with those headers. + emptydata = fill(0, (0, 0)) + return AstroImage(emptydata, (), (), read_header(hdu), Ref(emptywcs(emptydata)), Ref(false)) + end end function indexer(fits::FITS) -ext = 0 -for (i, hdu) in enumerate(fits) - if hdu isa ImageHDU && length(size(hdu)) >= 2 # check if Image is atleast 2D - ext = i - break + ext = 0 + for (i, hdu) in enumerate(fits) + if hdu isa ImageHDU && length(size(hdu)) >= 2# check if Image is atleast 2D + ext = i + break + end end + if ext > 1 + @info "Image was loaded from HDU $ext" + elseif ext == 0 + error("There are no ImageHDU extensions in '$(fits.filename)'") + end + return ext end -if ext > 1 - @info "Image was loaded from HDU $ext" -elseif ext == 0 - error("There are no ImageHDU extensions in '$(fits.filename)'") -end -return ext -end -indexer(fits::NTuple{N, FITS}) where {N} = ntuple(i -> indexer(fits[i]), N) +indexer(fits::NTuple{N,FITS}) where {N} = ntuple(i -> indexer(fits[i]), N) # Fallback for saving arbitrary arrays function fileio_save(f::File{format"FITS"}, args...) -FITS(f.filename, "w") do fits - for arg in args - writearg(fits, arg) + FITS(f.filename, "w") do fits + for arg in args + writearg(fits, arg) + end end end -end writearg(fits, img::AstroImage) = write(fits, arraydata(img), header=header(img)) # Fallback for writing plain arrays writearg(fits, arr::AbstractArray) = write(fits, arr) @@ -100,7 +156,7 @@ function writearg(fits, table) fits, colname_strings, columns; - hdutype=TableHDU, + hdutype=TableHDU # TODO: In future, we want to be able to access and round-trip coments # on table HDUs # header=nothing diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 795551fb..2418c874 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -9,14 +9,16 @@ end T = eltype(data) if ndims(data) != 2 - error("Image passed to `implot` must be two-dimensional. Got ndims(img)=$(ndims(img))") + error("Image passed to `implot` must be two-dimensional. Got ndims(img)=$(ndims(data))") end - # Show WCS coordinates if wcsticks is true or unspecified, and has at least one WCS axis present. showwcsticks = (!haskey(plotattributes, :wcsticks) || plotattributes[:wcsticks]) && + any(d->typeof(d) <: Wcs, dims(data)) && !all(==(""), wcs(data).ctype) showwcstitle = (!haskey(plotattributes, :wcstitle) || plotattributes[:wcstitle]) && + length(refdims(data)) > 0 && + any(d->typeof(d) <: Wcs, dims(data)) && !all(==(""), wcs(data).ctype) @@ -40,7 +42,12 @@ clims --> _default_clims[] stretch --> _default_stretch[] cmap --> _default_cmap[] + grid := false + # In most cases, a grid framestyle is a nicer looking default for images + # but the user can override. + framestyle --> :box + if T <: Colorant imgv = data @@ -81,16 +88,14 @@ if showwcstitle refdimslabel = join(map(refdims(imgv)) do d # match dimension with the wcs axis number - i = findfirst(dimnames) do dim_candidate - name(dim_candidate) == name(d) - end + i = wcsax(d) label = ctype_label(wcs(imgv).ctype[i], wcs(imgv).radesys) value = pix_to_world(imgv, [1,1], all=true)[i] unit = wcs(imgv).cunit[i] return @sprintf("%s = %.5g %s", label, value, unit) - end) + end, ", ") else - refdimslabel = join(map(d->"$(name(d))= $(d[1])", refdims(imgv))) + refdimslabel = join(map(d->"$(name(d))= $(d[1])", refdims(imgv)), ", ") end title --> refdimslabel end @@ -112,14 +117,19 @@ # the transformation from pixel to coordinates can be non-linear and curved. if showwcsticks - xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), dimindex(img,1), gridspec.tickpos1w)) - xguide --> ctype_label(wcs(imgv).ctype[dimindex(img,1)], wcs(imgv).radesys) + xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), wcsax(dims(img,1)), gridspec.tickpos1w)) + xguide --> ctype_label(wcs(imgv).ctype[wcsax(dims(img,1))], wcs(imgv).radesys) - yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), dimindex(img,2), gridspec.tickpos2w)) - yguide --> ctype_label(wcs(imgv).ctype[dimindex(img,2)], wcs(imgv).radesys) + yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), wcsax(dims(img,2)), gridspec.tickpos2w)) + yguide --> ctype_label(wcs(imgv).ctype[wcsax(dims(img,2))], wcs(imgv).radesys) + + view(parent(imgv), reverse(axes(imgv,1)),:) end - view(arraydata(imgv), reverse(axes(imgv,1)),:) + ax1 = collect(parent(dims(imgv,1))) + ax2 = collect(parent(dims(imgv,2))) + + ax1, ax2, view(parent(imgv), reverse(axes(imgv,1)),:) end # If wcs=true (default) and grid=true (not default), overplot a WCS @@ -183,8 +193,8 @@ xticks := [] ymirror := true yticks := cbticks - ylabel := colorbar_title - xlabel := "" + yguide := colorbar_title + xguide := "" xlims := Tuple(axes(cbimg, 2)) ylims := Tuple(axes(cbimg, 2)) title := "" @@ -212,13 +222,13 @@ # the transformation from pixel to coordinates can be non-linear and curved. if showwcsticks - xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), dimindex(img,1), gridspec.tickpos1w)) - xguide --> ctype_label(wcs(imgv).ctype[dimindex(img,1)], wcs(imgv).radesys) + xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), wcsax(dims(img,1)), gridspec.tickpos1w)) + xguide --> ctype_label(wcs(imgv).ctype[wcsax(dims(img,1))], wcs(imgv).radesys) - yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), dimindex(img,2), gridspec.tickpos2w)) - yguide --> ctype_label(wcs(imgv).ctype[dimindex(img,2)], wcs(imgv).radesys) + yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), wcsax(dims(img,2)), gridspec.tickpos2w)) + yguide --> ctype_label(wcs(imgv).ctype[wcsax(dims(img,2))], wcs(imgv).radesys) end - view(arraydata(imgv), reverse(axes(imgv,1)),:) + view(parent(imgv), reverse(axes(imgv,1)),:) end if showcolorbar @@ -227,6 +237,7 @@ colorbar_title = string(img[:UNIT]) end + @series begin subplot_i += 1 subplot := subplot_i @@ -237,8 +248,8 @@ ymirror := true ax = axes(cbimg,1) yticks := ([first(ax), mean(ax), last(ax)], ["-π", "0", "π"]) - ylabel := colorbar_title - xlabel := "" + yguide := colorbar_title + xguide := "" xlims := Tuple(axes(cbimg, 2)) ylims := Tuple(axes(cbimg, 2)) title := "" @@ -496,8 +507,8 @@ end end annotate = haskey(plotattributes, :gridlabels) && plotattributes[:gridlabels] - xguide --> ctype_label(wcs(wcsg.img).ctype[dimindex(wcsg.img,1)], wcs(wcsg.img).radesys) - yguide --> ctype_label(wcs(wcsg.img).ctype[dimindex(wcsg.img,2)], wcs(wcsg.img).radesys) + xguide --> ctype_label(wcs(wcsg.img).ctype[wcsax(dims(wcsg.img,1))], wcs(wcsg.img).radesys) + yguide --> ctype_label(wcs(wcsg.img).ctype[wcsax(dims(wcsg.img,2))], wcs(wcsg.img).radesys) xlims --> wcsg.extent[1], wcsg.extent[2] ylims --> wcsg.extent[3], wcsg.extent[4] @@ -1008,30 +1019,44 @@ end -function wcsvecticks(w,coords,ax,minx,maxx) - # x and y denote pixel coordinates (along `ax`), u and v are world coordinates roughly along same. - coordsx = convert(Vector{Float64}, collect(coords)) - # Find the extent of this slice in world coordinates - posxy = repeat(coordsx, 1, 2) - posxy[ax,1] = minx - posxy[ax,2] = maxx - posuv = pix_to_world(w, posxy) - minu, maxu = extrema(posuv[ax,:]) +@userplot PolQuiver +@recipe function f(h::PolQuiver) + cube = only(h.args) + bin = get(plotattributes, :bin, 4) + polintenfrac = get(plotattributes, :polintenfrac, 0.1) - # Find nice grid spacings using PlotUtils.optimize_ticks - # These heuristics can probably be improved - # TODO: this does not handle coordinates that wrap around - Q=[(1.0,1.0), (3.0, 0.8), (2.0, 0.7), (5.0, 0.5)] - k_min = 3 - k_ideal = 5 - k_max = 10 + i = cube[Pol=At(:I)] + q = cube[Pol=At(:Q)] + u = cube[Pol=At(:U)] + polinten = @. sqrt(q^2 + u^2) + linpolfrac = polinten ./ i - tickpos2x = Float64[] - tickpos2w = Float64[] - tickposv = optimize_ticks(6minv, 6maxv; Q, k_min, k_ideal, k_max)[1]./6 - griduv = posuv[:,1] - griduv[ax,:] .= urange - posxy = world_to_pix(wsg.w, griduv) + binratio=1/bin + xs = imresize([x for x in dims(cube,1), y in dims(cube,2)], ratio=binratio) + ys = imresize([y for x in dims(cube,1), y in dims(cube,2)], ratio=binratio) + qx = imresize(q, ratio=binratio) + qy = imresize(u, ratio=binratio) + qlinpolfrac = imresize(linpolfrac, ratio=binratio) + qpolintenr = imresize(polinten, ratio=binratio) -end + mask = isfinite.(qpolintenr) .&& qpolintenr .> polintenfrac * maximum(filter(isfinite,qpolintenr)) + a = 20 / maximum(filter(isfinite,qpolintenr)) + pointstmp = map(xs[mask],ys[mask],qx[mask],qy[mask]) do x,y,qxi,qyi + return ([x, x+a*qxi, NaN], [y, y+a*qyi, NaN]) + end + xs = reduce(vcat, getindex.(pointstmp, 1)) + ys = reduce(vcat, getindex.(pointstmp, 2)) + + colors = qlinpolfrac[mask] + if !isnothing(colors) + line_z := repeat(colors, inner=3) + end + + label --> "" + color --> :turbo + + @series begin + xs, ys + end +end \ No newline at end of file diff --git a/src/wcs.jl b/src/wcs.jl index e7003ae3..96238613 100644 --- a/src/wcs.jl +++ b/src/wcs.jl @@ -384,15 +384,11 @@ function WCS.pix_to_world(img::AstroImage, pixcoords; all=false) # output to only include the dims of the current slice? # out = zeros(Float64, length(dims(img))+length(refdims(img)), size(pixcoords,2)) for (i, dim) in enumerate(dims(img)) - j = findfirst(dimnames) do dim_candidate - name(dim_candidate) == name(dim) - end + j = wcsax(dim) parentcoords_prepared[j,:] .= parentcoords[i,:] end for dim in refdims(img) - j = findfirst(dimnames) do dim_candidate - name(dim_candidate) == name(dim) - end + j = wcsax(dim) parentcoords_prepared[j,:] .= dim[1] end @@ -412,9 +408,7 @@ function WCS.pix_to_world(img::AstroImage, pixcoords; all=false) world_coords_of_these_axes = zeros(length(dims(img))) end for (i, dim) in enumerate(dims(img)) - j = findfirst(dimnames) do dim_candidate - name(dim_candidate) == name(dim) - end + j = wcsax(dim) world_coords_of_these_axes[i,:] .= worldcoords_out[j,:] end @@ -455,15 +449,11 @@ function WCS.world_to_pix!(pixcoords_out, img::AstroImage, worldcoords) # output to only include the dims of the current slice? # out = zeros(Float64, length(dims(img))+length(refdims(img)), size(worldcoords,2)) for (i, dim) in enumerate(dims(img)) - j = findfirst(dimnames) do dim_candidate - name(dim_candidate) == name(dim) - end + j = wcsax(dim) worldcoords_prepared[j,:] = worldcoords[i,:] end for dim in refdims(img) - j = findfirst(dimnames) do dim_candidate - name(dim_candidate) == name(dim) - end + j = wcsax(dim) worldcoords_prepared[j,:] .= dim[1] end @@ -475,16 +465,12 @@ function WCS.world_to_pix!(pixcoords_out, img::AstroImage, worldcoords) coordoffsets = zeros(length(dims(img))+length(refdims(img))) coordsteps = zeros(length(dims(img))+length(refdims(img))) for (i, dim) in enumerate(dims(img)) - j = findfirst(dimnames) do dim_candidate - name(dim_candidate) == name(dim) - end + j = wcsax(dim) coordoffsets[j] = first(dims(img)[i]) coordsteps[j] = step(dims(img)[i]) end for dim in refdims(img) - j = findfirst(dimnames) do dim_candidate - name(dim_candidate) == name(dim) - end + j = wcsax(dim) coordoffsets[j] = first(dim) coordsteps[j] = step(dim) end @@ -496,17 +482,6 @@ end -## Helpers -function dimindex(img::AstroImage, ind::Int) - return dimindex(dims(img), ind) -end -function dimindex(imgdims, ind::Int) - findfirst(dimnames) do dim_candidate - name(dim_candidate) == name(imgdims[ind]) - end -end - - ## For now, we use a copied version of FITSIO's show method for FITSHeader. From dd21ea240bdf12dadd40accc269ce79471afa819 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 29 Mar 2022 07:57:45 -0700 Subject: [PATCH 092/178] Improved polquiver --- src/plot-recipes.jl | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 2418c874..1415bcdd 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -171,12 +171,12 @@ if showcolorbar if T <: Complex layout := @layout [ - imgmag{0.95w, 0.5h} colorbar{0.5h} - imgangle{0.95w, 0.5h} colorbarangle{0.5h} + imgmag{0.97w, 0.5h} colorbar{0.5h} + imgangle{0.97w, 0.5h} colorbarangle{0.5h} ] else layout := @layout [ - img{0.95w} colorbar + img{0.97w} colorbar ] end colorbar_title = get(plotattributes, :colorbar_title, "") @@ -1023,8 +1023,7 @@ end @userplot PolQuiver @recipe function f(h::PolQuiver) cube = only(h.args) - bin = get(plotattributes, :bin, 4) - polintenfrac = get(plotattributes, :polintenfrac, 0.1) + bins = get(plotattributes, :bins, 4) i = cube[Pol=At(:I)] q = cube[Pol=At(:Q)] @@ -1032,7 +1031,7 @@ end polinten = @. sqrt(q^2 + u^2) linpolfrac = polinten ./ i - binratio=1/bin + binratio=1/bins xs = imresize([x for x in dims(cube,1), y in dims(cube,2)], ratio=binratio) ys = imresize([y for x in dims(cube,1), y in dims(cube,2)], ratio=binratio) qx = imresize(q, ratio=binratio) @@ -1040,8 +1039,13 @@ end qlinpolfrac = imresize(linpolfrac, ratio=binratio) qpolintenr = imresize(polinten, ratio=binratio) - mask = isfinite.(qpolintenr) .&& qpolintenr .> polintenfrac * maximum(filter(isfinite,qpolintenr)) - a = 20 / maximum(filter(isfinite,qpolintenr)) + + # We want the longest ticks to be around 1 bin long. + qmaxlen = quantile(filter(isfinite,qpolintenr), 0.98) + a = bins / qmaxlen + # Only show arrows where the data is finite, and more than a couple pixels + # long. + mask = isfinite.(qpolintenr) pointstmp = map(xs[mask],ys[mask],qx[mask],qy[mask]) do x,y,qxi,qyi return ([x, x+a*qxi, NaN], [y, y+a*qyi, NaN]) end @@ -1055,6 +1059,16 @@ end label --> "" color --> :turbo + framestyle --> :box + aspect_ratio --> 1 + linewidth --> 1.5 + colorbar --> true + colorbar_title --> "Linear polarization fraction" + + xl = first(dims(i,2)), last(dims(i,2)) + yl = first(dims(i,1)), last(dims(i,1)) + ylims --> yl + xlims --> xl @series begin xs, ys From 28eff9a61166eb56c04a1ecec42bb93d51ba2024 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 29 Mar 2022 08:18:09 -0700 Subject: [PATCH 093/178] Tweaks to polquiver --- src/plot-recipes.jl | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 1415bcdd..d473e64e 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -171,12 +171,12 @@ if showcolorbar if T <: Complex layout := @layout [ - imgmag{0.97w, 0.5h} colorbar{0.5h} - imgangle{0.97w, 0.5h} colorbarangle{0.5h} + imgmag{0.95w, 0.5h} colorbar{0.5h} + imgangle{0.95w, 0.5h} colorbarangle{0.5h} ] else layout := @layout [ - img{0.97w} colorbar + img{0.95w} colorbar ] end colorbar_title = get(plotattributes, :colorbar_title, "") @@ -1024,6 +1024,8 @@ end @recipe function f(h::PolQuiver) cube = only(h.args) bins = get(plotattributes, :bins, 4) + ticklen = get(plotattributes, :ticklen, nothing) + minpol = get(plotattributes, :minpol, 0.1) i = cube[Pol=At(:I)] q = cube[Pol=At(:Q)] @@ -1040,12 +1042,16 @@ end qpolintenr = imresize(polinten, ratio=binratio) - # We want the longest ticks to be around 1 bin long. + # We want the longest ticks to be around 1 bin long by default. qmaxlen = quantile(filter(isfinite,qpolintenr), 0.98) - a = bins / qmaxlen + if isnothing(ticklen) + a = bins / qmaxlen + else + a = ticklen / qmaxlen + end # Only show arrows where the data is finite, and more than a couple pixels # long. - mask = isfinite.(qpolintenr) + mask = isfinite.(qpolintenr) .&& qpolintenr .>= minpol.*qmaxlen pointstmp = map(xs[mask],ys[mask],qx[mask],qy[mask]) do x,y,qxi,qyi return ([x, x+a*qxi, NaN], [y, y+a*qyi, NaN]) end @@ -1073,4 +1079,22 @@ end @series begin xs, ys end -end \ No newline at end of file +end + +""" + polquiver(polqube::AstroImage) + +Given a data cube (of at least 2 spatial dimensions, plus a polarization axis), +plot a vector field of polarization data. +The tick length represents the polarization intensity, sqrt(q^2 + u^2), +and the color represents the linear polarization fraction, sqrt(q^2 + u^2) / i. + +There are several ways you can adjust the appearance of the plot using keyword arguments: +* `bins` (default = 1) By how much should we bin down the polarization data before drawing the ticks? This reduced clutter from higher resolution datasets. Can be fractional. +* `ticklen` (default = bins) How long the 98th percentile arrow should be. By default, 1 bin long. Make this larger to draw longer arrows. +* `color` (default = :turbo) What colorscheme should be used for linear polarization fraction. +* `minpol` (default = 0.2) Hides arrows that are shorter than `minpol` times the 98th percentile arrow to make a cleaner image. Set to 0 to display all data. + +Use `implot` and `polquiver!` to overplot polarization data over an image. +""" +polquiver \ No newline at end of file From 39da97a4f266ef5c6a1f947455787c26be6d2ea0 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 29 Mar 2022 08:48:07 -0700 Subject: [PATCH 094/178] Standardize dimensions during loading --- src/AstroImages.jl | 129 ++++++++++++++++----------------------------- src/io.jl | 32 +++++------ src/showmime.jl | 56 -------------------- 3 files changed, 62 insertions(+), 155 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index d6255ddf..ef9be280 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -50,6 +50,7 @@ export load, wcs, Comment, History, + Centered, pix_to_world, pix_to_world!, world_to_pix, @@ -106,11 +107,23 @@ export X, Y, Z, Dim export At, Near, Between, .. export dims, refdims -# We need to keep a canonical order of dimensions to match back with WCS -# dimension numbers. E.g. if we see Z(), we need to know this is WCSTransform(..).ctype[3]. -# Currently this is supported up to dimension 10, but this feels arbitrary. -# In future, let's just hardcode X,Y,Z and then use the dimension number itself -# after that. +""" + Centered() + +Pass centered as a dimesion range to automatically center a dimension +along that axis. + +Example: +```julia +cube = load("abc.fits", (X=Centered(), Y=Centered(), Pol=[:I, :Q, :U])) +``` + +In that case, cube will have dimsions with the centre of the image at 0 +in both the X and Y axes. +""" +struct Centered end + +# Default dimension names if none are provided const dimnames = ( X, Y, Z, (Dim{i} for i in 4:10)... @@ -206,53 +219,6 @@ AstroImage before proceeding. AstroImage(img::AstroImage) = img -# """ -# AstroImage(data::AbstractArray, [header::FITSHeader,] [wcs::WCSTransform,]) - -# Create an AstroImage from an array, and optionally header or header and a -# WCSTransform. -# """ -# function AstroImage( -# data::AbstractArray{T,N}, -# header::FITSHeader=emptyheader(), -# wcs::Union{WCSTransform,Nothing}=nothing -# ) where {T, N} -# wcs_stale = isnothing(wcs) -# if isnothing(wcs) -# wcs = emptywcs(data) -# end -# # If the user passes in a WCSTransform of their own, we use it and mark -# # wcs_stale=false. It will be kept unless they manually change a WCS header. -# # If they don't pass anything, we start with empty WCS information regardless -# # of what's in the header but we mark it as stale. -# # If/when the WCS info is accessed via `wcs(img)` it will be computed and cached. -# # This avoids those computations if the WCS transform is not needed. -# # It also allows us to create images with invalid WCS header, -# # only erroring when/if they are used. - -# # Fields for DimensionalData. -# # Name dimensions always as X,Y,Z, then Dim{4}, Dim{5}, etc. -# # If we wanted to do something smarter e.g. time axes we would have -# # to look at the WCSTransform, and we want to avoid doing this on construction -# # for the reasons described above. -# dimnames = ( -# X, Y, Z -# )[1:min(3,N)] -# if N > 3 -# dimnames = ( -# dimnames..., -# (Dim{i} for i in 4:N)... -# ) -# end -# dimaxes = map(dimnames, axes(data)) do dim, ax -# dim(ax) -# end -# dims = DimensionalData.format(dimaxes, data) -# refdims = () - -# return AstroImage(data, dims, refdims, header, Ref(wcs), Ref(wcs_stale)) -# end - """ AstroImage(data::AbstractArray, [header::FITSHeader,] [wcs::WCSTransform,]) @@ -281,44 +247,38 @@ function AstroImage( # only erroring when/if they are used. # Fields for DimensionalData. + # TODO: cleanup logic if dims == () if wcsdims ourdims = Tuple(Wcs{i} for i in 1:ndims(data)) else ourdims = dimnames[1:ndims(data)] end - dimaxes = map(ourdims, axes(data)) do dim, ax + dims = map(ourdims, axes(data)) do dim, ax dim(ax) end - dims = DimensionalData.format(dimaxes, data) - else - dims = DimensionalData.format(dims, data) end - - + # Replace any occurences of Centered() with an automatic range + # from the data. + dimvals = map(dims, axes(data)) do dim, ax + if dim isa Centered + ax .- mean(ax) + else + dim + end + end + if dims isa NamedTuple + dims = NamedTuple{keys(dims)}(dimvals) + elseif !(dims isa NTuple{N,Dimensions.Dimension} where N) && + !(all(d-> d isa Union{UnionAll,DataType} && d <: Dimensions.Dimension, dims)) + k = name.(dimnames[1:ndims(data)]) + dims = NamedTuple{k}(dimvals) + end + dims = DimensionalData.format(dims, data) if length(dims) != ndims(data) error("Number of dims does not match the shape of the data") end - # Name dimensions always as X,Y,Z, then Dim{4}, Dim{5}, etc. - # If we wanted to do something smarter e.g. time axes we would have - # to look at the WCSTransform, and we want to avoid doing this on construction - # for the reasons described above. - # dimnames = ( - # X, Y, Z - # )[1:min(3,N)] - # if N > 3 - # dimnames = ( - # dimnames..., - # (Dim{i} for i in 4:N)... - # ) - # end - # dimaxes = map(dimnames, axes(data)) do dim, ax - # dim(ax) - # end - # dims = DimensionalData.format(dimaxes, data) - # refdims = () - return AstroImage(data, dims, refdims, header, Ref(wcs), Ref(wcs_stale)) end function AstroImage( @@ -333,18 +293,21 @@ function AstroImage( end return AstroImage(parent(darr), dims(darr), refdims(darr), header, Ref(wcs), Ref(wcs_stale)) end +AstroImage( + data::AbstractArray, + dims::Union{Tuple,NamedTuple}, + header::FITSHeader, + wcs::Union{WCSTransform,Nothing}=nothing; + wcsdims=false +) = AstroImage(data, dims, (), header, wcs; wcsdims) AstroImage( data::AbstractArray, header::FITSHeader, wcs::Union{WCSTransform,Nothing}=nothing; wcsdims=false ) = AstroImage(data, (), (), header, wcs; wcsdims) -# AstroImage( -# data::AbstractArray, -# header::FITSHeader=emptyheader(), -# wcs::Union{WCSTransform,Nothing}=nothing; -# wcsdims=false -# ) = AstroImage(data, dimnames[begin:begin+ndims(data)], (), header, wcs; wcsdims) + + # TODO: ensure this gets WCS dims. AstroImage(data::AbstractArray, wcs::WCSTransform) = AstroImage(data, emptyheader(), wcs) diff --git a/src/io.jl b/src/io.jl index b3d6860c..6419a5d1 100644 --- a/src/io.jl +++ b/src/io.jl @@ -5,23 +5,23 @@ AstroImage(fits::FITS, ext::Int=1) Given an open FITS file from the FITSIO library, load the HDU number `ext` as an AstroImage. """ -AstroImage(fits::FITS, ext::Int=1; wcsdims=false) = AstroImage(fits[ext]; wcsdims) +AstroImage(fits::FITS, ext::Int=1, args...; kwargs...) = AstroImage(fits[ext], args...; kwargs...) """ AstroImage(hdu::HDU) Given an open FITS HDU, load it as an AstroImage. """ -AstroImage(hdu::HDU; wcsdims=false) = AstroImage(read(hdu), read_header(hdu); wcsdims) +AstroImage(hdu::HDU, args...; kwargs...) = AstroImage(read(hdu), args..., read_header(hdu); kwargs...) """ img = AstroImage(filename::AbstractString, ext::Integer=1) Load an image HDU `ext` from the FITS file at `filename` as an AstroImage. """ -function AstroImage(filename::AbstractString, ext::Integer=1; wcsdims=false) +function AstroImage(filename::AbstractString, ext::Integer=1, args...; kwargs...) return FITS(filename, "r") do fits - return AstroImage(fits[ext]; wcsdims) + return AstroImage(fits[ext], args...; kwargs...) end end """ @@ -38,17 +38,17 @@ imgs = AstroImage("abc.fits", 1:3) # loads the first three HDUs as images. imgs = AstroImage("abc.fits", :) # loads all HDUs as images. ``` """ -function AstroImage(filename::AbstractString, exts::Union{NTuple{N,<:Integer},AbstractArray{<:Integer}}; wcsdims=false) where {N} +function AstroImage(filename::AbstractString, exts::Union{NTuple{N,<:Integer},AbstractArray{<:Integer}}, args...; kwargs...) where {N} return FITS(filename, "r") do fits return map(exts) do ext - return AstroImage(fits[ext]; wcsdims) + return AstroImage(fits[ext], args...; kwargs...) end end end -function AstroImage(filename::AbstractString, ::Colon; wcsdims=false) where {N} +function AstroImage(filename::AbstractString, ::Colon, args...; kwargs...) where {N} return FITS(filename, "r") do fits return map(fits) do hdu - return AstroImage(hdu; wcsdims) + return AstroImage(hdu, args...; kwargs...) end end end @@ -78,34 +78,34 @@ returned as AstroImage, and TableHDUs are returned as column tables. !! Currently comments on TableHDUs are not supported and are ignored. """ -function fileio_load(f::File{format"FITS"}, ext::Union{Int,Nothing}=nothing; wcsdims=false) where {N} +function fileio_load(f::File{format"FITS"}, ext::Union{Int,Nothing}=nothing, args...; kwargs...) where {N} return FITS(f.filename, "r") do fits if isnothing(ext) ext = indexer(fits) end - _loadhdu(fits[ext]; wcsdims) + _loadhdu(fits[ext], args...; kwargs...) end end -function fileio_load(f::File{format"FITS"}, exts::Union{NTuple{N,<:Integer},AbstractArray{<:Integer}}; wcsdims=false) where {N} +function fileio_load(f::File{format"FITS"}, exts::Union{NTuple{N,<:Integer},AbstractArray{<:Integer}}, args...; kwargs...) where {N} return FITS(f.filename, "r") do fits map(exts) do ext - _loadhdu(fits[ext]; wcsdims) + _loadhdu(fits[ext], args...; kwargs...) end end end -function fileio_load(f::File{format"FITS"}, ::Colon; wcsdims=false) where {N} +function fileio_load(f::File{format"FITS"}, ::Colon, args...; kwargs...) where {N} return FITS(f.filename, "r") do fits exts_resolved = 1:length(fits) map(exts_resolved) do ext - _loadhdu(fits[ext]; wcsdims) + _loadhdu(fits[ext], args...; kwargs...) end end end _loadhdu(hdu::FITSIO.TableHDU) = Tables.columntable(hdu) -function _loadhdu(hdu::FITSIO.ImageHDU; wcsdims=false) +function _loadhdu(hdu::FITSIO.ImageHDU, args...; kwargs...) if size(hdu) != () - return AstroImage(hdu; wcsdims) + return AstroImage(hdu, args...; kwargs...) else # Sometimes files have an empty data HDU that shows up as an image HDU but has headers. # Fallback to creating an empty AstroImage with those headers. diff --git a/src/showmime.jl b/src/showmime.jl index 426c0645..31edc822 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -57,59 +57,3 @@ function render(img::AstroImageMat{T,N}) where {T,N} return colorview(Gray, f.(_float.(img.data))) end ImageCore.colorview(img::AstroImageMat) = render(img) - - -# using Base64 - - -# """ -# interact_cube(cube::AbstractArray, initial_slices=) -# If running in an interactive environment like IJulia, allow scrolling through -# the slices of a cube interactively using `imview`. -# Accepts the same keyword arguments as `imview`, with one exception. Here, -# if `clims` is a function, it is applied once to all the finite pixels in the cube -# to determine the color limits rather than just the currently displayed slice. -# """ -# function interact_cube( -# cube::Union{AbstractArray{T,3},AbstractArray{T,4},AbstractArray{T,5}}, -# initial_slices=first.(axes.(Ref(cube),3:ndims(cube))); -# clims=_default_clims[], -# imview_kwargs... -# ) where T -# # Create a single view that updates -# buf = cube[:,:,initial_slices...] - -# # If not provided, calculate clims by applying to the whole cube -# # rather than just one slice -# # Users can pass clims as an array or tuple containing the minimum and maximum values -# if typeof(clims) <: AbstractArray || typeof(clims) <: Tuple -# if length(clims) != 2 -# error("clims must have exactly two values if provided.") -# end -# clims = (first(clims), last(clims)) -# # Or as a callable that computes them given an iterator -# else -# clims = clims(skipmissingnan(cube)) -# end - -# v = imview(buf; clims, imview_kwargs...) - -# cubesliders = map(3:ndims(cube)) do ax_i -# ax = axes(cube, ax_i) -# return Interact.slider(ax, initial_slices[ax_i-2], label=string(dimnames[ax_i])); -# end - -# function viz(sliderindexes) -# buf .= view(cube,:,:,sliderindexes...) -# b64 = Base64.base64encode() do io -# show(io, MIME("image/png"), v) -# end -# HTML("
") -# end - -# return vbox(cubesliders..., map(viz, cubesliders...)) -# end - -# # This is used in Jupyter notebooks -# Base.show(io::IO, mime::MIME"text/html", cube::Union{AstroImage{T,3},AstroImage{T,4},AstroImage{T,5}}; kwargs...) where T = -# show(io, mime, interact_cube(cube), kwargs...) \ No newline at end of file From ffe5f778a201a2c72cb6c93c8422c3e1b8ca1d42 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Thu, 7 Apr 2022 12:16:10 -0700 Subject: [PATCH 095/178] Add contrast & bias adjustments after stretch and clip --- src/contrib/images.jl | 5 +---- src/imview.jl | 15 ++++++++++----- src/plot-recipes.jl | 20 ++++++++++++++++---- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/contrib/images.jl b/src/contrib/images.jl index db41eb88..f3b61c19 100644 --- a/src/contrib/images.jl +++ b/src/contrib/images.jl @@ -51,14 +51,11 @@ function clampednormedview(img::AbstractArray{T}, lims) where T <: Normed Δ = abs(imgmax - imgmin) normeddata = mappedarray( pix -> clamp((pix - imgmin)/Δ, zero(T), one(T)), - pix_norm -> pix_norm*Δ + imgmin, + # pix_norm -> pix_norm*Δ + imgmin, # TODO img ) return maybe_shareheader(img, normeddata) end -function clampednormedview(img::AbstractArray{Bool}, lims) - return img -end # Restrict downsizes images by roughly a factor of two. diff --git a/src/imview.jl b/src/imview.jl index 058c7f6f..a501a689 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -94,7 +94,7 @@ end """ - imview(img; clims=extrema, stretch=identity, cmap=nothing) + imview(img; clims=extrema, stretch=identity, cmap=:magma, contrast=1.0, bias=0.5) Create a read only view of an array or AstroImageMat mapping its data values to Colors according to `clims`, `stretch`, and `cmap`. @@ -150,6 +150,8 @@ function imview( clims=_default_clims[], stretch=_default_stretch[], cmap=_default_cmap[], + contrast=1.0, + bias=0.5 ) where {T} isempt = isempty(img) @@ -181,7 +183,7 @@ function imview( imgmin, imgmax = clims(skipmissingnan(img)) end normed = clampednormedview(img, (imgmin, imgmax)) - return _imview(img, normed, stretch, _lookup_cmap(cmap)) + return _imview(img, normed, stretch, _lookup_cmap(cmap), contrast, bias) end # Special handling for complex images """ @@ -205,7 +207,7 @@ function imview(img::AbstractMatrix{T}; kwargs...) where {T<:Complex} vcat(mag_view,angle_view) end -function _imview(img, normed::AbstractArray{T}, stretch, cmap) where T +function _imview(img, normed::AbstractArray{T}, stretch, cmap, contrast, bias) where T if T <: Union{Missing,<:Number} TT = typeof(first(skipmissing(normed))) @@ -232,8 +234,9 @@ function _imview(img, normed::AbstractArray{T}, stretch, cmap) where T # are +-Inf stretched = pixr else - stretched = stretch(pixn) + stretched = (stretch(pixn) - bias)*contrast+0.5 end + # We treat NaN/missing values as transparent return if ismissing(stretched) || isnan(stretched) RGBA{TT}(0,0,0,0) @@ -285,6 +288,8 @@ function imview_colorbar( clims=_default_clims[], stretch=_default_stretch[], cmap=_default_cmap[], + contrast=1, + bias=0.5 ) imgmin, imgmax = _resolve_clims(img, clims) cbpixlen = 100 @@ -312,7 +317,7 @@ function imview_colorbar( # Strech the ticks # Construct the image to use as a colorbar - cbimg = imview(data; clims=(imgmin,imgmax), stretch=identity, cmap) + cbimg = imview(data; clims=(imgmin,imgmax), stretch=identity, cmap, contrast, bias) # And the colorbar tick locations & labels ticks, _, _ = optimize_ticks(Float64(imgmin), Float64(imgmax), k_min=3) # Now map these to pixel locations through streching and colorlimits: diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index d473e64e..801839e1 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -42,6 +42,10 @@ clims --> _default_clims[] stretch --> _default_stretch[] cmap --> _default_cmap[] + bias --> 0.5 + contrast--> 1 + bias = plotattributes[:bias] + contrast = plotattributes[:contrast] grid := false # In most cases, a grid framestyle is a nicer looking default for images @@ -61,7 +65,7 @@ else img = data end - imgv = imview(img; clims, stretch, cmap) + imgv = imview(img; clims, stretch, cmap, contrast, bias) end @@ -89,10 +93,16 @@ refdimslabel = join(map(refdims(imgv)) do d # match dimension with the wcs axis number i = wcsax(d) - label = ctype_label(wcs(imgv).ctype[i], wcs(imgv).radesys) + ct = wcs(imgv).ctype[i] + label = ctype_label(ct, wcs(imgv).radesys) value = pix_to_world(imgv, [1,1], all=true)[i] + @show value unit = wcs(imgv).cunit[i] - return @sprintf("%s = %.5g %s", label, value, unit) + if ct == "STOKES" + return _stokes_name(_stokes_symbol(value)) + else + return @sprintf("%s = %.5g %s", label, value, unit) + end end, ", ") else refdimslabel = join(map(d->"$(name(d))= $(d[1])", refdims(imgv)), ", ") @@ -189,7 +199,7 @@ subplot := subplot_i aspect_ratio := :none colorbar := false - cbimg, cbticks = imview_colorbar(img; clims, stretch, cmap) + cbimg, cbticks = imview_colorbar(img; clims, stretch, cmap, contrast, bias) xticks := [] ymirror := true yticks := cbticks @@ -456,6 +466,8 @@ function ctype_label(ctype,radesys) elseif startswith(ctype, "GLAT") return "Galactic Latitude" # elseif startswith(ctype, "TLAT") + elseif ctype == "STOKES" + return "Polarization" else return ctype end From 36d9034323ba4abfee8b70d004e5ce4dd928463e Mon Sep 17 00:00:00 2001 From: William Thompson Date: Thu, 7 Apr 2022 12:16:36 -0700 Subject: [PATCH 096/178] Fix coordinate offset from 1 based arrays --- src/wcs.jl | 83 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 67 insertions(+), 16 deletions(-) diff --git a/src/wcs.jl b/src/wcs.jl index 96238613..c24f4201 100644 --- a/src/wcs.jl +++ b/src/wcs.jl @@ -302,6 +302,70 @@ function wcsfromheader(img::AstroImage; relax=WCS.HDR_ALL) end end +# Map FITS stokes numbers to a symbol +function _stokes_symbol(i) + return if i == 1 + :I + elseif i == 2 + :Q + elseif i == 3 + :U + elseif i == 4 + :V + elseif i == -1 + :RR + elseif i == -2 + :LL + elseif i == -3 + :RL + elseif i == -4 + :LR + elseif i == -5 + :XX + elseif i == -6 + :YY + elseif i == -7 + :XY + elseif i == -8 + :YX + else + @warn "unknown FITS stokes number $i. See \"Representations of world coordinates in FITS\", Table 7." + nothing + end +end +function _stokes_name(symb) + return if symb == :I + "Stokes Unpolarized" + elseif symb == :Q + "Stokes Linear Q" + elseif symb == :U + "Stokes Linear U" + elseif symb == :V + "Stokes Circular" + elseif symb == :RR + "Right-right cicular" + elseif symb == :LL + "Left-left cicular" + elseif symb == :RL + "Right-left cross-cicular" + elseif symb == :LR + "Left-right cross-cicular" + elseif symb == :XX + "X parallel linear" + elseif symb == :YY + "Y parallel linear" + elseif symb == :XY + "XY cross linear" + elseif symb == :YX + "YX cross linear" + else + @warn "unknown FITS stokes key $symb. See \"Representations of world coordinates in FITS\", Table 7." + "" + end +end + + + # Smart versions of pix_to_world and world_to_pix """ @@ -339,19 +403,6 @@ julia> world_coords = pix_to_world(img, [1, 1], all=true) !! Coordinates must be provided in the order of `dims(img)`. If you transpose an image, the order you pass the coordinates should not change. """ -# function WCS.pix_to_world(img::AstroImage, pixcoords::NTuple{N,DimensionalData.Dimension}) where N -# pixcoords_prepared = zeros(Float64, length(pixcoords)) -# for dim in pixcoords -# j = findfirst(dimnames) do dim_candidate -# name(dim_candidate) == name(dim) -# end -# pixcoords_prepared[j] = dim[] -# end -# D_out = length(dims(img))+length(refdims(img)) -# out = zeros(Float64, D_out) -# return WCS.pix_to_world!(out, img, pixcoords_prepared) -# end -# WCS.pix_to_world(img::AstroImage, pixcoords::DimensionalData.Dimension...) = WCS.pix_to_world(img, pixcoords) function WCS.pix_to_world(img::AstroImage, pixcoords; all=false) if pixcoords isa Array{Float64} pixcoords_prepared = pixcoords @@ -385,11 +436,11 @@ function WCS.pix_to_world(img::AstroImage, pixcoords; all=false) # out = zeros(Float64, length(dims(img))+length(refdims(img)), size(pixcoords,2)) for (i, dim) in enumerate(dims(img)) j = wcsax(dim) - parentcoords_prepared[j,:] .= parentcoords[i,:] + parentcoords_prepared[j,:] .= parentcoords[i,:] .- 1 end for dim in refdims(img) j = wcsax(dim) - parentcoords_prepared[j,:] .= dim[1] + parentcoords_prepared[j,:] .= dim[1] .- 1 end # Get world coordinates along all slices @@ -528,4 +579,4 @@ function serializeheader(io, hdr::FITSHeader) print(io) end end -end \ No newline at end of file +end From 257c9de235d56c55234861ff04661179bc3c8fbd Mon Sep 17 00:00:00 2001 From: William Thompson Date: Thu, 7 Apr 2022 13:26:18 -0700 Subject: [PATCH 097/178] Change percent to a callable struct instead of a closure --- src/imview.jl | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/imview.jl b/src/imview.jl index a501a689..fac9ebaa 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -11,7 +11,7 @@ powerdiststretch(x, a=1000) = (a^x - 1) / (a - 1) """ percent(99.5) -Returns a function that calculates display limits that include the given +Returns a callable that calculates display limits that include the given percent of the image data. Example: @@ -20,12 +20,14 @@ julia> imview(img, clims=percent(90)) ``` This will set the limits to be the 5th percentile to the 95th percentile. """ -function percent(perc::Number) - trim = (1 - perc/100)/2 - clims(data::AbstractMatrix) = quantile(vec(data), (trim, 1-trim)) - clims(data) = quantile(data, (trim, 1-trim)) - return clims +struct percent + perc::Float64 + trim::Float64 + percent(percentage::Number) = new(Float64(percentage), (1 - percentage/100)/2) end +(p::percent)(data::AbstractMatrix) = quantile(vec(data), (p.trim, 1-p.trim)) +(p::percent)(data) = quantile(data, (p.trim, 1-p.trim)) +Base.show(io::IO, p::percent; kwargs...) = print(io, "percent($(p.perc))", kwargs...) const _default_cmap = Base.RefValue{Union{Symbol,Nothing}}(:magma)#nothing) const _default_clims = Base.RefValue{Any}(percent(99.5)) From 78f25c4a6b108d3d40a3122c411ebc3c6e3160c7 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Thu, 7 Apr 2022 14:05:51 -0700 Subject: [PATCH 098/178] Ensure overplotting implot works as expected --- src/plot-recipes.jl | 61 ++++++++++++++++++++------------------------- src/wcs.jl | 53 ++++++++++++++++++++------------------- 2 files changed, 55 insertions(+), 59 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 801839e1..078c6279 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -21,11 +21,12 @@ any(d->typeof(d) <: Wcs, dims(data)) && !all(==(""), wcs(data).ctype) - - minx = first(axes(data,2)) - maxx = last(axes(data,2)) - miny = first(axes(data,1)) - maxy = last(axes(data,1)) + + + minx = first(parent(dims(data,1))) + maxx = last(parent(dims(data,1))) + miny = first(parent(dims(data,2))) + maxy = last(parent(dims(data,2))) extent = (minx-0.5, maxx+0.5, miny-0.5, maxy+0.5) if haskey(plotattributes, :xlims) extent = (plotattributes[:xlims]..., extent[3:4]...) @@ -95,8 +96,7 @@ i = wcsax(d) ct = wcs(imgv).ctype[i] label = ctype_label(ct, wcs(imgv).radesys) - value = pix_to_world(imgv, [1,1], all=true)[i] - @show value + value = pix_to_world(imgv, [1,1], all=true, parent=true)[i] unit = wcs(imgv).cunit[i] if ct == "STOKES" return _stokes_name(_stokes_symbol(value)) @@ -110,6 +110,13 @@ title --> refdimslabel end + # To ensure the physical axis tick labels are correct the axes must be + # tight to the image + xl = first(dims(imgv,1))-0.5, last(dims(imgv,1))+0.5 + yl = first(dims(imgv,2))-0.5, last(dims(imgv,2))+0.5 + ylims --> yl + xlims --> xl + subplot_i = 0 # Actual image series (RGB pixels by this point) @series begin @@ -117,9 +124,6 @@ subplot := subplot_i colorbar := false - - - # Note: if the axes are on unusual sides (e.g. y-axis at right, x-axis at top) # then these coordinates are not correct. They are only correct exactly # along the axis. @@ -132,13 +136,11 @@ yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), wcsax(dims(img,2)), gridspec.tickpos2w)) yguide --> ctype_label(wcs(imgv).ctype[wcsax(dims(img,2))], wcs(imgv).radesys) - - view(parent(imgv), reverse(axes(imgv,1)),:) end + ax1 = collect(parent(dims(imgv,1))) ax2 = collect(parent(dims(imgv,2))) - ax1, ax2, view(parent(imgv), reverse(axes(imgv,1)),:) end @@ -155,13 +157,6 @@ gridcolor --> :lightgray end - # To ensure the physical axis tick labels are correct the axes must be - # tight to the image - xl = first(axes(imgv,2))-0.5, last(axes(imgv,2))+0.5 - yl = first(axes(imgv,1))-0.5, last(axes(imgv,1))+0.5 - ylims --> yl - xlims --> xl - wcsg, gridspec end end @@ -485,14 +480,12 @@ curvature of the WCS grid projected on the image coordinates. """ function WCSGrid(img::AstroImageMat) - minx = first(axes(img,1)) - maxx = last(axes(img,1)) - miny = first(axes(img,2)) - maxy = last(axes(img,2)) - # extent = (minx, maxx, miny, maxy) + minx = first(dims(img,2)) + maxx = last(dims(img,2)) + miny = first(dims(img,1)) + maxy = last(dims(img,1)) extent = (minx-0.5, maxx+0.5, miny-0.5, maxy+0.5) - # extent = (minx-2, maxx+2, miny-2, maxy+2) - + @show extent return WCSGrid(img, extent) end @@ -587,7 +580,7 @@ function wcsgridspec(wsg::WCSGrid) minx minx maxx maxx miny maxy miny maxy ] - posuv = pix_to_world(wsg.img, posxy) + posuv = pix_to_world(wsg.img, posxy, parent=true) (minu, maxu), (minv, maxv) = extrema(posuv, dims=2) # In general, grid can be curved when plotted back against the image, @@ -629,7 +622,7 @@ function wcsgridspec(wsg::WCSGrid) griduv = repeat(posuv[:,1], 1, N_points) griduv[1,:] .= urange griduv[2,:] .= tickv - posxy = world_to_pix(wsg.img, griduv) + posxy = world_to_pix(wsg.img, griduv; parent=true) # Now that we have the grid in pixel coordinates, # if we find out where the grid intersects the axes we can put @@ -793,7 +786,7 @@ function wcsgridspec(wsg::WCSGrid) griduv = repeat(posuv[:,1], 1, N_points) griduv[1,:] .= ticku griduv[2,:] .= vrange - posxy = world_to_pix(wsg.img, griduv) + posxy = world_to_pix(wsg.img, griduv; parent=true) # Now that we have the grid in pixel coordinates, # if we find out where the grid intersects the axes we can put @@ -938,7 +931,7 @@ function wcsgridspec(wsg::WCSGrid) griduv = posuv[:,1] griduv[1] = ticku griduv[2] = mean(vrange) - posxy = world_to_pix(wsg.img, griduv) + posxy = world_to_pix(wsg.img, griduv, parent=true) if !(minx < posxy[1] < maxx) || !(miny < posxy[2] < maxy) continue @@ -950,7 +943,7 @@ function wcsgridspec(wsg::WCSGrid) # Now find slope (TODO: stepsize) # griduv[ax[2]] -= 1 griduv[2] += 0.1step(vrange) - posxy2 = world_to_pix(wsg.img, griduv) + posxy2 = world_to_pix(wsg.img, griduv, parent=true) θ = atan( posxy2[2] - posxy[2], posxy2[1] - posxy[1], @@ -966,7 +959,7 @@ function wcsgridspec(wsg::WCSGrid) griduv = posuv[:,1] griduv[1] = mean(urange) griduv[2] = tickv - posxy = world_to_pix(wsg.img, griduv) + posxy = world_to_pix(wsg.img, griduv, parent=true) if !(minx < posxy[1] < maxx) || !(miny < posxy[2] < maxy) continue @@ -976,7 +969,7 @@ function wcsgridspec(wsg::WCSGrid) push!(annotations2y, posxy[2]) griduv[1] += 0.1step(urange) - posxy2 = world_to_pix(wsg.img, griduv) + posxy2 = world_to_pix(wsg.img, griduv, parent=true) θ = atan( posxy2[2] - posxy[2], posxy2[1] - posxy[1], diff --git a/src/wcs.jl b/src/wcs.jl index c24f4201..9f3f5306 100644 --- a/src/wcs.jl +++ b/src/wcs.jl @@ -403,7 +403,7 @@ julia> world_coords = pix_to_world(img, [1, 1], all=true) !! Coordinates must be provided in the order of `dims(img)`. If you transpose an image, the order you pass the coordinates should not change. """ -function WCS.pix_to_world(img::AstroImage, pixcoords; all=false) +function WCS.pix_to_world(img::AstroImage, pixcoords; all=false, parent=false) if pixcoords isa Array{Float64} pixcoords_prepared = pixcoords else @@ -421,7 +421,11 @@ function WCS.pix_to_world(img::AstroImage, pixcoords; all=false) # pixcoords_floored = floor.(Int, pixcoords) # pixcoords_frac = (pixcoords .- pixcoords_floored) .* step.(dims(img)) # parentcoords = getindex.(dims(img), pixcoords_floored) .+ pixcoords_frac - parentcoords = pixcoords .* step.(dims(img)) .+ first.(dims(img)) + if parent + parentcoords = pixcoords + else + parentcoords = pixcoords .* step.(dims(img)) .+ first.(dims(img)) + end # WCS.jl is very restrictive. We need to supply a Vector{Float64} # as input, not any other kind of collection. # TODO: avoid allocation in case where refdims=() and pixcoords isa Array{Float64} @@ -430,9 +434,6 @@ function WCS.pix_to_world(img::AstroImage, pixcoords; all=false) else parentcoords_prepared = zeros(length(dims(img))+length(refdims(img))) end - - # TODO: we need to pass in ref dims locations as well, and then filter the - # output to only include the dims of the current slice? # out = zeros(Float64, length(dims(img))+length(refdims(img)), size(pixcoords,2)) for (i, dim) in enumerate(dims(img)) j = wcsax(dim) @@ -468,7 +469,7 @@ end ## -function WCS.world_to_pix(img::AstroImage, worldcoords) +function WCS.world_to_pix(img::AstroImage, worldcoords; parent=false) if worldcoords isa Array{Float64} worldcoords_prepared = worldcoords else @@ -480,9 +481,9 @@ function WCS.world_to_pix(img::AstroImage, worldcoords) else out = similar(worldcoords_prepared, Float64, D_out) end - return WCS.world_to_pix!(out, img, worldcoords_prepared) + return WCS.world_to_pix!(out, img, worldcoords_prepared; parent) end -function WCS.world_to_pix!(pixcoords_out, img::AstroImage, worldcoords) +function WCS.world_to_pix!(pixcoords_out, img::AstroImage, worldcoords; parent=false) # # Find the coordinates in the parent array. # # Dimensional data # worldcoords_floored = floor.(Int, worldcoords) @@ -509,25 +510,27 @@ function WCS.world_to_pix!(pixcoords_out, img::AstroImage, worldcoords) end # This returns the parent pixel coordinates. - # WCS.world_to_pix!(wcs(img), worldcoords_prepared, pixcoords_out) - pixcoords_out = WCS.world_to_pix(wcs(img), worldcoords_prepared) - + # TODO: switch to non-allocating version. + pixcoords_out .= WCS.world_to_pix(wcs(img), worldcoords_prepared) + + if !parent + coordoffsets = zeros(length(dims(img))+length(refdims(img))) + coordsteps = zeros(length(dims(img))+length(refdims(img))) + for (i, dim) in enumerate(dims(img)) + j = wcsax(dim) + coordoffsets[j] = first(dims(img)[i]) + coordsteps[j] = step(dims(img)[i]) + end + for dim in refdims(img) + j = wcsax(dim) + coordoffsets[j] = first(dim) + coordsteps[j] = step(dim) + end - coordoffsets = zeros(length(dims(img))+length(refdims(img))) - coordsteps = zeros(length(dims(img))+length(refdims(img))) - for (i, dim) in enumerate(dims(img)) - j = wcsax(dim) - coordoffsets[j] = first(dims(img)[i]) - coordsteps[j] = step(dims(img)[i]) + pixcoords_out .-= coordoffsets + pixcoords_out .= (pixcoords_out .+ 1) ./ coordsteps end - for dim in refdims(img) - j = wcsax(dim) - coordoffsets[j] = first(dim) - coordsteps[j] = step(dim) - end - - pixcoords_out .-= coordoffsets - pixcoords_out .= pixcoords_out ./ coordsteps .+ 1 + return pixcoords_out end From 53d8e5e8e68e625941e1b5ddca8c4c8a200cee3b Mon Sep 17 00:00:00 2001 From: William Thompson Date: Thu, 7 Apr 2022 14:21:39 -0700 Subject: [PATCH 099/178] Support arbitrary color gradients in addition to named colorschemes --- src/AstroImages.jl | 2 +- src/imview.jl | 30 ++++++------------------------ 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index ef9be280..2338bc32 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -18,7 +18,7 @@ using Tables using RecipesBase using AstroAngles using Printf -using PlotUtils: optimize_ticks +using PlotUtils: optimize_ticks, AbstractColorList diff --git a/src/imview.jl b/src/imview.jl index fac9ebaa..fbe2fb8e 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -71,13 +71,14 @@ Helper to iterate over data skipping missing and non-finite values. skipmissingnan(itr) = Iterators.filter(el->!ismissing(el) && isfinite(el), itr) -function _lookup_cmap(cmap) +function _lookup_cmap(cmap::Symbol) if cmap ∉ keys(ColorSchemes.colorschemes) error("$cmap not found in ColorSchemes.colorschemes. See: https://juliagraphics.github.io/ColorSchemes.jl/stable/catalogue/") end - return cmap + return ColorSchemes.colorschemes[cmap] end -_lookup_cmap(cmap::Nothing) = nothing +_lookup_cmap(::Nothing) = ColorSchemes.colorschemes[:grays] +_lookup_cmap(acl::AbstractColorList) = acl function _resolve_clims(img, clims) # Tuple or abstract array @@ -223,13 +224,6 @@ function _imview(img, normed::AbstractArray{T}, stretch, cmap, contrast, bias) w stretchmin = stretch(zero(TT)) stretchmax = stretch(one(TT)) - # Peviously no colormap would fall back to Gray, but - # it's simpler to keep a single codepath and use the :grays - # color scheme. - if isnothing(cmap) - cmap = :grays - end - cscheme = ColorSchemes.colorschemes[cmap] mapper = mappedarray(img, normed) do pixr, pixn if ismissing(pixr) || !isfinite(pixr) || ismissing(pixn) || !isfinite(pixn) # We check pixr in addition to pixn because we want to preserve if the pixels @@ -250,23 +244,11 @@ function _imview(img, normed::AbstractArray{T}, stretch, cmap, contrast, bias) w RGBA{TT}(0,0,0,1) end else - RGBA{TT}(get(cscheme::ColorScheme, stretched, (stretchmin, stretchmax))) - end + RGBA{TT}(get(cmap, stretched, (stretchmin, stretchmax))) + end::RGBA{TT} end # Flip image to match conventions of other programs - # flipped_view = view(mapper', reverse(axes(mapper,2)),:) - # return maybe_copyheader(img, flipped_view) - # return maybe_copyheader(img, mapper) - - # flipped_view = OffsetArray( - # view( - # mapper', - # reverse(axes(mapper,1)), - # :, - # ), - # axes(img)... - # ) flipped_view = view( mapper', reverse(axes(mapper,2)), From df520f39541f92de785df15b5063e41ba51ae7f5 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 8 Apr 2022 08:23:47 -0700 Subject: [PATCH 100/178] Automatically downscale images before plotting,but maintain correct coordinates --- src/contrib/images.jl | 7 ++++++- src/plot-recipes.jl | 18 ++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/contrib/images.jl b/src/contrib/images.jl index f3b61c19..ab27cf12 100644 --- a/src/contrib/images.jl +++ b/src/contrib/images.jl @@ -62,7 +62,12 @@ end # We want to keep the wrapper but downsize the underlying array # TODO: correct dimensions after restrict. ImageTransformations.restrict(img::AstroImage, ::Tuple{}) = img -ImageTransformations.restrict(img::AstroImage, region::Dims) = shareheader(img, restrict(arraydata(img), region)) +function ImageTransformations.restrict(img::AstroImage, region::Dims) + restricted = restrict(arraydata(img), region) + steps = cld.(size(img), size(restricted)) + newdims = Tuple(d[begin:s:end] for (d,s) in zip(dims(img),steps)) + return AstroImage(restricted, newdims, refdims(img), header(img), Ref(getfield(img, :wcs)[]), Ref(getfield(img, :wcs_stale)[])) +end # TODO: use WCS info # ImageCore.pixelspacing(img::ImageMeta) = pixelspacing(arraydata(img)) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 078c6279..33f67dc7 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -43,10 +43,9 @@ clims --> _default_clims[] stretch --> _default_stretch[] cmap --> _default_cmap[] - bias --> 0.5 - contrast--> 1 - bias = plotattributes[:bias] - contrast = plotattributes[:contrast] + + bias = get(plotattributes, :bias, 0.5) + contrast = get(plotattributes, :contrast, 1) grid := false # In most cases, a grid framestyle is a nicer looking default for images @@ -69,6 +68,17 @@ imgv = imview(img; clims, stretch, cmap, contrast, bias) end + # Reduce large images using the same heuristic as Images.jl + maxpixels = get(plotattributes, :maxpixels, 10^6) + _length1(A::AbstractArray) = length(eachindex(A)) + _length1(A) = length(A) + @show axes(imgv) + while _length1(imgv) > maxpixels + @info "downscaling" + imgv = restrict(imgv) + @show axes(imgv) + end + # We have to do a lot of flipping to keep the orientation corect yflip := false From dfbb9f7f65dca029a486b40d605708a58c901b96 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 8 Apr 2022 08:24:09 -0700 Subject: [PATCH 101/178] Support rendering cubes, vectors using imview. Useful for animations. --- src/imview.jl | 46 +++++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/src/imview.jl b/src/imview.jl index fbe2fb8e..78a24182 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -149,7 +149,7 @@ save("output.png", v) ``` """ function imview( - img::AbstractMatrix{T}; + img::AbstractArray{T}; clims=_default_clims[], stretch=_default_stretch[], cmap=_default_cmap[], @@ -157,17 +157,36 @@ function imview( bias=0.5 ) where {T} - isempt = isempty(img) + # Create flipped view of to match conventions of other programs. + # Origin is centre of pixel (1,1) at bottom left. + if ndims(img) == 2 + imgT = view( + permutedims(img,(2,1)), + reverse(axes(img,2)), + :, + ) + elseif ndims(img) >= 3 + newdims = (2,1, 3:ndims(img)...) + ds = Tuple(((:) for _ in 2:ndims(img))) + imgT = view( + permutedims(img,newdims), + reverse(axes(img,2)), + ds..., + ) + else + imgT = img + end + isempt = isempty(imgT) if isempt @warn "imview called with empty argument" return fill(RGBA{N0f8}(0,0,0,0), 1,1) end # Users will occaisionally pass in data that is 0D, filled with NaN, or filled with missing. # We still need to do something reasonable in those caes. - nonempty = any(x-> !ismissing(x) && isfinite(x), img) + nonempty = any(x-> !ismissing(x) && isfinite(x), imgT) if !nonempty @warn "imview called with all missing or non-finite values" - return map(px->RGBA{N0f8}(0,0,0,0), img) + return map(px->RGBA{N0f8}(0,0,0,0), imgT) end # TODO: Images.jl has logic to downsize huge images before displaying them. @@ -183,10 +202,10 @@ function imview( imgmax = last(clims) # Or as a callable that computes them given an iterator else - imgmin, imgmax = clims(skipmissingnan(img)) + imgmin, imgmax = clims(skipmissingnan(imgT)) end - normed = clampednormedview(img, (imgmin, imgmax)) - return _imview(img, normed, stretch, _lookup_cmap(cmap), contrast, bias) + normed = clampednormedview(imgT, (imgmin, imgmax)) + return _imview(imgT, normed, stretch, _lookup_cmap(cmap), contrast, bias) end # Special handling for complex images """ @@ -203,7 +222,7 @@ vcat( ) ``` """ -function imview(img::AbstractMatrix{T}; kwargs...) where {T<:Complex} +function imview(img::AbstractArray{T}; kwargs...) where {T<:Complex} mag_view = imview(abs.(img); kwargs...) angle_view = imview(angle.(img), clims=(-pi, pi), stretch=identity, cmap=:cyclic_mygbm_30_95_c78_n256_s25) @@ -248,14 +267,7 @@ function _imview(img, normed::AbstractArray{T}, stretch, cmap, contrast, bias) w end::RGBA{TT} end - # Flip image to match conventions of other programs - flipped_view = view( - mapper', - reverse(axes(mapper,2)), - :, - ) - - return maybe_copyheader(img, flipped_view) + return maybe_copyheader(img, mapper) end @@ -267,7 +279,7 @@ Create a colorbar for a given image matching how it is displayed by `orientation` can be `:vertical` or `:horizontal`. """ function imview_colorbar( - img::AbstractMatrix; + img::AbstractArray; orientation=:vertical, clims=_default_clims[], stretch=_default_stretch[], From bc7c66fb1a3c33878470d76aad420460f7fde38f Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 8 Apr 2022 11:12:01 -0700 Subject: [PATCH 102/178] Improved support for plotly --- Project.toml | 7 ++++--- src/imview.jl | 15 ++++++++++++++- src/plot-recipes.jl | 14 +++++++------- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/Project.toml b/Project.toml index c66995a5..92cac8fc 100644 --- a/Project.toml +++ b/Project.toml @@ -12,13 +12,13 @@ FITSIO = "525bcba6-941b-5504-bd06-fd0dc1a4d2eb" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" ImageAxes = "2803e5a7-5153-5ecf-9a86-9b4c37f5f5ac" ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534" -ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" +# ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" ImageMetadata = "bc367c6b-8a6b-528e-b4bd-a4b897500b49" ImageShow = "4e3cecfd-b093-5904-9786-8bbb286a6a31" ImageTransformations = "02fcd773-0e25-5acc-982a-7f6622650795" -InlineStrings = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48" +# InlineStrings = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48" MappedArrays = "dbb5928d-eab1-5f90-85c2-b9b0edb7c900" -OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +# OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" @@ -31,6 +31,7 @@ WCS = "15f3aee2-9e10-537f-b834-a6fb8bdb944d" [compat] Reproject = "^0.3.0" julia = "^1.6.0" +DimensionalData = "^0.20" [extras] JLD = "4138dd39-2aa7-5051-a626-17a0bb65d9c8" diff --git a/src/imview.jl b/src/imview.jl index 78a24182..7043ee05 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -207,6 +207,20 @@ function imview( normed = clampednormedview(imgT, (imgmin, imgmax)) return _imview(imgT, normed, stretch, _lookup_cmap(cmap), contrast, bias) end + +# Unwrap AstroImages before view, then rebuild. +# We have to permute the dimensions of the image to get the origin at the bottom left. +# But we don't want this to affect the dimensions of the array. +# Also, this reduces the number of methods we need to compile for imview by standardizing types +# earlier on. The compiled code for showing an array is the same as an array wrapped by an +# AstroImage, except for one unwrapping step. +function imview( + img::AstroImage; + kwargs... +) + return shareheader(img, imview(parent(img); kwargs...)) +end + # Special handling for complex images """ imview(img::AbstractArray{<:Complex}; ...) @@ -223,7 +237,6 @@ vcat( ``` """ function imview(img::AbstractArray{T}; kwargs...) where {T<:Complex} - mag_view = imview(abs.(img); kwargs...) angle_view = imview(angle.(img), clims=(-pi, pi), stretch=identity, cmap=:cyclic_mygbm_30_95_c78_n256_s25) vcat(mag_view,angle_view) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 33f67dc7..bbb9c321 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -72,14 +72,10 @@ maxpixels = get(plotattributes, :maxpixels, 10^6) _length1(A::AbstractArray) = length(eachindex(A)) _length1(A) = length(A) - @show axes(imgv) while _length1(imgv) > maxpixels - @info "downscaling" imgv = restrict(imgv) - @show axes(imgv) end - # We have to do a lot of flipping to keep the orientation corect yflip := false xflip := false @@ -151,7 +147,9 @@ ax1 = collect(parent(dims(imgv,1))) ax2 = collect(parent(dims(imgv,2))) - ax1, ax2, view(parent(imgv), reverse(axes(imgv,1)),:) + # Views of images are not currently supported by plotly() so we have to collect them. + # ax1, ax2, view(parent(imgv), reverse(axes(imgv,1)),:) + ax1, ax2, parent(imgv)[reverse(axes(imgv,1)),:] end # If wcs=true (default) and grid=true (not default), overplot a WCS @@ -213,7 +211,9 @@ xlims := Tuple(axes(cbimg, 2)) ylims := Tuple(axes(cbimg, 2)) title := "" - view(cbimg, reverse(axes(cbimg,1)),:) + # Views of images are not currently supported by plotly so we have to collect them + # view(cbimg, reverse(axes(cbimg,1)),:) + cbimg[reverse(axes(cbimg,1)),:] end end @@ -243,7 +243,7 @@ yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), wcsax(dims(img,2)), gridspec.tickpos2w)) yguide --> ctype_label(wcs(imgv).ctype[wcsax(dims(img,2))], wcs(imgv).radesys) end - view(parent(imgv), reverse(axes(imgv,1)),:) + view(collect(imgv), reverse(axes(imgv,1)),:) end if showcolorbar From 918db4f2ca27574028682fc6d275b393948206af Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 8 Apr 2022 11:12:29 -0700 Subject: [PATCH 103/178] For empty HDUs return a zero dim array of missing. That way people can still access headers but we don't auto show an image. --- src/io.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/io.jl b/src/io.jl index 6419a5d1..cb7346df 100644 --- a/src/io.jl +++ b/src/io.jl @@ -109,7 +109,7 @@ function _loadhdu(hdu::FITSIO.ImageHDU, args...; kwargs...) else # Sometimes files have an empty data HDU that shows up as an image HDU but has headers. # Fallback to creating an empty AstroImage with those headers. - emptydata = fill(0, (0, 0)) + emptydata = fill(missing) return AstroImage(emptydata, (), (), read_header(hdu), Ref(emptywcs(emptydata)), Ref(false)) end end From 747fcc8956cfc09fedc5ed0d67836dc729a37640 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 10 Apr 2022 09:29:00 -0700 Subject: [PATCH 104/178] Removed WCS Axes concept in favour of tracking the order of dims inside each struct --- src/AstroImages.jl | 84 +++++++++++++++++++++++++------------------ src/contrib/images.jl | 21 +++++++---- src/imview.jl | 73 +++++++++++++++++++------------------ src/plot-recipes.jl | 24 ++++++------- src/wcs.jl | 14 ++++---- 5 files changed, 117 insertions(+), 99 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 2338bc32..b80851f2 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -12,12 +12,12 @@ using WCS using Statistics using MappedArrays using ColorSchemes -using PlotUtils: zscale using DimensionalData using Tables using RecipesBase using AstroAngles using Printf +using PlotUtils: PlotUtils using PlotUtils: optimize_ticks, AbstractColorList @@ -32,7 +32,7 @@ export load, ccd2rgb, composechannels, reset!, - zscale, + zscale3, percent, logstretch, powstretch, @@ -82,7 +82,7 @@ end Provides access to a FITS image along with its accompanying header and WCS information, if applicable. """ -struct AstroImage{T,N,D<:Tuple,R<:Tuple,A<:AbstractArray{T,N}} <: AbstractDimArray{T,N,D,A} +struct AstroImage{T,N,D<:Tuple,R<:Tuple,A<:AbstractArray{T,N},W<:Tuple} <: AbstractDimArray{T,N,D,A} # Parent array we are wrapping data::A # Fields for DimensionalData @@ -96,10 +96,12 @@ struct AstroImage{T,N,D<:Tuple,R<:Tuple,A<:AbstractArray{T,N}} <: AbstractDimArr # The next access to the wcs object will regenerate from # the new header on demand. wcs_stale::Base.RefValue{Bool} + # Correspondance between dims & refdims -> WCS Axis numbers + wcs_dims::W end # Provide type aliases for 1D and 2D versions of our data structure. -const AstroImageVec{T,D,R,A} = AstroImage{T,1,D,R,A} where {T,D,R,A} -const AstroImageMat{T,D,R,A} = AstroImage{T,2,D,R,A} where {T,D,R,A} +const AstroImageVec{T,D} = AstroImage{T,1} where {T} +const AstroImageMat{T,D} = AstroImage{T,2} where {T} # Re-export symbols from DimensionalData that users will need # for indexing. @@ -131,18 +133,27 @@ const dimnames = ( const Spec = Dim{:Spec} const Pol = Dim{:Pol} -struct Wcs{N,T} <: DimensionalData.Dimension{T} - val::T +# struct Wcs{N,T} <: DimensionalData.Dimension{T} +# val::T +# end +# Wcs{N}(val::T) where {N,T} = Wcs{N,T}(val) +# Wcs{N}() where N = Wcs{N}(:) +# DimensionalData.name(::Type{<:Wcs{N}}) where N = Symbol("Wcs$N") +# DimensionalData.basetypeof(::Type{<:Wcs{N}}) where N = Wcs{N} +# # DimensionalData.key2dim(::Val{N}) where N<:Integer = Wcs{N}() +# DimensionalData.dim2key(::Type{D}) where D<:Wcs{N} where N = Symbol("Wcs$N") +# wcsax(::Wcs{N}) where N = N + +""" + wcsax(img, dim) + +Return the WCS axis number associated with a dimension. +""" +function wcsax(img::AstroImage, dim) + return findfirst(di->name(di)==name(dim), img.wcs_dims) end -Wcs{N}(val::T) where {N,T} = Wcs{N,T}(val) -Wcs{N}() where N = Wcs{N}(:) -DimensionalData.name(::Type{<:Wcs{N}}) where N = Symbol("Wcs$N") -DimensionalData.basetypeof(::Type{<:Wcs{N}}) where N = Wcs{N} -# DimensionalData.key2dim(::Val{N}) where N<:Integer = Wcs{N}() -DimensionalData.dim2key(::Type{D}) where D<:Wcs{N} where N = Symbol("Wcs$N") -wcsax(::Wcs{N}) where N = N -export Spec, Pol, Wcs -# TODO: Sep? + +export Spec, Pol#, Wcs # Accessors header(img::AstroImage) = getfield(img, :header) @@ -184,8 +195,9 @@ DimensionalData.metadata(::AstroImage) = DimensionalData.Dimensions.LookupArrays # A cached WCSTransform object for this data wcs::WCSTransform=getfield(img, :wcs)[], wcs_stale::Bool=getfield(img, :wcs_stale)[], + wcsdims::Tuple=(dims...,refdims...), ) - return AstroImage(data, dims, refdims, header, Ref(wcs), Ref(wcs_stale)) + return AstroImage(data, dims, refdims, header, Ref(wcs), Ref(wcs_stale), wcsdims) end @inline DimensionalData.rebuildsliced( f::Function, @@ -195,11 +207,11 @@ end header=deepcopy(header(img)), wcs=getfield(img, :wcs)[], wcs_stale=getfield(img, :wcs_stale)[], -) = rebuild(img, data, DimensionalData.slicedims(f, img, I)..., nothing, nothing, header, wcs, wcs_stale) + wcsdims=getfield(img, :wcs_dims), +) = rebuild(img, data, DimensionalData.slicedims(f, img, I)..., nothing, nothing, header, wcs, wcs_stale, wcsdims) -# For these functions that return lazy wrappers, we want to -# share header -# Return result wrapped in array +# Return result wrapped in AstroImage +# For these functions that return lazy wrappers, we want to share header for f in [ :(Base.adjoint), :(Base.transpose), @@ -231,7 +243,7 @@ function AstroImage( refdims::Union{Tuple,NamedTuple}=(), header::FITSHeader=emptyheader(), wcs::Union{WCSTransform,Nothing}=nothing; - wcsdims=false + wcsdims=nothing ) where {T, N} wcs_stale = isnothing(wcs) if isnothing(wcs) @@ -249,11 +261,11 @@ function AstroImage( # Fields for DimensionalData. # TODO: cleanup logic if dims == () - if wcsdims - ourdims = Tuple(Wcs{i} for i in 1:ndims(data)) - else + # if wcsdims + # ourdims = Tuple(Wcs{i} for i in 1:ndims(data)) + # else ourdims = dimnames[1:ndims(data)] - end + # end dims = map(ourdims, axes(data)) do dim, ax dim(ax) end @@ -279,33 +291,35 @@ function AstroImage( error("Number of dims does not match the shape of the data") end - return AstroImage(data, dims, refdims, header, Ref(wcs), Ref(wcs_stale)) + if isnothing(wcsdims) + wcsdims = (dims...,refdims...) + end + + return AstroImage(data, dims, refdims, header, Ref(wcs), Ref(wcs_stale), wcsdims) end function AstroImage( darr::AbstractDimArray, header::FITSHeader=emptyheader(), wcs::Union{WCSTransform,Nothing}=nothing; - wcsdims=false ) wcs_stale = isnothing(wcs) if isnothing(wcs) wcs = emptywcs(darr) end - return AstroImage(parent(darr), dims(darr), refdims(darr), header, Ref(wcs), Ref(wcs_stale)) + wcsdims = (dims(darr)..., refdims(darr)...) + return AstroImage(parent(darr), dims(darr), refdims(darr), header, Ref(wcs), Ref(wcs_stale), wcsdims) end AstroImage( data::AbstractArray, dims::Union{Tuple,NamedTuple}, header::FITSHeader, wcs::Union{WCSTransform,Nothing}=nothing; - wcsdims=false -) = AstroImage(data, dims, (), header, wcs; wcsdims) +) = AstroImage(data, dims, (), header, wcs) AstroImage( data::AbstractArray, header::FITSHeader, wcs::Union{WCSTransform,Nothing}=nothing; - wcsdims=false -) = AstroImage(data, (), (), header, wcs; wcsdims) +) = AstroImage(data, (), (), header, wcs) # TODO: ensure this gets WCS dims. @@ -376,7 +390,7 @@ header of `imgnew` does not affect the header of `img`. See also: [`shareheader`](@ref). """ copyheader(img::AstroImage, data::AbstractArray) = - AstroImage(data, dims(img), refdims(img), deepcopy(header(img)), Ref(getfield(img, :wcs)[]), Ref(getfield(img, :wcs_stale)[])) + AstroImage(data, dims(img), refdims(img), deepcopy(header(img)), Ref(getfield(img, :wcs)[]), Ref(getfield(img, :wcs_stale)[]), getfield(img,:wcs_dims)) export copyheader """ @@ -386,7 +400,7 @@ using the data of the AbstractArray `data`. The two images have synchronized header; modifying one also affects the other. See also: [`copyheader`](@ref). """ -shareheader(img::AstroImage, data::AbstractArray) = AstroImage(data, dims(img), refdims(img), header(img), Ref(getfield(img, :wcs)[]), Ref(getfield(img, :wcs_stale)[])) +shareheader(img::AstroImage, data::AbstractArray) = AstroImage(data, dims(img), refdims(img), header(img), Ref(getfield(img, :wcs)[]), Ref(getfield(img, :wcs_stale)[]), getfield(img,:wcs_dims)) export shareheader # Share header if an AstroImage, do nothing if AbstractArray maybe_shareheader(img::AstroImage, data) = shareheader(img, data) diff --git a/src/contrib/images.jl b/src/contrib/images.jl index ab27cf12..c3312290 100644 --- a/src/contrib/images.jl +++ b/src/contrib/images.jl @@ -33,10 +33,13 @@ See also: normedview """ function clampednormedview(img::AbstractArray{T}, lims) where T imgmin, imgmax = lims - Δ = abs(imgmax - imgmin) + Δ = imgmax - imgmin + # Do not introduce NaNs if colorlimits are identical + if Δ == false + Δ = true + end normeddata = mappedarray( - pix -> clamp((pix - imgmin)/Δ, zero(T), one(T)), - pix_norm -> convert(T, pix_norm*Δ + imgmin), + pix -> clamp((pix - imgmin)/Δ, false, true), img ) return maybe_shareheader(img, normeddata) @@ -48,10 +51,13 @@ function clampednormedview(img::AbstractArray{T}, lims) where T <: Normed return img end imgmin, imgmax = lims - Δ = abs(imgmax - imgmin) + Δ = imgmax - imgmin + # Do not introduce NaNs if colorlimits are identical + if Δ == false + Δ = true + end normeddata = mappedarray( - pix -> clamp((pix - imgmin)/Δ, zero(T), one(T)), - # pix_norm -> pix_norm*Δ + imgmin, # TODO + pix -> clamp((pix - imgmin)/Δ, false, true), img ) return maybe_shareheader(img, normeddata) @@ -66,9 +72,10 @@ function ImageTransformations.restrict(img::AstroImage, region::Dims) restricted = restrict(arraydata(img), region) steps = cld.(size(img), size(restricted)) newdims = Tuple(d[begin:s:end] for (d,s) in zip(dims(img),steps)) - return AstroImage(restricted, newdims, refdims(img), header(img), Ref(getfield(img, :wcs)[]), Ref(getfield(img, :wcs_stale)[])) + return rebuild(img, restricted, newdims) end + # TODO: use WCS info # ImageCore.pixelspacing(img::ImageMeta) = pixelspacing(arraydata(img)) diff --git a/src/imview.jl b/src/imview.jl index 7043ee05..312ecc5c 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -25,10 +25,32 @@ struct percent trim::Float64 percent(percentage::Number) = new(Float64(percentage), (1 - percentage/100)/2) end -(p::percent)(data::AbstractMatrix) = quantile(vec(data), (p.trim, 1-p.trim)) -(p::percent)(data) = quantile(data, (p.trim, 1-p.trim)) +(p::percent)(data::AbstractArray) = quantile(vec(data), (p.trim, 1-p.trim)) +(p::percent)(data) = p(collect(data)) Base.show(io::IO, p::percent; kwargs...) = print(io, "percent($(p.perc))", kwargs...) + +""" + zscale(data) + +Wraps PlotUtils.zscale to first collect iterators. +""" +Base.@kwdef struct zscale3 + nsamples::Int=1000 + contrast::Float64=0.25 + max_reject::Float64=0.5 + min_npixels::Float64=5 + k_rej::Float64=2.5 + max_iterations::Int=5 +end +(z::zscale3)(data::AbstractArray) = PlotUtils.zscale(vec(data), z.nsamples; z.contrast, z.max_reject, z.min_npixels, z.k_rej, z.max_iterations) +(z::zscale3)(data) = z(collect(data)) +Base.show(io::IO, z::zscale3; kwargs...) = print(io, "zscale()", kwargs...) + +zscale2(data::AbstractArray) = PlotUtils.zscale(data) +zscale2(data) = PlotUtils.zscale(collect(data)) + + const _default_cmap = Base.RefValue{Union{Symbol,Nothing}}(:magma)#nothing) const _default_clims = Base.RefValue{Any}(percent(99.5)) const _default_stretch = Base.RefValue{Any}(identity) @@ -80,7 +102,7 @@ end _lookup_cmap(::Nothing) = ColorSchemes.colorschemes[:grays] _lookup_cmap(acl::AbstractColorList) = acl -function _resolve_clims(img, clims) +function _resolve_clims(img::AbstractArray, clims) # Tuple or abstract array if typeof(clims) <: AbstractArray || typeof(clims) <: Tuple if length(clims) != 2 @@ -92,6 +114,7 @@ function _resolve_clims(img, clims) else imgmin, imgmax = clims(skipmissingnan(img)) end + return imgmin, imgmax end @@ -189,21 +212,7 @@ function imview( return map(px->RGBA{N0f8}(0,0,0,0), imgT) end - # TODO: Images.jl has logic to downsize huge images before displaying them. - # We should use that here before applying all this processing instead of - # letting Images.jl handle it after. - - # Users can pass clims as an array or tuple containing the minimum and maximum values - if typeof(clims) <: AbstractArray || typeof(clims) <: Tuple - if length(clims) != 2 - error("clims must have exactly two values if provided.") - end - imgmin = first(clims) - imgmax = last(clims) - # Or as a callable that computes them given an iterator - else - imgmin, imgmax = clims(skipmissingnan(imgT)) - end + imgmin, imgmax = _resolve_clims(imgT, clims) normed = clampednormedview(imgT, (imgmin, imgmax)) return _imview(imgT, normed, stretch, _lookup_cmap(cmap), contrast, bias) end @@ -244,19 +253,7 @@ end function _imview(img, normed::AbstractArray{T}, stretch, cmap, contrast, bias) where T - if T <: Union{Missing,<:Number} - TT = typeof(first(skipmissing(normed))) - else - TT = T - end - if TT == Bool - TT = N0f8 - end - - stretchmin = stretch(zero(TT)) - stretchmax = stretch(one(TT)) - - mapper = mappedarray(img, normed) do pixr, pixn + function colormap(pixr, pixn)::RGBA{N0f8} if ismissing(pixr) || !isfinite(pixr) || ismissing(pixn) || !isfinite(pixn) # We check pixr in addition to pixn because we want to preserve if the pixels # are +-Inf @@ -266,19 +263,21 @@ function _imview(img, normed::AbstractArray{T}, stretch, cmap, contrast, bias) w end # We treat NaN/missing values as transparent - return if ismissing(stretched) || isnan(stretched) - RGBA{TT}(0,0,0,0) + pix= if ismissing(stretched) || isnan(stretched) + RGBA{N0f8}(0,0,0,0) # We treat Inf values as white / -Inf as black elseif isinf(stretched) if stretched > 0 - RGBA{TT}(1,1,1,1) + RGBA{N0f8}(1,1,1,1) else - RGBA{TT}(0,0,0,1) + RGBA{N0f8}(0,0,0,1) end else - RGBA{TT}(get(cmap, stretched, (stretchmin, stretchmax))) - end::RGBA{TT} + RGBA{N0f8}(get(cmap, stretched, (false, true))) + end + return pix end + mapper = mappedarray(colormap, img, normed) return maybe_copyheader(img, mapper) end diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index bbb9c321..16e17e63 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -14,11 +14,9 @@ # Show WCS coordinates if wcsticks is true or unspecified, and has at least one WCS axis present. showwcsticks = (!haskey(plotattributes, :wcsticks) || plotattributes[:wcsticks]) && - any(d->typeof(d) <: Wcs, dims(data)) && !all(==(""), wcs(data).ctype) showwcstitle = (!haskey(plotattributes, :wcstitle) || plotattributes[:wcstitle]) && length(refdims(data)) > 0 && - any(d->typeof(d) <: Wcs, dims(data)) && !all(==(""), wcs(data).ctype) @@ -99,7 +97,7 @@ if showwcstitle refdimslabel = join(map(refdims(imgv)) do d # match dimension with the wcs axis number - i = wcsax(d) + i = wcsax(imgv, d) ct = wcs(imgv).ctype[i] label = ctype_label(ct, wcs(imgv).radesys) value = pix_to_world(imgv, [1,1], all=true, parent=true)[i] @@ -137,11 +135,11 @@ # the transformation from pixel to coordinates can be non-linear and curved. if showwcsticks - xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), wcsax(dims(img,1)), gridspec.tickpos1w)) - xguide --> ctype_label(wcs(imgv).ctype[wcsax(dims(img,1))], wcs(imgv).radesys) + xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), wcsax(imgv, dims(imgv,1)), gridspec.tickpos1w)) + xguide --> ctype_label(wcs(imgv).ctype[wcsax(imgv, dims(imgv,1))], wcs(imgv).radesys) - yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), wcsax(dims(img,2)), gridspec.tickpos2w)) - yguide --> ctype_label(wcs(imgv).ctype[wcsax(dims(img,2))], wcs(imgv).radesys) + yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), wcsax(imgv, dims(imgv,2)), gridspec.tickpos2w)) + yguide --> ctype_label(wcs(imgv).ctype[wcsax(imgv, dims(imgv,2))], wcs(imgv).radesys) end @@ -237,11 +235,11 @@ # the transformation from pixel to coordinates can be non-linear and curved. if showwcsticks - xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), wcsax(dims(img,1)), gridspec.tickpos1w)) - xguide --> ctype_label(wcs(imgv).ctype[wcsax(dims(img,1))], wcs(imgv).radesys) + xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), wcsax(imgv, dims(imgv,1)), gridspec.tickpos1w)) + xguide --> ctype_label(wcs(imgv).ctype[wcsax(imgv, dims(imgv,1))], wcs(imgv).radesys) - yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), wcsax(dims(img,2)), gridspec.tickpos2w)) - yguide --> ctype_label(wcs(imgv).ctype[wcsax(dims(img,2))], wcs(imgv).radesys) + yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), wcsax(imgv, dims(imgv,2)), gridspec.tickpos2w)) + yguide --> ctype_label(wcs(imgv).ctype[wcsax(imgv, dims(imgv,2))], wcs(imgv).radesys) end view(collect(imgv), reverse(axes(imgv,1)),:) end @@ -522,8 +520,8 @@ end end annotate = haskey(plotattributes, :gridlabels) && plotattributes[:gridlabels] - xguide --> ctype_label(wcs(wcsg.img).ctype[wcsax(dims(wcsg.img,1))], wcs(wcsg.img).radesys) - yguide --> ctype_label(wcs(wcsg.img).ctype[wcsax(dims(wcsg.img,2))], wcs(wcsg.img).radesys) + xguide --> ctype_label(wcs(wcsg.img).ctype[wcsax(wcsg.img, dims(wcsg.img,1))], wcs(wcsg.img).radesys) + yguide --> ctype_label(wcs(wcsg.img).ctype[wcsax(wcsg.img, dims(wcsg.img,2))], wcs(wcsg.img).radesys) xlims --> wcsg.extent[1], wcsg.extent[2] ylims --> wcsg.extent[3], wcsg.extent[4] diff --git a/src/wcs.jl b/src/wcs.jl index 9f3f5306..bc1695d6 100644 --- a/src/wcs.jl +++ b/src/wcs.jl @@ -436,11 +436,11 @@ function WCS.pix_to_world(img::AstroImage, pixcoords; all=false, parent=false) end # out = zeros(Float64, length(dims(img))+length(refdims(img)), size(pixcoords,2)) for (i, dim) in enumerate(dims(img)) - j = wcsax(dim) + j = wcsax(img, dim) parentcoords_prepared[j,:] .= parentcoords[i,:] .- 1 end for dim in refdims(img) - j = wcsax(dim) + j = wcsax(img, dim) parentcoords_prepared[j,:] .= dim[1] .- 1 end @@ -460,7 +460,7 @@ function WCS.pix_to_world(img::AstroImage, pixcoords; all=false, parent=false) world_coords_of_these_axes = zeros(length(dims(img))) end for (i, dim) in enumerate(dims(img)) - j = wcsax(dim) + j = wcsax(img, dim) world_coords_of_these_axes[i,:] .= worldcoords_out[j,:] end @@ -501,11 +501,11 @@ function WCS.world_to_pix!(pixcoords_out, img::AstroImage, worldcoords; parent=f # output to only include the dims of the current slice? # out = zeros(Float64, length(dims(img))+length(refdims(img)), size(worldcoords,2)) for (i, dim) in enumerate(dims(img)) - j = wcsax(dim) + j = wcsax(img, dim) worldcoords_prepared[j,:] = worldcoords[i,:] end for dim in refdims(img) - j = wcsax(dim) + j = wcsax(img, dim) worldcoords_prepared[j,:] .= dim[1] end @@ -517,12 +517,12 @@ function WCS.world_to_pix!(pixcoords_out, img::AstroImage, worldcoords; parent=f coordoffsets = zeros(length(dims(img))+length(refdims(img))) coordsteps = zeros(length(dims(img))+length(refdims(img))) for (i, dim) in enumerate(dims(img)) - j = wcsax(dim) + j = wcsax(img, dim) coordoffsets[j] = first(dims(img)[i]) coordsteps[j] = step(dims(img)[i]) end for dim in refdims(img) - j = wcsax(dim) + j = wcsax(img, dim) coordoffsets[j] = first(dim) coordsteps[j] = step(dim) end From 68eb2c790b9b3dc07e63d08007fa1d8425aa5eb6 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 10 Apr 2022 09:34:36 -0700 Subject: [PATCH 105/178] remove wcsdims flag --- src/io.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/io.jl b/src/io.jl index cb7346df..1930b0c8 100644 --- a/src/io.jl +++ b/src/io.jl @@ -55,23 +55,23 @@ end """ -load(fitsfile::String; wcsdims=false) +load(fitsfile::String) Read and return the data from the first ImageHDU in a FITS file as an AstroImage. If no ImageHDUs are present, an error is returned. -load(fitsfile::String, ext::Int; wcsdims=false) +load(fitsfile::String, ext::Int) Read and return the data from the HDU `ext`. If it is an ImageHDU, as AstroImage is returned. If it is a TableHDU, a plain Julia column table is returned. -load(fitsfile::String, :; wcsdims=false) +load(fitsfile::String, :) Read and return the data from each HDU in an FITS file. ImageHDUs are returned as AstroImage, and TableHDUs are returned as column tables. -load(fitsfile::String, exts::Union{NTuple, AbstractArray}; wcsdims=false) +load(fitsfile::String, exts::Union{NTuple, AbstractArray}) Read and return the data from the HDUs given by `exts`. ImageHDUs are returned as AstroImage, and TableHDUs are returned as column tables. From 2efd538416c56954337f4a7f7fa8b292dcbc52cf Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 10 Apr 2022 09:37:07 -0700 Subject: [PATCH 106/178] Standardize variable names --- src/AstroImages.jl | 10 +++++----- src/imview.jl | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index b80851f2..80121eaf 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -97,7 +97,7 @@ struct AstroImage{T,N,D<:Tuple,R<:Tuple,A<:AbstractArray{T,N},W<:Tuple} <: Abstr # the new header on demand. wcs_stale::Base.RefValue{Bool} # Correspondance between dims & refdims -> WCS Axis numbers - wcs_dims::W + wcsdims::W end # Provide type aliases for 1D and 2D versions of our data structure. const AstroImageVec{T,D} = AstroImage{T,1} where {T} @@ -150,7 +150,7 @@ const Pol = Dim{:Pol} Return the WCS axis number associated with a dimension. """ function wcsax(img::AstroImage, dim) - return findfirst(di->name(di)==name(dim), img.wcs_dims) + return findfirst(di->name(di)==name(dim), img.wcsdims) end export Spec, Pol#, Wcs @@ -207,7 +207,7 @@ end header=deepcopy(header(img)), wcs=getfield(img, :wcs)[], wcs_stale=getfield(img, :wcs_stale)[], - wcsdims=getfield(img, :wcs_dims), + wcsdims=getfield(img, :wcsdims), ) = rebuild(img, data, DimensionalData.slicedims(f, img, I)..., nothing, nothing, header, wcs, wcs_stale, wcsdims) # Return result wrapped in AstroImage @@ -390,7 +390,7 @@ header of `imgnew` does not affect the header of `img`. See also: [`shareheader`](@ref). """ copyheader(img::AstroImage, data::AbstractArray) = - AstroImage(data, dims(img), refdims(img), deepcopy(header(img)), Ref(getfield(img, :wcs)[]), Ref(getfield(img, :wcs_stale)[]), getfield(img,:wcs_dims)) + AstroImage(data, dims(img), refdims(img), deepcopy(header(img)), Ref(getfield(img, :wcs)[]), Ref(getfield(img, :wcs_stale)[]), getfield(img,:wcsdims)) export copyheader """ @@ -400,7 +400,7 @@ using the data of the AbstractArray `data`. The two images have synchronized header; modifying one also affects the other. See also: [`copyheader`](@ref). """ -shareheader(img::AstroImage, data::AbstractArray) = AstroImage(data, dims(img), refdims(img), header(img), Ref(getfield(img, :wcs)[]), Ref(getfield(img, :wcs_stale)[]), getfield(img,:wcs_dims)) +shareheader(img::AstroImage, data::AbstractArray) = AstroImage(data, dims(img), refdims(img), header(img), Ref(getfield(img, :wcs)[]), Ref(getfield(img, :wcs_stale)[]), getfield(img,:wcsdims)) export shareheader # Share header if an AstroImage, do nothing if AbstractArray maybe_shareheader(img::AstroImage, data) = shareheader(img, data) diff --git a/src/imview.jl b/src/imview.jl index 312ecc5c..ab9c345a 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -35,7 +35,7 @@ Base.show(io::IO, p::percent; kwargs...) = print(io, "percent($(p.perc))", kwarg Wraps PlotUtils.zscale to first collect iterators. """ -Base.@kwdef struct zscale3 +Base.@kwdef struct zscale nsamples::Int=1000 contrast::Float64=0.25 max_reject::Float64=0.5 @@ -43,9 +43,9 @@ Base.@kwdef struct zscale3 k_rej::Float64=2.5 max_iterations::Int=5 end -(z::zscale3)(data::AbstractArray) = PlotUtils.zscale(vec(data), z.nsamples; z.contrast, z.max_reject, z.min_npixels, z.k_rej, z.max_iterations) -(z::zscale3)(data) = z(collect(data)) -Base.show(io::IO, z::zscale3; kwargs...) = print(io, "zscale()", kwargs...) +(z::zscale)(data::AbstractArray) = PlotUtils.zscale(vec(data), z.nsamples; z.contrast, z.max_reject, z.min_npixels, z.k_rej, z.max_iterations) +(z::zscale)(data) = z(collect(data)) +Base.show(io::IO, z::zscale; kwargs...) = print(io, "zscale()", kwargs...) zscale2(data::AbstractArray) = PlotUtils.zscale(data) zscale2(data) = PlotUtils.zscale(collect(data)) From 24713ac0104085c625190f6e6e657e8c435834b2 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 11 Apr 2022 07:10:44 -0700 Subject: [PATCH 107/178] Fix readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d3fa5539..8f6a00c6 100644 --- a/README.md +++ b/README.md @@ -8,17 +8,17 @@ Introduction ------------ -`AstroImageMat.jl` allows you to plot images from an +`AstroImages.jl` allows you to plot images from an astronomical [`FITS`](https://en.wikipedia.org/wiki/FITS) file using the popular [`Images.jl`](https://github.com/JuliaImages/Images.jl) and [`Plots.jl`](https://github.com/JuliaPlots/Plots.jl) Julia packages. -`AstroImageMat.jl` uses [`FITSIO.jl`](https://github.com/JuliaAstro/FITSIO.jl) to +`AstroImages.jl` uses [`FITSIO.jl`](https://github.com/JuliaAstro/FITSIO.jl) to read FITS files. Installation ------------ -`AstroImageMat.jl` is available for Julia 1.0 and later versions, and can be +`AstroImages.jl` is available for Julia 1.6 and later versions, and can be installed with [Julia built-in package manager](https://docs.julialang.org/en/v1/stdlib/Pkg/). From b91c7eaecd7a38f2b03675dac775aab0e6de03f9 Mon Sep 17 00:00:00 2001 From: Will Thompson Date: Mon, 11 Apr 2022 07:10:52 -0700 Subject: [PATCH 108/178] Update docs/Project.toml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mosè Giordano --- docs/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Project.toml b/docs/Project.toml index fb315d18..3a52a5db 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -2,4 +2,4 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" [compat] -Documenter = "0.27" \ No newline at end of file +Documenter = "0.27" From 80603d3d5bba2ef7f78d4e62c8544b27e3d775f5 Mon Sep 17 00:00:00 2001 From: Will Thompson Date: Mon, 11 Apr 2022 07:11:14 -0700 Subject: [PATCH 109/178] Update src/wcs.jl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mosè Giordano --- src/wcs.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/wcs.jl b/src/wcs.jl index bc1695d6..6092c067 100644 --- a/src/wcs.jl +++ b/src/wcs.jl @@ -206,7 +206,6 @@ const WCS_HEADERS_TEMPLATES = [ ] # Expand the headers containing lower case specifers into N copies -Is = [""; string.(1:4); string.('a':'d')] # Find all lower case templates const WCS_HEADERS = Set(mapreduce(vcat, WCS_HEADERS_TEMPLATES) do template if any(islowercase, template) @@ -216,7 +215,7 @@ const WCS_HEADERS = Set(mapreduce(vcat, WCS_HEADERS_TEMPLATES) do template for replace_target in chars newout = String[] for template in out - for i in Is + for i in [""; string.(1:4); string.('a':'d')] push!(newout, replace(template, replace_target=>i)) end end From 0774e7146da5e553fd384570be4820bd53ea6013 Mon Sep 17 00:00:00 2001 From: Will Thompson Date: Mon, 11 Apr 2022 07:11:31 -0700 Subject: [PATCH 110/178] Update src/io.jl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mosè Giordano --- src/io.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/io.jl b/src/io.jl index 1930b0c8..55f72712 100644 --- a/src/io.jl +++ b/src/io.jl @@ -161,4 +161,4 @@ function writearg(fits, table) # on table HDUs # header=nothing ) -end \ No newline at end of file +end From 67d9edf0b49aad9b0f483958f053bb989c58f9ac Mon Sep 17 00:00:00 2001 From: Will Thompson Date: Mon, 11 Apr 2022 07:11:40 -0700 Subject: [PATCH 111/178] Update src/plot-recipes.jl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mosè Giordano --- src/plot-recipes.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 16e17e63..61e062cd 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -1110,4 +1110,4 @@ There are several ways you can adjust the appearance of the plot using keyword a Use `implot` and `polquiver!` to overplot polarization data over an image. """ -polquiver \ No newline at end of file +polquiver From ee42cbfde117992f7010df8f310caddc0aeb66f1 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 11 Apr 2022 07:12:49 -0700 Subject: [PATCH 112/178] !!! admonitions. --- src/io.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/io.jl b/src/io.jl index 55f72712..b44758da 100644 --- a/src/io.jl +++ b/src/io.jl @@ -76,7 +76,7 @@ load(fitsfile::String, exts::Union{NTuple, AbstractArray}) Read and return the data from the HDUs given by `exts`. ImageHDUs are returned as AstroImage, and TableHDUs are returned as column tables. -!! Currently comments on TableHDUs are not supported and are ignored. +!!! Currently any header on TableHDUs are not supported and are ignored. """ function fileio_load(f::File{format"FITS"}, ext::Union{Int,Nothing}=nothing, args...; kwargs...) where {N} return FITS(f.filename, "r") do fits From e90afa33811106ba88a545e86842deb2f4f43afe Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 11 Apr 2022 07:20:25 -0700 Subject: [PATCH 113/178] Clean up clampednormedview --- src/contrib/images.jl | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/src/contrib/images.jl b/src/contrib/images.jl index c3312290..5e89cc6d 100644 --- a/src/contrib/images.jl +++ b/src/contrib/images.jl @@ -14,6 +14,10 @@ ImageCore.normedview(img::AstroImageMat{<:FixedPoint}) = img function ImageCore.normedview(img::AstroImageMat{T}) where T imgmin, imgmax = extrema(skipmissingnan(img)) Δ = abs(imgmax - imgmin) + # Do not introduce NaNs if limits are identical + if Δ == 0 + Δ = one(imgmin) + end normeddata = mappedarray( pix -> (pix - imgmin)/Δ, pix_norm -> convert(T, pix_norm*Δ + imgmin), @@ -35,35 +39,16 @@ function clampednormedview(img::AbstractArray{T}, lims) where T imgmin, imgmax = lims Δ = imgmax - imgmin # Do not introduce NaNs if colorlimits are identical - if Δ == false - Δ = true - end - normeddata = mappedarray( - pix -> clamp((pix - imgmin)/Δ, false, true), - img - ) - return maybe_shareheader(img, normeddata) -end -function clampednormedview(img::AbstractArray{T}, lims) where T <: Normed - # If the data is in a Normed type and the limits are [0,1] then - # it already lies in that range. - if lims[1] == 0 && lims[2] == 1 - return img - end - imgmin, imgmax = lims - Δ = imgmax - imgmin - # Do not introduce NaNs if colorlimits are identical - if Δ == false - Δ = true + if Δ == 0 + Δ = one(imgmin) end normeddata = mappedarray( - pix -> clamp((pix - imgmin)/Δ, false, true), + pix -> clamp((pix - imgmin)/Δ, zero(pix), one(pix)), img ) return maybe_shareheader(img, normeddata) end - # Restrict downsizes images by roughly a factor of two. # We want to keep the wrapper but downsize the underlying array # TODO: correct dimensions after restrict. From c73adb2a0bd3957003895cf7edfc4c211db5d7aa Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 11 Apr 2022 07:20:40 -0700 Subject: [PATCH 114/178] Cleanup Zscale struct --- src/AstroImages.jl | 2 +- src/imview.jl | 36 +++++++++++++++++++++--------------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 80121eaf..c33ab77b 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -32,7 +32,7 @@ export load, ccd2rgb, composechannels, reset!, - zscale3, + Zscale, percent, logstretch, powstretch, diff --git a/src/imview.jl b/src/imview.jl index ab9c345a..0db504ac 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -20,22 +20,32 @@ julia> imview(img, clims=percent(90)) ``` This will set the limits to be the 5th percentile to the 95th percentile. """ -struct percent +struct Percent perc::Float64 trim::Float64 - percent(percentage::Number) = new(Float64(percentage), (1 - percentage/100)/2) + Percent(percentage::Number) = new(Float64(percentage), (1 - percentage/100)/2) end -(p::percent)(data::AbstractArray) = quantile(vec(data), (p.trim, 1-p.trim)) -(p::percent)(data) = p(collect(data)) -Base.show(io::IO, p::percent; kwargs...) = print(io, "percent($(p.perc))", kwargs...) +(p::Percent)(data::AbstractArray) = quantile(vec(data), (p.trim, 1-p.trim)) +(p::Percent)(data) = p(collect(data)) +Base.show(io::IO, p::Percent; kwargs...) = print(io, "Percent($(p.perc))", kwargs...) """ - zscale(data) + Zscale(options)(data) Wraps PlotUtils.zscale to first collect iterators. + +Default parameters: +``` +nsamples::Int=1000 +contrast::Float64=0.25 +max_reject::Float64=0.5 +min_npixels::Float64=5 +k_rej::Float64=2.5 +max_iterations::Int=5 +``` """ -Base.@kwdef struct zscale +Base.@kwdef struct Zscale nsamples::Int=1000 contrast::Float64=0.25 max_reject::Float64=0.5 @@ -43,13 +53,9 @@ Base.@kwdef struct zscale k_rej::Float64=2.5 max_iterations::Int=5 end -(z::zscale)(data::AbstractArray) = PlotUtils.zscale(vec(data), z.nsamples; z.contrast, z.max_reject, z.min_npixels, z.k_rej, z.max_iterations) -(z::zscale)(data) = z(collect(data)) -Base.show(io::IO, z::zscale; kwargs...) = print(io, "zscale()", kwargs...) - -zscale2(data::AbstractArray) = PlotUtils.zscale(data) -zscale2(data) = PlotUtils.zscale(collect(data)) - +(z::Zscale)(data::AbstractArray) = PlotUtils.zscale(vec(data), z.nsamples; z.contrast, z.max_reject, z.min_npixels, z.k_rej, z.max_iterations) +(z::Zscale)(data) = z(collect(data)) +Base.show(io::IO, z::Zscale; kwargs...) = print(io, "Zscale()", kwargs...) const _default_cmap = Base.RefValue{Union{Symbol,Nothing}}(:magma)#nothing) const _default_clims = Base.RefValue{Any}(percent(99.5)) @@ -67,7 +73,7 @@ function set_cmap!(cmap) end """ set_clims!(clims::Tuple) - set_clims!(clims::Function) + set_clims!(clims::Callable) Alter the default limits used to display images when using `imview` or displaying an AstroImageMat. From 5c9d59060a83b6ecc5f9d34560e47bcb2b8860ae Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 11 Apr 2022 07:43:23 -0700 Subject: [PATCH 115/178] Typo --- src/imview.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/imview.jl b/src/imview.jl index 0db504ac..21169d06 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -58,7 +58,7 @@ end Base.show(io::IO, z::Zscale; kwargs...) = print(io, "Zscale()", kwargs...) const _default_cmap = Base.RefValue{Union{Symbol,Nothing}}(:magma)#nothing) -const _default_clims = Base.RefValue{Any}(percent(99.5)) +const _default_clims = Base.RefValue{Any}(Percent(99.5)) const _default_stretch = Base.RefValue{Any}(identity) """ @@ -126,7 +126,7 @@ end """ - imview(img; clims=extrema, stretch=identity, cmap=:magma, contrast=1.0, bias=0.5) + imview(img; clims=Percent(99.5), stretch=identity, cmap=:magma, contrast=1.0, bias=0.5) Create a read only view of an array or AstroImageMat mapping its data values to Colors according to `clims`, `stretch`, and `cmap`. From 4e575732f82aac770eff232edfaf70f7c46a7c17 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 11 Apr 2022 07:43:36 -0700 Subject: [PATCH 116/178] Add precompile and clean up exports --- src/AstroImages.jl | 17 +++++------------ src/precompile.jl | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 src/precompile.jl diff --git a/src/AstroImages.jl b/src/AstroImages.jl index c33ab77b..85fc8504 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -19,7 +19,7 @@ using AstroAngles using Printf using PlotUtils: PlotUtils using PlotUtils: optimize_ticks, AbstractColorList - +using UUIDs export load, @@ -27,13 +27,11 @@ export load, AstroImage, AstroImageVec, AstroImageMat, - Wcs, WCSGrid, ccd2rgb, - composechannels, - reset!, + # composechannels, Zscale, - percent, + Percent, logstretch, powstretch, sqrtstretch, @@ -42,9 +40,6 @@ export load, sinhstretch, powerdiststretch, imview, - clampednormedview, - # wcsticks, - # wcsgridlines, arraydata, header, wcs, @@ -54,8 +49,7 @@ export load, pix_to_world, pix_to_world!, world_to_pix, - world_to_pix!, - x + world_to_pix! @@ -443,8 +437,7 @@ include("contrib/abstract-ffts.jl") include("contrib/reproject.jl") include("ccd2rgb.jl") -# include("patches.jl") -using UUIDs +include("precompile.jl") function __init__() diff --git a/src/precompile.jl b/src/precompile.jl new file mode 100644 index 00000000..a6a2bd8a --- /dev/null +++ b/src/precompile.jl @@ -0,0 +1,38 @@ + + + +for T in [Float32, Float64, Int, Int8, UInt8, N0f8] + + a = rand(T, 5, 5) + i = AstroImage(a) + for stretch in [ + logstretch, + powstretch, + sqrtstretch, + squarestretch, + asinhstretch, + sinhstretch, + powerdiststretch + ] + # Easiest way to precompile everything we need is just to call these functions. + # They have no side-effects. + imview(a; stretch) + + # And precompile on an astroimage + imview(i; stretch) + end + TI = typeof(i) + precompile(arraydata, (TI,)) + precompile(header, (TI,)) + precompile(wcs, (TI,)) + precompile(getindex, (TI, Symbol)) + precompile(getindex, (TI, String)) + precompile(getindex, (TI, Int)) + precompile(getindex, (TI, Int, Int)) + precompile(getindex, (TI, Vector{Int})) + precompile(getindex, (TI, Vector{Bool})) + precompile(getindex, (TI, Matrix{Bool})) + precompile(setindex!, (TI, Matrix{Bool})) + precompile(world_to_pix, (TI, Vector{Float64})) + precompile(pix_to_world, (TI, Vector{Float64})) +end \ No newline at end of file From 95e2e34205ceab71fceac6422ced55680186fbc8 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 11 Apr 2022 07:55:37 -0700 Subject: [PATCH 117/178] Additional precompile directives --- src/precompile.jl | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/precompile.jl b/src/precompile.jl index a6a2bd8a..eab1b50d 100644 --- a/src/precompile.jl +++ b/src/precompile.jl @@ -35,4 +35,13 @@ for T in [Float32, Float64, Int, Int8, UInt8, N0f8] precompile(setindex!, (TI, Matrix{Bool})) precompile(world_to_pix, (TI, Vector{Float64})) precompile(pix_to_world, (TI, Vector{Float64})) -end \ No newline at end of file +end + + +# From trace-compile: +precompile(Tuple{typeof(AstroImages.imview), Array{Float64, 2}}) +precompile(Tuple{AstroImages.var"##imview#60", AstroImages.Percent, Function, Symbol, Float64, Float64, typeof(AstroImages.imview), Array{Float64, 2}}) +precompile(Tuple{typeof(AstroImages._imview), Base.SubArray{Float64, 2, Array{Float64, 2}, Tuple{Base.StepRange{Int64, Int64}, Base.Slice{Base.OneTo{Int64}}}, false}, MappedArrays.ReadonlyMappedArray{Any, 2, Base.SubArray{Float64, 2, Array{Float64, 2}, Tuple{Base.StepRange{Int64, Int64}, Base.Slice{Base.OneTo{Int64}}}, false}, AstroImages.var"#118#119"{Float64}}, typeof(Base.identity), ColorSchemes.ColorScheme{Array{ColorTypes.RGB{Float64}, 1}, String, String}, Float64, Float64}) +precompile(Tuple{AstroImages.var"#imview##kw", NamedTuple{(:clims, :stretch, :cmap, :contrast, :bias), Tuple{AstroImages.Percent, typeof(Base.identity), Symbol, Int64, Float64}}, typeof(AstroImages.imview), AstroImages.AstroImage{Float64, 2, Tuple{DimensionalData.Dimensions.X{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}, DimensionalData.Dimensions.Y{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}}, Tuple{}, Array{Float64, 2}, Tuple{DimensionalData.Dimensions.X{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}, DimensionalData.Dimensions.Y{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}}}}) +precompile(Tuple{typeof(AstroImages._imview), Base.SubArray{Float64, 2, Array{Float64, 2}, Tuple{Base.StepRange{Int64, Int64}, Base.Slice{Base.OneTo{Int64}}}, false}, MappedArrays.ReadonlyMappedArray{Any, 2, Base.SubArray{Float64, 2, Array{Float64, 2}, Tuple{Base.StepRange{Int64, Int64}, Base.Slice{Base.OneTo{Int64}}}, false}, AstroImages.var"#118#119"{Float64}}, typeof(Base.identity), ColorSchemes.ColorScheme{Array{ColorTypes.RGB{Float64}, 1}, String, String}, Int64, Float64}) +precompile(Tuple{AstroImages.var"#imview_colorbar##kw", NamedTuple{(:clims, :stretch, :cmap, :contrast, :bias), Tuple{AstroImages.Percent, typeof(Base.identity), Symbol, Int64, Float64}}, typeof(AstroImages.imview_colorbar), AstroImages.AstroImage{Float64, 2, Tuple{DimensionalData.Dimensions.X{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}, DimensionalData.Dimensions.Y{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}}, Tuple{}, Array{Float64, 2}, Tuple{DimensionalData.Dimensions.X{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}, DimensionalData.Dimensions.Y{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}}}}) \ No newline at end of file From 6e94d7d5b6a0e513eb284d70ed44d0ffc5b02014 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 11 Apr 2022 08:08:37 -0700 Subject: [PATCH 118/178] Begin adapting tests and fixing breakages --- src/io.jl | 8 +- test/runtests.jl | 268 ++++++++++++++++++++--------------------------- 2 files changed, 123 insertions(+), 153 deletions(-) diff --git a/src/io.jl b/src/io.jl index b44758da..74167fbe 100644 --- a/src/io.jl +++ b/src/io.jl @@ -45,6 +45,12 @@ function AstroImage(filename::AbstractString, exts::Union{NTuple{N,<:Integer},Ab end end end +function AstroImage(filename::AbstractString; kwargs...) where {N} + return FITS(filename, "r") do fits + ext = indexer(fits) + return AstroImage(fits[ext]; kwargs...) + end +end function AstroImage(filename::AbstractString, ::Colon, args...; kwargs...) where {N} return FITS(filename, "r") do fits return map(fits) do hdu @@ -116,7 +122,7 @@ end function indexer(fits::FITS) ext = 0 for (i, hdu) in enumerate(fits) - if hdu isa ImageHDU && length(size(hdu)) >= 2# check if Image is atleast 2D + if hdu isa ImageHDU && length(size(hdu)) >= 1 # check if Image is atleast 1D ext = i break end diff --git a/test/runtests.jl b/test/runtests.jl index ceb8ea86..38d5ea1f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,9 +1,9 @@ -using AstroImages, FITSIO, Images, Random, Widgets, WCS, JLD +using AstroImages, FITSIO, Images, Random, WCS using Test, WCS using SHA: sha256 -import AstroImages: _float, render, _brightness_contrast, brightness_contrast +import AstroImages: _float, render @testset "Conversion to float and fixed-point" begin @testset "Float" begin @@ -37,29 +37,15 @@ end FITS(fname, "w") do f write(f, data) end - @test load(fname, 1)[1] == data - @test load(fname, (1, 1))[1] == (data, data) - img = AstroImageMat(fname) - rendered_img = colorview(img) - @test iszero(minimum(rendered_img)) + @test load(fname, 1) == data + @test load(fname, (1, 1)) == (data, data) + img = AstroImage(fname) + rendered_img = imview(img) + @test eltype(rendered_img) <: RGBA end rm(fname, force = true) end -@testset "Control contrast" begin - @test @inferred(_brightness_contrast(Gray, ones(Float32, 2, 2), 100, 100)) isa - Array{Gray{Float32},2} - Random.seed!(1) - M = rand(2, 2) - @test @inferred(_brightness_contrast(Gray, M, 100, 100)) ≈ - Gray.([0.48471895908315565 0.514787046406301; - 0.5280458879184159 0.39525854250610226]) rtol = 1e-12 - @test @inferred(_brightness_contrast(Gray, M, 0, 255)) == Gray.(M) - @test @inferred(_brightness_contrast(Gray, M, 255, 0)) == Gray.(ones(size(M))) - @test @inferred(_brightness_contrast(Gray, M, 0, 0)) == Gray.(zeros(size(M))) - @test brightness_contrast(AstroImageMat(M)) isa Widgets.Widget{:manipulate,Any} -end - @testset "default handler" begin fname = tempname() * ".fits" @testset "less dimensions than 2" begin @@ -67,7 +53,7 @@ end FITS(fname, "w") do f write(f, data) end - @test_throws ErrorException AstroImageMat(fname) + @test ndims(AstroImage(fname)) == 1 end @testset "no ImageHDU" begin @@ -86,23 +72,23 @@ end FITS(fname, "w") do f write(f, indata; varcols=["vcol", "VCOL"]) - @test_throws MethodError AstroImageMat(f) + @test_throws Exception AstroImage(f) end end - @testset "Opening AstroImageMat in different ways" begin + @testset "Opening AstroImage in different ways" begin data = rand(2,2) wcs = WCSTransform(2;) FITS(fname, "w") do f write(f, data) end f = FITS(fname) - @test AstroImageMat(fname, 1) isa AstroImageMat - @test AstroImageMat(Gray ,fname, 1) isa AstroImageMat - @test AstroImageMat(Gray, f, 1) isa AstroImageMat - @test AstroImageMat(data, wcs) isa AstroImageMat - @test AstroImageMat((data,data), (wcs,wcs)) isa AstroImageMat - @test AstroImageMat(Gray, data, wcs) isa AstroImageMat + header = read_header(f[1]) + @test AstroImage(fname, 1) isa AstroImage + @test AstroImage(f, 1) isa AstroImage + @test AstroImage(data, header) isa AstroImage + @test AstroImage(data, wcs) isa AstroImage + @test AstroImage(data, wcs) isa AstroImage close(f) end @@ -125,132 +111,110 @@ end write(f, rand(2, 2)) end - @test @test_logs (:info, "Image was loaded from HDU 3") AstroImageMat(fname) isa AstroImageMat + @test @test_logs (:info, "Image was loaded from HDU 3") AstroImage(fname) isa AstroImage end rm(fname, force = true) end @testset "Utility functions" begin - @test size(AstroImageMat((rand(10,10), rand(10,10)))) == ((10,10), (10,10)) - @test length(AstroImageMat((rand(10,10), rand(10,10)))) == 2 -end - -@testset "multi image AstroImageMat" begin - data1 = rand(10,10) - data2 = rand(10,10) - fname = tempname() * ".fits" - FITS(fname, "w") do f - write(f, data1) - write(f, data2) - end - - img = AstroImageMat(fname, (1,2)) - @test length(img.data) == 2 - @test img.data[1] == data1 - @test img.data[2] == data2 - - f = FITS(fname) - img = AstroImageMat(Gray, f, (1,2)) - @test length(img.data) == 2 - @test img.data[1] == data1 - @test img.data[2] == data2 - close(f) -end - -@testset "multi wcs AstroImageMat" begin - fname = tempname() * ".fits" - f = FITS(fname, "w") - inhdr = FITSHeader(["CTYPE1", "CTYPE2", "RADESYS", "FLTKEY", "INTKEY", "BOOLKEY", "STRKEY", "COMMENT", - "HISTORY"], - ["RA---TAN", "DEC--TAN", "UNK", 1.0, 1, true, "string value", nothing, nothing], - ["", - "", - "", - "floating point keyword", - "", - "boolean keyword", - "string value", - "this is a comment", - "this is a history"]) - - indata = reshape(Float32[1:100;], 5, 20) - write(f, indata; header=inhdr) - write(f, indata; header=inhdr) - close(f) - - img = AstroImageMat(fname, (1,2)) - f = FITS(fname) - @test length(img.wcs) == 2 - @test WCS.to_header(img.wcs[1]) === WCS.to_header(WCS.from_header(read_header(f[1], String))[1]) - @test WCS.to_header(img.wcs[2]) === WCS.to_header(WCS.from_header(read_header(f[2], String))[1]) - - img = AstroImageMat(Gray, f, (1,2)) - @test length(img.wcs) == 2 - @test WCS.to_header(img.wcs[1]) === WCS.to_header(WCS.from_header(read_header(f[1], String))[1]) - @test WCS.to_header(img.wcs[2]) === WCS.to_header(WCS.from_header(read_header(f[2], String))[1]) - close(f) -end - -@testset "multi file AstroImageMat" begin - fname1 = tempname() * ".fits" - f = FITS(fname1, "w") - inhdr = FITSHeader(["CTYPE1", "CTYPE2", "RADESYS", "FLTKEY", "INTKEY", "BOOLKEY", "STRKEY", "COMMENT", - "HISTORY"], - ["RA---TAN", "DEC--TAN", "UNK", 1.0, 1, true, "string value", nothing, nothing], - ["", - "", - "", - "floating point keyword", - "", - "boolean keyword", - "string value", - "this is a comment", - "this is a history"]) - - indata1 = reshape(Int[1:100;], 5, 20) - write(f, indata1; header=inhdr) - close(f) - - fname2 = tempname() * ".fits" - f = FITS(fname2, "w") - indata2 = reshape(Int[1:100;], 5, 20) - write(f, indata2; header=inhdr) - close(f) - - fname3 = tempname() * ".fits" - f = FITS(fname3, "w") - indata3 = reshape(Int[1:100;], 5, 20) - write(f, indata3; header=inhdr) - close(f) - - img = AstroImageMat((fname1, fname2, fname3)) - f1 = FITS(fname1) - f2 = FITS(fname2) - f3 = FITS(fname3) - - @test length(img.data) == length(img.wcs) == 3 - @test img.data[1] == indata1 - @test img.data[2] == indata2 - @test img.data[3] == indata3 - @test WCS.to_header(img.wcs[1]) == WCS.to_header(img.wcs[2]) == - WCS.to_header(img.wcs[3]) == WCS.to_header(WCS.from_header(read_header(f1[1], String))[1]) - @test eltype(eltype(img.data)) == Int - - img = AstroImageMat(Gray, (f1, f2, f3), (1,1,1)) - @test length(img.data) == length(img.wcs) == 3 - @test img.data[1] == indata1 - @test img.data[2] == indata2 - @test img.data[3] == indata3 - @test WCS.to_header(img.wcs[1]) == WCS.to_header(img.wcs[2]) == - WCS.to_header(img.wcs[3]) == WCS.to_header(WCS.from_header(read_header(f1[1], String))[1]) - @test eltype(eltype(img.data)) == Int - close(f1) - close(f2) - close(f3) - rm(fname1, force = true) - rm(fname2, force = true) - rm(fname3, force = true) + @test size(AstroImage(rand(10,10))) == (10,10) + @test length(AstroImage(rand(10,10))) == 100 end -include("plots.jl") -include("ccd2rgb.jl") +# @testset "multi wcs AstroImage" begin +# fname = tempname() * ".fits" +# f = FITS(fname, "w") +# inhdr = FITSHeader(["CTYPE1", "CTYPE2", "RADESYS", "FLTKEY", "INTKEY", "BOOLKEY", "STRKEY", "COMMENT", +# "HISTORY"], +# ["RA---TAN", "DEC--TAN", "UNK", 1.0, 1, true, "string value", nothing, nothing], +# ["", +# "", +# "", +# "floating point keyword", +# "", +# "boolean keyword", +# "string value", +# "this is a comment", +# "this is a history"]) + +# indata = reshape(Float32[1:100;], 5, 20) +# write(f, indata; header=inhdr) +# write(f, indata; header=inhdr) +# close(f) + +# img = AstroImage(fname, (1,2)) +# f = FITS(fname) +# @test length(img.wcs) == 2 +# @test WCS.to_header(img.wcs[1]) === WCS.to_header(WCS.from_header(read_header(f[1], String))[1]) +# @test WCS.to_header(img.wcs[2]) === WCS.to_header(WCS.from_header(read_header(f[2], String))[1]) + +# img = AstroImage(Gray, f, (1,2)) +# @test length(img.wcs) == 2 +# @test WCS.to_header(img.wcs[1]) === WCS.to_header(WCS.from_header(read_header(f[1], String))[1]) +# @test WCS.to_header(img.wcs[2]) === WCS.to_header(WCS.from_header(read_header(f[2], String))[1]) +# close(f) +# end + +# @testset "multi file AstroImage" begin +# fname1 = tempname() * ".fits" +# f = FITS(fname1, "w") +# inhdr = FITSHeader(["CTYPE1", "CTYPE2", "RADESYS", "FLTKEY", "INTKEY", "BOOLKEY", "STRKEY", "COMMENT", +# "HISTORY"], +# ["RA---TAN", "DEC--TAN", "UNK", 1.0, 1, true, "string value", nothing, nothing], +# ["", +# "", +# "", +# "floating point keyword", +# "", +# "boolean keyword", +# "string value", +# "this is a comment", +# "this is a history"]) + +# indata1 = reshape(Int[1:100;], 5, 20) +# write(f, indata1; header=inhdr) +# close(f) + +# fname2 = tempname() * ".fits" +# f = FITS(fname2, "w") +# indata2 = reshape(Int[1:100;], 5, 20) +# write(f, indata2; header=inhdr) +# close(f) + +# fname3 = tempname() * ".fits" +# f = FITS(fname3, "w") +# indata3 = reshape(Int[1:100;], 5, 20) +# write(f, indata3; header=inhdr) +# close(f) + +# img = AstroImage((fname1, fname2, fname3)) +# f1 = FITS(fname1) +# f2 = FITS(fname2) +# f3 = FITS(fname3) + +# @test length(img.data) == length(img.wcs) == 3 +# @test img.data[1] == indata1 +# @test img.data[2] == indata2 +# @test img.data[3] == indata3 +# @test WCS.to_header(img.wcs[1]) == WCS.to_header(img.wcs[2]) == +# WCS.to_header(img.wcs[3]) == WCS.to_header(WCS.from_header(read_header(f1[1], String))[1]) +# @test eltype(eltype(img.data)) == Int + +# img = AstroImage(Gray, (f1, f2, f3), (1,1,1)) +# @test length(img.data) == length(img.wcs) == 3 +# @test img.data[1] == indata1 +# @test img.data[2] == indata2 +# @test img.data[3] == indata3 +# @test WCS.to_header(img.wcs[1]) == WCS.to_header(img.wcs[2]) == +# WCS.to_header(img.wcs[3]) == WCS.to_header(WCS.from_header(read_header(f1[1], String))[1]) +# @test eltype(eltype(img.data)) == Int +# close(f1) +# close(f2) +# close(f3) +# rm(fname1, force = true) +# rm(fname2, force = true) +# rm(fname3, force = true) +# end + +# include("plots.jl") +# include("ccd2rgb.jl") From 2c51c025c1e72a52849a8b6a87955cf54c528b32 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 11 Apr 2022 08:39:28 -0700 Subject: [PATCH 119/178] Fixes for categorical (e.g. pol.) dimensions --- src/wcs.jl | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/wcs.jl b/src/wcs.jl index 6092c067..6a24151d 100644 --- a/src/wcs.jl +++ b/src/wcs.jl @@ -440,7 +440,15 @@ function WCS.pix_to_world(img::AstroImage, pixcoords; all=false, parent=false) end for dim in refdims(img) j = wcsax(img, dim) - parentcoords_prepared[j,:] .= dim[1] .- 1 + # Non numeric reference dims can be used, e.g. a polarization axis of symbols I, Q, U, etc. + if eltype(dim) <: Number + z = dim[1] - 1 + else + # Find the index of the symbol into the parent cube + parentrefdim = img.wcsdims[findfirst(d->name(d)==name(dim), img.wcsdims)] + z = findfirst(==(first(dim)), collect(parentrefdim)) - 1 + end + parentcoords_prepared[j,:] .= z end # Get world coordinates along all slices @@ -505,7 +513,15 @@ function WCS.world_to_pix!(pixcoords_out, img::AstroImage, worldcoords; parent=f end for dim in refdims(img) j = wcsax(img, dim) - worldcoords_prepared[j,:] .= dim[1] + # Non numeric reference dims can be used, e.g. a polarization axis of symbols I, Q, U, etc. + if eltype(dim) <: Number + z = dim[1] + else + # Find the index of the symbol into the parent cube + parentrefdim = img.wcsdims[findfirst(d->name(d)==name(dim), img.wcsdims)] + z = findfirst(==(first(dim)),collect(parentrefdim)) -1 + end + worldcoords_prepared[j,:] .= z end # This returns the parent pixel coordinates. @@ -522,8 +538,17 @@ function WCS.world_to_pix!(pixcoords_out, img::AstroImage, worldcoords; parent=f end for dim in refdims(img) j = wcsax(img, dim) - coordoffsets[j] = first(dim) - coordsteps[j] = step(dim) + # Non numeric reference dims can be used, e.g. a polarization axis of symbols I, Q, U, etc. + if eltype(dim) <: Number + coordoffsets[j] = first(dim) + coordsteps[j] = step(dim) + else + # Find the index of the symbol into the parent cube + parentrefdim = img.wcsdims[findfirst(d->name(d)==name(dim), img.wcsdims)] + z = findfirst(==(first(dim)),collect(parentrefdim)) + coordoffsets[j] = z - 1 + coordsteps[j] = 1 + end end pixcoords_out .-= coordoffsets From 3972fd1602397b360c76b67447adee216b86807a Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 11 Apr 2022 08:41:17 -0700 Subject: [PATCH 120/178] Allow turning grid off with `grid=false` --- src/plot-recipes.jl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 61e062cd..8843273f 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -89,9 +89,7 @@ # we have a wcs flag (from the image by default) so that users can skip over # plotting in physical coordinates. This is especially important # if the WCS headers are mallformed in some way. - showgrid = (!haskey(plotattributes, :xgrid) || haskey(plotattributes, :xgrid)) && - (!haskey(plotattributes, :ygrid) || haskey(plotattributes, :ygrid)) - + showgrid = get(plotattributes, :xgrid, true) && get(plotattributes, :ygrid, true) # Display a title giving our position along unplotted dimensions if length(refdims(imgv)) > 0 if showwcstitle From 574c51a3711d2d7a801d9afdc8ad5145305be88d Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 11 Apr 2022 08:55:26 -0700 Subject: [PATCH 121/178] Fix precompile --- src/io.jl | 2 +- src/precompile.jl | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/io.jl b/src/io.jl index 74167fbe..ef879ee8 100644 --- a/src/io.jl +++ b/src/io.jl @@ -19,7 +19,7 @@ img = AstroImage(filename::AbstractString, ext::Integer=1) Load an image HDU `ext` from the FITS file at `filename` as an AstroImage. """ -function AstroImage(filename::AbstractString, ext::Integer=1, args...; kwargs...) +function AstroImage(filename::AbstractString, ext::Integer, args...; kwargs...) return FITS(filename, "r") do fits return AstroImage(fits[ext], args...; kwargs...) end diff --git a/src/precompile.jl b/src/precompile.jl index eab1b50d..66d542ac 100644 --- a/src/precompile.jl +++ b/src/precompile.jl @@ -40,8 +40,5 @@ end # From trace-compile: precompile(Tuple{typeof(AstroImages.imview), Array{Float64, 2}}) -precompile(Tuple{AstroImages.var"##imview#60", AstroImages.Percent, Function, Symbol, Float64, Float64, typeof(AstroImages.imview), Array{Float64, 2}}) -precompile(Tuple{typeof(AstroImages._imview), Base.SubArray{Float64, 2, Array{Float64, 2}, Tuple{Base.StepRange{Int64, Int64}, Base.Slice{Base.OneTo{Int64}}}, false}, MappedArrays.ReadonlyMappedArray{Any, 2, Base.SubArray{Float64, 2, Array{Float64, 2}, Tuple{Base.StepRange{Int64, Int64}, Base.Slice{Base.OneTo{Int64}}}, false}, AstroImages.var"#118#119"{Float64}}, typeof(Base.identity), ColorSchemes.ColorScheme{Array{ColorTypes.RGB{Float64}, 1}, String, String}, Float64, Float64}) precompile(Tuple{AstroImages.var"#imview##kw", NamedTuple{(:clims, :stretch, :cmap, :contrast, :bias), Tuple{AstroImages.Percent, typeof(Base.identity), Symbol, Int64, Float64}}, typeof(AstroImages.imview), AstroImages.AstroImage{Float64, 2, Tuple{DimensionalData.Dimensions.X{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}, DimensionalData.Dimensions.Y{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}}, Tuple{}, Array{Float64, 2}, Tuple{DimensionalData.Dimensions.X{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}, DimensionalData.Dimensions.Y{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}}}}) -precompile(Tuple{typeof(AstroImages._imview), Base.SubArray{Float64, 2, Array{Float64, 2}, Tuple{Base.StepRange{Int64, Int64}, Base.Slice{Base.OneTo{Int64}}}, false}, MappedArrays.ReadonlyMappedArray{Any, 2, Base.SubArray{Float64, 2, Array{Float64, 2}, Tuple{Base.StepRange{Int64, Int64}, Base.Slice{Base.OneTo{Int64}}}, false}, AstroImages.var"#118#119"{Float64}}, typeof(Base.identity), ColorSchemes.ColorScheme{Array{ColorTypes.RGB{Float64}, 1}, String, String}, Int64, Float64}) precompile(Tuple{AstroImages.var"#imview_colorbar##kw", NamedTuple{(:clims, :stretch, :cmap, :contrast, :bias), Tuple{AstroImages.Percent, typeof(Base.identity), Symbol, Int64, Float64}}, typeof(AstroImages.imview_colorbar), AstroImages.AstroImage{Float64, 2, Tuple{DimensionalData.Dimensions.X{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}, DimensionalData.Dimensions.Y{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}}, Tuple{}, Array{Float64, 2}, Tuple{DimensionalData.Dimensions.X{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}, DimensionalData.Dimensions.Y{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}}}}) \ No newline at end of file From a48b4c33947376c8a24c2088602cd0a5191973e4 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 11 Apr 2022 11:56:02 -0700 Subject: [PATCH 122/178] Support multiple wcs --- src/plot-recipes.jl | 67 ++++++++++++++++++++++----------------------- src/wcs.jl | 19 ++++++------- 2 files changed, 41 insertions(+), 45 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 8843273f..8e87c9db 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -12,12 +12,10 @@ error("Image passed to `implot` must be two-dimensional. Got ndims(img)=$(ndims(data))") end + wcsn = get(plotattributes, :wcsn, 1) # Show WCS coordinates if wcsticks is true or unspecified, and has at least one WCS axis present. - showwcsticks = (!haskey(plotattributes, :wcsticks) || plotattributes[:wcsticks]) && - !all(==(""), wcs(data).ctype) - showwcstitle = (!haskey(plotattributes, :wcstitle) || plotattributes[:wcstitle]) && - length(refdims(data)) > 0 && - !all(==(""), wcs(data).ctype) + showwcsticks = get(plotattributes, :wcsticks, true) && !all(==(""), wcs(data, wcsn).ctype) + showwcstitle = get(plotattributes, :wcstitle, true) && length(refdims(data)) > 0 && !all(==(""), wcs(data, wcsn).ctype) @@ -33,7 +31,7 @@ extent = (extent[1:2]..., plotattributes[:ylims]...) end if showwcsticks - wcsg = WCSGrid(data, Float64.(extent)) + wcsg = WCSGrid(data, Float64.(extent), wcsn) gridspec = wcsgridspec(wcsg) end @@ -96,10 +94,10 @@ refdimslabel = join(map(refdims(imgv)) do d # match dimension with the wcs axis number i = wcsax(imgv, d) - ct = wcs(imgv).ctype[i] - label = ctype_label(ct, wcs(imgv).radesys) - value = pix_to_world(imgv, [1,1], all=true, parent=true)[i] - unit = wcs(imgv).cunit[i] + ct = wcs(imgv, wcsn).ctype[i] + label = ctype_label(ct, wcs(imgv, wcsn).radesys) + value = pix_to_world(imgv, [1,1]; wcsn, all=true, parent=true)[i] + unit = wcs(imgv, wcsn).cunit[i] if ct == "STOKES" return _stokes_name(_stokes_symbol(value)) else @@ -133,11 +131,11 @@ # the transformation from pixel to coordinates can be non-linear and curved. if showwcsticks - xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), wcsax(imgv, dims(imgv,1)), gridspec.tickpos1w)) - xguide --> ctype_label(wcs(imgv).ctype[wcsax(imgv, dims(imgv,1))], wcs(imgv).radesys) + xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv, wcsn), wcsax(imgv, dims(imgv,1)), gridspec.tickpos1w)) + xguide --> ctype_label(wcs(imgv, wcsn).ctype[wcsax(imgv, dims(imgv,1))], wcs(imgv, wcsn).radesys) - yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), wcsax(imgv, dims(imgv,2)), gridspec.tickpos2w)) - yguide --> ctype_label(wcs(imgv).ctype[wcsax(imgv, dims(imgv,2))], wcs(imgv).radesys) + yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv, wcsn), wcsax(imgv, dims(imgv,2)), gridspec.tickpos2w)) + yguide --> ctype_label(wcs(imgv, wcsn).ctype[wcsax(imgv, dims(imgv,2))], wcs(imgv, wcsn).radesys) end @@ -233,11 +231,11 @@ # the transformation from pixel to coordinates can be non-linear and curved. if showwcsticks - xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv), wcsax(imgv, dims(imgv,1)), gridspec.tickpos1w)) - xguide --> ctype_label(wcs(imgv).ctype[wcsax(imgv, dims(imgv,1))], wcs(imgv).radesys) + xticks --> (gridspec.tickpos1x, wcslabels(wcs(imgv, wcsn), wcsax(imgv, dims(imgv,1)), gridspec.tickpos1w)) + xguide --> ctype_label(wcs(imgv, wcsn).ctype[wcsax(imgv, dims(imgv,1))], wcs(imgv, wcsn).radesys) - yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv), wcsax(imgv, dims(imgv,2)), gridspec.tickpos2w)) - yguide --> ctype_label(wcs(imgv).ctype[wcsax(imgv, dims(imgv,2))], wcs(imgv).radesys) + yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv, wcsn), wcsax(imgv, dims(imgv,2)), gridspec.tickpos2w)) + yguide --> ctype_label(wcs(imgv, wcsn).ctype[wcsax(imgv, dims(imgv,2))], wcs(imgv, wcsn).radesys) end view(collect(imgv), reverse(axes(imgv,1)),:) end @@ -332,6 +330,7 @@ implot struct WCSGrid img::AstroImage extent::NTuple{4,Float64} + wcsn::Int end @@ -342,13 +341,13 @@ Generate nice tick labels for an AstroImageMat along axis `axnum` Returns a vector of pixel positions and a vector of strings. Example: -plot(img, xticks=wcsticks(img, 1), yticks=wcsticks(img, 2)) +plot(img, xticks=wcsticks(WCSGrid(img), 1), yticks=wcsticks(WCSGrid(img), 2)) """ -function wcsticks(img::AstroImageMat, axnum, gs = wcsgridspec(WCSGrid(img))) +function wcsticks(wcsg::WCSGrid, axnum, gs = wcsgridspec(wcsg)) tickposx = axnum == 1 ? gs.tickpos1x : gs.tickpos2x tickposw = axnum == 1 ? gs.tickpos1w : gs.tickpos2w return tickposx, wcslabels( - wcs(img), + wcs(wcsg.img, wcsg.wcsn), axnum, tickposw ) @@ -485,14 +484,14 @@ This function has to work on both plotted axes at once to handle rotation and ge curvature of the WCS grid projected on the image coordinates. """ -function WCSGrid(img::AstroImageMat) +function WCSGrid(img::AstroImageMat, wcsn=1) minx = first(dims(img,2)) maxx = last(dims(img,2)) miny = first(dims(img,1)) maxy = last(dims(img,1)) extent = (minx-0.5, maxx+0.5, miny-0.5, maxy+0.5) @show extent - return WCSGrid(img, extent) + return WCSGrid(img, extent, wcsn) end @@ -518,8 +517,8 @@ end end annotate = haskey(plotattributes, :gridlabels) && plotattributes[:gridlabels] - xguide --> ctype_label(wcs(wcsg.img).ctype[wcsax(wcsg.img, dims(wcsg.img,1))], wcs(wcsg.img).radesys) - yguide --> ctype_label(wcs(wcsg.img).ctype[wcsax(wcsg.img, dims(wcsg.img,2))], wcs(wcsg.img).radesys) + xguide --> ctype_label(wcs(wcsg.img, wcsg.wcsn).ctype[wcsax(wcsg.img, dims(wcsg.img,1))], wcs(wcsg.img, wcsg.wcsn).radesys) + yguide --> ctype_label(wcs(wcsg.img, wcsg.wcsn).ctype[wcsax(wcsg.img, dims(wcsg.img,2))], wcs(wcsg.img, wcsg.wcsn).radesys) xlims --> wcsg.extent[1], wcsg.extent[2] ylims --> wcsg.extent[3], wcsg.extent[4] @@ -527,8 +526,8 @@ end grid := false tickdirection := :none - xticks --> wcsticks(wcsg.img, 1, gridspec) - yticks --> wcsticks(wcsg.img, 2, gridspec) + xticks --> wcsticks(wcsg, 1, gridspec) + yticks --> wcsticks(wcsg, 2, gridspec) @series xs, ys @@ -586,7 +585,7 @@ function wcsgridspec(wsg::WCSGrid) minx minx maxx maxx miny maxy miny maxy ] - posuv = pix_to_world(wsg.img, posxy, parent=true) + posuv = pix_to_world(wsg.img, posxy; wsg.wcsn, parent=true) (minu, maxu), (minv, maxv) = extrema(posuv, dims=2) # In general, grid can be curved when plotted back against the image, @@ -628,7 +627,7 @@ function wcsgridspec(wsg::WCSGrid) griduv = repeat(posuv[:,1], 1, N_points) griduv[1,:] .= urange griduv[2,:] .= tickv - posxy = world_to_pix(wsg.img, griduv; parent=true) + posxy = world_to_pix(wsg.img, griduv; wsg.wcsn, parent=true) # Now that we have the grid in pixel coordinates, # if we find out where the grid intersects the axes we can put @@ -792,7 +791,7 @@ function wcsgridspec(wsg::WCSGrid) griduv = repeat(posuv[:,1], 1, N_points) griduv[1,:] .= ticku griduv[2,:] .= vrange - posxy = world_to_pix(wsg.img, griduv; parent=true) + posxy = world_to_pix(wsg.img, griduv; wsg.wcsn, parent=true) # Now that we have the grid in pixel coordinates, # if we find out where the grid intersects the axes we can put @@ -937,7 +936,7 @@ function wcsgridspec(wsg::WCSGrid) griduv = posuv[:,1] griduv[1] = ticku griduv[2] = mean(vrange) - posxy = world_to_pix(wsg.img, griduv, parent=true) + posxy = world_to_pix(wsg.img, griduv; wsg.wcsn, parent=true) if !(minx < posxy[1] < maxx) || !(miny < posxy[2] < maxy) continue @@ -949,7 +948,7 @@ function wcsgridspec(wsg::WCSGrid) # Now find slope (TODO: stepsize) # griduv[ax[2]] -= 1 griduv[2] += 0.1step(vrange) - posxy2 = world_to_pix(wsg.img, griduv, parent=true) + posxy2 = world_to_pix(wsg.img, griduv; wsg.wcsn, parent=true) θ = atan( posxy2[2] - posxy[2], posxy2[1] - posxy[1], @@ -965,7 +964,7 @@ function wcsgridspec(wsg::WCSGrid) griduv = posuv[:,1] griduv[1] = mean(urange) griduv[2] = tickv - posxy = world_to_pix(wsg.img, griduv, parent=true) + posxy = world_to_pix(wsg.img, griduv; wsg.wcsn, parent=true) if !(minx < posxy[1] < maxx) || !(miny < posxy[2] < maxy) continue @@ -975,7 +974,7 @@ function wcsgridspec(wsg::WCSGrid) push!(annotations2y, posxy[2]) griduv[1] += 0.1step(urange) - posxy2 = world_to_pix(wsg.img, griduv, parent=true) + posxy2 = world_to_pix(wsg.img, griduv; wsg.wcsn, parent=true) θ = atan( posxy2[2] - posxy[2], posxy2[1] - posxy[1], diff --git a/src/wcs.jl b/src/wcs.jl index 6a24151d..6f8431fb 100644 --- a/src/wcs.jl +++ b/src/wcs.jl @@ -291,13 +291,10 @@ function wcsfromheader(img::AstroImage; relax=WCS.HDR_ALL) @warn "WCSTransform was generated by ignoring rejected header. It may not be valid." exception=err end - if length(wcsout) == 1 - return only(wcsout) - elseif length(wcsout) == 0 - return emptywcs(img) + if length(wcsout) == 0 + return [emptywcs(img)] else - @warn "Mutiple WCSTransform returned from header, using first and ignoring the rest." - return first(wcsout) + return wcsout end end @@ -402,7 +399,7 @@ julia> world_coords = pix_to_world(img, [1, 1], all=true) !! Coordinates must be provided in the order of `dims(img)`. If you transpose an image, the order you pass the coordinates should not change. """ -function WCS.pix_to_world(img::AstroImage, pixcoords; all=false, parent=false) +function WCS.pix_to_world(img::AstroImage, pixcoords; wcsn=1, all=false, parent=false) if pixcoords isa Array{Float64} pixcoords_prepared = pixcoords else @@ -452,7 +449,7 @@ function WCS.pix_to_world(img::AstroImage, pixcoords; all=false, parent=false) end # Get world coordinates along all slices - WCS.pix_to_world!(wcs(img), parentcoords_prepared, worldcoords_out) + WCS.pix_to_world!(wcs(img, wcsn), parentcoords_prepared, worldcoords_out) # If user requested world coordinates in all dims, not just selected # dims of img @@ -476,7 +473,7 @@ end ## -function WCS.world_to_pix(img::AstroImage, worldcoords; parent=false) +function WCS.world_to_pix(img::AstroImage, worldcoords; parent=false, wcsn=1) if worldcoords isa Array{Float64} worldcoords_prepared = worldcoords else @@ -490,7 +487,7 @@ function WCS.world_to_pix(img::AstroImage, worldcoords; parent=false) end return WCS.world_to_pix!(out, img, worldcoords_prepared; parent) end -function WCS.world_to_pix!(pixcoords_out, img::AstroImage, worldcoords; parent=false) +function WCS.world_to_pix!(pixcoords_out, img::AstroImage, worldcoords; wcsn=1, parent=false) # # Find the coordinates in the parent array. # # Dimensional data # worldcoords_floored = floor.(Int, worldcoords) @@ -526,7 +523,7 @@ function WCS.world_to_pix!(pixcoords_out, img::AstroImage, worldcoords; parent=f # This returns the parent pixel coordinates. # TODO: switch to non-allocating version. - pixcoords_out .= WCS.world_to_pix(wcs(img), worldcoords_prepared) + pixcoords_out .= WCS.world_to_pix(wcs(img, wcsn), worldcoords_prepared) if !parent coordoffsets = zeros(length(dims(img))+length(refdims(img))) From 9ebb74c08f573797090cc6a54764809d8871ad96 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 11 Apr 2022 11:56:15 -0700 Subject: [PATCH 123/178] Cleanup exports --- src/AstroImages.jl | 53 ++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 85fc8504..646e688a 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -45,7 +45,14 @@ export load, wcs, Comment, History, + # Dimensions Centered, + Spec, + Pol, + Ti, + X, Y, Z, Dim, + At, Near, Between, .., + dims, refdims, pix_to_world, pix_to_world!, world_to_pix, @@ -84,8 +91,8 @@ struct AstroImage{T,N,D<:Tuple,R<:Tuple,A<:AbstractArray{T,N},W<:Tuple} <: Abstr refdims::R # FITS Heads beloning to this image, if any header::FITSHeader - # A cached WCSTransform object for this data - wcs::Base.RefValue{WCSTransform} + # cached WCSTransform objects for this data. + wcs::Vector{WCSTransform} # A flag that is set when a user modifies a WCS header. # The next access to the wcs object will regenerate from # the new header on demand. @@ -97,11 +104,6 @@ end const AstroImageVec{T,D} = AstroImage{T,1} where {T} const AstroImageMat{T,D} = AstroImage{T,2} where {T} -# Re-export symbols from DimensionalData that users will need -# for indexing. -export X, Y, Z, Dim -export At, Near, Between, .. -export dims, refdims """ Centered() @@ -147,19 +149,20 @@ function wcsax(img::AstroImage, dim) return findfirst(di->name(di)==name(dim), img.wcsdims) end -export Spec, Pol#, Wcs - # Accessors header(img::AstroImage) = getfield(img, :header) header(::AbstractArray) = emptyheader() function wcs(img::AstroImage) if getfield(img, :wcs_stale)[] - getfield(img, :wcs)[] = wcsfromheader(img) + empty!(getfield(img, :wcs)) + append!(getfield(img, :wcs), wcsfromheader(img)) getfield(img, :wcs_stale)[] = false end - return getfield(img, :wcs)[] + return getfield(img, :wcs) end -wcs(arr::AbstractArray) = emptywcs(arr) +wcs(arr::AbstractArray) = [emptywcs(arr)] +wcs(img, ind) = wcs(img)[ind] + """ ImageMetadata.arraydata(img::AstroImage) @@ -187,11 +190,11 @@ DimensionalData.metadata(::AstroImage) = DimensionalData.Dimensions.LookupArrays # FITS Header beloning to this image, if any header::FITSHeader=deepcopy(header(img)), # A cached WCSTransform object for this data - wcs::WCSTransform=getfield(img, :wcs)[], + wcs::Vector{WCSTransform}=getfield(img, :wcs), wcs_stale::Bool=getfield(img, :wcs_stale)[], wcsdims::Tuple=(dims...,refdims...), ) - return AstroImage(data, dims, refdims, header, Ref(wcs), Ref(wcs_stale), wcsdims) + return AstroImage(data, dims, refdims, header, wcs, Ref(wcs_stale), wcsdims) end @inline DimensionalData.rebuildsliced( f::Function, @@ -199,7 +202,7 @@ end data, I, header=deepcopy(header(img)), - wcs=getfield(img, :wcs)[], + wcs=getfield(img, :wcs), wcs_stale=getfield(img, :wcs_stale)[], wcsdims=getfield(img, :wcsdims), ) = rebuild(img, data, DimensionalData.slicedims(f, img, I)..., nothing, nothing, header, wcs, wcs_stale, wcsdims) @@ -241,7 +244,7 @@ function AstroImage( ) where {T, N} wcs_stale = isnothing(wcs) if isnothing(wcs) - wcs = emptywcs(data) + wcs = [emptywcs(data)] end # If the user passes in a WCSTransform of their own, we use it and mark # wcs_stale=false. It will be kept unless they manually change a WCS header. @@ -289,35 +292,35 @@ function AstroImage( wcsdims = (dims...,refdims...) end - return AstroImage(data, dims, refdims, header, Ref(wcs), Ref(wcs_stale), wcsdims) + return AstroImage(data, dims, refdims, header, wcs, Ref(wcs_stale), wcsdims) end function AstroImage( darr::AbstractDimArray, header::FITSHeader=emptyheader(), - wcs::Union{WCSTransform,Nothing}=nothing; + wcs::Union{Vector{WCSTransform},Nothing}=nothing; ) wcs_stale = isnothing(wcs) if isnothing(wcs) - wcs = emptywcs(darr) + wcs = [emptywcs(darr)] end wcsdims = (dims(darr)..., refdims(darr)...) - return AstroImage(parent(darr), dims(darr), refdims(darr), header, Ref(wcs), Ref(wcs_stale), wcsdims) + return AstroImage(parent(darr), dims(darr), refdims(darr), header, wcs, Ref(wcs_stale), wcsdims) end AstroImage( data::AbstractArray, dims::Union{Tuple,NamedTuple}, header::FITSHeader, - wcs::Union{WCSTransform,Nothing}=nothing; + wcs::Union{Vector{WCSTransform},Nothing}=nothing; ) = AstroImage(data, dims, (), header, wcs) AstroImage( data::AbstractArray, header::FITSHeader, - wcs::Union{WCSTransform,Nothing}=nothing; + wcs::Union{Vector{WCSTransform},Nothing}=nothing; ) = AstroImage(data, (), (), header, wcs) # TODO: ensure this gets WCS dims. -AstroImage(data::AbstractArray, wcs::WCSTransform) = AstroImage(data, emptyheader(), wcs) +AstroImage(data::AbstractArray, wcs::Vector{WCSTransform}) = AstroImage(data, emptyheader(), wcs) @@ -384,7 +387,7 @@ header of `imgnew` does not affect the header of `img`. See also: [`shareheader`](@ref). """ copyheader(img::AstroImage, data::AbstractArray) = - AstroImage(data, dims(img), refdims(img), deepcopy(header(img)), Ref(getfield(img, :wcs)[]), Ref(getfield(img, :wcs_stale)[]), getfield(img,:wcsdims)) + AstroImage(data, dims(img), refdims(img), deepcopy(header(img)), copy(getfield(img, :wcs)), Ref(getfield(img, :wcs_stale)[]), getfield(img,:wcsdims)) export copyheader """ @@ -394,7 +397,7 @@ using the data of the AbstractArray `data`. The two images have synchronized header; modifying one also affects the other. See also: [`copyheader`](@ref). """ -shareheader(img::AstroImage, data::AbstractArray) = AstroImage(data, dims(img), refdims(img), header(img), Ref(getfield(img, :wcs)[]), Ref(getfield(img, :wcs_stale)[]), getfield(img,:wcsdims)) +shareheader(img::AstroImage, data::AbstractArray) = AstroImage(data, dims(img), refdims(img), header(img), getfield(img, :wcs), Ref(getfield(img, :wcs_stale)[]), getfield(img,:wcsdims)) export shareheader # Share header if an AstroImage, do nothing if AbstractArray maybe_shareheader(img::AstroImage, data) = shareheader(img, data) From a65be928847b47106b08ae1e0f20dd55e196b4e5 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 11 Apr 2022 11:56:27 -0700 Subject: [PATCH 124/178] Fix ffts with non-default parameters --- src/contrib/abstract-ffts.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contrib/abstract-ffts.jl b/src/contrib/abstract-ffts.jl index cc286bbf..fd909b71 100644 --- a/src/contrib/abstract-ffts.jl +++ b/src/contrib/abstract-ffts.jl @@ -10,14 +10,14 @@ for f in [ :(AbstractFFTs.rfft), ] # TODO: should we try to alter the image headers to change the units? - @eval ($f)(img::AstroImage, args...; kwargs...) = copyheader(img, $f(arraydata(img))) + @eval ($f)(img::AstroImage, args...; kwargs...) = copyheader(img, $f(arraydata(img), args...; kwargs...)) end for f in [ :(AbstractFFTs.fftshift), ] # TODO: should we try to alter the image headers to change the units? - @eval ($f)(img::AstroImage, args...; kwargs...) = shareheader(img, $f(arraydata(img))) + @eval ($f)(img::AstroImage, args...; kwargs...) = shareheader(img, $f(arraydata(img), args...; kwargs...)) end From 3767528618ad720321438f9417b9d0c2569c62c6 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 18 Apr 2022 08:19:07 -0700 Subject: [PATCH 125/178] Adjust project requirements --- Project.toml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Project.toml b/Project.toml index 92cac8fc..1f9e8c9b 100644 --- a/Project.toml +++ b/Project.toml @@ -12,13 +12,10 @@ FITSIO = "525bcba6-941b-5504-bd06-fd0dc1a4d2eb" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" ImageAxes = "2803e5a7-5153-5ecf-9a86-9b4c37f5f5ac" ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534" -# ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" ImageMetadata = "bc367c6b-8a6b-528e-b4bd-a4b897500b49" ImageShow = "4e3cecfd-b093-5904-9786-8bbb286a6a31" ImageTransformations = "02fcd773-0e25-5acc-982a-7f6622650795" -# InlineStrings = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48" MappedArrays = "dbb5928d-eab1-5f90-85c2-b9b0edb7c900" -# OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" @@ -29,9 +26,9 @@ UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" WCS = "15f3aee2-9e10-537f-b834-a6fb8bdb944d" [compat] +DimensionalData = "^0.20" Reproject = "^0.3.0" julia = "^1.6.0" -DimensionalData = "^0.20" [extras] JLD = "4138dd39-2aa7-5051-a626-17a0bb65d9c8" From 62048247355dbac0fffa89a5827e6af985c80dfd Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 18 Apr 2022 08:19:42 -0700 Subject: [PATCH 126/178] Add explicit writefits function for use without FileIO --- src/io.jl | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/io.jl b/src/io.jl index ef879ee8..720e5fe1 100644 --- a/src/io.jl +++ b/src/io.jl @@ -139,7 +139,17 @@ indexer(fits::NTuple{N,FITS}) where {N} = ntuple(i -> indexer(fits[i]), N) # Fallback for saving arbitrary arrays function fileio_save(f::File{format"FITS"}, args...) - FITS(f.filename, "w") do fits + return writefits(f.filename, args...) +end +""" + writefits("abc.fits", img1, img2, table1,...) + +Write arguments to a FITS file. + +See also `Fileio.save` +""" +function writefits(fname, args...) + FITS(fname, "w") do fits for arg in args writearg(fits, arg) end From 3510ec962ceb7c0f519e6891f5341d174cdb00f1 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 18 Apr 2022 08:32:17 -0700 Subject: [PATCH 127/178] Fallbacks for more FITS keywords encountered in the wild --- src/plot-recipes.jl | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 8e87c9db..9743eab1 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -96,6 +96,9 @@ i = wcsax(imgv, d) ct = wcs(imgv, wcsn).ctype[i] label = ctype_label(ct, wcs(imgv, wcsn).radesys) + if label == "NONE" + label = name(d)[1] + end value = pix_to_world(imgv, [1,1]; wcsn, all=true, parent=true)[i] unit = wcs(imgv, wcsn).cunit[i] if ct == "STOKES" @@ -187,8 +190,12 @@ ] end colorbar_title = get(plotattributes, :colorbar_title, "") - if !haskey(plotattributes, :colorbar_title) && haskey(header(img), "UNIT") - colorbar_title = string(img[:UNIT]) + if !haskey(plotattributes, :colorbar_title) + if haskey(header(img), "UNIT") + colorbar_title = string(img[:UNIT]) + elseif haskey(header(img), "BUNIT") + colorbar_title = string(img[:BUNIT]) + end end subplot_i += 1 From fea17c7bd8a0510ced73cd0f1da1635423449f42 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 18 Apr 2022 08:55:38 -0700 Subject: [PATCH 128/178] Explicit aspect_ratio=1 for backends that do not have this as a default (plottly) --- src/plot-recipes.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 9743eab1..b754830b 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -126,6 +126,7 @@ subplot_i += 1 subplot := subplot_i colorbar := false + aspect_ratio --> 1 # Note: if the axes are on unusual sides (e.g. y-axis at right, x-axis at top) # then these coordinates are not correct. They are only correct exactly @@ -229,6 +230,7 @@ subplot := subplot_i colorbar := false title := "" + aspect_ratio --> 1 # Note: if the axes are on unusual sides (e.g. y-axis at right, x-axis at top) From 6aa1533350b6be7225a0ac6476ba18d63776c8bb Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 18 Apr 2022 09:21:02 -0700 Subject: [PATCH 129/178] Add `recenter` function --- src/AstroImages.jl | 53 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 646e688a..189fdb7f 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -53,6 +53,7 @@ export load, X, Y, Z, Dim, At, Near, Between, .., dims, refdims, + recenter, pix_to_world, pix_to_world!, world_to_pix, @@ -323,13 +324,29 @@ AstroImage( AstroImage(data::AbstractArray, wcs::Vector{WCSTransform}) = AstroImage(data, emptyheader(), wcs) +""" +Index for accessing a comment associated with a header keyword +or COMMENT entry. +Example: +``` +img = AstroImage(randn(10,10)) +img["ABC"] = 1 +img["ABC", Comment] = "A comment describing this key" +push!(img, Comment, "The purpose of this file is to demonstrate comments") +img[Comment] # ["The purpose of this file is to demonstrate comments")] +``` +""" +struct Comment end +""" +Allows accessing and setting HISTORY header entries - - -struct Comment end +img = AstroImage(randn(10,10)) +push!(img, History, "2023-04-19: Added history entry.") +img[History] # ["2023-04-19: Added history entry."] +""" struct History end @@ -429,6 +446,36 @@ Convenience function to create a FITSHeader with no keywords set. emptyheader() = FITSHeader(String[],[],String[]) +""" + recenter(img::AstroImage, newcentx, newcenty, ...) + +Adjust the dimensions of an AstroImage so that they are centered on the pixel locations given by `newcentx`, .. etc. + +This does not affect the underlying array, it just updates the dimensions associated with it by the AstroImage. + +Example: +```julia +a = AstroImage(randn(11,11)) +a[1,1] # Bottom left +a[At(1),At(1)] # Bottom left +r = recenter(a, 6, 6) +r[1,1] # Still bottom left +r[At(1),At(1)] # Center pixel +``` +""" +function recenter(img::AstroImage, centers::Number...) + newdims = map(dims(img), axes(img), centers) do d, a, c + return AstroImages.name(d) => a .- c + end + newdimsformatted = AstroImages.DimensionalData.format(NamedTuple(newdims), parent(img)) + l = length(newdimsformatted) + if l < ndims(img) + newdimsformatted = (newdimsformatted..., dims(img)[l+1:end]...) + end + AstroImages.rebuild(img, parent(img), newdimsformatted) +end + + include("wcs.jl") include("io.jl") include("imview.jl") From 5628d29a4a64c5408256995ae4a2b112d6968657 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 18 Apr 2022 09:38:37 -0700 Subject: [PATCH 130/178] Symbols aren't iterable --- src/plot-recipes.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index b754830b..b35f90ec 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -97,7 +97,7 @@ ct = wcs(imgv, wcsn).ctype[i] label = ctype_label(ct, wcs(imgv, wcsn).radesys) if label == "NONE" - label = name(d)[1] + label = name(d) end value = pix_to_world(imgv, [1,1]; wcsn, all=true, parent=true)[i] unit = wcs(imgv, wcsn).cunit[i] From 9fbf3a472023b6aafeb3346058853b6cd98d7859 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 18 Apr 2022 10:21:30 -0700 Subject: [PATCH 131/178] Trust WCS naxis over length of dims for WCS computations --- src/wcs.jl | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/wcs.jl b/src/wcs.jl index 6f8431fb..80541a74 100644 --- a/src/wcs.jl +++ b/src/wcs.jl @@ -405,7 +405,7 @@ function WCS.pix_to_world(img::AstroImage, pixcoords; wcsn=1, all=false, parent= else pixcoords_prepared = [Float64(c) for c in pixcoords] end - D_out = length(dims(img))+length(refdims(img)) + D_out = wcs(img,wcsn).naxis if ndims(pixcoords_prepared) > 1 worldcoords_out = similar(pixcoords_prepared, Float64, D_out, size(pixcoords_prepared,2)) else @@ -426,11 +426,11 @@ function WCS.pix_to_world(img::AstroImage, pixcoords; wcsn=1, all=false, parent= # as input, not any other kind of collection. # TODO: avoid allocation in case where refdims=() and pixcoords isa Array{Float64} if ndims(pixcoords_prepared) > 1 - parentcoords_prepared = zeros(length(dims(img))+length(refdims(img)), size(pixcoords_prepared,2)) + parentcoords_prepared = zeros(wcs(img,wcsn).naxis, size(pixcoords_prepared,2)) else - parentcoords_prepared = zeros(length(dims(img))+length(refdims(img))) + parentcoords_prepared = zeros(wcs(img,wcsn).naxis) end - # out = zeros(Float64, length(dims(img))+length(refdims(img)), size(pixcoords,2)) + # out = zeros(Float64, wcs(img,wcsn).naxis, size(pixcoords,2)) for (i, dim) in enumerate(dims(img)) j = wcsax(img, dim) parentcoords_prepared[j,:] .= parentcoords[i,:] .- 1 @@ -479,7 +479,7 @@ function WCS.world_to_pix(img::AstroImage, worldcoords; parent=false, wcsn=1) else worldcoords_prepared = [Float64(c) for c in worldcoords] end - D_out = length(dims(img))+length(refdims(img)) + D_out = wcs(img,wcsn).naxis if ndims(worldcoords_prepared) > 1 out = similar(worldcoords_prepared, Float64, D_out, size(worldcoords_prepared,2)) else @@ -497,13 +497,13 @@ function WCS.world_to_pix!(pixcoords_out, img::AstroImage, worldcoords; wcsn=1, # as input, not any other kind of collection. # TODO: avoid allocation in case where refdims=() and worldcoords isa Array{Float64} if ndims(worldcoords) > 1 - worldcoords_prepared = zeros(length(dims(img))+length(refdims(img)),size(worldcoords,2)) + worldcoords_prepared = zeros(wcs(img,wcsn).naxis,size(worldcoords,2)) else - worldcoords_prepared = zeros(length(dims(img))+length(refdims(img))) + worldcoords_prepared = zeros(wcs(img,wcsn).naxis) end # TODO: we need to pass in ref dims locations as well, and then filter the # output to only include the dims of the current slice? - # out = zeros(Float64, length(dims(img))+length(refdims(img)), size(worldcoords,2)) + # out = zeros(Float64, wcs(img,wcsn).naxis, size(worldcoords,2)) for (i, dim) in enumerate(dims(img)) j = wcsax(img, dim) worldcoords_prepared[j,:] = worldcoords[i,:] @@ -526,8 +526,8 @@ function WCS.world_to_pix!(pixcoords_out, img::AstroImage, worldcoords; wcsn=1, pixcoords_out .= WCS.world_to_pix(wcs(img, wcsn), worldcoords_prepared) if !parent - coordoffsets = zeros(length(dims(img))+length(refdims(img))) - coordsteps = zeros(length(dims(img))+length(refdims(img))) + coordoffsets = zeros(wcs(img,wcsn).naxis) + coordsteps = zeros(wcs(img,wcsn).naxis) for (i, dim) in enumerate(dims(img)) j = wcsax(img, dim) coordoffsets[j] = first(dims(img)[i]) From 66db70c7a1c9bddec181fed27e777a276abd57bc Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 18 Apr 2022 10:21:48 -0700 Subject: [PATCH 132/178] Use steps of dims for images.pixelspacing --- src/contrib/images.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/contrib/images.jl b/src/contrib/images.jl index 5e89cc6d..dc96fb27 100644 --- a/src/contrib/images.jl +++ b/src/contrib/images.jl @@ -61,8 +61,7 @@ function ImageTransformations.restrict(img::AstroImage, region::Dims) end -# TODO: use WCS info -# ImageCore.pixelspacing(img::ImageMeta) = pixelspacing(arraydata(img)) +ImageCore.pixelspacing(img::AstroImage) = step.(dims(img)) # ImageContrastAdjustment From e4d9f0f6c280cf6f0d7e88d5b2450dd60809de61 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 18 Apr 2022 11:00:31 -0700 Subject: [PATCH 133/178] Add compat for Interpolations. --- Project.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 1f9e8c9b..09207c23 100644 --- a/Project.toml +++ b/Project.toml @@ -26,9 +26,10 @@ UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" WCS = "15f3aee2-9e10-537f-b834-a6fb8bdb944d" [compat] +julia = "^1.6.0" DimensionalData = "^0.20" Reproject = "^0.3.0" -julia = "^1.6.0" +Interpolations = "0.13" [extras] JLD = "4138dd39-2aa7-5051-a626-17a0bb65d9c8" From c4fba117089596600e3bbf8ac5eb17253b655c72 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 18 Apr 2022 11:04:53 -0700 Subject: [PATCH 134/178] Revert previous --- Project.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Project.toml b/Project.toml index 09207c23..7bf8b796 100644 --- a/Project.toml +++ b/Project.toml @@ -29,7 +29,6 @@ WCS = "15f3aee2-9e10-537f-b834-a6fb8bdb944d" julia = "^1.6.0" DimensionalData = "^0.20" Reproject = "^0.3.0" -Interpolations = "0.13" [extras] JLD = "4138dd39-2aa7-5051-a626-17a0bb65d9c8" From ded071cb4a993f547e58f209f6ec7dc59b01c1b7 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 18 Apr 2022 11:10:59 -0700 Subject: [PATCH 135/178] Bump version number and add author --- Project.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 7bf8b796..197b30c2 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "AstroImages" uuid = "fe3fc30c-9b16-11e9-1c73-17dabf39f4ad" -authors = ["Mosè Giordano", "Rohit Kumar"] -version = "0.2.0" +authors = ["Mosè Giordano", "Rohit Kumar", "William Thompson"] +version = "0.3.0" [deps] AbstractFFTs = "621f4979-c628-5d54-868e-fcf4e3e8185c" From 9500c95d2fdc21dc7591e92aabbd327c3759927b Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 18 Apr 2022 11:55:52 -0700 Subject: [PATCH 136/178] Reproject holding back interpolations --- Project.toml | 4 +--- src/AstroImages.jl | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Project.toml b/Project.toml index 197b30c2..800eef79 100644 --- a/Project.toml +++ b/Project.toml @@ -19,16 +19,14 @@ MappedArrays = "dbb5928d-eab1-5f90-85c2-b9b0edb7c900" PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" -Reproject = "d1dcc2e6-806e-11e9-2897-3f99785db2ae" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" WCS = "15f3aee2-9e10-537f-b834-a6fb8bdb944d" [compat] -julia = "^1.6.0" DimensionalData = "^0.20" -Reproject = "^0.3.0" +julia = "^1.6.0" [extras] JLD = "4138dd39-2aa7-5051-a626-17a0bb65d9c8" diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 189fdb7f..6166d565 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -484,7 +484,7 @@ include("plot-recipes.jl") include("contrib/images.jl") include("contrib/abstract-ffts.jl") -include("contrib/reproject.jl") +# include("contrib/reproject.jl") include("ccd2rgb.jl") include("precompile.jl") From f1a5a7aa8b94f4b84f8ad1fbc686aba85b86727e Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 18 Apr 2022 12:55:34 -0700 Subject: [PATCH 137/178] Skip reproject dependency --- src/AstroImages.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 6166d565..c670355c 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -7,7 +7,6 @@ using FileIO # We also need ImageShow so that user's images appear automatically. using ImageCore, ImageShow, ImageMetadata, ImageAxes, ImageTransformations # TODO: maybe this can be ImagesCore -using Reproject using WCS using Statistics using MappedArrays From c6aad34aa8b28f801287ee99c3dd94caa1ce5218 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 18 Apr 2022 13:22:31 -0700 Subject: [PATCH 138/178] Fix for empty HDU header access --- src/io.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/io.jl b/src/io.jl index 720e5fe1..bfeb5373 100644 --- a/src/io.jl +++ b/src/io.jl @@ -116,7 +116,7 @@ function _loadhdu(hdu::FITSIO.ImageHDU, args...; kwargs...) # Sometimes files have an empty data HDU that shows up as an image HDU but has headers. # Fallback to creating an empty AstroImage with those headers. emptydata = fill(missing) - return AstroImage(emptydata, (), (), read_header(hdu), Ref(emptywcs(emptydata)), Ref(false)) + return AstroImage(emptydata, (), (), read_header(hdu), [emptywcs(emptydata)], Ref(false), ()) end end function indexer(fits::FITS) From 5e9caa1c85f218b4aacd39eb411fb4e5a4e803c0 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 24 Apr 2022 19:28:00 -0700 Subject: [PATCH 139/178] one -> oneunit --- src/contrib/images.jl | 6 +++--- src/showmime.jl | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/contrib/images.jl b/src/contrib/images.jl index dc96fb27..05d2e4c5 100644 --- a/src/contrib/images.jl +++ b/src/contrib/images.jl @@ -16,7 +16,7 @@ function ImageCore.normedview(img::AstroImageMat{T}) where T Δ = abs(imgmax - imgmin) # Do not introduce NaNs if limits are identical if Δ == 0 - Δ = one(imgmin) + Δ = oneunit(imgmin) end normeddata = mappedarray( pix -> (pix - imgmin)/Δ, @@ -40,10 +40,10 @@ function clampednormedview(img::AbstractArray{T}, lims) where T Δ = imgmax - imgmin # Do not introduce NaNs if colorlimits are identical if Δ == 0 - Δ = one(imgmin) + Δ = oneunit(imgmin) end normeddata = mappedarray( - pix -> clamp((pix - imgmin)/Δ, zero(pix), one(pix)), + pix -> clamp((pix - imgmin)/Δ, zero(pix), oneunit(pix)), img ) return maybe_shareheader(img, normeddata) diff --git a/src/showmime.jl b/src/showmime.jl index 31edc822..e485e083 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -53,7 +53,7 @@ function render(img::AstroImageMat{T,N}) where {T,N} imgmin, imgmax = extrema(img) # Add one to maximum to work around this issue: # https://github.com/JuliaMath/FixedPointNumbers.jl/issues/102 - f = scaleminmax(_float(imgmin), _float(max(imgmax, imgmax + one(T)))) + f = scaleminmax(_float(imgmin), _float(max(imgmax, imgmax + oneunit(T)))) return colorview(Gray, f.(_float.(img.data))) end ImageCore.colorview(img::AstroImageMat) = render(img) From 5442bfa23e96b47ef23913f2ff32dd2f23b46cbb Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 24 Apr 2022 19:28:30 -0700 Subject: [PATCH 140/178] one -> oneunit --- src/imview.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/imview.jl b/src/imview.jl index 21169d06..70c37487 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -336,7 +336,7 @@ function imview_colorbar( ticks, _, _ = optimize_ticks(Float64(imgmin), Float64(imgmax), k_min=3) # Now map these to pixel locations through streching and colorlimits: stretchmin = stretch(zero(eltype(data))) - stretchmax = stretch(one(eltype(data))) + stretchmax = stretch(oneunit(eltype(data))) normedticks = clampednormedview(ticks, (imgmin, imgmax)) ticksloc = map(normedticks) do tickn stretched = stretch(tickn) From 4a4babaae4d56af1a953e03232479f6d1c48875d Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 24 Apr 2022 19:42:16 -0700 Subject: [PATCH 141/178] Drop dep on ImageTransformations; swap ImageCore for ImageBase --- Project.toml | 4 ++-- src/AstroImages.jl | 2 +- src/contrib/images.jl | 13 +++++-------- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/Project.toml b/Project.toml index 800eef79..5762e44a 100644 --- a/Project.toml +++ b/Project.toml @@ -11,10 +11,9 @@ DimensionalData = "0703355e-b756-11e9-17c0-8b28908087d0" FITSIO = "525bcba6-941b-5504-bd06-fd0dc1a4d2eb" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" ImageAxes = "2803e5a7-5153-5ecf-9a86-9b4c37f5f5ac" -ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534" +ImageBase = "c817782e-172a-44cc-b673-b171935fbb9e" ImageMetadata = "bc367c6b-8a6b-528e-b4bd-a4b897500b49" ImageShow = "4e3cecfd-b093-5904-9786-8bbb286a6a31" -ImageTransformations = "02fcd773-0e25-5acc-982a-7f6622650795" MappedArrays = "dbb5928d-eab1-5f90-85c2-b9b0edb7c900" PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" @@ -27,6 +26,7 @@ WCS = "15f3aee2-9e10-537f-b834-a6fb8bdb944d" [compat] DimensionalData = "^0.20" julia = "^1.6.0" +ImageBase = "^0.1.5" [extras] JLD = "4138dd39-2aa7-5051-a626-17a0bb65d9c8" diff --git a/src/AstroImages.jl b/src/AstroImages.jl index c670355c..0fe7f9bc 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -5,7 +5,7 @@ using FileIO # Rather than pulling in all of Images.jl, just grab the packages # we need to extend to our basic functionality. # We also need ImageShow so that user's images appear automatically. -using ImageCore, ImageShow, ImageMetadata, ImageAxes, ImageTransformations # TODO: maybe this can be ImagesCore +using ImageBase, ImageShow, ImageMetadata, ImageAxes using WCS using Statistics diff --git a/src/contrib/images.jl b/src/contrib/images.jl index 05d2e4c5..d1eb75b5 100644 --- a/src/contrib/images.jl +++ b/src/contrib/images.jl @@ -1,6 +1,3 @@ -#= -ImageTransformations -=# # function warp(img::AstroImageMat, args...; kwargs...) # out = warp(arraydatat(img), args...; kwargs...) # return copyheaders(img, out) @@ -10,8 +7,8 @@ ImageTransformations # Instead of using a datatype like N0f32 to interpret integers as fixed point values in [0,1], # we use a mappedarray to map the native data range (regardless of type) to [0,1] -ImageCore.normedview(img::AstroImageMat{<:FixedPoint}) = img -function ImageCore.normedview(img::AstroImageMat{T}) where T +ImageBase.normedview(img::AstroImageMat{<:FixedPoint}) = img +function ImageBase.normedview(img::AstroImageMat{T}) where T imgmin, imgmax = extrema(skipmissingnan(img)) Δ = abs(imgmax - imgmin) # Do not introduce NaNs if limits are identical @@ -52,8 +49,8 @@ end # Restrict downsizes images by roughly a factor of two. # We want to keep the wrapper but downsize the underlying array # TODO: correct dimensions after restrict. -ImageTransformations.restrict(img::AstroImage, ::Tuple{}) = img -function ImageTransformations.restrict(img::AstroImage, region::Dims) +ImageBase.restrict(img::AstroImage, ::Tuple{}) = img +function ImageBase.restrict(img::AstroImage, region::Dims) restricted = restrict(arraydata(img), region) steps = cld.(size(img), size(restricted)) newdims = Tuple(d[begin:s:end] for (d,s) in zip(dims(img),steps)) @@ -61,7 +58,7 @@ function ImageTransformations.restrict(img::AstroImage, region::Dims) end -ImageCore.pixelspacing(img::AstroImage) = step.(dims(img)) +ImageBase.pixelspacing(img::AstroImage) = step.(dims(img)) # ImageContrastAdjustment From 25a9077e9ecb723aa7b7b579be13edc6c110f3a8 Mon Sep 17 00:00:00 2001 From: Will Thompson Date: Sun, 24 Apr 2022 20:11:11 -0700 Subject: [PATCH 142/178] Update src/ccd2rgb.jl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mosè Giordano --- src/ccd2rgb.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ccd2rgb.jl b/src/ccd2rgb.jl index 7cfee4a9..5a4d76be 100644 --- a/src/ccd2rgb.jl +++ b/src/ccd2rgb.jl @@ -111,4 +111,4 @@ function composechannels( # ColorBlnding # missing/NaN handling end -export composechannels \ No newline at end of file +export composechannels From 1c92df9b603fb63c6a4c944e691be3d41575e6cf Mon Sep 17 00:00:00 2001 From: Will Thompson Date: Sun, 24 Apr 2022 20:12:06 -0700 Subject: [PATCH 143/178] Update src/AstroImages.jl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mosè Giordano --- src/AstroImages.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 0fe7f9bc..41443870 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -544,4 +544,4 @@ TODO: * FITSIO PR/issue (performance) * PlotUtils PR/issue (zscale with iteratble) -=# \ No newline at end of file +=# From 43f751f286c8331530180a45a51c483b557446de Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 25 Apr 2022 07:10:08 -0700 Subject: [PATCH 144/178] Add platescale option to implot Helpful for overplotting in some cases. Does not affect how WCS ticks and grid are calculated. --- src/plot-recipes.jl | 95 +++++++++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 43 deletions(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index b35f90ec..9599a01b 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -42,6 +42,7 @@ bias = get(plotattributes, :bias, 0.5) contrast = get(plotattributes, :contrast, 1) + platescale = get(plotattributes, :platescale, 1) grid := false # In most cases, a grid framestyle is a nicer looking default for images @@ -115,8 +116,8 @@ # To ensure the physical axis tick labels are correct the axes must be # tight to the image - xl = first(dims(imgv,1))-0.5, last(dims(imgv,1))+0.5 - yl = first(dims(imgv,2))-0.5, last(dims(imgv,2))+0.5 + xl = (first(dims(imgv,1))-0.5)*platescale, (last(dims(imgv,1))+0.5)*platescale + yl = (first(dims(imgv,2))-0.5)*platescale, (last(dims(imgv,2))+0.5)*platescale ylims --> yl xlims --> xl @@ -143,8 +144,8 @@ end - ax1 = collect(parent(dims(imgv,1))) - ax2 = collect(parent(dims(imgv,2))) + ax1 = collect(parent(dims(imgv,1))) .* platescale + ax2 = collect(parent(dims(imgv,2))) .* platescale # Views of images are not currently supported by plotly() so we have to collect them. # ax1, ax2, view(parent(imgv), reverse(axes(imgv,1)),:) ax1, ax2, parent(imgv)[reverse(axes(imgv,1)),:] @@ -246,7 +247,12 @@ yticks --> (gridspec.tickpos2x, wcslabels(wcs(imgv, wcsn), wcsax(imgv, dims(imgv,2)), gridspec.tickpos2w)) yguide --> ctype_label(wcs(imgv, wcsn).ctype[wcsax(imgv, dims(imgv,2))], wcs(imgv, wcsn).radesys) end - view(collect(imgv), reverse(axes(imgv,1)),:) + + ax1 = collect(parent(dims(imgv,1))) .* platescale + ax2 = collect(parent(dims(imgv,2))) .* platescale + # Views of images are not currently supported by plotly() so we have to collect them. + # ax1, ax2, view(parent(imgv), reverse(axes(imgv,1)),:) + ax1, ax2, parent(imgv)[reverse(axes(imgv,1)),:] end if showcolorbar @@ -283,56 +289,59 @@ end """ - implot(img::AstroImageMat; clims=extrema, stretch=identity, cmap=nothing) + implot( + img::AbstractArray; + clims=Percent(99.5), + stretch=identity, + cmap=:magma, + bias=0.5, + contrast=1, + wcsticks=true, + grid=true, + platescale=1 + ) Create a read only view of an array or AstroImageMat mapping its data values -to Colors according to `clims`, `stretch`, and `cmap`. - -The data is first clamped to `clims`, which can either be a tuple of (min, max) -values or a function accepting an iterator of pixel values that returns (min, max). -By default, `clims=extrema` i.e. the minimum and maximum of `img`. -Convenient functions to use for `clims` are: -`extrema`, `zscale`, and `percent(p)` +to an array of Colors. Equivalent to: + + implot( + imview( + img::AbstractArray; + clims=Percent(99.5), + stretch=identity, + cmap=:magma, + bias=0.5, + contrast=1, + ), + wcsn=1, + wcsticks=true, + wcstitle=true, + grid=true, + platescale=1 + ) -Next, the data is rescaled to [0,1] and remapped according to the function `stretch`. -Stretch can be any monotonic function mapping values in the range [0,1] to some range [a,b]. -Note that `log(0)` is not defined so is not directly supported. -For a list of convenient stretch functions, see: -`logstretch`, `powstretch`, `squarestretch`, `asinhstretch`, `sinhstretch`, `powerdiststretch` +### Image Rendering +See `imview` for how data is mapped to RGBA pixel values. -Finally the data is mapped to RGB values according to `cmap`. If cmap is `nothing`, -grayscale is used. ColorSchemes.jl defines hundreds of colormaps. A few nice ones for -images include: `:viridis`, `:magma`, `:plasma`, `:thermal`, and `:turbo`. +### WCS & Image Coordinates +If provided with an AstroImage that has WCS headers set, the tick marks and plot grid +are calculated using WCS.jl. By default, use the first WCS coordinate system. +The underlying pixel coordinates are those returned by `dims(img)` multiplied by `platescale`. +This allows you to overplot lines, regions, etc. using pixel coordinates. +If you wish to compute the pixel coordinate of a point in world coordinates, see `world_to_pix`. -Crucially, this function returns a view over the underlying data. If `img` is updated -then those changes will be reflected by this view with the exception of `clims` which -is not recalculated. +* `wcsn` (default `1`) select which WCS transform in the headers to use for ticks & grid +* `wcsticks` (default `true` if WCS headers present) display ticks and labels, and title using world coordinates +* `wcstitle` (default `true` if WCS headers present and `length(refdims(img))>0`). When slicing a cube, display the location along unseen axes in world coordinates instead of pixel coordinates. +* `grid` (default `true`) show a grid over the plot. Uses WCS coordinates if `wcsticks` is true, otherwise pixel coordinates multiplied by `platescale`. +* `platescale` (default `1`). Scales the underlying pixel coordinates to ease overplotting, etc. If `wcsticks` is false, the displayed pixel coordinates are also scaled. -Note: if clims or stretch is a function, the pixel values passed in are first filtered -to remove non-finite or missing values. ### Defaults The default values of `clims`, `stretch`, and `cmap` are `extrema`, `identity`, and `nothing` respectively. You may alter these defaults using `AstroImages.set_clims!`, `AstroImages.set_stretch!`, and `AstroImages.set_cmap!`. - -### Automatic Display -Arrays wrapped by `AstroImageMat()` get displayed as images automatically by calling -`imview` on them with the default settings when using displays that support showing PNG images. - -### Missing data -Pixels that are `NaN` or `missing` will be displayed as transparent when `cmap` is set -or black if. -+/- Inf will be displayed as black or white respectively. - -### Exporting Images -The view returned by `imview` can be saved using general `FileIO.save` methods. -Example: -```julia -v = imview(data, cmap=:magma, stretch=asinhstretch, clims=percent(95)) -save("output.png", v) -``` """ implot From 6e8d890f094076a51974f53fc82bfa3494af7d18 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 22 May 2022 08:45:04 -0700 Subject: [PATCH 145/178] Support rendering to an arbitrary colorant or named color. imview and implot now accept arbitrary colorants or "#RGB" strings as `cmap` which uses PlotUtils.cgrad to create a gradient. This lets us render an image to a particular colorchannel like `imview(img, cmap="red") or `imview(img, cmap="#F00")` --- src/imview.jl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/imview.jl b/src/imview.jl index 70c37487..bd86d573 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -93,12 +93,11 @@ end -""" -Helper to iterate over data skipping missing and non-finite values. -""" +# Helper to iterate over data skipping missing and non-finite values. skipmissingnan(itr) = Iterators.filter(el->!ismissing(el) && isfinite(el), itr) - +# Convert argument into a colorscheme or AbstractColorList which allow converting +# from numerical data into colors. function _lookup_cmap(cmap::Symbol) if cmap ∉ keys(ColorSchemes.colorschemes) error("$cmap not found in ColorSchemes.colorschemes. See: https://juliagraphics.github.io/ColorSchemes.jl/stable/catalogue/") @@ -107,6 +106,8 @@ function _lookup_cmap(cmap::Symbol) end _lookup_cmap(::Nothing) = ColorSchemes.colorschemes[:grays] _lookup_cmap(acl::AbstractColorList) = acl +_lookup_cmap(colorant::Colorant) = PlotUtils.cgrad([:black, colorant]) +_lookup_cmap(colorant::String) = PlotUtils.cgrad([:black, colorant]) function _resolve_clims(img::AbstractArray, clims) # Tuple or abstract array From e7e7d07e2a4aae32ac08ea7d47691ff80d4d0677 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 22 May 2022 09:56:07 -0700 Subject: [PATCH 146/178] Add composecolors --- src/AstroImages.jl | 2 +- src/ccd2rgb.jl | 169 ++++++++++++++++++--------------------------- src/imview.jl | 3 +- 3 files changed, 69 insertions(+), 105 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 0fe7f9bc..6c5c0b9e 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -28,7 +28,7 @@ export load, AstroImageMat, WCSGrid, ccd2rgb, - # composechannels, + composecolors, Zscale, Percent, logstretch, diff --git a/src/ccd2rgb.jl b/src/ccd2rgb.jl index 5a4d76be..835836b3 100644 --- a/src/ccd2rgb.jl +++ b/src/ccd2rgb.jl @@ -1,114 +1,77 @@ """ - ccd2rgb(red::ImageHDU, green::ImageHDU, blue::ImageHDU; stretch = identity, shape_out = size(red)) - ccd2rgb(red::Tuple{AbstractMatrix, WCSTransform}, green::Tuple{AbstractMatrix, WCSTransform}, - blue::Tuple{AbstractMatrix, WCSTransform}; stretch = identity, shape_out = size(red[1])) - -Converts 3 grayscale ImageHDU into RGB by reprojecting them. - -# Arguments -- `red`: Red channel data. -- `green`: Green channel data. -- `blue`: Blue channel data. -- `stretch`: Stretch function applied. -- `shape_out`: Shape of output RGB image. - -# Examples -```julia-repl -julia> ccd2rgb(r, b, g, shape_out = (1000,1000)) - -julia> ccd2rgb(r, b, g, shape_out = (1000,1000), stretch = log) - -julia> ccd2rgb(r, b, g, shape_out = (1000,1000), stretch = sqrt) - -julia> ccd2rgb(r, b, g, shape_out = (1000,1000), stretch = asinh) + composecolors( + images, + cmap=["#F00", "#0F0", "#00F"]; + clims, + stretch, + contrast, + bias, + multiplier + ) + +Create a color composite of multiple images by applying `imview` and blending +the results. This function can be used to create RGB composites using any number of channels +(e.g. red, green, blue, and hydrogen alpha) as well as more exotic images like blending +radio and optical data using two different colormaps. + +`cmap` should be a list of colorants, named colors (see Colors.jl), or colorschemes (see ColorSchemes.jl). +`clims`, `stretch`, `contrast`, and `bias` are passed on to `imview`. They can be a single value or +a list of different values for each image. + +Examples: +```julia +# Basic RGB +composecolors([redimage, greenimage, blueimage]) +# Non-linear stretch before blending +composecolors([redimage, greenimage, blueimage], stretch=asinhstretch) +# More than three channels are allowed (H alpha in pink) +composecolors( + [antred, antgreen, antblue, anthalp], + ["red", "green", "blue", "maroon1"], + multiplier=[1,2,1,1] +) +# Can mix +composecolors([radioimage, xrayimage], [:ice, :magma], clims=extrema) +composecolors([radioimage, xrayimage], [:magma, :viridis], clims=[Percent(99), Zscale()]) ``` """ -function ccd2rgb( - red::AstroImageMat, - green::AstroImageMat, - blue::AstroImageMat; - stretch = identity, - shape_out = size(red[1]) -) - red_rp = reproject(red, red, shape_out = shape_out)[1] - green_rp = reproject(green, red, shape_out = shape_out)[1] - blue_rp = reproject(blue, red, shape_out = shape_out)[1] - - I = (red_rp .+ green_rp .+ blue_rp) ./ 3 - I .= (x -> stretch(x)/x).(I) - - red_rp .*= I - green_rp .*= I - blue_rp .*= I - - m1 = maximum(x->isnan(x) ? -Inf : x, red_rp) - m2 = maximum(x->isnan(x) ? -Inf : x, green_rp) - m3 = maximum(x->isnan(x) ? -Inf : x, blue_rp) - return colorview(RGB, red_rp./m1 , green_rp./m2, blue_rp./m3) -end - -ccd2rgb(red::ImageHDU, green::ImageHDU, blue::ImageHDU; stretch = identity, shape_out = size(red)) = - ccd2rgb((read(red), WCS.from_header(read_header(red, String))[1]), (read(green), WCS.from_header(read_header(green, String))[1]), - (read(blue), WCS.from_header(read_header(blue, String))[1]), stretch = stretch, shape_out = shape_out) - - - -function composechannels( +function composecolors( images, - multipliers=ones(size(images)), # 0.299 * R + 0.587 * G + 0.114 * B - channels=["#F00", "#0F0", "#00F"]; - clims=extrema, - stretch=identity, - # reproject = all(==(wcs(first(images))), wcs(img) for img in images) ? false : wcs(first(images)), - reproject = false, - shape_out = size(first(images)), + cmap=nothing; + clims=_default_clims[], + stretch=_default_stretch[], + contrast=1.0, + bias=0.5, + multiplier=1.0 ) - if reproject == false - reprojected = images - else - if reproject == true - reproject = first(images) - end - reprojected = map(images) do image - Reproject.reproject(image, reproject; shape_out)[1] - end + if isempty(images) + error("At least one image is required.") end - I = broadcast(+, reprojected...) ./ length(reprojected) - I .= (x -> stretch(x)/x).(I) - - colors = parse.(Colorant, channels) - # @show colors - - # red_rp .*= I - # green_rp .*= I - # blue_rp .*= I - - # m1 = maximum(x->isnan(x) ? -Inf : x, red_rp) - # m2 = maximum(x->isnan(x) ? -Inf : x, green_rp) - # m3 = maximum(x->isnan(x) ? -Inf : x, blue_rp) - # return colorview(RGB, red_rp./m1 , green_rp./m2, blue_rp./m3) - - # return colorview(RGB, (reprojected .* multipliers)...) - - ## TODO: this all needs to be lazy - - colorized = map(eachindex(reprojected)) do i - reprojected[i] .* multipliers[i] .* colors[i] + if !allequal(size.(images)) + error("Images must have the same dimensions to compose them.") + end + if length(images) == 3 && isnothing(cmap) + cmap = ["red", "green", "blue"] + end + if length(cmap) < length(images) + error("Please provide a color channel for each image") end - mapped = (+).(colorized...) ./ length(reprojected) - T = coloralpha(eltype(mapped)) - mapped = T.(mapped) - mapped[isnan.(mapped)] .= RGBA(0,0,0,0) - - # Flip image to match conventions of other programs - flipped_view = view(mapped', reverse(axes(mapped,2)),:) - - return maybe_copyheaders(first(images), flipped_view) - # return (reprojected .* multipliers .* colors) + # Use imview to render each channel to RGBA + images_rendered = broadcast(images, cmap, clims, stretch, contrast, bias) do image, cmap, clims, stretch, contrast, bias + imview(image; cmap, clims, stretch, contrast, bias) + end - # TODO: more flexible blending - # ColorBlnding - # missing/NaN handling + # Now blend, ensuring each color channel never exceeds [0,1] + combined = mappedarray(images_rendered...) do channels... + pxblended = sum(channels .* multiplier) + return typeof(pxblended)( + clamp(pxblended.r,0,1), + clamp(pxblended.g,0,1), + clamp(pxblended.b,0,1), + clamp(pxblended.alpha,0,1) + ) + end + return combined end export composechannels diff --git a/src/imview.jl b/src/imview.jl index bd86d573..58c67fca 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -27,9 +27,9 @@ struct Percent end (p::Percent)(data::AbstractArray) = quantile(vec(data), (p.trim, 1-p.trim)) (p::Percent)(data) = p(collect(data)) +Base.broadcastable(p::Percent) = Ref(p) Base.show(io::IO, p::Percent; kwargs...) = print(io, "Percent($(p.perc))", kwargs...) - """ Zscale(options)(data) @@ -56,6 +56,7 @@ end (z::Zscale)(data::AbstractArray) = PlotUtils.zscale(vec(data), z.nsamples; z.contrast, z.max_reject, z.min_npixels, z.k_rej, z.max_iterations) (z::Zscale)(data) = z(collect(data)) Base.show(io::IO, z::Zscale; kwargs...) = print(io, "Zscale()", kwargs...) +Base.broadcastable(z::Zscale) = Ref(z) const _default_cmap = Base.RefValue{Union{Symbol,Nothing}}(:magma)#nothing) const _default_clims = Base.RefValue{Any}(Percent(99.5)) From 25d0f5952d959daa0f34d6f8408768e1ab7d546a Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 24 May 2022 10:27:45 -0700 Subject: [PATCH 147/178] For cmap=nothing, use a completely linear grayscale colormap instead of gray colorscheme --- src/imview.jl | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/imview.jl b/src/imview.jl index 58c67fca..687444fc 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -105,7 +105,7 @@ function _lookup_cmap(cmap::Symbol) end return ColorSchemes.colorschemes[cmap] end -_lookup_cmap(::Nothing) = ColorSchemes.colorschemes[:grays] +_lookup_cmap(::Nothing) = nothing _lookup_cmap(acl::AbstractColorList) = acl _lookup_cmap(colorant::Colorant) = PlotUtils.cgrad([:black, colorant]) _lookup_cmap(colorant::String) = PlotUtils.cgrad([:black, colorant]) @@ -281,7 +281,14 @@ function _imview(img, normed::AbstractArray{T}, stretch, cmap, contrast, bias) w RGBA{N0f8}(0,0,0,1) end else - RGBA{N0f8}(get(cmap, stretched, (false, true))) + if isnothing(cmap) + # true/false used as numerical values to prevent unucessary promotion + s = clamp(stretched, false, true) + RGBA{N0f8}(s,s,s,1) + else + # Look up colormap + RGBA{N0f8}(get(cmap, stretched, (false, true))) + end end return pix end From e2ce839555c1848a0b832cbb380f5eaf3cd43d5a Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 24 May 2022 10:28:18 -0700 Subject: [PATCH 148/178] Begin adding tests --- test/runtests.jl | 209 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 176 insertions(+), 33 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 38d5ea1f..ace19076 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3,6 +3,8 @@ using AstroImages, FITSIO, Images, Random, WCS using Test, WCS using SHA: sha256 +using Statistics + import AstroImages: _float, render @testset "Conversion to float and fixed-point" begin @@ -78,7 +80,6 @@ end @testset "Opening AstroImage in different ways" begin data = rand(2,2) - wcs = WCSTransform(2;) FITS(fname, "w") do f write(f, data) end @@ -87,8 +88,6 @@ end @test AstroImage(fname, 1) isa AstroImage @test AstroImage(f, 1) isa AstroImage @test AstroImage(data, header) isa AstroImage - @test AstroImage(data, wcs) isa AstroImage - @test AstroImage(data, wcs) isa AstroImage close(f) end @@ -121,39 +120,183 @@ end @test length(AstroImage(rand(10,10))) == 100 end -# @testset "multi wcs AstroImage" begin -# fname = tempname() * ".fits" -# f = FITS(fname, "w") -# inhdr = FITSHeader(["CTYPE1", "CTYPE2", "RADESYS", "FLTKEY", "INTKEY", "BOOLKEY", "STRKEY", "COMMENT", -# "HISTORY"], -# ["RA---TAN", "DEC--TAN", "UNK", 1.0, 1, true, "string value", nothing, nothing], -# ["", -# "", -# "", -# "floating point keyword", -# "", -# "boolean keyword", -# "string value", -# "this is a comment", -# "this is a history"]) +@testset "multi wcs AstroImage" begin + fname = tempname() * ".fits" + f = FITS(fname, "w") + inhdr = FITSHeader([ + "FLTKEY", "INTKEY", "BOOLKEY", "STRKEY", "COMMENT", "HISTORY", + "CRVAL1a", + "CRVAL2a", + "CRPIX1a", + "CRPIX2a", + "CDELT1a", + "CDELT2a", + "CTYPE1a", + "CTYPE2a", + "CUNIT1a", + "CUNIT2a", -# indata = reshape(Float32[1:100;], 5, 20) -# write(f, indata; header=inhdr) -# write(f, indata; header=inhdr) -# close(f) + "CRVAL1b", + "CRVAL2b", + "CRPIX1b", + "CRPIX2b", + "CDELT1b", + "CDELT2b", + "CTYPE1b", + "CTYPE2b", + "CUNIT1b", + "CUNIT2b", + ], + [ + 1.0, 1, true, "string value", nothing, nothing, + 0.5, + 89.5, + 1, + 1, + 1, + -1, + "RA---TAN", + "DEC--TAN", + "deg ", + "deg ", -# img = AstroImage(fname, (1,2)) -# f = FITS(fname) -# @test length(img.wcs) == 2 -# @test WCS.to_header(img.wcs[1]) === WCS.to_header(WCS.from_header(read_header(f[1], String))[1]) -# @test WCS.to_header(img.wcs[2]) === WCS.to_header(WCS.from_header(read_header(f[2], String))[1]) + 0.5, + 89.5, + 1, + 1, + 1, + -1, + "RA---TAN", + "DEC--TAN", + "deg ", + "deg ", + ], + [ + "floating point keyword", "", "boolean keyword", "string value", "this is a comment", "this is a history", + "", + "", + "", + "", + "", + "", + "Terrestrial East Longitude", + "Terrestrial North Latitude", + "", + "", + + "", + "", + "", + "", + "", + "", + "Terrestrial East Longitude", + "Terrestrial North Latitude", + "", + "", + ]) -# img = AstroImage(Gray, f, (1,2)) -# @test length(img.wcs) == 2 -# @test WCS.to_header(img.wcs[1]) === WCS.to_header(WCS.from_header(read_header(f[1], String))[1]) -# @test WCS.to_header(img.wcs[2]) === WCS.to_header(WCS.from_header(read_header(f[2], String))[1]) -# close(f) -# end + indata = reshape(Float32[1:100;], 5, 20) + write(f, indata; header=inhdr) + close(f) + + img = AstroImage(fname) + f = FITS(fname) + @test length(wcs(img)) == 2 + @test WCS.to_header(wcs(img,1)) === WCS.to_header(WCS.from_header(read_header(f[1], String))[1]) + @test WCS.to_header(wcs(img,2)) === WCS.to_header(WCS.from_header(read_header(f[1], String))[2]) + + img = AstroImage(f) + @test length(wcs(img)) == 2 + @test WCS.to_header(wcs(img,1)) === WCS.to_header(WCS.from_header(read_header(f[1], String))[1]) + @test WCS.to_header(wcs(img,2)) === WCS.to_header(WCS.from_header(read_header(f[1], String))[2]) + close(f) +end + +## +@testset "imview" begin + + arr1 = permutedims(reshape(1:9,3,3)) + img = AstroImage(arr1) + + @test imview(arr1) == imview(img) + @test imview(img) isa AstroImage + @test !(imview(arr1) isa AstroImage) + + img_rendered_1 = imview(img, clims=(1,9), stretch=identity, contrast=1, bias=0.5, cmap=nothing) + + # Image Orientation + @test CartesianIndex(3,1) == argmin(Gray.(img_rendered_1)) + @test CartesianIndex(1,3) == argmax(Gray.(img_rendered_1)) + + # Rendering Basics + @test allunique(img_rendered_1) + # It is intended that the rendered image is flipped vs it's data + @test img_rendered_1[3,1] == RGBA(0,0,0,1) + @test img_rendered_1[1,3] == RGBA(1,1,1,1) + @test all(p -> p.r==p.g==p.b && p.alpha==1, img_rendered_1) + + # Limits + img_rendered_2 = imview(img, clims=(3,7), stretch=identity, contrast=1, bias=0.5, cmap=nothing) + @test length(unique(img_rendered_2)) == 5 + @test count(==(RGBA(0,0,0,1)), img_rendered_2) == 3 + @test count(==(RGBA(1,1,1,1)), img_rendered_2) == 3 + + # Calculated limits + @test img_rendered_1 == imview(img, clims=extrema, stretch=identity, contrast=1, bias=0.5, cmap=nothing) + img_rendered_3 = imview(img, clims=Zscale(), stretch=identity, contrast=1, bias=0.5, cmap=nothing) + img_rendered_4 = imview(img, clims=Percent(100), stretch=identity, contrast=1, bias=0.5, cmap=nothing) + @test img_rendered_1 == img_rendered_3 + @test img_rendered_1 == img_rendered_4 + + # Stretching + for stretchfunc in (sqrtstretch, asinhstretch, powerdiststretch, logstretch, powstretch, squarestretch, sinhstretch) + img_rendered_5 = imview(arr1, clims=(1,9), stretch=stretchfunc, contrast=1, bias=0.5, cmap=nothing) + @test extrema(Gray.(img_rendered_5)) == (0,1) + manual_stretch = stretchfunc.(AstroImages.clampednormedview(arr1,(1,9))) + @test Gray.(img_rendered_5) ≈ + N0f8.((manual_stretch.-minimum(manual_stretch)) ./ + (maximum(manual_stretch)-minimum(manual_stretch)))'[end:-1:begin,:] + end + + # Contrast/Bias + @test Gray.(imview(img, clims=extrema, stretch=identity, contrast=1, bias=0.6, cmap=nothing)) == + N0f8.(clamp.(N0f8.(Gray.(img_rendered_1)) .- 0.1,false,true)) + + img_rendered_5 = imview(arr1, clims=(1,9), stretch=sqrtstretch, contrast=0.5, bias=0.5, cmap=nothing) + + @show mean(diff(sort(Gray.(vec(img_rendered_1))))) ≈ 2mean(diff(sort(Gray.(vec(img_rendered_5))))) + + # Missing/NaN + for m in (NaN, missing) + arr2 = [ + 1 2 3 + 4 m 6 + 7 8 9 + ] + @test imview(arr2) == imview(AstroImage(arr2)) + @test imview(arr2)[2,2].alpha == 0 + @test 8 == count(img_rendered_1 .== imview(arr2, clims=(1,9), stretch=identity, contrast=1, bias=0.5, cmap=nothing)) + end + + img_rendered_6 = imview([1, 2, NaN, missing, -Inf, Inf], clims=extrema) + img_rendered_6b = imview([1, 2], clims=extrema) + + @test img_rendered_6[1] == img_rendered_6b[1] + @test img_rendered_6[2] == img_rendered_6b[2] + @test img_rendered_6[1].alpha == 1 + @test img_rendered_6[2].alpha == 1 + @test img_rendered_6[3].alpha == 0 + @test img_rendered_6[4].alpha == 0 + @test img_rendered_6[5] == RGBA(0,0,0,1) + @test img_rendered_6[6] == RGBA(1,1,1,1) +end + +## + +1 + +## # @testset "multi file AstroImage" begin # fname1 = tempname() * ".fits" From 2d5fff28c60c5e43837dc22838a5579c39d515b5 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 24 May 2022 10:30:58 -0700 Subject: [PATCH 149/178] Migrate to Github actions --- .github/workflows/ci.yml | 68 ++++++++++++++++++++++++++++++++++++++ .github/workflows/docs.yml | 35 ++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..0ace1308 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,68 @@ +name: CI +on: + pull_request: + branches: + - master + push: + branches: + - master + tags: '*' +jobs: + test: + name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + version: + - '1.7' # Replace this with the minimum Julia version that your package supports. E.g. if your package requires Julia 1.5 or higher, change this to '1.5'. + - '1' # Leave this line unchanged. '1' will automatically expand to the latest stable 1.x release of Julia. + - 'nightly' + os: + - ubuntu-latest + arch: + - x64 + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: actions/cache@v1 + env: + cache-name: cache-artifacts + with: + path: ~/.julia/artifacts + key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} + restore-keys: | + ${{ runner.os }}-test-${{ env.cache-name }}- + ${{ runner.os }}-test- + ${{ runner.os }}- + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v1 + with: + file: lcov.info + # docs: + # name: Documentation + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v2 + # - uses: julia-actions/setup-julia@v1 + # with: + # version: '1' + # - run: | + # julia --project=docs -e ' + # using Pkg + # Pkg.develop(PackageSpec(path=pwd())) + # Pkg.instantiate()' + # - run: | + # julia --project=docs -e ' + # using Documenter: doctest + # using MYPACKAGE + # doctest(MYPACKAGE)' # change MYPACKAGE to the name of your package + # - run: julia --project=docs docs/make.jl + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} \ No newline at end of file diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..472b631f --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,35 @@ +name: Documentation + +on: + push: + branches: + - master + tags: '*' + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@latest + with: + version: '1.7' + - name: Install dependencies + run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' + - uses: actions/cache@v1 + env: + cache-name: cache-artifacts + with: + path: ~/.julia/artifacts + key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} + restore-keys: | + ${{ runner.os }}-test-${{ env.cache-name }}- + ${{ runner.os }}-test- + ${{ runner.os }}- + - name: Build and deploy + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # If authenticating with GitHub Actions token + # DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # If authenticating with SSH deploy key + run: julia --project=docs/ docs/make.jl + From a9789323c94cb3c4cb32308c40a10391b35146fe Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 24 May 2022 10:37:06 -0700 Subject: [PATCH 150/178] Fix test dependencies --- Project.toml | 5 +---- test/runtests.jl | 3 +-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Project.toml b/Project.toml index 5762e44a..8882f690 100644 --- a/Project.toml +++ b/Project.toml @@ -29,11 +29,8 @@ julia = "^1.6.0" ImageBase = "^0.1.5" [extras] -JLD = "4138dd39-2aa7-5051-a626-17a0bb65d9c8" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -Widgets = "cc8bc4a8-27d6-5769-a93b-9d913e69aa62" [targets] -test = ["Test", "Random", "Widgets", "JLD", "SHA"] +test = ["Test", "WCS", "FITSIO", "Random", "Statistics"] diff --git a/test/runtests.jl b/test/runtests.jl index ace19076..b9f72a02 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,7 +1,6 @@ -using AstroImages, FITSIO, Images, Random, WCS +using AstroImages, FITSIO, Random, WCS using Test, WCS -using SHA: sha256 using Statistics From cb916f6fed403e9979fee49f88a536e61f68427b Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 24 May 2022 10:46:13 -0700 Subject: [PATCH 151/178] Attempt 2 to fix test dependencies --- .github/workflows/ci.yml | 2 +- Project.toml | 6 +++++- test/runtests.jl | 6 +----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ace1308..f6bbbde8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: version: - - '1.7' # Replace this with the minimum Julia version that your package supports. E.g. if your package requires Julia 1.5 or higher, change this to '1.5'. + - '1.6' # Replace this with the minimum Julia version that your package supports. E.g. if your package requires Julia 1.5 or higher, change this to '1.5'. - '1' # Leave this line unchanged. '1' will automatically expand to the latest stable 1.x release of Julia. - 'nightly' os: diff --git a/Project.toml b/Project.toml index 8882f690..b381863f 100644 --- a/Project.toml +++ b/Project.toml @@ -31,6 +31,10 @@ ImageBase = "^0.1.5" [extras] Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +WCS = "15f3aee2-9e10-537f-b834-a6fb8bdb944d" +FITSIO = "525bcba6-941b-5504-bd06-fd0dc1a4d2eb" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +ImageBase = "c817782e-172a-44cc-b673-b171935fbb9e" [targets] -test = ["Test", "WCS", "FITSIO", "Random", "Statistics"] +test = ["Test", "WCS", "FITSIO", "Random", "Statistics", "ImageBase"] diff --git a/test/runtests.jl b/test/runtests.jl index b9f72a02..a81d1cb8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,8 +1,4 @@ -using AstroImages, FITSIO, Random, WCS - -using Test, WCS - -using Statistics +using AstroImages, FITSIO, Random, WCS, ImageBase, Test, WCS, Statistics import AstroImages: _float, render From b161d0c005313cd70d8d7744a61eead263ebb9e1 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Tue, 24 May 2022 10:55:39 -0700 Subject: [PATCH 152/178] Resolve syntax error in Julia 1.6 by sticking to .& instead of .&& --- src/plot-recipes.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plot-recipes.jl b/src/plot-recipes.jl index 9599a01b..bc5bd477 100644 --- a/src/plot-recipes.jl +++ b/src/plot-recipes.jl @@ -1079,7 +1079,7 @@ end end # Only show arrows where the data is finite, and more than a couple pixels # long. - mask = isfinite.(qpolintenr) .&& qpolintenr .>= minpol.*qmaxlen + mask = (isfinite.(qpolintenr)) .& (qpolintenr .>= minpol.*qmaxlen) pointstmp = map(xs[mask],ys[mask],qx[mask],qy[mask]) do x,y,qxi,qyi return ([x, x+a*qxi, NaN], [y, y+a*qyi, NaN]) end From 091e6a5f3244d39d17092eea4c7ee553a5352a8c Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 30 May 2022 11:30:46 -0700 Subject: [PATCH 153/178] Stop exporting Between --- src/AstroImages.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index aa22ce89..9626ee70 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -41,6 +41,8 @@ export load, imview, arraydata, header, + copyheader, + shareheader, wcs, Comment, History, @@ -50,7 +52,7 @@ export load, Pol, Ti, X, Y, Z, Dim, - At, Near, Between, .., + At, Near, .., dims, refdims, recenter, pix_to_world, @@ -404,7 +406,6 @@ See also: [`shareheader`](@ref). """ copyheader(img::AstroImage, data::AbstractArray) = AstroImage(data, dims(img), refdims(img), deepcopy(header(img)), copy(getfield(img, :wcs)), Ref(getfield(img, :wcs_stale)[]), getfield(img,:wcsdims)) -export copyheader """ shareheader(img::AstroImage, data) -> imgnew @@ -414,7 +415,6 @@ synchronized header; modifying one also affects the other. See also: [`copyheader`](@ref). """ shareheader(img::AstroImage, data::AbstractArray) = AstroImage(data, dims(img), refdims(img), header(img), getfield(img, :wcs), Ref(getfield(img, :wcs_stale)[]), getfield(img,:wcsdims)) -export shareheader # Share header if an AstroImage, do nothing if AbstractArray maybe_shareheader(img::AstroImage, data) = shareheader(img, data) maybe_shareheader(::AbstractArray, data) = data From 2647d7a3944647553befa5f92d81f96ce9a5cbe8 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 30 May 2022 11:33:53 -0700 Subject: [PATCH 154/178] Improve CI & docs according to @giordano's suggestions --- .github/workflows/ci.yml | 33 +-------------------------------- .github/workflows/docs.yml | 15 +++------------ 2 files changed, 4 insertions(+), 44 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6bbbde8..877c1dd0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,46 +23,15 @@ jobs: arch: - x64 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: julia-actions/setup-julia@v1 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - uses: actions/cache@v1 - env: - cache-name: cache-artifacts - with: - path: ~/.julia/artifacts - key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} - restore-keys: | - ${{ runner.os }}-test-${{ env.cache-name }}- - ${{ runner.os }}-test- - ${{ runner.os }}- - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v1 with: file: lcov.info - # docs: - # name: Documentation - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v2 - # - uses: julia-actions/setup-julia@v1 - # with: - # version: '1' - # - run: | - # julia --project=docs -e ' - # using Pkg - # Pkg.develop(PackageSpec(path=pwd())) - # Pkg.instantiate()' - # - run: | - # julia --project=docs -e ' - # using Documenter: doctest - # using MYPACKAGE - # doctest(MYPACKAGE)' # change MYPACKAGE to the name of your package - # - run: julia --project=docs docs/make.jl - # env: - # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} \ No newline at end of file diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 472b631f..4e16f0a1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -11,25 +11,16 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: julia-actions/setup-julia@latest with: version: '1.7' - name: Install dependencies - run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' + run: julia --color=yes --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' - uses: actions/cache@v1 - env: - cache-name: cache-artifacts - with: - path: ~/.julia/artifacts - key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} - restore-keys: | - ${{ runner.os }}-test-${{ env.cache-name }}- - ${{ runner.os }}-test- - ${{ runner.os }}- - name: Build and deploy env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # If authenticating with GitHub Actions token # DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # If authenticating with SSH deploy key - run: julia --project=docs/ docs/make.jl + run: julia --color=yes --project=docs/ docs/make.jl From 7de1b7f64ace60454a21c3407a9c0a330466a4a9 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 30 May 2022 11:38:21 -0700 Subject: [PATCH 155/178] CI typo --- .github/workflows/ci.yml | 2 +- .github/workflows/docs.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 877c1dd0..69dd5e98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: actions/cache@v1 + - uses: julia-actions/cache@v1 - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4e16f0a1..9928b682 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -17,7 +17,7 @@ jobs: version: '1.7' - name: Install dependencies run: julia --color=yes --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' - - uses: actions/cache@v1 + - uses: julia-actions/cache@v1 - name: Build and deploy env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # If authenticating with GitHub Actions token From 077c1dfb9855443d8ad660cd266cd7fc9037ecc7 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 30 May 2022 12:51:07 -0700 Subject: [PATCH 156/178] Use DemoCards.jl for examples --- docs/Project.toml | 2 ++ docs/examples/basics/loading.jl | 23 +++++++++++++++++++++++ docs/examples/config.json | 4 ++++ docs/examples/index.md | 3 +++ docs/make.jl | 26 +++++++++++++++++++++++--- docs/src/tour.md | 21 --------------------- 6 files changed, 55 insertions(+), 24 deletions(-) create mode 100644 docs/examples/basics/loading.jl create mode 100644 docs/examples/config.json create mode 100644 docs/examples/index.md delete mode 100644 docs/src/tour.md diff --git a/docs/Project.toml b/docs/Project.toml index 3a52a5db..c90cce91 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,4 +1,6 @@ [deps] +AstroImages = "fe3fc30c-9b16-11e9-1c73-17dabf39f4ad" +DemoCards = "311a05b2-6137-4a5a-b473-18580a3d38b5" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" [compat] diff --git a/docs/examples/basics/loading.jl b/docs/examples/basics/loading.jl new file mode 100644 index 00000000..5491a437 --- /dev/null +++ b/docs/examples/basics/loading.jl @@ -0,0 +1,23 @@ +# --- +# title: Loading Images +# description: Loading FITS images from files. +# author: "[William Thompson](https://github.com/sefffal)" +# cover: assets/loading-images.png +# --- + +# We'll start by downloading a sample image. If you have an image stored locally, +# you would skip this step. +using AstroImages +fname = download( + "http://www.astro.uvic.ca/~wthompson/astroimages/fits/656nmos.fits", + "eagle-656nmos.fits" +) #hide + +# Load the image by filename. +# If unspecified, the image is loaded from the first image-HDU in the fits file. +img = AstroImage("eagle-656nmos.fits") + + +# --- save covers --- #src +mkpath("assets") #src +save("assets/loading-images.png", imview(img)) #src \ No newline at end of file diff --git a/docs/examples/config.json b/docs/examples/config.json new file mode 100644 index 00000000..84b31814 --- /dev/null +++ b/docs/examples/config.json @@ -0,0 +1,4 @@ +{ + "template": "index.md", + "theme": "list" +} \ No newline at end of file diff --git a/docs/examples/index.md b/docs/examples/index.md new file mode 100644 index 00000000..96ca2426 --- /dev/null +++ b/docs/examples/index.md @@ -0,0 +1,3 @@ +# Examples + +{{{democards}}} \ No newline at end of file diff --git a/docs/make.jl b/docs/make.jl index 4c879704..1c1c35d4 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,12 +1,30 @@ -using Documenter, AstroImages +using Documenter, DemoCards, AstroImages + +# 1. generate demo files +demopage, postprocess_cb, demo_assets = makedemos("examples") # this is the relative path to docs/ + +# if there are generated css assets, pass it to Documenter.HTML +assets = [] +isnothing(demo_assets) || (push!(assets, demo_assets)) + +# 2. normal Documenter usage +format = Documenter.HTML(assets = assets) +makedocs(format = format, + pages = [ + "Home" => "index.md", + demopage, + ], + sitename = "Awesome demos") + + makedocs( sitename="AstroImages.jl", pages = [ "Home" => "index.md", - "Tour" => "tour.md", - "Tutorials" => [ + "Manual" => [ "Getting Started" => "getting-started.md" ], + demopage, "API" => "api.md", ], format = Documenter.HTML( @@ -15,6 +33,8 @@ makedocs( workdir=".." ) +# 3. postprocess after makedocs +postprocess_cb() # deploydocs( # repo = "github.com/sefffal/AstroImages.jl.git", diff --git a/docs/src/tour.md b/docs/src/tour.md deleted file mode 100644 index 7c3efe2d..00000000 --- a/docs/src/tour.md +++ /dev/null @@ -1,21 +0,0 @@ -# Package Tour - -To follow along, download the images from the [Fits Liberator](https://esahubble.org/projects/fits_liberator/eagledata/) page and unzip them. - -```@meta -DocTestSetup = quote - using AstroImages -end -``` - -```@repl -using AstroImages -``` - -Let's start by loading a FITS file. -```@repl -using AstroImages -eagle_656 = load("fits/656nmos.fits") -save("eagle-1.png") # hide -``` -![eagle](eagle-1.png) From 84efb96e2f5862379051b61de8298aef89e744bd Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 30 May 2022 13:11:10 -0700 Subject: [PATCH 157/178] Fix set_cmap! --- src/imview.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/imview.jl b/src/imview.jl index 687444fc..28caf370 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -58,7 +58,7 @@ end Base.show(io::IO, z::Zscale; kwargs...) = print(io, "Zscale()", kwargs...) Base.broadcastable(z::Zscale) = Ref(z) -const _default_cmap = Base.RefValue{Union{Symbol,Nothing}}(:magma)#nothing) +const _default_cmap = Base.RefValue{Union{Symbol,Nothing}}(:magma) const _default_clims = Base.RefValue{Any}(Percent(99.5)) const _default_stretch = Base.RefValue{Any}(identity) @@ -70,7 +70,9 @@ Alter the default color map used to display images when using `imview` or displaying an AstroImageMat. """ function set_cmap!(cmap) - _default_cmap[] = _lookup_cmap(cmap) + # Ensure it's valid + _lookup_cmap(cmap) + _default_cmap[] = cmap end """ set_clims!(clims::Tuple) From a50773970e69dead73ae2b3395dce2a94d497eee Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 30 May 2022 13:11:26 -0700 Subject: [PATCH 158/178] cleanup --- src/AstroImages.jl | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 9626ee70..1d7aed40 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -131,21 +131,14 @@ const dimnames = ( const Spec = Dim{:Spec} const Pol = Dim{:Pol} -# struct Wcs{N,T} <: DimensionalData.Dimension{T} -# val::T -# end -# Wcs{N}(val::T) where {N,T} = Wcs{N,T}(val) -# Wcs{N}() where N = Wcs{N}(:) -# DimensionalData.name(::Type{<:Wcs{N}}) where N = Symbol("Wcs$N") -# DimensionalData.basetypeof(::Type{<:Wcs{N}}) where N = Wcs{N} -# # DimensionalData.key2dim(::Val{N}) where N<:Integer = Wcs{N}() -# DimensionalData.dim2key(::Type{D}) where D<:Wcs{N} where N = Symbol("Wcs$N") -# wcsax(::Wcs{N}) where N = N """ wcsax(img, dim) -Return the WCS axis number associated with a dimension. +Return the WCS axis number associated with a dimension, even if the image +has been slices or otherwise transformed. + +Internally, the order is stored in the field `wcsdims`. """ function wcsax(img::AstroImage, dim) return findfirst(di->name(di)==name(dim), img.wcsdims) From edc93570c4c0c4de974f53419d8037913c1f0976 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Mon, 30 May 2022 13:11:47 -0700 Subject: [PATCH 159/178] docs WIP --- docs/Project.toml | 2 + docs/examples/basics/displaying.jl | 60 ++++++++++++++++++++++++++++++ docs/examples/basics/loading.jl | 2 +- 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 docs/examples/basics/displaying.jl diff --git a/docs/Project.toml b/docs/Project.toml index c90cce91..c980d5bb 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -2,6 +2,8 @@ AstroImages = "fe3fc30c-9b16-11e9-1c73-17dabf39f4ad" DemoCards = "311a05b2-6137-4a5a-b473-18580a3d38b5" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19" +Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" [compat] Documenter = "0.27" diff --git a/docs/examples/basics/displaying.jl b/docs/examples/basics/displaying.jl new file mode 100644 index 00000000..85b71ba9 --- /dev/null +++ b/docs/examples/basics/displaying.jl @@ -0,0 +1,60 @@ +# --- +# title: Displaying Images +# author: "[William Thompson](https://github.com/sefffal)" +# cover: assets/displaying-images.png +# --- + +# We'll start by downloading a sample image. If you have an image stored locally, +# you would skip this step. +using AstroImages + +AstroImages.set_clims!(Percent(99.5)) #src +AstroImages.set_cmap!(:magma) #src +AstroImages.set_stretch!(identity) #src + + +# Any AbstractArray can be visualized with the `imview` function. +arr = randn(10,10) +imview(arr) + +# Let's load an astronomical image to see how we can tweak its display +fname = download( + "http://www.astro.uvic.ca/~wthompson/astroimages/fits/656nmos.fits", + "eagle-656nmos.fits" +); +img = AstroImage("eagle-656nmos.fits"); +imview(img) + +# We can adjust the color limits manually +imview(img, clims=(0,100)) + +# Or provide a function to calculate them for us +imview(img, clims=extrema) + +# AstroImages includes some handy callables, like Percent and Zscale.flags +# `Percent` sets the limits to include some central percentage of the data range +# For example, 95% sets the color limits to clip the top and bottom 2.5% of pixels. +# Percent(99.5) is the default value of clims. +imview(img, clims=Percent(95)) + + + + +# Arrays wrapped by `AstroImage` are displayed automatically using `imview` +AstroImage(randn(10,10)) + +# The settings for automatic imview are controlled using package defaults that can +# be adjusted to suit your tastes +AstroImages.set_clims!(Zscale()) # Display the full range automatically +AstroImages.set_cmap!(:viridis) +AstroImages.set_stretch!(asinhstretch) +AstroImage(randn(10,10)) + +# --- restore defaults --- #src +AstroImages.set_clims!(Percent(99.5)) #src +AstroImages.set_cmap!(:magma) #src +AstroImages.set_stretch!(identity) #src + +# --- save covers --- #src +mkpath("assets") #src +save("assets/loading-images.png", imview(img)) #src \ No newline at end of file diff --git a/docs/examples/basics/loading.jl b/docs/examples/basics/loading.jl index 5491a437..d116e4ca 100644 --- a/docs/examples/basics/loading.jl +++ b/docs/examples/basics/loading.jl @@ -11,7 +11,7 @@ using AstroImages fname = download( "http://www.astro.uvic.ca/~wthompson/astroimages/fits/656nmos.fits", "eagle-656nmos.fits" -) #hide +); # Load the image by filename. # If unspecified, the image is loaded from the first image-HDU in the fits file. From d2431f5a9223686d6cb329ca0e84240b9234bd93 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Thu, 2 Jun 2022 14:18:08 -0700 Subject: [PATCH 160/178] Add keyword argument version of DimensionalData.rebuild --- src/AstroImages.jl | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 1d7aed40..42b4b25f 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -191,6 +191,25 @@ DimensionalData.metadata(::AstroImage) = DimensionalData.Dimensions.LookupArrays ) return AstroImage(data, dims, refdims, header, wcs, Ref(wcs_stale), wcsdims) end +# Keyword argument version. +# We have to define this since our struct contains additional field names. +@inline function DimensionalData.rebuild( + img::AstroImage; + data, + # Fields for DimensionalData + dims::Tuple=DimensionalData.dims(img), + refdims::Tuple=DimensionalData.refdims(img), + name::Union{Symbol,DimensionalData.AbstractName,Nothing}=nothing, + metadata::Union{DimensionalData.LookupArrays.AbstractMetadata,Nothing}=nothing, + # FITS Header beloning to this image, if any + header::FITSHeader=deepcopy(header(img)), + # A cached WCSTransform object for this data + wcs::Vector{WCSTransform}=getfield(img, :wcs), + wcs_stale::Bool=getfield(img, :wcs_stale)[], + wcsdims::Tuple=(dims...,refdims...), +) + return AstroImage(data, dims, refdims, header, wcs, Ref(wcs_stale), wcsdims) +end @inline DimensionalData.rebuildsliced( f::Function, img::AstroImage, From edba8f0ebff6eda2857fdafffc6a876279f35c72 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Thu, 2 Jun 2022 14:18:28 -0700 Subject: [PATCH 161/178] Docs WIP --- docs/src/basics.md | 4 ++++ docs/src/getting-started.md | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 docs/src/basics.md diff --git a/docs/src/basics.md b/docs/src/basics.md new file mode 100644 index 00000000..ea7f9d7d --- /dev/null +++ b/docs/src/basics.md @@ -0,0 +1,4 @@ +The AstroImages package provides a wrapper, `AstroImage`, that can wrap any AbstractArray. +An `AstroImage` should behave like a plain array, but gains a few extra abilities. + +First, AstroImages are automatically displayed when returned as results in many environements, including VSCode, Jupyter, Pluto, ImageShow, and ImageInTerminal \ No newline at end of file diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md index bad55622..b4c406c9 100644 --- a/docs/src/getting-started.md +++ b/docs/src/getting-started.md @@ -1 +1,24 @@ # Getting Started + +To get started, you will first need to install AstroImages. +After starting Julia, enter package-mode by typing `]` and then +```julia-repl +pkg> add AstroImages +``` + +To display images and save them in traditional graphics formats like PNG, JPG, GIF, etc., you +will also need to add the `ImageIO` package. Once installed, this package doesn't need to be +loaded explicitly. + + +For some of the more advanced visualizations you may also want `Plots`: +```julia-repl +pkg> add Plots +``` + +To load the package, run: +```julia +using AstroImages +# And if desired: +using Plots +``` \ No newline at end of file From bbcff54ef2f244867d954f666e7ec718fa63a168 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 3 Jun 2022 07:19:06 -0700 Subject: [PATCH 162/178] Cleanup --- Project.toml | 19 ++++++++++++++----- src/AstroImages.jl | 27 +-------------------------- src/showmime.jl | 44 ++------------------------------------------ 3 files changed, 17 insertions(+), 73 deletions(-) diff --git a/Project.toml b/Project.toml index b381863f..af841203 100644 --- a/Project.toml +++ b/Project.toml @@ -12,7 +12,6 @@ FITSIO = "525bcba6-941b-5504-bd06-fd0dc1a4d2eb" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" ImageAxes = "2803e5a7-5153-5ecf-9a86-9b4c37f5f5ac" ImageBase = "c817782e-172a-44cc-b673-b171935fbb9e" -ImageMetadata = "bc367c6b-8a6b-528e-b4bd-a4b897500b49" ImageShow = "4e3cecfd-b093-5904-9786-8bbb286a6a31" MappedArrays = "dbb5928d-eab1-5f90-85c2-b9b0edb7c900" PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043" @@ -20,19 +19,29 @@ Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" -UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" WCS = "15f3aee2-9e10-537f-b834-a6fb8bdb944d" + [compat] -DimensionalData = "^0.20" julia = "^1.6.0" +AbstractFFTs = "1.1" +AstroAngles = "0.1" +ColorSchemes = "3.18" +DimensionalData = "^0.20" +FITSIO = "0.16" +FileIO = "1.14" +ImageAxes = "0.6" ImageBase = "^0.1.5" +ImageShow = "0.3" +MappedArrays = "0.4" +PlotUtils = "1.2" +RecipesBase = "1.2" +Tables = "1.7" +WCS = "0.6" [extras] Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -WCS = "15f3aee2-9e10-537f-b834-a6fb8bdb944d" -FITSIO = "525bcba6-941b-5504-bd06-fd0dc1a4d2eb" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" ImageBase = "c817782e-172a-44cc-b673-b171935fbb9e" diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 42b4b25f..8955325e 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -18,7 +18,6 @@ using AstroAngles using Printf using PlotUtils: PlotUtils using PlotUtils: optimize_ticks, AbstractColorList -using UUIDs export load, @@ -27,7 +26,6 @@ export load, AstroImageVec, AstroImageMat, WCSGrid, - ccd2rgb, composecolors, Zscale, Percent, @@ -39,6 +37,7 @@ export load, sinhstretch, powerdiststretch, imview, + render, # deprecated arraydata, header, copyheader, @@ -533,27 +532,3 @@ function __init__() end end # module - - -#= -TODO: - - -* properties? -* contrast/bias? -* interactive (Jupyter) -* Plots & Makie recipes -* RGB and other composites -* tests -* histogram equaization - -* FileIO Registration. -* fits.gz support -* Table wrapper for TableHDUs that preserves comment access, units. -* Reading/writing subbarrays -* Specifying what kind of table, ASCII or TableHDU when wriring. - -* FITSIO PR/issue (performance) -* PlotUtils PR/issue (zscale with iteratble) - -=# diff --git a/src/showmime.jl b/src/showmime.jl index e485e083..651a6019 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -1,11 +1,3 @@ -# _brightness_contrast(color, matrix::AbstractMatrix{T}, brightness, contrast) where {T} = -# @. color(matrix / 255 * T(contrast) + T(brightness) / 255) - -# """ -# brightness_contrast(image::AstroImageMat; brightness_range = 0:255, contrast_range = 1:1000, header_number = 1) - -# Visualize the fits image by changing the brightness and contrast of image. - # This is used in VSCode and others @@ -18,42 +10,10 @@ Base.show(io::IO, mime::MIME"image/png", img::AstroImageMat{T}; kwargs...) where Base.show(io::IO, mime::MIME"image/png", img::AstroImageMat{T}; kwargs...) where {T<:Union{Number,Missing}} = show(io, mime, imview(img), kwargs...) -# # Special handling for complex images -# function Base.show(io::IO, mime::MIME"image/png", img::AstroImageMat{T}; kwargs...) where {T<:Complex} -# # Not sure we really want to support this functionality, but we will allow it for -# # now with a warning. -# @warn "Displaying complex image as magnitude and phase (maxlog=1)" maxlog=1 -# mag_view = imview(abs.(img)) -# angle_view = imview(angle.(img), clims=(-pi, pi), cmap=:cyclic_mygbm_30_95_c78_n256_s25) -# show(io, mime, vcat(mag_view,angle_view), kwargs...) -# end - -# const _autoshow = Base.RefValue{Bool}(true) -# """ -# set_autoshow!(autoshow::Bool) - -# By default, `display`ing a 2D AstroImage e.g. at the REPL or in a notebook -# shows it as a PNG image using the `imview` function and user's default -# colormap, stretch, etc. -# If set to false, displaying an image will just show a textual representation. -# You can still visualize images using `imview`. -# """ -# function set_autoshow!(autoshow::Bool) -# _autoshow[] = autoshow -# end -# TODO: for this to work, we need to actually add and remove a show method. TBD how. -# TODO: ensure this still works and is backwards compatible +# Deprecated # Lazily reinterpret the AstroImageMat as a Matrix{Color}, upon request. # By itself, Images.colorview works fine on AstroImages. But # AstroImages are not normalized to be between [0,1]. So we override # colorview to first normalize the data using scaleminmax -function render(img::AstroImageMat{T,N}) where {T,N} - # imgmin, imgmax = img.minmax - imgmin, imgmax = extrema(img) - # Add one to maximum to work around this issue: - # https://github.com/JuliaMath/FixedPointNumbers.jl/issues/102 - f = scaleminmax(_float(imgmin), _float(max(imgmax, imgmax + oneunit(T)))) - return colorview(Gray, f.(_float.(img.data))) -end -ImageCore.colorview(img::AstroImageMat) = render(img) +@deprecate render(img::AstroImageMat) imview(img, clims=extrema, cmap=nothing) From a967da63cf729d75d379705e0ecf75e91c121c67 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 3 Jun 2022 08:06:16 -0700 Subject: [PATCH 163/178] Update README --- README.md | 108 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 72 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 8f6a00c6..2f52584b 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,14 @@ # AstroImages.jl -| **Build Status** | **Code Coverage** | -|:-----------------------------------------:|:-------------------------------:| -| [![Build Status][travis-img]][travis-url] | [![][coveral-img]][coveral-url] | -| [![Build Status][appvey-img]][appvey-url] | [![][codecov-img]][codecov-url] | +| **Documentation** | **Build Status** | **Code Coverage** | +|:------------------|:-----------------------------------------:|:-------------------------------:| +| [![](https://img.shields.io/badge/docs-dev-blue.svg)](https://juliaastro.github.io/AstroImages.jl/dev/) | ![Build status](https://github.com/JuliaAstro/AstroImages/actions/workflows/ci.yml/badge.svg) | [![][codecov-img]][codecov-url] | Introduction ------------ -`AstroImages.jl` allows you to plot images from an -astronomical [`FITS`](https://en.wikipedia.org/wiki/FITS) file using the +`AstroImages.jl` allows you load and visualize images from a +astronomical [`FITS`](https://en.wikipedia.org/wiki/FITS) files using the popular [`Images.jl`](https://github.com/JuliaImages/Images.jl) and [`Plots.jl`](https://github.com/JuliaPlots/Plots.jl) Julia packages. `AstroImages.jl` uses [`FITSIO.jl`](https://github.com/JuliaAstro/FITSIO.jl) to @@ -26,6 +25,8 @@ manager](https://docs.julialang.org/en/v1/stdlib/Pkg/). pkg> add AstroImages ``` +You may also need to install `ImageIO.jl` for images to display in certain environments. + Usage ----- @@ -35,6 +36,10 @@ After installing the package, you can start using it with julia> using AstroImages ``` +Images will automatically display in many environments, including VS Code, Jupyter, and Pluto. +If you're using a REPL, you may want to install an external viewer like ImageShow.jl, ElectronDisplay.jl, +or ImageInTerminal.jl. + ## Reading extensions from FITS file You can load and read the the first extension of a FITS file with the `load` @@ -55,52 +60,91 @@ julia> load("file.fits", 3) [...] ``` -## AstroImageMat type +If unspecified, the first image HDU will be loaded. + +## AstroImage type -The package provides a new type, `AstroImageMat` to integrate FITS images with -Julia packages for plotting and image processing. The `AstroImageMat` function has +The package provides a new type, `AstroImage` to integrate FITS images with +Julia packages for plotting and image processing. The `AstroImage` function has the same syntax as `load`. This command: ```julia -julia> img = AstroImageMat("file.fits") -AstroImages.AstroImageMat{UInt16,ColorTypes.Gray,1,Float64}[...] +julia> img = AstroImage("file.fits") ``` -will read the first valid extension from the `file.fits` file and wrap its content in -a `NTuple{N, Matrix{Gray}}`, that can be easily used with `Images.jl` and related packages. +will read the first valid extension from the `file.fits` file. -If you are working in a Jupyter notebook, an `AstroImageMat` object is +If you are working in a Jupyter notebook, an `AstroImage` object is automatically rendered as a PNG image. -`AstroImageMat` automatically extracts and store `wcs` information of images in a `NTuple{N, WCSTransform}`. +`AstroImage` automatically extracts and store `wcs` information of images in a `NTuple{N, WCSTransform}`. + +## Headers +FITS Headers can be accessed directly from an AstroImage: +```julia +julia> img["HEAD1"] = 1.0 +julia> img["HEAD1",Comment] = "A comment describes the meaning of a header keyword" +julia> img["HEAD1"] +1.0 + +julia> push!(img, History, "We can record the history of processes applied to this image in header HISTORY entries.") +``` + +## Visualization -## Forming RGB image -`AstroImageMat` can automatically construct a RGB image if 3 different colour band data is given. +Any AbstractArray (including an AstroImage) can be displayed using `imview`. This function renders an +arbitrary array into an array of `RGBA` values using a number of parameters. If the input is an AstroImage{<:Number}, +an AstroImage{RGBA} will be returned that retains headers, WCS information, etc. ```julia -julia> img = AstroImageMat(RGB, ("file1.fits","file2.fits", "file3.fits")) +julia> imview(img; clims=Percent(99.5), cmap=:magma, stretch=identity, contrast=1.0, bias=0.5) ``` -Where 1st index of `file1.fits`, `file2.fits`, `file3.fits` contains band data of red, blue and green channels respectively. -Optionally, `ccd2rgb` method can be used to form a coloured image from 3 bands without creating an `AstroImageMat`. +Very large Images are automatically downscaled to ensure consistent performance using `restrict` from Images.jl. This function filters the data before downscaling to prevent aliasing, so it may take a moment for truly huge images. In these cases, a faster method that doesn't prevent aliasing would be `imview(img[begin:10:end, begin:10:end])` or similar. + +`imview` is called automatically on `AstroImage{<:Number}` when using a Julia environment with rich graphical IO capabilities (e.g. VSCode, Jupyter, Pluto, etc.). +The defaults for this case can be modified using `AstroImages.set_clims!(...)`, `AstroImages.set_cmap!(...)`, and `AstroImages.set_stretch!(...)`. + +## Forming Color Composite Images -The formed image can be accessed using `img.property.rgb_image`. -`set_brightness!` and `set_contrast!` methods can be used to change brightness and contrast of formed `rgb_image`. -`add_label!` method can be used to add/store Astronomical labels in an `AstroImageMat`. -`reset!` method resets `brightness`, `contrast` and `label` fields to defaults and construct a fresh `rgb_image` without any brightness, contrast operations. +A color composite image (e.g. RGB) can be constructed using the `composecolors` function. +```julia +julia> rgb = composecolors([img1, img2, img3]) +``` +Where `img1`, `img2`, `img3` are arrays or AstroImages containing data of red, blue and green channels respectively. + +`composecolors` also supports more complex mappings, for example merging two bands according to color schemes from +ColorSchemes.jl. +See the docs for more information. -## Plotting an AstroImageMat +## Plotting an AstroImage -An `AstroImageMat` object can be plotted with `Plots.jl` package. Just use +An `AstroImage` object can be plotted with `Plots.jl` package. Just use ```julia julia> using Plots -julia> plot(img) +julia> implot(img) ``` -and the image will be displayed as a heatmap using your favorite backend. +and the image will be displayed as an image series using your favorite backend. +Plotly, PyPlot, and GR backends have been tested. + +`implot` supports all the same syntax as `imview` in addition to keyword arguments +for controlling axis tick marks, WCS grid lines, and the colorbar. + +## Resolving World Coordinates +If your FITS file contains world coordinate system headers, AstroImages.jl can use WCS.jl to convert between pixel and world coordinates. +This works even if you have sliced or your image to select a region of interest: + +```julia +julia> img_slice = img[100:200,100:200] +julia> coords_world = pix_to_world(img_slice, [5,5]) +[..., ...] +julia> world_to_pix(img_slice, coords_world) +[5.0,5.0] # approximately +``` License ------- @@ -108,14 +152,6 @@ License The `AstroImages.jl` package is licensed under the MIT "Expat" License. The original author is Mosè Giordano. -[travis-img]: https://travis-ci.org/JuliaAstro/AstroImages.jl.svg?branch=master -[travis-url]: https://travis-ci.org/JuliaAstro/AstroImages.jl - -[appvey-img]: https://ci.appveyor.com/api/projects/status/7gaxwe0c8hjx3d1s?svg=true -[appvey-url]: https://ci.appveyor.com/project/giordano/astroimages-jl - -[coveral-img]: https://coveralls.io/repos/JuliaAstro/AstroImages.jl/badge.svg?branch=master&service=github -[coveral-url]: https://coveralls.io/github/JuliaAstro/AstroImages.jl?branch=master [codecov-img]: http://codecov.io/github/JuliaAstro/AstroImages.jl/coverage.svg?branch=master [codecov-url]: http://codecov.io/github/JuliaAstro/AstroImages.jl?branch=master From e9dfae087f30179e2629f3ff5ebedfcb0ef97f25 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 3 Jun 2022 08:06:26 -0700 Subject: [PATCH 164/178] Project.toml cleanup --- Project.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Project.toml b/Project.toml index af841203..672fc6ae 100644 --- a/Project.toml +++ b/Project.toml @@ -8,6 +8,7 @@ AbstractFFTs = "621f4979-c628-5d54-868e-fcf4e3e8185c" AstroAngles = "5c4adb95-c1fc-4c53-b4ea-2a94080c53d2" ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" DimensionalData = "0703355e-b756-11e9-17c0-8b28908087d0" +ElectronDisplay = "d872a56f-244b-5cc9-b574-2017b5b909a8" FITSIO = "525bcba6-941b-5504-bd06-fd0dc1a4d2eb" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" ImageAxes = "2803e5a7-5153-5ecf-9a86-9b4c37f5f5ac" @@ -21,9 +22,7 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" WCS = "15f3aee2-9e10-537f-b834-a6fb8bdb944d" - [compat] -julia = "^1.6.0" AbstractFFTs = "1.1" AstroAngles = "0.1" ColorSchemes = "3.18" @@ -38,12 +37,13 @@ PlotUtils = "1.2" RecipesBase = "1.2" Tables = "1.7" WCS = "0.6" +julia = "^1.6.0" [extras] +ImageBase = "c817782e-172a-44cc-b673-b171935fbb9e" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" -ImageBase = "c817782e-172a-44cc-b673-b171935fbb9e" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test", "WCS", "FITSIO", "Random", "Statistics", "ImageBase"] +test = ["Test", "WCS", "FITSIO", "Random", "Statistics", "ImageBase"] From 990c782c02bee6791b11e02e85fc5d6f5602310d Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 3 Jun 2022 08:38:24 -0700 Subject: [PATCH 165/178] Drop `arraydata` in favour of Base.parent for consistency with DimensionalData --- src/AstroImages.jl | 15 +++------------ src/contrib/abstract-ffts.jl | 4 ++-- src/contrib/images.jl | 12 ++++++++++-- src/io.jl | 2 +- src/precompile.jl | 2 +- src/showmime.jl | 2 +- 6 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 8955325e..363c5cbe 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -38,7 +38,6 @@ export load, powerdiststretch, imview, render, # deprecated - arraydata, header, copyheader, shareheader, @@ -157,16 +156,8 @@ end wcs(arr::AbstractArray) = [emptywcs(arr)] wcs(img, ind) = wcs(img)[ind] -""" - ImageMetadata.arraydata(img::AstroImage) - -Returns the underlying wrapped array of `img`. -""" -ImageMetadata.arraydata(img::AstroImage) = getfield(img, :data) - - # Implement DimensionalData interface -Base.parent(img::AstroImage) = arraydata(img) +Base.parent(img::AstroImage) = getfield(img, :data) DimensionalData.dims(A::AstroImage) = getfield(A, :dims) DimensionalData.refdims(A::AstroImage) = getfield(A, :refdims) DimensionalData.data(A::AstroImage) = getfield(A, :data) @@ -228,7 +219,7 @@ for f in [ :(Base.view) ] # TODO: these functions are copying headers - @eval ($f)(img::AstroImage) = shareheader(img, $f(arraydata(img))) + @eval ($f)(img::AstroImage) = shareheader(img, $f(parent(img))) end @@ -441,7 +432,7 @@ Base.copy(img::AstroImage) = rebuild(img, copy(parent(img))) Base.convert(::Type{AstroImage}, A::AstroImage) = A Base.convert(::Type{AstroImage}, A::AbstractArray) = AstroImage(A) Base.convert(::Type{AstroImage{T}}, A::AstroImage{T}) where {T} = A -Base.convert(::Type{AstroImage{T}}, A::AstroImage) where {T} = shareheader(A, convert(AbstractArray{T}, arraydata(A))) +Base.convert(::Type{AstroImage{T}}, A::AstroImage) where {T} = shareheader(A, convert(AbstractArray{T}, parent(A))) Base.convert(::Type{AstroImage{T}}, A::AbstractArray{T}) where {T} = AstroImage(A) Base.convert(::Type{AstroImage{T}}, A::AbstractArray) where {T} = AstroImage(convert(AbstractArray{T}, A)) Base.convert(::Type{AstroImage{T,N,D,R,AT}}, A::AbstractArray{T,N}) where {T,N,D,R,AT} = AstroImage(convert(AbstractArray{T}, A)) diff --git a/src/contrib/abstract-ffts.jl b/src/contrib/abstract-ffts.jl index fd909b71..51118018 100644 --- a/src/contrib/abstract-ffts.jl +++ b/src/contrib/abstract-ffts.jl @@ -10,14 +10,14 @@ for f in [ :(AbstractFFTs.rfft), ] # TODO: should we try to alter the image headers to change the units? - @eval ($f)(img::AstroImage, args...; kwargs...) = copyheader(img, $f(arraydata(img), args...; kwargs...)) + @eval ($f)(img::AstroImage, args...; kwargs...) = copyheader(img, $f(parent(img), args...; kwargs...)) end for f in [ :(AbstractFFTs.fftshift), ] # TODO: should we try to alter the image headers to change the units? - @eval ($f)(img::AstroImage, args...; kwargs...) = shareheader(img, $f(arraydata(img), args...; kwargs...)) + @eval ($f)(img::AstroImage, args...; kwargs...) = shareheader(img, $f(parent(img), args...; kwargs...)) end diff --git a/src/contrib/images.jl b/src/contrib/images.jl index d1eb75b5..e66e9c76 100644 --- a/src/contrib/images.jl +++ b/src/contrib/images.jl @@ -1,5 +1,5 @@ # function warp(img::AstroImageMat, args...; kwargs...) -# out = warp(arraydatat(img), args...; kwargs...) +# out = warp(parentt(img), args...; kwargs...) # return copyheaders(img, out) # end @@ -51,7 +51,7 @@ end # TODO: correct dimensions after restrict. ImageBase.restrict(img::AstroImage, ::Tuple{}) = img function ImageBase.restrict(img::AstroImage, region::Dims) - restricted = restrict(arraydata(img), region) + restricted = restrict(parent(img), region) steps = cld.(size(img), size(restricted)) newdims = Tuple(d[begin:s:end] for (d,s) in zip(dims(img),steps)) return rebuild(img, restricted, newdims) @@ -73,3 +73,11 @@ ImageBase.pixelspacing(img::AstroImage) = step.(dims(img)) + +# """ +# ImageMetadata.parent(img::AstroImage) + +# Returns the underlying wrapped array of `img`. +# """ +# ImageMetadata.parent(img::AstroImage) = getfield(img, :data) + diff --git a/src/io.jl b/src/io.jl index bfeb5373..651a16c6 100644 --- a/src/io.jl +++ b/src/io.jl @@ -155,7 +155,7 @@ function writefits(fname, args...) end end end -writearg(fits, img::AstroImage) = write(fits, arraydata(img), header=header(img)) +writearg(fits, img::AstroImage) = write(fits, parent(img), header=header(img)) # Fallback for writing plain arrays writearg(fits, arr::AbstractArray) = write(fits, arr) # For table compatible data. diff --git a/src/precompile.jl b/src/precompile.jl index 66d542ac..320cec1f 100644 --- a/src/precompile.jl +++ b/src/precompile.jl @@ -22,7 +22,7 @@ for T in [Float32, Float64, Int, Int8, UInt8, N0f8] imview(i; stretch) end TI = typeof(i) - precompile(arraydata, (TI,)) + precompile(parent, (TI,)) precompile(header, (TI,)) precompile(wcs, (TI,)) precompile(getindex, (TI, Symbol)) diff --git a/src/showmime.jl b/src/showmime.jl index 651a6019..f77aecee 100644 --- a/src/showmime.jl +++ b/src/showmime.jl @@ -4,7 +4,7 @@ # If the user displays a AstroImageMat of colors (e.g. one created with imview) # fal through and display the data as an image Base.show(io::IO, mime::MIME"image/png", img::AstroImageMat{T}; kwargs...) where {T<:Colorant} = - show(io, mime, arraydata(img), kwargs...) + show(io, mime, parent(img), kwargs...) # Otherwise, call imview with the default settings. Base.show(io::IO, mime::MIME"image/png", img::AstroImageMat{T}; kwargs...) where {T<:Union{Number,Missing}} = From 8d25e20d6a25b3fe891ef2e50131a200ab726e9a Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 3 Jun 2022 08:38:53 -0700 Subject: [PATCH 166/178] Need to keep UUIDs until registered with FileIO --- Project.toml | 3 +++ src/AstroImages.jl | 11 ++--------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/Project.toml b/Project.toml index 672fc6ae..53cd5801 100644 --- a/Project.toml +++ b/Project.toml @@ -20,6 +20,7 @@ Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" +UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" WCS = "15f3aee2-9e10-537f-b834-a6fb8bdb944d" [compat] @@ -44,6 +45,8 @@ ImageBase = "c817782e-172a-44cc-b673-b171935fbb9e" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +WCS = "15f3aee2-9e10-537f-b834-a6fb8bdb944d" +FITSIO = "525bcba6-941b-5504-bd06-fd0dc1a4d2eb" [targets] test = ["Test", "WCS", "FITSIO", "Random", "Statistics", "ImageBase"] diff --git a/src/AstroImages.jl b/src/AstroImages.jl index 363c5cbe..a9679ce3 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -5,7 +5,7 @@ using FileIO # Rather than pulling in all of Images.jl, just grab the packages # we need to extend to our basic functionality. # We also need ImageShow so that user's images appear automatically. -using ImageBase, ImageShow, ImageMetadata, ImageAxes +using ImageBase, ImageShow#, ImageAxes using WCS using Statistics @@ -18,6 +18,7 @@ using AstroAngles using Printf using PlotUtils: PlotUtils using PlotUtils: optimize_ticks, AbstractColorList +using UUIDs # can remove once reigstered with FileIO export load, @@ -511,14 +512,6 @@ function __init__() [:FITSIO => UUID("525bcba6-941b-5504-bd06-fd0dc1a4d2eb")], [:AstroImages => UUID("fe3fc30c-9b16-11e9-1c73-17dabf39f4ad")] ) - # TODO: How to add FileIO support for fits.gz files? We can open these - # with AstroImage("...fits.gz") but not load, since the .gz takes precedence. - # add_format(format"FITS.GZ", - # [0x1f, 0x8b, 0x08], - # [".fits.gz", ".fts.gz", ".FIT.gz", ".FITS.gz", ".FTS.gz"], - # [:FITSIO => UUID("525bcba6-941b-5504-bd06-fd0dc1a4d2eb")], - # [:AstroImages => UUID("fe3fc30c-9b16-11e9-1c73-17dabf39f4ad")] - # ) end From e0fb31ef70afc93538e0cb9fe7564e0cfca5ffec Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 3 Jun 2022 08:39:03 -0700 Subject: [PATCH 167/178] Turn on deploy --- docs/make.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 1c1c35d4..eab90965 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -36,7 +36,7 @@ makedocs( # 3. postprocess after makedocs postprocess_cb() -# deploydocs( -# repo = "github.com/sefffal/AstroImages.jl.git", -# devbranch = "main" -# ) +deploydocs( + repo = "github.com/sefffal/AstroImages.jl.git", + devbranch = "master" +) From ac1771fb4f46929607680fa9c304ed4e0ff04415 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 3 Jun 2022 08:40:26 -0700 Subject: [PATCH 168/178] Drop accidental electron display dep --- Project.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 53cd5801..ff152f79 100644 --- a/Project.toml +++ b/Project.toml @@ -8,7 +8,6 @@ AbstractFFTs = "621f4979-c628-5d54-868e-fcf4e3e8185c" AstroAngles = "5c4adb95-c1fc-4c53-b4ea-2a94080c53d2" ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" DimensionalData = "0703355e-b756-11e9-17c0-8b28908087d0" -ElectronDisplay = "d872a56f-244b-5cc9-b574-2017b5b909a8" FITSIO = "525bcba6-941b-5504-bd06-fd0dc1a4d2eb" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" ImageAxes = "2803e5a7-5153-5ecf-9a86-9b4c37f5f5ac" @@ -41,12 +40,12 @@ WCS = "0.6" julia = "^1.6.0" [extras] +FITSIO = "525bcba6-941b-5504-bd06-fd0dc1a4d2eb" ImageBase = "c817782e-172a-44cc-b673-b171935fbb9e" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" WCS = "15f3aee2-9e10-537f-b834-a6fb8bdb944d" -FITSIO = "525bcba6-941b-5504-bd06-fd0dc1a4d2eb" [targets] test = ["Test", "WCS", "FITSIO", "Random", "Statistics", "ImageBase"] From dd6e3492b0118898a8a62bd846b141bdea41362d Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 3 Jun 2022 08:50:28 -0700 Subject: [PATCH 169/178] Turn on docs push_preview --- docs/make.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/make.jl b/docs/make.jl index eab90965..8e87ed8c 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -38,5 +38,6 @@ postprocess_cb() deploydocs( repo = "github.com/sefffal/AstroImages.jl.git", - devbranch = "master" + devbranch = "master", + push_preview = true ) From 2d524f375ffefb53fec695f2988f5ec3e8afdf5b Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 3 Jun 2022 08:50:43 -0700 Subject: [PATCH 170/178] Silence printlng from runtests.jl --- test/runtests.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index a81d1cb8..43c9bd31 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -260,8 +260,6 @@ end img_rendered_5 = imview(arr1, clims=(1,9), stretch=sqrtstretch, contrast=0.5, bias=0.5, cmap=nothing) - @show mean(diff(sort(Gray.(vec(img_rendered_1))))) ≈ 2mean(diff(sort(Gray.(vec(img_rendered_5))))) - # Missing/NaN for m in (NaN, missing) arr2 = [ From e2e20812a7eccb04fcf7344c60d71001f9deb2b8 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 3 Jun 2022 10:45:24 -0700 Subject: [PATCH 171/178] Add doc strings --- docs/src/api.md | 27 ++++++++--------------- src/AstroImages.jl | 40 +++++++++++++++++++++++++++++++++- src/imview.jl | 54 ++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 98 insertions(+), 23 deletions(-) diff --git a/docs/src/api.md b/docs/src/api.md index 37dd6741..95d9c0ad 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -5,17 +5,14 @@ load save AstroImage -AstroImageVec -AstroImageMat -arraydata -header +imview +implot +dims +refdims Comment History -wcs pix_to_world -pix_to_world! world_to_pix -world!_to_pix X Y Z @@ -24,15 +21,12 @@ At Near Between .. -dims -refdims -clampednormedview +header +wcs WCSGrid -ccd2rgb -composechannels -reset! -zscale -percent +composecolors +Zscale +Percent logstretch powstretch sqrtstretch @@ -40,9 +34,6 @@ squarestretch asinhstretch sinhstretch powerdiststretch -imview -implot copyheader shareheader -ImageCore.normedview ``` \ No newline at end of file diff --git a/src/AstroImages.jl b/src/AstroImages.jl index a9679ce3..a30900b3 100644 --- a/src/AstroImages.jl +++ b/src/AstroImages.jl @@ -144,8 +144,30 @@ function wcsax(img::AstroImage, dim) end # Accessors +""" + header(img::AstroImage) + +Return the underlying FITSIO.FITSHeader object wrapped by an AstroImage. +Note that this object has less flexible getindex and setindex methods. +Indexing by symbol, Comment, History, etc are not supported. +""" header(img::AstroImage) = getfield(img, :header) +""" + header(array::AbstractArray) + +Returns an empty FITSIO.FITSHeader object when called with a non-AstroImage +abstract array. +""" header(::AbstractArray) = emptyheader() + +""" + wcs(img) + +Computes and returns a list of World Coordinate System WCSTransform objects from WCS.jl. +The resultss are cached after the first call, so subsequent calls are fast. +Modifying a WCS header invalidates this cache automatically, so users should call `wcs(...)` +each time rather than keeping the WCSTransform object around. +""" function wcs(img::AstroImage) if getfield(img, :wcs_stale)[] empty!(getfield(img, :wcs)) @@ -154,8 +176,24 @@ function wcs(img::AstroImage) end return getfield(img, :wcs) end -wcs(arr::AbstractArray) = [emptywcs(arr)] +""" + wcs(img, index) + +Computes and returns a World Coordinate System WCSTransform objects from WCS.jl by index. +This is to support files with multiple WCS transforms specified. +`wcs(img,1)` is useful for selecting selecting the first WCSTranform object. +The resultss are cached after the first call, so subsequent calls are fast. +Modifying a WCS header invalidates this cache automatically, so users should call `wcs(...)` +each time rather than keeping the WCSTransform object around. +""" wcs(img, ind) = wcs(img)[ind] +""" +wcs(array) + +Returns a list with a single basic WCSTransform object when called with a non-AstroImage +abstract array. +""" +wcs(arr::AbstractArray) = [emptywcs(arr)] # Implement DimensionalData interface Base.parent(img::AstroImage) = getfield(img, :data) diff --git a/src/imview.jl b/src/imview.jl index 28caf370..77153b17 100644 --- a/src/imview.jl +++ b/src/imview.jl @@ -1,22 +1,59 @@ # These reproduce the behaviour of DS9 according to http://ds9.si.edu/doc/ref/how.html + +""" + logstretch(num,a=1000) + +A log-stretch as defined by the SAO DS9 application: http://ds9.si.edu/doc/ref/how.html +""" logstretch(x,a=1000) = log(a*x+1)/log(a) +""" + powstretch(num, a=1000) + +A power-stretch as defined by the SAO DS9 application: http://ds9.si.edu/doc/ref/how.html +""" powstretch(x,a=1000) = (a^x - 1)/a -sqrtstretch = sqrt +""" + sqrtstretch(num) + +A square root stretch (simply defined as Base.sqrt) +""" +sqrtstretch(x) = sqrt(x) +""" + squarestretch(num) + +A squarestretch-stretch as defined by the SAO DS9 application: http://ds9.si.edu/doc/ref/how.html +""" squarestretch(x) = x^2 +""" + asinhstretch(num) + +A hyperbolic arcsin stretch as defined by the SAO DS9 application: http://ds9.si.edu/doc/ref/how.html. +""" asinhstretch(x) = asinh(10x)/3 +""" + sinhstretch(num) + +A hyperbolic sin stretch as defined by the SAO DS9 application: http://ds9.si.edu/doc/ref/how.html +""" sinhstretch(x) = sinh(3x)/10 # These additional stretches reproduce behaviour from astropy +""" + logstretch(num,a=1000) + +A power distance stretch as defined by astropy. +""" powerdiststretch(x, a=1000) = (a^x - 1) / (a - 1) """ - percent(99.5) + Percent(99.5) Returns a callable that calculates display limits that include the given percent of the image data. +Reproduces the behaviour of the SAO DS9 scale menu. Example: ```julia -julia> imview(img, clims=percent(90)) +julia> imview(img, clims=Percent(90)) ``` This will set the limits to be the 5th percentile to the 95th percentile. """ @@ -33,7 +70,16 @@ Base.show(io::IO, p::Percent; kwargs...) = print(io, "Percent($(p.perc))", kwarg """ Zscale(options)(data) -Wraps PlotUtils.zscale to first collect iterators. +Wraps PlotUtils.zscale in a callable with default parameters. +This is a common algorithm for agressively stretching astronomical data +to see faint structure that originated in IRAF: https://iraf.net/forum/viewtopic.php?showtopic=134139 +but is now seen in many other applications/libraries (DS9, Astropy, etc.) + +Usage: +``` +imview(img, clims=Zscale()) +implot(img, clims=Zscale(contrast=0.1)) +``` Default parameters: ``` From d3ae34f3c6eb44a4e840286f9c0275b2d100e26d Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 3 Jun 2022 14:12:55 -0700 Subject: [PATCH 172/178] Docs WIP --- docs/make.jl | 14 ++- docs/src/api.md | 1 - docs/src/guide/image-filtering.md | 0 docs/src/guide/image-transformations.md | 0 docs/src/guide/photometry.md | 0 docs/src/guide/reproject.md | 0 docs/src/manual/conventions.md | 21 ++++ .../dimensions-and-world-coordinates.md | 36 ++++++ docs/src/manual/displaying-images.md | 0 docs/src/{ => manual}/getting-started.md | 0 docs/src/manual/loading-images.md | 106 ++++++++++++++++++ docs/src/manual/polarization.md | 0 docs/src/manual/spec.md | 0 13 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 docs/src/guide/image-filtering.md create mode 100644 docs/src/guide/image-transformations.md create mode 100644 docs/src/guide/photometry.md create mode 100644 docs/src/guide/reproject.md create mode 100644 docs/src/manual/conventions.md create mode 100644 docs/src/manual/dimensions-and-world-coordinates.md create mode 100644 docs/src/manual/displaying-images.md rename docs/src/{ => manual}/getting-started.md (100%) create mode 100644 docs/src/manual/loading-images.md create mode 100644 docs/src/manual/polarization.md create mode 100644 docs/src/manual/spec.md diff --git a/docs/make.jl b/docs/make.jl index 8e87ed8c..f8933ece 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -22,7 +22,19 @@ makedocs( pages = [ "Home" => "index.md", "Manual" => [ - "Getting Started" => "getting-started.md" + "Getting Started" => "manual/getting-started.md", + "Loading & Saving Images" => "manual/loading-images.md", + "Displaying Images" => "manual/displaying-images.md", + "Dimensions and World Coordinates" => "manual/dimensions-and-world-coordinates.md", + "Polarization" => "manual/polarization.md", + "Spectral Axes" => "manual/spec.md", + "Conventions" => "manual/conventions.md", + ], + "Guides" => [ + "Extracting Photometry" => "guide/photometry.md", + "Reprojecting Images" => "guide/reproject.md", + "Blurring & Filtering Images" => "guide/image-filtering.md", + "Transforming Images" => "guide/image-transformations.md", ], demopage, "API" => "api.md", diff --git a/docs/src/api.md b/docs/src/api.md index 95d9c0ad..f364756c 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -19,7 +19,6 @@ Z Dim At Near -Between .. header wcs diff --git a/docs/src/guide/image-filtering.md b/docs/src/guide/image-filtering.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/guide/image-transformations.md b/docs/src/guide/image-transformations.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/guide/photometry.md b/docs/src/guide/photometry.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/guide/reproject.md b/docs/src/guide/reproject.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/manual/conventions.md b/docs/src/manual/conventions.md new file mode 100644 index 00000000..ec68aa40 --- /dev/null +++ b/docs/src/manual/conventions.md @@ -0,0 +1,21 @@ + +# Conventions + +In the Julia Astro ecosystem, images follow the following conventions. + +## Axes +For simple 2D images, the first axis is the horizontal axis and the second axis is the vertical axis. +So images are indexed by `img[xi, yi]`. + +The origin is at the bottom left of the image, so `img[1,1]` refers to the bottom left corner +as does `img[begin,begin]`. +`img[end,end]` is the top right corner, `img[begin,end]` is the top left, etc. + + +## Pixel Indices specify the Centers of Pixels +The exact location of `img[1,1]` is the center of the pixel in the bottom left corner. +This means that plot limits should have the `1` tick slightly away from the left/bottom spines of the image. +The default plot limits for `implot` are `-0.5` to `end+0.5` along both axes. + +There is a known bug with the Plots.jl GR backend that leads ticks to be slightly offset. PyPlot and Plotly backends +show the correct tick locations. \ No newline at end of file diff --git a/docs/src/manual/dimensions-and-world-coordinates.md b/docs/src/manual/dimensions-and-world-coordinates.md new file mode 100644 index 00000000..94c765da --- /dev/null +++ b/docs/src/manual/dimensions-and-world-coordinates.md @@ -0,0 +1,36 @@ +# Dimensions and World Coordinates + +AstroImages are based on [Dimensional Data](https://github.com/rafaqz/DimensionalData.jl). Each axis is assigned a dimension name +and the indices are tracked. +The automatic dimension names are `X`, `Y`, `Z`, `Dim{4}`, `Dim{5}`, and so on; however you can pass in other names or orders to the load function and/or AstroImage contructor: + +```julia +julia> img = load("img.fits",1,(Y=1:1600,Z=1:1600)) +1600×1600 AstroImage{Float32,2} with dimensions: + Y Sampled 1:1600 ForwardOrdered Regular Points, + Z Sampled 1:1600 ForwardOrdered Regular Points +``` +Other useful dimension names are `Spec` for spectral axes, `Pol` for polarization data, and `Ti` for time axes. + +These will be further discussed in Dimensions and World Coordinates. + +For certain applications, it may be useful to use offset axes or axes with different steps. +```julia +julia> img = load("img.fits",1,(X=801:2400,Y=1:2:3200)) +1600×1600 AstroImage{Float32,2} with dimensions: + X Sampled 801:2400 ForwardOrdered Regular Points, + Y Sampled 1:2:3199 ForwardOrdered Regular Points +... +``` + +Unlike OffsetArrays, the usual indexing remains so `img[1,1]` is still the bottom left of the image; +however, data can be looked up according to the offset axes when using specifiers: +```julia +julia> img[X=Near(2000),Y=1..100] +50-element AstroImage{Float32,1} with dimensions: + Y Sampled 1:2:99 ForwardOrdered Regular Points +and reference dimensions: + X Sampled 2000:2000 ForwardOrdered Regular Points + 0.0 +``` + diff --git a/docs/src/manual/displaying-images.md b/docs/src/manual/displaying-images.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/getting-started.md b/docs/src/manual/getting-started.md similarity index 100% rename from docs/src/getting-started.md rename to docs/src/manual/getting-started.md diff --git a/docs/src/manual/loading-images.md b/docs/src/manual/loading-images.md new file mode 100644 index 00000000..5c563f3e --- /dev/null +++ b/docs/src/manual/loading-images.md @@ -0,0 +1,106 @@ +# Loading Images + +FITS (Flexible Image Transport System) files can be loaded and saved using AstroImages thanks to the FITSIO package. + +AstroImages is registered with [FileIO](https://juliaio.github.io/FileIO.jl/stable/), so if you have FileIO and AstroImages +installed you can get started with the `load` function. When you pass a file name with the appropriate file extension (".fits", ".fit", etc.) +FileIO will import AstroImages automatically. + +Alternatively, you can use the `AstroImage` contructor instead of load. This will work on fits files with any file extension, including compressed +files (e.g. ".fits.gz"). + +```julia +julia> img = load("myfitsimg.fits") +1600×1600 AstroImage{Float32,2} with dimensions: + X Sampled Base.OneTo(1600) ForwardOrdered Regular Points, + Y Sampled Base.OneTo(1600) ForwardOrdered Regular Points + 0.0 0.0 0.0 0.0 0.0 … 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + ⋮ ⋱ + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +``` + +Note: if you are in an interactive environment like VSCode, Jupyter, or Pluto, instead of a REPL, AstroImages are automatically +rendered to images and displayed. You can see this plain text output by explicitly calling: +`show(stdout, MIME("text/plain"), img)`. + +Or: +```julia + julia> img = AstroImage("myfitsimg.fits.gz") +1600×1600 AstroImage{Float32,2} with dimensions: + X Sampled Base.OneTo(1600) ForwardOrdered Regular Points, + Y Sampled Base.OneTo(1600) ForwardOrdered Regular Points + 0.0 0.0 0.0 0.0 0.0 … 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + ⋮ ⋱ + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +``` + +A FITS file can contain multiple N-dimensional images and tables. +When you call load or AstroImage with a file name and no other arguments, the package will search through the file +and return the first image HDU. That is, it will skip any FITS tables or empty HDUs with only headers. + +You can also specify an HDU number explicitly: +```julia +julia> img = load("myfitsimg.fits",1) +1600×1600 AstroImage{Float32,2} with dimensions: + X Sampled Base.OneTo(1600) ForwardOrdered Regular Points, + Y Sampled Base.OneTo(1600) ForwardOrdered Regular Points +... +``` +This way, you can load specific images from multi-extension files. + +You can load all HDUs simultaneously by passing `:`: + +```julia +julia> hdus = load("multiext.fits", :); +julia> hdus[2] # Second HDU as an AstroImage +10×10 AstroImage{Float64,2} with dimensions: + X Sampled Base.OneTo(10) ForwardOrdered Regular Points, + Y Sampled Base.OneTo(10) ForwardOrdered Regular Points + -0.777315 -1.36683 -0.580179 1.39629 … -2.14298 0.450059 0.432065 + -1.09619 0.789249 0.938415 0.959903 -0.88995 -1.29406 -0.4291 + 0.47427 -1.41855 0.814823 -1.15975 0.0427149 -1.20116 -0.0920709 + -0.179858 -1.60228 1.09648 -0.497927 -1.31824 -0.156529 -0.0223846 + 2.64162 0.131437 0.320476 0.331197 -0.914713 -1.55162 -0.18862 + 0.209669 -1.17923 -0.656512 0.000775311 … 0.377461 -0.24278 0.967202 + 1.01442 -0.762895 -2.13238 -0.456932 -0.415733 -1.21416 -1.6108 + 0.385626 0.389335 -0.00726015 0.309936 -0.533175 0.157878 0.100876 + -1.24799 0.461216 -0.868826 -0.255654 -0.37151 0.49479 -1.87129 + 1.39356 2.29254 0.0548325 1.50674 -0.0880865 0.580978 -1.81629 +julia> # Or: +julia> hdu1, hdu2, hdu3 = load("multiext.fits", :); +``` + +There is also limited support for table HDUs. In this case, a bare-bones Tables.jl compatible +object is returned. + +## Dimension Names +You may have noticed the entries above the image array: +``` +10×10 AstroImage{Float64,2} with dimensions: + X Sampled Base.OneTo(10) ForwardOrdered Regular Points, + Y Sampled Base.OneTo(10) ForwardOrdered Regular Points +``` + +AstroImages are based on [Dimensional Data](https://github.com/rafaqz/DimensionalData.jl). Each axis is assigned a dimension name +and the indices are tracked. +The automatic dimension names are `X`, `Y`, `Z`, `Dim{4}`, `Dim{5}`, and so on; however you can pass in other names or orders to the load function and/or AstroImage contructor: + +```julia +julia> img = load("img.fits",1,(Y=1:1600,Z=1:1600)) +1600×1600 AstroImage{Float32,2} with dimensions: + Y Sampled 1:1600 ForwardOrdered Regular Points, + Z Sampled 1:1600 ForwardOrdered Regular Points +``` +Other useful dimension names are `Spec` for spectral axes, `Pol` for polarization data, and `Ti` for time axes. + +These will be further discussed in Dimensions and World Coordinates. diff --git a/docs/src/manual/polarization.md b/docs/src/manual/polarization.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/manual/spec.md b/docs/src/manual/spec.md new file mode 100644 index 00000000..e69de29b From 30b5938b15271261b8cfe6707e9f665830a2c971 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 3 Jun 2022 15:46:50 -0700 Subject: [PATCH 173/178] WIP docs --- docs/Project.toml | 6 ++ docs/make.jl | 15 +++++ docs/src/guide/photometry.md | 85 ++++++++++++++++++++++++++++ docs/src/index.md | 8 ++- docs/src/manual/displaying-images.md | 79 ++++++++++++++++++++++++++ docs/src/manual/loading-images.md | 27 +++++++++ 6 files changed, 219 insertions(+), 1 deletion(-) diff --git a/docs/Project.toml b/docs/Project.toml index c980d5bb..00f7d4a0 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -2,8 +2,14 @@ AstroImages = "fe3fc30c-9b16-11e9-1c73-17dabf39f4ad" DemoCards = "311a05b2-6137-4a5a-b473-18580a3d38b5" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +ImageFiltering = "6a3955dd-da59-5b1f-98d4-e7296123deb5" ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19" +ImageTransformations = "02fcd773-0e25-5acc-982a-7f6622650795" Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" +Photometry = "af68cb61-81ac-52ed-8703-edc140936be4" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +Reproject = "d1dcc2e6-806e-11e9-2897-3f99785db2ae" +WCS = "15f3aee2-9e10-537f-b834-a6fb8bdb944d" [compat] Documenter = "0.27" diff --git a/docs/make.jl b/docs/make.jl index f8933ece..3a1d875d 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,5 +1,20 @@ using Documenter, DemoCards, AstroImages +# Deps for examples +ENV["GKSwstype"] = "nul" +using Plots, Photometry, ImageTransformations, ImageFiltering, WCS, Reproject + +setup = quote + using AstroImages + using Random + Random.seed!(123456) + + AstroImages.set_clims!(Percent(99.5)) + AstroImages.set_cmap!(:magma) + AstroImages.set_stretch!(identity) +end +DocMeta.setdocmeta!(Photometry, :DocTestSetup, setup; recursive = true) + # 1. generate demo files demopage, postprocess_cb, demo_assets = makedemos("examples") # this is the relative path to docs/ diff --git a/docs/src/guide/photometry.md b/docs/src/guide/photometry.md index e69de29b..dfe52a70 100644 --- a/docs/src/guide/photometry.md +++ b/docs/src/guide/photometry.md @@ -0,0 +1,85 @@ +# Photometry + +The following examples are adapted from [Photometry.jl](https://github.com/JuliaAstro/Photometry.jl/) to show the same examples +combined with AstroImages.jl. +To learn how to measure background levels, perform aperture photometry, etc see the [Photometry.jl documentation](https://juliaastro.github.io/Photometry.jl/dev/). + + +## Background Estimation + +From Photometry.jl: +> Estimating backgrounds is an important step in performing photometry. Ideally, we could perfectly describe the background with a scalar value or with some distribution. Unfortunately, it's impossible for us to precisely separate the background and foreground signals. Here, we use mixture of robust statistical estimators and meshing to let us get the spatially varying background from an astronomical photo. +> Let's show an example +> Now let's try and estimate the background using estimate_background. First, we'll si gma-clip to try and remove the signals from the stars. Then, the background is broken down into boxes, in this case of size (50, 50). Within each box, the given statistical estimators get the background value and RMS. By default, we use SourceExtractorBackground and StdRMS. This creates a low-resolution image, which we then need to resize. We can accomplish this using an interpolator, by default a cubic-spline interpolator via ZoomInterpolator. The end result is a smooth estimate of the spatially varying background and background RMS. + +```@setup phot +using AstroImages +AstroImages.set_clims!(Percent(99.5)) +AstroImages.set_cmap!(:magma) +AstroImages.set_stretch!(identity) +``` + +```@example phot +using Photometry +using AstroImages +using Plots # optional, for implot functionality + +# Download our image, courtesy of astropy +image = AstroImage(download("https://rawcdn.githack.com/astropy/photutils-datasets/8c97b4fa3a6c9e6ea072faeed2d49a20585658ba/data/M6707HH.fits")) + +# sigma-clip +clipped = sigma_clip(image, 1, fill=NaN) + +# get background and background rms with box-size (50, 50) +bkg, bkg_rms = estimate_background(clipped, 50) + +imview(image) +imview(clipped) +imview(bkg) +imview(bkg_rms) +``` + +Or, if you have Plots loaded: +```@example phot +using Plots + + AstroImages.set_clims!(Percent(99.5)) + AstroImages.set_cmap!(:magma) + AstroImages.set_stretch!(identity) +plot( + implot(image, title="Original"), + implot(clipped, title="Sigma-Clipped"), + implot(bkg, title="Background"), + implot(bkg_rms, title="Background RMS"), + layout=(2, 2), + ticks=false +) +``` +![](/assets/manual-photometry-2.png) + + +> We could apply a median filter, too, by specifying filter_size +```@example phot +# get background and background rms with box-size (50, 50) and filter_size (5, 5) +bkg_f, bkg_rms_f = estimate_background(clipped, 50, filter_size=5) + +# plot +plot( + implot(bkg, title="Unfiltered", ylabel="Background"), + implot(bkg_f, title="Filtered"), + implot(bkg_rms, ylabel="RMS"), + implot(bkg_rms_f); + layout=(2, 2),) +``` + +> Now we can see our image after subtracting the filtered background and ready for Aperture Photometry! + +```@example phot +subt = image .- bkg_f[axes(image)...] +clims = extrema(vcat(vec(image), vec(subt))) +plot( + implot(image; title="Original", clims), + implot(subt; title="Subtracted", clims), + size=(900,500) +) +``` \ No newline at end of file diff --git a/docs/src/index.md b/docs/src/index.md index 72cbe334..5a3b6560 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,5 +1,11 @@ # Home -AstroImages.jl is a Julia package for loading, manipulating, visualizing, and saving astronomical images. +[GitHub link](https://github.com/JuliaAstro/AstroImages.jl) +`AstroImage.jl` allows you to plot images from an +astronomical [`FITS`](https://en.wikipedia.org/wiki/FITS) file using the +popular [`Images.jl`](https://github.com/JuliaImages/Images.jl) +and [`Plots.jl`](https://github.com/JuliaPlots/Plots.jl) Julia packages. +`AstroImage.jl` uses [`FITSIO.jl`](https://github.com/JuliaAstro/FITSIO.jl) to +read FITS files. diff --git a/docs/src/manual/displaying-images.md b/docs/src/manual/displaying-images.md index e69de29b..08c1e2f3 100644 --- a/docs/src/manual/displaying-images.md +++ b/docs/src/manual/displaying-images.md @@ -0,0 +1,79 @@ +# Displaying Images + + +Any AbstractArray (including an AstroImage) can be displayed using `imview`. This function renders an +arbitrary array into an array of `RGBA` values using a number of parameters. If the input is an AstroImage{<:Number}, +an AstroImage{RGBA} will be returned that retains headers, WCS information, etc. + +```@setup 1 +using AstroImages +using Plots +``` + +The defaults for the `imview` function are: +```@example 1 +img = randn(50,50); +imview(img; clims=Percent(99.5), cmap=:magma, stretch=identity, contrast=1.0, bias=0.5) +``` + +We can adjust the color limits explicitly: +```@example 1 +imview(img; clims=(-1, 1)) +``` + +Or pass a function/callable object to calculate them for us: +```@example 1 +imview(img; clims=Zscale()) +``` + +We turn off the colormap and use it in grayscale mode: +```@example 1 +imview(img; cmap=nothing) +``` + +Pass any color scheme from ColorSchemes.jl: +```@example 1 +imview(img; cmap=:ice) +``` +```@example 1 +imview(img; cmap=:seaborn_rocket_gradient) +``` + +Or an RGB or named color value: +```@example 1 +imview(img; cmap="#F00") +imview(img; cmap="red") +``` + +Let's now switch to an astronomical image: +```@example 1 +fname = download( + "http://www.astro.uvic.ca/~wthompson/astroimages/fits/656nmos.fits", + "eagle-656nmos.fits" +); +img = AstroImage("eagle-656nmos.fits") +``` + +We can apply a non-linear stretch like a log-scale, power-scale, or asinh stretch: +```@example 1 +imview(img, stretch=asinhstretch) +``` + +Once rendered, we can also tweak the bias and contrast: +```@example 1 +imview(img, stretch=asinhstretch, contrast=1.5) +``` +```@example 1 +imview(img, stretch=asinhstretch, contrast=1.5, bias=0.6) +``` +These are the parameters that change when you click and drag in some applications like DS9. + +Once rendered via `imview`, the resulting image can be saved in traditional image formats like PNG, JPG, GIF, etc: +```julia +save("out.png", imview(img, cmap=:viridis)) +``` + +Very large Images are automatically downscaled to ensure consistent performance using `restrict` from Images.jl. This function filters the data before downscaling to prevent aliasing, so it may take a moment for truly huge images. In these cases, a faster method that doesn't prevent aliasing would be `imview(img[begin:10:end, begin:10:end])` or similar. + +`imview` is called automatically on `AstroImage{<:Number}` when using a Julia environment with rich graphical IO capabilities (e.g. VSCode, Jupyter, Pluto, etc.). +The defaults for this case can be modified using `AstroImages.set_clims!(...)`, `AstroImages.set_cmap!(...)`, and `AstroImages.set_stretch!(...)`. diff --git a/docs/src/manual/loading-images.md b/docs/src/manual/loading-images.md index 5c563f3e..edb3d9a4 100644 --- a/docs/src/manual/loading-images.md +++ b/docs/src/manual/loading-images.md @@ -104,3 +104,30 @@ julia> img = load("img.fits",1,(Y=1:1600,Z=1:1600)) Other useful dimension names are `Spec` for spectral axes, `Pol` for polarization data, and `Ti` for time axes. These will be further discussed in Dimensions and World Coordinates. + + +## Saving Images +You can save one or more AstroImages and tables to a FITS file using the `save` function: + +```julia +julia> save("abc.fits", astroimage1, astroimage2, table1) +``` + +You can also save individual images to traditional graphics formats by first rendering them with `imview` (for more on imview, see Displaying Images). +```julia +julia> save("abc.png", imview(astroimage1)) +``` + +You can save animated GIFs by saving a 3D datacube that has been rendered with imview: +```julia +julia> cube = imview(AstroImage(randn(100,100,10))); +julia> save("abc.gif", cube, fps=10) + +julia> # Or a more complex example (changing color schemes each frame) +julia> img = randn(10,10) +julia> cube2 = [imview(img1, cmap=:magma) ;;; imview(img2, cmap=:plasma) ;;; imview(img3, cmap=:viridis)] +julia> # Alternative syntax: +julia> cube2 = cat(imview(img1, cmap=:magma), imview(img2, cmap=:plasma), imview(img3, cmap=:viridis), dims=3) +julia> save("abc.gif", cube, fps=10) +``` + From e8359ec383ddfd6c1bf7f54da6a09d880d0773bc Mon Sep 17 00:00:00 2001 From: William Thompson Date: Fri, 3 Jun 2022 16:06:12 -0700 Subject: [PATCH 174/178] First crack at a logo --- docs/src/assets/logo.png | Bin 0 -> 18599 bytes docs/src/manual/displaying-images.md | 31 ++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 docs/src/assets/logo.png diff --git a/docs/src/assets/logo.png b/docs/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..46bf5cfcc23164ff0b7665c7d021074d8926ce27 GIT binary patch literal 18599 zcmV)9K*hg_P)EX>4Tx04R}tkv&MmKpe$iQ?()$2aAX}WT;LSM6EbV6^me@v=v%)FuC+YXws0R zxHt-~1qVMCs}3&Cx;nTDg5U>;qmz@Oi`@MHl=SKon7lnR+6}v_(bAarW+RVI`Qbkx9)Fhls^u8_R9XN`^{2MI2UCjq-)8 z%L?Z$&T6^Jn)l={4Cb}vG}mbkBaTHRkc0>sRcxRP3lUm1QcR?1Kjz^da{Nhh$>iDq zBgZ@{P$4;f@IUz7ty!3yaFc>Dp!3DHKSqGSF3_mi_V=-EH%pV2qvfc{&cXVvYkxsTHaAVXa(-2exN zz-W=O*F4_c-QL^3XPW)}0Ocog)d`cOWdHyG24YJ`L;wH)0002_L%V+f000SaNLh0L z04^f{04^f|c%?sf00007bV*G`2j&I?7XStLKS>J!000?uMObu0Z*6U5Zgc=ca%Ew3 zWn>_CX>@2HM@dakSAh-}001BWNkl8o|Mf$D z*cblP&0pZ8nKSQd?i5oV`DK1P1qDPfBJcVTK?Dl1M0EIVdR}^#K}i2^UMnK>|1taw zMhsCSl0U~{(EPrFaqtDkOL?S^W9TBgQPCZ==wCeFO;5DN8Z|S1bD-}hze$ESvst&*D zAG)>9RtzI?EMtqRayb9?Un)oUL9{1mM1V(rg(?^U%|ZsyyeQfWHb1;ZjBEt@ZsGR= zOv2v$Btl*gve0!<4n+yXL2axEQ5D_#T-{nI&5EZehRwgjb5&7M9{YB4og!PGO%^_k zFtnn0A^~Nodg&etvFS6ke+vha#41K0dO{XP>_w~Z7VYJG`O9~5y6>g7m#+DlC_wC` zq3P0ebBEv4#Pk+{en-o;3dp5@H@9nszi3{I;j^}$t63SW7$#D%jNNemy@FUw+Pzz% z)~$OHQ3?!UYtQ*?4FHTPDl&X_l=Pex5kc6Ypdk{`Y)O*J1x?CTiNhhBYXin1DoFIG zQLGv|Lb4aC6vohKvON7pG6+%Quzsp~X`xm1d#oqVP62|(Z0+;b+-%@s(KUZBBH3go z7m?TWl6B~Xb@_ez*Rv(?%e!NEP8QgOw!`h4ynFiVAuFAgjk_4kJL&rR_X=CW-&hbO z_E(biy~NO=tOh*XG2gwUmfP2wQj9+qQmF^-8Y!8v8{` z*2|z0x2y#}kP7_J3efw#VQIE@^Ona)wl1mn*Ro~)d*#r)K$jibwwquu)x1l~wRaEs z2bKQ4Hi*E9Z29@iSixjd9*CE(Uy|S~UYm{VBPJ$mcLEJL&PE5uR z=`)N9orW!0MS_NmUD8$d?pCvPK`*bfUP1wv*ZtWjfI`TNAldKq{=VO=gNQ{gse~C` z#Q^w>Z2L(09y?!&o4q%Hxoyw5y(_kbD`AO{8{-(N?}zBCXA#67sXEilQup3`Z}zuPFKVH>3C@VjCw>ab#xRS-idj>1?< zDRQ)72A9?U+-eviCxaQ*_>$B70pmOO!vny)APTx=xzjGzEx#;-kO$x=!b~g;0und7 zBLk z!StrQzq;(wN)fvJyr4>H3?pMW^BrNZG@2s=Lm*-T#kSjt8L4ab*S!K6ksODoUDo9VA5b@nX!7Bam`#AmA!K$Zsszy?zKB^t zqk`8y%#K*UhlAFQtg6qvu_EeCd5nPV{X1-OF(Rmn@A}#pcBmgppWUHj2Bs zrcG$M0i%~KhNuh{uHRXjHa`xtiQn^k#Vpt+#*8~mR-sKc!jY_iW=tVye&6)BE~9&! z?T*jykgg@aSB}6Whlu?(tkrKN!=8}>2yop^P>nnd&83W9zkpWk5I4&fARK^?m`l6) zl2Y{1<&YWFEq9U@)yTFr*r?U@a>Q}lToQJZErkxIxOV~Oa>%-sNmLY3Gk{eqDWd5A z7P8ATTUXv=_l`(`#Yn;&0Rx(%3dx|?yeq1(V)E}(-eTJw$zXNg8wqVxYlCd!7Djt! z7~t5i2cV*tP-PErgpk3Z6PrS!tg$Zp?oG;AE`hskFw-M^v-Jt)J(; z;!*6Da`Yy*4+XbfhTRG)Q{qSlU@Key_RsCzgnbtXEitS$3_w$mVWCZ*p;X?L=8_c0 zXSXDp?_GO8pTb0|CWos!L@wKTgWl&SIHbIv3HU%3z)G@#p^xvk^7tE9utDmf53^+R z9@|!96GCl-BLY=6hirjGxfFIuN<-(Howpu?!Ea>;Wh)TdD4P!N)$hO|v>L)yhSwXw z<+QF5Ly@21w!%xvpm8FOnlM~b0(U~lpBcCE`^lth{_G4jv)hsFHBpEeG;f5hLnx@1 zlLP!9ao`6~03Fcj_U@BQ0ox{)7rAWUH-eWf6VIhsx!>8{76y>5hKz1LJFWPQpEln?^OYwcOWEPVw8R@+Wu{eZM%ob?dgnX4+Laq_??-Mn? zD~wHMK4=^YWPLEBTPtP|;+9b5dq`SQ3S()bhrVF+tSmqmQ>Lol{Z0(m+?XT39(I2p z<8I72mqNmnFWC0HI^+;GieiRcm<6K`Xl*}e+*n~CBlCE_3!4_9QaSK?S71FyC37~&MQ9>;s0m3p7G-a z{_F7-fq!&u=SQOex3X6Kcwzr~eVqe0uI#-0fr~-E0qp$P&HlA}>;k_b?*sFG5C!-T ze!P}{Z6Ck(qbLV|?8oc)*Y@%8A4MYQFAepKIA+yNCglukU8=ut5ipPxcc;3%P?_RV z!KJT*_pdWsxdXFB=D5VhMFvKV%e8Qu)3GjpnXT^&Br$qaa_pYtdIg)&Qu=%P7of`t zt+175=?!V2Y1f$i8ikag0X)zO1c4T1ElPvZ5(zoi5OTsPpp>LEd94AD(h=!2xB{Gv|2q4xuN2zV|VHX2l?YX?2s?Xg}r1MJ{Fj`n-KF&_XP~ z_A@W>+G|gFeX&TX(VSM@rfm1Zw_H|)UI}^u+pGx1mcRuDp_`n4FSFn+g3Cg#H1QaO z4VS@hB}PVh)#6auOTsq!XQvy}nQ9wk-a-0%o!@91lJ`RQIdvPk%pP(|;xuQ%M=!Wa z9F6)Rp{6mtUW{Qj+Cx!}mT&&hiJlLn0HbnxsV=@}@ub&udBv*y z+@7v2!)tU9ejGA>U}CZa7VrX2Qx>k@@$FDyfE^t3L?30ls?paWfONl7;5E?YwFsKX zqYj&jaUx909a#SGGWn)|9D8znw(N~@7m>+*un4` z5sF-s>y^QfSh#je@KqLe1PT?brrlp@+5;11VA@Fj-e~%o6cO!GyENccbskoaga5G#9n@;2e4H|-w9Wo7A2dr#ZiHv2csh@}QvA_ay@%ur_T1z%+0 zmwDIQ!OSz1UQ30ECpe z{qJq&FO03Qh?;Rnx~BCO>Mk;s%%Fo}*unJ)jrxJAE&>rlWi0bp4}~Mu@82x)%oRYk z3Nu~nEZ#J>V0tG5Kmyp_KGP5aHISgQ%Rfj*$o*LKQcFuhL|!8xf*|CZ=6Ji%O|V z$fm#R+szaj4p;rdr1j8z#$rILsN zeD>$lsI*?FQ069a7JQBvN=>Xkr!vsHd%A17SIrbJr6%H03Sd_ajlfDGooyH$s@GVH{Iz)8BTPftmqEMtS}h5BSa(PU*6Mi;U_E)eHg?m#=3u zJ5AuNH|gU51rdXC^IFa6aYZmf7lne*IUGWTPy>~Sxmw#W2)IVB1~f1RiYpP9{&DGqMf5T)=%52 zkNJyClIQ_iL8R2adncLYLIPD0IiDBIqoX8dHc-rFJ1ZOki{e#?M({ycip}biqJfnp zeB@MUB{h#u^1cFZ4!`dcHfc9_O^Dv)xP2!xaZ7|r-ucs@RH|5%QVf+w%1nmv2x~FSViIa$ zBcf18o6x=J0Or#5Dw7HL9lHbGk+{B!#P_ZBi$mK`96j(F&U)`th_2C0ArxJD@@V)JW45H3Oq z0;6$dbjefX@wl z!YDrw?eE!JfaK?0UesjV90~CCUzJ#j%%3y#!e{8NDYsI$rNRW5#NG`k9l}arl(9w` z1t*b7;VEp)i4;YnV0}TY7H1cV4R%jilA9Pfc4gzRl5mjOk z>e?kLTA^8Cz{Q9uBQXlaC}6B&ib!#9%K94}M|DYUpteez^Ae+>nIj3k$oU8dj8*8e zqEy*cja9NsNEoBYO{XnkO95hbP9G8o-1N0sRCLJj*{y!J9?3d-;FoS@JB4ernJ+ad zg3aJe6t2Z2yeP^y_E`<_T_64(JIeXjXHf@-WTbzw7mL&#dw9{cq#V;(e4m6#k5#bYm ze~(9R%=z>CkzF+u8EnR_T!2Lw<$F99v(;CS1XF0rU0A?mbAKV^+Cn0neyA?sQn4nZ zdpm@CN%$p}kyt7*moj+U#hOBeBFFYFYwbA4*|QkAFopvS+!)nVm1k5X(`&6#?s!5g zl-}bTi)vclsEIT0qsiS!8*?9`P>nr1*K4Y)ik(_w1F;e8-K)ft3%bLO=2T(cF&-&V zgwZavbH`a%QQAmd7c9>z%8@5{gB~|@6bbyjGbcWbyyS&03to)SZUQH42BN287-e_R z*AJYgyg^~0%Pw+HEGrS-$dODMP@(U;NuRj|uv=)~l7wH1rOLps+_D12P@zjgFLh(~ zZ4T=Sh5ZoO*TBShZWbPEBl`zysvVCh(tS5eM`#6CDpV0$D4{j9wZVwe1%s7{wQ2f? zwMvY_WVdDKT0^l9cIvYyu;1 zSzn~Uv2%{4I68zce@nR*;i^>@J|&Z;VmQ&H1aZq1RJsXO{Z{wFSR=IxYm>5uFaPum z*(*8v{1ZA=#H8ZT`-bYzG7K!lZWN&hdnD99l>)a_xaoI%Me>V9PKowH?-gJalewST zj#KsgCi*90jAu|nx1VWos6L@yYpAd#X9Y21rXjDKSjO1CaDns_ExTgn5$ z?iR@21f8#mwI_c0bu3M8(1w%PTsC4T;!!sb&P)bQ!Ib$FDRee2M5=F z^`{8p`1Zf}GrF&OL=Ey0$NYX^q>7hRC*BvOm81lPYRXmUrsmL(oryVvInzjq&|1T{ z{$j~oE-;evCP5>m*bmbMq@f zdsCQJu(N|)zlVCk{F^V}zZ3AS1@BO!)KYL1Dqeo6X8%(MOn>pGX}<7P+WY5BKeof_ zxBfL+I9A`RxM~ASt?1tJeCFqbx4#sr)!-!w9F%c{T7*iKnMNv2IfkHS7_{cH>sm*{ zNDQ@2d%E9H;NDlj&%73}o$PU929sZ9S5ROmw+lEK=oQ)QIZeCXWWP6yN~IBJB30oj zN>4cr#Lf_-5at1_;7U**(V&E-QtTv@rK-4T`rcG|7LkFGZ;`RZj4kHY9wG{}8L~42 zjmS$M$Nuc+kZKI|f~om3@pUCON;D4n$c)i98!jG$`MpO-^Dnt_FQE5IR&SnR#w}+@ z!nE*=oagFA!G#!(nBnA%J&jDFaIo95Giy0I9W!qnAqq3&nYb`SF(N4^;Y`3sgM@?t@AW3osdD=cEZ>efdnbgL zyK)8WC{o@m@J9hNQnUdn3{n`1JtfvDSs>P8uZ@Z4HD>I<2)gbtS0N3CvOs04flWRh|_O1bl!q`mS+V!G36@G-)xz@ ze1+AUr&QW;=cNu^1VRvYJIkX#f&(IB=NU}`2QPWXcSqd&$>(|U7ss4`;f$`clrAtC zwN#TpvvOFIl3R6MAT2cBFpH@mWitzEgkjBxhynWD-*?icP;VoAw_U!~9`Q2(KBZTH zwCm^ZK-ITbNiu(8a@&_v!o0(b@`R6&%!ZKuH8YEpHidB~Ys`Lyy|D*nf%=GyE6hip zhbuQRw?2wY_rMkC`T{=Qp~vr_XHU`d3uHD+4(|3<w`@ywo^P`rGNaDL8>kB%6< zxk89^D~q*($@4;7)x=e#TN^|)MJ+xuj)sv4QxUuv#wHS@`5?u>4;lrS0sQqbeR}X_xmDw#gnuPb|kU-bLSe71`TGuH6`TKhuxx6OJA+ zZ^t}38*z4C5>~K!Vi?_Wyz;q{H-2})qp@Z0Xu;EqF;{16Cf5TyKYIhST;qx{GOlnh z-$NUZyLX5BcfUwc1&W&^+;Ky(D-!S|aLfPC z6G1M6KP!ERw?X>F5a(x>4%of1B>B z-$Bf4Naryn81YPh;a*Z`XJIzO>>Sg*`UtlQR82=!TSl#;h|0vJ1W}vU`TISdDJirs z{tSwc5q}@@O?8voFSg8npXey^&bPz+st_BnRj&IN2HBf|`XsNDH8QO* zm4n$B>K3d;CSxco#8_z7&@AD5DS+=FdUSDuo;`u}3hEkdTlDmT?$MIv zZ3iYec8sSG%Pa;aRmWjdQ8>6lNA_GFG*%n9pAS$Rf{XWSKxV`4aYNa5x8qjZaVwjj_B zLl{#ExZH~MUC+|({l0GK)QM@66u_ZW5-5yPSx*-YRS}3$hygS}s0C9hCZxDtcK6YX zCE{vm16n#HMr1aI@fg|LPvPQv38M+xHSgUkz9pU=CgoV1!Qwpae($04Xx$N(0Wsj$ z28X9wwYW;y8#RnZp2tU1$}5UksE-2I|JIDR|6s|3-#v!P@-$TJyzSZl-+zykN}+$h z$KRRU`@i^}56&^+eeSzNj|V}#IJlFIk<^F`VJH$ za2DB{VXjVbQ=ymys*$HMk*Nm85vD3k-iHtH$LphK=mNG-e^2f(TfoB(Db6JPo=n** zLP5l#l#*g#V^Y4}m`vgr3EDsmg4=wlQs74oy4*`KZmk}s!YDz2RN8P zHA=he@Bw=G4sz`-=7mqf@|^hUpQ86)hl4AyUZPJ=&~<|@YvM&i{Z1gXLg*ab+CfaA z>DYPJ%Yv%%h>IM5HK0vFeZS%QO9hW#4_HwiP;yIJW;-2j3|h@_pZyu){WrkQV6`AV zx{utr4bG)$UpJmdpT3in)ELBo85^qoNV5ox3QrRa6Sb_O!JF@|0Q1m1%W^PVg`lmj z9F@WJ+bn#SeZA3M3F4)Rn<7w76)PY)za7?zvtLd;VX4@00%E8VzV#hsHcr>Dv!AqM zwSW*{Jca2DiUQfY0Y!z`y@obx=vu^96FaKz+nmB`P>cWYm%7CX^406go`W z+-lA0t6Y8ej2&pzDda0NN8Xane=gHn6VWE3xkoET!4(nf;$|v#C%Is*XcHl>@PF}b z?DbvDo$Ii-hkWGYaB&Q)Wzwqk5;_wMK|8`*a#a-;jKaCzgKo8$Wiczuxs|(mUfYB6=A0azaWKv*f0ecp4E@HYA zI-BhQ+aF3bFxUQjYPflqB1qox8xlmKWEEKnOh)|ulp7e>zsk@(;-@oY(41;Rz7n+w4Up|yly zat$G+ofXiKt8-SVFI*}oJIm>VlEOtMla>$+Q|l?zP^;ze(6Kz5aAVZ)69;FUo{qUV zFL@eA-1_x96ff>zp1Xs_h(3M{g+oW6$48hucVIM5s~019pK=}s#3KCcnOD2=kWiIMQBNnpH*Jl=xN%QRsppXd2lXhq-@= zEnL`;C}S>ANg$2{Ya?A_u|)3PUJ@}pelq4FIv)5D=kt zyDWd}_nG|4PoWnVNkOec*DX3ZLq=n;2A!Xyb%XjytQ8H34ZZU^_e?OYsmJ60i^o&uQ z^Hf#9*hsL3)v{!9Ua+^n=7kp;)@P1u^D(tpY^mJ-sfL>qgFapoe&@I0-d#p7&In)q zRx5b*001BWNkl0uN z=%WW|#a3&=!xL!c$k<`;TuUyX^GI2uah|(3>`w0R9#zTs+R9NdGdtLLcthA6_xQw=^8`RS<1rW zTay{VxyLXX=BtwSojvZ{m@~c^82dHuz+rFh;a<9p{itJo*l_;e{!d*0Cw~{Y^?VZk z({UkxIFC9WRqz!(Zl8#&kY2} z6#iADa<7rZ+c%;ZlT5tirhSnNz9eaSKQlK5K|-pTk4GNg*>ntx#DvUUQXmb6&t+3sh{1`O6YC;`YmYvzl@ll1koo zBFHI(kQ8qeBUXM83$4#i0ir6MMAjM@6Djh+3z5__k6uSKi=jbS8pFyY0J_e7 zdMoL>ZQ^i@OW$ZwAWYO!=0OxLH1gbJo)~X$k+f77ODKZusS;D-- zT^%vr@f14+=I+gO{ksR~!w1ODF51?F?>@jicNJSqnZ7tikKRE~kH9)GCJ|R+a(!(y z$XgSl2Z#jVuTnKFGV`b2bXR*rk3d#lQJ099L6La9mdCdlLs#4 zhD#NEFtk4HXe&xlf?sC~qG@LaQcr(s@3zz;v?8nxEVBu(QEG{-^Dw454`d1`V`FHd z;>9o*;i@`DDg$m2z_)@`gW8-IC<5bY%V^&-e!jp=On)*3Qdp!akX;As1=V$z($)u8 z(d7y$M(E>1t6`Zw=TymA+P`Z$5M!oj+RG7WY?M#N=@OsAMn{Q^2aPDP*HE1({1 zfj)d4uG|8bsZZ4?0mJDo#E0n7@i5D%T^p>V+(a!h9D(zpa00uSr&*cH>AxsSG^xAQ zOFjRZou1SRe)2+y*A0p8oQsURf=Z5rGx?PiJ*`t&&E_wT%f)(f~eLyu2jwSt4IiJjy- z)O)zNfP7pHfOCP_v}1YU2uK=5_dW_R9NhJ6!oYwiv~hz2G-|r$m=r+`c!_l4@RBe< zM>5eq8afq9HQ1;e{?dxqKYqqv+&Jaws7fGnqNuU^$}&61@DP;niQd*#h1?JRmyy(K*&9cl1tb7n~@i4OA1)>~@z} z#~~os_R|fQC30{T4z42yH<8I6x;#rJzg;KBkwoOy3&?1e2&th%t{kAJC+P7ZQjQX( z%{p|kgr+7gTBI_R)4&>&CO3=H$@|v=o@p`2$20`35QmtM(y=CG>r@nP(zq=zj%ZTP zdn=aCn4}1j`vf99_>Z3QC$Bu=@p#Rw+~G!fm601^BpKo^de+^NhvkCr?tO=kU%$x< zU%A1~AB|b%eEm}TVT|cOXHR6E1>T$fpy>n;pLf4(jan&l>)Q5_w`DH9jBTJY$@Evd zf#Qly3J?`Nn`fWD0|(c!ANdqi(~~N?2a)vDwgk@qEkz!3EYP511G*usQygz0HTT~4Baf6o8BacP-}9ekS#(K zMaAJos5L3T!(V^Omp<`b%3{o|@f~)m-AkvBlG~*pR@XuZ{Iv*`7@s9^)}-_ z9pwRxS;_Kr&YFPNv`C$XVZk~L!%^=})4`F27)BUMO*ITZ43ISJOUG>NnZ8g^%$H1G zC@@80Cdt&nbO*V59i|iH>Rn{_8oXZTjSq3CUlPVq0$-Q!feht`q09_{y=C*7O-~%L+4|*2ypo{@zm#TsKv1Q zdken)$~#o$gzJ+VOv>r^{mjx;T%X>=8q3=p5?l6M5;gaQD~$7U65(V)<_$;70lUPp{a65mFe;?if8kMRs

=Tmewt&r22;Dyc`)|voo zC+}|oo@r}X)J)&bqb^=5raTTUo`7sAL9z&&ld?RKRxIwW^62Fwd=0=QXZ&p@|M{=t&X!D{12eM7?Yr2|{2bl4zf1Uk zK99XIL1LaFWD~zn>*afU5lm7Htrtj95zh~y>ykUMLR@=_JI|xn?xJse4PCCmN9ZDA z(jk;KXDvPNKOkaeOOBqU3xIU;XV^NDH7Rv=!5G#_7%(qZev;Smq>B+U5OZ?QIL7moj9jh*gpNzf-t*5P7buIeJ{;5nS7$ zZHK@AfW?>Y(>@lO)-ikd2I2pBjcU>{dT|7Gn}k)3(etP1=^=9Cx$hA>cWAqY(4lRO z9==VSFYp(E>iJoUDCz|=o+h3^(-F?v#05YpZN!A0TL1o?LFSoq0`qzHI#~OD*xHtY z#8j<&dpXC7r8EvFf%cjpdn-y?QrREw@G%i#>?WML6PDLj6q>tc^NCpz<@%)Jt;J$% zwCYw9P#*d=70XByxurf=uKy=DIs2DSxq7FAHZc11J&ynIEl$4N5&V={*`P&7xs!VS z{b`F0ircS=-~I~m{_B_*p2Oa_mrcGI0APIdT53W$IZXV-!y_*K^?k-K9blfn1(PxH zowxJwVGmcXB9l|%NlWksA2uh9lilg}dz^>**(pGOey|gp!;QG53QAOyfuYN1xqsN|$F2cN#98 zs%`R&Z=*6Cv|Oq%QDquouk6^l<&fC1-_%qir5+t{<7W)VpATHy>1Y~9^ak%eu1rJ} ze-a4G1?7vo$Yg{*engyo9bAQschF`<{MMIJ_tEJw@!?@&Qtg(^Ub#Vd?JVUH+B(tR zA0ML~gyRQ^(-=}uq{;ITTj2-F13a^nLxD?2t&^u59o4@u3ExLiS;a%~(=?vDA0n*KlwuMyEr1=5i! zkjrRz<)-K4q4L%L;cf0b*Rp&n%$^tCd`p-cNg2$}5RGA6dde!`#!9hoh;>A)Lf4|F zPgBi!F-IS~fi?{~Um|y&qkZd~<~&jrE!sugbwx%wQaYa#<}GpAVI~H1^MLs9DQ35% z+EM&$#QKS)$fVkqh}REhMa>77zh3%IdmbPC+`vuz?KKi(GSy)oX%C@`9gBAH!|ndi zv1%8@m7#l(KBtRm&c4r~plY%RZS=G;G3k3SsJc1Gp$5M8cF6-RS*zvjp`{VW=*3;; zCgz>pBv`&Id!-GD}VlZbI#hY@$XpLZzxUd@ETaf zmJkD7^mOV8F$`6)zdgRrH8;se*QaURk~x7FCQ;Z&HgdWu3DMAM#e_&J%BfhIXb6R4 zRhQ_vqFGpqgEX5!CS~&1s|I~~PCQ?r7fYxcy3-DS9@zc#4!fV(!^|pT9q`oz^U{mx zssZDW{TbCqrkJY-$aF?HI;VS5vv|j_IxkqN<-!OT1R4$Rp9p%kgpe>~n}*Txna~k4 z8^~)IV*HrHFU>wV{5dRt0^ConIawcb&D@~0EZOcp z6a*gwU#`zt#1rX!_~AtX%BDp;)db{{09151@_h&yww#6lr@sTm zo**xt18^|D!nl}x-}Li&ea6Y^n0gsle%-PDGesn1fSD3gz3=bMQ(yHv4Xb>tO_~AR zXrLz2QH6K0&dfb`>aI&xp(Y{3yoW& z*_B!&U7q2$sukIVovPvB*M5n~i+6dh{$Du!yi&jI`PzT~4c_!4u*#i1r86T6*Cjox z1EI-m;#yfhfXQNxySQn{(F9n)+=x`!iwr+bJ26I*j5F*TWFWqPVu3`F*K0$GF zpYYGWz$>q}+{h#v7uI~UB3QceM9(qxfcp*@53g=6=lfQguWi@ST3^DiFKo(q$! z3dSNgZlF(}(yb%0_K1b`{hGy5#YJ86#8kZMl>4#aB-X68qlx}~yMLc81{luKQ&i+V z$E@}X{PQEjr*|E9E6a{c^zKDa-d;z(u?T#r&V7MmXuqx;e;veuy#-QeqJ%9W9_7() z()SPuV<~y%<|nwj{~WXGfYKFov0+}%d2;rEZymqNx?O$GVpES_f+~$fx=bg(Fv8PV zu`eCp_^Z(IRN-nWDM^{Zcu;S_t?ORJk!Co`me{00oMR-Wd ziPqHWS%!u>cGR)^@S*^b9RFQ1>l-Eb`H|rt-YEI_tl&zO<_Ky&Q&?i8i*VL?UR?(M z@56>KW8S3_ZbkUm+1^(`fzrxEmKgo^V+mxol*o_iuR65`CHx`e1{pef# z{u}=)m8k(|%wRbnF@nl>EmmWMRp)4rg=k>>Z_k+C`d`u2isp-lNVHu4FMgHsxjW4N z+5d>3*qMVpi#c0!<%V$fYG87wptx1RXoUTVpC&%KiHnx$l^NmwA#qt_4@Sf`rl};} zVO>e%I~GTl*Xk*+TEio0IEriAj#C&)bU>j-nnFocag3xsLGFUWA`>UcW?8%-~4Ts-FX&zf9h$v?zJjLVm{AEIgN%O zf;VaG&|9Yy#!tcBpR+u3Ob!CY-P_PD7{9VZJekAVqtQcE;*V>J$|6Q^Wgz^)?*IY0 z`4Qx!pC)|!+sN2qf<+f=ihu7HX@2`l)Q4-9_t!kWm~rYY-z^MB(s3GF&SQ;gq>de3 z40JJ2YxocqAn*Q5GguoU9Y$?E^z;Ed*}8e*>#oi-`e|}Q`es9p7D&w_Kf2} zBrz8!k&#Hk3lc&|NGK0M5FYq9c;q4f01_yO7bNnG#7iDPNKu3o5eqO$u;W{hJs!`^ z-Jb5M>gu|jbN1eghrQ3KYR}cvZp9I`q*7mIRMqF~eb!ogt?&ER_72(EZKNpfal^WG zhODk}lQCDuBmN*0ip=nLyVdIWBCF2{su-K|&6mH$7oYwO@}8x;RnY0@5sJN@paQy8 zP%RvbX~v7@OBANdzr6WRI9J6JLJ9#HQOSFVVIgp_P>#x&aa#ncfK$ucKNz#9dlXCM zbLE`DcfQNux1Pgn52ApT5BNLdSm(EbwVtpHurq4u!F?8kL^>KO7pq z@yvi{1_k+ri{$5@!esZCH5ZX6?mQTPwRGx|9jo}#^G>xA|hmkB`a(sYvwy_bd3 zM7ZmOkAje^!PbVna&*gmn9ZQBkxm!&o-iuOE^Z+mi?ubzCWW)i!sGyb>u31ecX9iU z#a+u}x6AD}1MeRWxGg!itnh*L+?JZ_Va_C!G2v5B85~YMPNQyJP~~vB_rS&L<~!eB z)}zz&097hPGz;|m7dkxG%gA=lk?)*){J#G74*0+&zsK*MFZkEVoJG@QN-_Uym%qxd zTzHYZ$mpN#kaZrffs_@N{@D)R1zx!D1zwuG%(dM&$x|2*5zY|j`V1A$XkbR1=-G^q z@2RqsSTdd%p&+9_>R{egwl6LzwiJJN8HJ{niW$tIjP(hfF9~-(j6x5~k}xf)ZkEjU zGWPfT+*tIP7{h)p>>1A?o{`q^sHYXnRN{82lpdO|kAVj`-EQD|<}boGwk)3?7MNb2 zYve(?ek;Hkzg^7^yVeWBixUoCLEe)5xRH3sn3R)RKIvHjt z-~w8E^k7a{PN}EL^h3+x{ubk9Y(DMg!q_NN97pOoq-Gi_mRd&$dwSIx8)*#^yQND% zqF8sTNnl&hL3rtG#xOJ3;SiI3^0$uRV2j>t#;*-59Mtg)pvsl)D?EMnIc#Al`uUSJ zulgBT$8vu7G~4|P_;Pwo;Vc7YA`B8EsH=(>Vd#~)#Q7*D2pNj=!kCN-Lp>Q%{m+6k zUBlp92_3`ojw2gpP}S6PVLr;3&br(^?6Mnj9KybZk8C8bEQkr+ftHl&Db=mH``FFY zwzIk_c!fvw0#26~km7#V76k8jwwoi?M!x%#CMEqIwY5Ck%Q$Ob5~2%u?##3Fivf9I zG4{#Eq=*r+j-{XX+1a|l?sAeEDl3S%r1DahephH5K|7FuBchrLRpw%GX?R1Xus7+_ zeM%VJ>@uH6;)@t17|=`@Eixuzm>HO4P)gt^v6MZ^*`DRrY|bLY@~)%>F4eUzzk%Te z)}Q>7QA18x2Z(NH>1&=BWh;+Kon%k`wqi5HT87qe&IscWcl)3iVvWUQ=85OzhzK?_ zU@YCDhfkaVuSz8mXXix7LX7z&tWqGNaAN@~>I%7}h7uuhun@!B*Lo~Pm>9z{LAI6} z3BSv{rNuB$Oyv?!sgbanlgH{_a`82RD%2525#q4OhKi?&vTiYiN8kZYHwVyFZ6Di+ zLduCJS|brpSgf4{tudgh3FRl*v?e|4A@Kzkcm`xFB#m@WN#{@lb4*e>i@&P{f=E)Nzk@PWQiDx2I16r z0)32b->DaZn%%evktN(RT>)(flJ`#VeuO|3U>a83x5`zhdO|qa1qUCaXX#2Rb<~=% z(3&GG-8AfKZlJGmx>=6KImE!yNE>lo5}67#Ji@s6Wl|t03+_^3r%LfZO6i0IcMmP_M(#mm{UVY;S+QvMys}?;u6j=>OHA=(Sbwcb)Hx1n z&#kpy)4}X+bct1jmWz=m+FY*k)V^XeAK}z93w6W+m^w<{i_kHKzCx~sB0XVsYF;%% zA4`cIpeAY*_#}^4Ln5bY%T?5*PvA7*P!{SEXE9JFAWN;Di$%_@W!=K(e4@oSNUQbM zdy9R1BsJ_bF@dVb;>^2qhYKD*n>~?hQdRtHh7Zbnv$~Dj3cP=CozZNcy7biLla)VR z<#DBFG9Phse4R4XRH0@TDyCX7(TWi@Q;F_;YNDiEU^U3G2?M@y|I&P*VrT2a8$qO758Z*R{Td6-k`T2HW@<2>}TF@Sx)B=Q})q zG$jm&*x}YEbIM1uy@A356=O||tdDniTWKdNpk7~RR+!|eH zQXNv5JR$@H7HLGCMUpUc0T=>txMN5PBr8?1xcCj;z!Nt_Lr-@pJ*$%&f6FQmtpET9 zW=TXrR8^Hp0&&#psWmZ|6Me18?tRJw;*l@H=~h?=jZ3bXJ^s{dSYt40d*qWg(_a;RT9>~Dx;WwbpR5^_=iA(Gfcfi_nyWqGqJ%i5 z!}~bB%a~*NgH|POGN#_&$1mo*HeT}GTMKUbCibtcv{~Zo4%JB_@UI z5kZ4eEgbVn$>OkLJm2TFTR-OC-upr90;U&47vRAX3b1K$p7nSN;D@)vA}N<)IsHw7PUUjlgAE{f9xmI{D7G zt9pF8cz}<4g%1_}^4fyGxFWoCHgLWyk?|N)A8TsgI zeOL#A9LF1e|9`*Fba}{Eu6&it{tD&1z-A^Ec~VUBKH%$scaB?=8@zVwzxj_3Ucn{~ zW5Z=cBA?N#P~%!!yVCFom8BGHNk&ghqGZEjEf*lg{*aD%j*u4KwWJh>kNHJ<&n;`& zcaD#I83nh+P=@V zFYjZ;;_)o&C8MJ~t{+_EKi_|aBXy)PP{fFkiKT0Dx{@TOKTGumAmAsI&3fi0@W$~3df+`upOP6=}(e*nq0n|o% zUq1$MkDfEEVKG#d$?V++uEAFSs!ZqCPGdI z<%3eW88pS?z*=iDp@MXUBwkAhk0qb!`^&?NoavsY+v!r(6{Gn9cV-_R3n3zcH3lmd zE7ER+=Kq-VJ45`MCq09%$?2J#LM$CDg+$U@JGC>x+z+*;N(((pE!yipZK4xq;oc8GoB_>#=+AunjQmUzMYWY=9GM1c?~FcyWg}cK7eW3a;k| z9|mQ22`oJzT<#3Hqq8>NwUEDbfu`LA@n>l+d@U}l@7|k7dvLTDovg?)&Z4w>AW3CB z4TjJpBO{_MI5Z|)21BgM`IN<%!I6ofn>IqDypf5)2Tk%fF>x!?0yiopEiWR;(lqt` zrnMVVocF=E8GcK(yI-ZWsgI}<9zhH_)k>iA3-WAmZ1H(WeY!fE9}~zI}RgiP1b4lj;K6 z9)%qq14)}`t%#$I46U|{?$guWJo({ROpo7NF{ugE$i0LX^ol^`W~bf^Dv2_E_gL*> z_Lkm+!JV@G>?L(v*sOmYDJ9yaI%|8u|ptpf9-~WmBRP{$xAA_dP*w|;p-U*)qJ^Xu{ z`}j+1-|O4##2er{z+`hTf9Y%t{B3SNol?)Atkpd>uh0I=1K<3#chsK}g~~TK__O^Q z1AqK!_5vDv1NaAG4`e&NvW=(xOxO~56Zm`J&w*Drw`5}*+t|i7wy}+EY-1bS*v2-t yv5jqPV;kGp#x}OGjcsgW8{62%Hny? Date: Sat, 4 Jun 2022 13:36:02 -0700 Subject: [PATCH 175/178] Docs WIP --- docs/make.jl | 1 + docs/src/guide/image-filtering.md | 96 +++++++++++++++++++++++++++ docs/src/manual/conventions.md | 6 +- docs/src/manual/preserving-wrapper.md | 32 +++++++++ 4 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 docs/src/manual/preserving-wrapper.md diff --git a/docs/make.jl b/docs/make.jl index 3a1d875d..9a9a9c96 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -43,6 +43,7 @@ makedocs( "Dimensions and World Coordinates" => "manual/dimensions-and-world-coordinates.md", "Polarization" => "manual/polarization.md", "Spectral Axes" => "manual/spec.md", + "Preserving Wrapper" => "manual/preserving-wrapper.md", "Conventions" => "manual/conventions.md", ], "Guides" => [ diff --git a/docs/src/guide/image-filtering.md b/docs/src/guide/image-filtering.md index e69de29b..33368c48 100644 --- a/docs/src/guide/image-filtering.md +++ b/docs/src/guide/image-filtering.md @@ -0,0 +1,96 @@ +# Image Filtering + +The package [ImageFiltering.jl](https://juliaimages.org/ImageFiltering.jl/stable/) makes it easy to apply arbitrary filters to images. + +```@setup ex1 +using AstroImages +AstroImages.set_clims!(Percent(99.5)) +AstroImages.set_cmap!(:magma) +AstroImages.set_stretch!(identity) +``` + + +## Gaussian Blurs +Let's start by downloading a radio image of Hercules A: +```@example ex1 +using AstroImages +using ImageFiltering + +fname = download( + "http://www.astro.uvic.ca/~wthompson/astroimages/fits/herca/herca_radio.fits", + "herca-radio.fits" +) + +herca = load("herca-radio.fits") +``` + +Let's now apply a Gaussian blur (aka a low pass filter) using the `imfilter` function: +```@example ex1 +herca_blur_20 = imfilter(herca, Kernel.gaussian(20.0)) +``` +The image has been smoothed out by convolving it with a wide Gaussian. + +Let's now do the opposite and perform a high-pass filter. This will bring out faint variations in structure. +We can do this by subtracting a blurred image from the original: + +```@example ex1 +herca_blur_4 = imfilter(herca, Kernel.gaussian(4.0)) +herca_highpass = herca .- herca_blur_4 +``` +We now see lots of faint structure inside the jets! + + +Finally, let's adjust how the image is displayed and apply a non-linear stretch: +```@example ex1 +imview( + herca_highpass, + cmap=:seaborn_rocket_gradient, + clims=(-50,1500), + stretch=asinhstretch +) +``` + +If you have Plots loaded, we can add a colorbar and coordinate axes by switching to `implot`: +```@example ex1 +using Plots +implot( + herca_highpass, + cmap=:seaborn_rocket_gradient, + clims=(-50,1500), + stretch=asinhstretch +) +``` + + +## Median Filtering +In addition to linear filters using `imfilter`, ImageFiltering.jl also includes a great function called `mapwindow`. This functions allows you to map an arbitrary function over a patch of an image. + +Let's use `mapwindow` to perform a median filter. This is a great way to suppress salt and pepper noise, or remove stars from some images. + +We'll use a Hubble picture of the Eagle nebula: +```@example ex1 +using AstroImages +using ImageFiltering + +fname = download( + "http://www.astro.uvic.ca/~wthompson/astroimages/fits/eagle/673nmos.fits", + "eagle-673nmos.fits" +) + +eagle673 = load("eagle-673nmos.fits") +``` +The data is originally from https://esahubble.org/projects/fits_liberator/eagledata/. + + +We can apply a median filter using `mapwindow`. Make sure the patch size is an odd number in each direction! +```@example ex1 +using Statistics +medfilt = copyheader(eagle673, mapwindow(median, eagle673, (11,11))) +``` + +We use `copyheader` here since `mapwindow` returns a plain array and drops the image meta data. + +We can put this side by side with the original to see how some of the faint stars have been removed from the image: +```@example ex1 +imview([eagle673[1:800,1:800]; medfilt[1:800,1:800]]) +``` \ No newline at end of file diff --git a/docs/src/manual/conventions.md b/docs/src/manual/conventions.md index ec68aa40..5328fb8e 100644 --- a/docs/src/manual/conventions.md +++ b/docs/src/manual/conventions.md @@ -12,10 +12,10 @@ as does `img[begin,begin]`. `img[end,end]` is the top right corner, `img[begin,end]` is the top left, etc. -## Pixel Indices specify the Centers of Pixels -The exact location of `img[1,1]` is the center of the pixel in the bottom left corner. +## Pixels +This library considers the exact location of `img[1,1]` to be the center of the pixel in the bottom left corner. This means that plot limits should have the `1` tick slightly away from the left/bottom spines of the image. The default plot limits for `implot` are `-0.5` to `end+0.5` along both axes. -There is a known bug with the Plots.jl GR backend that leads ticks to be slightly offset. PyPlot and Plotly backends +There is a [known bug](https://github.com/JuliaPlots/Plots.jl/issues/4158) with the Plots.jl GR backend that leads ticks to be slightly offset. PyPlot and Plotly backends show the correct tick locations. \ No newline at end of file diff --git a/docs/src/manual/preserving-wrapper.md b/docs/src/manual/preserving-wrapper.md new file mode 100644 index 00000000..59df3fd8 --- /dev/null +++ b/docs/src/manual/preserving-wrapper.md @@ -0,0 +1,32 @@ +# Preserving the AstroImage Wrapper + +Wherever possible, overloads have been added to DimensionalData and AstroImages so that common operations retain the `AstroImage` wrapper with associated dimensions, FITS header, and WCS information. +Most of the time this works automatically if libraries follow good patterns like allocating outputs using `Base.similar`. +However, some other library functions may follow patterns like allocating a plain `Array` of the correct size and then filling it. + +To make it easier to work with these libraries, AstroImages exports two functions `copyheader` and `shareheader`. +These functions wrap an AbstractArray in an AstroImage while copying over the header, dimensions, and WCS info. + +Consider the function: +```julia +function badfunc(arr) + out = zeros(size(arr)) # instead of similar(arr) + out .= arr.^2 + return out +end +``` + +Calling `badfunc(astroimg)` will return a plain `Array` . + +We can use `copyheader` to retain the `AstroImage` wrapper: +```julia +copyheader(astroimg, badfunc(astroimg)) +``` + +For particularly incompatible functions that require an Array (not subtype of AbstractArray) we can go one step further: +```julia +copyheader(astroimg, worsefunc(parent(astroimg))) +# Or: +copyheader(astroimg, worsefunc(collect(astroimg))) +``` + From cc218a5aafb0043face9aeea72e3e360c80bfb3d Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sat, 4 Jun 2022 14:51:00 -0700 Subject: [PATCH 176/178] Docs WIP --- docs/make.jl | 4 +- docs/src/guide/image-transformations.md | 57 +++++++++++++++++++++++++ docs/src/guide/photometry.md | 5 +-- docs/src/guide/reproject.md | 1 + docs/src/manual/displaying-images.md | 14 ++++++ 5 files changed, 76 insertions(+), 5 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 9a9a9c96..73bc572b 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -47,10 +47,10 @@ makedocs( "Conventions" => "manual/conventions.md", ], "Guides" => [ - "Extracting Photometry" => "guide/photometry.md", - "Reprojecting Images" => "guide/reproject.md", "Blurring & Filtering Images" => "guide/image-filtering.md", "Transforming Images" => "guide/image-transformations.md", + "Reprojecting Images" => "guide/reproject.md", + "Extracting Photometry" => "guide/photometry.md", ], demopage, "API" => "api.md", diff --git a/docs/src/guide/image-transformations.md b/docs/src/guide/image-transformations.md index e69de29b..f3461d08 100644 --- a/docs/src/guide/image-transformations.md +++ b/docs/src/guide/image-transformations.md @@ -0,0 +1,57 @@ +# Image Transformations + +The [ImageTransformations.jl](https://juliaimages.org/latest/pkgs/transformations/) package contains many useful functions for manipulating astronomical images. + +Note however that many of these functions drop the AstroImage wrapper and return plain +arrays or OffsetArrays. They can be re-wrapped using `copyheader` or `shareheader` if you'd like to preserve the FITS header, dimension labels, WCS information, etc. + +You can install ImageTransformations by running `] add ImageTransformations` at the REPL. + + +```@setup transforms +using AstroImages +AstroImages.set_clims!(Percent(99.5)) +AstroImages.set_cmap!(:magma) +AstroImages.set_stretch!(identity) +``` + +For these examples, we'll download an image of the Antenae galaxies from Hubble: +```@example transforms +using AstroImages +using ImageTransformations + +fname = download( + "http://www.astro.uvic.ca/~wthompson/astroimages/fits/antenae/blue.fits", + "ant-blue.fits" +) + +antblue = load("ant-blue.fits") + +# We'll change the defaults to avoid setting them each time +AstroImages.set_clims!(Percent(99)) +AstroImages.set_cmap!(:ice) +AstroImages.set_stretch!(asinhstretch) + +imview(antblue) +``` + +## Rotations + +We can rotate images using the `imrotate` function. + +```@example transforms +imrotate(antblue, 3π/4) |> imview +``` +The rotation angle is in radians, but you can use the function `rad2deg` to convert from degrees. + +## Resizing +We can resize images using the `imresize` function: +```@example transforms +imresize(antblue, ratio=0.2) |> imview +``` + +## Arbitrary Transformations +Arbitrary transformations can be performed using ImageTransformation's `warp` function. See the documentation linked above for more details. + +## Mapping from One Coordinate System to Another +For transforming an image from one coordiante system (say, RA & DEC) to another (e.g., galactic lattitude & logitude), see [Reprojecting Images](@ref). \ No newline at end of file diff --git a/docs/src/guide/photometry.md b/docs/src/guide/photometry.md index dfe52a70..58d5a6cc 100644 --- a/docs/src/guide/photometry.md +++ b/docs/src/guide/photometry.md @@ -51,8 +51,7 @@ plot( implot(clipped, title="Sigma-Clipped"), implot(bkg, title="Background"), implot(bkg_rms, title="Background RMS"), - layout=(2, 2), - ticks=false + layout=(2, 2) ) ``` ![](/assets/manual-photometry-2.png) @@ -80,6 +79,6 @@ clims = extrema(vcat(vec(image), vec(subt))) plot( implot(image; title="Original", clims), implot(subt; title="Subtracted", clims), - size=(900,500) + size=(1600,1000) ) ``` \ No newline at end of file diff --git a/docs/src/guide/reproject.md b/docs/src/guide/reproject.md index e69de29b..0da3edfc 100644 --- a/docs/src/guide/reproject.md +++ b/docs/src/guide/reproject.md @@ -0,0 +1 @@ +# Reprojecting Images \ No newline at end of file diff --git a/docs/src/manual/displaying-images.md b/docs/src/manual/displaying-images.md index c292a712..969b91f3 100644 --- a/docs/src/manual/displaying-images.md +++ b/docs/src/manual/displaying-images.md @@ -84,6 +84,20 @@ Very large Images are automatically downscaled to ensure consistent performance `imview` is called automatically on `AstroImage{<:Number}` when using a Julia environment with rich graphical IO capabilities (e.g. VSCode, Jupyter, Pluto, etc.). The defaults for this case can be modified using `AstroImages.set_clims!(...)`, `AstroImages.set_cmap!(...)`, and `AstroImages.set_stretch!(...)`. +## Note on Views +The function `imview` has its name because it produces a "view" into the image. The result from calling `imview` is an object that lazily maps data values into RGBA intensities on the fly. +This means that if you change the underlying data array, the view will update (the next time it is shown). +If you have many data files to render, you may find it faster to create a single `imview` and then mutate the data in the underlying array. This is faster since `imview` only has to resolve colormaps and compute limits once. + +For example: +```julia +data = randn(100,100) +iv = imview(data) +display(iv) +data[1:50,1:50] .= 0 +display(iv) +``` +`iv` will reflect the changes to `data` when it is displayed the second time. ## `implot` From e9a5d0a204aa81201db2942201a7a3087d9d7a27 Mon Sep 17 00:00:00 2001 From: William Thompson Date: Sun, 5 Jun 2022 14:10:18 -0700 Subject: [PATCH 177/178] Docs WIP --- docs/make.jl | 1 + docs/src/manual/conventions.md | 2 + docs/src/manual/displaying-images.md | 2 +- docs/src/manual/headers.md | 47 ++++++++++++++++++ examples/imview-pluto.jl | 72 ++++++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 docs/src/manual/headers.md create mode 100644 examples/imview-pluto.jl diff --git a/docs/make.jl b/docs/make.jl index 73bc572b..0364239f 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -40,6 +40,7 @@ makedocs( "Getting Started" => "manual/getting-started.md", "Loading & Saving Images" => "manual/loading-images.md", "Displaying Images" => "manual/displaying-images.md", + "Headers" => "manual/headers.md", "Dimensions and World Coordinates" => "manual/dimensions-and-world-coordinates.md", "Polarization" => "manual/polarization.md", "Spectral Axes" => "manual/spec.md", diff --git a/docs/src/manual/conventions.md b/docs/src/manual/conventions.md index 5328fb8e..35881fc9 100644 --- a/docs/src/manual/conventions.md +++ b/docs/src/manual/conventions.md @@ -11,6 +11,8 @@ The origin is at the bottom left of the image, so `img[1,1]` refers to the botto as does `img[begin,begin]`. `img[end,end]` is the top right corner, `img[begin,end]` is the top left, etc. +Note that this is transposed and flipped from how how Julia prints arrays at the REPL, + ## Pixels This library considers the exact location of `img[1,1]` to be the center of the pixel in the bottom left corner. diff --git a/docs/src/manual/displaying-images.md b/docs/src/manual/displaying-images.md index 969b91f3..11a225f7 100644 --- a/docs/src/manual/displaying-images.md +++ b/docs/src/manual/displaying-images.md @@ -85,7 +85,7 @@ Very large Images are automatically downscaled to ensure consistent performance The defaults for this case can be modified using `AstroImages.set_clims!(...)`, `AstroImages.set_cmap!(...)`, and `AstroImages.set_stretch!(...)`. ## Note on Views -The function `imview` has its name because it produces a "view" into the image. The result from calling `imview` is an object that lazily maps data values into RGBA intensities on the fly. +The function `imview` has its name because it produces a "view" into the image. The result from calling `imview` is an object that lazily maps data values into RGBA colors on the fly. This means that if you change the underlying data array, the view will update (the next time it is shown). If you have many data files to render, you may find it faster to create a single `imview` and then mutate the data in the underlying array. This is faster since `imview` only has to resolve colormaps and compute limits once. diff --git a/docs/src/manual/headers.md b/docs/src/manual/headers.md new file mode 100644 index 00000000..81805f43 --- /dev/null +++ b/docs/src/manual/headers.md @@ -0,0 +1,47 @@ +# Headers + +FITS files consist of one or more HDUs (header data units), and each HDU can contain an N-dimensional image or table. +Before the data is a *header*. Headers contain (key, value, comment) groups as well as dedicated long-form COMMENT and HISTORY sections used to document, for example, the series of post-processing steps applied to an image. + +## Accessing Headers + +Here are some examples of how to set and read keys, comments, and history. + +Well start by making a blank image. +```julia +img = AstroImage(zeros(10,10)) +# Set keys to values with different data types +img["KEY1"] = 2 # Integer +img["KEY2"] = 2.0 # Float +img["KEY3"] = "STRING" +img["KEY4"] = true +img["KEY5"] = false +img["KEY6"] = nothing + +# Set comments +img["KEY1", Comment] = "A key with an integer value" + +# Read keys +a = img["KEY3"] + +# Read comment +com = img["KEY1", Comment] + +# Add long-form COMMENT +push!(img, Comment, """ +We now describe how to add a long form comment to the end of a header. +""") + +# Add HISTORY entry +push!(img, History, """ +We now describe how to add a long form history to the end of a header. +""") + +# Retrieve long form comments/ history +comment_strings = img[Comment] +history_strings = img[History] +``` + +Note that floating point values are formatted as ASCII strings when written to the FITS files, so the precision may be limited. + +`AstroImage` objects wrap a FITSIO.jl `FITSHeader`. If necessary, you can recover it using `header(img)`; however, in most cases you can access header keywords directly from the image. \ No newline at end of file diff --git a/examples/imview-pluto.jl b/examples/imview-pluto.jl new file mode 100644 index 00000000..9c429eb4 --- /dev/null +++ b/examples/imview-pluto.jl @@ -0,0 +1,72 @@ +### A Pluto.jl notebook ### +# v0.19.6 + +using Markdown +using InteractiveUtils + +# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error). +macro bind(def, element) + quote + local iv = try Base.loaded_modules[Base.PkgId(Base.UUID("6e696c72-6542-2067-7265-42206c756150"), "AbstractPlutoDingetjes")].Bonds.initial_value catch; b -> missing; end + local el = $(esc(element)) + global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : iv(el) + el + end +end + +# ╔═╡ 685479e8-1ad5-48d8-b9fe-f2cf8a672700 +using AstroImages, PlutoUI + +# ╔═╡ 59e1675f-9426-4bc4-88cc-e686ed90b6b5 +md""" +Download a FITS image and open it. +Apply `restrict` to downscale 2x for faster rendering. +""" + +# ╔═╡ d1e5947b-2c1a-46fc-ab8f-feeba03453e7 +img = AstroImages.restrict( + AstroImage(download("http://www.astro.uvic.ca/~wthompson/astroimages/fits/656nmos.fits")) +); + +# ╔═╡ c9ebe984-4630-47c1-a941-795293f5b3c1 +md""" +Display options +""" + +# ╔═╡ a3e81f3f-203b-47b7-ac60-b4267eddfad4 +md""" + +| parameter | value | +|-----------|-------| +|`cmap` | $( @bind cmap Select([:magma, :turbo, :ice, :viridis, :seaborn_icefire_gradient, "red"]) ) | +|`clims`| $( @bind clims Select([Percent(99.5), Percent(95), Percent(80), Zscale(), (0, 400)]) ) | +| `stretch` | $( @bind stretch Select([identity, asinhstretch, logstretch, sqrtstretch, powstretch, powerdiststretch, squarestretch])) | +| `contrast` | $(@bind contrast Slider(0:0.1:2.0, default=1.0)) | +| `bias` | $(@bind bias Slider(0:0.1:1.0, default=0.5)) | +""" + +# ╔═╡ 2315ffec-dc49-413a-b0d6-1bcce2addd76 +imview(img; cmap, clims, stretch, contrast, bias) + +# ╔═╡ d2bd2f13-ed23-42c5-9317-5b48ec3a8bb7 +md""" +## `implot` +Uncomment the following cells to use `Plots` instead. +""" + +# ╔═╡ fe6b5b76-8b77-4bfc-a2e8-bcc0b78ad764 +# using Plots + +# ╔═╡ f557784e-828c-415e-abb0-964b3a9fe8ef +# implot(img; cmap, clims, stretch, contrast, bias) + +# ╔═╡ Cell order: +# ╠═685479e8-1ad5-48d8-b9fe-f2cf8a672700 +# ╟─59e1675f-9426-4bc4-88cc-e686ed90b6b5 +# ╠═d1e5947b-2c1a-46fc-ab8f-feeba03453e7 +# ╟─c9ebe984-4630-47c1-a941-795293f5b3c1 +# ╟─a3e81f3f-203b-47b7-ac60-b4267eddfad4 +# ╠═2315ffec-dc49-413a-b0d6-1bcce2addd76 +# ╟─d2bd2f13-ed23-42c5-9317-5b48ec3a8bb7 +# ╠═fe6b5b76-8b77-4bfc-a2e8-bcc0b78ad764 +# ╠═f557784e-828c-415e-abb0-964b3a9fe8ef From 383299f723fc89cdb71cc013b34a89af8dbba84f Mon Sep 17 00:00:00 2001 From: William Thompson Date: Wed, 15 Jun 2022 08:26:23 -0700 Subject: [PATCH 178/178] Address review comments --- docs/examples/basics/displaying.jl | 2 +- docs/examples/basics/loading.jl | 2 +- docs/examples/config.json | 2 +- docs/src/basics.md | 4 ---- src/precompile.jl | 2 +- 5 files changed, 4 insertions(+), 8 deletions(-) delete mode 100644 docs/src/basics.md diff --git a/docs/examples/basics/displaying.jl b/docs/examples/basics/displaying.jl index 85b71ba9..39496708 100644 --- a/docs/examples/basics/displaying.jl +++ b/docs/examples/basics/displaying.jl @@ -57,4 +57,4 @@ AstroImages.set_stretch!(identity) #src # --- save covers --- #src mkpath("assets") #src -save("assets/loading-images.png", imview(img)) #src \ No newline at end of file +save("assets/loading-images.png", imview(img)) #src diff --git a/docs/examples/basics/loading.jl b/docs/examples/basics/loading.jl index d116e4ca..fc4c59b9 100644 --- a/docs/examples/basics/loading.jl +++ b/docs/examples/basics/loading.jl @@ -20,4 +20,4 @@ img = AstroImage("eagle-656nmos.fits") # --- save covers --- #src mkpath("assets") #src -save("assets/loading-images.png", imview(img)) #src \ No newline at end of file +save("assets/loading-images.png", imview(img)) #src diff --git a/docs/examples/config.json b/docs/examples/config.json index 84b31814..02081db5 100644 --- a/docs/examples/config.json +++ b/docs/examples/config.json @@ -1,4 +1,4 @@ { "template": "index.md", "theme": "list" -} \ No newline at end of file +} diff --git a/docs/src/basics.md b/docs/src/basics.md deleted file mode 100644 index ea7f9d7d..00000000 --- a/docs/src/basics.md +++ /dev/null @@ -1,4 +0,0 @@ -The AstroImages package provides a wrapper, `AstroImage`, that can wrap any AbstractArray. -An `AstroImage` should behave like a plain array, but gains a few extra abilities. - -First, AstroImages are automatically displayed when returned as results in many environements, including VSCode, Jupyter, Pluto, ImageShow, and ImageInTerminal \ No newline at end of file diff --git a/src/precompile.jl b/src/precompile.jl index 320cec1f..8edfd32b 100644 --- a/src/precompile.jl +++ b/src/precompile.jl @@ -41,4 +41,4 @@ end # From trace-compile: precompile(Tuple{typeof(AstroImages.imview), Array{Float64, 2}}) precompile(Tuple{AstroImages.var"#imview##kw", NamedTuple{(:clims, :stretch, :cmap, :contrast, :bias), Tuple{AstroImages.Percent, typeof(Base.identity), Symbol, Int64, Float64}}, typeof(AstroImages.imview), AstroImages.AstroImage{Float64, 2, Tuple{DimensionalData.Dimensions.X{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}, DimensionalData.Dimensions.Y{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}}, Tuple{}, Array{Float64, 2}, Tuple{DimensionalData.Dimensions.X{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}, DimensionalData.Dimensions.Y{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}}}}) -precompile(Tuple{AstroImages.var"#imview_colorbar##kw", NamedTuple{(:clims, :stretch, :cmap, :contrast, :bias), Tuple{AstroImages.Percent, typeof(Base.identity), Symbol, Int64, Float64}}, typeof(AstroImages.imview_colorbar), AstroImages.AstroImage{Float64, 2, Tuple{DimensionalData.Dimensions.X{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}, DimensionalData.Dimensions.Y{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}}, Tuple{}, Array{Float64, 2}, Tuple{DimensionalData.Dimensions.X{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}, DimensionalData.Dimensions.Y{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}}}}) \ No newline at end of file +precompile(Tuple{AstroImages.var"#imview_colorbar##kw", NamedTuple{(:clims, :stretch, :cmap, :contrast, :bias), Tuple{AstroImages.Percent, typeof(Base.identity), Symbol, Int64, Float64}}, typeof(AstroImages.imview_colorbar), AstroImages.AstroImage{Float64, 2, Tuple{DimensionalData.Dimensions.X{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}, DimensionalData.Dimensions.Y{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}}, Tuple{}, Array{Float64, 2}, Tuple{DimensionalData.Dimensions.X{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}, DimensionalData.Dimensions.Y{DimensionalData.Dimensions.LookupArrays.Sampled{Int64, Base.OneTo{Int64}, DimensionalData.Dimensions.LookupArrays.ForwardOrdered, DimensionalData.Dimensions.LookupArrays.Regular{Int64}, DimensionalData.Dimensions.LookupArrays.Points, DimensionalData.Dimensions.LookupArrays.NoMetadata}}}}})