Getting Started

To begin, we first need to gather some MPI data. To do this we could use data from the OpenMPIData initiative, however for these examples we will use the data used in testing MPIReco.

To access this test data, first, enter the Pkg mode in Julia (]) and execute the unit tests of MPIReco::

test MPIReco

This will download and unpack some MPI measurements and calibration MDF files and perform tests with the data. The download location will be printed at the start of the test execution. You can cancel the test execution once the data is downloaded by pressing Ctrl + C or closing the Julia process.

After the download, several MPI files will be present in the specified directory. All subsequent examples assume that you have assigned the path to this directory to a variable named datadir:

const datadir = joinpath("...","artifacts", "...") # enter path here

First Reconstruction

We will start looking at a small reconstruction script. First, we load MPIReco:

using MPIReco

Next, we open handles for the system matrix and measurement data. Both are created via MPIFile function, which can be, for instance, an MDFFile or a BrukerFile.

bSF = MPIFile(joinpath(datadir, "calibrations", "12.mdf"))
b = MPIFile(joinpath(datadir, "measurements", "20211226_203916_MultiPatch", "1.mdf"))
MDFFile
	Study: "FocusField", Dates.DateTime("2014-11-13T16:20:01.217")
	Experiment: "Ph5DotsDF10Overlap0LL (E99)", Dates.DateTime("2014-11-27T14:40:01.280")

To interact with the files, you can use the functionality of MPIFiles, which is also exported by MPIReco:

acqNumFrames(b)
1

We refer to the documentation for MPIFiles.jl and MDF for more information on the available data and functions.

Now we will perform a system-matrix based reconstruction using the reconstruct function. The function takes as arguments the algorithm to use, the measurement data, and various keyword arguments. The available parameters and their interpretation depend on the chosen algorithm. In this case, the algorithm also expects the system matrix to be set. We set the SNR threshold to 5, meaning that only matrix rows with an SNR above 5 will be used during reconstruction. The parameter frame decides which frame of the measured data should be reconstructed.

c = reconstruct("SinglePatch", b;
                   SNRThresh=5,
                   sf = bSF,
                   frames=1:acqNumFrames(b),
                   minFreq=80e3,
                   recChannels=1:rxNumChannels(b),
                   iterations=1,
                   spectralLeakageCorrection=true)
Float32 ImageMeta with:
  data: 5-dimensional AxisArray{Float32,5,...} with axes:
    :color, 1:1
    :x, (-27.375:1.25:11.375) mm
    :y, (-11.375:1.25:27.375) mm
    :z, (0.0:1.0:0.0) mm
    :time, (0.0:6.5280000000000005:0.0) s
And data, a 1×32×32×1×1 Array{Float32, 5}
  properties:
    tracerName: ["Resovist"]
    version: 2.0.1
    experimentDescription: Ph5DotsDF10Overlap0LL (E99)
    scannerOperator: nmrsu
    rxNumSamplingPoints: 1632
    acqOffsetField: [-0.01; 0.01; -0.0;;;]
    dfStrength: [0.01 0.01 0.0;;;]
    scannerTopology: FFP
    rxDataConversionFactor: [0.0001 0.0001 0.0001; 0.0 0.0 0.0]
    experimentNumber: 99
    dfDivider: [102; 96; 99;;]
    tracerBatch: ["0"]
    acqStartTime: 2014-11-27T14:40:01.280
    tracerInjectionTime: [DateTime("2014-11-27T14:40:01.280")]
    dfWaveform: ["sine";;]
    dim: 2
    uuid: 8eadd786-d630-4f53-ae91-c8f90721bbdc
    dfBaseFrequency: 2.5e6
    dfCycle: 0.0006528
    experimentName: Ph5DotsDF10Overlap0LL (E99)
    studyUuid: 12246d79-646a-41b8-a278-5274fc9ae7c5
    tracerVolume: [0.0]
    studyDescription: n.a.
    experimentSubject: FocusField
    experimentUuid: 787b3b7b-734a-4872-8bad-a38a64478cca
    acqNumFrames: 1
    tracerConcentration: [0.5]
    dfNumChannels: 3
    dfPhase: [1.5708 1.5708 1.5708;;;]
    scannerManufacturer: Bruker/Philips
    studyTime: 2014-11-13T16:20:01.217
    scannerFacility: Universitätsklinikum Hamburg Eppendorf
    studyName: FocusField
    scannerName: Preclinical MPI System
    tracerSolute: ["Fe"]
    size: [32, 32, 1]
    time: 2021-12-26T19:52:45.666
    experimentIsSimulation: false
    datatype: MPI
    acqNumPeriodsPerFrame: 1
    experimentIsCalibration: false
    studyNumber: 1
    rxBandwidth: 1.25e6
    tracerVendor: ["n.a."]
    rxUnit: a.u.
    acqNumAverages: 10000
    rxNumChannels: 3
    acqGradient: [-1.25 0.0 0.0; 0.0 -1.25 0.0; 0.0 0.0 2.5;;;;]

