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,50 @@
1
+ module PhotoUtils
2
+
3
+ class Lens
4
+
5
+ attr_accessor :name
6
+ attr_reader :focal_length
7
+ attr_accessor :min_aperture
8
+ attr_accessor :max_aperture
9
+ attr_accessor :aperture
10
+
11
+ def initialize(params={})
12
+ params.each { |k, v| send("#{k}=", v) }
13
+ @aperture = @max_aperture
14
+ end
15
+
16
+ def inspect
17
+ "<#{self.class}: name=#{@name.inspect}, focal_length=#{@focal_length.inspect}, min_aperture=#{@min_aperture.inspect}, max_aperture=#{@max_aperture.inspect}, aperture=#{@aperture.inspect}>"
18
+ end
19
+
20
+ def to_s
21
+ if @name
22
+ "#{@name} (#{@focal_length})"
23
+ else
24
+ @focal_length.to_s
25
+ end
26
+ end
27
+
28
+ def name
29
+ @name || @focal_length.to_s
30
+ end
31
+
32
+ def focal_length=(f)
33
+ @focal_length = Length.new(f)
34
+ end
35
+
36
+ def min_aperture=(a)
37
+ @min_aperture = Aperture.new(a)
38
+ end
39
+
40
+ def max_aperture=(a)
41
+ @max_aperture = Aperture.new(a)
42
+ end
43
+
44
+ def aperture=(a)
45
+ @aperture = Aperture.new(a)
46
+ end
47
+
48
+ end
49
+
50
+ end
@@ -0,0 +1,180 @@
1
+ module PhotoUtils
2
+
3
+ class Scene
4
+
5
+ attr_accessor :description
6
+ attr_accessor :subject_distance
7
+ attr_accessor :background_distance
8
+ attr_accessor :camera
9
+ attr_accessor :sensitivity
10
+ attr_accessor :brightness
11
+
12
+ def initialize
13
+ {
14
+ background_distance: Math::Infinity,
15
+ sensitivity: 100,
16
+ brightness: 100,
17
+ }.each { |k, v| send("#{k}=", v) }
18
+ end
19
+
20
+ def sensitivity=(s)
21
+ @sensitivity = Sensitivity.new(s)
22
+ end
23
+
24
+ def brightness=(b)
25
+ @brightness = Brightness.new(b)
26
+ end
27
+
28
+ def subject_distance=(s)
29
+ @subject_distance = Length.new(s)
30
+ end
31
+
32
+ def background_distance=(s)
33
+ @background_distance = Length.new(s)
34
+ end
35
+
36
+ def circle_of_confusion
37
+ # http://en.wikipedia.org/wiki/Circle_of_confusion
38
+ @camera.format.frame.diagonal / 1750
39
+ end
40
+
41
+ def aperture_for_depth_of_field(near_limit, far_limit)
42
+ a = ((@camera.lens.focal_length ** 2) / circle_of_confusion) * ((far_limit - near_limit) / (2 * near_limit * far_limit))
43
+ Aperture.new(a)
44
+ end
45
+
46
+ def hyperfocal_distance
47
+ # http://en.wikipedia.org/wiki/Hyperfocal_distance
48
+ raise "Need focal length, aperture, and circle of confusion to determine hyperfocal distance" \
49
+ unless @camera.lens.focal_length && @camera.lens.aperture && circle_of_confusion
50
+ h = ((@camera.lens.focal_length ** 2) / (@camera.lens.aperture * circle_of_confusion)) + @camera.lens.focal_length
51
+ Length.new(h)
52
+ end
53
+
54
+ def depth_of_field
55
+ h = hyperfocal_distance
56
+ s = subject_distance
57
+ dof = HashStruct.new
58
+ dof.near = (h * s) / (h + s)
59
+ if s < h
60
+ dof.far = (h * s) / (h - s)
61
+ else
62
+ dof.far = Math::Infinity
63
+ end
64
+ dof.near = Length.new(dof.near)
65
+ dof.far = Length.new(dof.far)
66
+ dof
67
+ end
68
+
69
+ def near_distance_from_subject
70
+ d = subject_distance - depth_of_field.near
71
+ Length.new(d)
72
+ end
73
+
74
+ def far_distance_from_subject
75
+ d = (depth_of_field.far == Math::Infinity) ? Math::Infinity : (depth_of_field.far - subject_distance)
76
+ Length.new(d)
77
+ end
78
+
79
+ def total_depth_of_field
80
+ d = (depth_of_field.far == Math::Infinity) ? Math::Infinity : (depth_of_field.far - depth_of_field.near)
81
+ Length.new(d)
82
+ end
83
+
84
+ def field_of_view(distance)
85
+ raise "Need focal length and format size to determine field of view" unless @camera.lens.focal_length && @camera.format
86
+ @camera.format.field_of_view(@camera.lens.focal_length, distance)
87
+ end
88
+
89
+ def magnification
90
+ # http://en.wikipedia.org/wiki/Depth_of_field#Hyperfocal_magnification
91
+ @camera.lens.focal_length / (subject_distance - @camera.lens.focal_length)
92
+ end
93
+
94
+ def subject_distance_for_field_of_view(fov)
95
+ d_w = fov.width / @camera.format.frame.width * @camera.lens.focal_length
96
+ d_h = fov.height / @camera.format.frame.height * @camera.lens.focal_length
97
+ [d_w, d_h].max
98
+ end
99
+
100
+ # AKA bellows factor
101
+
102
+ def working_aperture
103
+ # http://en.wikipedia.org/wiki/F-number#Working_f-number
104
+ Aperture.new((1 - magnification) * @camera.lens.aperture)
105
+ end
106
+
107
+ def blur_at_distance(d)
108
+ # http://en.wikipedia.org/wiki/Depth_of_field#Foreground_and_background_blur
109
+ xd = (d - subject_distance).abs
110
+ b = (@camera.lens.focal_length * magnification) / @camera.lens.aperture
111
+ if d < subject_distance
112
+ b *= xd / (subject_distance - xd)
113
+ else
114
+ b *= xd / (subject_distance + xd)
115
+ end
116
+ # diameter of blur disk, in mm
117
+ Length.new(b.mm)
118
+ end
119
+
120
+ def absolute_aperture
121
+ @camera.lens.aperture.absolute(@camera.lens.focal_length)
122
+ end
123
+
124
+ def exposure
125
+ Exposure.new(
126
+ light: @brightness,
127
+ sensitivity: @sensitivity,
128
+ aperture: @camera.lens.aperture,
129
+ time: @camera.shutter)
130
+ end
131
+
132
+ def set_exposure
133
+ exp = exposure
134
+ @camera.lens.aperture = exp.aperture
135
+ @camera.shutter = exp.time
136
+ end
137
+
138
+ def print_camera(io=STDOUT)
139
+ io.puts "CAMERA:"
140
+ io.puts " name: #{@camera.name}"
141
+ io.puts " format: #{@camera.format} (35mm crop factor: #{@camera.format.crop_factor.format(10)})"
142
+ io.puts " shutter range: #{@camera.max_shutter} ~ #{@camera.min_shutter}"
143
+ io.puts " aperture range: #{@camera.lens.max_aperture} ~ #{@camera.lens.min_aperture}"
144
+ io.puts " lens: #{@camera.lens.name} - #{@camera.lens.focal_length} (#{
145
+ %w{35 6x4.5 6x6 6x7 5x7}.map { |f| "#{f}: #{@camera.format.focal_length_equivalent(@camera.lens.focal_length, Format[f])}" }.join(', ')
146
+ })"
147
+ io.puts " angle of view: #{@camera.angle_of_view}"
148
+ io.puts " shutter: #{@camera.shutter}"
149
+ io.puts " aperture: #{@camera.lens.aperture}"
150
+ io.puts
151
+ end
152
+
153
+ def print_exposure(io=STDOUT)
154
+ exposure.print(io)
155
+ end
156
+
157
+ def print_depth_of_field(io=STDOUT)
158
+ io.puts "FIELD:"
159
+ io.puts " subject dist: #{subject_distance.to_s(:imperial)}"
160
+ io.puts " subject FOV: #{field_of_view(subject_distance).to_s(:imperial)}"
161
+ io.puts " subject mag: #{'%.2f' % magnification}x"
162
+ io.puts " subject DOF: #{total_depth_of_field.to_s(:imperial)} (-#{near_distance_from_subject.to_s(:imperial)}/+#{far_distance_from_subject.to_s(:imperial)})"
163
+ io.puts " background dist: #{background_distance.to_s(:imperial)}"
164
+ if background_distance != Math::Infinity
165
+ io.puts " background FOV: #{field_of_view(background_distance).to_s(:imperial)}"
166
+ io.puts " background blur: #{blur_at_distance(background_distance).to_s(:metric)}"
167
+ end
168
+ io.puts " hyperfocal dist: #{hyperfocal_distance.to_s(:imperial)}"
169
+ io.puts " working aperture: #{working_aperture}"
170
+ io.puts
171
+ end
172
+
173
+ def print(io=STDOUT)
174
+ print_depth_of_field(io)
175
+ print_exposure(io)
176
+ end
177
+
178
+ end
179
+
180
+ end
@@ -0,0 +1,134 @@
1
+ require 'photo_utils'
2
+
3
+ module PhotoUtils
4
+
5
+ class SceneView
6
+
7
+ attr_accessor :scene
8
+ attr_accessor :width
9
+ attr_accessor :height
10
+ attr_accessor :max_distance
11
+ attr_accessor :camera_width
12
+ attr_accessor :camera_height
13
+ attr_accessor :scale
14
+
15
+ def initialize(scene, options={})
16
+ @scene = scene
17
+ @width = options[:width] || 900
18
+ @height = options[:height] || 50
19
+ @max_distance = options[:max_distance] || @scene.depth_of_field.far
20
+ @camera_width = options[:camera_width] || @scene.focal_length
21
+ @camera_height = options[:camera_height] || [@scene.absolute_aperture, @scene.format.height].max
22
+ @scale = (@width.to_f - @height) / (@camera_width + @max_distance)
23
+ @camera_scale = [
24
+ @height.to_f / @camera_width,
25
+ @height.to_f / @camera_height
26
+ ].min
27
+ end
28
+
29
+ def to_svg
30
+ xml = Builder::XmlMarkup.new(indent: 2)
31
+ xml.svg(width: @width, height: @height) do
32
+ xml.defs do
33
+ 1.upto(9).each do |std_dev|
34
+ xml.filter(id: "gb#{std_dev}") do
35
+ xml.feGaussianBlur(in: 'SourceGraphic', stdDeviation: std_dev)
36
+ end
37
+ end
38
+ end
39
+ xml.g(transform: "translate(#{@height},0)") do
40
+ draw_camera(xml)
41
+ draw_dof(xml)
42
+ draw_subject(xml)
43
+ # draw_hyperfocal(xml)
44
+ end
45
+ end
46
+ xml.target!
47
+ end
48
+
49
+ def draw_camera(xml)
50
+ fh2 = (@scene.format.height / 2) * @camera_scale
51
+ aa2 = (@scene.absolute_aperture / 2) * @camera_scale
52
+ points = [
53
+ [0, -fh2],
54
+ [@scene.focal_length * @camera_scale, -aa2],
55
+ [@scene.focal_length * @camera_scale, aa2],
56
+ [0, fh2],
57
+ ]
58
+ xml.g(transform: "translate(0,#{@height / 2})") do
59
+ xml.polygon(
60
+ x: -@height,
61
+ y: 0,
62
+ points: points.map { |p| p.join(',') }.join(' '),
63
+ fill: 'black')
64
+ end
65
+ end
66
+
67
+ def draw_dof(xml)
68
+ # blur
69
+
70
+ if true
71
+
72
+ step = @max_distance / 20
73
+ step.step(@max_distance, step).map { |d| Length.new(d) }.each do |d|
74
+ blur = @scene.blur_at_distance(d)
75
+ if blur == 0
76
+ std_dev = 0
77
+ else
78
+ ratio = @scene.circle_of_confusion / blur
79
+ std_dev = [9 - (ratio * 10).to_i, 0].max
80
+ end
81
+ xml.circle(
82
+ cx: d * @scale,
83
+ cy: @height / 2,
84
+ r: (step * @scale) / 2 / 2,
85
+ fill: 'black',
86
+ filter: (std_dev > 0) ? "url(\#gb#{std_dev})" : ())
87
+ end
88
+
89
+ else
90
+ step = (@max_distance / @width) * 10
91
+ 0.step(@max_distance, step).map { |d| Length.new(d) }.each do |distance|
92
+ blur = @scene.blur_at_distance(distance)
93
+ opacity = [1, @scene.circle_of_confusion / blur].min
94
+ xml.rect(
95
+ x: distance * @scale,
96
+ y: (@height - (@scene.field_of_view(distance).height * @scale)) / 2,
97
+ width: step * @scale,
98
+ height: @scene.field_of_view(distance).height * @scale,
99
+ fill: 'blue',
100
+ 'fill-opacity' => opacity)
101
+ end
102
+ end
103
+
104
+ # depth of focus area
105
+ xml.rect(
106
+ x: @scene.depth_of_field.near * @scale,
107
+ y: 0,
108
+ width: @scene.total_depth_of_field * @scale,
109
+ height: @height,
110
+ stroke: 'blue',
111
+ fill: 'none')
112
+ end
113
+
114
+ def draw_subject(xml)
115
+ xml.rect(
116
+ x: @scene.subject_distance * @scale,
117
+ y: 0,
118
+ width: 1,
119
+ height: @height,
120
+ fill: 'red')
121
+ end
122
+
123
+ def draw_hyperfocal(xml)
124
+ xml.line(
125
+ x1: @scene.hyperfocal_distance * scale,
126
+ y1: 0,
127
+ x2: @scene.hyperfocal_distance * scale,
128
+ y2: @height,
129
+ stroke: 'green')
130
+ end
131
+
132
+ end
133
+
134
+ end
@@ -0,0 +1,44 @@
1
+ module PhotoUtils
2
+
3
+ class Sensitivity < Value
4
+
5
+ C = 3.125
6
+
7
+ def self.new_from_iso(n)
8
+ new(n)
9
+ end
10
+
11
+ def self.new_from_v(v)
12
+ new(C * (2 ** v.to_f))
13
+ end
14
+
15
+ def to_v
16
+ Math.log2(self / C)
17
+ end
18
+
19
+ def to_iso
20
+ to_f
21
+ end
22
+
23
+ def format_iso
24
+ 'ISO ' + to_iso.format(10)
25
+ end
26
+
27
+ def format_value
28
+ "Sv:#{to_v.format}"
29
+ end
30
+
31
+ def to_s(format=:iso)
32
+ case format
33
+ when :iso
34
+ format_iso
35
+ when :value
36
+ format_value
37
+ else
38
+ raise "Unknown format: #{format.inspect}"
39
+ end
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -0,0 +1,53 @@
1
+ module PhotoUtils
2
+
3
+ class Time < Value
4
+
5
+ def self.new_from_seconds(s)
6
+ new(s)
7
+ end
8
+
9
+ def self.new_from_v(v)
10
+ new(2 ** -v.to_f)
11
+ end
12
+
13
+ def to_v
14
+ -Math.log2(self.to_f)
15
+ end
16
+
17
+ def to_seconds
18
+ to_f
19
+ end
20
+
21
+ def format_seconds
22
+ if (seconds = to_seconds) < 1
23
+ '1/' + (1/self).format(1)
24
+ else
25
+ seconds.format
26
+ end + 's'
27
+ end
28
+
29
+ def format_value
30
+ "Tv:#{to_v.format(10)}"
31
+ end
32
+
33
+ def to_s(format=:seconds)
34
+ case format
35
+ when :seconds
36
+ format_seconds
37
+ when :value
38
+ format_value
39
+ else
40
+ raise "Unknown format: #{format.inspect}"
41
+ end
42
+ end
43
+
44
+ # http://www.apug.org/forums/forum37/22334-fuji-neopan-400-reciprocity-failure-data.html
45
+
46
+ def reciprocity
47
+ tc = self + (0.3 * (self ** 1.62))
48
+ Time.new(tc)
49
+ end
50
+
51
+ end
52
+
53
+ end