LinearMapsAA overview

This page illustrates the Julia package LinearMapsAA.

This page comes from a single Julia file: 01-overview.jl.

You can access the source code for such Julia documentation using the 'Edit on GitHub' link in the top right. You can view the corresponding notebook in nbviewer here: 01-overview.ipynb, or open it in binder here: 01-overview.ipynb.

Setup

Packages needed here.

using LinearMapsAA
using ImagePhantoms: shepp_logan, SheppLoganToft
using MIRTjim: jim, prompt
using InteractiveUtils: versioninfo

The following line is helpful when running this file as a script; this way it will prompt user to hit a key after each figure is displayed.

isinteractive() ? jim(:prompt, true) : prompt(:draw);

Overview

Many computational imaging methods use system models that are too large to store explicitly as dense matrices, but nevertheless are represented mathematically by a linear mapping A.

Often that linear map is thought of as a matrix, but in imaging problems it often is more convenient to think of it as a more general linear operator.

The LinearMapsAA package can represent both "matrix" versions and "operator" versions of linear mappings. This page illustrates both versions in the context of single-image super-resolution imaging, where the operator A maps a M × N image into a coarser sampled image of size M÷2 × N÷2.

Here the operator A is akin to down-sampling, except, rather than simple decimation, each coarse-resolution pixel is the average of a 2 × 2 block of pixels in the fine-resolution image.

System operator (linear mapping) for down-sampling

Here is the "forward" function needed to model 2× down-sampling:

