-
Notifications
You must be signed in to change notification settings - Fork 0
/
stackimages
executable file
·324 lines (290 loc) · 11.7 KB
/
stackimages
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
#!/usr/bin/env python
"""
Produces the median of a set of input images after shifting them so that
the largest bright feature is coaligned.
Usage:
stackimages [-b] [-e P] OUTFN INFNS...
stackimages [-a] [-c] [-e P] -r RMEAN ROUT INFNS...
stackimages (-h | --help)
stackimages (-v | --version)
Arguments:
OUTFN Filename for the output
RMEAN Filename for the output average image
ROUT Filename for the output outliers image
INFNS Input filenames
Options:
-b --bkgd (NOT YET IMPLEMENTED) Align the background instead of the
largest feature.
-e P --exif=P Look for Exif metadata in the files matching filename
pattern P instead of in INFNS. Useful when INFNS is a
set of TIFFs remapped by hugin from a set of JPEGs matching
P. Use quoting to stop the shell from expanding P.
-r --routliers Instead of aligning the images, produce robust estimates of
their average and outliers. Alignment can be done ahead of
time with hugin (select write remapped images in the
stitcher).
-a --align Force aligning to the largest feature even when using -r.
Not recommended; use hugin or its align_image_stack instead.
-c --colors Weight each color seperately when using -r. This often
produces strange color casts.
-h --help Show this and exit.
-v --version Print version info and exit.
"""
from __future__ import absolute_import, print_function
from glob import glob
import numpy as np
import pyexiv2
from scipy import misc
from scipy import ndimage as ndi
import sys
import ttystatus
def getProgressBar(n, title=' Progress ', period=0.1, counter='x'):
ts = ttystatus.TerminalStatus(period=period)
ts.add(ttystatus.ElapsedTime())
ts.add(ttystatus.Literal(title))
ts.clear()
ts.add(ttystatus.ElapsedTime())
ts.add(ttystatus.Literal(' '))
ts.add(ttystatus.Counter(counter))
ts.add(ttystatus.Literal(' of %d ' % n))
ts.add(ttystatus.PercentDone('done', 'total', decimals=2))
ts.add(ttystatus.Literal(' done '))
ts.add(ttystatus.RemainingTime('done', 'total'))
ts.add(ttystatus.Literal(' '))
ts.add(ttystatus.ProgressBar('done', 'total'))
ts['done'] = 0
ts['total'] = n
return ts
def read_imstack(fnlist):
"""
Given a list of image filenames, return a list of numpy arrays for
the images.
"""
return [misc.imread(fn) for fn in fnlist]
def cent_of_main_feature(img, medrad=(5,5,1), thresh=32):
"""
Return the center of the main feature in img.
"""
med = ndi.median_filter(img.astype(float), size=medrad, mode='constant')
med = 1.0 / (1.0 + np.exp(thresh - med))
return ndi.center_of_mass(med)
def align_imstack(instack):
"""
Given a list of images of the same size and dimensionality, return
a list of images that have been cropped and shifted to the nearest pixel.
"""
nim = len(instack)
ts = getProgressBar(nim, ' Getting centroids: ')
wi, hi = instack[0].shape[:2]
cms = []
for i in range(nim):
ts['x'] = i
cms.append(cent_of_main_feature(instack[i]))
ts['done'] += 1
cms = np.array(cms)
print("\ncenters of mass:")
print(cms)
cms -= np.mean(cms, axis=0)
#TODO: Account for color shifts.
print("Nominal shifts:")
print(cms)
cms = np.round(cms).astype(np.int16)
mincx = min(cms[:, 0])
mincy = min(cms[:, 1])
wo = wi - max(cms[:, 0]) + mincx
ho = hi - max(cms[:, 1]) + mincy
outstack = np.zeros((nim, wo, ho, 3), instack[0].dtype)
for i in range(nim):
d = cms[i, :2]
xstart = d[0] - mincx
xstop = xstart + wo
ystart = d[1] - mincy
ystop = ystart + ho
inim = instack[i]
outstack[i] = inim[xstart:xstop, ystart:ystop, :]
return outstack
def med_filt_fnlist(fnlist):
return np.median(align_imstack(read_imstack(fnlist)), axis=0)
def robust_mean_and_outliers(aligned_stack, center_of_stack=None, wexp=4,
scale=32.0, softener=0.5, bindcolors=True):
"""
Calculates a weighted mean and antimean of aligned_stack.
Parameters
----------
aligned_stack: list of images
The images can be a mix of grayscale m x n arrays or RGB m x n x 3 arrays,
but they must be aligned.
center_of_stack: m x n or m x n x 3 array or None
An array representing the nominal center of the stack.
If None np.median(aligned_stack, axis=0) will be used.
wexp: float
Exponent to use for the weighting of the mean and antimean.
The higher it is the more the outliers will be dominated by the intensities
farthest from center_of_stack's, and the closer the mean will be to a simple
average of intensities in the neighborhood of center_of_stack.
scale: float
The scale of the intensity difference separating outliers from the main
distribution.
softener: float > 0
This reduces the noise amplification in routliers by mixing some of rmean into
it, especially when the outlier amplitude is < scale. It also avoids a 0/0 in
the estimate when a pixel's value is the same for all images, which can happen
since the pixel values are integers.
bindcolors: bool
If False, colors are weighted separately, possibly producing odd results.
Returns
-------
rmean: np.average(aligned_stack, weights=1.0/(1.0 + z**wexp)),
where z = |im - center| / scale
routliers: np.average(aligned_stack,
weights=(softener**2 + (z**wexp)**2)**0.5/(1.0 + z**wexp))
"""
ni = len(aligned_stack)
nx, ny, nc = aligned_stack[0].shape
if not nc % 2: # Assume the last channel is alpha, i.e. RGBA
nc -= 1
mask_by_alpha = True
else:
mask_by_alpha = False
# Memory problems...memmap helps but is still thrashy when the stack is
# processed in toto. The problem requires handling the ni axis in one go,
# but nx, ny, and/or nc can be split.
# stack = np.memmap('stack.mmap', dtype=np.uint8, mode='w+',
# shape=(ni, ny, nc))
# del stack
# stack = np.memmap('stack.mmap', dtype=np.uint8, mode='r+',
# shape=(ni, ny, nc))
ts = getProgressBar(nx, ' Initializing arrays: ')
#cos = np.empty((ny, nc))
rmean = np.empty((nx, ny, nc), dtype=np.uint8)
routliers = np.empty((nx, ny, nc), dtype=np.uint8)
stsl = np.empty((ni, ny, nc), dtype=np.uint8)
zwexp = np.empty((ni, ny, nc))
if mask_by_alpha:
#rmean = np.ma.masked_array(rmean)
#routliers = np.ma.masked_array(routliers)
stsl = np.ma.masked_array(stsl)
stsl.mask = np.zeros_like(stsl)
zwexp = np.ma.masked_array(zwexp)
avg = np.ma.average
else:
avg = np.average
s2 = softener**2
for xi in range(nx):
ts['x'] = xi
for ind in range(ni):
# The :nc is needed when masking by alpha, and harmless otherwise.
stsl[ind] = aligned_stack[ind][xi, :, :nc]
if center_of_stack is None:
if mask_by_alpha:
for ind in range(ni):
stsl.mask[ind, :, 0] = aligned_stack[ind][xi, :, nc] == 0
for ci in range(1, nc): # Broadcast to the other colors
stsl.mask[ind, :, ci] = stsl.mask[ind, :, 0]
cos = np.ma.median(stsl, axis=0) # Normal median does NOT work.
else:
cos = np.median(stsl, axis=0)
else:
cos = center_of_stack[xi]
for ind in range(ni):
zwexp[ind] = ((stsl[ind] - cos) / scale)**wexp
if bindcolors:
avz = np.mean(zwexp, axis=-1)
# This aggravatingly more than doubles the runtime, but unfortunately it's
# probably inevitable. See
# http://www.socouldanyone.com/2013/03/converting-grayscale-to-rgb-with-numpy.html
#zwexp[:,:,:] = avz[:, :, np.newaxis]
for ci in range(nc):
zwexp[..., ci] = avz
w = 1.0/(1.0 + zwexp)
#sw = np.sum(w, axis=0)
#w[sw == 0] = 1.0
rmean[xi] = np.round(avg(stsl, weights=w, axis=0)).astype(np.uint8)
w = (zwexp**2 + s2)**0.5
routliers[xi] = np.round(avg(stsl, weights=w, axis=0)).astype(np.uint8)
ts['done'] += 1
print() # The progress bar needs a \n at the end.
return rmean, routliers
def summarize_exif(fnlist, outfn, comment=""):
"""
Merge the EXIF tags of the images in fnlist, add a comment saying
what was done, and write the result to outfn. outfn must already exist.
"""
out = pyexiv2.ImageMetadata(outfn)
out.read()
mid = pyexiv2.ImageMetadata(fnlist[len(fnlist) // 2])
mid.read()
# Does not work and will crash ipython!
#out.update(mid)
for k, v in mid.items():
try:
out[k] = v.value # Use value instead of raw_value.
except:
print(k, "could not be copied.")
times = []
isos = []
exps = []
fnumbers = []
foci = []
for f in fnlist:
metadata = pyexiv2.ImageMetadata(f)
metadata.read()
times.append(metadata['Exif.Photo.DateTimeOriginal'].value)
isos.append(metadata['Exif.Photo.ISOSpeedRatings'].value)
exps.append(metadata['Exif.Photo.ExposureTime'].value)
fnumbers.append(metadata['Exif.Photo.FNumber'].value)
foci.append(metadata['Exif.Photo.FocalLength'].value)
if not comment:
comment = "Median of " + ", ".join(fnlist) + "\n"
if len(set([t.date() for t in times])) == 1:
comment += "Date: %s\n" % times[0].strftime("%Y-%m-%d")
comment += "Times: " + ", ".join([t.strftime("%H:%M:%S")
for t in times]) + "\n"
else:
comment += "Times: " + ", ".join([t.strftime("%Y-%m-%d %H:%M:%S")
for t in times]) + "\n"
es = []
for e in exps:
if e.denominator > 1:
es.append("%d/%d" % (e.numerator, e.denominator))
else:
es.append("%d" % e.numerator)
comment += "Exposures: " + ", ".join(es) + "s\n"
comment += "ISOs: " + ", ".join(map(str, isos)) + "\n"
if len(set(fnumbers)) == 1:
comment += "f stop: %.1f\n" % fnumbers[0]
else:
comment += "f stops: " + ", ".join(["%.1f" % fstop
for fstop in fnumbers]) + "\n"
if len(set(foci)) == 1:
comment += "Focal length: %.2f mm\n" % foci[0]
else:
comment += "Focal lengths: " + ", ".join(["%.2f" % flength
for flength in foci])
out['Exif.Photo.UserComment'] = comment
out.write()
return out
if __name__ == '__main__':
from docopt import docopt
args = docopt(__doc__, version="1.3.0")
if args['--exif']:
exiffns = []
for pat in args['--exif'].split():
exiffns += glob(pat)
else:
exiffns = args['INFNS']
comment = "Made with " + " ".join(sys.argv) + "\n"
if args['--routliers']:
stack = read_imstack(args['INFNS'])
if args['--align']:
stack = align_imstack(stack)
rmean, routliers = robust_mean_and_outliers(stack,
bindcolors=not args['--colors'])
misc.imsave(args['RMEAN'], rmean)
misc.imsave(args['ROUT'], routliers)
summarize_exif(exiffns, args['RMEAN'], comment)
summarize_exif(exiffns, args['ROUT'], comment)
else:
medim = med_filt_fnlist(args['INFNS'])
misc.imsave(args['OUTFN'], medim)
summarize_exif(exiffns, args['OUTFN'], comment)