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.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/Gemfile +4 -0
- data/README.rdoc +1 -0
- data/Rakefile +1 -0
- data/TODO.txt +34 -0
- data/bin/photo-util +39 -0
- data/lib/photo_utils.rb +28 -0
- data/lib/photo_utils/angle.rb +17 -0
- data/lib/photo_utils/aperture.rb +62 -0
- data/lib/photo_utils/apex.rb +210 -0
- data/lib/photo_utils/brightness.rb +49 -0
- data/lib/photo_utils/camera.rb +82 -0
- data/lib/photo_utils/compensation.rb +29 -0
- data/lib/photo_utils/extensions/array.rb +11 -0
- data/lib/photo_utils/extensions/float.rb +18 -0
- data/lib/photo_utils/extensions/math.rb +11 -0
- data/lib/photo_utils/extensions/numeric.rb +23 -0
- data/lib/photo_utils/formats.rb +146 -0
- data/lib/photo_utils/frame.rb +36 -0
- data/lib/photo_utils/illuminance.rb +48 -0
- data/lib/photo_utils/length.rb +92 -0
- data/lib/photo_utils/lens.rb +50 -0
- data/lib/photo_utils/scene.rb +180 -0
- data/lib/photo_utils/scene_view.rb +134 -0
- data/lib/photo_utils/sensitivity.rb +44 -0
- data/lib/photo_utils/time.rb +53 -0
- data/lib/photo_utils/tool.rb +17 -0
- data/lib/photo_utils/tools/blur.rb +43 -0
- data/lib/photo_utils/tools/brightness.rb +20 -0
- data/lib/photo_utils/tools/calc_aperture.rb +45 -0
- data/lib/photo_utils/tools/cameras.rb +24 -0
- data/lib/photo_utils/tools/chart_dof.rb +146 -0
- data/lib/photo_utils/tools/compare.rb +305 -0
- data/lib/photo_utils/tools/dof.rb +42 -0
- data/lib/photo_utils/tools/dof_table.rb +45 -0
- data/lib/photo_utils/tools/film_test.rb +57 -0
- data/lib/photo_utils/tools/focal_length.rb +25 -0
- data/lib/photo_utils/tools/reciprocity.rb +36 -0
- data/lib/photo_utils/tools/test.rb +91 -0
- data/lib/photo_utils/value.rb +37 -0
- data/lib/photo_utils/version.rb +5 -0
- data/photo-utils.gemspec +29 -0
- data/test/aperture_test.rb +40 -0
- data/test/apex_test.rb +28 -0
- data/test/brightness_test.rb +36 -0
- data/test/length_test.rb +40 -0
- data/test/scene_test.rb +25 -0
- data/test/sensitivity_test.rb +36 -0
- data/test/time_test.rb +42 -0
- 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
|