Skip to content

Commit 02f15f1

Browse files
committed
implement generic ROF model using Chambolle04 primal-dual method
1 parent 1bbf666 commit 02f15f1

File tree

10 files changed

+307
-4
lines changed

10 files changed

+307
-4
lines changed

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
.vscode
55
docs/build
66
docs/site
7-
docs/Manifest.toml
87
docs/src/democards
9-
/Manifest.toml
8+
Manifest.toml
109
/.benchmarkci
1110
/benchmark/*.json

Project.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ ComputationalResources = "ed09eef8-17a6-5b46-8889-db040fac31e3"
99
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
1010
FFTViews = "4f61f5a4-77b1-5117-aa51-3ab5ef4ef0cd"
1111
FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341"
12+
ImageBase = "c817782e-172a-44cc-b673-b171935fbb9e"
1213
ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534"
1314
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1415
OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
@@ -24,6 +25,7 @@ ComputationalResources = "0.3"
2425
DataStructures = "0.17.7, 0.18"
2526
FFTViews = "0.3"
2627
FFTW = "0.3, 1"
28+
ImageBase = "0.1.5"
2729
ImageCore = "0.9"
2830
OffsetArrays = "1.9"
2931
Reexport = "1.1"
@@ -33,10 +35,14 @@ julia = "1"
3335

3436
[extras]
3537
AxisArrays = "39de3d68-74b9-583c-8d2d-e117c070f3a9"
38+
ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19"
39+
ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1"
3640
ImageMetadata = "bc367c6b-8a6b-528e-b4bd-a4b897500b49"
41+
ImageQualityIndexes = "2996bd0c-7a13-11e9-2da2-2f5ce47296a9"
3742
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
3843
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
3944
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
45+
TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990"
4046

4147
[targets]
42-
test = ["AxisArrays", "ImageMetadata", "Logging", "Random", "Test"]
48+
test = ["AxisArrays", "ImageIO", "ImageMagick", "ImageMetadata", "ImageQualityIndexes", "Logging", "Random", "Test", "TestImages"]

docs/src/function_reference.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ Algorithm.IIR
7272
Algorithm.Mixed
7373
```
7474

75+
# Solvers for predefined models
76+
77+
```@autodocs
78+
Modules = [Models]
79+
```
80+
7581
# Internal machinery
7682

7783
```@docs

src/ImageFiltering.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ include("mapwindow.jl")
9292
using .MapWindow
9393
include("extrema.jl")
9494

95+
include("models.jl")
96+
9597
function __init__()
9698
# See ComputationalResources README for explanation
9799
push!(LOAD_PATH, dirname(@__FILE__))

src/models.jl

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
module Models
2+
3+
using ImageBase
4+
using ImageBase.ImageCore.MappedArrays: of_eltype
5+
using ImageBase.FiniteDiff
6+
7+
# Introduced in ColorVectorSpace v0.9.3
8+
# https://github.com/JuliaGraphics/ColorVectorSpace.jl/pull/172
9+
using ImageBase.ImageCore.ColorVectorSpace.Future: abs2
10+
11+
"""
12+
This submodule provides predefined image-related models and its solvers that can be reused
13+
by many image processing tasks.
14+
15+
- solve the Rudin Osher Fatemi (ROF) model using the primal-dual method: [`solve_ROF_PD`](@ref)
16+
"""
17+
Models
18+
19+
export solve_ROF_PD
20+
21+
22+
##### implementation details
23+
24+
"""
25+
solve_ROF_PD(img::AbstractArray, λ; kwargs...)
26+
27+
Perform Rudin-Osher-Fatemi (ROF) filtering, more commonly known as Total Variation (TV)
28+
denoising or TV regularization.
29+
30+
This function applies to generic n-dimensional colorant array and is also CUDA-compatible.
31+
32+
# Arguments
33+
34+
- `img`: the input image, usually is a noisy image.
35+
- `λ`: the regularization coefficient. Larger `λ` would produce more smooth image.
36+
37+
# Parameters
38+
39+
- `num_iters::Int`: The number of iterations before stopping.
40+
41+
# Examples
42+
43+
```julia
44+
using ImageFiltering
45+
using ImageFiltering.Models: solve_ROF_PD
46+
using ImageQualityIndexes
47+
using TestImages
48+
49+
img_ori = float.(testimage("cameraman"))
50+
img_noisy = img_ori .+ 0.1 .* randn(size(img_ori))
51+
assess_psnr(img_noisy, img_ori) # ~20 dB
52+
53+
img_smoothed = solve_ROF_PD(img_noisy, 0.015, 50)
54+
assess_psnr(img_smoothed, img_ori) # ~27 dB
55+
56+
# larger λ produces over-smoothed result
57+
img_smoothed = solve_ROF_PD(img_noisy, 5, 50)
58+
assess_psnr(img_smoothed, img_ori) # ~21 dB
59+
```
60+
61+
# Extended help
62+
63+
Mathematically, this function solves the following ROF model using the primal-dual method:
64+
65+
```math
66+
\\min_u \\lVert u - g \\rVert^2 + \\lambda\\lvert\\nabla u\\rvert
67+
```
68+
69+
# References
70+
71+
- [1] Chambolle, A. (2004). "An algorithm for total variation minimization and applications". _Journal of Mathematical Imaging and Vision_. 20: 89–97
72+
- [2] https://en.wikipedia.org/wiki/Total_variation_denoising
73+
"""
74+
function solve_ROF_PD(img::AbstractArray, λ::Real, num_iters::Integer)
75+
# Total Variation regularized image denoising using the primal dual algorithm
76+
# Implement according to reference [1]
77+
τ = 1/4 # see 2nd remark after proof of Theorem 3.1.
78+
79+
g = of_eltype(floattype(eltype(img)), img) # use the same symbol in the paper
80+
u = similar(g)
81+
p = fgradient(g)
82+
div_p = similar(g)
83+
∇u = map(similar, p)
84+
∇u_mag = similar(g, eltype(eltype(g)))
85+
86+
# This iterates Eq. (9) of [1]
87+
# TODO(johnnychen94): set better stop criterion
88+
for _ in 1:num_iters
89+
fdiv!(div_p, p)
90+
# multiply term inside ∇ by -λ. Thm. 3.1 relates this to `u` via Eq. 7.
91+
@. u = g - λ*div_p
92+
fgradient!(∇u, u)
93+
_l2norm_vec!(∇u_mag, ∇u) # |∇(g - λdiv p)|
94+
# Eq. (9): update p
95+
for i in 1:length(p)
96+
@. p[i] = (p[i] -/λ)*∇u[i])/(1 +/λ) * ∇u_mag)
97+
end
98+
end
99+
return u
100+
end
101+
102+
103+
function _l2norm_vec!(out, Vs::Tuple)
104+
all(v->axes(out) == axes(v), Vs) || throw(ArgumentError("All axes of input data should be the same."))
105+
@. out = abs2(Vs[1])
106+
for v in Vs[2:end]
107+
@. out += abs2(v)
108+
end
109+
@. out = sqrt(out)
110+
return out
111+
end
112+
113+
114+
end # module

test/cuda/Project.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[deps]
2+
CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba"
3+
ImageBase = "c817782e-172a-44cc-b673-b171935fbb9e"
4+
ImageFiltering = "6a3955dd-da59-5b1f-98d4-e7296123deb5"
5+
ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19"
6+
ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1"
7+
ImageQualityIndexes = "2996bd0c-7a13-11e9-2da2-2f5ce47296a9"
8+
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
9+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
10+
TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990"

test/cuda/models.jl

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using ImageFiltering.Models
2+
3+
@testset "solve_ROF_PD" begin
4+
# This testset is modified from its CPU version
5+
6+
@testset "Numerical" begin
7+
# 2D Gray
8+
img = restrict(testimage("cameraman"))
9+
img_noisy = img .+ 0.05randn(MersenneTwister(0), size(img))
10+
img_smoothed = solve_ROF_PD(img_noisy, 0.05, 20)
11+
@test ndims(img_smoothed) == 2
12+
@test eltype(img_smoothed) <: Gray
13+
@test assess_psnr(img_smoothed, img) > 31.67
14+
@test assess_ssim(img_smoothed, img) > 0.90
15+
16+
img_noisy_cu = CuArray(float32.(img_noisy))
17+
img_smoothed_cu = solve_ROF_PD(img_noisy_cu, 0.05, 20)
18+
@test img_smoothed_cu isa CuArray
19+
@test eltype(eltype(img_smoothed_cu)) == Float32
20+
@test Array(img_smoothed_cu) img_smoothed
21+
22+
# 2D RGB
23+
img = restrict(testimage("lighthouse"))
24+
img_noisy = img .+ colorview(RGB, ntuple(i->0.05.*randn(MersenneTwister(i), size(img)), 3)...)
25+
img_smoothed = solve_ROF_PD(img_noisy, 0.03, 20)
26+
@test ndims(img_smoothed) == 2
27+
@test eltype(img_smoothed) <: RGB
28+
@test assess_psnr(img_smoothed, img) > 32.15
29+
@test assess_ssim(img_smoothed, img) > 0.90
30+
31+
img_noisy_cu = CuArray(float32.(img_noisy))
32+
img_smoothed_cu = solve_ROF_PD(img_noisy_cu, 0.03, 20)
33+
@test img_smoothed_cu isa CuArray
34+
@test eltype(eltype(img_smoothed_cu)) == Float32
35+
@test Array(img_smoothed_cu) img_smoothed
36+
37+
# 3D Gray
38+
img = restrict(testimage("mri"), (1, 2))
39+
img_noisy = img .+ 0.05randn(MersenneTwister(0), size(img))
40+
img_smoothed = solve_ROF_PD(img_noisy, 0.02, 20)
41+
@test ndims(img_smoothed) == 3
42+
@test eltype(img_smoothed) <: Gray
43+
@test assess_psnr(img_smoothed, img) > 31.78
44+
@test assess_ssim(img_smoothed, img) > 0.85
45+
46+
img_noisy_cu = CuArray(float32.(img_noisy))
47+
img_smoothed_cu = solve_ROF_PD(img_noisy_cu, 0.02, 20)
48+
@test img_smoothed_cu isa CuArray
49+
@test eltype(eltype(img_smoothed_cu)) == Float32
50+
@test Array(img_smoothed_cu) img_smoothed
51+
52+
# 3D RGB
53+
img = RGB.(restrict(testimage("mri"), (1, 2)))
54+
img_noisy = img .+ colorview(RGB, ntuple(i->0.05.*randn(MersenneTwister(i), size(img)), 3)...)
55+
img_smoothed = solve_ROF_PD(img_noisy, 0.02, 20)
56+
@test ndims(img_smoothed) == 3
57+
@test eltype(img_smoothed) <: RGB
58+
@test assess_psnr(img_smoothed, img) > 31.17
59+
@test assess_ssim(img_smoothed, img) > 0.79
60+
61+
img_noisy_cu = CuArray(float32.(img_noisy))
62+
img_smoothed_cu = solve_ROF_PD(img_noisy_cu, 0.02, 20)
63+
@test img_smoothed_cu isa CuArray
64+
@test eltype(eltype(img_smoothed_cu)) == Float32
65+
@test Array(img_smoothed_cu) img_smoothed
66+
end
67+
end

test/cuda/runtests.jl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# This file is maintained in a way to support CUDA-only test via
2+
# `julia --project=test/cuda -e 'include("runtests.jl")'`
3+
using ImageFiltering
4+
using CUDA
5+
using TestImages
6+
using ImageBase
7+
using ImageQualityIndexes
8+
using Test
9+
using Random
10+
11+
CUDA.allowscalar(false)
12+
13+
@testset "ImageFiltering" begin
14+
if CUDA.functional()
15+
include("models.jl")
16+
end
17+
end

test/models.jl

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using ImageFiltering.Models
2+
3+
@testset "solve_ROF_PD" begin
4+
# Note: random seed really matters a lot
5+
6+
@testset "Numerical" begin
7+
# 2D Gray
8+
img = restrict(testimage("cameraman"))
9+
img_noisy = img .+ 0.05randn(MersenneTwister(0), size(img))
10+
img_smoothed = solve_ROF_PD(img_noisy, 0.05, 20)
11+
@test ndims(img_smoothed) == 2
12+
@test eltype(img_smoothed) <: Gray
13+
@test assess_psnr(img_smoothed, img) > 31.67
14+
@test assess_ssim(img_smoothed, img) > 0.90
15+
16+
# 2D RGB
17+
img = restrict(testimage("lighthouse"))
18+
img_noisy = img .+ colorview(RGB, ntuple(i->0.05.*randn(MersenneTwister(i), size(img)), 3)...)
19+
img_smoothed = solve_ROF_PD(img_noisy, 0.03, 20)
20+
@test ndims(img_smoothed) == 2
21+
@test eltype(img_smoothed) <: RGB
22+
@test assess_psnr(img_smoothed, img) > 32.15
23+
@test assess_ssim(img_smoothed, img) > 0.90
24+
25+
# 3D Gray
26+
img = restrict(testimage("mri"), (1, 2))
27+
img_noisy = img .+ 0.05randn(MersenneTwister(0), size(img))
28+
img_smoothed = solve_ROF_PD(img_noisy, 0.02, 20)
29+
@test ndims(img_smoothed) == 3
30+
@test eltype(img_smoothed) <: Gray
31+
@test assess_psnr(img_smoothed, img) > 31.78
32+
@test assess_ssim(img_smoothed, img) > 0.85
33+
34+
# 3D RGB
35+
img = RGB.(restrict(testimage("mri"), (1, 2)))
36+
img_noisy = img .+ colorview(RGB, ntuple(i->0.05.*randn(MersenneTwister(i), size(img)), 3)...)
37+
img_smoothed = solve_ROF_PD(img_noisy, 0.02, 20)
38+
@test ndims(img_smoothed) == 3
39+
@test eltype(img_smoothed) <: RGB
40+
@test assess_psnr(img_smoothed, img) > 31.17
41+
@test assess_ssim(img_smoothed, img) > 0.79
42+
end
43+
44+
@testset "FixedPointNumbers" begin
45+
A = rand(N0f8, 20, 20)
46+
@test solve_ROF_PD(A, 0.01, 5) solve_ROF_PD(float32.(A), 0.01, 5)
47+
end
48+
49+
@testset "OffsetArray" begin
50+
Ao = OffsetArray(rand(N0f8, 20, 20), -1, -1)
51+
out = solve_ROF_PD(Ao, 0.01, 5)
52+
@test axes(out) == axes(Ao)
53+
end
54+
end

test/runtests.jl

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
using ImageFiltering, ImageCore
1+
using ImageFiltering, ImageCore, ImageBase
2+
using OffsetArrays
23
using Test
4+
using TestImages
5+
using ImageQualityIndexes
36
import StaticArrays
47
using Random
58

@@ -31,5 +34,30 @@ include("mapwindow.jl")
3134
include("extrema.jl")
3235
include("basic.jl")
3336
include("gabor.jl")
37+
include("models.jl")
3438

39+
40+
CUDA_INSTALLED = false
41+
try
42+
global CUDA_INSTALLED
43+
# This errors with `IOError` when nvidia driver is not available,
44+
# in which case we don't even need to try `using CUDA`
45+
run(pipeline(`nvidia-smi`, stdout=devnull, stderr=devnull))
46+
push!(LOAD_PATH, "@v#.#") # force using global CUDA installation
47+
48+
@eval using CUDA
49+
CUDA.allowscalar(false)
50+
CUDA_INSTALLED = true
51+
catch e
52+
e isa Base.IOError || @warn e LOAD_PATH
53+
end
54+
CUDA_FUNCTIONAL = CUDA_INSTALLED && CUDA.functional()
55+
if CUDA_FUNCTIONAL
56+
@info "CUDA test: enabled"
57+
@testset "CUDA" begin
58+
include("cuda/runtests.jl")
59+
end
60+
else
61+
@warn "CUDA test: disabled"
62+
end
3563
nothing

0 commit comments

Comments
 (0)