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,82 @@
1
+ module PhotoUtils
2
+
3
+ class Camera
4
+
5
+ CAMERAS_PATH = Path.new(ENV['HOME']) / '.cameras.rb'
6
+
7
+ def self.cameras
8
+ unless class_variable_defined?('@@cameras')
9
+ if CAMERAS_PATH.exist?
10
+ @@cameras = eval(CAMERAS_PATH.read)
11
+ end
12
+ end
13
+ @@cameras
14
+ end
15
+
16
+ def self.find(params)
17
+ if (sel = params[:name])
18
+ cameras.find { |c| sel === c.name }
19
+ else
20
+ raise "Don't know how to search for camera with params: #{params.inspect}"
21
+ end
22
+ end
23
+
24
+ def self.[](name)
25
+ find(name: name)
26
+ end
27
+
28
+ attr_accessor :name
29
+ attr_accessor :formats
30
+ attr_accessor :format
31
+ attr_accessor :min_shutter
32
+ attr_accessor :max_shutter
33
+ attr_accessor :shutter
34
+ attr_accessor :lenses
35
+ attr_accessor :lens
36
+
37
+ def initialize(params={})
38
+ if params[:format]
39
+ params[:formats] = [params.delete(:format)]
40
+ end
41
+ params.each { |k, v| send("#{k}=", v) }
42
+ @format ||= @formats.first
43
+ normal = @format.frame.diagonal
44
+ # set the lens to the one closest to normal (diagonal of frame)
45
+ @lens = @lenses.sort_by { |l| (normal - l.focal_length).abs }.first
46
+ @shutter = @max_shutter
47
+ end
48
+
49
+ def min_shutter=(t)
50
+ @min_shutter = t ? Time.new(t) : nil
51
+ end
52
+
53
+ def max_shutter=(t)
54
+ @max_shutter = t ? Time.new(t) : nil
55
+ end
56
+
57
+ def shutter=(t)
58
+ @shutter = t ? Time.new(t) : nil
59
+ end
60
+
61
+ def angle_of_view
62
+ raise "Need focal length and format size to determine angle of view" unless @lens && @lens.focal_length && @format
63
+ @format.angle_of_view(@lens.focal_length)
64
+ end
65
+
66
+ def print(io=STDOUT)
67
+ io.puts "#{@name}: format: #{@format}, shutter: #{@max_shutter} - #{@min_shutter}"
68
+ @lenses.sort_by(&:focal_length).each do |lens|
69
+ io.puts "\t%s %s: focal length: %s (35mm equiv: %s), aperture: %s~%s" % [
70
+ lens == self.lens ? '*' : ' ',
71
+ lens.name,
72
+ lens.focal_length.format_metric(1),
73
+ @format.focal_length_equivalent(lens.focal_length).format_metric(1),
74
+ lens.max_aperture,
75
+ lens.min_aperture,
76
+ ]
77
+ end
78
+ end
79
+
80
+ end
81
+
82
+ end
@@ -0,0 +1,29 @@
1
+ module PhotoUtils
2
+
3
+ class Compensation < Value
4
+
5
+ # amount specified in stops
6
+
7
+ def self.new_from_v(v)
8
+ new(v)
9
+ end
10
+
11
+ def format_value
12
+ "Cv:%s%s" % [
13
+ (self < 0) ? '-' : '+',
14
+ abs.format(10)
15
+ ]
16
+ end
17
+
18
+ def to_s(format=:value)
19
+ case format
20
+ when :value
21
+ format_value
22
+ else
23
+ raise "Unknown format: #{format.inspect}"
24
+ end
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,11 @@
1
+ class Array
2
+
3
+ def sum
4
+ inject(nil) { |sum, x| sum ? sum + x : x }
5
+ end
6
+
7
+ def mean
8
+ sum / size
9
+ end
10
+
11
+ end
@@ -0,0 +1,18 @@
1
+ class Float
2
+
3
+ def prec(x)
4
+ if self == self.round
5
+ self
6
+ else
7
+ ('%.*f' % [x, self]).to_f
8
+ end
9
+ end
10
+
11
+ def format(precision=nil)
12
+ '%.*f' % [
13
+ (precision.nil? || self < precision) ? 1 : 0,
14
+ self,
15
+ ]
16
+ end
17
+
18
+ end
@@ -0,0 +1,11 @@
1
+ module Math
2
+
3
+ NaN = 0.0/0
4
+ Infinity = 1.0/0
5
+ MinusInfinity = -Infinity
6
+
7
+ def self.arcdeg(a)
8
+ a * (180 / PI)
9
+ end
10
+
11
+ end
@@ -0,0 +1,23 @@
1
+ require 'photo_utils'
2
+
3
+ INCHES_PER_METER = 39.3700787402
4
+
5
+ class Numeric
6
+
7
+ def feet
8
+ PhotoUtils::Length.new(self.to_f * 12 / INCHES_PER_METER * 1000)
9
+ end
10
+
11
+ def inches
12
+ PhotoUtils::Length.new(self.to_f / INCHES_PER_METER * 1000)
13
+ end
14
+
15
+ def meters
16
+ PhotoUtils::Length.new(self * 1000)
17
+ end
18
+
19
+ def mm
20
+ PhotoUtils::Length.new(self)
21
+ end
22
+
23
+ end
@@ -0,0 +1,146 @@
1
+ module PhotoUtils
2
+
3
+ class Format
4
+
5
+ # http://photo.net/medium-format-photography-forum/00LZPS
6
+ # http://www.largeformatphotography.info/forum/showthread.php?t=2503
7
+ # http://www.kenrockwell.com/tech/format.htm#120
8
+ # http://www.mamiya.com/rb67-pro-sd-accessories-film-magazines,-holders-inserts-roll-film-magazines.html
9
+ # from http://en.wikipedia.org/wiki/Image_sensor_format
10
+
11
+ # name height width aliases
12
+ FormatDescriptions = %q{
13
+ 1/6 2.4 1.8
14
+ 1/4 2.7 3.6
15
+ 1/3.6 3 4
16
+ 1/3.2 3.42 4.54
17
+ 1/3 3.6 4.8
18
+ 1/2.7 4.04 5.37
19
+ 1/2.5 4.29 5.76
20
+ 1/2 4.8 6.4
21
+ 1/1.8 5.32 7.18
22
+ 1/1.7 5.7 7.6 Canon PowerShot G9, Canon PowerShot G10
23
+ 1/1.6 6.01 8.08
24
+ 2/3 9.6 12.8
25
+ 1 9.6 12.8
26
+ 4/3 13 17.3
27
+
28
+ APS-C 14.8 22.2 Canon EOS DIGITAL REBEL XT, Canon EOS DIGITAL REBEL XTi, Canon EOS DIGITAL REBEL XSi, Canon EOS 450D
29
+ APS-H 19.1 28.7
30
+ R-D1 15.6 23.7 Epson R-D1, Epson R-D1s, Epson R-D1g, Epson R-D1x
31
+
32
+ DX 15.5 23.6 NIKON D70
33
+
34
+ Leica S2 30 45
35
+
36
+ 35 24 36 FF, Canon EOS 5D
37
+
38
+ 6x4.5 56 42 645
39
+ 6x4.5 short 56 40.5
40
+ 6x4.5 Mamiya RB/RZ67 56 41.5
41
+ 6x6 56 56
42
+ 6x6 short 56 54
43
+ 6x7 56 72
44
+ 6x7 short 56 69.5
45
+ 6x7 Mamiya RB/RZ67 56 69.5
46
+ 6x7 Toyo 56 67
47
+ 6x7 Horseman 56 68
48
+ 6x7 Pentax 55 70
49
+ 6x7 Cambo-Sinar-Wista 56 70
50
+ 6x7 Linhof Super Rollex 56 72
51
+ 6x7 Linhof Rapid Rollex 57 76
52
+ 6x8 56 76
53
+ 6x9 56 84
54
+ 6x9 23 Graphic 56 83
55
+ 6x9 Cambo-Horseman-Wista 56 82
56
+ 6x9 Toyo 56 84
57
+ 6x9 Linhof 56 85
58
+ 6x9 Sinar 57 88
59
+ 6x10 56 92
60
+ 6x12 56 112
61
+ 6x12 Linhof 57 120
62
+ 6x17 56 168
63
+ 6x17 56 168
64
+
65
+ Polaroid 660 73 95
66
+ Polaroid 660 on Mamiya RB/RZ67 73 73
67
+
68
+ Polaroid 550 92 126
69
+ Polaroid 545 95 122
70
+ 4x5 Quickload 95 120
71
+ 4x5 Fidelity 97 120 4x5
72
+
73
+ 5x7 127 178
74
+
75
+ 8x10 203 254
76
+ }
77
+
78
+ def self.load_formats
79
+ @@formats = {}
80
+ FormatDescriptions.split("\n").each do |line|
81
+ case line.sub(/#.*/, '').strip
82
+ when /^(.*?)\s{2,}([\d\.]+)\s+([\d\.]+)\s*(.*?)$/
83
+ name, height, width, aliases = $1, $2, $3, $4
84
+ frame = Frame.new(width.to_f, height.to_f)
85
+ format = Format.new(name: name, frame: frame)
86
+ @@formats[name] = format
87
+ aliases.split(/,\s*/).each { |a| @@formats[a] = format }
88
+ when ''
89
+ # ignore blank line
90
+ else
91
+ raise "Can't parse format line: #{line.inspect}"
92
+ end
93
+ end
94
+ end
95
+
96
+ def self.[](name)
97
+ load_formats unless class_variable_defined?('@@formats')
98
+ @@formats[name]
99
+ end
100
+
101
+ attr_accessor :name
102
+ attr_accessor :frame
103
+
104
+ def initialize(params={})
105
+ params.each { |k, v| send("#{k}=", v) }
106
+ end
107
+
108
+ def inspect
109
+ "<#{self.class} name=#{@name.inspect} frame=#{@frame.inspect}>"
110
+ end
111
+
112
+ def to_s(short=true)
113
+ if short
114
+ @name
115
+ else
116
+ "#{@name} (#{@frame})"
117
+ end
118
+ end
119
+
120
+ def focal_length_equivalent(focal_length, other=Format['35'])
121
+ f = focal_length * crop_factor(other)
122
+ Length.new(f)
123
+ end
124
+
125
+ def crop_factor(reference=Format['35'])
126
+ # http://en.wikipedia.org/wiki/Crop_factor
127
+ reference.frame.diagonal / @frame.diagonal
128
+ end
129
+
130
+ def angle_of_view(focal_length)
131
+ # http://imaginatorium.org/stuff/angle.htm
132
+ # http://en.wikipedia.org/wiki/Angle_of_view
133
+ a = Math.arcdeg(2 * Math.atan(@frame.diagonal / (2 * focal_length)))
134
+ Angle.new(a)
135
+ end
136
+
137
+ def field_of_view(focal_length, subject_distance)
138
+ # http://en.wikipedia.org/wiki/Field_of_view
139
+ Frame.new(
140
+ subject_distance * (@frame.height / focal_length),
141
+ subject_distance * (@frame.width / focal_length))
142
+ end
143
+
144
+ end
145
+
146
+ end
@@ -0,0 +1,36 @@
1
+ module PhotoUtils
2
+
3
+ # http://photo.net/medium-format-photography-forum/00QiiV
4
+
5
+ class Frame
6
+
7
+ attr_accessor :height
8
+ attr_accessor :width
9
+
10
+ def initialize(height, width)
11
+ # correct swapped values
12
+ width, height = height, width if height < width
13
+ @height = Length.new(height)
14
+ @width = Length.new(width)
15
+ end
16
+
17
+ def inspect
18
+ "<#{self.class} height=#{@height.inspect} width=#{@width.inspect}>"
19
+ end
20
+
21
+ def to_s(format=:metric)
22
+ if @height == Math::Infinity && @width == Math::Infinity
23
+ "n/a"
24
+ else
25
+ "#{@height.to_s(format)} x #{@width.to_s(format)}"
26
+ end
27
+ end
28
+
29
+ def diagonal
30
+ d = Math.sqrt((@height ** 2) + (@width ** 2))
31
+ Length.new(d)
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,48 @@
1
+ module PhotoUtils
2
+
3
+ class Illuminance < Value
4
+
5
+ N = 2 ** Rational(-7, 4)
6
+ C = 224
7
+ NC = N * C
8
+
9
+ # amount specified in lux
10
+
11
+ def self.new_from_lux(lux)
12
+ new(lux)
13
+ end
14
+
15
+ def self.new_from_v(v)
16
+ new((2 ** v.to_f) * NC)
17
+ end
18
+
19
+ def to_v
20
+ Math.log2(self / NC)
21
+ end
22
+
23
+ def to_lux
24
+ to_f
25
+ end
26
+
27
+ def format_lux
28
+ to_lux.format(10) + ' lux'
29
+ end
30
+
31
+ def format_value
32
+ "Iv:#{to_v.format(10)}"
33
+ end
34
+
35
+ def to_s(format=:lux)
36
+ case format
37
+ when :lux
38
+ format_lux
39
+ when :value
40
+ format_value
41
+ else
42
+ raise "Unknown format: #{format.inspect}"
43
+ end
44
+ end
45
+
46
+ end
47
+
48
+ end
@@ -0,0 +1,92 @@
1
+ require 'delegate'
2
+
3
+ module PhotoUtils
4
+
5
+ class Length < DelegateClass(Float)
6
+
7
+ def initialize(x)
8
+ case x
9
+ when Length
10
+ super(x)
11
+ when Numeric
12
+ super(x.to_f)
13
+ when '∞'
14
+ super(Math::Infinity)
15
+ when String
16
+ n = 0
17
+ s = x.dup
18
+ until s.empty?
19
+ if s.gsub!(/^\s*(\d+(\.\d+)?)\s*/, '')
20
+ n2 = $1.to_f
21
+ n2 = if s.gsub!(/^('|ft\b)/, '')
22
+ n2.feet
23
+ elsif s.gsub!(/^("|in\b)/, '')
24
+ n2.inches
25
+ elsif s.gsub!(/^\s*m\b/, '')
26
+ n2.meters
27
+ elsif s.gsub!(/^\s*(mm\b)?/, '')
28
+ n2.mm
29
+ else
30
+ raise "Can't parse unit: #{s.inspect}"
31
+ end
32
+ n += n2
33
+ else
34
+ raise "Can't parse number: #{s.inspect}"
35
+ end
36
+ end
37
+ super(n)
38
+ else
39
+ raise "Can't make length from #{x.class}: #{x.inspect}"
40
+ end
41
+ end
42
+
43
+ def format_imperial
44
+ inches = self * INCHES_PER_METER / 1000
45
+ if inches.floor >= 12
46
+ feet = (inches / 12).floor
47
+ inches %= 12
48
+ else
49
+ feet = 0
50
+ end
51
+ if inches > 0
52
+ inches = inches.ceil
53
+ if inches == 12
54
+ feet += 1
55
+ inches = 0
56
+ end
57
+ end
58
+ (feet > 0 ? "#{feet}'" : '') + (inches > 0 ? "#{inches}\"" : '')
59
+ end
60
+
61
+ def format_metric(precision=nil)
62
+ if self >= 1000
63
+ "#{(self / 1000).format(precision)}m"
64
+ else
65
+ "#{format(precision)}mm"
66
+ end
67
+ end
68
+
69
+ def to_s(format=:metric)
70
+ if self == Math::Infinity
71
+ '∞'
72
+ else
73
+ case format
74
+ when :imperial
75
+ format_imperial
76
+ when :metric
77
+ format_metric
78
+ end
79
+ end
80
+ end
81
+
82
+ def -(other)
83
+ self.class.new(super(other))
84
+ end
85
+
86
+ def abs
87
+ self.class.new(super)
88
+ end
89
+
90
+ end
91
+
92
+ end