GithubHelp home page GithubHelp logo

Comments (19)

behinger avatar behinger commented on June 22, 2024 1

Actually, I missremembered the exact comment. They were planning to do that, but now just put it to the mean value as far as I can see:
https://github.com/mne-tools/mne-python/blob/9e4a0b492299d3638203e2e6d2264ea445b13ac0/mne/viz/topomap.py#L642

They either put the extra points on the convex hull, or on the circle of the plot.

EEGlab uses griddata internally which simply returns NaNs for extrapolated values.

Proposed solution: Put pad_value = mean(data) by default. (havent tried it)

Regarding the issue, I wrote it on my list, but will soon be on parental leave, so not sure it will happen..

from topoplots.jl.

SimonDanisch avatar SimonDanisch commented on June 22, 2024 1

Ah yeah, I guess that was an old, deprecated scene attribute ...

from topoplots.jl.

yakir12 avatar yakir12 commented on June 22, 2024 1

I agree that we would need to choose a good-enough variance, just like a smoothing factor. But unlike introducing novel zero points at the perimeter that "push away" peaks from their original coordinates, what I suggest will keep peaks at their original locations.

from topoplots.jl.

palday avatar palday commented on June 22, 2024

I suspect the problem would also occur at all the other boundaries if there were electrodes close to them. I'll investigate a bit.

from topoplots.jl.

yakir12 avatar yakir12 commented on June 22, 2024

I think that the main problem here is that of extrapolation: How does one correctly extrapolate from data in two or three dimensional irregular coordinates?

The approach taken here is to add an outer perimeter that encloses the known data. This perimeter can have a certain geometry (shape), padding (how far out does it stretch outside the known coordinates), pad_value (defaults to zero), and number of discrete data points it has. All of these parameters influence the "quality" of the extrapolation. A perimeter with low padding (i.e. close to the data), low pad value (relative to the data), and densely populated will "push away" hot spots from their original coordinates, which is what we're seeing here.

I therefore suspect that datasets with a relatively low number of points will suffer more than denser datasets.

I managed to improve on some examples by choosing values that are different to the defaults. However, no set of values worked for all of my examples (i.e. I did not find globally better defaults).

One expensive idea I imagine could work is if we used some diffusion model to allow the data to diffuse outwards freely and then crop the result with some shape (i.e. geometry). This seems to me like the most natural type of extrapolation.

For context, neither Interpolations.jl nor Dierckx.jl tackle 2D extrapolation of irregularly spaced coordinates, so this is not a simple problem.

from topoplots.jl.

behinger avatar behinger commented on June 22, 2024

that makes a lot of sense - iirc MNE sets the padding values to the closest electrode, instead of having the same value for all padded locations (not sure exactly how padding works here).

I could not set padding=0. or padding=0.2, I could only change the pad_value. Do you know why?

from topoplots.jl.

yakir12 avatar yakir12 commented on June 22, 2024

sets the padding values to the closest electrode

This would be an improvement, but I suspect it won't always work. I wonder how do they manage to end up with a circle/ellipse using that rule..?

I could not set padding=0. or padding=0.2, I could only change the pad_value. Do you know why?

I suspect it's because there is a depreciation check to warn about that specific keyword that has been used before for padding figures (actually Scenes) from within the plotting function. I see the same issue (unrelated to this specific issue though). It's worth filing an issue separately and I'm sure it would be appreciated if you did so 😄

from topoplots.jl.

behinger avatar behinger commented on June 22, 2024

I fixed the padding bug, by renaming it to enlarge, padding seems to be somewhat protected, thus als no issue on makie.jl


Just in case here was my finished MWE, before I thought that padding might be protected somehow @SimonDanisch might immediately know whether it is worth to post to Makie.jl or not.

	@recipe(TestPlot,data) do scene
	return Attributes(
		padding = 0.1
	)
end
function Makie.plot!(p::TestPlot)
	plot(p.data)
end
	testplot([1,2,3],padding=5)

from topoplots.jl.

yakir12 avatar yakir12 commented on June 22, 2024

A really easy cheat we could use to avoid the original issue here is to treat the raw data as a zero-image where the pixels closest to the coordinates are equal to the coordinates' corresponding values. Then convolve the image with say a gaussian with large enough variance. The result can then either be the end result of the topology, or can be used as the input data to the topology. That's actually really simple.

from topoplots.jl.

behinger avatar behinger commented on June 22, 2024

@ykair12 (I hope I got the idea) while possible, I don't think that would be a great interpolator, if variance is too small, every electrode is simply a "bump", if variance is too large, we get mean value everywhere. Thus each electrode distance would require their specific variance specification - if the distance is too large, we would automatcally interpolate with 0 . IIRC, something very similar is implemented via #5 ScatteredInterpolation.jl directly as an interpolator via Gaussian

