1 ################### |
|
2 # Image generation |
|
3 ################### |
|
4 |
|
5 module ImGenerate |
|
6 |
|
7 using ColorTypes: Gray |
|
8 import TestImages |
|
9 # We don't really *directly* depend on QuartzImageIO. The import here is |
|
10 # merely a workaround to suppress warnings when loading TestImages. |
|
11 # Something is broken in those packages. |
|
12 import QuartzImageIO |
|
13 |
|
14 using AlgTools.Util |
|
15 using AlgTools.Comms |
|
16 using ImageTools.Translate |
|
17 |
|
18 using ..OpticalFlow: Image, DisplacementConstant, DisplacementFull |
|
19 using ..Radon |
|
20 |
|
21 # Added for reproducibility |
|
22 import StableRNGs: StableRNG, Random |
|
23 const rng = StableRNG(314159) |
|
24 |
|
25 # Added for PET |
|
26 import PoissonRandom: pois_rand |
|
27 import Random: shuffle |
|
28 import Images: center, warp, imresize |
|
29 import CoordinateTransformations: recenter |
|
30 import Rotations: RotMatrix |
|
31 import Interpolations: Flat |
|
32 import MAT: matread |
|
33 |
|
34 |
|
35 ############## |
|
36 # Our exports |
|
37 ############## |
|
38 |
|
39 export ImGen, |
|
40 OnlineData, |
|
41 imgen_square, |
|
42 imgen_shake, |
|
43 PetOnlineData, |
|
44 imgen_shepplogan_radon, |
|
45 imgen_brainphantom_radon |
|
46 |
|
47 ################## |
|
48 # Data structures |
|
49 ################## |
|
50 |
|
51 struct ImGen |
|
52 f :: Function |
|
53 dim :: Tuple{Int64,Int64} |
|
54 Λ :: Float64 |
|
55 dynrange :: Float64 |
|
56 name :: String |
|
57 end |
|
58 |
|
59 struct OnlineData{DisplacementT} |
|
60 b_true :: Image |
|
61 b_noisy :: Image |
|
62 v :: DisplacementT |
|
63 v_true :: DisplacementT |
|
64 v_cumul_true :: DisplacementT |
|
65 end |
|
66 |
|
67 struct PetOnlineData{DisplacementT} |
|
68 b_true :: Image |
|
69 sinogram_true :: Image |
|
70 sinogram_noisy :: Image |
|
71 v :: DisplacementT |
|
72 v_true :: DisplacementT |
|
73 v_cumul_true :: DisplacementT |
|
74 theta :: DisplacementT # theta = thetaknown, theta_cumul |
|
75 S :: Image |
|
76 end |
|
77 |
|
78 ################### |
|
79 # Shake generation |
|
80 ################### |
|
81 |
|
82 function make_const_v(displ, sz) |
|
83 v = zeros(2, sz...) |
|
84 v[1, :, :] .= displ[1] |
|
85 v[2, :, :] .= displ[2] |
|
86 return v |
|
87 end |
|
88 |
|
89 function shake(params) |
|
90 if !haskey(params, :shaketype) || params.shaketype == :gaussian |
|
91 return () -> params.shake.*randn(rng,2) |
|
92 elseif params.shaketype == :disk |
|
93 return () -> begin |
|
94 θ = 2π*rand(rng,Float64) |
|
95 r = params.shake*√(rand(rng,Float64)) |
|
96 return [r*cos(θ), r*sin(θ)] |
|
97 end |
|
98 elseif params.shaketype == :circle |
|
99 return () -> begin |
|
100 θ = 2π*rand(rng,Float64) |
|
101 r = params.shake |
|
102 return [r*cos(θ), r*sin(θ)] |
|
103 end |
|
104 else |
|
105 error("Unknown shaketype $(params.shaketype)") |
|
106 end |
|
107 end |
|
108 |
|
109 pixelwise = (shakefn, sz) -> () -> make_const_u(shakefn(), sz) |
|
110 |
|
111 |
|
112 function rotatebytheta(params) |
|
113 r = params.rotation_factor*randn(rng) |
|
114 return r |
|
115 end |
|
116 |
|
117 function generate_radonmask(params) |
|
118 imdim = params.radondims |
|
119 sino_sparse = params.sino_sparsity |
|
120 numzero = Int64(round(sino_sparse*imdim[1]*imdim[2])) |
|
121 numone = imdim[1]*imdim[2]-numzero |
|
122 A = shuffle(rng,reshape([ones(numone); zeros(numzero)],(imdim[1],imdim[2]))) |
|
123 return A |
|
124 end |
|
125 |
|
126 ################ |
|
127 # Moving square |
|
128 ################ |
|
129 |
|
130 function generate_square(sz, |
|
131 :: Type{DisplacementT}, |
|
132 datachannel :: Channel{OnlineData{DisplacementT}}, |
|
133 params) where DisplacementT |
|
134 |
|
135 if false |
|
136 v₀ = make_const_v(0.1.*(-1, 1), sz) |
|
137 nextv = () -> v₀ |
|
138 elseif DisplacementT == DisplacementFull |
|
139 nextv = pixelwise(shake(params), sz) |
|
140 elseif DisplacementT == DisplacementConstant |
|
141 nextv = shake(params) |
|
142 else |
|
143 @error "Invalid DisplacementT" |
|
144 end |
|
145 |
|
146 # Constant linear displacement everywhere has Jacobian determinant one |
|
147 # (modulo the boundaries which we ignore here) |
|
148 m = round(Int, sz[1]/5) |
|
149 b_orig = zeros(sz...) |
|
150 b_orig[sz[1].-(2*m:3*m), 2*m:3*m] .= 1 |
|
151 |
|
152 v_true = nextv() |
|
153 v_cumul = copy(v_true) |
|
154 |
|
155 while true |
|
156 # Flow original data and add noise |
|
157 b_true = zeros(sz...) |
|
158 translate_image!(b_true, b_orig, v_cumul; threads=true) |
|
159 b = b_true .+ params.noise_level.*randn(rng,sz...) |
|
160 v = v_true.*(1.0 .+ params.shake_noise_level.*randn(rng,size(v_true)...)) |
|
161 # Pass true data to iteration routine |
|
162 data = OnlineData{DisplacementT}(b_true, b, v, v_true, v_cumul) |
|
163 if !put_unless_closed!(datachannel, data) |
|
164 return |
|
165 end |
|
166 # Next step shake |
|
167 v_true = nextv() |
|
168 v_cumul .+= v_true |
|
169 end |
|
170 end |
|
171 |
|
172 function imgen_square(sz) |
|
173 return ImGen(curry(generate_square, sz), sz, 1, 1, "square$(sz[1])x$(sz[2])") |
|
174 end |
|
175 |
|
176 ################ |
|
177 # Shake a photo |
|
178 ################ |
|
179 |
|
180 function generate_shake_image(im, sz, |
|
181 :: Type{DisplacementConstant}, |
|
182 datachannel :: Channel{OnlineData{DisplacementConstant}}, |
|
183 params :: NamedTuple) |
|
184 |
|
185 |
|
186 # Set up counter and zero factor for stabilisation |
|
187 @assert(params.maxiter ≥ maximum(params.stable_interval)) |
|
188 indx = 1 |
|
189 zero_factor = indx in params.stable_interval ? 0.0 : 1.0 |
|
190 |
|
191 # Restart the seed to enable comparison across predictors |
|
192 Random.seed!(rng,314159) |
|
193 |
|
194 |
|
195 nextv = shake(params) |
|
196 v_true = zero_factor.*nextv() |
|
197 v_cumul = copy(v_true) |
|
198 |
|
199 while true |
|
200 # Extract subwindow of original image and add noise |
|
201 b_true = zeros(sz...) |
|
202 extract_subimage!(b_true, im, v_cumul; threads=true) |
|
203 b = b_true .+ params.noise_level.*randn(rng,sz...) |
|
204 v = v_true.*(1.0 .+ params.shake_noise_level.*randn(rng,size(v_true)...)) |
|
205 # Pass data to iteration routine |
|
206 data = OnlineData{DisplacementConstant}(b_true, b, v, v_true, v_cumul) |
|
207 if !put_unless_closed!(datachannel, data) |
|
208 return |
|
209 end |
|
210 # Next step shake |
|
211 v_true = zero_factor.*nextv() |
|
212 v_cumul .+= v_true |
|
213 # Update indx and zero factor |
|
214 indx += 1 |
|
215 zero_factor = indx in params.stable_interval ? 0.0 : 1.0 |
|
216 end |
|
217 end |
|
218 |
|
219 function imgen_shake(imname, sz) |
|
220 im = Float64.(Gray.(TestImages.testimage(imname))) |
|
221 dynrange = maximum(im) |
|
222 return ImGen(curry(generate_shake_image, im, sz), sz, 1, dynrange, |
|
223 "$(imname)$(sz[1])x$(sz[2])") |
|
224 end |
|
225 |
|
226 |
|
227 |
|
228 ######################################################################## |
|
229 # PETscan |
|
230 ######################################################################## |
|
231 function generate_sinogram(im, sz, |
|
232 :: Type{DisplacementConstant}, |
|
233 datachannel :: Channel{PetOnlineData{DisplacementConstant}}, |
|
234 params :: NamedTuple) |
|
235 |
|
236 # Set up counter and zero factor for stabilisation |
|
237 @assert(params.maxiter ≥ maximum(params.stable_interval)) |
|
238 indx = 1 |
|
239 zero_factor = indx in params.stable_interval ? 0.0 : 1.0 |
|
240 |
|
241 # Restart the seed to enable comparison across predictors |
|
242 Random.seed!(rng,314159) |
|
243 |
|
244 nextv = shake(params) |
|
245 v_true = zero_factor.*nextv() |
|
246 v_cumul = copy(v_true) |
|
247 |
|
248 S_true = generate_radonmask(params) |
|
249 |
|
250 theta_true = zero_factor*rotatebytheta(params) |
|
251 theta_cumul = copy(theta_true) |
|
252 |
|
253 while true |
|
254 # Define the transformation matrix |
|
255 center_point = (sz[1]/2 + v_true[1], sz[2]/2 + v_true[2]) |
|
256 tform = recenter(RotMatrix(theta_cumul), center_point) |
|
257 |
|
258 # Apply the transformation to the image using warp |
|
259 b_true = copy(warp(im, tform, axes(im), fillvalue=Flat())) |
|
260 |
|
261 v = v_true.*(1.0 .+ params.shake_noise_level.*randn(rng,size(v_true)...)) |
|
262 theta = theta_true*(1.0 + params.rotation_noise_level.*randn(rng)) |
|
263 |
|
264 # Generate the true and noisy sinograms |
|
265 sinogram_true = zeros(params.radondims...) |
|
266 sinogram_true .*= params.scale |
|
267 radon!(sinogram_true, b_true) |
|
268 sinogram_noisy = copy(sinogram_true) |
|
269 |
|
270 for i=1:params.radondims[1], j=1:params.radondims[2] |
|
271 sinogram_noisy[i, j] += pois_rand(rng,params.noise_level) |
|
272 end |
|
273 |
|
274 # Pass data to iteration routine |
|
275 data = PetOnlineData{DisplacementConstant}(b_true, sinogram_true, sinogram_noisy, v, v_true, v_cumul, [theta, theta_cumul], S_true) |
|
276 if !put_unless_closed!(datachannel, data) |
|
277 return |
|
278 end |
|
279 |
|
280 # Next step shake |
|
281 v_true = zero_factor.*nextv() |
|
282 v_cumul .+= v_true |
|
283 # Next theta |
|
284 theta_true = zero_factor*rotatebytheta(params) |
|
285 theta_cumul += theta_true |
|
286 # Next sinogram mask |
|
287 S_true = generate_radonmask(params) |
|
288 # Update indx and zero factor |
|
289 indx += 1 |
|
290 zero_factor = indx in params.stable_interval ? 0.0 : 1.0 |
|
291 end |
|
292 end |
|
293 |
|
294 function imgen_shepplogan_radon(sz) |
|
295 im = convert(Array{Float64},TestImages.shepp_logan(sz[1], highContrast=true)) |
|
296 dynrange = maximum(im) |
|
297 return ImGen(curry(generate_sinogram, im, sz), sz, 1, dynrange, "shepplogan$(sz[1])x$(sz[2])") |
|
298 end |
|
299 |
|
300 function imgen_brainphantom_radon(sz) |
|
301 data = matread("src/PET/phantom_slice.mat") |
|
302 im = normalise(imresize(convert(Array{Float64},data["square_data"]),sz)) |
|
303 dynrange = maximum(im) |
|
304 return ImGen(curry(generate_sinogram, im, sz), sz, 1, dynrange, "brainphantom$(sz[1])x$(sz[2])") |
|
305 end |
|
306 |
|
307 normalise = (data) -> data./maximum(data) |
|
308 end # Module |
|