Notice that the result is not just an array of particle concentration, but contains a variety of metadata as well. This is because c is a:

typeof(c)
ImageMeta{Float32, 5, AxisArray{Float32, 5, Array{Float32, 5}, Tuple{Axis{:color, UnitRange{Int64}}, Axis{:x, StepRangeLen{Unitful.Quantity{Float64, 𝐋, Unitful.FreeUnits{(mm,), 𝐋, nothing}}, Base.TwicePrecision{Unitful.Quantity{Float64, 𝐋, Unitful.FreeUnits{(mm,), 𝐋, nothing}}}, Base.TwicePrecision{Unitful.Quantity{Float64, 𝐋, Unitful.FreeUnits{(mm,), 𝐋, nothing}}}, Int64}}, Axis{:y, StepRangeLen{Unitful.Quantity{Float64, 𝐋, Unitful.FreeUnits{(mm,), 𝐋, nothing}}, Base.TwicePrecision{Unitful.Quantity{Float64, 𝐋, Unitful.FreeUnits{(mm,), 𝐋, nothing}}}, Base.TwicePrecision{Unitful.Quantity{Float64, 𝐋, Unitful.FreeUnits{(mm,), 𝐋, nothing}}}, Int64}}, Axis{:z, StepRangeLen{Unitful.Quantity{Float64, 𝐋, Unitful.FreeUnits{(mm,), 𝐋, nothing}}, Base.TwicePrecision{Unitful.Quantity{Float64, 𝐋, Unitful.FreeUnits{(mm,), 𝐋, nothing}}}, Base.TwicePrecision{Unitful.Quantity{Float64, 𝐋, Unitful.FreeUnits{(mm,), 𝐋, nothing}}}, Int64}}, Axis{:time, StepRangeLen{Unitful.Quantity{Float64, 𝐓, Unitful.FreeUnits{(s,), 𝐓, nothing}}, Base.TwicePrecision{Unitful.Quantity{Float64, 𝐓, Unitful.FreeUnits{(s,), 𝐓, nothing}}}, Base.TwicePrecision{Unitful.Quantity{Float64, 𝐓, Unitful.FreeUnits{(s,), 𝐓, nothing}}}, Int64}}}}, Dict{Symbol, Any}}

You can access metadata as properties, such as:

c.tracerName
1-element Vector{String}:
 "Resovist"

The result can also be treated like a normal 5-dimensional array. The first dimension is for different contrasts, followed by the three spatial dimensions and lastly the temporal dimension of the different frames.

size(c)
(1, 32, 32, 1, 1)

You can access the underlying data just like any other array in Julia. For example, you can access an XY slice as follows:

slice = c[1, :, :, 1, 1]
typeof(slice)
ImageMeta{Float32, 2, AxisArrays.AxisMatrix{Float32, Matrix{Float32}, Tuple{Axis{:x, StepRangeLen{Unitful.Quantity{Float64, 𝐋, Unitful.FreeUnits{(mm,), 𝐋, nothing}}, Base.TwicePrecision{Unitful.Quantity{Float64, 𝐋, Unitful.FreeUnits{(mm,), 𝐋, nothing}}}, Base.TwicePrecision{Unitful.Quantity{Float64, 𝐋, Unitful.FreeUnits{(mm,), 𝐋, nothing}}}, Int64}}, Axis{:y, StepRangeLen{Unitful.Quantity{Float64, 𝐋, Unitful.FreeUnits{(mm,), 𝐋, nothing}}, Base.TwicePrecision{Unitful.Quantity{Float64, 𝐋, Unitful.FreeUnits{(mm,), 𝐋, nothing}}}, Base.TwicePrecision{Unitful.Quantity{Float64, 𝐋, Unitful.FreeUnits{(mm,), 𝐋, nothing}}}, Int64}}}}, Dict{Symbol, Any}}

Note that the slice is still an ImageMeta array. Inside the array is an AxisArray, another array type with metadata. Not every Julia package has methods for such metadata arrays. To visualize our results with CairoMakie, we need to extract the underlying data.

sliceRaw = slice.data.data
typeof(sliceRaw)
Matrix{Float32} (alias for Array{Float32, 2})

We can now visualize our first MPI reconstruction using MPIReco:

using CairoMakie
fig = heatmap(sliceRaw)
hidedecorations!(fig.axis)
fig
Example block output

This page was generated using Literate.jl.