Skip to content

Commit b6ed074

Browse files
committed
[panorama] added panorama annotation
1 parent 1c943d8 commit b6ed074

File tree

3 files changed

+309
-2
lines changed

3 files changed

+309
-2
lines changed

src/instrumentman/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def cli_convert() -> None:
117117
"""Convert between various file formats."""
118118

119119

120-
@cli.group("calculate", aliases=["calc"]) # type: ignore[misc]
120+
@cli.group("calculate", aliases=["calc", "process"]) # type: ignore[misc]
121121
def cli_calc() -> None:
122122
"""Preform calculations from measurement results."""
123123

@@ -161,6 +161,7 @@ def cli_upload() -> None:
161161
cli_calc.add_command(setmeasurement.cli_calc)
162162
cli_calc.add_command(inclination.cli_calc)
163163
cli_calc.add_command(station.cli_calc)
164+
cli_calc.add_command(panorama.cli_calc)
164165
cli_test.add_command(protocoltest.cli_geocom)
165166
cli_test.add_command(protocoltest.cli_gsidna)
166167
cli_merge.add_command(setmeasurement.cli_merge)

src/instrumentman/panorama/__init__.py

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
extra_command,
55
argument,
66
option,
7+
option_group,
78
File,
8-
Choice
9+
file_path,
10+
Choice,
11+
IntRange
912
)
13+
from cloup.constraints import constraint, If, Equal, Not, require_all
1014

