sqed 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +27 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +18 -0
  5. data/Gemfile +7 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +36 -0
  8. data/Rakefile +9 -0
  9. data/lib/sqed.rb +111 -0
  10. data/lib/sqed/boundaries.rb +79 -0
  11. data/lib/sqed/boundary_finder.rb +150 -0
  12. data/lib/sqed/boundary_finder/color_line_finder.rb +83 -0
  13. data/lib/sqed/boundary_finder/cross_finder.rb +23 -0
  14. data/lib/sqed/boundary_finder/stage_finder.rb +139 -0
  15. data/lib/sqed/extractor.rb +45 -0
  16. data/lib/sqed/parser.rb +11 -0
  17. data/lib/sqed/parser/barcode_parser.rb +27 -0
  18. data/lib/sqed/parser/ocr_parser.rb +52 -0
  19. data/lib/sqed/result.rb +15 -0
  20. data/lib/sqed/version.rb +3 -0
  21. data/lib/sqed_config.rb +112 -0
  22. data/spec/lib/sqed/boundaries_spec.rb +35 -0
  23. data/spec/lib/sqed/boundary_finder/color_line_finder_spec.rb +167 -0
  24. data/spec/lib/sqed/boundary_finder/cross_finder_spec.rb +28 -0
  25. data/spec/lib/sqed/boundary_finder/stage_finder_spec.rb +9 -0
  26. data/spec/lib/sqed/boundary_finder_spec.rb +108 -0
  27. data/spec/lib/sqed/extractor_spec.rb +82 -0
  28. data/spec/lib/sqed/parser_spec.rb +6 -0
  29. data/spec/lib/sqed/result_spec.rb +17 -0
  30. data/spec/lib/sqed_spec.rb +200 -0
  31. data/spec/spec_helper.rb +34 -0
  32. data/spec/support/files/2Dbarcode.png +0 -0
  33. data/spec/support/files/CrossyBlackLinesSpecimen.jpg +0 -0
  34. data/spec/support/files/CrossyGreenLinesSpecimen.jpg +0 -0
  35. data/spec/support/files/Quadrant_2_3.jpg +0 -0
  36. data/spec/support/files/black_stage_green_line_specimen.jpg +0 -0
  37. data/spec/support/files/boundary_cross_green.jpg +0 -0
  38. data/spec/support/files/boundary_left_t_yellow.jpg +0 -0
  39. data/spec/support/files/boundary_offset_cross_red.jpg +0 -0
  40. data/spec/support/files/boundary_right_t_green.jpg +0 -0
  41. data/spec/support/files/greenlineimage.jpg +0 -0
  42. data/spec/support/files/label_images/black_stage_green_line_specimen_label.jpg +0 -0
  43. data/spec/support/files/test0.jpg +0 -0
  44. data/spec/support/files/test1.jpg +0 -0
  45. data/spec/support/files/test2.jpg +0 -0
  46. data/spec/support/files/test3.jpg +0 -0
  47. data/spec/support/files/test4.jpg +0 -0
  48. data/spec/support/files/test4OLD.jpg +0 -0
  49. data/spec/support/files/test_barcode.JPG +0 -0
  50. data/spec/support/files/test_ocr0.jpg +0 -0
  51. data/spec/support/files/types_21.jpg +0 -0
  52. data/spec/support/files/types_8.jpg +0 -0
  53. data/spec/support/image_helpers.rb +78 -0
  54. data/sqed.gemspec +31 -0
  55. metadata +244 -0