down1 = (x) -> (x[1:2:end,:] + x[2:2:end,:])/2 # 1D down-sampling by 2×
down2 = (x) -> down1(down1(x)')'; # 2D down-sampling by factor of 2×

The down2 function is a (bounded) linear operator and here is its adjoint:

down2_adj(y::AbstractMatrix{<:Number}) = kron(y, fill(0.25, (2,2)));

Mathematically, and adjoint is a generalization of the (Hermitian) transpose of a matrix. For a (bounded) linear mapping A between inner product space X with inner product <.,.>X and inner product space Y with inner product <.,.>Y, the adjoint of A, denoted A', is the unique bound linear mapping that satisfies <A x, y>Y = <x, A' y>X for all x ∈ X and y ∈ Y. One can verify that the down2_adj function satisfies that equality for the usual inner product on the space of M × N images.

LinearMap as an operator for super-resolution

We now pick a specific image size and define the linear mapping using the two functions above:

nx, ny = 200, 256
A = LinearMapAA(down2, down2_adj, ((nx÷2)*(ny÷2), nx*ny);
    idim = (nx,ny), odim = (nx,ny) .÷ 2)
LinearMapAO: 12800 × 51200 odim=(100, 128) idim=(200, 256) T=Float32 Do=2 Di=2
map = 12800×51200 LinearMaps.FunctionMap{Float32,false}(#6, #8; issymmetric=false, ishermitian=false, isposdef=false)

The idim argument specifies that the input image is of size nx × ny and the odim argument specifies that the output image is of size nx÷2 × ny÷2. This means that when we invoke y = A * x the input x must be a 2D array of size nx × ny (not a 1D vector!) and the output y will have size nx÷2 × ny÷2. This behavior is a generalization of what one might expect from a conventional matrix-vector expression, but is quite appropriate and convenient for imaging problems.

Here is an illustration. We start with a 2D test image.

image = shepp_logan(ny, SheppLoganToft())[(ny-nx)÷2 .+ (1:nx),:]
jim(image, "SheppLogan")
Example block output

Apply the operator A to this image to down-sample it:

down = A * image
jim(down, title="Down-sampled image")
Example block output

Apply the adjoint of A to that result to "up-sample" it:

up = A' * down
jim(up, title="Adjoint: A' * y")
Example block output

That up-sampled image does not have the same range of values as the original image because A' is an adjoint, not an inverse!

AbstractMatrix version

Some users may prefer that the operator A behave more like a matrix. We can implement approach from the same ingredients by using vec and reshape judiciously. The code is less elegant, but similarly efficient because vec and reshape are non-allocating operations.

B = LinearMapAA(
        x -> vec(down2(reshape(x,nx,ny))),
        y -> vec(down2_adj(reshape(y,Int(nx/2),Int(ny/2)))),
        ((nx÷2)*(ny÷2), nx*ny),
    )
LinearMapAM: 12800 × 51200 odim=(12800,) idim=(51200,) T=Float32 Do=1 Di=1
map = 12800×51200 LinearMaps.FunctionMap{Float32,false}(#5, #6; issymmetric=false, ishermitian=false, isposdef=false)

To apply this version to our image we must first vectorize it because the expected input is a vector in this case. And then we have to reshape the vector output as a 2D array to look at it. (This is why the operator version with idim and odim is preferable.)

y = B * vec(image) # This would fail here without the `vec`!
jim(reshape(y, nx÷2, ny÷2)) # Annoying reshape needed here!
Example block output

Even though we write y = A * x above, one must remember that internally A is not stored as a dense matrix. It is simply a special variable type that stores the forward function down2 and the adjoint function down2_adj, along with a few other values like nx,ny, and applies those functions when needed. Nevertheless, we can examine elements of A and B just like one would with any matrix, at least for small enough examples to fit in memory.

Examine A and A'

nx, ny = 8,6
idim = (nx,ny)
odim = (nx,ny) .÷ 2
A = LinearMapAA(down2, down2_adj, ((nx÷2)*(ny÷2), nx*ny); idim, odim)
LinearMapAO: 12 × 48 odim=(4, 3) idim=(8, 6) T=Float32 Do=2 Di=2
map = 12×48 LinearMaps.FunctionMap{Float32,false}(#6, #8; issymmetric=false, ishermitian=false, isposdef=false)

Here is A shown as a Matrix:

jim(Matrix(A)', "A")
Example block output

Here is A' shown as a Matrix:

jim(Matrix(A')', "A'")
Example block output

When defining the adjoint function of a linear mapping, it is very important to verify that it is correct (truly the adjoint).

For a small problem we simply use the following test:

@assert Matrix(A)' == Matrix(A')

For some applications we must check approximate equality like this:

@assert Matrix(A)' ≈ Matrix(A')

Here is a statistical test that is suitable for large operators. Often one would repeat this test several times:

T = eltype(A)
x = randn(T, idim)
y = randn(T, odim)

@assert sum((A*x) .* y) ≈ sum(x .* (A'*y)) # <Ax,y> = <x,A'y>

Reproducibility

This page was generated with the following version of Julia:

using InteractiveUtils: versioninfo
io = IOBuffer(); versioninfo(io); split(String(take!(io)), '\n')
12-element Vector{SubString{String}}:
 "Julia Version 1.10.3"
 "Commit 0b4590a5507 (2024-04-30 10:59 UTC)"
 "Build Info:"
 "  Official https://julialang.org/ release"
 "Platform Info:"
 "  OS: Linux (x86_64-linux-gnu)"
 "  CPU: 4 × AMD EPYC 7763 64-Core Processor"
 "  WORD_SIZE: 64"
 "  LIBM: libopenlibm"
 "  LLVM: libLLVM-15.0.7 (ORCJIT, znver3)"
 "Threads: 1 default, 0 interactive, 1 GC (on 4 virtual cores)"
 ""

And with the following package versions

import Pkg; Pkg.status()
Status `~/work/LinearMapsAA.jl/LinearMapsAA.jl/docs/Project.toml`
  [6e4b80f9] BenchmarkTools v1.5.0
  [e30172f5] Documenter v1.4.1
  [7a1cc6ca] FFTW v1.8.0
  [71a99df6] ImagePhantoms v0.8.0
  [7031d0ef] LazyGrids v1.0.0
  [599c1a8e] LinearMapsAA v0.12.0 `~/work/LinearMapsAA.jl/LinearMapsAA.jl`
  [98b081ad] Literate v2.18.0
  [170b2178] MIRTjim v0.24.0
  [b77e0a4c] InteractiveUtils
  [37e2e46d] LinearAlgebra

This page was generated using Literate.jl.