1115
from ..utils import (
1216
com_port_argument,
@@ -39,3 +43,83 @@ def cli_measure(**kwargs: Any) -> None:
3943
from .measure import main
4044

4145
main(**kwargs)
46+
47+
48+
@extra_command(
49+
"panorama",
50+
params=None,
51+
context_settings={"auto_envvar_prefix": None}
52+
) # type: ignore[misc]
53+
@argument(
54+
"metadata",
55+
help="Metadata file produced by the measurement program",
56+
type=file_path(exists=True)
57+
)
58+
@argument(
59+
"image",
60+
help="Panorama image part",
61+
type=file_path(exists=True),
62+
nargs=-1,
63+
required=True
64+
)
65+
@option(
66+
"--action",
67+
help="Processing to perform",
68+
type=Choice(("annotate",), case_sensitive=False),
69+
default="annotate"
70+
)
71+
@option_group(
72+
"Point list file options",
73+
option(
74+
"--points",
75+
help="Coordinate list of points to annotate on the images",
76+
type=file_path(exists=True)
77+
),
78+
option(
79+
"--skip",
80+
help="Number of header rows to skip",
81+
type=IntRange(0),
82+
default=0
83+
),
84+
option(
85+
"--delimiter",
86+
help="Column delimiter",
87+
type=str,
88+
default=","
89+
)
90+
)
91+
@option_group(
92+
"Annotation options",
93+
option(
94+
"--rgb",
95+
help="Color in RGB8 notation",
96+
type=(IntRange(0, 255), IntRange(0, 255), IntRange(0, 255)),
97+
default=(0, 0, 0)
98+
),
99+
option(
100+
"--fontsize",
101+
help="Text size in pixels",
102+
type=IntRange(1),
103+
default=50
104+
),
105+
option(
106+
"--marker",
107+
help="Point marker shape",
108+
type=Choice(("cross", "dot"), case_sensitive=False),
109+
default="cross"
110+
),
111+
option(
112+
"--markersize",
113+
help="Point marker size in pixels",
114+
type=IntRange(1),
115+
default=50
116+
)
117+
)
118+
@constraint(
119+
If(Not(Equal("action", "stitch")), require_all),
120+
["action", "points"]
121+
)
122+
def cli_calc(**kwargs: Any) -> None:
123+
from .process import main
124+
125+
main(**kwargs)
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
from pathlib import Path
2+
import math
3+
from typing import Callable
4+
5+
from PIL import Image, ImageDraw, ImageFont
6+
from geocompy.data import Coordinate, Angle, Vector
7+
8+
9+
def read_points(
10+
path: Path,
11+
skip: int = 0,
12+
delimiter: str = ";"
13+
) -> list[tuple[str, Coordinate]]:
14+
points: list[tuple[str, Coordinate]] = []
15+
with path.open("rt", encoding="utf8") as file:
16+
for i in range(skip):
17+
next(file)
18+
19+
for line in file:
20+
pt, x, y, z = line.strip().split(delimiter)
21+
points.append(
22+
(
23+
pt,
24+
Coordinate(
25+
float(x),
26+
float(y),
27+
float(z)
28+
)
29+
)
30+
)
31+
32+
return points
33+
34+
35+
def read_metadata(
36+
path: Path
37+
) -> dict[str, tuple[Angle, Angle, Coordinate, Vector]]:
38+
pictures: dict[str, tuple[Angle, Angle, Coordinate, Vector]] = {}
39+
with path.open("rt", encoding="utf8") as file:
40+
next(file)
41+
for line in file:
42+
(
43+
img,
44+
fov_hz,
45+
fov_v,
46+
pos_x,
47+
pos_y,
48+
pos_z,
49+
dir_x,
50+
dir_y,
51+
dir_z
52+
) = line.strip().split(",")
53+
54+
pictures[img] = (
55+
Angle.parse(fov_hz),
56+
Angle.parse(fov_v),
57+
Coordinate(
58+
float(pos_x),
59+
float(pos_y),
60+
float(pos_z)
61+
),
62+
Vector(
63+
float(dir_x),
64+
float(dir_y),
65+
float(dir_z)
66+
)
67+
)
68+
69+
return pictures
70+
71+
72+
def draw_marker_dot(
73+
draw: ImageDraw.ImageDraw,
74+
x: float,
75+
y: float,
76+
text: str,
77+
font: ImageFont.FreeTypeFont,
78+
markersize: float,
79+
rgb: tuple[int, int, int]
80+
) -> None:
81+
draw.circle((x, y), markersize / 2, rgb)
82+
draw.text(
83+
(
84+
x + markersize / 3,
85+
y + markersize / 3
86+
), text, rgb, font, anchor="la"
87+
)
88+
89+
90+
def draw_marker_cross(
91+
draw: ImageDraw.ImageDraw,
92+
x: float,
93+
y: float,
94+
text: str,
95+
font: ImageFont.FreeTypeFont,
96+
markersize: float,
97+
rgb: tuple[int, int, int]
98+
) -> None:
99+
leg = markersize / 2
100+
draw.line(
101+
(
102+
x - leg, y,
103+
x + leg, y
104+
),
105+
rgb,
106+
round(markersize / 10)
107+
)
108+
draw.line(
109+
(
110+
x, y - leg,
111+
x, y + leg
112+
),
113+
rgb,
114+
round(markersize / 10)
115+
)
116+
draw.text(
117+
(
118+
x + markersize / 4,
119+
y + markersize / 4
120+
), text, rgb, font, anchor="la"
121+
)
122+
123+
124+
def annotate_image(
125+
imgpath: Path,
126+
info: tuple[Angle, Angle, Coordinate, Vector],
127+
points: list[tuple[str, Coordinate]],
128+
markerdrawer: Callable[[ImageDraw.ImageDraw, float, float, str], None]
129+
) -> None:
130+
image = Image.open(imgpath)
131+
draw = ImageDraw.Draw(image)
132+
fov_hz, fov_v, pos, vec = info
133+
half_width = image.width / 2
134+
half_height = image.height / 2
135+
half_fov_hz = fov_hz / 2
136+
half_fov_v = fov_v / 2
137+
138+
img_hz, img_v, _ = (Coordinate.from_vector(vec) - pos).to_polar()
139+
140+
for pt, coord in points:
141+
pt_hz, pt_v, _ = (coord - pos).to_polar()
142+
alpha = pt_hz.relative_to(img_hz)
143+
beta = pt_v.relative_to(img_v)
144+
145+
x = round(half_width + math.tan(alpha) *
146+
half_width / math.tan(half_fov_hz))
147+
y = round(half_height + math.tan(beta) *
148+
half_height / math.tan(half_fov_v))
149+
150+
markerdrawer(draw, x, y, pt)
151+
152+
image.save(
153+
imgpath.parent.joinpath(
154+
imgpath.stem + "_annotated" + imgpath.suffix
155+
)
156+
)
157+
158+
159+
def run_annotate(
160+
meta: dict[str, tuple[Angle, Angle, Coordinate, Vector]],
161+
images: tuple[Path],
162+
points: list[tuple[str, Coordinate]],
163+
rgb: tuple[int, int, int] = (0, 0, 0),
164+
fontsize: int = 50,
165+
marker: str = "cross",
166+
markersize: int = 50
167+
) -> None:
168+
font = ImageFont.truetype("arial.ttf", fontsize)
169+
match marker:
170+
case "cross":
171+
markerdrawer = draw_marker_cross
172+
case "dot":
173+
markerdrawer = draw_marker_dot
174+
175+
for path in images:
176+
info = meta.get(path.stem + path.suffix)
177+
if info is None:
178+
continue
179+
180+
annotate_image(
181+
path,
182+
info,
183+
points,
184+
lambda draw, x, y, text: markerdrawer(
185+
draw,
186+
x,
187+
y,
188+
text,
189+
font,
190+
markersize,
191+
rgb
192+
)
193+
)
194+
195+
196+
def main(
197+
metadata: Path,
198+
image: tuple[Path],
199+
action: str = "annotate",
200+
points: Path | None = None,
201+
skip: int = 0,
202+
delimiter: str = ",",
203+
rgb: tuple[int, int, int] = (0, 0, 0),
204+
fontsize: int = 50,
205+
marker: str = "cross",
206+
markersize: int = 50
207+
) -> None:
208+
meta = read_metadata(metadata)
209+
match action:
210+
case "annotate" if points is not None:
211+
point = read_points(points, skip, delimiter)
212+
run_annotate(
213+
meta,
214+
image,
215+
point,
216+
rgb,
217+
fontsize,
218+
marker,
219+
markersize
220+
)
221+
case _:
222+
raise ValueError(f"Unknown action '{action}'")

0 commit comments

Comments
 (0)