Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aruco markers #511

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
4,142 changes: 2,393 additions & 1,749 deletions poetry.lock

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ matplotlib = "^3.5.3"
reportlab = "^3.6.11"
toml = "^0.10.2"
celery = {extras = ["SQLAlchemy", "redis"], version = "^5.2.7"}
opencv-python-headless = "^4.10.0.84"
opencv-python-headless = "<4.8.0.76" # https://github.com/ultralytics/ultralytics/issues/893
qrcode = "^7.3.1"
svglib = "^1.4.1"
PyMuPDF = {extras = ["Pillow"], version = "^1.21.0"}
Expand All @@ -39,11 +39,11 @@ torchvision = [
{ version = "^0.19.0+cpu", source = "pytorch-cpu", platform = "linux" },
{ version = "^0.19.0", source = "pypi", platform = "darwin"}
]
"sam 2" = { git = "https://github.com/facebookresearch/segment-anything-2.git" }
"sam-2" = { git = "https://github.com/facebookresearch/segment-anything-2.git" }
setuptools = "^72.2.0" # dependencies of gdal
numpy = "^2.0.1" # dependencies of gdal
wheel = "^0.43.0" # dependencies of gdal
flower = "^2.0.1"
numpy = "<2" # dependency of gdal and opencv

[tool.poetry.group.dev.dependencies]
# Versions are fixed to match versions used by pre-commit
Expand All @@ -55,12 +55,12 @@ vcrpy = "^4.2.1"
pytest-celery = "^1.0.1"
locust = "^2.14.2"
hypothesis = "^6.88.4"
ruff = "^0.4.7"
ruff = "^0.7.3"
approvaltests = "^12.0.0"
matplotlib = "^3.8.4"
geopandas = "^1.0.1"
testcontainers = {extras = ["postgres", "redis"], version = "^4.7.1"}
opencv-python = "^4.6.0"
opencv-python = "<4.8.0.76"
psycopg2-binary = "^2.9.9" # dev only. In prod psycopg2 (non-binary) depending on system libraries (libpg) is used.


Expand Down
139 changes: 91 additions & 48 deletions sketch_map_tool/map_generation/generate_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,36 @@
from io import BytesIO
from typing import Tuple

import cv2
import fitz
import reportlab.pdfgen.canvas
from PIL import Image as PILImage
from PIL import Image
from reportlab.graphics import renderPDF
from reportlab.graphics.shapes import Drawing
from reportlab.lib.pagesizes import landscape
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import cm
from reportlab.lib.utils import ImageReader
from reportlab.pdfgen import canvas
from reportlab.platypus import Frame, Paragraph
from reportlab.platypus.flowables import Image, Spacer
from reportlab.pdfgen.canvas import Canvas

# from reportlab.pdfgen import canvas
from reportlab.platypus import Frame, Paragraph, flowables
from svglib.svglib import svg2rlg

from sketch_map_tool.definitions import PDF_RESOURCES_PATH, get_attribution
from sketch_map_tool.helpers import resize_rlg_by_width
from sketch_map_tool.models import Layer, PaperFormat

# PIL should be able to open high resolution PNGs of large Maps:
Image.MAX_IMAGE_PIXELS = None
flowables.Image.MAX_IMAGE_PIXELS = None


