Skip to content

Commit 5a85f8e

Browse files
committed
implement generic ROF model using Chambolle04 primal-dual method
1 parent a86e651 commit 5a85f8e

File tree

5 files changed

+176
-1
lines changed

5 files changed

+176
-1
lines changed

Project.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
1717
ImageFiltering = "6a3955dd-da59-5b1f-98d4-e7296123deb5"
1818
ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19"
1919
ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1"
20+
ImageQualityIndexes = "2996bd0c-7a13-11e9-2da2-2f5ce47296a9"
2021
OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
22+
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
2123
StackViews = "cae243ae-269e-4f55-b966-ac2d0dc13c15"
2224
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
2325
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
2426
TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990"
2527

2628
[targets]
27-
test = ["Aqua", "Documenter", "Test", "ImageFiltering", "ImageIO", "ImageMagick", "OffsetArrays", "Statistics", "StackViews", "TestImages"]
29+
test = ["Aqua", "Documenter", "Test", "ImageFiltering", "ImageIO", "ImageMagick", "ImageQualityIndexes", "OffsetArrays", "Random", "Statistics", "StackViews", "TestImages"]

src/ImageBase.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ include("diff.jl")
2626
include("restrict.jl")
2727
include("utils.jl")
2828
include("statistics.jl")
29+
30+
include("models.jl")
31+
2932
include("compat.jl")
3033
include("deprecated.jl")
3134

src/models.jl

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
module Models
2+
3+
using ImageCore
4+
using ImageCore.MappedArrays: of_eltype
5+
using ..FiniteDiff
6+
7+
# Introduced in ColorVectorSpace v0.9.3
8+
# https://github.com/JuliaGraphics/ColorVectorSpace.jl/pull/172
9+
using 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.
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 ImageBase
45+
using ImageBase.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, λ::Number, iterations::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:iterations
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+
return out
110+
end
111+
112+
113+
end # module

test/models.jl

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using ImageBase.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.01, 20)
11+
@test ndims(img_smoothed) == 2
12+
@test eltype(img_smoothed) <: Gray
13+
@test assess_psnr(img_smoothed, img) > 28.70
14+
@test assess_ssim(img_smoothed, img) > 0.845
15+
16+
# 2D RGB
17+
img = restrict(testimage("mandril"))
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.001, 20)
20+
@test ndims(img_smoothed) == 2
21+
@test eltype(img_smoothed) <: RGB
22+
@test assess_psnr(img_smoothed, img) > 28.82
23+
@test assess_ssim(img_smoothed, img) > 0.849
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.005, 20)
29+
@test ndims(img_smoothed) == 3
30+
@test eltype(img_smoothed) <: Gray
31+
@test assess_psnr(img_smoothed, img) > 27.92
32+
@test assess_ssim(img_smoothed, img) > 0.66
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.001, 20)
38+
@test ndims(img_smoothed) == 3
39+
@test eltype(img_smoothed) <: RGB
40+
@test assess_psnr(img_smoothed, img) > 30.73
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(A, -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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using ImageBase, OffsetArrays, StackViews
22
using ImageFiltering
3+
using ImageQualityIndexes
34
using Test, TestImages, Aqua, Documenter
5+
using Random
46

57
using OffsetArrays: IdentityUnitRange
68
include("testutils.jl")
@@ -24,6 +26,7 @@ include("testutils.jl")
2426
include("diff.jl")
2527
include("restrict.jl")
2628
include("statistics.jl")
29+
include("models.jl")
2730

2831
@info "deprecations are expected"
2932
include("deprecated.jl")

0 commit comments

Comments
 (0)