forked from NLeSC/structure-from-motion
-
Notifications
You must be signed in to change notification settings - Fork 0
/
run-sfm.py
executable file
·774 lines (674 loc) · 36.1 KB
/
run-sfm.py
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
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
#!/usr/bin/python
# ###
# This RunSfM.py script is based on the bundler.py script distributed
# with bundler. The original licence can be found below:
# #### BEGIN LICENSE BLOCK ####
#
# bundler.py - Python convenience module for running Bundler.
# Copyright (C) 2013 Isaac Lenton (aka ilent2)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# #### END LICENSE BLOCK ####
import argparse
#import gzip
import os
import sys
#import Image
import glob
import subprocess
import tempfile
import fileinput
import shutil
import time
import multiprocessing
from multiprocessing.pool import ThreadPool
from PIL import Image, ExifTags
VERSION = "RunSfM 1.0"
DESCRIPTION = """\
Python convenience module to process a series of images and reconstruct
the scene using Bundler followed by CMVS/PMVS2.
Bundler is a structure-from-motion system for unordered image
collections (for instance, images from the Internet). Bundler takes a
set of images, image features, and image matches as input, and
produces a 3D reconstruction of the camera and (sparse) scene geometry
as output.
CMVS/PMVS is multi-view stereo software that takes a set of images and
camera parameters (generated by Bundler), and then reconstructs 3D structure
of an object or a scene visible in the images. The software outputs a dense
point cloud, that is, a set of oriented points where both the 3D coordinate
and the surface normal are estimated for each point."""
# This module replaces the existing RunBundler.sh script with a more
# cross platform implementation. Additional elements replaced:
# - RunBundler.sh 2008-2013 Noah Snavely
# - ToSift.sh
# - extract_focal.pl 2005-2009 Noah Snavely
# - jhead
MOD_PATH = os.path.dirname(__file__)
BUNDLER_BIN_PATH = os.path.join(MOD_PATH, "./bundler_sfm/bin")
BUNDLER_LIB_PATH = os.path.join(MOD_PATH, "./bundler_sfm/lib")
CMVS_PMVS_BIN_PATH = os.path.join(MOD_PATH, "./cmvs-pmvs/program/build/main")
BIN_SIFT = None
BIN_BUNDLER = None
BIN_MATCHKEYS = None
BIN_MATCHKEYS_PART = None
BIN_MATCHKEYS_FULL = None
BIN_BUNDLE2PMVS = None
BIN_RADIAL_UNDISTORT = None
BIN_CMVS = None
BIN_GEN_OPTION = None
BIN_PMVS2 = None
CCD_WIDTHS = {
"Asahi Optical Co.,Ltd. PENTAX Optio330RS" : 7.176, # 1/1.8"
"Canon Canon DIGITAL IXUS 400" : 7.176, # 1/1.8"
"Canon Canon DIGITAL IXUS 40" : 5.76, # 1/2.5"
"Canon Canon DIGITAL IXUS 430" : 7.176, # 1/1.8"
"Canon Canon DIGITAL IXUS 500" : 7.176, # 1/1.8"
"Canon Canon DIGITAL IXUS 50" : 5.76, # 1/2.5"
"Canon Canon DIGITAL IXUS 55" : 5.76, # 1/2.5"
"Canon Canon DIGITAL IXUS 60" : 5.76, # 1/2.5"
"Canon Canon DIGITAL IXUS 65" : 5.76, # 1/2.5"
"Canon Canon DIGITAL IXUS 700" : 7.176, # 1/1.8"
"Canon Canon DIGITAL IXUS 750" : 7.176, # 1/1.8"
"Canon Canon DIGITAL IXUS 800 IS" : 5.76, # 1/2.5"
"Canon Canon DIGITAL IXUS II" : 5.27, # 1/2.7"
"Canon Canon IXUS 240 HS" : 6.16, # 1/2.3"
"Canon Canon EOS 10D" : 22.7,
"Canon Canon EOS-1D Mark II" : 28.7,
"Canon Canon EOS-1Ds Mark II" : 35.95,
"Canon Canon EOS 20D" : 22.5,
"Canon Canon EOS 20D" : 22.5,
"Canon Canon EOS 300D DIGITAL" : 22.66,
"Canon Canon EOS 30D" : 22.5,
"Canon Canon EOS 350D DIGITAL" : 22.2,
"Canon Canon EOS 400D DIGITAL" : 22.2,
"Canon Canon EOS 40D" : 22.2,
"Canon Canon EOS 5D" : 35.8,
"Canon Canon EOS 5D Mark II" : 36.0,
"Canon Canon EOS 5D Mark III" : 36.0,
"Canon Canon EOS DIGITAL REBEL" : 22.66,
"Canon Canon EOS DIGITAL REBEL XT" : 22.2,
"Canon Canon EOS DIGITAL REBEL XTi" : 22.2,
"Canon Canon EOS Kiss Digital" : 22.66,
"Canon Canon EOS 1100D" : 22.2,
"Canon Canon IXY DIGITAL 600" : 7.176, # 1/1.8"
"Canon Canon PowerShot A20" : 7.176, # 1/1.8"
"Canon Canon PowerShot A400" : 4.54, # 1/3.2"
"Canon Canon PowerShot A40" : 5.27, # 1/2.7"
"Canon Canon PowerShot A510" : 5.76, # 1/2.5"
"Canon Canon PowerShot A520" : 5.76, # 1/2.5"
"Canon Canon PowerShot A530" : 5.76, # 1/2.5"
"Canon Canon PowerShot A60" : 5.27, # 1/2.7"
"Canon Canon PowerShot A620" : 7.176, # 1/1.8"
"Canon Canon PowerShot A630" : 7.176, # 1/1.8"
"Canon Canon PowerShot A640" : 7.176, # 1/1.8"
"Canon Canon PowerShot A700" : 5.76, # 1/2.5"
"Canon Canon PowerShot A70" : 5.27, # 1/2.7"
"Canon Canon PowerShot A710 IS" : 5.76, # 1/2.5"
"Canon Canon PowerShot A75" : 5.27, # 1/2.7"
"Canon Canon PowerShot A80" : 7.176, # 1/1.8"
"Canon Canon PowerShot A85" : 5.27, # 1/2.7"
"Canon Canon PowerShot A95" : 7.176, # 1/1.8"
"Canon Canon PowerShot G1" : 7.176, # 1/1.8"
"Canon Canon PowerShot G2" : 7.176, # 1/1.8"
"Canon Canon PowerShot G3" : 7.176, # 1/1.8"
"Canon Canon PowerShot G5" : 7.176, # 1/1.8"
"Canon Canon PowerShot G6" : 7.176, # 1/1.8"
"Canon Canon PowerShot G7" : 7.176, # 1/1.8"
"Canon Canon PowerShot G9" : 7.600, # 1/1.7"
"Canon Canon PowerShot Pro1" : 8.8, # 2/3"
"Canon Canon PowerShot S110" : 5.27, # 1/2.7"
"Canon Canon PowerShot S1 IS" : 5.27, # 1/2.7"
"Canon Canon PowerShot S200" : 5.27, # 1/2.7"
"Canon Canon PowerShot S2 IS" : 5.76, # 1/2.5"
"Canon Canon PowerShot S30" : 7.176, # 1/1.8"
"Canon Canon PowerShot S3 IS" : 5.76, # 1/2.5"
"Canon Canon PowerShot S400" : 7.176, # 1/1.8"
"Canon Canon PowerShot S40" : 7.176, # 1/1.8"
"Canon Canon PowerShot S410" : 7.176, # 1/1.8"
"Canon Canon PowerShot S45" : 7.176, # 1/1.8"
"Canon Canon PowerShot S500" : 7.176, # 1/1.8"
"Canon Canon PowerShot S50" : 7.176, # 1/1.8"
"Canon Canon PowerShot S60" : 7.176, # 1/1.8"
"Canon Canon PowerShot S70" : 7.176, # 1/1.8"
"Canon Canon PowerShot S80" : 7.176, # 1/1.8"
"Canon Canon PowerShot SD1000" : 5.75, # 1/2.5"
"Canon Canon PowerShot SD100" : 5.27, # 1/2.7"
"Canon Canon PowerShot SD10" : 5.75, # 1/2.5"
"Canon Canon PowerShot SD110" : 5.27, # 1/2.7"
"Canon Canon PowerShot SD200" : 5.76, # 1/2.5"
"Canon Canon PowerShot SD300" : 5.76, # 1/2.5"
"Canon Canon PowerShot SD400" : 5.76, # 1/2.5"
"Canon Canon PowerShot SD450" : 5.76, # 1/2.5"
"Canon Canon PowerShot SD500" : 7.176, # 1/1.8"
"Canon Canon PowerShot SD550" : 7.176, # 1/1.8"
"Canon Canon PowerShot SD600" : 5.76, # 1/2.5"
"Canon Canon PowerShot SD630" : 5.76, # 1/2.5"
"Canon Canon PowerShot SD700 IS" : 5.76, # 1/2.5"
"Canon Canon PowerShot SD750" : 5.75, # 1/2.5"
"Canon Canon PowerShot SD800 IS" : 5.76, # 1/2.5"
"Canon Canon PowerShot SX500 IS" : 6.17, # 1/2.3"
"Canon EOS 300D DIGITAL" : 22.66,
"Canon EOS 1100D" : 22.2,
"Canon EOS DIGITAL REBEL" : 22.66,
"Canon PowerShot A510" : 5.76, # 1/2.5" ???
"Canon PowerShot S30" : 7.176, # 1/1.8"
"CASIO COMPUTER CO.,LTD. EX-S500" : 5.76, # 1/2.5"
"CASIO COMPUTER CO.,LTD. EX-Z1000" : 7.716, # 1/1.8"
"CASIO COMPUTER CO.,LTD EX-Z30" : 5.76, # 1/2.5 "
"CASIO COMPUTER CO.,LTD. EX-Z600" : 5.76, # 1/2.5"
"CASIO COMPUTER CO.,LTD. EX-Z60" : 7.176, # 1/1.8"
"CASIO COMPUTER CO.,LTD EX-Z750" : 7.176, # 1/1.8"
"CASIO COMPUTER CO.,LTD. EX-Z850" : 7.176,
"EASTMAN KODAK COMPANY KODAK CX7330 ZOOM DIGITAL CAMERA" : 5.27, # 1/2.7"
"EASTMAN KODAK COMPANY KODAK CX7530 ZOOM DIGITAL CAMERA" : 5.76, # 1/2.5"
"EASTMAN KODAK COMPANY KODAK DX3900 ZOOM DIGITAL CAMERA" : 7.176, # 1/1.8"
"EASTMAN KODAK COMPANY KODAK DX4900 ZOOM DIGITAL CAMERA" : 7.176, # 1/1.8"
"EASTMAN KODAK COMPANY KODAK DX6340 ZOOM DIGITAL CAMERA" : 5.27, # 1/2.7"
"EASTMAN KODAK COMPANY KODAK DX6490 ZOOM DIGITAL CAMERA" : 5.76, # 1/2.5"
"EASTMAN KODAK COMPANY KODAK DX7630 ZOOM DIGITAL CAMERA" : 7.176, # 1/1.8"
"EASTMAN KODAK COMPANY KODAK Z650 ZOOM DIGITAL CAMERA" : 5.76, # 1/2.5"
"EASTMAN KODAK COMPANY KODAK Z700 ZOOM DIGITAL CAMERA" : 5.76, # 1/2.5"
"EASTMAN KODAK COMPANY KODAK Z740 ZOOM DIGITAL CAMERA" : 5.76, # 1/2.5"
"FUJIFILM FinePix2600Zoom" : 5.27, # 1/2.7"
"FUJIFILM FinePix40i" : 7.600, # 1/1.7"
"FUJIFILM FinePix A310" : 5.27, # 1/2.7"
"FUJIFILM FinePix A330" : 5.27, # 1/2.7"
"FUJIFILM FinePix A600" : 7.600, # 1/1.7"
"FUJIFILM FinePix E500" : 5.76, # 1/2.5"
"FUJIFILM FinePix E510" : 5.76, # 1/2.5"
"FUJIFILM FinePix E550" : 7.600, # 1/1.7"
"FUJIFILM FinePix E900" : 7.78, # 1/1.6"
"FUJIFILM FinePix F10" : 7.600, # 1/1.7"
"FUJIFILM FinePix F30" : 7.600, # 1/1.7"
"FUJIFILM FinePix F450" : 5.76, # 1/2.5"
"FUJIFILM FinePix F601 ZOOM" : 7.600, # 1/1.7"
"FUJIFILM FinePix S3Pro" : 23.0,
"FUJIFILM FinePix S5000" : 5.27, # 1/2.7"
"FUJIFILM FinePix S5200" : 5.76, # 1/2.5"
"FUJIFILM FinePix S5500" : 5.27, # 1/2.7"
"FUJIFILM FinePix S6500fd" : 7.600, # 1/1.7"
"FUJIFILM FinePix S7000" : 7.600, # 1/1.7"
"FUJIFILM FinePix Z2" : 5.76, # 1/2.5"
"Hewlett-Packard hp 635 Digital Camera" : 4.54, # 1/3.2"
"Hewlett-Packard hp PhotoSmart 43x series" : 5.27, # 1/2.7"
"Hewlett-Packard HP PhotoSmart 618 (V1.1)" : 5.27, # 1/2.7"
"Hewlett-Packard HP PhotoSmart C945 (V01.61)" : 7.176, # 1/1.8"
"Hewlett-Packard HP PhotoSmart R707 (V01.00)" : 7.176, # 1/1.8"
"KONICA MILOLTA DYNAX 5D" : 23.5,
"Konica Minolta Camera, Inc. DiMAGE A2" : 8.80, # 2/3"
"KONICA MINOLTA CAMERA, Inc. DiMAGE G400" : 5.76, # 1/2.5"
"Konica Minolta Camera, Inc. DiMAGE Z2" : 5.76, # 1/2.5"
"KONICA MINOLTA DiMAGE A200" : 8.80, # 2/3"
"KONICA MINOLTA DiMAGE X1" : 7.176, # 1/1.8"
"KONICA MINOLTA DYNAX 5D" : 23.5,
"Minolta Co., Ltd. DiMAGE F100" : 7.176, # 1/2.7"
"Minolta Co., Ltd. DiMAGE Xi" : 5.27, # 1/2.7"
"Minolta Co., Ltd. DiMAGE Xt" : 5.27, # 1/2.7"
"Minolta Co., Ltd. DiMAGE Z1" : 5.27, # 1/2.7"
"NIKON COOLPIX L3" : 5.76, # 1/2.5"
"NIKON COOLPIX P2" : 7.176, # 1/1.8"
"NIKON COOLPIX S4" : 5.76, # 1/2.5"
"NIKON COOLPIX S7c" : 5.76, # 1/2.5"
"NIKON CORPORATION NIKON D100" : 23.7,
"NIKON CORPORATION NIKON D1" : 23.7,
"NIKON CORPORATION NIKON D1H" : 23.7,
"NIKON CORPORATION NIKON D200" : 23.6,
"NIKON CORPORATION NIKON D2H" : 23.3,
"NIKON CORPORATION NIKON D2X" : 23.7,
"NIKON CORPORATION NIKON D40" : 23.7,
"NIKON CORPORATION NIKON D50" : 23.7,
"NIKON CORPORATION NIKON D60" : 23.6,
"NIKON CORPORATION NIKON D70" : 23.7,
"NIKON CORPORATION NIKON D70s" : 23.7,
"NIKON CORPORATION NIKON D80" : 23.6,
"NIKON CORPORATION NIKON D90" : 23.6,
"NIKON CORPORATION NIKON D3300" : 23.5,
"NIKON E2500" : 5.27, # 1/2.7"
"NIKON E2500" : 5.27, # 1/2.7"
"NIKON E3100" : 5.27, # 1/2.7"
"NIKON E3200" : 5.27,
"NIKON E3700" : 5.27, # 1/2.7"
"NIKON E4200" : 7.176, # 1/1.8"
"NIKON E4300" : 7.18,
"NIKON E4500" : 7.176, # 1/1.8"
"NIKON E4600" : 5.76, # 1/2.5"
"NIKON E5000" : 8.80, # 2/3"
"NIKON E5200" : 7.176, # 1/1.8"
"NIKON E5400" : 7.176, # 1/1.8"
"NIKON E5600" : 5.76, # 1/2.5"
"NIKON E5700" : 8.80, # 2/3"
"NIKON E5900" : 7.176, # 1/1.8"
"NIKON E7600" : 7.176, # 1/1.8"
"NIKON E775" : 5.27, # 1/2.7"
"NIKON E7900" : 7.176, # 1/1.8"
"NIKON E7900" : 7.176, # 1/1.8"
"NIKON E8800" : 8.80, # 2/3"
"NIKON E990" : 7.176, # 1/1.8"
"NIKON E995" : 7.176, # 1/1.8"
"NIKON S1" : 5.76, # 1/2.5"
"Nokia N80" : 5.27, # 1/2.7"
"Nokia N80" : 5.27, # 1/2.7"
"Nokia N93" : 4.536, # 1/3.1"
"Nokia N95" : 5.7, # 1/2.7"
"OLYMPUS CORPORATION C-5000Z" : 7.176, # 1/1.8"
"OLYMPUS CORPORATION C5060WZ" : 7.176, # 1/1.8"
"OLYMPUS CORPORATION C750UZ" : 5.27, # 1/2.7"
"OLYMPUS CORPORATION C765UZ" : 5.76, # 1//2.5"
"OLYMPUS CORPORATION C8080WZ" : 8.80, # 2/3"
"OLYMPUS CORPORATION X250,D560Z,C350Z" : 5.76, # 1/2.5"
"OLYMPUS CORPORATION X-3,C-60Z" : 7.176, # 1.8"
"OLYMPUS CORPORATION X400,D580Z,C460Z" : 5.27, # 1/2.7"
"OLYMPUS IMAGING CORP. E-500" : 17.3, # 4/3?
"OLYMPUS IMAGING CORP. FE115,X715" : 5.76, # 1/2.5"
"OLYMPUS IMAGING CORP. SP310" : 7.176, # 1/1.8"
"OLYMPUS IMAGING CORP. SP510UZ" : 5.75, # 1/2.5"
"OLYMPUS IMAGING CORP. SP550UZ" : 5.76, # 1/2.5"
"OLYMPUS IMAGING CORP. uD600,S600" : 5.75, # 1/2.5"
"OLYMPUS_IMAGING_CORP. X450,D535Z,C370Z" : 5.27, # 1/2.7"
"OLYMPUS IMAGING CORP. X550,D545Z,C480Z" : 5.76, # 1/2.5"
"OLYMPUS OPTICAL CO.,LTD C2040Z" : 6.40, # 1/2"
"OLYMPUS OPTICAL CO.,LTD C211Z" : 5.27, # 1/2.7"
"OLYMPUS OPTICAL CO.,LTD C2Z,D520Z,C220Z" : 4.54, # 1/3.2"
"OLYMPUS OPTICAL CO.,LTD C3000Z" : 7.176, # 1/1.8"
"OLYMPUS OPTICAL CO.,LTD C300Z,D550Z" : 5.4,
"OLYMPUS OPTICAL CO.,LTD C4100Z,C4000Z" : 7.176, # 1/1.8"
"OLYMPUS OPTICAL CO.,LTD C750UZ" : 5.27, # 1/2.7"
"OLYMPUS OPTICAL CO.,LTD X-2,C-50Z" : 7.176, # 1/1.8"
"OLYMPUS SP550UZ" : 5.76, # 1/2.5"
"OLYMPUS X100,D540Z,C310Z" : 5.27, # 1/2.7"
"Panasonic DMC-FX01" : 5.76, # 1/2.5"
"Panasonic DMC-FX07" : 5.75, # 1/2.5"
"Panasonic DMC-FX9" : 5.76, # 1/2.5"
"Panasonic DMC-FZ20" : 5.760, # 1/2.5"
"Panasonic DMC-FZ2" : 4.54, # 1/3.2"
"Panasonic DMC-FZ30" : 7.176, # 1/1.8"
"Panasonic DMC-FZ50" : 7.176, # 1/1.8"
"Panasonic DMC-FZ5" : 5.760, # 1/2.5"
"Panasonic DMC-FZ7" : 5.76, # 1/2.5"
"Panasonic DMC-LC1" : 8.80, # 2/3"
"Panasonic DMC-LC33" : 5.760, # 1/2.5"
"Panasonic DMC-LX1" : 8.50, # 1/6.5"
"Panasonic DMC-LZ2" : 5.76, # 1/2.5"
"Panasonic DMC-TZ1" : 5.75, # 1/2.5"
"Panasonic DMC-TZ3" : 5.68, # 1/2.35"
"Panasonic DMC-TZ10" : 6.23,
"Panasonic HC-X900" : 3.20,
"PENTAX Corporation PENTAX *ist DL" : 23.5,
"PENTAX Corporation PENTAX *ist DS2" : 23.5,
"PENTAX Corporation PENTAX *ist DS" : 23.5,
"PENTAX Corporation PENTAX K100D" : 23.5,
"PENTAX Corporation PENTAX Optio 450" : 7.176, # 1/1.8"
"PENTAX Corporation PENTAX Optio 550" : 7.176, # 1/1.8"
"PENTAX Corporation PENTAX Optio E10" : 5.76, # 1/2.5"
"PENTAX Corporation PENTAX Optio S40" : 5.76, # 1/2.5"
"PENTAX Corporation PENTAX Optio S4" : 5.76, # 1/2.5"
"PENTAX Corporation PENTAX Optio S50" : 5.76, # 1/2.5"
"PENTAX Corporation PENTAX Optio S5i" : 5.76, # 1/2.5"
"PENTAX Corporation PENTAX Optio S5z" : 5.76, # 1/2.5"
"PENTAX Corporation PENTAX Optio SV" : 5.76, # 1/2.5"
"PENTAX Corporation PENTAX Optio WP" : 5.75, # 1/2.5"
"PENTAX Corporation PENTAX K10D" : 23.5,
"RICOH CaplioG3 modelM" : 5.27, # 1/2.7"
"RICOH Caplio GX" : 7.176, # 1/1.8"
"RICOH Caplio R30" : 5.75, # 1/2.5"
"Samsung Digimax 301" : 5.27, # 1/2.7"
"Samsung Techwin <Digimax i5, Samsung #1>" : 5.76, # 1/2.5"
"SAMSUNG TECHWIN Pro 815" : 8.80, # 2/3"
"SONY DSC-F828" : 8.80, # 2/3"
"SONY DSC-N12" : 7.176, # 1/1.8"
"SONY DSC-P100" : 7.176, # 1/1.8"
"SONY DSC-P10" : 7.176, # 1/1.8"
"SONY DSC-P12" : 7.176, # 1/1.8"
"SONY DSC-P150" : 7.176, # 1/1.8"
"SONY DSC-P200" : 7.176, # 1/1.8");
"SONY DSC-P52" : 5.27, # 1/2.7"
"SONY DSC-P72" : 5.27, # 1/2.7"
"SONY DSC-P73" : 5.27,
"SONY DSC-P8" : 5.27, # 1/2.7"
"SONY DSC-R1" : 21.5,
"SONY DSC-S40" : 5.27, # 1/2.7"
"SONY DSC-S600" : 5.760, # 1/2.5"
"SONY DSC-T9" : 7.18,
"SONY DSC-V1" : 7.176, # 1/1.8"
"SONY DSC-W1" : 7.176, # 1/1.8"
"SONY DSC-W30" : 5.760, # 1/2.5"
"SONY DSC-W50" : 5.75, # 1/2.5"
"SONY DSC-W5" : 7.176, # 1/1.8"
"SONY DSC-W7" : 7.176, # 1/1.8"
"SONY DSC-W80" : 5.75, # 1/2.5"
}
def get_images():
"""Searches the present directory for JPEG images."""
images = glob.glob("./*.[jJ][pP][gG]")
if len(images) == 0:
error_str = ("Error: No images supplied! "
"No JPEG files found in directory!")
raise Exception(error_str)
return sorted(images)
def extract_focal_length(images=[], scale=1.0):
"""Extracts (pixel) focal length from images where available.
The functions returns a dictionary of image, focal length pairs.
If no focal length is extracted for an image, the second pair is None.
"""
if len(images) == 0:
error_str = ("Error: No images supplied! "
"No JPEG files found in directory!")
raise Exception(error_str)
ret = []
for image in images:
print "[Extracting EXIF tags from image {0}]".format(image)
tags = {}
with open(image, 'rb') as fp:
img = Image.open(fp)
if hasattr(img, '_getexif'):
exifinfo = img._getexif()
if exifinfo is not None:
for tag, value in exifinfo.items():
tags[ExifTags.TAGS.get(tag, tag)] = value
# Extract Focal Length
focalN, focalD = tags.get('FocalLength', (0, 1))
focal_length = float(focalN)/float(focalD)
# Extract Resolution from the exif
img_width = tags.get('ExifImageWidth', 0)
img_height = tags.get('ExifImageHeight', 0)
# Also extract the actual resolution of the image
real_img_width = img.size[0]
real_img_height = img.size[1]
# Check if we are confused about the resolution
if not img_width == real_img_width:
print "[WARNING: EXIF resolution (%d x %d) does not match image resolution (%d x %d) for image %s (using image resolution instead)]" % (img_width,img_height,real_img_width,real_img_height, image)
img_width = real_img_width
img_height = real_img_height
if img_width < img_height:
img_width,img_height = img_height,img_width
# Extract CCD Width (Prefer Lookup Table)
ccd_width = 1.0
make_model = tags.get('Make', '') + ' ' + tags.get('Model', '')
if CCD_WIDTHS.has_key(make_model.strip()):
ccd_width = CCD_WIDTHS[make_model.strip()]
else:
fplaneN, fplaneD = tags.get('FocalPlaneXResolution', (0, 1))
if fplaneN != 0:
ccd_width = 25.4*float(img_width)*float(fplaneD)/float(fplaneN)
print " [Using CCD width from EXIF tags]"
else:
ccd_width = 0
print " [EXIF focal length = {0}mm]".format(focal_length)
print " [EXIF CCD width = {0}mm]".format(ccd_width)
print " [EXIF resolution = {0} x {1}]".format(img_width, img_height)
if ccd_width == 0:
print "ERROR: No CCD width available for camera {0}]".format(make_model)
exit(1)
if (img_width==0 or img_height==0 or focalN==0 or ccd_width==0):
print "ERROR: Could not determine pixel focal length for image", image
exit(1)
# Compute Focal Length in Pixels
result = img_width * (focal_length / ccd_width) * scale
ret.append((image, result))
print " [Focal length (pixels) = {0}]".format(result)
return ret
def sift_image(image):
"""Extracts SIFT features from a single image. See sift_images."""
pgm_filename = image.rsplit('.', 1)[0] + ".pgm"
key_filename = image.rsplit('.', 1)[0] + ".key"
# Convert image to PGM format (grayscale)
with open(image, 'rb') as fp_img:
image = Image.open(fp_img)
image.convert('L').save(pgm_filename)
# Extract SIFT data
with open(pgm_filename, 'rb') as fp_in:
with open(key_filename, 'wb') as fp_out:
subprocess.call(BIN_SIFT, stdin=fp_in, stdout=fp_out)
# Remove pgm file
os.remove(pgm_filename)
# GZIP compress key file (and remove)
# with open(key_filename, 'rb') as fp_in:
# with gzip.open(key_filename + ".gz", 'wb') as fp_out:
# fp_out.writelines(fp_in)
# os.remove(key_filename)
return key_filename
def sift_images(images):
"""Extracts SIFT features from images in 'images'.
'images' should be a list of file names. The function creates a
SIFT compressed key file for each image in 'images' with a '.key.gz'
extension. A list of the uncompressed key file names is returned.
"""
pool = ThreadPool()
return pool.map(sift_image, images)
def match_image(image):
# Add lib folder to LD_LIBRARY_PATH
env = dict(os.environ)
if env.has_key('LD_LIBRARY_PATH'):
env['LD_LIBRARY_PATH'] = env['LD_LIBRARY_PATH'] + ':' + BUNDLER_LIB_PATH
else:
env['LD_LIBRARY_PATH'] = BUNDLER_LIB_PATH
matches_file = 'match.' + str(image[0]) + '.txt'
subprocess.call([BIN_MATCHKEYS_PART, image[1], str(image[0]), matches_file], env=env)
return matches_file
def par_match_images(key_file, image_count, matches_file):
"Executes KeyMatchPart to match key points in each image."""
images = []
for i in range(image_count-1, 0, -1):
images.append((i, key_file))
pool = ThreadPool()
match_filenames = pool.map(match_image, images)
with open(matches_file, 'w') as fout:
for line in fileinput.input(match_filenames):
fout.write(line)
def match_images(key_files, matches_file):
keys_file = ""
with tempfile.NamedTemporaryFile(delete=False) as fp:
for key in key_files:
fp.write(key + '\n')
keys_file = fp.name
par_match_images(keys_file, len(key_files), matches_file)
os.remove(keys_file)
def bundler(image_list=None, options_file=None, shell=False, *args, **kwargs):
"""Run bundler, parsing arguments from args and kwargs through.
For Bundler usage run bundler("--help").
image_list : File containing list of images.
options_file : Specify an options file for bundler (optional).
shell : Enable full shell support for parsing args (default: False).
"""
def kwargs_bool(b, r):
if b: return r
else: return []
kwargs_dict = {
'match_table' : lambda k,v: ['--'+k,v],
'output' : lambda k,v: ['--'+k,v],
'output_all' : lambda k,v: ['--'+k,v],
'output_dir' : lambda k,v: ['--'+k,v],
'variable_focal_length' : lambda k,v: kwargs_bool(v, ['--'+k]),
'use_focal_estimate' : lambda k,v: kwargs_bool(v, ['--'+k]),
'constrain_focal' : lambda k,v: kwargs_bool(v, ['--'+k]),
'constrain_focal_weight' : lambda k,v: ['--'+k,str(v)],
'estimate_distortion' : lambda k,v: kwargs_bool(v, ['--'+k]),
'projection_estimation_threshold' : lambda k,v: ['--'+k,str(v)],
'construct_max_connectivity' : lambda k,v: kwargs_bool(v, ['--'+k]),
'use_ceres' : lambda k,v: kwargs_bool(v, ['--'+k]),
'run_bundle' : lambda k,v: kwargs_bool(v, ['--'+k]),
}
str_args = [a for a in args if type(a) == str]
for k,v in kwargs.items():
if not kwargs_dict.has_key(k): continue
str_args.extend(kwargs_dict[k](k,v))
if len(str_args) != 0 and options_file is not None:
with open(options_file, 'wb') as fp:
for o in str_args:
if o.startswith('--'): fp.write('\n')
else: fp.write(' ')
fp.write(o)
image_list_file = ""
if type(image_list) == dict:
with tempfile.NamedTemporaryFile(delete=False) as fp:
for image,value in image_list.items():
if value == None: fp.write(image + '\n')
else: fp.write(' '.join([image, '0', str(value), '\n']))
image_list_file = fp.name
elif type(image_list) == str:
image_list_file = image_list
else:
raise Exception("Error: Not a valid list or filename for image_list!")
# Add lib folder to LD_LIBRARY_PATH
env = dict(os.environ)
if env.has_key('LD_LIBRARY_PATH'):
env['LD_LIBRARY_PATH'] = env['LD_LIBRARY_PATH'] + ':' + BUNDLER_LIB_PATH
else:
env['LD_LIBRARY_PATH'] = BUNDLER_LIB_PATH
try: os.mkdir("bundle")
except: pass
with open(os.path.join("bundle", "out"), 'wb') as fp_out:
if options_file is not None:
subprocess.call([BIN_BUNDLER, image_list_file, "--options_file",
options_file], shell=shell, env=env, stdout=fp_out)
else:
subprocess.call([BIN_BUNDLER, image_list_file] + str_args,
shell=shell, env=env, stdout=fp_out)
if type(image_list) == dict:
os.remove(image_list_file)
def save_image_list(image_list=None, filename='list.txt'):
with open(filename, 'wb') as fp:
for image in image_list:
fp.write(' '.join([image[0], '0', str(image[1]), '\n']))
def create_dense_pointcloud(image_names, image_list='list.txt', bundle_out="bundle.out"):
# Start by running bundle2pmvs. This will create the pmvs directory, and
# generate the undistortion matrices for each image.
# Add lib folder to LD_LIBRARY_PATH
env = dict(os.environ)
if env.has_key('LD_LIBRARY_PATH'):
env['LD_LIBRARY_PATH'] = env['LD_LIBRARY_PATH'] + ':' + BUNDLER_LIB_PATH
else:
env['LD_LIBRARY_PATH'] = BUNDLER_LIB_PATH
bundle_output = os.path.join('bundle', 'bundle.out')
subprocess.call([BIN_BUNDLE2PMVS, image_list, bundle_output], env=env)
# Next run the image undistort.
subprocess.call([BIN_RADIAL_UNDISTORT, image_list, bundle_output, 'pmvs'], env=env)
# Create the necessay directories
try: os.mkdir(os.path.join('pmvs', 'txt'))
except: pass
try: os.mkdir(os.path.join('pmvs', 'visualize'))
except: pass
try: os.mkdir(os.path.join('pmvs', 'models'))
except: pass
# Move some temp files to their final location. This is needed by CMVS
count = 0
for image in image_names:
source = image.rsplit('.', 1)[0] + '.rd.jpg'
target = str(count).zfill(8) + '.jpg'
if os.path.exists(os.path.join('pmvs', source)):
shutil.move(os.path.join('pmvs', source), os.path.join('pmvs', 'visualize', target))
source = str(count).zfill(8) + '.txt'
shutil.move(os.path.join('pmvs', source), os.path.join('pmvs', 'txt', source))
count += 1
# Run CMVS to generate the required centers.ply and vis.dat
# FIXME: hardcoded CPU count!
subprocess.call([BIN_CMVS, os.path.join('.', 'pmvs', ''), '100', '16'], env=env)
# Run genOption to generate the required PMVS2 config.
cores = multiprocessing.cpu_count()
# The first option is a hardcoded 'quality' level. 1 is standard, 0 produces hi-res pointclouds.
# The other options are the default setting, except for the last one, which is the number of
# threads to use in PMVS2.
subprocess.call([BIN_GEN_OPTION, os.path.join('.', 'pmvs', ''), '0', '2', '0.7', '7', '3', str(cores)], env=env)
# Run genOption to generate the required PMVS2 config.
subprocess.call([BIN_PMVS2, os.path.join('.', 'pmvs', ''), 'option-0000'], env=env)
def run_bundler():
"""Prepare images and run bundler with default options."""
# Prepare the list of executables we need.
global BIN_SIFT, BIN_BUNDLER, BIN_MATCHKEYS_FULL, BIN_MATCHKEYS_PART, BIN_BUNDLE2PMVS, BIN_RADIAL_UNDISTORT, BIN_CMVS, BIN_GEN_OPTION, BIN_PMVS2, BUNDLER_BIN_PATH, CMVS_PMVS_BIN_PATH
start = time.time()
if sys.platform == 'win32' or sys.platform == 'cygwin':
BIN_SIFT = os.path.join(BUNDLER_BIN_PATH, "siftWin32.exe")
BIN_BUNDLER = os.path.join(BUNDLER_BIN_PATH, "Bundler.exe")
BIN_MATCHKEYS_FULL = os.path.join(BUNDLER_BIN_PATH, "KeyMatchFull.exe")
BIN_MATCHKEYS_PART = os.path.join(BUNDLER_BIN_PATH, "KeyMatchPart.exe")
BIN_BUNDLE2PMVS = os.path.join(BUNDLER_BIN_PATH, "Bundle2PMVS.exe")
BIN_RADIAL_UNDISTORT = os.path.join(BUNDLER_BIN_PATH, "RadialUndistort.exe")
BIN_CMVS = os.path.join(CMVS_PMVS_BIN_PATH, "cmvs.exe")
BIN_GEN_OPTION = os.path.join(CMVS_PMVS_BIN_PATH, "genOption.exe")
BIN_PMVS2 = os.path.join(CMVS_PMVS_BIN_PATH, "pmvs2.exe")
else:
BIN_SIFT = os.path.join(BUNDLER_BIN_PATH, "sift")
BIN_BUNDLER = os.path.join(BUNDLER_BIN_PATH, "bundler")
BIN_MATCHKEYS_FULL = os.path.join(BUNDLER_BIN_PATH, "KeyMatchFull")
BIN_MATCHKEYS_PART = os.path.join(BUNDLER_BIN_PATH, "KeyMatchPart")
BIN_BUNDLE2PMVS = os.path.join(BUNDLER_BIN_PATH, "Bundle2PMVS")
BIN_RADIAL_UNDISTORT = os.path.join(BUNDLER_BIN_PATH, "RadialUndistort")
BIN_CMVS = os.path.join(CMVS_PMVS_BIN_PATH, "cmvs")
BIN_GEN_OPTION = os.path.join(CMVS_PMVS_BIN_PATH, "genOption")
BIN_PMVS2 = os.path.join(CMVS_PMVS_BIN_PATH, "pmvs2")
step,totalsteps = 1,6
# Create list of images
print "[- Step %d/%d Creating list of images -]" % (step,totalsteps)
images = get_images()
step += 1
time1 = time.time()
if len(images) < 10:
print "ERROR: not enough images (have %d, need at least 10)" % len(images)
exit(1)
else:
print "[- Retrieved %d images in %.1f seconds -]" % (len(images), time1-start)
# Extract focal length
print "[- Step %d/%d Extracting EXIF tags from images -]" % (step,totalsteps)
images_focal = extract_focal_length(images)
step += 1
time2 = time.time()
print "[- Retrieved EXIF tags of %d images in %.1f seconds -]" % (len(images_focal), time2-time1)
if not len(images_focal) == len(images):
print "ERROR: some images are missing necessasy EXIF information!"
exit(1)
# Save the image list, since we need it later on.
save_image_list(images_focal)
# Extract SIFT features from images
print "[- Step %d/%d Extracting keypoints -]" % (step,totalsteps)
key_files = sift_images(images)
step += 1
time3 = time.time()
print "[- Extracting keypoints took %.1f seconds -]" % (time3-time2)
# Match images
print "[- Step %d/%d Matching keypoints (this can take a while) -]" % (step,totalsteps)
matches_file = "matches.init.txt"
match_images(key_files, matches_file)
step += 1
time4 = time.time()
print "[- Matching keypoints took %.1f seconds -]" % (time4-time3)
# Run Bundler
print "[- Step %d/%d Running Bundler (sparse pointcloud generation) -]" % (step,totalsteps)
bundler(image_list='list.txt',
options_file="options.txt",
match_table=matches_file,
output="bundle.out",
output_all="bundle_",
output_dir="bundle",
variable_focal_length=True,
use_focal_estimate=True,
constrain_focal=True,
constrain_focal_weight=0.0001,
estimate_distortion=True,
projection_estimation_threshold=1.1,
construct_max_connectivity=True,
use_ceres=True,
run_bundle=True)
step += 1
time5 = time.time()
print "[- Bundler took %.1f seconds-]" % (time5-time4)
print "[- Step %d/%d Creating CMVS/PMVS configuration and running PMVS2 -]" % (step,totalsteps)
create_dense_pointcloud(images, image_list='list.txt', bundle_out="bundle.out")
step += 1
end = time.time()
print "[- Creating dense point cloud took %.1f seconds -]" % (end-time5)
print "[- Done in %.1f seconds -]" % (end-start)
if __name__ == '__main__':
run_bundler()