Skip to content
Snippets Groups Projects
Select Git revision
  • 4cb490c8f342ca56486a4142abefe324f697dfa6
  • master default protected
  • releases
  • releases/3.0.3
4 results

plotansi.m4

Blame
  • image.jl 14.17 KiB
    export evaluate_bilinear, halve, warp_backwards
    
    # General Utility Methods on Images
    
    function mean_squared_error(img, img_ref)
        axes(img) == axes(img_ref) ||
            throw(ArgumentError("images need matching axes"))
        n = length(img)
        return sum((img .- img_ref) .^ 2 ./ n)
    end
    
    function peak_signal_to_noise_ratio(img, img_ref)
        mse = mean_squared_error(img, img_ref)
        imax = 1.0 # maximum intensity
        return 10 * log10(imax / mse)
    end
    
    # FIXME: unused?
    from_img(img) = permutedims(reverse(arr; dims = 1))
    
    eval_neumann(img, x) = img[clamp.(Tuple(x), axes(img))...]
    
    # uses native array indexing, i.e. 1:n
    function evaluate_bilinear(img, x)
        x0 = floor.(Int, x)
        x1 = x0 .+ 1
    
        val = zero(eltype(img))
        for idx in CartesianIndices(ntuple(_->0:1, ndims(img)))
            cornerbool = Bool.(Tuple(idx))
            λ = ifelse.(cornerbool, x .- x0, x1 .- x)
            corner = ifelse.(cornerbool, x1, x0)
            val += prod(λ) * eval_neumann(img, corner)
        end
        return val
    end
    
    function warp_backwards(img, u)
        d = ndims(img)
        axes(img) == axes(u)[2:end] && 1:d == axes(u, 1) ||
            throw(ArgumentError("invalid dimensions"))
    
        res = similar(img)
        for I in CartesianIndices(img)
            x = Tuple(I) .+ ntuple(i -> u[i, I], d)
            res[I] = evaluate_bilinear(img, x)
        end
        return res
    end
    
    function halve(img)
        all(iseven.(size(img))) || throw(ArgumentError("uneven size"))
    
        res = similar(img, size(img) .÷ 2)
        fill!(res, zero(eltype(res)))
    
        for i in CartesianIndices(img)
            j = CartesianIndex((Tuple(i) .+ 1) .÷ 2)
            res[j] += img[i]
        end
        res ./= 2 ^ ndims(img)
    
        return res
    end
    
    # ArrayFunction mesh function wrapper
    
    # TODO: inherit from some abstract mesh function type
    # TODO: make mesh type a parameter for performance
    struct ArrayFunction{Img}
        mesh::Mesh
        img::Img
        cell::Base.RefValue{Int}
    end
    
    ArrayFunction(mesh, img) = ArrayFunction(mesh, img, Ref(0))
    
    bind!(f::ArrayFunction, cell) = f.cell[] = cell
    evaluate(f::ArrayFunction, xloc) = evaluate_bilinear(f.img,
        elmap(f.mesh, f.cell[])(xloc))
    
    # Sampling
    
    # evaluate the function on a rectangular grid of coordinates with integer
    # spacing bounded by rounded minimum and maximum vertex coordinates.
    # i.e. pixel center coordinates of an image. output indexing as for images:
    # first index is y downwards, second index is x rightwards
    # TODO: get rid of this hack as soon as we use size=() for scalar functions
    function sample(f::FeFunction)
        out = _sample(f)
        return f.space.size == (1,) ?
            reshape(out, size(out)[2:end]) : out
    end
    
    function _sample(f::FeFunction)
        # assumes dual grid
        mesh = f.space.mesh
    
        m0 = NTuple{2}(minimum(mesh.vertices, dims = 2))
        m1 = NTuple{2}(maximum(mesh.vertices, dims = 2))
    
        fcolon = ntuple(_ -> :, length(f.space.size))
    
        outsize = (f.space.size..., round.(Int, m1 .- m0 .+ 1)...)
        out = similar(f.data, outsize)
        for cell in cells(mesh)
            bind!(f, cell)
            A = SArray{Tuple{ndims_space(mesh), nvertices_cell(mesh)}}(
                view(mesh.vertices, :, view(mesh.cells, :, cell)))
    
            # TODO: cleanup when minimum/maximum with dims on StaticArrays becomes
            # type-stable
            I0 = round.(Int, SVector(minimum(A[1, :]), minimum(A[2, :])) .- m0) .+ 1
            I1 = round.(Int, SVector(maximum(A[1, :]), maximum(A[2, :])) .- m0) .+ 1
    
            for I in CartesianIndex(I0[1], I0[2]):CartesianIndex(I1[1], I1[2])
                x = SVector(Tuple(I) .- 1 .+ m0)
                # TODO: move to global-to-local function or inside-triangle test
                xloc = (A[:, SUnitRange(2, 3)] .- A[:, 1])::SArray \
                    (x .- A[:, 1])
                ε = eps()
                if all(xloc .>= -ε) && sum(xloc) <= 1 + ε
                    # use view here to handle scalar data assigment as well
                    view(out, fcolon..., I) .= evaluate(f, xloc)
                end
            end
        end
    
        return out
    end
    
    
    # Mesh Function Projection Methods
    
    # TODO: refine interface: projection vs interpolation, unify different
    # algorithms
    
    """
    interprets `img` as a bilinearly interpolated continuous function and applies
    the default interpolation operator for the discrete function u.
    """
    # TODO: should be called "interpolate_nodal"
    function interpolate!(u::FeFunction, img::AbstractArray)
        f = ArrayFunction(u.space.mesh, img)
        interpolate!(u, @inline (x; f) -> f; f)
    end
    
    project_l2_lagrange(space::FeSpace, img) =
        (u = FeFunction(space); project_l2_lagrange!(u, img))
    
    """
    standard l2 projection but using an adaptive quadrature rule based on cell size
    """
    # TODO: this could be a general l2 projection, accepting a mesh function and
    # some custom quadrature rule?
    function project_l2_lagrange!(u::FeFunction, img::AbstractArray)
        d = 2 # domain dimension
        space = u.space
        mesh = space.mesh
        f = ArrayFunction(mesh, img)
        opparams = (; f)
    
        nrdims = prod(space.size)
        # number of element dofs (i.e. local dofs not counting codomain dimensions)
        nldofs = ndofs(space.element)
    
        a(xloc, u, du, v, dv; f) = dot(u, v)
        l(xloc, v, dv; f) = dot(f, v)
    
        I = Int[]
        J = Int[]
        V = Float64[]
        n = length(img) * nrdims * nldofs * nrdims * nldofs
        sizehint!(I, n)
        sizehint!(J, n)
        sizehint!(V, n)
        b = zeros(ndofs(space))
    
        # mesh cells
        for cell in cells(mesh)
            foreach(f -> bind!(f, cell), opparams)
            # cell map is assumed to be constant per cell
            delmap = jacobian(elmap(mesh, cell), SA[0., 0.])
            delmapinv = inv(delmap)
            intel = abs(det(delmap))
    
            p = ceil(Int, diam(mesh, cell))
            qw, qx = quadrature_composite_lagrange_midpoint(p)
            nqpts = length(qw) # number of quadrature points
    
            qphi = zeros(nrdims, nrdims, nldofs, nqpts)
            dqphi = zeros(nrdims, d, nrdims, nldofs, nqpts)
            for k in axes(qx, 2)
                for r in 1:nrdims
                    qphi[r, r, :, k] .= evaluate_basis(space.element, SVector{d}(view(qx, :, k)))
                    dqphi[r, :, r, :, k] .= transpose(jacobian(x -> evaluate_basis(space.element, x), SVector{d}(view(qx, :, k))))
                end
            end
    
            # quadrature points
            for k in axes(qx, 2)
                xhat = SVector{d}(view(qx, :, k))
                x = elmap(mesh, cell)(xhat)
                opvalues = map(f -> evaluate(f, xhat), opparams)
    
                # local test-function dofs
                for jdim in 1:nrdims, ldofj in 1:nldofs
                    gdofj = space.dofmap[jdim, ldofj, cell]
    
                    phij = SArray{Tuple{space.size...}}(view(qphi, :, jdim, ldofj, k))
                    dphij = SArray{Tuple{space.size..., d}}(
                        SArray{Tuple{nrdims, d}}(view(dqphi, :, :, jdim, ldofj, k)) * delmapinv)
    
                    lv = qw[k] * l(x, phij, dphij; opvalues...) * intel
                    b[gdofj] += lv
    
                    # local trial-function dofs
                    for idim in 1:nrdims, ldofi in 1:nldofs
                        gdofi = space.dofmap[idim, ldofi, cell]
    
                        phii = SArray{Tuple{space.size...}}(view(qphi, :, idim, ldofi, k))
                        dphii = SArray{Tuple{space.size..., d}}(
                            SArray{Tuple{nrdims, d}}(view(dqphi, :, :, idim, ldofi, k)) * delmapinv)
    
                        av = qw[k] * a(x, phii, dphii, phij, dphij; opvalues...) * intel
                        push!(I, gdofi)
                        push!(J, gdofj)
                        push!(V, av)
                    end
                end
            end
        end
    
        ngdofs = ndofs(space)
        A = sparse(I, J, V, ngdofs, ngdofs)
    
        u.data .= A \ b
        return u
    end
    
    project_qi_lagrange(space::FeSpace, img) =
        (u = FeFunction(space); project_qi_lagrange!(u, img))
    
    """
    L1-stable quasi-interpolation operator
    (https://arxiv.org/pdf/1505.06931.pdf)
    """
    # FIXME: currently only approximate quadrature by sampling on lagrange lattice
    # based on element size. Exact evaluation is tricky to implement (lots of
    # intersection handling).
    function project_qi_lagrange!(u::FeFunction, img)
        u.space.element == P1() ||
            throw(ArgumentError("element unsupported"))
        d = 2 # domain dimension
        space = u.space
        mesh = space.mesh
        f = ArrayFunction(mesh, img)
        # count contributions to respective dof for subsequent averaging
        gdofcount = zeros(Int, size(u.data))
        area_refel = 0.5
    
        u.data .= zero(eltype(u.data))
        for cell in cells(mesh)
            bind!(f, cell)
    
            # size: nrdims × nldofs
            # TODO: should be statically sized
            gdofs = u.space.dofmap[:, :, cell]
    
            p = ceil(Int, diam(mesh, cell))
            qw, qx = quadrature_composite_lagrange_midpoint(p)
            for k in axes(qx, 2)
                xhat = SVector{d}(view(qx, :, k))
                x = elmap(mesh, cell)(xhat)
    
                # size: nrdims
                value = evaluate(f, xhat)
                # size: nldofs
                # actually: transformation to barycentric coordinates!
                phix = evaluate_basis(space.element, xhat)
    
                # no integration element used
                #u.data[gdofs] .+= qw[k] ./ area_refel .* 3 .* value * phix'
                u.data[gdofs] .+= qw[k] ./ area_refel * value * (12 .* phix' .- 3)
    
                #rhox = SA[-12 -12; 12 0; 0 12] * xhat + SA[9, -3, -3]
                #u.data[gdofs] .+= qw[k] ./ area_refel * value * rhox'
            end
            gdofcount[gdofs] .+= 1
        end
        u.data ./= gdofcount
        return u
    end
    
    struct PixelIterator{A, G}
        "cell vertices"
        A::A
        "CartesianIndices covering the cell"
        grid::G
    end
    
    Base.IteratorSize(::PixelIterator) = Base.SizeUnknown()
    Base.eltype(it::PixelIterator) = eltype(it.grid)
    
    """
        PixelIterator(mesh, cell)
        PixelIterator(mesh, A)
    
    Return an iterator over all integer cartesian indices (corresponding to pixel
    centers) intersecting a given `cell` or geometry `A`.
    The returned indices represent coordinates in the given mesh.
    """
    function PixelIterator(mesh::Mesh, cell::Int)
        A = SArray{Tuple{ndims_space(mesh), nvertices_cell(mesh)}}(
            view(mesh.vertices, :, view(mesh.cells, :, cell)))
        return PixelIterator(mesh, A)
    end
    
    function PixelIterator(mesh::Mesh, A)
        # rounding is probably correct since we don't miss any coordinates even
        # when we round inwards to the element
        I0 = round.(Int, SVector(minimum(A[1, :]), minimum(A[2, :])))
        I1 = round.(Int, SVector(maximum(A[1, :]), maximum(A[2, :])))
        return PixelIterator(A, CartesianIndex(I0...):CartesianIndex(I1...))
    end
    
    function _intersects(it::PixelIterator, I)
        x = SVector(Tuple(I))
        # TODO: move to global-to-local function or inside-triangle test
        xloc = (it.A[:, SUnitRange(2, 3)] .- it.A[:, 1])::SArray \
            (x .- it.A[:, 1])
        ε = eps()
        return all(xloc .>= -ε) && sum(xloc) <= 1 + ε
    end
    
    function Base.iterate(it::PixelIterator, state...)
        y = iterate(it.grid, state...)
        isnothing(y) && return nothing
        # TODO: do something more sophisticated than walking the whole cartesian
        # bounding box
        while !_intersects(it, first(y))
            y = iterate(it.grid, last(y))
            isnothing(y) && return nothing
        end
        return y
    end
    
    function project_l2_pixel!(u::FeFunction, img)
        d = 2 # domain dimension
        space = u.space
        mesh = space.mesh
        f = ArrayFunction(mesh, img)
        opparams = (; f)
    
        nrdims = prod(space.size)
        # number of element dofs (i.e. local dofs not counting codomain dimensions)
        nldofs = ndofs(space.element)
    
        # first loop to count number of triangles intersecting pixel evaluation
        # points
        ncells = zeros(axes(img))
        for cell in cells(mesh)
            pixels = PixelIterator(mesh, cell)
            for I in pixels
                ncells[I] += 1
            end
        end
        all(!iszero, ncells) ||
            throw(ErrorException("not all pixels are covered by a cell"))
        pixelweights = inv.(ncells)
    
        # typical L2 projection of f
        a(xloc, u, du, v, dv; f) = dot(u, v)
        l(xloc, v, dv; f) = dot(f, v)
    
        I = Float64[]
        J = Float64[]
        V = Float64[]
        b = zeros(ndofs(space))
    
        # buffer for basis value/derivative at a single point
        qphi = zero(MArray{Tuple{nrdims, nrdims, nldofs}})
        dqphi = zero(MArray{Tuple{nrdims, d, nrdims, nldofs}})
    
        for cell in cells(mesh)
            foreach(f -> bind!(f, cell), opparams)
            # cell map is assumed to be constant per cell
            delmap = jacobian(elmap(mesh, cell), SA[0., 0.])
            delmapinv = inv(delmap)
            intel = abs(det(delmap))
    
            # pixels as quadrature points
            pixels = PixelIterator(mesh, cell)
            for P in pixels
                x = SVector(Tuple(P))
                # TODO: move to global-to-local function or inside-triangle test
                xhat = (pixels.A[:, SUnitRange(2, 3)] .- pixels.A[:, 1])::SArray \
                    (x .- pixels.A[:, 1])
                opvalues = map(f -> evaluate(f, xhat), opparams)
    
                # fill basis value/derivative buffer
                for r in 1:nrdims
                    qphi[r, r, :] .=
                        evaluate_basis(space.element, SVector{d}(xhat))
                    dqphi[r, :, r, :]::MArray{Tuple{d, nldofs}, Float64, 2, d * nldofs} .=
                        transpose(jacobian(
                            x -> evaluate_basis(space.element, x),
                            SVector{d}(xhat)))
                end
    
                # local test-function dofs
                for jdim in 1:nrdims, ldofj in 1:nldofs
                    gdofj = space.dofmap[jdim, ldofj, cell]
    
                    phij = SArray{Tuple{space.size...}}(view(qphi, :, jdim, ldofj))
                    dphij = SArray{Tuple{space.size..., d}}(
                        SArray{Tuple{nrdims, d}}(view(dqphi, :, :, jdim, ldofj)) * delmapinv)
    
                    lv = pixelweights[P] * l(x, phij, dphij; opvalues...) * intel
                    b[gdofj] += lv
    
                    # local trial-function dofs
                    for idim in 1:nrdims, ldofi in 1:nldofs
                        gdofi = space.dofmap[idim, ldofi, cell]
    
                        phii = SArray{Tuple{space.size...}}(view(qphi, :, idim, ldofi))
                        dphii = SArray{Tuple{space.size..., d}}(
                            SArray{Tuple{nrdims, d}}(view(dqphi, :, :, idim, ldofi)) * delmapinv)
    
                        av = pixelweights[P] * a(x, phii, dphii, phij, dphij; opvalues...) * intel
                        push!(I, gdofi)
                        push!(J, gdofj)
                        push!(V, av)
                    end
                end
            end
        end
    
        ngdofs = ndofs(space)
        A = sparse(I, J, V, ngdofs, ngdofs)
    
        u.data .= A \ b
        return u
    end