Maybe I understood it wrongly, how would that solve the boundary issue? In the boundary we would interpolate towards 0 still - correct?

from topoplots.jl.

yakir12 avatar yakir12 commented on June 22, 2024

Here is an implementation of my suggestion:

using SparseArrays, Statistics, LinearAlgebra
using GLMakie, ImageFiltering, Delaunay, Interpolations
function toindex(x, r, n)
    x += r
    x /= 2r/(n - 1)
    return round(Int, x + 1)
end
function sparseimg(v, cxy, r, n)
    i = toindex.(first.(cxy), r, n)
    j = toindex.(last.(cxy), r, n)
    return collect(sparse(i, j, v, n, n))
end
function plottopo(v, xy; buff = 0.05, l = 13, σ_factor = 8)
    n = 2l + 1
    c = mean(xy)
    xy .-= c
    r = maximum(norm, xy)
    r *= 1 + buff
    img0 = sparseimg(v, xy, r, n)
    imgw = imfilter(img0, Kernel.gaussian(n/σ_factor))
    xs = range(-r, r, n) .+ c[1]
    ys = range(-r, r, n) .+ c[2]
    #
    keep = [ij for ij in CartesianIndices((n, n)) if norm(Tuple(ij) .- l .- 1)  l]
    #
    data = imgw[keep]
    positions = Point2.(xs, ys')[keep]
    itp = LinearInterpolation((xs, ys), imgw)
    for θ in range(0, 2π, 51)[1:end-1]
        p = c + r * Point2(reverse(sincos(θ)))
        push!(positions, p)
        push!(data, itp[p...])
    end
    #
    m = delaunay(convert(Matrix{Float64}, hcat(first.(positions), last.(positions))));
    #
    fig = Figure()
    ax = Axis(fig[1,1], aspect = DataAspect())
    mesh!(ax, m.points, m.simplices, color = data)
    scatter!(ax, xy .+ c, color = v, colormap = :reds)
    fig
end
using TopoPlots
data, positions = TopoPlots.example_data()
plottopo(vec(data[:,6,3]), positions)

Which looks like this:
tmp

Now it's just the matter of integrating this into the existing code here. I'll start a draft PR.

from topoplots.jl.

yakir12 avatar yakir12 commented on June 22, 2024

I found an even better shortcut: instead of convolving gaussian filters with a sparse image, I can just compose a mixture model from Distributions.jl! Much much cheaper:

using Statistics, LinearAlgebra
using GLMakie, Delaunay, Distributions
function random_disk_points(c, n, r)
    positions = fill(c, n)
    for i in 1:n
        R = r * sqrt(rand())
        θ = 2π * rand()
        positions[i] += Point2(R .* sincos(θ))
    end
    return positions
end
function get_perimeter(c, n, r)
    positions = fill(c, n)
    for (i, θ) in pairs(range(0, 2π, n + 1)[1:end-1])
        positions[i] += Point2(r .* sincos(θ))
    end
    return positions
end
function plottopo(v, xy; buff = 0.05, σ_factor = 5, n = 200)
    c = mean(xy)
    r = maximum(norm, xy .- c)
    r *= 1 + buff
    positions = copy(xy)
    random = random_disk_points(c, n, r)
    perimeter = get_perimeter(c, 50, r)
    append!(positions, random, perimeter)
    σ = r/σ_factor
    p = v .- minimum(v)
    p /= sum(p)
    a = MixtureModel(MvNormal, tuple.(xy, σ), p)
    data = pdf.(Ref(a), positions)
    m = delaunay(convert(Matrix{Float64}, hcat(first.(positions), last.(positions))));
    fig = Figure()
    ax = Axis(fig[1,1], aspect = DataAspect())
    mesh!(ax, m.points, m.simplices, color = data)
    scatter!(ax, xy, color = v, colormap = :reds)
    fig
end
using TopoPlots
data, xy = TopoPlots.example_data()
v = vec(data[:,6,3])
fig = plottopo(v, xy)

tmp

from topoplots.jl.

behinger avatar behinger commented on June 22, 2024

cool! could you plot data[:,340,1] instead?

One Q: Maybe I missunderstand something, but isnt there the same artefact as in the previous interpolators?

from topoplots.jl.

yakir12 avatar yakir12 commented on June 22, 2024

cool! could you plot data[:,340,1] instead?

Sure:
tmp

One Q: Maybe I missunderstand something, but isnt there the same artefact as in the previous interpolators?

No. The points along the perimeter in TopoPlots.jl are all zero-valued, and therefore affect the resulting gradient. The points along the perimeter here are calculated from the mixture model and are therefore closer to the data we have (without the need to assume that they fall to absolute zero exactly at the perimeter).

Here is an example where the standard deviation is much smaller:

fig = plottopo(v, xy; σ_factor=20, n=10000)

You can see that the peaks match the data exactly:
tmp

from topoplots.jl.

behinger avatar behinger commented on June 22, 2024

Ah. I was confused about the scatter colorscale.

I tried playing around with your solution, not sure whats going on, this is how it should look like (data340)
grafik

could it be that negative values are "removed" due to the v .- minimum(v)?

from topoplots.jl.

yakir12 avatar yakir12 commented on June 22, 2024

Ah. I was confused about the scatter colorscale.

Yeah, I just set the color to the value of the specific point so that I'll better understand the contribution of each point to the resulting gradient.

Late last night, I realised why you asked me to try data[:,340,1] and how my implementation (with the mixture model) was wrong! Instead of using the Probability Density Function of a mixture model, I should use the sum of the Gaussian functions directly.

Here I define a gaussian function with a set standard deviation, a mean equal to the coordinate of the data, and a maximum equal to the data's "height" or value. The sum of all the points' gaussians describes the topology of the data.

Here is the implementation and result.

using Statistics, LinearAlgebra
using GLMakie, Delaunay, Distributions
iso_2d_gaussian(μx, μy, σ, v, x, y) = v*exp(-((x - μx)^2 + (y - μy)^2)/2σ^2) 
iso_2d_gaussian(μ, σ, v, xy) = iso_2d_gaussian..., σ, v, xy...)
function random_disk_points(c, n, r)
    positions = fill(c, n)
    for i in 1:n
        R = r * sqrt(rand())
        θ = 2π * rand()
        positions[i] += Point2(R .* sincos(θ))
    end
    return positions
