A4 용지에 ArUco 마커를 실제 크기(mm) 기준으로 배치해서 PDF로 저장하는 파이썬 소스

OpenCV 공식 문서 기준으로 ArUco 이미지는 cv2.aruco.generateImageMarker()로 생성하며, 예를 들어 DICT_6X6_250은 ID 0~249 범위를 사용합니다.

1. 설치

pip install opencv-contrib-python pillow numpy

2. 소스

aruco_print.py

import cv2
import numpy as np
from PIL import Image, ImageDraw, ImageFont
import argparse
import os


A4_WIDTH_MM = 210
A4_HEIGHT_MM = 297


def mm_to_px(mm, dpi):
    return int(round(mm / 25.4 * dpi))


def generate_aruco_marker(dictionary_name, marker_id, marker_px, border_bits=1):
    aruco = cv2.aruco

    if not hasattr(aruco, dictionary_name):
        raise ValueError(f"지원하지 않는 dictionary 이름입니다: {dictionary_name}")

    dictionary_id = getattr(aruco, dictionary_name)
    dictionary = aruco.getPredefinedDictionary(dictionary_id)

    # OpenCV 4.7+
    if hasattr(aruco, "generateImageMarker"):
        marker_img = aruco.generateImageMarker(
            dictionary,
            marker_id,
            marker_px,
            borderBits=border_bits
        )
    # 구버전 OpenCV 대응
    elif hasattr(aruco, "drawMarker"):
        marker_img = np.zeros((marker_px, marker_px), dtype=np.uint8)
        aruco.drawMarker(dictionary, marker_id, marker_px, marker_img, border_bits)
    else:
        raise RuntimeError("cv2.aruco.generateImageMarker 또는 drawMarker를 찾을 수 없습니다.")

    return Image.fromarray(marker_img).convert("RGB")


def create_aruco_print_sheet(
    output_path,
    dictionary_name="DICT_6X6_250",
    start_id=0,
    count=12,
    marker_size_mm=40,
    margin_mm=10,
    gap_mm=8,
    dpi=300,
    border_bits=1,
    show_id=True
):
    page_w_px = mm_to_px(A4_WIDTH_MM, dpi)
    page_h_px = mm_to_px(A4_HEIGHT_MM, dpi)

    margin_px = mm_to_px(margin_mm, dpi)
    gap_px = mm_to_px(gap_mm, dpi)
    marker_px = mm_to_px(marker_size_mm, dpi)

    label_h_px = mm_to_px(7, dpi) if show_id else 0

    cell_w_px = marker_px + gap_px
    cell_h_px = marker_px + label_h_px + gap_px

    usable_w_px = page_w_px - 2 * margin_px
    usable_h_px = page_h_px - 2 * margin_px

    cols = max(1, usable_w_px // cell_w_px)
    rows = max(1, usable_h_px // cell_h_px)
    max_markers_per_page = cols * rows

    if count > max_markers_per_page:
        print(f"[경고] 현재 설정에서는 A4 한 장에 {max_markers_per_page}개만 들어갑니다.")
        print(f"[정보] 요청 개수 {count}개 중 {max_markers_per_page}개만 생성합니다.")
        count = max_markers_per_page

    page = Image.new("RGB", (page_w_px, page_h_px), "white")
    draw = ImageDraw.Draw(page)

    try:
        font = ImageFont.truetype("DejaVuSans.ttf", mm_to_px(4, dpi))
    except:
        font = ImageFont.load_default()

    for i in range(count):
        marker_id = start_id + i

        row = i // cols
        col = i % cols

        x = margin_px + col * cell_w_px
        y = margin_px + row * cell_h_px

        marker = generate_aruco_marker(
            dictionary_name=dictionary_name,
            marker_id=marker_id,
            marker_px=marker_px,
            border_bits=border_bits
        )

        page.paste(marker, (x, y))

        if show_id:
            text = f"{dictionary_name} / ID {marker_id} / {marker_size_mm}mm"
            text_x = x
            text_y = y + marker_px + mm_to_px(1.5, dpi)
            draw.text((text_x, text_y), text, fill="black", font=font)

    os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)

    ext = os.path.splitext(output_path)[1].lower()

    if ext == ".pdf":
        page.save(output_path, "PDF", resolution=dpi)
    elif ext in [".png", ".jpg", ".jpeg"]:
        page.save(output_path)
    else:
        raise ValueError("출력 파일 확장자는 .pdf, .png, .jpg 중 하나를 사용하세요.")

    print(f"[완료] 저장됨: {output_path}")
    print(f"[설정] dictionary={dictionary_name}, start_id={start_id}, count={count}")
    print(f"[설정] marker_size={marker_size_mm}mm, dpi={dpi}, A4={A4_WIDTH_MM}x{A4_HEIGHT_MM}mm")


def main():
    parser = argparse.ArgumentParser(description="A4 ArUco 마커 인쇄용 PDF/이미지 생성기")

    parser.add_argument("--output", default="aruco_markers.pdf", help="출력 파일명")
    parser.add_argument("--dict", default="DICT_6X6_250", help="ArUco dictionary 이름")
    parser.add_argument("--start-id", type=int, default=0, help="시작 마커 ID")
    parser.add_argument("--count", type=int, default=12, help="생성할 마커 개수")
    parser.add_argument("--size-mm", type=float, default=40, help="마커 한 변 크기(mm)")
    parser.add_argument("--margin-mm", type=float, default=10, help="A4 여백(mm)")
    parser.add_argument("--gap-mm", type=float, default=8, help="마커 간격(mm)")
    parser.add_argument("--dpi", type=int, default=300, help="출력 DPI")
    parser.add_argument("--border-bits", type=int, default=1, help="ArUco 검은 테두리 bit 수")
    parser.add_argument("--no-label", action="store_true", help="ID 라벨 숨김")

    args = parser.parse_args()

    create_aruco_print_sheet(
        output_path=args.output,
        dictionary_name=args.dict,
        start_id=args.start_id,
        count=args.count,
        marker_size_mm=args.size_mm,
        margin_mm=args.margin_mm,
        gap_mm=args.gap_mm,
        dpi=args.dpi,
        border_bits=args.border_bits,
        show_id=not args.no_label
    )


if __name__ == "__main__":
    main()

3. 실행 예시

1) A4 PDF에 40mm 마커 12개 출력

