Class: UdiseCaptchaReader::CharacterSplitter

Inherits:
Object
  • Object
show all
Defined in:
lib/udise_captcha_reader/character_splitter.rb

Class Method Summary collapse

Class Method Details

.split_into_characters(image, original_filename) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/udise_captcha_reader/character_splitter.rb', line 5

def self.split_into_characters(image, original_filename)
  # Get image dimensions
  width = image.width
  height = image.height

  # Create a gridded version of the image
  gridded = MiniMagick::Image.create(".png") do |f|
    image.format "png"
    image.write f.path
  end
  
  # Convert to black and white for checking overlaps
  bw_image = MiniMagick::Image.create(".png") do |f|
    image.format "png"
    image.write f.path
  end
  bw_image.combine_options do |b|
    b.colorspace "gray"
    b.threshold "50%"
  end
  
  # Track potential split positions (red lines)
  potential_splits = []
  
  # Draw grid and find splits
  gridded.combine_options do |c|
    c.strokewidth "0.5"  # Made thinner
    
    # Calculate horizontal line positions
    horizontal_y_positions = 10.times.map { |i| ((i + 1) * height / 11.0).round }
    
    # Draw oblique lines and find splits
    100.times do |i|
      x = ((i + 1) * width / 101.0).round  # 101 segments = 100 lines
      
      # Calculate end point for -15 degree angle
      angle_rad = -15 * Math::PI / 180  # Convert -15 degrees to radians
      x_offset = (height * Math.tan(angle_rad)).round
      end_x = x + x_offset
      
      # Check intersection points with horizontal lines
      black_intersection_count = 0
      horizontal_y_positions.each do |y|
        # Calculate x position at this y level
        progress = y.to_f / height
        intersect_x = x + (x_offset * progress).round
        next if intersect_x < 0 || intersect_x >= width
        
        # Check 3-pixel wide column at intersection
        intersection_has_black = false
        (-1..1).each do |offset|
          check_x = intersect_x + offset
          next if check_x < 0 || check_x >= width
          
          result = MiniMagick::Tool::Convert.new do |convert|
            convert << bw_image.path
            convert.crop("1x1+#{check_x}+#{y}")
            convert << "txt:-"
          end
          
          if result.include?("#000000") || result.include?("black")
            intersection_has_black = true
            break
          end
        end
        
        black_intersection_count += 1 if intersection_has_black
        break if black_intersection_count > 1  # No need to check more if we're already over threshold
      end
      
      # Set color based on intersection checks - allow up to 1 black intersection
      if black_intersection_count > 1
        c.stroke "blue"
        c.strokewidth "0.75"  # Slightly thicker for overlapping lines
      else
        c.stroke "red"
        c.strokewidth "0.5"
        potential_splits << {x: x, end_x: end_x} if x > width * 0.10 && x < width * 0.90  # Store positions if not too close to edges
      end
      c.draw "line #{x},0 #{end_x},#{height}"
    end
    
    # Draw 10 horizontal black lines
    c.stroke "black"
    c.strokewidth "0.5"
    horizontal_y_positions.each do |y|
      c.draw "line 0,#{y} #{width},#{y}"
    end
  end
  
  # Save gridded version
  gridded_path = File.join(Dir.pwd, 'tmp', "#{original_filename}_gridded.png")
  gridded.write(gridded_path)
  
  # Group consecutive red lines
  groups = []
  current_group = []

  potential_splits.each_with_index do |split, i|
    if current_group.empty? || (split[:x] - potential_splits[i-1][:x]) < width * 0.02
      current_group << split
    else
      groups << current_group if current_group.any?
      current_group = [split]
    end
  end
  groups << current_group if current_group.any?
  
  # Sort groups by size (largest first) and x position
  sorted_groups = groups.sort_by { |group| [-group.size, group[0][:x]] }
  
  # Create grouped image showing first and last lines of each group
  grouped = MiniMagick::Image.create(".png") do |f|
    image.format "png"
    image.write f.path
  end
  
  # Define distinct colors for groups (rgba with 0.2 alpha for fill, rgb for stroke)
  colors = [
    { fill: "rgba(255,0,0,0.2)", stroke: "rgb(255,0,0)" },    # Red
    { fill: "rgba(0,255,0,0.2)", stroke: "rgb(0,255,0)" },    # Green
    { fill: "rgba(0,0,255,0.2)", stroke: "rgb(0,0,255)" },    # Blue
    { fill: "rgba(255,165,0,0.2)", stroke: "rgb(255,165,0)" }, # Orange
    { fill: "rgba(128,0,128,0.2)", stroke: "rgb(128,0,128)" }, # Purple
    { fill: "rgba(0,255,255,0.2)", stroke: "rgb(0,255,255)" }, # Cyan
    { fill: "rgba(255,0,255,0.2)", stroke: "rgb(255,0,255)" }, # Magenta
    { fill: "rgba(128,128,0,0.2)", stroke: "rgb(128,128,0)" }  # Olive
  ]
  
  grouped.combine_options do |c|
    # First draw the shaded regions
    sorted_groups.each_with_index do |group, index|
      color = colors[index % colors.size]
      c.fill color[:fill]
      c.stroke "none"
      first_line = group.first
      last_line = group.last
      # Draw filled polygon between the lines
      c.draw "polygon #{first_line[:x]},0 #{last_line[:x]},0 #{last_line[:end_x]},#{height} #{first_line[:end_x]},#{height}"
    end

    # Then draw the boundary lines on top
    c.strokewidth "2"  # Thicker lines to highlight group boundaries
    sorted_groups.each_with_index do |group, index|
      color = colors[index % colors.size]
      c.stroke color[:stroke]
      first_line = group.first
      last_line = group.last
      c.draw "line #{first_line[:x]},0 #{first_line[:end_x]},#{height}"
      c.draw "line #{last_line[:x]},0 #{last_line[:end_x]},#{height}"
      
      # Add text labels for x coordinates
      c.pointsize "12"
      c.fill color[:stroke]
      c.draw "text #{first_line[:x]},15 '#{first_line[:x]}'"
      c.draw "text #{last_line[:x]},30 '#{last_line[:x]}'"
    end
  end
  
  # Save grouped version
  grouped_path = File.join(Dir.pwd, 'tmp', "#{original_filename}_grouped.png")
  grouped.write(grouped_path)
  
  # Take the largest groups that are well-spaced
  final_separators = []
  min_distance = width / 10
  
  sorted_groups.each do |group|
    # Find a line from within the group that's closest to the middle
    middle_x = (group.first[:x] + group.last[:x]) / 2.0
    middle_end_x = (group.first[:end_x] + group.last[:end_x]) / 2.0
    chosen_line = {
      x: middle_x,
      end_x: middle_end_x
    }
    
    # Check if this line is well-spaced from existing separators
    too_close = final_separators.any? { |sep| (sep[:x] - chosen_line[:x]).abs < min_distance }
    next if too_close
    
    final_separators << chosen_line
    break if final_separators.size == 5
  end
  
  # Sort final separators by x position
  final_separators.sort_by! { |sep| sep[:x] }
  
  # If we don't have enough separators, fall back to equal spacing
  if final_separators.size < 5
    char_width = width / 6.0
    angle_rad = -15 * Math::PI / 180
    final_separators = 5.times.map do |i|
      x = ((i + 1) * char_width).round
      end_x = x + (height * Math.tan(angle_rad)).round
      {x: x, end_x: end_x}
    end
  end
  
  # Create marked image with only selected split lines
  marked = MiniMagick::Image.create(".png") do |f|
    image.format "png"
    image.write f.path
  end
  
  marked.combine_options do |c|
    c.stroke "red"
    c.strokewidth "1"  # Make split lines more visible
    final_separators.each_with_index do |sep, index|
      c.draw "line #{sep[:x]},0 #{sep[:end_x]},#{height}"
      # Add text label for x coordinate
      c.pointsize "12"
      c.fill "red"
      c.draw "text #{sep[:x]},15 '#{sep[:x].round}'"
    end
  end
  
  # Save marked version
  marked_path = File.join(Dir.pwd, 'tmp', "#{original_filename}_marked.png")
  marked.write(marked_path)
  
  # Create array for character images
  char_images = []
  
  # Split positions including image edges
  split_positions = [{x: 0, end_x: 0}] + final_separators + [{x: width, end_x: width}]
  
  # Create tmp directory if it doesn't exist
  tmp_dir = File.join(Dir.pwd, 'tmp')
  FileUtils.mkdir_p(tmp_dir)
  
  # Create character images from the segments
  6.times do |i|
    # Create path for split character in tmp folder
    char_path = File.join(tmp_dir, "#{original_filename}_#{i + 1}.png")
    
    # Get current and next split lines
    current_split = split_positions[i]
    next_split = split_positions[i + 1]
    
    # Calculate bounding box that contains both lines
    x_start = [current_split[:x], current_split[:end_x]].min
    x_end = [next_split[:x], next_split[:end_x]].max
    crop_width = x_end - x_start
    
    # Create a new image for the character
    char_image = MiniMagick::Image.create(".png") do |f|
      image.format "png"
      image.write f.path
    end
    
    # First crop the bounding box
    char_image.combine_options do |c|
      c.crop "#{crop_width}x#{height}+#{x_start}+0"
      c.repage.+
    end
    
    # Create a mask for the area between the lines
    mask = MiniMagick::Image.create(".png") do |f|
      char_image.format "png"
      char_image.write f.path
    end
    
    # Draw white polygons to mask out areas outside the lines
    mask.combine_options do |c|
      # Convert coordinates to be relative to cropped image
      left_line_start_x = current_split[:x] - x_start
      left_line_end_x = current_split[:end_x] - x_start
      right_line_start_x = next_split[:x] - x_start
      right_line_end_x = next_split[:end_x] - x_start
      
      # Mask out area above/left of first line
      c.draw "fill white stroke none polygon 0,0 #{left_line_start_x},0 #{left_line_end_x},#{height} 0,#{height}"
      
      # Mask out area below/right of second line
      c.draw "fill white stroke none polygon #{right_line_start_x},0 #{crop_width},0 #{crop_width},#{height} #{right_line_end_x},#{height}"
    end
    
    # Apply the mask to the character image
    char_image = char_image.composite(mask) do |c|
      c.compose "over"
    end
    
    # Save the character image to tmp directory
    char_image.write(char_path)
    char_images << File.new(char_path)
  end
  
  # Create a joined image from all split characters
  joined = MiniMagick::Image.create(".png") do |f|
    first_char = MiniMagick::Image.open(char_images.first.path)
    first_char.format "png"
    first_char.write f.path
  end

  # Calculate total width needed
  total_width = char_images.sum do |char_file|
    MiniMagick::Image.open(char_file.path).width
  end

  # Create blank white canvas
  canvas = MiniMagick::Image.open(char_images.first.path)
  canvas.combine_options do |c|
    c.gravity "center"
    c.background "white"
    c.extent "#{total_width}x#{height}"
  end

  # Composite each character onto the canvas
  x_offset = 0
  char_images.each do |char_file|
    char = MiniMagick::Image.open(char_file.path)
    canvas = canvas.composite(char) do |c|
      c.compose "over"
      c.geometry "+#{x_offset}+0"
    end
    x_offset += char.width
  end

  # Save joined version
  joined_path = File.join(Dir.pwd, 'tmp', "#{original_filename}_joined.png")
  canvas.write(joined_path)
  
  char_images
end