end
function get_perimeter(c, n, r)
    positions = fill(c, n)
    for (i, θ) in pairs(range(0, 2π, n + 1)[1:end-1])
        positions[i] += Point2(r .* sincos(θ))
    end
    return positions
end
function plottopo(v, xy; buff = 0.05, σ_factor = 5, n = 200)
    c = mean(xy)
    r = maximum(norm, xy .- c)
    r *= 1 + buff
    positions = copy(xy)
    random = random_disk_points(c, n, r)
    perimeter = get_perimeter(c, 50, r)
    append!(positions, random, perimeter)
    σ = r/σ_factor
    fun = x -> sum(zip(xy, v)) do (μ, h)
        iso_2d_gaussian(μ, σ, h, x)
    end
    data = fun.(positions)
    m = delaunay(convert(Matrix{Float64}, hcat(first.(positions), last.(positions))));
    fig = Figure()
    ax = Axis(fig[1,1])#, aspect = DataAspect())
    mesh!(ax, m.points, m.simplices, color = data)
    scatter!(ax, xy, color = :transparent, strokecolor = :black, strokewidth = 1, markersize = 5)#v, colormap = :reds)
    fig
end
using TopoPlots
data, xy = TopoPlots.example_data()
v = vec(data[:,340,1])
fig = plottopo(v, xy)

tmp

I think that while this is better than introducing tons of zeros (or pad_values) at the perimeter, it still suffers from the heavy-handed smoothing...

from topoplots.jl.

behinger avatar behinger commented on June 22, 2024

I guess it works well for high density caps, and maybe less so for 10/20 caps?
I gess that is true for all interpolation schemes.

I think it is worth to look into the MNE scheme how they do it (linked above), and what griddata in Matlab does

from topoplots.jl.

yakir12 avatar yakir12 commented on June 22, 2024

OK, what a loop. I was wrong about a few things, most importantly, Dierckx.jl does indeed tackle 2D extrapolation of irregularly spaced coordinates! Following is a really simple implementation that seems to work well (although there are some linear artefacts in the extrapolation).

There are two instances where new points need to be added to the data:

  1. along the perimeter
  2. gridded data for plotting the contour lines