def generate_pdf(
map_image_input: PILImage,
map_image_input: Image.Image,
qr_code: Drawing,
format_: PaperFormat,
scale: float,
layer: Layer,
aruco_markers: bool = False,
) -> Tuple[BytesIO, BytesIO]:
"""
Generate a sketch map pdf, i.e. a PDF containing the given map image
Expand Down Expand Up @@ -89,15 +91,15 @@ def generate_pdf(
portrait,
m_per_px,
img_format,
aruco_markers,
)

map_pdf = BytesIO()
# create output canvas
canv_map = canvas.Canvas(map_pdf)
canv_map.setPageSize(landscape((format_.height * cm, format_.width * cm)))
canvas = Canvas(map_pdf)
canvas.setPageSize(landscape((format_.height * cm, format_.width * cm)))
# Add map to canvas:
canv_map_margin = map_margin
canv_map.drawImage(
canvas.drawImage(
ImageReader(map_img),
canv_map_margin * cm,
canv_map_margin * cm,
Expand All @@ -108,19 +110,19 @@ def generate_pdf(

# TODO move to create_map_frame
# Add a border around the map
canv_map.rect(
canvas.rect(
map_margin * cm,
map_margin * cm,
frame_width * cm,
frame_height * cm,
fill=0,
)

canv_map.setFontSize(format_.font_size)
canv_map.setFillColorRGB(0, 0, 0)
canvas.setFontSize(format_.font_size)
canvas.setFillColorRGB(0, 0, 0)

draw_right_column(
canv_map,
canvas,
column_width,
column_height,
column_origin_x,
Expand All @@ -134,14 +136,14 @@ def generate_pdf(
)

# Generate PDFs:
canv_map.save()
canvas.save()

map_pdf.seek(0)
map_img.seek(0)

if portrait: # Rotate the map frame for correct georeferencing
map_img_rotated_bytes = BytesIO()
map_img_rotated = PILImage.open(map_img).rotate(270, expand=1)
map_img_rotated = Image.open(map_img).rotate(270, expand=1)
map_img_rotated.save(map_img_rotated_bytes, format="png")
map_img_rotated_bytes.seek(0)
map_img = map_img_rotated_bytes
Expand All @@ -150,7 +152,7 @@ def generate_pdf(


def draw_right_column(
canv: canvas.Canvas,
canvas: Canvas,
width: float,
height: float,
x: float,
Expand Down Expand Up @@ -184,21 +186,23 @@ def draw_right_column(

# fills up the remaining space, placed between
# the TOP and the BOTTOM aligned elements
space_filler = Spacer(width, 0) # height will be filled after list creation
space_filler = flowables.Spacer(
width, 0
) # height will be filled after list creation
# order all elements in column
flowables = [
flowables_ = [
smt_logo,
Spacer(width, 2 * em),
flowables.Spacer(width, 2 * em),
heigit_logo,
space_filler,
compass,
Spacer(width, 2 * em),
flowables.Spacer(width, 2 * em),
p_copyright,
Spacer(width, 2 * em),
flowables.Spacer(width, 2 * em),
qr_code,
]
space_filler.height = calculate_space_filler_height(
canv, flowables, width, height, margin
canvas, flowables_, width, height, margin
)
frame = Frame(
x,
Expand All @@ -210,15 +214,15 @@ def draw_right_column(
rightPadding=margin,
topPadding=margin,
)
frame.addFromList(flowables, canv)
frame.addFromList(flowables_, canvas)


def calculate_space_filler_height(canv, flowables, width, height, margin):
def calculate_space_filler_height(canvas, flowables_, width, height, margin):
flowables_height = 0
for f in flowables:
for f in flowables_:
if isinstance(f, Paragraph):
# a Paragraph doesn't have a height without this command
f.wrapOn(canv, width - margin, height)
f.wrapOn(canvas, width - margin, height)
flowables_height += f.height
return height - 2 * margin - flowables_height

Expand Down Expand Up @@ -249,45 +253,49 @@ def create_map_frame(
portrait: bool,
m_per_px: float,
img_format: str,
aruco_markers: bool = False,
) -> BytesIO:
map_frame = BytesIO()
canv = canvas.Canvas(map_frame)
canv.setPageSize(landscape((height, width)))
canvas = Canvas(map_frame)
canvas.setPageSize(landscape((height, width)))

globe_size = width / 37
if portrait:
canv.rotate(90)
canv.drawImage(
canvas.rotate(90)
canvas.drawImage(
map_image,
0,
-height,
mask="auto",
width=width,
height=height,
)
canv.rotate(-90)
add_globes(canv, globe_size, height=width, width=height)
canvas.rotate(-90)
draw_markers(canvas, globe_size, height=width, width=height)
add_scalebar(
canv, width=height, height=width, m_per_px=m_per_px, paper_format=format_
canvas, width=height, height=width, m_per_px=m_per_px, paper_format=format_
)
else:
canv.drawImage(
canvas.drawImage(
map_image,
0,
0,
mask="auto",
width=width,
height=height,
)
add_globes(canv, globe_size, height, width)
add_scalebar(canv, width, height, m_per_px, format_)
if aruco_markers:
draw_markers(canvas, globe_size, height, width)
else:
draw_globes(canvas, globe_size, height, width)
add_scalebar(canvas, width, height, m_per_px, format_)

canv.save()
canvas.save()
map_frame.seek(0)
return pdf_page_to_img(map_frame, img_format=img_format)


def add_globes(canv: canvas.Canvas, size: float, height: float, width: float):
def draw_globes(canvas: Canvas, size: float, height: float, width: float):
globe_1, globe_2, globe_3, globe_4 = get_globes(size)

h = height - size
Expand Down Expand Up @@ -322,7 +330,7 @@ def add_globes(canv: canvas.Canvas, size: float, height: float, width: float):
(w / 2, 0),
]
for globe, (x, y) in zip(globes, positions):
renderPDF.draw(globe, canv, x, y)
renderPDF.draw(globe, canvas, x, y)


def get_globes(expected_size) -> Tuple[Drawing, ...]:
Expand All @@ -335,6 +343,32 @@ def get_globes(expected_size) -> Tuple[Drawing, ...]:
return tuple(globes)


def draw_markers(canvas: Canvas, size: float, height: float, width: float):
markers = get_aruco_markers(int(size))

h = height - size
w = width - size

positions = [
# corner globes
# bottom left
(0, 0),
# top left
(0, h),
# top right
(w, h),
# bottom right
(w, 0),
# middle globes
(0, h / 2),
(w / 2, h),
(w, h / 2),
(w / 2, 0),
]
for m, (x, y) in zip(markers, positions):
canvas.drawImage(ImageReader(Image.fromarray(m)), x, y)


def get_compass(size: float, portrait=False) -> Drawing:
file_name = "north.svg"
if portrait:
Expand All @@ -345,7 +379,7 @@ def get_compass(size: float, portrait=False) -> Drawing:


def add_scalebar(
canv: reportlab.pdfgen.canvas.Canvas,
canvas: Canvas,
width: int,
height: int,
m_per_px: float,
Expand Down Expand Up @@ -378,23 +412,23 @@ def add_scalebar(
width + paper_format.scale_relative_xy[0] - scale_bar_length,
height + paper_format.scale_relative_xy[1],
)
canv.setFillColorRGB(255, 255, 255)
canvas.setFillColorRGB(255, 255, 255)
background_params = paper_format.scale_background_params
canv.rect(
canvas.rect(
scale_bar_x + background_params[0],
scale_bar_y + background_params[1],
scale_bar_length + background_params[2],
background_params[3],
fill=True,
)
canv.setFillColorRGB(0, 0, 0)
canv.rect(
canvas.setFillColorRGB(0, 0, 0)
canvas.rect(
scale_bar_x, scale_bar_y, scale_bar_length, paper_format.scale_height, fill=True
)
canv.setFont(
canvas.setFont(
"Times-Roman", paper_format.font_size * 2
) # Should be a bit bigger than e.g. the copyright note
canv.drawString(
canvas.drawString(
scale_bar_x,
scale_bar_y - paper_format.scale_distance_to_text,
f"{corresponding_meters}m",
Expand All @@ -412,3 +446,12 @@ def pdf_page_to_img(pdf: BytesIO, img_format, page_id=0) -> BytesIO:
page.get_pixmap().pil_save(img, format=img_format)
img.seek(0)
return img


# TODO: add typing
def get_aruco_markers(size: int) -> tuple:
dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50)
markers = []
for i in range(8):
markers.append(cv2.aruco.generateImageMarker(dictionary, i, size))
return tuple(markers)
12 changes: 12 additions & 0 deletions sketch_map_tool/upload_processing/clip.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,18 @@ def clip(photo: NDArray, template: NDArray) -> NDArray:
return cv2.warpPerspective(photo, homography_matrix, (width, height))


def detect_aruco_markers(image: NDArray):
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
parameters = cv2.aruco.DetectorParameters()
dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50)
detector = cv2.aruco.ArucoDetector(dictionary=dictionary, detectorParams=parameters)
marker_corners, marker_ids, _ = detector.detectMarkers(gray)
if marker_ids is None:
# TODO: create custom exception
raise AssertionError
return marker_corners, marker_ids


def limit_keypoints(
keypoints: list,
descriptors: NDArray,
Expand Down
Loading
Loading