feat(elixir): implement proper AprilTag generation
This commit is contained in:
@@ -8,8 +8,9 @@ defmodule ComponentsElixir.AprilTag do
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias ComponentsElixir.AprilTag.Tag36h11
|
||||
|
||||
@tag36h11_count 587
|
||||
@apriltag_size 200
|
||||
|
||||
@doc """
|
||||
Returns the total number of available AprilTags in the tag36h11 family.
|
||||
@@ -116,43 +117,11 @@ defmodule ComponentsElixir.AprilTag do
|
||||
@doc """
|
||||
Generates an SVG string for an AprilTag with the given ID.
|
||||
|
||||
This creates a basic SVG representation of the AprilTag pattern
|
||||
with the ID displayed below it for human readability.
|
||||
|
||||
Note: This is a simplified implementation. For production use,
|
||||
you'd want to use the actual AprilTag generation algorithm or
|
||||
pre-generated assets.
|
||||
This creates an actual AprilTag pattern using real tag36h11 data
|
||||
when available, or falls back to a placeholder pattern.
|
||||
"""
|
||||
def generate_apriltag_svg(apriltag_id, opts \\ []) do
|
||||
size = Keyword.get(opts, :size, @apriltag_size)
|
||||
margin = Keyword.get(opts, :margin, div(size, 10))
|
||||
|
||||
# For now, create a placeholder square pattern
|
||||
# In a real implementation, you'd generate the actual AprilTag pattern
|
||||
square_size = size - (2 * margin)
|
||||
|
||||
"""
|
||||
<svg width="#{size}" height="#{size + 30}" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- White background -->
|
||||
<rect width="#{size}" height="#{size + 30}" fill="white"/>
|
||||
|
||||
<!-- AprilTag placeholder (simplified) -->
|
||||
<rect x="#{margin}" y="#{margin}" width="#{square_size}" height="#{square_size}"
|
||||
fill="white" stroke="black" stroke-width="2"/>
|
||||
|
||||
<!-- Simplified tag pattern - in reality this would be the actual AprilTag -->
|
||||
<rect x="#{margin + 10}" y="#{margin + 10}" width="#{square_size - 20}" height="#{square_size - 20}"
|
||||
fill="black"/>
|
||||
<rect x="#{margin + 20}" y="#{margin + 20}" width="#{square_size - 40}" height="#{square_size - 40}"
|
||||
fill="white"/>
|
||||
|
||||
<!-- ID text below -->
|
||||
<text x="#{size / 2}" y="#{size + 20}" text-anchor="middle"
|
||||
font-family="Arial" font-size="14" font-weight="bold">
|
||||
ID: #{String.pad_leading(to_string(apriltag_id), 3, "0")}
|
||||
</text>
|
||||
</svg>
|
||||
"""
|
||||
Tag36h11.generate_apriltag_svg(apriltag_id, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
243
lib/components_elixir/apriltag/tag36h11.ex
Normal file
243
lib/components_elixir/apriltag/tag36h11.ex
Normal file
@@ -0,0 +1,243 @@
|
||||
defmodule ComponentsElixir.AprilTag.Tag36h11 do
|
||||
@moduledoc """
|
||||
Tag36h11 AprilTag family data and generation.
|
||||
|
||||
Contains the actual bit patterns for all 587 tags in the tag36h11 family,
|
||||
sourced from the AprilRobotics apriltag-imgs repository.
|
||||
"""
|
||||
|
||||
import Bitwise
|
||||
|
||||
# PostScript file path containing all tag36h11 patterns
|
||||
@patterns_file "apriltags.ps"
|
||||
|
||||
# Parse a PostScript line like: (april.tag.Tag36h11, id = 42) <hexpattern> maketag
|
||||
defp parse_ps_line(line) do
|
||||
case Regex.run(~r/\(april\.tag\.Tag36h11, id = (\d+)\) <([0-9a-fA-F]+)> maketag/, line) do
|
||||
[_, id_str, hex_pattern] ->
|
||||
{String.to_integer(id_str), String.downcase(hex_pattern)}
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Extract patterns from PostScript file at compile time
|
||||
@all_patterns (
|
||||
path = Path.join([File.cwd!(), "apriltags.ps"])
|
||||
|
||||
if File.exists?(path) do
|
||||
File.read!(path)
|
||||
|> String.split("\n")
|
||||
|> Enum.filter(&String.contains?(&1, "april.tag.Tag36h11"))
|
||||
|> Enum.map(fn line ->
|
||||
case Regex.run(~r/\(april\.tag\.Tag36h11, id = (\d+)\) <([0-9a-fA-F]+)> maketag/, line) do
|
||||
[_, id_str, hex_pattern] ->
|
||||
{String.to_integer(id_str), String.downcase(hex_pattern)}
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Map.new()
|
||||
else
|
||||
raise "Error: apriltags.ps file not found in project root. This file is required for AprilTag pattern generation."
|
||||
end
|
||||
)
|
||||
|
||||
# Sample of real tag36h11 hex patterns from AprilRobotics repository
|
||||
# This will be populated with patterns extracted from the PostScript file
|
||||
@tag36h11_patterns @all_patterns
|
||||
|
||||
@doc """
|
||||
Extracts all tag36h11 hex patterns from the PostScript file.
|
||||
|
||||
Returns a map of {id => hex_pattern} for all 587 tags.
|
||||
"""
|
||||
def extract_patterns_from_ps_file(file_path \\ nil) do
|
||||
path = file_path || Path.join([File.cwd!(), @patterns_file])
|
||||
|
||||
if File.exists?(path) do
|
||||
File.read!(path)
|
||||
|> String.split("\n")
|
||||
|> Enum.filter(&String.contains?(&1, "april.tag.Tag36h11"))
|
||||
|> Enum.map(&parse_ps_line/1)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Map.new()
|
||||
else
|
||||
%{} # Return empty map if file not found, will fall back to hardcoded patterns
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the hex pattern for a given AprilTag ID.
|
||||
|
||||
Returns nil if the ID is not available in our pattern data.
|
||||
"""
|
||||
def get_hex_pattern(id) when is_integer(id) and id >= 0 and id < 587 do
|
||||
Map.get(@tag36h11_patterns, id)
|
||||
end
|
||||
def get_hex_pattern(_), do: nil
|
||||
|
||||
@doc """
|
||||
Converts a PostScript hex pattern to a 10x10 binary matrix using 2-bits-per-pixel
|
||||
with proper row padding handling.
|
||||
|
||||
PostScript pads each scanline to byte boundary: 10 pixels × 2 bits = 20 bits,
|
||||
rounded up to 24 bits = 3 bytes per row. The last 2 pixels per row are padding.
|
||||
|
||||
Returns a list of lists where true = black pixel, false = white pixel.
|
||||
"""
|
||||
def hex_to_binary_matrix_2bpp(hex_string) when is_binary(hex_string) do
|
||||
clean_hex =
|
||||
hex_string
|
||||
|> String.replace(~r/[<>\s]/, "")
|
||||
|> String.upcase()
|
||||
|
||||
bytes = Base.decode16!(clean_hex, case: :mixed)
|
||||
|
||||
# Each scanline is padded to a byte boundary: 3 bytes per 10 pixels (2 bpp)
|
||||
row_bytes = 3
|
||||
rows =
|
||||
for row <- 0..9 do
|
||||
<<_::binary-size(row * row_bytes), r::binary-size(row_bytes), _::binary>> = bytes
|
||||
<<b0, b1, b2>> = r
|
||||
|
||||
samples =
|
||||
[
|
||||
b0 >>> 6 &&& 0x3, b0 >>> 4 &&& 0x3, b0 >>> 2 &&& 0x3, b0 &&& 0x3,
|
||||
b1 >>> 6 &&& 0x3, b1 >>> 4 &&& 0x3, b1 >>> 2 &&& 0x3, b1 &&& 0x3,
|
||||
b2 >>> 6 &&& 0x3, b2 >>> 4 &&& 0x3, b2 >>> 2 &&& 0x3, b2 &&& 0x3
|
||||
]
|
||||
|> Enum.take(10) # drop the 2 padding samples at end of row
|
||||
|> Enum.map(&(&1 == 0)) # 0 = black, 3 = white → boolean
|
||||
|
||||
samples
|
||||
end
|
||||
|
||||
rows
|
||||
end
|
||||
|
||||
@doc """
|
||||
Legacy method - kept for backward compatibility.
|
||||
Use hex_to_binary_matrix_2bpp/1 for correct AprilTag parsing.
|
||||
"""
|
||||
def hex_to_binary_matrix(hex_string) when is_binary(hex_string) do
|
||||
hex_to_binary_matrix_2bpp(hex_string)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates an SVG string from a binary matrix.
|
||||
|
||||
Takes a 10x10 binary matrix and converts it to an SVG representation
|
||||
using integer coordinates with viewBox scaling to eliminate seams.
|
||||
Only black modules are drawn over a white background.
|
||||
"""
|
||||
def binary_matrix_to_svg(binary_matrix, opts \\ []) do
|
||||
size = Keyword.get(opts, :size, 200) # final CSS size in px
|
||||
id_text = Keyword.get(opts, :id_text, "")
|
||||
|
||||
# binary_matrix is 10x10 of booleans: true=black, false=white
|
||||
modules_w = 10
|
||||
modules_h = 10
|
||||
|
||||
# Build rects only for black modules, aligned to integer coords in the viewBox space
|
||||
black_cells =
|
||||
binary_matrix
|
||||
|> Enum.with_index()
|
||||
|> Enum.flat_map(fn {row, y} ->
|
||||
row
|
||||
|> Enum.with_index()
|
||||
|> Enum.filter(fn {is_black, _x} -> is_black end)
|
||||
|> Enum.map(fn {_is_black, x} ->
|
||||
~s(<rect x="#{x}" y="#{y}" width="1" height="1" />)
|
||||
end)
|
||||
end)
|
||||
|> Enum.join()
|
||||
|
||||
"""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="#{size}" height="#{size + 24}"
|
||||
viewBox="0 0 #{modules_w} #{modules_h + 2}"
|
||||
shape-rendering="crispEdges">
|
||||
<!-- white background (includes 2 units below for caption area) -->
|
||||
<rect x="0" y="0" width="#{modules_w}" height="#{modules_h + 2}" fill="white"/>
|
||||
<!-- tag area outline (optional) -->
|
||||
<rect x="0" y="0" width="#{modules_w}" height="#{modules_h}" fill="white" stroke="black" stroke-width="0"/>
|
||||
|
||||
<!-- black modules -->
|
||||
<g fill="black">
|
||||
#{black_cells}
|
||||
</g>
|
||||
|
||||
<!-- caption -->
|
||||
#{if id_text != "" do
|
||||
~s(<text x="#{modules_w/2}" y="#{modules_h + 1.4}" text-anchor="middle"
|
||||
font-family="Arial" font-size="0.9">#{id_text}</text>)
|
||||
else "" end}
|
||||
</svg>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates a complete AprilTag SVG for a given ID.
|
||||
|
||||
This is the main function that combines hex pattern lookup,
|
||||
binary conversion, and SVG generation.
|
||||
"""
|
||||
def generate_apriltag_svg(id, opts \\ []) do
|
||||
case get_hex_pattern(id) do
|
||||
nil ->
|
||||
# Fallback to placeholder for IDs we don't have patterns for yet
|
||||
generate_placeholder_svg(id, opts)
|
||||
|
||||
hex_pattern ->
|
||||
binary_matrix = hex_to_binary_matrix_2bpp(hex_pattern)
|
||||
id_text = "ID: #{String.pad_leading(to_string(id), 3, "0")}"
|
||||
opts_with_id = Keyword.put(opts, :id_text, id_text)
|
||||
binary_matrix_to_svg(binary_matrix, opts_with_id)
|
||||
end
|
||||
end # Generate a placeholder pattern for IDs we don't have real data for yet
|
||||
defp generate_placeholder_svg(id, opts) do
|
||||
size = Keyword.get(opts, :size, 200)
|
||||
margin = Keyword.get(opts, :margin, div(size, 10))
|
||||
square_size = size - (2 * margin)
|
||||
|
||||
"""
|
||||
<svg width="#{size}" height="#{size + 30}" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- White background -->
|
||||
<rect width="#{size}" height="#{size + 30}" fill="white"/>
|
||||
|
||||
<!-- AprilTag placeholder (simplified) -->
|
||||
<rect x="#{margin}" y="#{margin}" width="#{square_size}" height="#{square_size}"
|
||||
fill="white" stroke="black" stroke-width="2"/>
|
||||
|
||||
<!-- Simplified tag pattern - placeholder for missing real pattern -->
|
||||
<rect x="#{margin + 10}" y="#{margin + 10}" width="#{square_size - 20}" height="#{square_size - 20}"
|
||||
fill="black"/>
|
||||
<rect x="#{margin + 20}" y="#{margin + 20}" width="#{square_size - 40}" height="#{square_size - 40}"
|
||||
fill="white"/>
|
||||
|
||||
<!-- ID text below -->
|
||||
<text x="#{size / 2}" y="#{size + 20}" text-anchor="middle"
|
||||
font-family="Arial" font-size="14" font-weight="bold">
|
||||
ID: #{String.pad_leading(to_string(id), 3, "0")} (Placeholder)
|
||||
</text>
|
||||
</svg>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of AprilTag IDs that have real pattern data available.
|
||||
"""
|
||||
def available_patterns do
|
||||
Map.keys(@tag36h11_patterns) |> Enum.sort()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns whether a real pattern is available for the given ID.
|
||||
"""
|
||||
def has_real_pattern?(id) do
|
||||
Map.has_key?(@tag36h11_patterns, id)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user