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 NotebookThe 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.1mDoor width: 0.9mWindow width: 1.2mWindow height: 1.4mWall 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 / heightCARD_RATIO_TOL = 0.15def 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 nilend
Step 7: Scale calibration
Once the card is found in pixels:
card = find_credit_card(lines)scale_px_per_mm = card[:width_px] / 85.6 # pixels per millimeterdef 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 lineresult.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 labelresult.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.
- ruby-libgd: https://github.com/ggerman/ruby-libgd
- docs: https://ggerman.github.io/ruby-libgd/en/index.html
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 NotebookGerman Silva ( @ruby_stack_news )