python3 aruco_print.py --output aruco_markers.pdf --dict DICT_6X6_250 --start-id 0 --count 12 --size-mm 40

2) 50mm 마커 6개 출력

python3 aruco_print.py --output aruco_50mm.pdf --dict DICT_5X5_100 --start-id 1 --count 6 --size-mm 50

2) 60mm 마커 6개 출력

python3 aruco_print.py --output aruco_60mm.pdf --dict DICT_6X6_250 --start-id 10 --count 6 --size-mm 60

3) ID 라벨 없이 출력

python3 aruco_print.py --output aruco_clean.pdf --start-id 0 --count 8 --size-mm 50 --no-label

python3 aruco_print.py --output 4X4_50_100mm_aruco_markers.pdf --dict DICT_4X4_50 --start-id 0 --count 1 --size-mm 100

4) 자주 쓰는 Dictionary

DICT_4X4_50
DICT_4X4_100
DICT_4X4_250
DICT_5X5_100
DICT_5X5_250
DICT_6X6_250
DICT_6X6_1000
DICT_7X7_250
DICT_APRILTAG_36h11

로봇/드론 비전용이면 보통은 DICT_6X6_250 + 40~80mm부터 쓰는 게 무난합니다. 너무 작은 마커는 거리 멀어지면 인식률이 바로 떨어집니다.

5. 전체 인쇄소스

touch aruco_DICT_4X4_50_10cm_A4.py

import cv2
import cv2.aruco as aruco
import numpy as np
from PIL import Image
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
from reportlab.lib.utils import ImageReader


# =========================
# 설정값
# =========================
#OUTPUT_PDF = "aruco_DICT_6X6_250_10cm_A4.pdf"
#OUTPUT_PDF = "aruco_DICT_5X5_100_10cm_A4.pdf"
OUTPUT_PDF = "aruco_DICT_4X4_50_10cm_A4.pdf"

MARKER_SIZE_CM = 10
MARKER_PIXEL_SIZE = 600

START_ID = 0
#END_ID = 249
#END_ID = 99
END_ID = 49

SHOW_ID_TEXT = True


# =========================
# ArUco Dictionary
# =========================
#aruco_dict = aruco.getPredefinedDictionary(aruco.DICT_6X6_250)
#aruco_dict = aruco.getPredefinedDictionary(aruco.DICT_5X5_100)
aruco_dict = aruco.getPredefinedDictionary(aruco.DICT_4X4_50)


def generate_marker(marker_id: int, size_px: int) -> Image.Image:
    """
    OpenCV 버전에 따라 generateImageMarker 또는 drawMarker 사용
    """

    # OpenCV 최신 API
    if hasattr(aruco, "generateImageMarker"):
        marker_img = aruco.generateImageMarker(
            aruco_dict,
            marker_id,
            size_px
        )

    # OpenCV 구버전 API
    elif hasattr(aruco, "drawMarker"):
        marker_img = np.zeros((size_px, size_px), dtype=np.uint8)
        aruco.drawMarker(
            aruco_dict,
            marker_id,
            size_px,
            marker_img,
            1
        )

    else:
        raise RuntimeError(
            "현재 OpenCV의 cv2.aruco에 generateImageMarker()와 drawMarker()가 모두 없습니다.\n"
            "opencv-contrib-python을 설치해야 합니다.\n"
            "설치 명령:\n"
            "pip uninstall opencv-python opencv-contrib-python -y\n"
            "pip install opencv-contrib-python"
        )

    return Image.fromarray(marker_img)


def create_a4_pdf():
    page_width, page_height = A4
    marker_size_pt = MARKER_SIZE_CM * cm

    pdf = canvas.Canvas(OUTPUT_PDF, pagesize=A4)

    for marker_id in range(START_ID, END_ID + 1):
        marker_pil = generate_marker(marker_id, MARKER_PIXEL_SIZE)
        marker_reader = ImageReader(marker_pil)

        # A4 중앙 배치
        x = (page_width - marker_size_pt) / 2
        y = (page_height - marker_size_pt) / 2

        pdf.drawImage(
            marker_reader,
            x,
            y,
            width=marker_size_pt,
            height=marker_size_pt,
            preserveAspectRatio=True,
            mask="auto"
        )

        if SHOW_ID_TEXT:
            pdf.setFont("Helvetica", 16)
            pdf.drawCentredString(
                page_width / 2,
                y - 3.6 * cm,
                f"DICT_4X4_50 ID: {marker_id}"
            )

        pdf.showPage()

    pdf.save()
    print(f"PDF 생성 완료: {OUTPUT_PDF}")


if __name__ == "__main__":
    print("OpenCV version:", cv2.__version__)
    print("has generateImageMarker:", hasattr(aruco, "generateImageMarker"))
    print("has drawMarker:", hasattr(aruco, "drawMarker"))

    create_a4_pdf()

Leave a Comment