Class: LogoSoup::Core::PixelAnalyzer

Inherits:
Object
  • Object
show all
Defined in:
lib/logosoup/core/pixel_analyzer.rb

Overview

Measures visual features from sampled RGBA pixels.

Class Method Summary collapse

Class Method Details

.call(bytes:, sample_width:, sample_height:, original_width:, original_height:, contrast_threshold:, alpha_only:, bg_r:, bg_g:, bg_b:) ⇒ Hash?

Returns:

  • (Hash, nil)


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
# File 'lib/logosoup/core/pixel_analyzer.rb', line 8

def self.call(
  bytes:,
  sample_width:,
  sample_height:,
  original_width:,
  original_height:,
  contrast_threshold:,
  alpha_only:,
  bg_r:,
  bg_g:,
  bg_b:
)
  sw = sample_width
  sh = sample_height
  w = original_width
  h = original_height

  contrast_distance_sq = contrast_threshold.to_f * contrast_threshold.to_f * 3
  min_x = sw
  min_y = sh
  max_x = 0
  max_y = 0

  total_weight = 0.0
  weighted_x = 0.0
  weighted_y = 0.0

  filled_pixels = 0
  total_weighted_opacity = 0.0

  pixel_count = sw * sh
  pixel_count.times do |i|
    base = i * 4
    r = bytes[base]
    g = bytes[base + 1]
    b = bytes[base + 2]
    a = bytes[base + 3]
    next if a.nil? || a <= contrast_threshold

    if alpha_only
      weight = a * a
      opacity = a
    else
      dr = r - bg_r
      dg = g - bg_g
      db = b - bg_b
      dist_sq = (dr * dr) + (dg * dg) + (db * db)
      next if dist_sq < contrast_distance_sq

      weight = dist_sq * a
      opacity = [a, Math.sqrt(dist_sq)].min
    end

    x = i % sw
    y = (i - x) / sw

    min_x = x if x < min_x
    max_x = x if x > max_x
    min_y = y if y < min_y
    max_y = y if y > max_y

    total_weight += weight
    weighted_x += (x + 0.5) * weight
    weighted_y += (y + 0.5) * weight

    filled_pixels += 1
    total_weighted_opacity += opacity
  end

  return nil if min_x > max_x || min_y > max_y

  scan_area = (max_x - min_x + 1) * (max_y - min_y + 1)
  return nil if scan_area <= 0

  coverage_ratio = filled_pixels.to_f / scan_area
  average_opacity = filled_pixels.positive? ? (total_weighted_opacity / 255.0 / filled_pixels) : 0.0
  pixel_density = coverage_ratio * average_opacity

  scale_x = w.to_f / sw
  scale_y = h.to_f / sh

  cb_x = (min_x * scale_x).floor
  cb_y = (min_y * scale_y).floor
  cb_right = [[((max_x + 1) * scale_x).ceil.to_i, w].min, 0].max
  cb_bottom = [[((max_y + 1) * scale_y).ceil.to_i, h].min, 0].max
  content_box_width = [[cb_right - cb_x, 1].max, w].min
  content_box_height = [[cb_bottom - cb_y, 1].max, h].min

  if total_weight <= 0
    offset_x = 0.0
    offset_y = 0.0
  else
    global_center_x = (weighted_x / total_weight) * scale_x
    global_center_y = (weighted_y / total_weight) * scale_y
    local_center_x = global_center_x - cb_x
    local_center_y = global_center_y - cb_y

    geometric_center_x = content_box_width.to_f / 2
    geometric_center_y = content_box_height.to_f / 2

    offset_x = local_center_x - geometric_center_x
    offset_y = local_center_y - geometric_center_y
  end

  {
    pixel_density: pixel_density,
    content_box_width: content_box_width,
    content_box_height: content_box_height,
    visual_center_offset_x: offset_x,
    visual_center_offset_y: offset_y
  }
end