photo-utils 0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/Gemfile +4 -0
  4. data/README.rdoc +1 -0
  5. data/Rakefile +1 -0
  6. data/TODO.txt +34 -0
  7. data/bin/photo-util +39 -0
  8. data/lib/photo_utils.rb +28 -0
  9. data/lib/photo_utils/angle.rb +17 -0
  10. data/lib/photo_utils/aperture.rb +62 -0
  11. data/lib/photo_utils/apex.rb +210 -0
  12. data/lib/photo_utils/brightness.rb +49 -0
  13. data/lib/photo_utils/camera.rb +82 -0
  14. data/lib/photo_utils/compensation.rb +29 -0
  15. data/lib/photo_utils/extensions/array.rb +11 -0
  16. data/lib/photo_utils/extensions/float.rb +18 -0
  17. data/lib/photo_utils/extensions/math.rb +11 -0
  18. data/lib/photo_utils/extensions/numeric.rb +23 -0
  19. data/lib/photo_utils/formats.rb +146 -0
  20. data/lib/photo_utils/frame.rb +36 -0
  21. data/lib/photo_utils/illuminance.rb +48 -0
  22. data/lib/photo_utils/length.rb +92 -0
  23. data/lib/photo_utils/lens.rb +50 -0
  24. data/lib/photo_utils/scene.rb +180 -0
  25. data/lib/photo_utils/scene_view.rb +134 -0
  26. data/lib/photo_utils/sensitivity.rb +44 -0
  27. data/lib/photo_utils/time.rb +53 -0
  28. data/lib/photo_utils/tool.rb +17 -0
  29. data/lib/photo_utils/tools/blur.rb +43 -0
  30. data/lib/photo_utils/tools/brightness.rb +20 -0
  31. data/lib/photo_utils/tools/calc_aperture.rb +45 -0
  32. data/lib/photo_utils/tools/cameras.rb +24 -0
  33. data/lib/photo_utils/tools/chart_dof.rb +146 -0
  34. data/lib/photo_utils/tools/compare.rb +305 -0
  35. data/lib/photo_utils/tools/dof.rb +42 -0
  36. data/lib/photo_utils/tools/dof_table.rb +45 -0
  37. data/lib/photo_utils/tools/film_test.rb +57 -0
  38. data/lib/photo_utils/tools/focal_length.rb +25 -0
  39. data/lib/photo_utils/tools/reciprocity.rb +36 -0
  40. data/lib/photo_utils/tools/test.rb +91 -0
  41. data/lib/photo_utils/value.rb +37 -0
  42. data/lib/photo_utils/version.rb +5 -0
  43. data/photo-utils.gemspec +29 -0
  44. data/test/aperture_test.rb +40 -0
  45. data/test/apex_test.rb +28 -0
  46. data/test/brightness_test.rb +36 -0
  47. data/test/length_test.rb +40 -0
  48. data/test/scene_test.rb +25 -0
  49. data/test/sensitivity_test.rb +36 -0
  50. data/test/time_test.rb +42 -0
  51. metadata +185 -0