While the interpolator (i.e. Dierckx's spline in the following case) is used to generate points for the second instance, the first instance just uses zeros (i.e. pad_value) in the current implementation of TopoPlots.jl. Here, I use the interpolator to generate points for the perimeter as well.

My next step is to try and use the (superior?) ClaughTochter interpolator.

using GLMakie
using TopoPlots

using Statistics, LinearAlgebra
using Dierckx, Delaunay

# get the data 
data, xy = TopoPlots.example_data()
v = vec(data[:,340,1])

# some parameters
buff = 0.05
kx = ky = 2
s = 0.5
resolution = 512

# create the interpolating spline
itp = Spline2D(first.(xy), last.(xy), v; kx, ky, s)

# create perimeter
c = mean(xy)
r = maximum(norm, xy .- c)
r *= 1 + buff
perimeter_xy = decompose(Point2f, Circle(c, r))
perimeter_v = evaluate(itp, first.(perimeter_xy), last.(perimeter_xy))

# add the perimeter to the data
positions = vcat(xy, perimeter_xy)
data = vcat(v, perimeter_v)

# delaunay triangulation for the mesh
m = delaunay(convert(Matrix{Float64}, hcat(first.(positions), last.(positions))));

# gridded data for the contour
xl = range(c[1] - r, c[1] + r, resolution)
yl = range(c[2] - r, c[2] + r, resolution)
z = evalgrid(itp, xl, yl)
for (i, x) in pairs(xl), (j, y) in pairs(yl)
    if norm(Point2f(x, y) - c) > r
        z[i, j] = 0 # could be set to NaN too...
    end
end

# plot it all
fig = Figure()
ax = Axis(fig[1,1], aspect = DataAspect())
mesh!(ax, m.points, m.simplices, color = data)
contour!(ax, xl, yl, z, levels=6)
scatter!(ax, xy, color = :transparent, strokecolor = :black, strokewidth = 1, markersize = 5)

tmp

from topoplots.jl.

yakir12 avatar yakir12 commented on June 22, 2024

The saga continues...

I found that given some data "within a circle", such as the case with EEG, the extrapolation problem is easiest to solve if we just set the 4 corners of the enclosing square to zero. So by adding 4 new coordinates to the original data, one coordinate at each corner of the square that encloses the (enlarged) circle that itself encloses all the original coordinates, and by setting those 4 points to zero, we solve the whole thing. We now have our original data plus 4 zero corners, and now we can interpolate within this square all the data we need (i.e. the perimeter of the circle for the Delaunay mesh plot, and the gridded data within that circle for the contour plot). The only assumption is that data at the corners of the enclosing square is equal to zero (or some user defined value).

In the case of rectangular data, enlarging/extrapolating the data is dubious to begin with, but the same principals apply here as well.

using Statistics, LinearAlgebra
using SciPy, Delaunay

using GLMakie
using TopoPlots

function get_circle(xy; buff = 0.1)
    c = mean(xy)
    r = (1 + buff)*maximum(x -> norm(x - c), xy)
    return c, r
end

function get_boundingbox(c, r)
    bb_xy = Point2f[c + r*Point2f(sx, sy) for sx in (-1, 1) for sy in (-1, 1)]
    bb_v = zeros(4)
    return bb_xy, bb_v
end

function get_interpolator(xy, v, c, r; tol = 1e-6, maxiter = 400, rescale = false)
    # get the bounding box
    bb_xy, bb_v = get_boundingbox(c, r)
    append!(bb_xy, xy)
    append!(bb_v, v)

    # interpolate within the bounding box
    itp = SciPy.interpolate.CloughTocher2DInterpolator(Tuple.(bb_xy), bb_v; tol, maxiter, rescale)
    return itp
end


function plottopo(v, xy)
    # find enclosing circle
    c, r = get_circle(xy)
    itp = get_interpolator(xy, v, c, r)

    # gridded data
    resolution = 100
    xl = range(c[1] - r, c[1] + r, length = resolution)
    yl = range(c[2] - r, c[2] + r, length = resolution)
    z = collect(itp(xl' .* ones(length(yl)), ones(length(xl))' .* yl)')

    # set interpolated data outside enclosing circle to NaN
    incircle = [norm(Point2f(x, y) - c)  r for (i, x) in pairs(xl), (j, y) in pairs(yl)]
    z[(!).(incircle)] .= NaN

    # add bounding geometry to the data
    cxy = decompose(Point2f, Circle(c, r))
    cv = only.(itp.(cxy))
    xyl = Point2f.(xl, yl')
    append!(cxy, xyl[incircle])
    append!(cv, z[incircle])

    # delaunay triangulation for the mesh
    m = delaunay(convert(Matrix{Float64}, hcat(first.(cxy), last.(cxy))));

    # plot it all
    fig = Figure()
    ax = Axis(fig[1,1])
    mesh!(ax, m.points, m.simplices, color = cv, colormap = Reverse(:RdBu), shading=false)
    contour!(ax, xl, yl, z, levels=6, color=(:black, 0.5), linestyle=:dot)
    scatter!(ax, xy, color = v, colormap = Reverse(:RdBu), strokecolor = :black, strokewidth = 1, markersize = 5)
    return fig
end

and

data, xy = TopoPlots.example_data()
v = vec(data[:,340,1])
plottopo(v, xy)

results in
tmp

from topoplots.jl.

Related Issues (17)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.