Ruby Sees the World: Automatic Measurement from Photos

Ruby Sees the World: Automatic Measurement from Photos
Ruby Sees the World: Automatic Measurement from Photos

March 23, 2026

Published on RubyStackNews


What if Ruby could look at a photo of a house and tell you the width of the door, the height of the windows, and the total wall length?

That is not science fiction. It is photogrammetry. And it is buildable in pure Ruby with ruby-libgd.

This article walks through the full pipeline: edge detection, Hough transform, scale calibration from a credit card reference, and measurement overlay on the original photo.


Support RubyStackNews

This time, to support RubyStackNews, the Jupyter Notebook with the MeasureProcessor Output Report is available for 15 USD.

If you enjoy RubyStackNews, consider supporting the project by purchasing the notebook, which includes a complete example for processing MeasureProcessor data.

Buy the Notebook

The idea

Single-image photogrammetry extracts real-world dimensions from a photograph using one constraint: something in the photo has a known size.

Place a standard credit card (85.6 x 54mm) against the wall you want to measure. Take a photo. The algorithm finds the card, computes the pixel-to-millimeter ratio, and applies that ratio to everything else in the frame.

The result is a measurement overlay drawn directly on the photo:

Door height: 2.1m
Door width: 0.9m
Window width: 1.2m
Window height: 1.4m
Wall width: 8.0m

One photo. One credit card. Real measurements.


The pipeline

Photo (JPG or PNG)
1. Grayscale -- img.filter("grayscale") native C
2. Gaussian blur -- img.filter("gaussian_blur") native C
3. Sobel edges -- img.filter("sobel") native C
4. Hough transform -- Ruby (finds straight lines)
5. Line filtering -- Ruby (keeps architectural lines)
6. Credit card detect -- Ruby (finds the reference rectangle)
7. Scale calibration -- Ruby (px per mm ratio)
8. Measurement -- Ruby (door, window, wall sizes)
9. Overlay -- ruby-libgd (draws on photo)

Steps 1 to 3 run in native C via ruby-libgd. Milliseconds. Steps 4 to 9 run in Ruby. The Hough transform is the heaviest step.


Step 1 to 3: Edge detection (native C)

ruby-libgd ships Sobel, Gaussian blur, and grayscale as native C filters. Three lines:

img = GD::Image.open("house.jpg")
img.filter("grayscale")
img.filter("gaussian_blur")
img.filter("sobel")
img.save("edges.png")

The edge image highlights all structural boundaries: wall edges, door frame, window frames, the credit card outline.


Step 4: Hough transform (Ruby)

The Hough transform finds straight lines in an edge image. For each edge pixel, it votes for all lines that could pass through that point. Lines with many votes are real structural lines in the image.

def hough_transform(edges, width, height, threshold: 80)
max_rho = Math.sqrt(width**2 + height**2).ceil
thetas = (0..179).map { |t| t * Math::PI / 180.0 }
accumulator = Hash.new(0)
height.times do |y|
width.times do |x|
r, g, b = edges.get_pixel(x, y)
next if r < 128 # not an edge pixel
thetas.each_with_index do |theta, ti|
rho = (x * Math.cos(theta) + y * Math.sin(theta)).round
accumulator[[ti, rho + max_rho]] += 1
end
end
end
# Extract lines above threshold
accumulator.select { |_, v| v >= threshold }
.map { |(ti, rho_idx), votes|
{ theta: thetas[ti], rho: rho_idx - max_rho, votes: votes }
}
.sort_by { |l| -l[:votes] }
end

For a 1200×900 image this takes 15-30 seconds in Ruby. Acceptable for a measurement tool that runs once per photo.


Step 6: Credit card detection

The credit card is a rectangle with a known aspect ratio (85.6 / 54.0 = 1.585). From the Hough lines, find two pairs of parallel lines that form a rectangle with that aspect ratio.

CARD_RATIO = 85.6 / 54.0 # width / height
CARD_RATIO_TOL = 0.15
def find_credit_card(lines)
horizontals = lines.select { |l| l[:theta].between?(1.48, 1.66) }
verticals = lines.select { |l| l[:theta] < 0.17 || l[:theta] > 2.97 }
horizontals.combination(2).each do |h1, h2|
verticals.combination(2).each do |v1, v2|
card_h = (h1[:rho] - h2[:rho]).abs
card_w = (v1[:rho] - v2[:rho]).abs
next if card_h == 0
ratio = card_w.to_f / card_h
if (ratio - CARD_RATIO).abs < CARD_RATIO_TOL
return { width_px: card_w, height_px: card_h }
end
end
end
nil
end

Step 7: Scale calibration

Article content

Once the card is found in pixels:

card = find_credit_card(lines)
scale_px_per_mm = card[:width_px] / 85.6 # pixels per millimeter
def px_to_meters(px, scale_px_per_mm)
(px / scale_px_per_mm / 1000.0).round(2)
end

Step 8 to 9: Measure and overlay

Find the largest vertical line pair (door frame), horizontal line pairs (window sills), and compute their real dimensions. Draw the results directly on the original photo using ruby-libgd:

result = GD::Image.open("house.jpg")
green = GD::Color.rgb(0, 220, 140)
# Draw measurement line
result.line(x0, y0, x1, y0, green)
result.line(x0, y0-6, x0, y0+6, green)
result.line(x1, y0-6, x1, y0+6, green)
# Draw label
result.text("#{door_width}m", x: label_x, y: label_y,
size: 18, color: green, font: "font.ttf")
result.save("measured.png")

The constraint: one flat plane

This approach works correctly when all measured objects are on the same plane as the reference card. In a facade photo, that means the card should be taped to the wall, not placed on the ground.

If the card is on the ground and the door is on the wall, they are at different distances from the camera, and the scale derived from the card does not apply to the door. This is the fundamental constraint of single-image photogrammetry.

For exterior facade measurements: tape the card to the wall at door height. For interior room measurements: tape the card to the wall you are measuring.


Performance summary

Grayscale + blur + Sobel milliseconds (native C)
Hough transform 15-30 seconds (Ruby, 1200x900 image)
Card detection < 1 second (Ruby)
Scale + measurement < 1 second (Ruby)
Overlay drawing < 1 second (ruby-libgd)

Total: under 35 seconds for a complete measurement report on a standard photo.


What this opens up

The same pipeline applies to any scene where you have a reference object:

  • Room interior (tape measure on the wall)
  • Furniture dimensions (credit card next to the piece)
  • Land plot estimation (person of known height in the frame)
  • Construction site progress (reference post at known height)

Ruby now has the building blocks for all of these. Edge detection and convolution are native C. The geometry is pure Ruby. The output is rendered by ruby-libgd.


Languages grow when communities push boundaries. Photogrammetry is one more boundary Ruby just crossed.



Support RubyStackNews

This time, to support RubyStackNews, the Jupyter Notebook with the MeasureProcessor Output Report is available for 15 USD.

If you enjoy RubyStackNews, consider supporting the project by purchasing the notebook, which includes a complete example for processing MeasureProcessor data.

Buy the Notebook

German Silva ( @ruby_stack_news )

Article content

Leave a comment