Folder structure
README.md
How to run it in the container
To run development
docker compose \
-f docker-compose.yml \
-f docker-compose.dev.yml \
up
To run production:
docker compose down
docker compose build --no-cache
docker compose up -d
The difference between dev and production version is that the app is not in the volume
To run witout a docker compose
docker run --rm \
-v "$(pwd):/app" \
-w /app \
zeiss-positions \
python script.py \
examples/B_01_old_refs.pos \
examples/B_02_new_refs.pos \
examples/B_03_old_positions.pos
docker-compose.yml
services:
zeiss-positions:
build: .
restart: unless-stopped
ports:
- "5000:5000"
command: python app.py
docker-compose.dev.yml
services:
zeiss-positions:
volumes:
- .:/app
Dockerfile
FROM python:3.13.9-slim@sha256:ed5ef34cda36cfa2f3a340f07cac7e7814f91c7f3c411f6d3562323a866c5c66
WORKDIR /app
# Create virtual environment
RUN python -m venv /venv
# Use it from now on
ENV PATH="/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN mkdir jobs uploads outputs
COPY . .
CMD ["python", "script.py"]
NOTE: Adding the @sha256:… binds it to the most definded version of python as possible.
I add it after the fact it was build, the information is hiddein in:
docker images # to get the name of the image
docker inspect <image-name> # search tehere for the sha
app.py
from flask import Flask
from flask import render_template
from flask import request
from flask import send_file
import os
import uuid
import sys
import subprocess
app = Flask(__name__)
@app.route("/")
def home():
return render_template(
"convert.html"
)
@app.route("/convert", methods=["POST"])
def transform():
old_refs = request.files["old_refs"]
new_refs = request.files["new_refs"]
positions = request.files["positions"]
job = uuid.uuid4().hex
job_dir = os.path.join("jobs", job)
os.makedirs(job_dir)
output_path = os.path.join(job_dir, "converted.pos")
# Destination filenames
old_refs_path = os.path.join(job_dir, "old_refs.pos")
new_refs_path = os.path.join(job_dir, "new_refs.pos")
positions_path = os.path.join(job_dir, "old_positions.pos")
# Save uploaded files
old_refs.save(old_refs_path)
new_refs.save(new_refs_path)
positions.save(positions_path)
subprocess.run(
[
"python",
"script.py",
old_refs_path,
new_refs_path,
positions_path,
],
check=True,
)
# Return the converted file
return send_file(
output_path,
as_attachment=True,
download_name="converted.pos"
)
app.run(
host="0.0.0.0",
port=5000,
debug=True
)
requirments.txt
blinker==1.9.0
click==8.4.2
Flask==3.1.3
ImageIO==2.37.3
itsdangerous==2.2.0
Jinja2==3.1.6
lazy-loader==0.5
MarkupSafe==3.0.3
networkx==3.6.1
numpy==2.5.0
packaging==26.2
pillow==12.3.0
scikit-image==0.26.0
scipy==1.18.0
tifffile==2026.6.1
Werkzeug==3.1.8
NOTE:
This was obtained after the application was working using:
pip freeze > requirements.txt
script.py
#!/usr/bin/env python3
import re
import sys
import numpy as np
from skimage.transform import estimate_transform
from pathlib import Path
from datetime import datetime
# ------------------------------------------------------------
# Read coordinates from a Zeiss .pos file
# ------------------------------------------------------------
def read_positions(filename):
"""
Returns an (N,3) numpy array with columns X,Y,Z.
"""
with open(filename, encoding="latin1") as f:
text = f.read()
# Ignore whatever unit follows the number (µm, µm, etc.)
pattern = re.compile(
r"X\s*=\s*([-+]?\d+(?:\.\d+)?).*?"
r"Y\s*=\s*([-+]?\d+(?:\.\d+)?).*?"
r"Z\s*=\s*([-+]?\d+(?:\.\d+)?)",
re.DOTALL,
)
positions = np.array(
[[float(x), float(y), float(z)]
for x, y, z in pattern.findall(text)],
dtype=float,
)
return positions
# ------------------------------------------------------------
# Write transformed coordinates back into a template file
# ------------------------------------------------------------
def write_positions(template_file, output_file, positions):
"""
Copy template_file while replacing only X,Y,Z values.
"""
with open(template_file, encoding="latin1") as f:
text = f.read()
values = iter(positions)
pattern = re.compile(
r"(X\s*=\s*)([-+]?\d+(?:\.\d+)?)(.*?)"
r"(Y\s*=\s*)([-+]?\d+(?:\.\d+)?)(.*?)"
r"(Z\s*=\s*)([-+]?\d+(?:\.\d+)?)",
re.DOTALL,
)
def replace(match):
try:
x, y, z = next(values)
except StopIteration:
raise ValueError(
"Template contains more positions than supplied."
)
return (
f"{match.group(1)}{x:.3f}"
f"{match.group(3)}"
f"{match.group(4)}{y:.3f}"
f"{match.group(6)}"
f"{match.group(7)}{z:.3f}"
)
new_text = pattern.sub(replace, text)
try:
next(values)
raise ValueError(
"More coordinates supplied than template contains."
)
except StopIteration:
pass
with open(output_file, "w", encoding="latin1", newline="\r\n") as f:
f.write(new_text)
#with open(output_file, "w", encoding="latin1") as f:
# f.write(new_text)
# ------------------------------------------------------------
# Main
# ------------------------------------------------------------
if len(sys.argv) != 4:
print(
"Usage:\n"
" python script.py old_refs.pos new_refs.pos old_positions.pos"
)
sys.exit(1)
old_refs = read_positions(sys.argv[1])
new_refs = read_positions(sys.argv[2])
old_positions = read_positions(sys.argv[3])
print("Old reference points")
print(old_refs)
print("\nNew reference points")
print(new_refs)
print("\nOld positions")
print(old_positions)
if old_refs.shape != new_refs.shape:
raise ValueError("Reference files contain different numbers of points.")
if old_refs.shape[0] < 2:
raise ValueError("Need at least two reference points.")
# Compute XY transformation only
tform = estimate_transform(
"euclidean",
old_refs[:, :2],
new_refs[:, :2],
)
print("\nEstimated transform")
print("Rotation (deg):", np.degrees(tform.rotation))
print("Translation:", tform.translation)
# Transform XY coordinates
new_positions = old_positions.copy()
new_positions[:, :2] = tform(old_positions[:, :2])
print("\nTransformed positions")
print(new_positions)
# Directory containing old_positions.pos
output_dir = Path(sys.argv[3]).parent
# Timestamp like 02-07_15-42-31
#timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
output_file = output_dir / f"converted.pos"
write_positions(
sys.argv[3],
output_file,
new_positions,
)
print(f"\nSaved {output_file}")
templates folder
<!DOCTYPE html>
<html>
<head>
<title>Zeiss Position Transformer</title>
</head>
<body>
<h1>Zeiss Position Transformer</h1>
<hr>
<form
action="/convert"
method="POST"
enctype="multipart/form-data">
<p>Old reference positions</p>
<input
type="file"
name="old_refs"
required>
<br><br>
<p>New reference positions</p>
<input
type="file"
name="new_refs"
required>
<br><br>
<p>Positions to transform</p>
<input
type="file"
name="positions"
required>
<br><br>
<hr>
<button
type="submit">
Transform
</button>
</form>
</body>
</html>