sqed 0.0.1

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 (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