Sorry for the long gap between updates.
Iโve tried a number of different approaches and benchmarked the results. The particular problem I tried to optimize is reducing the compute time for a pixel-to-value computation of an array, regardless of the setup time.
1 - Brute force
Using Loess.jl, fit the individual colors, and then sample them at an acceptable density, then compute the 3D distance between pixel color and the sampled colors. The minimum distance index gives the corresponding temperatrue.
Code Snippet
using Images, Loess, Statistics, ThreadsX
fn = "flir.jpg"
const lw, hi, ฮด = 18.0, 280., 0.1
const temp_rng = range(lw, hi, step = ฮด)
frame_flir = Images.load(fn)
idxs = (rows = 31:209, cols = 308:313)
cb = frame_flir[idxs...]
const t_samp_rng = range(lw, hi, length(idxs.rows)) |> collect
unzip(a) = map(x->getfield.(a, x), fieldnames(eltype(a)))
R, G, B = unzip(cb)
function (lm::Loess.LoessModel)(x)
return predict(lm, x)
end
span = 0.1 # 0.125
model_R = loess(t_samp_rng, mean(R, dims = 2) |> vec; span)
model_G = loess(t_samp_rng, mean(G, dims = 2) |> vec; span)
model_B = loess(t_samp_rng, mean(B, dims = 2) |> vec; span)
### method 1, brute force
const samples = (R = model_R(temp_rng),
G = model_G(temp_rng),
B = model_B(temp_rng))
function find_temp(A, samples = samples)
matB = map(x->(r = Float64(x.r), g = Float64(x.g), b = Float64(x.b)), A) :: Matrix{@NamedTuple{r::Float64, g::Float64, b::Float64}}
t = ThreadsX.map(pix -> temp_rng[findmin(map((r, g, b) -> (pix.r - r)^2 + (pix.g - g)^2 + (pix.b - b)^2, samples...))[2]], matB) :: Matrix{Float64}
return t
end
Using BenchmarkTools.jl, average execution time is ~500ms.
julia> @benchmark find_temp(frame_flir) seconds = 10
BenchmarkTools.Trial: 21 samples with 1 evaluation.
Range (min โฆ max): 370.657 ms โฆ 1.223 s โ GC (min โฆ max): 53.07% โฆ 69.91%
Time (median): 408.187 ms โ GC (median): 54.02%
Time (mean ยฑ ฯ): 507.752 ms ยฑ 246.115 ms โ GC (mean ยฑ ฯ): 58.57% ยฑ 5.36%
โโโ โ
โโโโ
โโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโ
โ
371 ms Histogram: frequency by time 1.22 s <
Memory estimate: 1.51 GiB, allocs estimate: 231131.
2 - Symbolic Regression
If an analytic expression linking the RGB encoding to the scalar value exists, then using it would be the fastest way to convert the entire picture. Using SymbolicRegression.jl, I tried to infer an analytic form starting from both the RGB and HSV colorspaces.
Code Snippet
using Images
import SymbolicRegression: SRRegressor
import MLJ: machine, fit!, predict, report
fn = "flir.jpg"
const lw, hi, ฮด = 18.0, 280., 0.1
const temp_rng = range(lw, hi, step = ฮด)
frame_flir = Images.load(fn)
idxs = (rows = 31:209, cols = 308:313)
cb = frame_flir[idxs...]
const t_samp_rng = range(lw, hi, length(idxs.rows)) |> collect
unzip(a) = map(x->getfield.(a, x), fieldnames(eltype(a)))
R, G, B = unzip(cb)
### method 2, symbolic regression
# RGB - colorspace
inRGB = (r = vec(R), g = vec(G), b = vec(B))
# T - temperature
inT = stack(t_samp_rng for i in 1:6) |> vec
model = SRRegressor(
niterations = 1000,
binary_operators = [+, -, *, /],
unary_operators=[exp, log, cos, sin, sqrt],
)
mach = machine(model, inRGB, inT)
fit!(mach)
rep = report(mach)
println(rep.equations[rep.best_idx])
julia> ((log(r + 0.06514106105173974) + sin(sin(b * 2.00389950943762))) * -114.86518971882892) + 117.47144076159321
julia> @benchmark bestf.(frame_flir)
BenchmarkTools.Trial: 2622 samples with 1 evaluation.
Range (min โฆ max): 1.724 ms โฆ 13.856 ms โ GC (min โฆ max): 0.00% โฆ 86.34%
Time (median): 1.830 ms โ GC (median): 0.00%
Time (mean ยฑ ฯ): 1.900 ms ยฑ 481.131 ฮผs โ GC (mean ยฑ ฯ): 4.03% ยฑ 9.53%
โโโโโโ
โโโโโโโโ
โโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโ โ
1.72 ms Histogram: log(frequency) by time 3.48 ms <
Memory estimate: 600.09 KiB, allocs estimate: 4.
which is a ~500x improvement, however thereโs a significant error; maybe itโs possible to refine this approach, but I am no familiar enough with the underlying package.
and HSV
Code Snippet
# HSV - colorspace
H, S, V = convert.(HSV, cb) |> unzip
inHSV = (h = vec(H), s = vec(S), v = vec(V))
# T - temperature
inT = stack(t_samp_rng for i in 1:6) |> vec
model = SRRegressor(
niterations = 1000,
binary_operators = [+, -, *, /],
unary_operators=[exp, log, cos, sin, sqrt],
)
mach = machine(model, inHSV, inT)
fit!(mach)
rep = report(mach)
println(rep.equations[rep.best_idx])
with similar results, both for time to execute and error.
3 - @mthelm85โs approach using k-d tree, with some unsophisticated parallelization via ThreadsX.jl
julia> @benchmark ThreadsX.map(pixel -> pixel_to_value(pixel, kdtree), flir)
BenchmarkTools.Trial: 975 samples with 1 evaluation.
Range (min โฆ max): 3.462 ms โฆ 26.377 ms โ GC (min โฆ max): 0.00% โฆ 6.33%
Time (median): 5.203 ms โ GC (median): 29.23%
Time (mean ยฑ ฯ): 5.106 ms ยฑ 1.761 ms โ GC (mean ยฑ ฯ): 19.10% ยฑ 15.10%
โ โโ
โโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
3.46 ms Histogram: frequency by time 11.8 ms <
Memory estimate: 22.39 MiB, allocs estimate: 615110.
evaluates in ~5ms, which is close to the analytical approach, but the error is much improved.
.
I will also look into the performance when feeding the complete list of points to
knn(...)
.
4 - @empetโs approach using Clustering.jl seems to be a good approach for when the colorbar is missing. The downside is that the colorscheme, and implicitly the underlying scalar is unsorted.
julia> zdata, cscheme = image2zvals(img; n_colors=128, maxiter=200, tol=1e-04);
julia> cscheme
And the execution time is the longest so far at ~ 2s.
julia> @benchmark image2zvals(img, n_colors = 128, maxiter = 50, tol = 1e-2) seconds = 60
BenchmarkTools.Trial: 32 samples with 1 evaluation.
Range (min โฆ max): 1.196 s โฆ 2.685 s โ GC (min โฆ max): 0.24% โฆ 0.15%
Time (median): 1.932 s โ GC (median): 0.19%
Time (mean ยฑ ฯ): 1.921 s ยฑ 368.344 ms โ GC (mean ยฑ ฯ): 0.17% ยฑ 0.05%
โ โ โ โ โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
1.2 s Histogram: frequency by time 2.69 s <
Memory estimate: 47.21 MiB, allocs estimate: 335.