@@ -0,0 +1,17 @@
1
+ module PhotoUtils
2
+
3
+ class Tool
4
+
5
+ def usage
6
+ end
7
+
8
+ def description
9
+ end
10
+
11
+ def run(args)
12
+ raise NotImplementedError, "Tool #{self.class} does not implement \#run"
13
+ end
14
+
15
+ end
16
+
17
+ end
@@ -0,0 +1,43 @@
1
+ require 'photo_utils/tool'
2
+
3
+ module PhotoUtils
4
+
5
+ class Tools
6
+
7
+ class Blur < Tool
8
+
9
+ def run(args)
10
+ scene = Scene.new
11
+ scene.sensitivity = 100
12
+ scene.subject_distance = 6.feet
13
+ scene.background_distance = 7.feet
14
+ flash_lux = 25
15
+ flash_seconds = 0.001
16
+ # flash_lux_seconds = flash_lux.to_f * (flash_seconds * 1000)
17
+ flash_lux_seconds = 25000 / 2
18
+ scene.brightness = PhotoUtils::Brightness.new_from_cdm2(flash_lux_seconds.to_f / ((scene.subject_distance / 1000) ** 2))
19
+ scene.camera = Camera[/Eastman/]
20
+ # scene.camera.lens = scene.camera.lenses.find { |l| l.focal_length == 12.inches }
21
+ scene.camera.lens.aperture = 32
22
+ scene.camera.shutter = nil
23
+
24
+ scene.print_camera
25
+ scene.print_exposure
26
+ scene.print_depth_of_field
27
+
28
+ 1.feet.step(scene.subject_distance * 2, 1.feet).map { |d| Length.new(d) }.each do |d|
29
+ blur = scene.blur_at_distance(d)
30
+ puts "%12s: blur disk: %7s, blur/CoC: %6d%% -- %s" % [
31
+ d.to_s(:imperial),
32
+ blur.to_s(:metric),
33
+ (blur / scene.circle_of_confusion) * 100,
34
+ blur <= scene.circle_of_confusion ? 'in focus' : 'out of focus'
35
+ ]
36
+ end
37
+ end
38
+
39
+ end
40
+
41
+ end
42
+
43
+ end
@@ -0,0 +1,20 @@
1
+ require 'photo_utils/tool'
2
+
3
+ module PhotoUtils
4
+
5
+ class Tools
6
+
7
+ class Brightness < Tool
8
+
9
+ def run(args)
10
+ scene = Scene.new
11
+ scene.camera = Camera[/Rollei/]
12
+ scene.description = "Salon L'Orient"
13
+ scene.print_exposure
14
+ end
15
+
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,45 @@
1
+ require 'photo_utils/tool'
2
+
3
+ module PhotoUtils
4
+
5
+ class Tools
6
+
7
+ class CalcAperture < Tool
8
+
9
+ def run(args)
10
+
11
+ # set up basic scene
12
+
13
+ basic_scene = Scene.new
14
+ basic_scene.camera = Camera[/hasselblad/i]
15
+ basic_scene.subject_distance = 12.feet
16
+ basic_scene.background_distance = 14.feet
17
+ basic_scene.camera.lens.aperture = 8
18
+ basic_scene.description = basic_scene.camera.name
19
+
20
+ puts "--- @ #{basic_scene.subject_distance.to_s(:imperial)}"
21
+ basic_scene.camera.lenses.each do |lens|
22
+ scene = basic_scene.dup
23
+ scene.camera.shutter = nil
24
+ scene.camera.lens = lens
25
+ # scene.aperture = scene.aperture_for_depth_of_field(scene.subject_distance - 9.inches, scene.subject_distance + 9.inches)
26
+ # next unless scene.aperture >= lens.max_aperture && scene.aperture <= lens.min_aperture
27
+ # background_fov = scene.field_of_view(scene.background_distance)
28
+ # next unless background_fov.width <= 4.feet && background_fov.height <= 4.feet
29
+ # subject_fov = scene.field_of_view(scene.subject_distance)
30
+ # next unless subject_fov.width >= 2.feet && subject_fov.height >= 2.feet
31
+ # next unless scene.time < 1.0/15
32
+ scene.description += ": #{scene.camera.lens.focal_length} @ #{scene.camera.lens.aperture}"
33
+ puts "#{scene.description}:"
34
+ scene.print_depth_of_field
35
+ scene.print_exposure
36
+ puts
37
+ end
38
+
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,24 @@
1
+ require 'photo_utils/tool'
2
+
3
+ module PhotoUtils
4
+
5
+ class Tools
6
+
7
+ class Cameras < Tool
8
+
9
+ def run(args)
10
+ if Camera.cameras
11
+ Camera.cameras.each do |camera|
12
+ camera.print
13
+ puts
14
+ end
15
+ else
16
+ warn "No cameras found."
17
+ end
18
+ end
19
+
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,146 @@
1
+ require 'photo_utils/tool'
2
+
3
+ module PhotoUtils
4
+
5
+ class Tools
6
+
7
+ class ChartDOF < Tool
8
+
9
+ def run(args)
10
+
11
+ # set up basic scene
12
+
13
+ basic_scene = Scene.new
14
+ basic_scene.subject_distance = 8.feet
15
+ basic_scene.sensitivity = 400
16
+ basic_scene.brightness = 8
17
+
18
+ scenes = []
19
+
20
+ if false
21
+
22
+ scene = basic_scene.dup
23
+ scene.format = Format['35']
24
+ scene.focal_length = 50.mm
25
+ scene.aperture = 2
26
+ scene.description = "#{scene.format}: #{scene.focal_length} @ #{scene.aperture}"
27
+ scenes << scene
28
+
29
+ scene = basic_scene.dup
30
+ scene.format = Format['6x6']
31
+ scene.focal_length = 92.mm
32
+ scene.aperture = 8
33
+ scene.description = "#{scene.format}: #{scene.focal_length} @ #{scene.aperture}"
34
+ scenes << scene
35
+
36
+ scene = basic_scene.dup
37
+ scene.format = Format['5x7']
38
+ scene.focal_length = 253.mm
39
+ scene.aperture = 64
40
+ scene.description = "#{scene.format}: #{scene.focal_length} @ #{scene.aperture}"
41
+ scenes << scene
42
+
43
+ end
44
+
45
+ if false
46
+
47
+ scene = basic_scene.dup
48
+ scene.format = Format['35']
49
+ scene.focal_length = 90.mm
50
+ scene.aperture = 2.8
51
+ scene.description = "#{scene.format}: #{scene.focal_length} @ #{scene.aperture}"
52
+ scenes << scene
53
+
54
+ scene = basic_scene.dup
55
+ scene.format = Format['35']
56
+ scene.focal_length = 90.mm
57
+ scene.aperture = 4
58
+ scene.description = "#{scene.format}: #{scene.focal_length} @ #{scene.aperture}"
59
+ scenes << scene
60
+
61
+ scene = basic_scene.dup
62
+ scene.format = Format['35']
63
+ scene.focal_length = 90.mm
64
+ scene.aperture = 5.6
65
+ scene.description = "#{scene.format}: #{scene.focal_length} @ #{scene.aperture}"
66
+ scenes << scene
67
+
68
+ scene = basic_scene.dup
69
+ scene.format = Format['35']
70
+ scene.focal_length = 85.mm
71
+ scene.aperture = 4
72
+ scene.description = "#{scene.format}: #{scene.focal_length} @ #{scene.aperture}"
73
+ scenes << scene
74
+
75
+ scene = basic_scene.dup
76
+ scene.format = Format['35']
77
+ scene.focal_length = 85.mm
78
+ scene.aperture = 5.6
79
+ scene.description = "#{scene.format}: #{scene.focal_length} @ #{scene.aperture}"
80
+ scenes << scene
81
+
82
+ end
83
+
84
+ if true
85
+
86
+ camera = Camera[/eastman/i] or raise "Can't find camera"
87
+ basic_scene.description = camera.name
88
+ basic_scene.camera = camera
89
+
90
+ aperture = camera.lens.max_aperture
91
+ while aperture <= camera.lens.min_aperture
92
+ scene = basic_scene.dup
93
+ camera.lens.aperture = aperture
94
+ # break if scene.time > 1.0/30
95
+ scene.description += ": #{camera.lens.focal_length} @ #{camera.lens.aperture}"
96
+ scenes << scene
97
+ aperture = Aperture.new_from_v(aperture.to_v + 1)
98
+ end
99
+
100
+ end
101
+
102
+ scenes.each do |scene|
103
+ scene.print; puts
104
+ end
105
+
106
+ # max_distance = scenes.map { |s| s.depth_of_field.far }.max
107
+ # max_distance = scenes.map { |s| s.hyperfocal_distance }.max
108
+ max_distance = 50.feet
109
+
110
+ camera_width = scenes.map { |s| s.focal_length }.max
111
+ camera_height = scenes.map { |s| [s.absolute_aperture, s.frame.height].max }.max
112
+
113
+ html = Builder::XmlMarkup.new(indent: 2)
114
+ html.declare!(:DOCTYPE, :html)
115
+ html.html do
116
+ html.head {}
117
+ html.body do
118
+ html.table do
119
+ scenes.each do |scene|
120
+ scene_view = SceneView.new(scene,
121
+ max_distance: max_distance,
122
+ camera_width: camera_width,
123
+ camera_height: camera_height)
124
+ html.tr do
125
+ html.td do
126
+ html << scene.description
127
+ end
128
+ html.td do
129
+ html << scene_view.to_svg
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ raise "Usage: #{$0} output-file.html" unless ARGV.first
138
+ File.open(ARGV.first, 'w') { |f| f.write(html.target!) }
139
+
140
+ end
141
+
142
+ end
143
+
144
+ end
145
+
146
+ end
@@ -0,0 +1,305 @@
1
+ require 'photo_utils/tool'
2
+
3
+ # require 'mini_exiftool'
4
+ # require 'pathname2'
5
+
6
+ module PhotoUtils
7
+
8
+ class Tools
9
+
10
+ class Compare < Tool
11
+
12
+ def run(args)
13
+ # given:
14
+ # an image file with EXIF data
15
+ # extract:
16
+ # aperture/speed/sensitivity/bias
17
+ # format (eg, APS-C)
18
+ # lens focal length (in that format)
19
+ # post-process exposure adjustment (from XMP data)
20
+ # estimate width/height
21
+ # desired minimum shutter
22
+ # calculate
23
+ # Ev100
24
+ # subject distance
25
+ # depth of field
26
+ # height or width (not given)
27
+ # focal length required in 35mm
28
+ # aperture required for equivalent depth of field
29
+ # (warn if shutter changes)
30
+ #
31
+ # scenes:
32
+ # stage performance at medium / far
33
+ # portrait at close / medium
34
+
35
+ #FIXME: Ugh
36
+
37
+ $min_sensitivity = Sensitivity.new(100)
38
+ $max_sensitivity = Sensitivity.new(1600)
39
+ $max_angle_of_view_delta = Angle.new(5)
40
+ $max_subject_distance_delta = 6.feet
41
+
42
+ base = Path.new('/Users/johnl/Pictures/Lightroom Burned Exports')
43
+
44
+ shots = %q{
45
+
46
+ # file width DoF description
47
+
48
+ 3787022130_ce334b0c20_o.jpg 56" 3' OCF poem typist: W/A closeup
49
+ 3894979751_8ef3683c78_o.jpg 54" 3' OCF poem typist: medium
50
+ 3711708327_917d493f1a_o.jpg 23" 1' OCF man with hat: torso
51
+ 58970238_dfe52a2c77_o.jpg 96" 3' backlit dancer: medium from medium
52
+ 3043470502_9b3406f4dd_o.jpg 81" 3' winged stripper
53
+ 3084813136_180bda6d84_o.jpg 75" 3' stripper moving away: close from close
54
+ IMG_2664.jpg 233" 10' yarddogs dancers: wide from far
55
+ IMG_2672.jpg 95" 10' yarddogs horns: medium from far
56
+ IMG_2864.jpg 49" 2' sideshow bug eating: close from near
57
+ IMG_2790.jpg 63" 3' sideshow ass: medium from near
58
+
59
+ 060512.031.jpg 233" 6' Japan: man on street at night
60
+ 060512.051.jpg 150' 50' Japan: Tokyo cityscape
61
+ 060512.139.jpg 15' 10' Japan: man in street
62
+ 060514.058.jpg 4' 1' Japan: bookstore
63
+ 060515.081.jpg 7' 4' Japan: plants at corner
64
+ 060519.001.jpg 14" 3" Japan: eggs
65
+ 060520.012.jpg 20' 10' Japan: haystack
66
+ 060524.023.jpg 9' 3' Japan: plants & rust
67
+ 060527.051.jpg 8' 3' Japan: racoon & bicycle
68
+ 060528.003.jpg 18" 6" Japan: tiny buddhas
69
+
70
+ }.split(/\n/).map { |line|
71
+ line.gsub!(/^\s+|\s+$/, '')
72
+ line.sub!(/#.*/, '')
73
+ if line.empty?
74
+ nil
75
+ else
76
+ file, width, dof, type = line.split(/\s+/, 4)
77
+ dof = Length.new(dof)
78
+ width = Length.new(width)
79
+ HashStruct.new(
80
+ type: type,
81
+ file: file,
82
+ subject_width: width,
83
+ desired_dof: dof)
84
+ end
85
+ }.compact
86
+
87
+ # FIXME: Ugh
88
+
89
+ def validate(scene, camera, lens, subject_distance_delta, angle_of_view_delta)
90
+
91
+ #
92
+ # validate subject distance delta
93
+ #
94
+
95
+ if subject_distance_delta > $max_subject_distance_delta
96
+ raise "subject distance delta too different (#{subject_distance_delta.to_s(:imperial)} > #{$max_subject_distance_delta.to_s(:imperial)})"
97
+ end
98
+
99
+ #
100
+ # validate aperture
101
+ #
102
+
103
+ if scene.aperture > lens.min_aperture || scene.aperture < lens.max_aperture
104
+ raise "aperture out of range (#{scene.aperture} != #{lens.max_aperture} .. #{lens.min_aperture})"
105
+ end
106
+
107
+ #
108
+ # validate angle of view
109
+ #
110
+
111
+ if angle_of_view_delta > $max_angle_of_view_delta
112
+ raise "angle of view too different (#{angle_of_view_delta} > #{$max_angle_of_view_delta})"
113
+ end
114
+
115
+ # if scene.angle_of_view - scene.angle_of_view > 5
116
+ # raise "angle of view too wide (#{scene2.angle_of_view} > #{scene.angle_of_view})"
117
+ # next
118
+ # end
119
+
120
+ #
121
+ # validate shutter
122
+ #
123
+
124
+ if scene.time > camera.max_shutter
125
+ raise "shutter too slow (#{scene.time} < #{camera.max_shutter})"
126
+ end
127
+
128
+ end
129
+
130
+ successes = {}
131
+
132
+ shots.each do |shot|
133
+
134
+ img = MiniExiftool.new(base + shot.file, numerical: true, timestamps: DateTime)
135
+
136
+ scene = Scene.new
137
+
138
+ model = img['Model']
139
+ scene.format = Format[model] or raise "Can't determine frame for model #{model.inspect} (#{shot.file})"
140
+
141
+ scene.description = "#{shot[:type]} [#{shot[:seq]}]"
142
+ scene.aperture = img['Aperture']
143
+ if img['ISO'].kind_of?(Numeric)
144
+ scene.sensitivity = img['ISO']
145
+ else # "0 800"
146
+ scene.sensitivity = img['ISO'].split(' ').last.to_f
147
+ end
148
+ scene.time = img['ExposureTime']
149
+ scene.focal_length = img['FocalLength']
150
+
151
+ exp_comp = img['ExposureCompensation'].to_f
152
+ if exp_comp != 0
153
+ scene.sensitivity = Sensitivity.new_from_v(scene.sensitivity.to_v + exp_comp)
154
+ end
155
+
156
+ # d = w * f / s
157
+ scene.subject_distance = Length.new(shot.subject_width * (scene.focal_length / scene.format.width))
158
+
159
+ puts
160
+ puts "--- #{scene.description}"
161
+ puts
162
+
163
+ scene.print_exposure
164
+ scene.print_depth_of_field
165
+ puts
166
+
167
+ # now compute equivalent scene for each camera
168
+
169
+ cameras.each do |camera|
170
+
171
+ scene2 = Scene.new
172
+ scene2.format = camera.format or raise "Unknown format: #{camera.format.inspect}"
173
+ scene2.aperture = scene.aperture
174
+ scene2.brightness = scene.brightness
175
+ # scene2.sensitivity = 400
176
+
177
+ # find the lens that would best fit
178
+
179
+ found = false
180
+
181
+ # NOTE: #uniq doesn't work well with delegate classes, so we cast the focal length to a float first
182
+ focal_lengths = camera.lenses.collect { |lens| lens.focal_length.to_f }.sort.reverse.uniq
183
+
184
+ focal_lengths.each do |focal_length|
185
+
186
+ lenses = camera.lenses.select { |lens| lens.focal_length == focal_length }.sort_by { |lens| lens.max_aperture }.reverse
187
+
188
+ lenses.each do |lens|
189
+
190
+ scene2.focal_length = lens.focal_length
191
+
192
+ # keeping subject width the same, compute new distance from given focal length
193
+
194
+ # o i
195
+ # --- = ---
196
+ # d f
197
+ #
198
+ # f = focal length
199
+ # d = subject distance
200
+ # o = subject dimension
201
+ # i = frame dimension
202
+
203
+ scene2.subject_distance = 1 / ((scene2.frame.width / scene2.focal_length) / shot.subject_width)
204
+
205
+ #
206
+ # calculate depth of field
207
+ #
208
+
209
+ near_limit = scene2.subject_distance - (shot.desired_dof / 2)
210
+ far_limit = scene2.subject_distance + (shot.desired_dof / 2)
211
+ scene2.aperture = scene2.aperture_for_depth_of_field(near_limit, far_limit)
212
+
213
+ #
214
+ # adjust aperture
215
+ #
216
+
217
+ # ;;a = scene2.aperture
218
+ # round aperture to closest 1/2 stop
219
+ scene2.aperture = Aperture.new_from_v((scene2.aperture.to_v / 0.5).round * 0.5)
220
+ # ;;puts "[1] #{a} => #{scene2.aperture}"
221
+ # clamp to maximum aperture
222
+ scene2.aperture = [scene2.aperture, lens.max_aperture].max
223
+ # ;;puts "[2] #{a} => #{scene2.aperture}"
224
+
225
+ #
226
+ # calculate sensitivity
227
+ #
228
+
229
+ # start with minimum shutter time
230
+ scene2.time = camera.max_shutter
231
+ # round up to the next ISO value
232
+ scene2.sensitivity = Sensitivity.new_from_v(scene2.sensitivity.to_v.ceil)
233
+ scene2.sensitivity = [scene2.sensitivity, $max_sensitivity].min
234
+ scene2.sensitivity = [scene2.sensitivity, $min_sensitivity].max
235
+ # # force recalculation of shutter
236
+ # scene2.time = nil
237
+
238
+ #
239
+ # compute subject distance difference
240
+ #
241
+
242
+ subject_distance_delta = scene.subject_distance - scene2.subject_distance
243
+
244
+ #
245
+ # compute angle of view difference
246
+ #
247
+
248
+ angle_of_view_delta = Angle.new(scene.angle_of_view - scene2.angle_of_view)
249
+
250
+ #
251
+ # validate
252
+ #
253
+
254
+ begin
255
+ validate(scene2, camera, lens, subject_distance_delta, angle_of_view_delta)
256
+ rescue => e
257
+ failure = e
258
+ end
259
+
260
+ puts " %-15.15s | %-19.19s | %10s @ %5s @ %8s | dist: %6s (%s%6s) | dof: %6s (-%5s .. +%5s) | %s" % [
261
+ camera.name,
262
+ lens.name,
263
+ scene2.aperture,
264
+ scene2.time,
265
+ scene2.sensitivity,
266
+ scene2.subject_distance.to_s(:imperial),
267
+ subject_distance_delta < 0 ? '-' : '+',
268
+ subject_distance_delta.abs.to_s(:imperial),
269
+ scene2.total_depth_of_field.to_s(:imperial),
270
+ scene2.near_distance_from_subject.to_s(:imperial),
271
+ scene2.far_distance_from_subject.to_s(:imperial),
272
+ failure || 'GOOD'
273
+ ] if options[:verbose] || !failure
274
+
275
+ successes[camera.name] ||= {}
276
+
277
+ if !failure
278
+ successes[camera.name][lens.name] ||= 0
279
+ successes[camera.name][lens.name] += 1
280
+ found = true
281
+ break
282
+ end
283
+
284
+ end
285
+
286
+ break if found
287
+
288
+ end
289
+
290
+ unless found
291
+ successes[camera.name]['FAILED'] ||= 0
292
+ successes[camera.name]['FAILED'] += 1
293
+ end
294
+
295
+ end
296
+
297
+ end
298
+
299
+ ;;pp successes
300
+ # ;;successes.sort_by { |k,v| v }.each { |k,v| puts "%2d: %s" % [v, k] }
301
+ end
302
+
303
+ end
304
+ end
305
+ end