@@ -0,0 +1,83 @@
1
+ require 'RMagick'
2
+
3
+ # This was "green" line finder attempting to be agnostic; now it is reworked to be color-specific line finder
4
+ #
5
+ class Sqed::BoundaryFinder::ColorLineFinder < Sqed::BoundaryFinder
6
+
7
+ def initialize(image: image, layout: layout, boundary_color: :green)
8
+ super(image: image, layout: layout)
9
+ raise 'No layout provided.' if @layout.nil?
10
+ @boundary_color = boundary_color
11
+ find_bands
12
+ end
13
+
14
+ private
15
+
16
+ def find_bands
17
+ case @layout # boundaries.coordinates are referenced from stage image
18
+
19
+ when :vertical_split # can vertical and horizontal split be re-used to do cross cases?
20
+ t = Sqed::BoundaryFinder.color_boundary_finder(image: img, boundary_color: @boundary_color) #detect vertical division, green line
21
+ return if t.nil?
22
+ boundaries.coordinates[0] = [0, 0, t[0], img.rows] # left section of image
23
+ boundaries.coordinates[1] = [t[2], 0, img.columns - t[2], img.rows] # right section of image
24
+ boundaries.complete = true
25
+
26
+ when :horizontal_split
27
+ t = Sqed::BoundaryFinder.color_boundary_finder(image: img, scan: :columns, boundary_color: @boundary_color) # set to detect horizontal division, (green line)
28
+ return if t.nil?
29
+ boundaries.coordinates[0] = [0, 0, img.columns, t[0]] # upper section of image
30
+ boundaries.coordinates[1] = [0, t[2], img.columns, img.rows - t[2]] # lower section of image
31
+ boundaries.complete = true
32
+ # boundaries.coordinates[2] = [0, 0, img.columns, t[1]] # upper section of image
33
+ # boundaries.coordinates[3] = [0, t[1], img.columns, img.rows - t[1]] # lower section of image
34
+
35
+ when :right_t # only 3 zones expected, with horizontal division in right-side of vertical division
36
+ t = Sqed::BoundaryFinder.color_boundary_finder(image: img, boundary_color: @boundary_color) #defaults to detect vertical division, green line
37
+ return if t.nil?
38
+ boundaries.coordinates[0] = [0, 0, t[0], img.rows] # left section of image
39
+ boundaries.coordinates[1] = [t[2], 0, img.columns - t[2], img.rows] # left section of image
40
+
41
+ # now subdivide right side
42
+ irt = img.crop(*boundaries.coordinates[1], true)
43
+ rt = Sqed::BoundaryFinder.color_boundary_finder(image: irt, scan: :columns, boundary_color: @boundary_color) # set to detect horizontal division, (green line)
44
+ return if rt.nil?
45
+ boundaries.coordinates[1] = [t[2], 0, img.columns - t[2], rt[0]] # upper section of image
46
+ boundaries.coordinates[2] = [t[2], rt[2], img.columns - t[2], img.rows - rt[2]] # lower section of image
47
+ boundaries.complete = true
48
+ # will return 1, 2, or 3
49
+
50
+ when :offset_cross # 4 zones expected, with horizontal division in right- and left- sides of vertical division
51
+ t = Sqed::BoundaryFinder.color_boundary_finder(image: img, boundary_color: @boundary_color) # defaults to detect vertical division, green line
52
+ raise if t.nil?
53
+ boundaries.coordinates[0] = [0, 0, t[0], img.rows] # left section of image
54
+ boundaries.coordinates[1] = [t[2], 0, img.columns - t[2], img.rows] # right section of image
55
+
56
+ # now subdivide left side
57
+ ilt = img.crop(*boundaries.coordinates[0], true)
58
+
59
+ lt = Sqed::BoundaryFinder.color_boundary_finder(image: ilt, scan: :columns, boundary_color: @boundary_color) # set to detect horizontal division, (green line)
60
+ if !lt.nil?
61
+ boundaries.coordinates[0] = [0, 0, t[0], lt[0]] # upper section of image
62
+ boundaries.coordinates[3] = [0, lt[2], t[0], img.rows - lt[2]] # lower section of image
63
+ end
64
+
65
+ # now subdivide right side
66
+ irt = img.crop(*boundaries.coordinates[1], true)
67
+ rt = Sqed::BoundaryFinder.color_boundary_finder(image: irt, scan: :columns, boundary_color: @boundary_color) # set to detect horizontal division, (green line)
68
+ return if rt.nil?
69
+
70
+ boundaries.coordinates[1] = [t[2], 0, img.columns - t[2], rt[0]] # upper section of image
71
+ boundaries.coordinates[2] = [t[2], rt[2], img.columns - t[2], img.rows - rt[2]] # lower section of image
72
+ # will return 1, 2, 3, or 4 //// does not handle staggered vertical boundary case
73
+ boundaries.complete = true
74
+
75
+ else
76
+ boundaries.coordinates[0] = [0, 0, img.columns, img.rows] # totality of image as default
77
+ return # return original image boundary if no method implemented
78
+ end
79
+
80
+ end
81
+
82
+
83
+ end
@@ -0,0 +1,23 @@
1
+ require 'RMagick'
2
+
3
+ # Return four equal quadrants, no parsing through the image
4
+ #
5
+ class Sqed::BoundaryFinder::CrossFinder < Sqed::BoundaryFinder
6
+
7
+ def initialize(image: image)
8
+ @image = image
9
+ find_edges
10
+ end
11
+
12
+ def find_edges
13
+ width = @image.columns / 2
14
+ height = @image.rows / 2
15
+
16
+ boundaries.coordinates[0] = [0, 0, width, height]
17
+ boundaries.coordinates[1] = [width, 0, width, height]
18
+ boundaries.coordinates[2] = [width, height, width, height]
19
+ boundaries.coordinates[3] = [0, height, width, height]
20
+ boundaries.complete = true
21
+ end
22
+
23
+ end
@@ -0,0 +1,139 @@
1
+ require 'RMagick'
2
+
3
+ # Some of this code was originally inspired by Emmanuel Oga's gist https://gist.github.com/EmmanuelOga/2476153.
4
+ #
5
+ class Sqed::BoundaryFinder::StageFinder < Sqed::BoundaryFinder
6
+
7
+ # The proc containing the border finding algorithim
8
+ attr_reader :is_border
9
+
10
+ # assume white-ish image on dark-ish background
11
+
12
+ # How small we accept a cropped picture to be. E.G. if it was 100x100 and
13
+ # ratio 0.1, min output should be 10x10
14
+ MIN_CROP_RATIO = 0.1
15
+
16
+ attr_reader :x0, :y0, :x1, :y1, :min_width, :min_height, :rows, :columns
17
+
18
+ def initialize(image: image, is_border_proc: nil, min_ratio: MIN_CROP_RATIO)
19
+ super(image: image, layout: :internal_box)
20
+
21
+ @min_ratio = min_ratio
22
+
23
+ # Initial co-ordinates
24
+ @x0, @y0 = 0, 0
25
+ @x1, @y1 = img.columns, img.rows
26
+ @min_width, @min_height = img.columns * @min_ratio, img.rows * @min_ratio # minimum resultant area
27
+ @columns, @rows = img.columns, img.rows
28
+
29
+ # We need a border finder proc. Provide one if none was given.
30
+ @is_border = is_border_proc || self.class.default_border_finder(img) # if no proc specified, use default below
31
+
32
+ @x00 = @x0
33
+ @y00 = @y0
34
+ @height0 = height
35
+ @width0 = width
36
+ find_edges
37
+ end
38
+
39
+ private
40
+
41
+ # Returns a Proc that, given a set of pixels (an edge of the image) decides
42
+ # whether that edge is a border or not.
43
+ #
44
+ # (img, samples = 5, threshold = 0.95, fuzz_factor = 0.5) # initially
45
+ # (img, samples = 50, threshold = 0.9, fuzz_factor = 0.1) # semi-working on synthetic images 08-dec-2014 (x)
46
+ # (img, samples = 20, threshold = 0.8, fuzz_factor = 0.2) # WORKS with synthetic images and changes to x0, y0, width, height
47
+ #
48
+ # appears to assume sharp transition will occur in 5 pixels x/y
49
+ #
50
+ # how is threshold defined?
51
+ # works for 0.5, >0.137; 0.60, >0.14 0.65, >0.146; 0.70, >0.1875; 0.75, >0.1875; 0.8, >0.237; 0.85, >0.24; 0.90, >0.28; 0.95, >0.25
52
+ # fails for 0.75, (0.18, 0.17,0.16,0.15); 0.70, 0.18;
53
+ #
54
+ def self.default_border_finder(img, samples = 5, threshold = 0.75, fuzz_factor = 0.40) # working on non-synthetic images 04-dec-2014
55
+ fuzz = ((::QuantumRange + 1) * fuzz_factor).to_i
56
+ # Returns true if the edge is a border (border meaning outer region to be cropped)
57
+ lambda do |edge|
58
+ border, non_border = 0.0, 0.0 # maybe should be called outer, inner
59
+
60
+ pixels = (0...samples).map { |n| edge[n * edge.length / samples] }
61
+ pixels.combination(2).each do |a, b|
62
+ if a.fcmp(b, fuzz) then
63
+ border += 1
64
+ else
65
+ non_border += 1
66
+ end
67
+ end
68
+ bratio = border.to_f / (border + non_border)
69
+ if bratio > threshold
70
+ return true
71
+ else
72
+ return false
73
+ end
74
+ border.to_f / (border + non_border) > threshold # number of matching string of pixels/(2 x total pixels - a.k.a. samples?)
75
+ end
76
+ end
77
+
78
+ def find_edges
79
+ # handle this exception
80
+ return unless is_border # return if no process defined or set for @is_border
81
+
82
+ u = x1 - 1 # rightmost pixel (kind of)
83
+ # increment from left to right
84
+ x0.upto(u) do |x|
85
+ if width_croppable? && is_border[vline(x)] then
86
+ @x0 = x + 1
87
+ else
88
+ break
89
+ end
90
+ end
91
+ # increment from left to right
92
+ (u).downto(x0) { |x| width_croppable? && is_border[vline(x)] ? @x1 = x - 1 : break }
93
+
94
+ u = y1 - 1
95
+ 0.upto(u) do |y|
96
+ if height_croppable? && is_border[hline y] then
97
+ @y0 = y + 1
98
+ else
99
+ break
100
+ end
101
+ end
102
+ (u).downto(y0) { |y| height_croppable? && is_border[hline y] ? @y1 = y - 1 : break }
103
+ u = 0
104
+
105
+ delta_x = 0 #width/50 # 2% of cropped image to make up for trapezoidal distortion
106
+ delta_y = 0 #height/50 # 2% of cropped image to make up for trapezoidal distortion <- NOT 3%
107
+
108
+ # TODO: add conditions
109
+ boundaries.complete = true
110
+ boundaries.coordinates[0] = [x0 + delta_x, y0 + delta_y, width - 2*delta_x, height - 2*delta_y]
111
+ end
112
+
113
+ def width_croppable?
114
+ width > min_width
115
+ end
116
+
117
+ def height_croppable?
118
+ height > min_height
119
+ end
120
+
121
+ def vline(x)
122
+ img.get_pixels x, @y00, 1, @height0 - 1
123
+ end
124
+
125
+ def hline(y)
126
+ img.get_pixels @x00, y, @width0 - 1, 1
127
+ end
128
+
129
+ # actually + 1 (starting at zero?)
130
+ def width
131
+ @x1 - @x0
132
+ end
133
+
134
+ # actually + 1 (starting at zero?)
135
+ def height
136
+ @y1 - @y0
137
+ end
138
+
139
+ end
@@ -0,0 +1,45 @@
1
+ require 'RMagick'
2
+
3
+ # An Extractor takes Boundries object and a layout pattern and returns a Sqed::Result
4
+ #
5
+ class Sqed::Extractor
6
+
7
+ attr_accessor :boundaries, :layout, :image
8
+
9
+ def initialize(boundaries: boundaries, layout: layout, image: image)
10
+ raise if boundaries.nil? || !boundaries.class == Sqed::Boundaries
11
+ raise if layout.nil? || !layout.class == Hash
12
+
13
+ @layout = layout
14
+ @boundaries = boundaries
15
+ @image = image
16
+ end
17
+
18
+ def result
19
+ r = Sqed::Result.new()
20
+
21
+ # assign the images to the result
22
+ boundaries.each do |section, coords|
23
+ r.send("#{LAYOUT_SECTION_TYPES[section]}=", extract_image(coords))
24
+ end
25
+
26
+ # assign the metadata to the result
27
+ layout.keys.each do |section_index, section_type|
28
+ # only extract data if a parser exists
29
+ if parser = SECTION_PARSERS[section_type]
30
+ r.send("#{section_type}=", parser.new(image: r.send(section_type + "_image").text) )
31
+ end
32
+ end
33
+
34
+ r
35
+ end
36
+
37
+ # coords are x1, y1, x2, y2
38
+ def extract_image(coords)
39
+ # crop takes x, y, width, height
40
+ # @image.crop(coords[0], coords[1], coords[2] - coords[0], coords[3] - coords[1] )
41
+ bp = 0
42
+ @image.crop(coords[0], coords[1], coords[2], coords[3], true)
43
+ end
44
+
45
+ end
@@ -0,0 +1,11 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Base class for Parsers
4
+ #
5
+ class Sqed::Parser
6
+ attr_accessor :image
7
+
8
+ def initialize(image)
9
+ @image = image
10
+ end
11
+ end
@@ -0,0 +1,27 @@
1
+ # Given an image, return an ordered array of detectable barcodes
2
+
3
+ class Sqed::Parser::BarcodeParser < Sqed::Parser
4
+ attr_accessor :barcodes
5
+
6
+ def initialize(image)
7
+ super
8
+ @barcodes = bar_codes
9
+ end
10
+
11
+ def bar_codes
12
+ # process the images, spit out the barcodes
13
+ # return ZXing.decode_all(@image) #['ABC 123', 'DEF 456']
14
+ # a = `/usr/local/Cellar/zbar/0.10_1/bin/zbarimg ~/src/sqed/spec/support/files/test_barcode.JPG`
15
+ # b = a.split("\n")
16
+ f = 'SessionID_BarcodeImage.JPG'
17
+ i = @image[:image]
18
+ if i.nil?
19
+ i = @image
20
+ end
21
+ i.write("tmp/#{f}")
22
+ c = `/usr/local/Cellar/zbar/0.10_1/bin/zbarimg #{f}`
23
+ d = c.split("\n")
24
+ return d
25
+ end
26
+
27
+ end
@@ -0,0 +1,52 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Given a single image return all text in that image.
4
+ #
5
+ # For past reference http://misteroleg.wordpress.com/2012/12/19/ocr-using-tesseract-and-imagemagick-as-pre-processing-task/
6
+ #
7
+ require 'rtesseract'
8
+
9
+ class Sqed::Parser::OcrParser < Sqed::Parser
10
+ attr_accessor :text
11
+
12
+ def text
13
+ img = @image #.white_threshold(245)
14
+
15
+ # @jrflood: this is where you will have to do some research, tuning images so that they can be better ocr-ed,
16
+ # all of these methods are from RMagick.
17
+ # get potential border pixel color (based on quadrant?)
18
+ new_color = img.pixel_color(1, 1)
19
+ # img = img.scale(2)
20
+ # img.write('foo0.jpg.jpg')
21
+ # img = img.enhance
22
+ # img = img.enhance
23
+ # img = img.enhance
24
+ # img = img.enhance
25
+ # img.write('foo1.jpg')
26
+ # img = img.quantize(8, Magick::GRAYColorspace)
27
+ # img.write('foo1.jpg')
28
+ # img = img.sharpen(1.0, 0.2)
29
+ # img.write('foo2.jpg')
30
+ # border_color = img.pixel_color(img.columns - 1, img.rows - 1)
31
+ # img = img.color_floodfill(img.columns - 1, img.rows - 1, new_color)
32
+ # img.write('tmp/foo4.jpg')
33
+ # img = img.quantize(2, Magick::GRAYColorspace)
34
+ # #img = img.threshold(0.5)
35
+ # img.write('foo4.jpg') # for debugging purposes, this is the image that is sent to OCR
36
+ # img = img.equalize #(32, Magick::GRAYColorspace)
37
+ # img.write('foo5.jpg') # for debugging purposes, this is the image that is sent to OCR
38
+ # #img.write('foo3.jpg') # for debugging purposes, this is the image that is sent to OCR
39
+ #
40
+ # img.write('foo.jpg') # for debugging purposes, this is the image that is sent to OCR
41
+
42
+ r = RTesseract.new(img, lang: 'eng', psm: 3)
43
+
44
+
45
+ # img = img.white_threshold(245)
46
+
47
+ @text = r.to_s
48
+ end
49
+
50
+ # Need to provide tuning methods here, i.e. image transormations that facilitate OCR
51
+
52
+ end
@@ -0,0 +1,15 @@
1
+
2
+ # A Sqed::Result is a wrapper for the results of the
3
+ # full process of data extraction from an image.
4
+ #
5
+ #
6
+ #
7
+ class Sqed::Result
8
+
9
+ SqedConfig::LAYOUT_SECTION_TYPES.each do |k|
10
+ attr_accessor k
11
+ attr_accessor "#{k}_image".to_sym
12
+ end
13
+ end
14
+
15
+
@@ -0,0 +1,3 @@
1
+ class Sqed
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,112 @@
1
+ # encoding: UTF-8
2
+
3
+ require_relative "sqed/parser"
4
+ require_relative "sqed/parser/ocr_parser"
5
+ require_relative "sqed/parser/barcode_parser"
6
+
7
+ require_relative "sqed/boundaries"
8
+ require_relative "sqed/boundary_finder"
9
+ require_relative "sqed/boundary_finder/cross_finder"
10
+ require_relative "sqed/boundary_finder/stage_finder"
11
+ require_relative "sqed/boundary_finder/color_line_finder"
12
+
13
+ # Sqed constants, including patterns for extraction etc.
14
+ #
15
+ module SqedConfig
16
+
17
+ # Layouts refer to the arrangement of the divided stage.
18
+ # Windows are enumerated from the top left, moving around the border
19
+ # in a clockwise position. For example:
20
+ # 0 | 1
21
+ # ----|---- :equal_cross //probably obviated by offset_cross
22
+ # 3 | 2
23
+ #
24
+ # | 1
25
+ # 0 |---- :right_t
26
+ # | 2
27
+ #
28
+ # should be an arbitrary cross layout
29
+ # 0 | 1
30
+ # |
31
+ # --------- :offset_cross //does not match current code
32
+ # 3 | 2
33
+ #
34
+ # 0 | 1
35
+ # |____
36
+ # ----| :offset_cross // matches current code
37
+ # 3 | 2
38
+ #
39
+ # 0
40
+ # -------- :horizontal_split
41
+ # 1
42
+ #
43
+ # |
44
+ # 0 | 1 :vertical_split
45
+ # |
46
+ #
47
+ # -----
48
+ # | 0 | :internal_box
49
+ # -----
50
+ #
51
+
52
+ # Hash values are used to stub out
53
+ # the Sqed::Boundaries instance.
54
+ #
55
+ LAYOUTS = {
56
+ cross: [0,1,2,3],
57
+ offset_cross: [0,1,2,3],
58
+ horizontal_split: [0,1],
59
+ vertical_split: [0,1],
60
+ right_t: [0,1,2],
61
+ left_t: [0,1,2],
62
+ internal_box: [0]
63
+ }
64
+
65
+ # Each element of the layout is a "section".
66
+ LAYOUT_SECTION_TYPES = [
67
+ :stage, # the image contains the full stage
68
+ :specimen, # the specimen only, no metadata should be present
69
+ :annotated_specimen, # a specimen is present, and metadata is too
70
+ :determination_labels, # the section contains text that determines the specimen
71
+ :labels, # the section contains collecting event and non-determination labels
72
+ :identifier, # the section contains an identifier (e.g. barcode or unique number)
73
+ :image_registration # the section contains only image registration information
74
+ ]
75
+
76
+ # Links section types to data parsers
77
+ SECTION_PARSERS = {
78
+ labels: Sqed::Parser::OcrParser,
79
+ identifier: Sqed::Parser::BarcodeParser,
80
+ deterimination_labels: Sqed::Parser::OcrParser
81
+ }
82
+
83
+ EXTRACTION_PATTERNS = {
84
+ right_t: {
85
+ boundary_finder: Sqed::BoundaryFinder::ColorLineFinder,
86
+ layout: :right_t,
87
+ metadata_map: {0 => :annotated_specimen, 1 => :identifiers, 2 =>:image_registration }
88
+ },
89
+ offset_cross: {
90
+ boundary_finder: Sqed::BoundaryFinder::ColorLineFinder,
91
+ layout: :offset_cross,
92
+ metadata_map: {0 => :annotated_specimen, 1 => :identifiers, 2 =>:image_registration }
93
+ },
94
+ standard_cross: {
95
+ boundary_finder: Sqed::BoundaryFinder::CrossFinder,
96
+ layout: :cross,
97
+ metadata_map: {0 => :labels, 1 => :specimen, 2 => :identifier, 3 => :specimen_determinations }
98
+ },
99
+ stage: {
100
+ boundary_finder: Sqed::BoundaryFinder::StageFinder,
101
+ layout: :internal_box,
102
+ metadata_map: {0 => :stage}
103
+ }
104
+ # etc. ...
105
+ }
106
+
107
+ DEFAULT_TMP_DIR = "/tmp"
108
+
109
+ def self.index_for_section_type(pattern, section_type)
110
+ EXTRACTION_PATTERNS[pattern][:metadata_map].invert[section_type]
111
+ end
112
+ end