Committed and Pushed by ButtonClick

This commit is contained in:
ChK
2025-12-22 16:00:50 +01:00
parent 05b35b22c8
commit 058cab3e7a
704 changed files with 5 additions and 15299 deletions

View File

@@ -1,10 +0,0 @@
{ "coordinateSystem":{
"MarkersUsed":"DICT_4X4_250",
"KnownMarkers":
{
"50": [0.0, 0.0, 0.0],
"71": [0.140, 0.0, 0.0],
"101": [0.0, -0.080, 0.0]
}
}
}

View File

@@ -1,10 +0,0 @@
{ "coordinateSystem":{
"MarkersUsed":"DICT_4X4_250",
"KnownMarkers":
{
"50": [0.0, 0.0, 0.0],
"71": [0.140, 0.0, 0.0],
"101": [0.0, -0.080, 0.0]
}
}
}

View File

@@ -1,10 +0,0 @@
{ "coordinateSystem":{
"MarkersUsed":"DICT_4X4_250",
"KnownMarkers":
{
"58": [0.0, 0.0, 0.0],
"65": [0.161, 0.0, 0.0],
"75": [0.0, -0.070, 0.0]
}
}
}

View File

@@ -1,29 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIFCTCCAvGgAwIBAgIUfD0V6IOHq6iL+tCtV3CMQ9w6uqQwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MTExNTA4NDMzOFoXDTI2MTEx
NTA4NDMzOFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF
AAOCAg8AMIICCgKCAgEArK1NqQEZBpXDgfOTNFrIcTf0qmsE8yfpqahytAdD/wL3
CE7YKPs5Bsq77YiPPC1Svc8Q8APETRjb3uPet+xfHtyCZX+cu8Dc45AA8sV1bAco
JDFH5x4oi9fnUBEMoxJewIrR6iGvPLN3OFzvq0QG65LTB7HEozYOeTBx2LNEzYWT
+iIu3Tj/iya4EWqsUKWv4LAdHxNfIyTScjYq98/thy9jbesVq2e62gV+q3Km3bqJ
Q+7NWwXM3sHvSXVU/+yqWxwMxiMO6t8QXFVP71ti6IdOdWlSOds5tTa1X0O6wdje
VKN+JXfI3M+Cq7fMLnOKyrm+olcQ9RHcrNHrN6LYrK/yXwW0XTdt0RDsbFE5VH8x
X887zS1Xj0q4IPXutm/Z5uHAGCchg5BDW+w08fxo3pdCIPVy6CVrpabR5lJ4cNPE
lzOAhCOAZYvFyATtJXOPw0CW5mVUDl2BTjPrijf/2YA0Vh+9j4SvwdPR7W2gXdPL
zsr04LwQmFLIEUKgpPlW28K2gun6H7vDshue+jJ9iuCF3BcC9MZI1hFi1omHaJ8T
ehX00Q5HTOhVZFdxzkGGCUbX7B7umKgbLXbItGU3Cnw2fdgi3+1zZbVSWAm/ixiQ
do/Iaw3CrO9pAgmy813hT95qnBKxmuXBeZ+pf1XlhvOAgJw+uhdWOdjGjyJPjIMC
AwEAAaNTMFEwHQYDVR0OBBYEFH0WYvzhiYjoWDpkmMdx0uk3I/NAMB8GA1UdIwQY
MBaAFH0WYvzhiYjoWDpkmMdx0uk3I/NAMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
hvcNAQELBQADggIBAHC96KCq7u7WR2t6crbg+MjFuvq8lOGvYdxea4pw8aCK4T31
KPSVU/4NzeIGHfjAvmghY8/J47wt15rI9nxGFyP0JBD+waSKo+Ruq1hOCSR1Qf3C
4GgUS64Fyj6uXrusszYsa8q50ZXitAYM6o4Nkd5PqQIouvwoHcAABSWBAdxxzXNO
6t+GGElYaajZ8Zv+pVKLdiRJny0A9PSSe3pPd/loqYecr8nX3Be2i+C6lgPFpYY4
wpswmbGjt7oQgt2UCSr4Tz1tsfeZ94as3HYUDL5W1eIAQKUdtltBRNdC2DT53MS6
9b2lgCDUNLXKtBe6naIpDRb/jWFXzPTUwEDyXJN6ORJFdAkYIBIoQKzGg+l9NEsO
lYyQB4bMoLwZ4a1B5R8PtUtWxtDcHAegdSIK/9fVz1/QcjYYr3/42NGF6Nr7kpGx
2QCj0z84hJbw/QEGBsg/yFvlOLRE83LTIkjoA4hPo3HRHPuy9/frc3bFk99LnX8+
BKWbVb7J2eyCXX3LAcUb9RU8x4UvYqpjqjA6BKpryyEsRyhgZh/sVHvjwoW/ifFG
JAqQoYe+TCAPf7s/rsxjB6Y5NKDesFYqwT4qu02+xCmy6LjZc1lyyrzJJCAZ/bjN
ATomtrbmCgMho7FvIT7D2c+VRUBb2NSO64egUvxgXBYho8wbQOW6xkAuf/Ft
-----END CERTIFICATE-----

View File

@@ -1,52 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCsrU2pARkGlcOB
85M0WshxN/SqawTzJ+mpqHK0B0P/AvcITtgo+zkGyrvtiI88LVK9zxDwA8RNGNve
49637F8e3IJlf5y7wNzjkADyxXVsBygkMUfnHiiL1+dQEQyjEl7AitHqIa88s3c4
XO+rRAbrktMHscSjNg55MHHYs0TNhZP6Ii7dOP+LJrgRaqxQpa/gsB0fE18jJNJy
Nir3z+2HL2Nt6xWrZ7raBX6rcqbduolD7s1bBczewe9JdVT/7KpbHAzGIw7q3xBc
VU/vW2Loh051aVI52zm1NrVfQ7rB2N5Uo34ld8jcz4Krt8wuc4rKub6iVxD1Edys
0es3otisr/JfBbRdN23REOxsUTlUfzFfzzvNLVePSrgg9e62b9nm4cAYJyGDkENb
7DTx/Gjel0Ig9XLoJWulptHmUnhw08SXM4CEI4Bli8XIBO0lc4/DQJbmZVQOXYFO
M+uKN//ZgDRWH72PhK/B09HtbaBd08vOyvTgvBCYUsgRQqCk+VbbwraC6fofu8Oy
G576Mn2K4IXcFwL0xkjWEWLWiYdonxN6FfTRDkdM6FVkV3HOQYYJRtfsHu6YqBst
dsi0ZTcKfDZ92CLf7XNltVJYCb+LGJB2j8hrDcKs72kCCbLzXeFP3mqcErGa5cF5
n6l/VeWG84CAnD66F1Y52MaPIk+MgwIDAQABAoICACrjqMRvh13BWRwv+cIpQlwM
v/KMPmB+62S+eC8LFvKCNAWWP85+B74OIPtwzdLulvyaL+TNqrZTlFkaVDlutnmd
362CMtXXo4XKQNIMBYxdgrTqoKdhMET7zXJvqanfaDV+xYDX+QSkttoDC2yIqwdc
IppopyS6yoGDbOOcM1yw7v5T+zvral2PsmxxCHfHj5XJaJJNZ3X9MWA44BdJSAAp
3xJwL6OxUhHRB5NiGNw99tYuvEb9e9NgbCTcxc4DZJTYtLuJ/ayM1y136zDSjBCh
evViwY+pSf4eppIQ6pQ3X5G6QhzgUb22tQgd0NNA05pi5FzCL24Pj7ZFNZ1OD278
mjozzNirn8plbgQ+OGCs5NHprjdwQNW+Q6sE34Yh8FBlF0JfNmk3uaKTetIwSsJF
laMHXR5OtDoX2cWqzjHijTDiUC5mk5ph8/lhOdY3Z1Ax1w52qKEFNOIC9gZrTgFE
2oyp/Nv0tdNUBuhMKOLvKF0xwCDP/2iWeYow0j7iclbpotK5oRKWjbIQqstKAAs7
vRW6n5CTPUIyzE1oLglp7DbYy++xAUpLBjPYzZn5aKb680FSmJtbBF3CZDupfKus
b3bUWeVmY6TIakSRYMNXK9BWz1fXQinCoJx/ASv0yxpMsrEr/9NFI2f+gkWlD4hu
HX2DwIzGcpghYnUvngXxAoIBAQDTn5Ci652mxBEBkKgfX7QXp9oBdebmisJWeYca
qxVEiXfqoyfEKW+59Y+IZIANKA3e5R7HJofshKKrXJCLRTi7wSm+4oo4Oo/6TDDa
5Qmsxx9WkTJkBm0E/9nDI3e8q53LwiLOvXel/WZAYWXrIKsQJMgnDjFqdes5efG9
k8jnZ8tf7TtaL7+B+ku5fLagWaLUltg3WS9GjdrS7ZMhkZDJ1ZqqVEQQYYgg756r
ksIim7jQL2dcW8wayB0g9SV7koIoIy/06lp7RX9HrMfhIH5Xorek44ZH2nCDPwtk
s2prjYIzneCt2+fibki/rj2riPlBXTTkf3z2SNe2tBlpx3etAoIBAQDQ4wIAKTVy
BSddk6oVqHNf3tfjub2pp28UXij+K0TMeZHDIDf3oy2AQ+LEatsA0h8g2m93TLIR
KxCBi/Us5DJzrvPNHEhB/XHWEcXinPSI0+guehOIqXKz6TtzA5+XuZE+r+NqhEL/
2iVSK3va7WweeD/CC1G+HYN0zSdAaW6RzfQcDS6UmykoEu2wyRgO5TNBXj45eVh/
/BFKv527oqetpchxhzEJ1ZzT+J1lJBtkKoE5AXb8Wsf3O5ZYwG8F8hQfLybzeIh2
CBskuJO37xZxPSRgcNn5nd0rPPGWKEiX4cRsSTob8Pse5ifBNzxkS8v6s8yIIBt1
S26WyWp1lFrvAoIBABoF5ihSpvlJ5Pl3S2VIRIIgLuu9Dt7Ms2ck3JtH7H6YFPny
hEJYAhgw/Sx9h02W3lXJgQZmU5KfIM3HvTKTGY3lC/ggLXUKpofV9LAGODFZ7x2b
D0JDlAZoW+PmKaQ2ylmzDsqze9IangdOstS+GKsMitxan3MC+yD/QN3aHXtvRRAP
wRuvAXK/T66IioCfZSmVPxNXUTvw17bWZiBboR1gufs2D4SgKbg7HxzkGCFfWtOm
8KPn2ep1LzfNTYWrl3vOD+ijJOtBuYwb0Bx7/W1TYhfRrsKJNwq8pu8ELRL6vMcS
I/3dK9+pRiLkD0tXtab3CjkLAFfcz0H1VaavU6UCggEABty+TxULfXBv56IXP1jm
WWrvurp6YZ1vh8LEI/116CXCRR/E1uzUbNdOFtfP1AoTHbgvW0L4wpmglDDt4Air
I6PGvKFGOmzCFZ9F2fkAC5KymPxHsgCnFQP/gPrIfmqJO/75QKGRtegLu9RT4FBW
cfXPWmeWyuEbVXX76SDNkhqq/1Trh9RFGNzuVBV3Jd4fvfEDqE21O5cjVkpPOz/P
tGOy3w/q64DKAyiyuwThpXvD/QRwTUAKO7QIb2f6/b4DLTcWV39JNF91zNIHgE5p
dVTl4gkzEAFAp8/7u8wc/mhbVJdfQlW7WjuDaNSQtlbLH7RSbtJnNIZC3s9FIRG7
nwKCAQBEJnM0+kFJurE8wRWcHN/oHHyQCrMuFptV+9o1+Ncy3B5+kdo2Pqy0AXDY
+OZucTdkVqIxb0goM1FluP0os3oVq9w5wxZH2tksIEehWPPj3BaWkMVFQMy8vV+O
tn4xeOPK4V89voj8BuLsn2s5t6tEa9vvKuiOTPrnTalfoBYKxYANrOo24MLrCzlS
H++cDZD7ug/cBCSNENQsZDKNMekPRJFd3SYAe8VfM8UCfRMZbX9jQZn7dsSdLf+W
KX28dhZ8WnSBpJE4IVBzAPfiya9wN5+kKxh/AxD3cq3O6UXNA9FTbTxrDTYvHgAM
hwXVuAeFYYyuRFN8xJ9SkIPdpr1Y
-----END PRIVATE KEY-----

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +0,0 @@
{
"name": "video-streamer",
"version": "1.1.0",
"description": "HTTPS + WSS dual-camera MJPEG streamer (modular)",
"main": "server.js",
"type": "commonjs",
"scripts": {
"start": "node server.js",
"dev": "PORT=8443 nodemon server.js"
},
"dependencies": {
"compression": "^1.7.4",
"express": "^4.21.1",
"helmet": "^7.1.0",
"ws": "^8.18.0"
},
"devDependencies": {
"nodemon": "^3.1.7"
}
}

View File

@@ -1,71 +0,0 @@
const WebSocket = require("ws");
/**
* Forwards WebSocket messages between browser clients (/robot)
* and a target WebSocket server (behind a firewall).
*
* @param {WebSocket.Server} wssInput - Local WebSocket server for browser clients
* @param {string} targetUrl - URL of target WebSocket server, e.g. "wss://internal.local:8080"
*/
function setupCommandForwarding(wssInput, targetUrl) {
let targetSocket;
const clients = new Set();
function connectTarget() {
console.log(`🔌 Connecting to target server: ${targetUrl}`);
targetSocket = new WebSocket(targetUrl);
targetSocket.on("open", () => {
console.log("✅ Connected to target server");
});
targetSocket.on("message", (msg) => {
const data = msg.toString();
console.log("⬅️ Message from target:", data);
// Broadcast to all connected browsers
for (const client of clients) {
if (client.readyState === WebSocket.OPEN) {
client.send(data);
}
}
});
targetSocket.on("close", () => {
console.warn("⚠️ Target connection closed. Reconnecting in 5s...");
setTimeout(connectTarget, 5000);
});
targetSocket.on("error", (err) => {
console.error("❌ Target connection error:", err.message);
});
}
connectTarget();
// When a browser connects to /robot
wssInput.on("connection", (ws, req) => {
console.log("🤖 Browser connected:", req.socket.remoteAddress);
clients.add(ws);
ws.on("message", (msg) => {
const data = msg.toString();
console.log("➡️ From browser → target:", data);
if (targetSocket?.readyState === WebSocket.OPEN) {
targetSocket.send(data);
} else {
console.warn("⚠️ Target not connected. Message dropped.");
}
});
ws.on("close", () => {
clients.delete(ws);
console.log("🔌 Browser disconnected");
});
ws.on("error", (err) => {
console.error("❌ Browser socket error:", err.message);
});
});
}
module.exports = { setupCommandForwarding };

View File

@@ -1,49 +0,0 @@
// programs/input.js
'use strict';
const fs = require('fs');
const path = require('path');
function byIdCaptureCandidates() {
const dir = '/dev/v4l/by-id';
try {
if (!fs.existsSync(dir)) return [];
return fs.readdirSync(dir)
.filter(n => n.endsWith('-index0'))
.map(n => fs.realpathSync(path.join(dir, n)));
} catch {
return [];
}
}
function naiveVideoNodes() {
try {
return fs.readdirSync('/dev')
.filter(n => /^video\d+$/.test(n))
.map(n => path.join('/dev', n))
.sort((a, b) => Number(a.replace(/\D/g, '')) - Number(b.replace(/\D/g, '')));
} catch {
return ['/dev/video0', '/dev/video2'];
}
}
function pickDevices(env = process.env) {
const DEV0 = env.DEV0 || null;
const DEV1 = env.DEV1 || null;
if (DEV0 && DEV1) return [DEV0, DEV1];
const byId = byIdCaptureCandidates();
if (DEV0 || DEV1) {
const pool = byId.length ? byId : naiveVideoNodes();
const d0 = DEV0 || pool[0];
const d1 = DEV1 || pool.find(d => d !== d0) || pool[1];
return [d0, d1];
}
if (byId.length >= 2) return [byId[0], byId[1]];
const naive = naiveVideoNodes();
return [naive[0], naive[1]];
}
module.exports = { pickDevices };

View File

@@ -1,165 +0,0 @@
// programs/log.js
'use strict';
const fs = require('fs');
const path = require('path');
// --- configuration ---
const LOG_DIR = path.join(__dirname, '..', 'logs');
fs.mkdirSync(LOG_DIR, { recursive: true });
function getLogFilePath(d = new Date()) {
const yyyy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
return path.join(LOG_DIR, `${yyyy}_${mm}_${dd}.txt`);
}
function write(obj) {
const line = JSON.stringify(obj) + '\n';
fs.appendFile(getLogFilePath(), line, (err) => {
if (err) console.error('[log] write error:', err);
});
}
// --- common extractors ---
function commonFromReq(req) {
try {
const xff = req?.headers?.['x-forwarded-for'];
const xRealIp = req?.headers?.['x-real-ip'];
const ipFromXff = xff ? xff.split(',')[0].trim() : null;
const ip =
ipFromXff ||
xRealIp ||
req?.ip ||
req?.socket?.remoteAddress ||
null;
const tls =
req?.socket?.encrypted
? {
protocol:
typeof req.socket.getProtocol === 'function'
? req.socket.getProtocol()
: null,
cipher:
typeof req.socket.getCipher === 'function'
? (req.socket.getCipher() || {}).name
: null,
}
: null;
// MAC is not available across routed networks
const mac = null;
return {
ip,
ips: Array.isArray(req?.ips) ? req.ips : [],
xff: xff || null,
remoteAddress: req?.socket?.remoteAddress || null,
remoteFamily: req?.socket?.remoteFamily || null,
userAgent: req?.headers?.['user-agent'] || null,
acceptLanguage: req?.headers?.['accept-language'] || null,
secChUa: req?.headers?.['sec-ch-ua'] || null,
secChUaPlatform: req?.headers?.['sec-ch-ua-platform'] || null,
secChUaMobile: req?.headers?.['sec-ch-ua-mobile'] || null,
referer: req?.headers?.['referer'] || null,
tls,
mac,
};
} catch {
return {};
}
}
function commonFromSocket(socket) {
return {
remoteAddress: socket?.remoteAddress || null,
remoteFamily: socket?.remoteFamily || null,
};
}
// --- specific log functions ---
function logHttpRequest(req) {
write({
ts: new Date().toISOString(),
type: 'http',
method: req?.method || null,
url: (req?.originalUrl ?? req?.url) || null,
...commonFromReq(req),
});
}
function logTcpConnection(socket) {
write({
ts: new Date().toISOString(),
type: 'tcp',
...commonFromSocket(socket),
});
}
function logHttpUpgrade(req) {
write({
ts: new Date().toISOString(),
type: 'http-upgrade',
url: req?.url || null,
...commonFromReq(req),
});
}
function logWssConnected(req) {
write({
ts: new Date().toISOString(),
type: 'wss',
url: req?.url || null,
...commonFromReq(req),
});
}
function logWssClosed(req, code, reason) {
write({
ts: new Date().toISOString(),
type: 'wss-close',
url: req?.url || null,
code: typeof code === 'number' ? code : null,
reason: reason ? reason.toString() : null,
...commonFromReq(req),
});
}
function logSnapshot(python, response){
write({
ts: new Date().toISOString(),
type: 'snapshot',
command: python.toString(),
wsResponse: response.toString()
})
}
// --- generic hooks you requested ---
function connected(context = {}) {
write({
ts: new Date().toISOString(),
type: 'connected',
...context,
});
}
function connectionLost(context = {}) {
write({
ts: new Date().toISOString(),
type: 'connection-lost',
...context,
});
}
module.exports = {
logHttpRequest,
logTcpConnection,
logHttpUpgrade,
logWssConnected,
logSnapshot,
logWssClosed,
connected,
connectionLost,
};

View File

@@ -1,369 +0,0 @@
#!/usr/bin/env python3
"""
ArUco detection with multi-marker machine-frame fit + camera pose output (OpenCV >= 4.8).
- Reads: webCam_1.jpg
- Detects DICT_4X4_250 markers (ids expected: 0, 5, 10, 15)
- Uses multiple reference markers with known machine coordinates to fit camera->machine transform
- Reports positions/orientations of all markers **and the camera** in machine coordinates
- Draws detected markers, per-marker axes, and the machine axes
- Saves: webCam_1a.jpg (annotated) and marker_poses_machine.csv (poses incl. camera)
Usage:
python3 readCamPos.py -i snapshot_video1_1764493534200.jpg -npz camera_intrinsics_v0.npz -setting settings.json
"""
import faulthandler
faulthandler.enable()
import argparse
import os
import sys
import csv
import json
import time
from typing import Tuple, Dict, List
import numpy as np
import cv2
# ----------------------- Configuration Defaults -----------------------
IMAGE_PATH = "default.jpg"
OUTPUT_IMAGE_PATH = "default.jpg"
OUTPUT_CSV_PATH = "default.csv"
OUTPUT_JSON_PATH = "default.json"
# Marker side length in meters (25 mm)
MARKER_LENGTH_M = 0.025
# Axis lengths for visualization (in meters)
AXIS_LENGTH_M = 0.05 # per-marker axis
MACHINE_AXIS_X_M = 0.200 # 200 mm along +X
MACHINE_AXIS_Y_M = -0.100 # -100 mm along Y (towards camera per description)
MACHINE_AXIS_Z_M = 0.100 # +Z visualized as 100 mm
# Known machine coordinates for reference markers (meters)
cam_anchor_pts = {}
EXPECTED_IDS = {50, 71, 101}
# ----------------------- Utilities -----------------------
def load_intrinsics_npz(npz_path: str) -> Tuple[np.ndarray, np.ndarray]:
if os.path.exists(npz_path):
print("NPZ from File:", npz_path)
data = np.load(npz_path)
for k in ('camera_matrix', 'mtx', 'K'):
if k in data:
camera_matrix = data[k].astype(np.float32)
break
else:
raise KeyError("Camera matrix not found.")
for k in ('dist_coeffs', 'dist', 'D'):
if k in data:
dist = data[k].astype(np.float32).reshape(-1, 1)
break
else:
dist = np.zeros((5, 1), dtype=np.float32)
return camera_matrix, dist
camera_matrix = np.array([[1400, 0, 640],
[0, 1400, 360],
[0, 0, 1]], dtype=np.float32)
dist_coeffs = np.zeros((5, 1), dtype=np.float32)
print("[WARN] Using default approximate intrinsics.")
return camera_matrix, dist_coeffs
def rvec_to_R(rvec: np.ndarray) -> np.ndarray:
R, _ = cv2.Rodrigues(rvec)
return R
def R_to_euler_zyx(R: np.ndarray) -> Tuple[float, float, float]:
yaw = float(np.degrees(np.arctan2(R[1,0], R[0,0])))
sp = np.sqrt(R[2,1]**2 + R[2,2]**2)
pitch = float(np.degrees(np.arctan2(-R[2,0], sp)))
roll = float(np.degrees(np.arctan2(R[2,1], R[2,2])))
return roll, pitch, yaw
def corners_to_image_points(corners: np.ndarray) -> np.ndarray:
return corners.reshape(4, 2).astype(np.float32)
def get_detector():
dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_250)
try:
params = cv2.aruco.DetectorParameters()
except Exception:
params = cv2.aruco.DetectorParameters_create()
try:
detector = cv2.aruco.ArucoDetector(dictionary, params)
return detector, None
except Exception:
return None, (dictionary, params)
def detect_markers(image: np.ndarray, detector_tuple):
detector, fallback = detector_tuple
print(detector)
if detector is not None:
corners, ids, rejected = detector.detectMarkers(image)
else:
dictionary, params = fallback
corners, ids, rejected = cv2.aruco.detectMarkers(image, dictionary, parameters=params)
return corners, ids, rejected
def rigid_transform_no_scale(A: np.ndarray, B: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
assert A.shape == B.shape and A.shape[1] == 3, "A and B must be Nx3"
N = A.shape[0]
if N < 2:
raise ValueError("Need at least 2 points; 3+ recommended.")
centroid_A = A.mean(axis=0)
centroid_B = B.mean(axis=0)
AA = A - centroid_A
BB = B - centroid_B
H = AA.T @ BB
U, S, Vt = np.linalg.svd(H)
R = Vt.T @ U.T
if np.linalg.det(R) < 0:
Vt[-1, :] *= -1
R = Vt.T @ U.T
t = centroid_B - R @ centroid_A
return R.astype(np.float32), t.astype(np.float32)
def readSettings(fileSetting):
global cam_anchor_pts
print("Read Settings")
if(fileSetting == None):
cam_anchor_pts = {
50: np.array([0.0, 0.0, 0.0], dtype=np.float32),
71: np.array([0.140, 0.0, 0.0], dtype=np.float32),
101: np.array([-0.0, -0.080, 0.0], dtype=np.float32),
#15: np.array([20,20,20]) # add if known
}
return
with open(fileSetting, 'r') as f:
settings = json.load(f)
for marker_id, coords in settings['coordinateSystem']['KnownMarkers'].items():
cam_anchor_pts[int(marker_id)] = np.array(coords, dtype=np.float32)
#KNOWN_MACHINE_POS = {int(k): np.array(v, dtype=np.float32) for k, v in settings.items()}
# ----------------------- Main -----------------------
def main():
parser = argparse.ArgumentParser(description="Detect ArUco markers in two images and compute camera poses in machine coordinates.")
parser.add_argument('-i', '--images', action='append', required=False,
help="Path to image. Provide this option twice: once per camera (e.g., -i2 cam1.jpg -i2 cam2.jpg)")
parser.add_argument('-npz', '--npz', action='append', required=False, default=['camera_intrinsics_v1.npz'])
parser.add_argument('--cam-calib', action='append', required=False,
help="Paths to calibration YAMLs for camera 1 and camera 2 (e.g., cam1.npz cam2.npz)")
parser.add_argument('--marker-size-mm', type=float, default=25,
help="Marker side length in millimeters (e.g., 50)")
parser.add_argument('--dict', default='DICT_4X4_250',
help="ArUco dictionary name (default: DICT_4X4_250)")
parser.add_argument('-settings', type=str, default=None,
help="Json File with Machine Settings")
args = parser.parse_args()
print("ABC 0")
readSettings(args.settings)
print("ABC 0")
if(args.images is None):
image = cv2.imread(IMAGE_PATH)
else:
image = cv2.imread(args.images[0])
OUTPUT_IMAGE_PATH = args.images[0].replace(".jpg","r.jpg").replace(".PNG","r.PNG")
OUTPUT_CSV_PATH = args.images[0].replace(".jpg",".csv").replace(".PNG",".csv")
OUTPUT_JSON_PATH = args.images[0].replace(".jpg",".json").replace(".PNG",".json")
if image is None:
print(f"[ERROR] Cannot read image '{IMAGE_PATH}'.")
sys.exit(1)
print("ABC 1")
camera_matrix, dist_coeffs = load_intrinsics_npz(args.npz[0])
print("ABC 1a")
detector_tuple = get_detector()
print("ABC 1b")
corners_list, ids, rejected = detect_markers(image, detector_tuple)
print("ABC 2")
if ids is None or len(ids) == 0:
print("[ERROR] No markers detected.")
sys.exit(1)
draw_img = image.copy()
cv2.aruco.drawDetectedMarkers(draw_img, corners_list, ids)
half = MARKER_LENGTH_M / 2.0
obj_points = np.array([
[-half, half, 0.0],
[ half, half, 0.0],
[ half, -half, 0.0],
[-half, -half, 0.0],
], dtype=np.float32)
poses_cam: Dict[int, Tuple[np.ndarray, np.ndarray]] = {}
centers_cam: Dict[int, np.ndarray] = {}
for i, marker_id in enumerate(ids.flatten()):
img_pts = corners_to_image_points(corners_list[i])
success, rvec, tvec = cv2.solvePnP(obj_points, img_pts, camera_matrix, dist_coeffs, flags=cv2.SOLVEPNP_IPPE_SQUARE)
if not success:
success, rvec, tvec = cv2.solvePnP(obj_points, img_pts, camera_matrix, dist_coeffs)
if success:
rvec = rvec.reshape(3,1)
tvec = tvec.reshape(3,1)
poses_cam[int(marker_id)] = (rvec, tvec)
centers_cam[int(marker_id)] = tvec.flatten()
try:
cv2.drawFrameAxes(draw_img, camera_matrix, dist_coeffs, rvec, tvec, AXIS_LENGTH_M)
except Exception:
pass
else:
print(f"[WARN] solvePnP failed for marker {marker_id}")
common_ids: List[int] = [mid for mid in cam_anchor_pts.keys() if mid in centers_cam]
if len(common_ids) < 2:
print(f"[ERROR] Need at least 2 reference markers; found {len(common_ids)}: {common_ids}")
sys.exit(1)
if len(common_ids) < 3:
print(f"[WARN] Only {len(common_ids)} references ({common_ids}). Fit may be less stable; 3+ recommended.")
A = np.stack([centers_cam[mid] for mid in common_ids], axis=0)
B = np.stack([cam_anchor_pts[mid] for mid in common_ids], axis=0)
R_cam_to_machine, t_cam_to_machine = rigid_transform_no_scale(A, B)
residuals_mm = []
for i, mid in enumerate(common_ids):
pred = R_cam_to_machine @ A[i] + t_cam_to_machine
err = np.linalg.norm(pred - B[i]) * 1000.0
residuals_mm.append(err)
rms = float(np.sqrt(np.mean(np.square(residuals_mm)))) if residuals_mm else 0.0
print("\nReference fit residuals (mm) per marker:")
for mid, e in zip(common_ids, residuals_mm):
print(f" ID {mid}: {e:.2f} mm")
print(f"RMS residual: {rms:.2f} mm")
# Camera pose in machine coordinates:
# Camera origin (0,0,0 in camera) maps to t_cam_to_machine
cam_pos_machine = t_cam_to_machine
cam_R_machine = R_cam_to_machine # camera basis expressed in machine frame
cam_roll, cam_pitch, cam_yaw = R_to_euler_zyx(cam_R_machine)
rows = [("id", "x_mm", "y_mm", "z_mm", "roll_deg", "pitch_deg", "yaw_deg")]
marker_list = []
print("\nMarker Positions and Orientations in Machine Coordinates:")
print(f"{'ID':>8} {'X(mm)':>10} {'Y(mm)':>10} {'Z(mm)':>10} {'Roll':>10} {'Pitch':>10} {'Yaw':>10}")
# Add camera first
cx, cy, cz = (cam_pos_machine * 1000.0).tolist()
print(f"{'camera':>8} {cx:10.2f} {cy:10.2f} {cz:10.2f} {cam_roll:10.2f} {cam_pitch:10.2f} {cam_yaw:10.2f}")
rows.append(("camera", f"{cx:.3f}", f"{cy:.3f}", f"{cz:.3f}", f"{cam_roll:.3f}", f"{cam_pitch:.3f}", f"{cam_yaw:.3f}"))
camera_pose = {
"id": "camera",
"position_mm": [float(x) for x in cam_pos_machine * 1000.0],
"orientation_deg": {"roll": cam_roll, "pitch": cam_pitch, "yaw": cam_yaw}
}
# Then markers
for marker_id in sorted(poses_cam.keys()):
rvec, tvec = poses_cam[marker_id]
R_marker_cam = rvec_to_R(rvec)
pos_machine = R_cam_to_machine @ tvec.flatten() + t_cam_to_machine
R_marker_machine = R_cam_to_machine @ R_marker_cam
roll_deg, pitch_deg, yaw_deg = R_to_euler_zyx(R_marker_machine)
x_mm, y_mm, z_mm = (pos_machine * 1000.0).tolist()
print(f"{marker_id:8d} {x_mm:10.2f} {y_mm:10.2f} {z_mm:10.2f} {roll_deg:10.2f} {pitch_deg:10.2f} {yaw_deg:10.2f}")
rows.append((marker_id, f"{x_mm:.3f}", f"{y_mm:.3f}", f"{z_mm:.3f}", f"{roll_deg:.3f}", f"{pitch_deg:.3f}", f"{yaw_deg:.3f}"))
marker_list.append({"id": marker_id, "position_mm": [x_mm, y_mm, z_mm], "orientation_deg": {"roll": roll_deg, "pitch": pitch_deg, "yaw": yaw_deg}})
# Save CSV
try:
with open(OUTPUT_CSV_PATH, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerows(rows)
print(f"\n[INFO] Saved CSV poses to '{OUTPUT_CSV_PATH}'.")
except Exception as e:
print(f"[WARN] Could not save CSV: {e}")
# Save JSON
json_data = {
"metadata": {
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"reference_markers": common_ids,
"rms_residual_mm": rms,
"description": "Multi-marker machine frame fit with camera pose"
},
"camera": camera_pose,
"markers": marker_list
}
with open(OUTPUT_JSON_PATH, 'w', encoding='utf-8') as f:
json.dump(json_data, f, indent=4)
# Warn about expected IDs
detected_ids = set(poses_cam.keys())
missing = EXPECTED_IDS - detected_ids
if missing:
print(f"[WARN] Expected markers not detected: {sorted(missing)}")
# Draw machine axes using global transform (machine->camera)
R_machine_to_cam = R_cam_to_machine.T
t_machine_to_cam = - R_machine_to_cam @ t_cam_to_machine
try:
machine_axes = np.float32([
[0.0, 0.0, 0.0],
[MACHINE_AXIS_X_M, 0.0, 0.0],
[0.0, MACHINE_AXIS_Y_M, 0.0],
[0.0, 0.0, MACHINE_AXIS_Z_M],
])
rvec_global, _ = cv2.Rodrigues(R_machine_to_cam)
imgpts, _ = cv2.projectPoints(machine_axes, rvec_global, t_machine_to_cam, camera_matrix, dist_coeffs)
origin = tuple(np.round(imgpts[0].ravel()).astype(int))
x_end = tuple(np.round(imgpts[1].ravel()).astype(int))
y_end = tuple(np.round(imgpts[2].ravel()).astype(int))
z_end = tuple(np.round(imgpts[3].ravel()).astype(int))
cv2.line(draw_img, origin, x_end, (0, 0, 255), 3)
cv2.line(draw_img, origin, y_end, (0, 255, 0), 3)
cv2.line(draw_img, origin, z_end, (255, 0, 0), 3)
cv2.putText(draw_img, "X (200 mm)", x_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,0,255), 2)
cv2.putText(draw_img, "Y (-100 mm)", y_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,255,0), 2)
cv2.putText(draw_img, "+Z (100 mm)", z_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,0,0), 2)
except Exception as e:
print(f"[WARN] Failed to draw machine axes: {e}")
ok = cv2.imwrite(OUTPUT_IMAGE_PATH, draw_img)
if ok:
print(f"[INFO] Annotated image saved as '{OUTPUT_IMAGE_PATH}'.")
else:
print(f"[ERROR] Failed to save annotated image '{OUTPUT_IMAGE_PATH}'.")
if __name__ == '__main__':
main()

View File

@@ -1,668 +0,0 @@
#!/usr/bin/env python3
"""
readCamPositionTwo.py
Two-camera ArUco detection with joint optimization of both camera extrinsics
against known machine-frame reference markers, plus triangulation of unknown
marker positions. Outputs camera pose and marker poses in machine coordinates,
with CSV and JSON similar to the single-camera script.
Dependencies: numpy, opencv-python (cv2)
Optional but NOT required: SciPy (we implement a simple LevenbergMarquardt).
Usage example:
python3 readTwoImages.py -i snapshot_video0_1764531874081.jpg -i snapshot_video1_1764531874081.jpg -npz callibration_cam0.npz -npz callibration_cam1.npz -settings settings.json
python3 readTwoImages.py -i snapshot_video0_1764524369655.jpg -i snapshot_video1_1764524369655.jpg -npz callibration_cam0.npz -npz callibration_cam1.npz -settings settings.json
python3 readTwoImages.py -i snapshot_video0_1765009029764.jpg -i snapshot_video1_1765009029764.jpg -npz callibration_cam0.npz -npz callibration_cam1.npz -settings settings.json
Settings JSON is expected to contain:
{
"coordinateSystem": {
"KnownMarkers": {
"50": [0.0, 0.0, 0.0],
"71": [0.140, 0.0, 0.0],
"101": [0.0, -0.080, 0.0]
}
}
}
Author: M365 Copilot (generated)
"""
import argparse
import os
import sys
import json
import time
from typing import Dict, Tuple, List
import numpy as np
import cv2
# ---------------- Configuration defaults ----------------
AXIS_LENGTH_M = 0.05
# ---------------- Utilities ----------------
def load_intrinsics_npz(npz_path: str) -> Tuple[np.ndarray, np.ndarray]:
print("NPZ reading of file:", npz_path)
if os.path.exists(npz_path):
data = np.load(npz_path)
for k in ('camera_matrix', 'mtx', 'K'):
if k in data:
camera_matrix = data[k].astype(np.float32)
break
else:
raise KeyError("Camera matrix not found in NPZ.")
for k in ('dist_coeffs', 'dist', 'D'):
if k in data:
dist = data[k].astype(np.float32).reshape(-1,1)
break
else:
dist = np.zeros((5,1), dtype=np.float32)
print("NPZ loaded:", npz_path)
return camera_matrix, dist
# Fallback default intrinsics
camera_matrix = np.array([[1400, 0, 640],
[0, 1400, 360],
[0, 0, 1]], dtype=np.float32)
dist_coeffs = np.zeros((5,1), dtype=np.float32)
print("[WARN] Using default approximate intrinsics.")
return camera_matrix, dist_coeffs
def get_aruco_detector(dict_name: str):
mapping = {
'DICT_4X4_250': cv2.aruco.DICT_4X4_250,
'DICT_5X5_100': cv2.aruco.DICT_5X5_100,
'DICT_6X6_250': cv2.aruco.DICT_6X6_250,
'DICT_ARUCO_ORIGINAL': cv2.aruco.DICT_ARUCO_ORIGINAL,
}
if dict_name not in mapping:
dict_id = cv2.aruco.DICT_4X4_250
else:
dict_id = mapping[dict_name]
dictionary = cv2.aruco.getPredefinedDictionary(dict_id)
try:
params = cv2.aruco.DetectorParameters()
except Exception:
params = cv2.aruco.DetectorParameters_create()
try:
detector = cv2.aruco.ArucoDetector(dictionary, params)
return detector, None
except Exception:
return None, (dictionary, params)
def detect_markers(image: np.ndarray, detector_tuple):
detector, fallback = detector_tuple
if detector is not None:
corners, ids, rejected = detector.detectMarkers(image)
else:
dictionary, params = fallback
corners, ids, rejected = cv2.aruco.detectMarkers(image, dictionary, parameters=params)
return corners, ids, rejected
def corners_to_image_points(corners: np.ndarray) -> np.ndarray:
return corners.reshape(4,2).astype(np.float32)
def marker_center_from_corners(corners: np.ndarray) -> np.ndarray:
pts = corners.reshape(4,2)
return pts.mean(axis=0).astype(np.float32)
def rvec_to_R(rvec: np.ndarray) -> np.ndarray:
R, _ = cv2.Rodrigues(rvec)
return R
def rigid_transform_no_scale(A: np.ndarray, B: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
"""Find R,t s.t. B ≈ R A + t. A,B: Nx3."""
assert A.shape == B.shape and A.shape[1] == 3, "A and B must be Nx3"
N = A.shape[0]
if N < 2:
raise ValueError("Need at least 2 points; 3+ recommended.")
centroid_A = A.mean(axis=0)
centroid_B = B.mean(axis=0)
AA = A - centroid_A
BB = B - centroid_B
H = AA.T @ BB
U, S, Vt = np.linalg.svd(H)
R = Vt.T @ U.T
if np.linalg.det(R) < 0:
Vt[-1, :] *= -1
R = Vt.T @ U.T
t = centroid_B - R @ centroid_A
return R.astype(np.float32), t.astype(np.float32)
def undistort_to_normalized(points_px: np.ndarray, K: np.ndarray, D: np.ndarray) -> np.ndarray:
"""points_px: Nx2 pixel. Returns Nx2 normalized coords (x,y) where projection is x=Xp/Z, y=Yp/Z.
cv2.undistortPoints with P=None yields normalized coordinates.
"""
pts = points_px.reshape(-1,1,2).astype(np.float32)
und = cv2.undistortPoints(pts, K, D, P=None) # returns Nx1x2
return und.reshape(-1,2)
# ---------------- Joint optimization (LM, numerical Jacobian) ----------------
def pack_params(omega1, t1, omega2, t2) -> np.ndarray:
return np.hstack([omega1, t1, omega2, t2]).astype(np.float64)
def unpack_params(theta: np.ndarray):
omega1 = theta[0:3]
t1 = theta[3:6]
omega2 = theta[6:9]
t2 = theta[9:12]
return omega1, t1, omega2, t2
def residuals_centers_normalized(theta: np.ndarray,
X_mach: np.ndarray,
obs1_norm: np.ndarray,
obs2_norm: np.ndarray) -> np.ndarray:
"""
Compute residuals in normalized coordinates (no intrinsics, no distortion).
For camera j: X_cam = R_j X_mach + t_j; proj: (x/z, y/z).
Returns stacked residuals [r1; r2] shape (4N,), where N = number of references.
"""
omega1, t1, omega2, t2 = unpack_params(theta)
R1 = cv2.Rodrigues(omega1)[0]
R2 = cv2.Rodrigues(omega2)[0]
# Camera 1 projections
X_cam1 = (R1 @ X_mach.T + t1.reshape(3,1)).T # Nx3
uv1 = X_cam1[:, :2] / X_cam1[:, 2:3]
r1 = (obs1_norm - uv1).reshape(-1)
# Camera 2 projections
X_cam2 = (R2 @ X_mach.T + t2.reshape(3,1)).T
uv2 = X_cam2[:, :2] / X_cam2[:, 2:3]
r2 = (obs2_norm - uv2).reshape(-1)
return np.hstack([r1, r2])
def numerical_jacobian(f, theta: np.ndarray, eps: float, *args) -> np.ndarray:
"""Finite-difference Jacobian: forward differences."""
r0 = f(theta, *args)
m = r0.size
n = theta.size
J = np.zeros((m, n), dtype=np.float64)
for k in range(n):
th = theta.copy()
th[k] += eps
rk = f(th, *args)
J[:, k] = (rk - r0) / eps
return J, r0
def lm_solve(theta0: np.ndarray,
X_mach: np.ndarray,
obs1_norm: np.ndarray,
obs2_norm: np.ndarray,
max_iter: int = 50,
eps_jac: float = 1e-6,
lambda_init: float = 1e-3) -> Tuple[np.ndarray, Dict]:
"""Simple LevenbergMarquardt on center normalized residuals."""
lam = lambda_init
theta = theta0.copy()
history = {"iters": [], "rms": []}
for it in range(max_iter):
J, r = numerical_jacobian(residuals_centers_normalized, theta, eps_jac,
X_mach, obs1_norm, obs2_norm)
rms = float(np.sqrt(np.mean(r*r))) if r.size else 0.0
history["iters"].append(it)
history["rms"].append(rms)
# Normal equations with damping
JTJ = J.T @ J
g = J.T @ r
H = JTJ + lam * np.eye(JTJ.shape[0])
try:
delta = -np.linalg.solve(H, g)
except np.linalg.LinAlgError:
# Fallback to least squares
delta, *_ = np.linalg.lstsq(H, -g, rcond=None)
theta_trial = theta + delta
r_trial = residuals_centers_normalized(theta_trial, X_mach, obs1_norm, obs2_norm)
rms_trial = float(np.sqrt(np.mean(r_trial*r_trial)))
if rms_trial < rms: # improve
theta = theta_trial
lam *= 0.5
else:
lam *= 2.0
# Convergence criteria
if np.linalg.norm(delta) < 1e-9 or abs(rms - rms_trial) < 1e-9:
break
return theta, history
# ---------------- Triangulation ----------------
def build_projection_matrix(K: np.ndarray, R: np.ndarray, t: np.ndarray) -> np.ndarray:
return K @ np.hstack([R, t.reshape(3,1)])
def triangulate_center(P1: np.ndarray, P2: np.ndarray, u1: np.ndarray, u2: np.ndarray) -> np.ndarray:
# u1,u2: (2,) pixel coordinates
x1 = u1.reshape(2,1)
x2 = u2.reshape(2,1)
X_h = cv2.triangulatePoints(P1, P2, x1, x2) # 4x1 homogeneous in machine frame if P maps machine->pixels
X = (X_h[:3] / X_h[3]).reshape(3)
return X.astype(np.float32)
# ---------------- Main ----------------
def main():
print("Started")
parser = argparse.ArgumentParser(description="Two-camera ArUco joint optimization and triangulation")
parser.add_argument('-i', '--images', action='append', required=True,
help="Two image paths: first camera then second camera")
parser.add_argument('-npz', '--npz', action='append', required=True,
help="Two NPZ intrinsics paths: cam1 then cam2")
parser.add_argument('-markerSizeMM', '--markerSizeMM', type=float, default=25.0,
help="Marker side length in millimeters")
parser.add_argument('--dict', default='DICT_4X4_250', help="ArUco dictionary name")
parser.add_argument('-settings', type=str, default=None,
help="Json settings file containing machine KnownMarkers")
args = parser.parse_args()
if len(args.images) != 2 or len(args.npz) != 2:
print("[ERROR] Provide exactly two images and two intrinsics NPZ files.")
sys.exit(1)
img1 = cv2.imread(args.images[0])
img2 = cv2.imread(args.images[1])
draw1 = img1.copy()
draw2 = img2.copy()
h, w = draw1.shape[:2]
#drawPNG1 = np.zeros((h, w, 4), dtype=np.uint8) # fully transparent
drawPNG1 = np.zeros((h, w, 3), dtype=np.uint8)
# Also prepare a matching canvas for camera2 to keep the layout tidy
drawPNG2 = np.zeros((h, w, 3), dtype=np.uint8)
if img1 is None or img2 is None:
print("[ERROR] Cannot read one of the images.")
sys.exit(1)
K1, D1 = load_intrinsics_npz(args.npz[0])
K2, D2 = load_intrinsics_npz(args.npz[1])
# Marker 3D local geometry (square corners)
half = (args.markerSizeMM / 1000.0) / 2.0
obj_points = np.array([
[-half, half, 0.0],
[ half, half, 0.0],
[ half, -half, 0.0],
[-half, -half, 0.0],
], dtype=np.float32)
# Read settings for machine known markers
known_markers: Dict[int, np.ndarray] = {}
if args.settings is not None and os.path.exists(args.settings):
with open(args.settings, 'r') as f:
settings = json.load(f)
for marker_id, coords in settings['coordinateSystem']['KnownMarkers'].items():
known_markers[int(marker_id)] = np.array(coords, dtype=np.float32)
print("[INFO] Loaded KnownMarkers from settings.")
else:
# Fallback defaults
known_markers = {
50: np.array([0.0, 0.0, 0.0], dtype=np.float32),
71: np.array([0.140, 0.0, 0.0], dtype=np.float32),
101: np.array([0.0, -0.080, 0.0], dtype=np.float32),
}
print("[WARN] Using default KnownMarkers; provide -settings for your machine.")
# Detect markers in both images
detector_tuple = get_aruco_detector(args.dict)
corners1, ids1, _ = detect_markers(img1, detector_tuple)
corners2, ids2, _ = detect_markers(img2, detector_tuple)
if ids1 is None or ids2 is None:
print("[ERROR] No markers detected in one or both images.")
sys.exit(1)
ids1 = ids1.flatten().tolist()
ids2 = ids2.flatten().tolist()
# Build dicts: id -> corners, center, rvec/tvec (per-camera PnP)
def build_marker_dict(img, corners_list, ids, K, D, draw = False) -> Tuple[Dict[int,np.ndarray], Dict[int,np.ndarray], Dict[int,Tuple[np.ndarray,np.ndarray]]]:
centers = {}
corners_dict = {}
poses = {}
for i, mid in enumerate(ids):
C = corners_list[i]
corners_dict[int(mid)] = C
centers[int(mid)] = marker_center_from_corners(C)
# Pose from single marker
img_pts = corners_to_image_points(C)
success, rvec, tvec = cv2.solvePnP(obj_points, img_pts, K, D, flags=cv2.SOLVEPNP_IPPE_SQUARE)
if not success:
success, rvec, tvec = cv2.solvePnP(obj_points, img_pts, K, D)
if success:
poses[int(mid)] = (rvec.reshape(3,1), tvec.reshape(3,1))
if(draw):
cv2.drawFrameAxes(draw1, K, D, rvec, tvec, 0.02) # slim orientation axes
cv2.drawFrameAxes(drawPNG1, K, D, rvec, tvec, 0.02) # slim orientation axes
return centers, corners_dict, poses
centers1, corners_dict1, poses1 = build_marker_dict(img1, corners1, ids1, K1, D1, draw = True)
centers2, corners_dict2, poses2 = build_marker_dict(img2, corners2, ids2, K2, D2)
# Common reference markers present in both images and known in machine frame
common_refs = [mid for mid in known_markers.keys() if (mid in centers1 and mid in centers2)]
if len(common_refs) < 3:
print(f"[ERROR] Need ≥3 common reference markers in both cameras; found {len(common_refs)}: {common_refs}")
sys.exit(1)
# Initial extrinsics from rigid fits per camera using tvec centers of references
# For camera j, A = camera-frame positions of reference markers (from PnP tvec), B = machine positions
def init_extrinsics_from_rigid(poses_cam: Dict[int,Tuple[np.ndarray,np.ndarray]], refs: List[int]) -> Tuple[np.ndarray,np.ndarray]:
A = []
B = []
for mid in refs:
if mid in poses_cam:
_, tvec = poses_cam[mid]
A.append(tvec.flatten())
B.append(known_markers[mid])
if len(A) < 2:
raise RuntimeError("Insufficient reference poses for rigid transform init.")
A = np.stack(A, axis=0)
B = np.stack(B, axis=0)
R_cm, t_cm = rigid_transform_no_scale(A, B) # camera->machine
# Convert to machine->camera
R_mc = R_cm.T
t_mc = - R_mc @ t_cm
return R_mc.astype(np.float32), t_mc.astype(np.float32)
R1_init, t1_init = init_extrinsics_from_rigid(poses1, common_refs)
R2_init, t2_init = init_extrinsics_from_rigid(poses2, common_refs)
# Observations: reference centers (pixels) -> normalized
X_mach_refs = np.stack([known_markers[mid] for mid in common_refs], axis=0).astype(np.float32)
obs1_px = np.stack([centers1[mid] for mid in common_refs], axis=0).astype(np.float32)
obs2_px = np.stack([centers2[mid] for mid in common_refs], axis=0).astype(np.float32)
obs1_norm = undistort_to_normalized(obs1_px, K1, D1)
obs2_norm = undistort_to_normalized(obs2_px, K2, D2)
# Pack initial params as Rodrigues vectors
omega1_init = cv2.Rodrigues(R1_init)[0].reshape(3)
omega2_init = cv2.Rodrigues(R2_init)[0].reshape(3)
theta0 = pack_params(omega1_init, t1_init.reshape(3), omega2_init, t2_init.reshape(3))
theta_opt, hist = lm_solve(theta0, X_mach_refs, obs1_norm, obs2_norm,
max_iter=60, eps_jac=1e-6, lambda_init=1e-3)
omega1, t1, omega2, t2 = unpack_params(theta_opt)
R1_opt = cv2.Rodrigues(omega1)[0]
R2_opt = cv2.Rodrigues(omega2)[0]
# Camera pose in machine coordinates (camera->machine): R_cm = R^T, t_cm = -R^T t
R1_cm = R1_opt.T
t1_cm = - R1_cm @ t1
R2_cm = R2_opt.T
t2_cm = - R2_cm @ t2
# Build projection matrices for triangulation (machine -> pixels)
P1 = build_projection_matrix(K1, R1_opt, t1)
P2 = build_projection_matrix(K2, R2_opt, t2)
# Collect markers seen by at least one camera
all_ids = set(ids1) | set(ids2)
# Output structures
rows = [("id", "x_mm", "y_mm", "z_mm", "roll_deg", "pitch_deg", "yaw_deg")]
marker_list = []
# Camera orientations in Euler (ZYX)
def R_to_euler_zyx(R: np.ndarray) -> Tuple[float,float,float]:
yaw = float(np.degrees(np.arctan2(R[1,0], R[0,0])))
sp = np.sqrt(R[2,1]**2 + R[2,2]**2)
pitch = float(np.degrees(np.arctan2(-R[2,0], sp)))
roll = float(np.degrees(np.arctan2(R[2,1], R[2,2])))
return roll, pitch, yaw
cam1_roll, cam1_pitch, cam1_yaw = R_to_euler_zyx(R1_cm)
cam2_roll, cam2_pitch, cam2_yaw = R_to_euler_zyx(R2_cm)
# Camera rows
c1_mm = (t1_cm * 1000.0).tolist()
rows.append(("camera 0", f"{c1_mm[0]:.2f}", f"{c1_mm[1]:.2f}", f"{c1_mm[2]:.2f}", f"{cam1_roll:.3f}", f"{cam1_pitch:.3f}", f"{cam1_yaw:.3f}"))
c2_mm = (t2_cm * 1000.0).tolist()
rows.append(("camera 1", f"{c2_mm[0]:.2f}", f"{c2_mm[1]:.2f}", f"{c2_mm[2]:.2f}", f"{cam2_roll:.3f}", f"{cam2_pitch:.3f}", f"{cam2_yaw:.3f}"))
# Triangulate and output markers
def orientation_in_machine(mid: int) -> Tuple[float,float,float]:
# Prefer camera1 orientation, else camera2
if mid in poses1:
Rm_cam = rvec_to_R(poses1[mid][0])
Rm_machine = R1_cm @ Rm_cam
elif mid in poses2:
Rm_cam = rvec_to_R(poses2[mid][0])
Rm_machine = R2_cm @ Rm_cam
else:
Rm_machine = np.eye(3, dtype=np.float32)
return R_to_euler_zyx(Rm_machine)
# Residual report for references
# Recompute residual RMS in pixels for references (after optimization)
refs_rms_px = []
for j,(K,R_opt,t_opt,centers_px) in enumerate([(K1,R1_opt,t1,centers1),(K2,R2_opt,t2,centers2)]):
errs = []
for mid in common_refs:
X = known_markers[mid]
X_cam = R_opt @ X + t_opt
x = K @ X_cam
u_pred = x[0]/x[2]
v_pred = x[1]/x[2]
u_obs, v_obs = centers_px[mid]
errs.append(np.hypot(u_obs-u_pred, v_obs-v_pred))
refs_rms_px.append(float(np.sqrt(np.mean(np.square(errs))) if errs else 0.0))
# Compute per-marker positions
for mid in sorted(all_ids):
# Triangulate if seen in both
if mid in centers1 and mid in centers2:
X_mach = triangulate_center(P1, P2, centers1[mid], centers2[mid])
elif mid in poses1:
# Fallback single-camera: transform tvec (camera->machine)
X_mach = (R1_cm @ poses1[mid][1].flatten() + t1_cm)
elif mid in poses2:
X_mach = (R2_cm @ poses2[mid][1].flatten() + t2_cm)
else:
continue
roll, pitch, yaw = orientation_in_machine(mid)
x_mm, y_mm, z_mm = (X_mach * 1000.0).tolist()
rows.append((mid, f"{x_mm:.2f}", f"{y_mm:.2f}", f"{z_mm:.2f}", f"{roll:.3f}", f"{pitch:.3f}", f"{yaw:.3f}"))
marker_list.append({
"id": int(mid),
"position_mm": [float(x_mm), float(y_mm), float(z_mm)],
"orientation_deg": {"roll": float(roll), "pitch": float(pitch), "yaw": float(yaw)}
})
# Save CSV & JSON
base1 = os.path.splitext(args.images[0])[0]
base2 = os.path.splitext(args.images[1])[0]
out_csv = f"{base1}_two_cam.csv"
out_json = f"{base1}_two_cam.json"
try:
import csv
with open(out_csv, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerows(rows)
print(f"[INFO] Saved CSV poses to '{out_csv}'.")
except Exception as e:
print(f"[WARN] Could not save CSV: {e}")
json_data = {
"metadata": {
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"reference_markers": common_refs,
"dict": args.dict,
"marker_size_mm": args.markerSizeMM,
"rms_refs_px_cam1": refs_rms_px[0] if refs_rms_px else None,
"rms_refs_px_cam2": refs_rms_px[1] if refs_rms_px else None,
"description": "Two-camera joint optimization with triangulation"
},
"cameras": [
{
"id": "camera1",
"position_mm": [float(x) for x in (t1_cm * 1000.0)],
"orientation_deg": {"roll": cam1_roll, "pitch": cam1_pitch, "yaw": cam1_yaw},
},
{
"id": "camera2",
"position_mm": [float(x) for x in (t2_cm * 1000.0)],
"orientation_deg": {"roll": cam2_roll, "pitch": cam2_pitch, "yaw": cam2_yaw},
}
],
"markers": marker_list
}
try:
with open(out_json, 'w', encoding='utf-8') as f:
json.dump(json_data, f, indent=4)
print(f"[INFO] Saved JSON poses to '{out_json}'.")
except Exception as e:
print(f"[WARN] Could not save JSON: {e}")
# Annotate images with machine axes using camera1 extrinsics
try:
R_machine_to_cam1 = R1_opt
t_machine_to_cam1 = t1
machine_axes = np.float32([
[0.0, 0.0, 0.0],
[0.200, 0.0, 0.0],
[0.0, -0.100, 0.0],
[0.0, 0.0, 0.100],
])
rvec_global, _ = cv2.Rodrigues(R_machine_to_cam1)
imgpts, _ = cv2.projectPoints(machine_axes, rvec_global, t_machine_to_cam1, K1, D1)
origin = tuple(np.round(imgpts[0].ravel()).astype(int))
x_end = tuple(np.round(imgpts[1].ravel()).astype(int))
y_end = tuple(np.round(imgpts[2].ravel()).astype(int))
z_end = tuple(np.round(imgpts[3].ravel()).astype(int))
# Draw marker outlines only (omit default small id labels) — we draw larger IDs below
cv2.aruco.drawDetectedMarkers(draw1, corners1)
cv2.aruco.drawDetectedMarkers(drawPNG1, corners1)
# Draw larger blue ID labels (keep default marker outlines as-is)
try:
for i, mid in enumerate(ids1):
try:
pts = corners1[i].reshape((4, 2))
center = tuple(np.round(pts.mean(axis=0)).astype(int))
except Exception:
continue
text = str(int(mid))
# Offset: 5px more to the right and 5px up (y axis is downwards)
pos = (int(center[0]) + 15, int(center[1]) - 15)
cv2.putText(draw1, text, pos, cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255,0,0), 3, lineType=cv2.LINE_AA)
cv2.putText(drawPNG1, text, pos, cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255,0,0,255), 3, lineType=cv2.LINE_AA)
except Exception:
pass
cv2.line(draw1, origin, x_end, (0,0,255), 3)
cv2.line(draw1, origin, y_end, (0,255,0), 3)
cv2.line(draw1, origin, z_end, (255,0,0), 3)
# Draw lines (RGBA colors: B,G,R,A). A=255 for fully opaque.
cv2.line(drawPNG1, origin, x_end, (0, 0, 255, 255), 3) # Red X
cv2.line(drawPNG1, origin, y_end, (0, 255, 0, 255), 3) # Green Y
cv2.line(drawPNG1, origin, z_end, (255, 0, 0, 255), 3) # Blue Z
cv2.putText(draw1, "X (200 mm)", x_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,0,255), 2)
cv2.putText(draw1, "Y (-100 mm)", y_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,255,0), 2)
cv2.putText(draw1, "+Z (100 mm)", z_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,0,0), 2)
# Try to draw text (might be jaggy on transparent BG in older OpenCV)
cv2.putText(drawPNG1, "X (200 mm)", x_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255, 255), 2)
cv2.putText(drawPNG1, "Y (-100 mm)", y_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0, 255), 2)
cv2.putText(drawPNG1, "+Z (100 mm)", z_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0, 255), 2)
out_img1 = f"{base1}_two_cam_annotated.jpg"
cv2.imwrite(out_img1, draw1)
print(f"[INFO] Annotated image saved as '{out_img1}'.")
# Save as transparent PNG
gray = cv2.cvtColor(drawPNG1, cv2.COLOR_BGR2GRAY)
_, alpha = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)
# 5) Merge BGR + alpha → RGBA transparent overlay
drawPNG_1 = cv2.merge([drawPNG1[:, :, 0], drawPNG1[:, :, 1], drawPNG1[:, :, 2], alpha])
out_png1 = f"{base1}_two_cam_overlay.png"
cv2.imwrite(out_png1, drawPNG_1)
except Exception as e:
print(f"[WARN] Failed to draw machine axes: {e}")
# Also annotate the second camera image and produce a transparent overlay
try:
machine_axes2 = np.float32([
[0.0, 0.0, 0.0],
[0.200, 0.0, 0.0],
[0.0, -0.100, 0.0],
[0.0, 0.0, 0.100],
])
rvec_global2, _ = cv2.Rodrigues(R2_opt)
imgpts2, _ = cv2.projectPoints(machine_axes2, rvec_global2, t2, K2, D2)
origin2 = tuple(np.round(imgpts2[0].ravel()).astype(int))
x_end2 = tuple(np.round(imgpts2[1].ravel()).astype(int))
y_end2 = tuple(np.round(imgpts2[2].ravel()).astype(int))
z_end2 = tuple(np.round(imgpts2[3].ravel()).astype(int))
# Draw marker outlines only (omit default small id labels) — we draw larger IDs below
cv2.aruco.drawDetectedMarkers(draw2, corners2)
cv2.aruco.drawDetectedMarkers(drawPNG2, corners2)
# Draw larger blue ID labels (keep default marker outlines as-is)
try:
for i, mid in enumerate(ids2):
try:
pts = corners2[i].reshape((4, 2))
center = tuple(np.round(pts.mean(axis=0)).astype(int))
except Exception:
continue
text = str(int(mid))
# Offset: 5px more to the right and 5px up (y axis is downwards)
pos = (int(center[0]) + 13, int(center[1]) + 3)
cv2.putText(draw2, text, pos, cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255,0,0), 3, lineType=cv2.LINE_AA)
cv2.putText(drawPNG2, text, pos, cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255,0,0,255), 3, lineType=cv2.LINE_AA)
except Exception:
pass
cv2.line(draw2, origin2, x_end2, (0,0,255), 3)
cv2.line(draw2, origin2, y_end2, (0,255,0), 3)
cv2.line(draw2, origin2, z_end2, (255,0,0), 3)
cv2.line(drawPNG2, origin2, x_end2, (0, 0, 255, 255), 3)
cv2.line(drawPNG2, origin2, y_end2, (0, 255, 0, 255), 3)
cv2.line(drawPNG2, origin2, z_end2, (255, 0, 0, 255), 3)
cv2.putText(draw2, "X (200 mm)", x_end2, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,0,255), 2)
cv2.putText(draw2, "Y (-100 mm)", y_end2, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,255,0), 2)
cv2.putText(draw2, "+Z (100 mm)", z_end2, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,0,0), 2)
cv2.putText(drawPNG2, "X (200 mm)", x_end2, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255, 255), 2)
cv2.putText(drawPNG2, "Y (-100 mm)", y_end2, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0, 255), 2)
cv2.putText(drawPNG2, "+Z (100 mm)", z_end2, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0, 255), 2)
out_img2 = f"{base2}_two_cam_annotated.jpg"
cv2.imwrite(out_img2, draw2)
print(f"[INFO] Annotated image saved as '{out_img2}'.")
gray2 = cv2.cvtColor(drawPNG2, cv2.COLOR_BGR2GRAY)
_, alpha2 = cv2.threshold(gray2, 0, 255, cv2.THRESH_BINARY)
drawPNG_2 = cv2.merge([drawPNG2[:, :, 0], drawPNG2[:, :, 1], drawPNG2[:, :, 2], alpha2])
out_png2 = f"{base2}_two_cam_overlay.png"
cv2.imwrite(out_png2, drawPNG_2)
print(f"[INFO] Overlay PNG saved as '{out_png2}'.")
except Exception as e:
print(f"[WARN] Failed to draw machine axes for camera2: {e}")
if __name__ == '__main__':
main()

View File

@@ -1,135 +0,0 @@
const fs = require('fs');
const path = require('path');
const { exec } = require('child_process');
const { logSnapshot } = require('./log');
function snapshot(outDir, cam0, cam1, ws){
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
const picDate = Date.now();
const name0 = `snapshot_video0_${picDate}.jpg`;
const name1 = `snapshot_video1_${picDate}.jpg`;
cam0.snapshot(path.join(outDir, name0));
cam1.snapshot(path.join(outDir, name1));
strFile0 = path.join(outDir, name0);
strFile1 = path.join(outDir, name1);
const relUrl = `/snapshots/${name0}`;
const relUrlApp = `/snapshots/${name0.replace('.jpg','_two_cam_annotated.jpg')}`;
// The Python postprocessor writes an overlay named "_two_cam_overlay.png" and a CSV named "_two_cam.csv"
const relOverlay = `/snapshots/${name0.replace('.jpg','_two_cam_overlay.png')}`;
const relOverlayCSV = `/snapshots/${name0.replace('.jpg','_two_cam.csv')}`;
const annotatedPath = path.join(outDir, name0.replace('.jpg','_two_cam_annotated.jpg'));
const overlayPath = path.join(outDir, name0.replace('.jpg','_two_cam_overlay.png'));
const csvPath = path.join(outDir, name0.replace('.jpg','_two_cam.csv'));
const command = `python3 /usr/src/app/programs/readTwoImages.py -i ${strFile0} -i ${strFile1} -npz /usr/src/app/data/settings/callibration_cam0.npz -npz /usr/src/app/data/settings/callibration_cam1.npz -settings /usr/src/app/data/settings/settings.json`;
console.log("Executing Python " + command);
// Run the Python post-processing and send the snapshot response only
// after the annotated files are present to avoid transient 404s in the browser.
exec(command, (error, stdout, stderr) => {
try {
if (error) {
console.error(`Error: ${error.message}`);
// Capture which generated files actually exist for debugging
const files = {
annotated: fs.existsSync(annotatedPath),
overlay: fs.existsSync(overlayPath),
csv: fs.existsSync(csvPath)
};
// Log full details server-side for diagnosis
const detailed = {
type: 'snapshot',
ok: false,
error: error.message,
stdout: String(stdout).slice(0, 4096),
stderr: String(stderr).slice(0, 4096),
files
};
logSnapshot(command, JSON.stringify(detailed));
// Send a short, user-friendly error to the client (no large stdout/stderr)
const shortError = String(stderr || error.message || '').includes('Corrupt JPEG')
? 'postprocessor failed: corrupt JPEG input'
: 'postprocessor failed';
try { ws.send(JSON.stringify({ type: 'snapshot', ok: false, error: shortError })); } catch (e) {}
return;
}
if (stderr) {
// Log stderr but don't fail outright; sometimes tools output warnings on stderr.
if (String(stderr).trim()) console.error(`Stderr: ${stderr}`);
}
console.log(`Output:\n${stdout}`);
// Wait up to ~1s (10 * 100ms) for the generated files to appear on disk.
const waitForFiles = (paths, attempts = 10, delayMs = 100) => new Promise((resolve) => {
let tries = 0;
(function poll() {
const ok = paths.every(p => fs.existsSync(p));
if (ok || tries >= attempts) return resolve(ok);
tries++;
setTimeout(poll, delayMs);
})();
});
waitForFiles([annotatedPath, overlayPath, csvPath]).then((found) => {
if (!found) {
const files = {
annotated: fs.existsSync(annotatedPath),
overlay: fs.existsSync(overlayPath),
csv: fs.existsSync(csvPath)
};
// Log details server-side
const detailed = {
type: 'snapshot',
ok: false,
url: relUrl,
urlApp: relUrlApp,
overlay: relOverlay,
overlayCSV: relOverlayCSV,
files
};
logSnapshot(command, JSON.stringify(detailed));
// Send a concise error to the client
try { ws.send(JSON.stringify({ type: 'snapshot', ok: false, error: 'postprocessor incomplete (missing outputs)' })); } catch (e) {}
return;
}
const response = JSON.stringify({
type: 'snapshot',
ok: found,
url: relUrl,
urlApp: relUrlApp,
overlay: relOverlay,
overlayCSV: relOverlayCSV
});
logSnapshot(command, response);
try { ws.send(response); } catch (e) {}
}).catch((waitErr) => {
console.error('waitForFiles failed:', waitErr);
const response = JSON.stringify({ type: 'snapshot', ok: false, error: String(waitErr) });
logSnapshot(command, response);
try { ws.send(response); } catch (e) {}
});
} catch (handlerErr) {
console.error('snapshot handler error:', handlerErr);
const response = JSON.stringify({ type: 'snapshot', ok: false, error: String(handlerErr) });
logSnapshot(command, response);
try { ws.send(response); } catch (e) {}
}
});
}
module.exports = { snapshot };

View File

@@ -1,234 +0,0 @@
// programs/videoServer.js
'use strict';
const fs = require('fs');
const { spawn } = require('child_process');
const WebSocket = require('ws');
class JpegFrameSplitter {
constructor(onFrame) {
this.onFrame = onFrame;
this.buffer = Buffer.alloc(0);
}
push(chunk) {
if (!chunk || !chunk.length) return;
this.buffer = Buffer.concat([this.buffer, chunk]);
let start = this.buffer.indexOf(Buffer.from([0xFF, 0xD8]));
while (start !== -1) {
const end = this.buffer.indexOf(Buffer.from([0xFF, 0xD9]), start + 2);
if (end === -1) break;
const frame = this.buffer.slice(start, end + 2);
try { this.onFrame(frame); } catch {}
this.buffer = this.buffer.slice(end + 2);
start = this.buffer.indexOf(Buffer.from([0xFF, 0xD8]));
}
if (this.buffer.length > 8 * 1024 * 1024) {
const nextSOI = this.buffer.indexOf(Buffer.from([0xFF, 0xD8]));
this.buffer = nextSOI !== -1 ? this.buffer.slice(nextSOI) : Buffer.alloc(0);
}
}
}
class FFmpegStreamer {
/**
* devicePath: '/dev/videoX'
* options: {
* name, width, height, fps, quality,
* input: { format, fps, size, useWallclock, threadQueueSize, channel },
* tryFormats: [ 'mjpeg', undefined, 'yuyv422', 'rgb24' ]
* }
*/
constructor(devicePath, options = {}) {
this.devicePath = devicePath;
this.name = options.name || devicePath;
this.opts = {
width: options.width ?? undefined,
height: options.height ?? undefined,
fps: options.fps ?? 20,
quality: options.quality ?? 5,
input: {
format: options.input?.format,
fps: options.input?.fps,
size: options.input?.size,
useWallclock: options.input?.useWallclock ?? true,
threadQueueSize: options.input?.threadQueueSize ?? 64,
channel: options.input?.channel,
},
tryFormats: (options.tryFormats || [options.input?.format, 'yuyv422', 'mjpeg', 'rgb24'])
.filter((v, i, a) => a.indexOf(v) === i),
};
this.proc = null;
this.clients = new Set();
this.startedAt = null;
this.latestFrame = null;
this.splitter = null;
this.formatIdx = 0;
this.currentFormat = this.opts.tryFormats[this.formatIdx];
this._restarting = false;
this._backoffMs = 500;
this._maxBackoffMs = 8000;
this._stderrBuf = [];
this._stderrMaxLines = 8;
this._quickFailCount = 0;
this._quickFailLimit = 6;
this._suspendedUntil = 0;
}
get running() { return !!this.proc; }
_scaling() { return Number(this.opts.width) > 0 && Number(this.opts.height) > 0; }
_buildFfmpegArgs() {
const outFps = this.opts.fps;
const quality = this.opts.quality;
const scaling = this._scaling();
const inFmt = this.currentFormat;
const inFps = this.opts.input.fps;
const inSize = this.opts.input.size;
const useWallclock = this.opts.input.useWallclock;
const tqs = this.opts.input.threadQueueSize;
const inChannel = this.opts.input.channel;
const args = [
'-hide_banner', '-loglevel', 'error', '-nostdin',
'-f', 'video4linux2',
...(tqs ? ['-thread_queue_size', String(tqs)] : []),
...(inFmt ? ['-input_format', String(inFmt)] : []),
...(inFps ? ['-framerate', String(inFps)] : []),
...(inSize ? ['-video_size', String(inSize)] : []),
...(typeof inChannel === 'number' ? ['-channel', String(inChannel)] : []),
...(useWallclock ? ['-use_wallclock_as_timestamps', '1'] : []),
'-i', this.devicePath,
'-fflags', 'nobuffer', '-flags', 'low_delay', '-an', '-sn',
];
if (inFmt === 'mjpeg' && !scaling) {
args.push('-vsync', 'passthrough', '-c:v', 'copy', '-f', 'mjpeg', 'pipe:1');
return args;
}
if (scaling) args.push('-vf', `scale=${Number(this.opts.width)}:${Number(this.opts.height)}`);
if (outFps) args.push('-r', String(outFps));
args.push('-f', 'mjpeg', '-q:v', String(quality), 'pipe:1');
return args;
}
_logStderr(d) {
const s = d.toString().trim();
if (!s) return;
this._stderrBuf.push(s);
if (this._stderrBuf.length > this._stderrMaxLines) this._stderrBuf.shift();
}
start() {
if (this.proc) return;
if (Date.now() < this._suspendedUntil) {
const wait = this._suspendedUntil - Date.now();
console.warn(`[FFmpeg] ${this.name} suspended for ${wait}ms due to repeated failures`);
return setTimeout(() => this.start(), wait);
}
const args = this._buildFfmpegArgs();
console.log(`[FFmpeg] Start ${this.devicePath} (${this.name}) :: ${args.join(' ')}`);
this._stderrBuf = [];
this.proc = spawn('ffmpeg', args, { stdio: ['ignore', 'pipe', 'pipe'], detached: true });
this.startedAt = Date.now();
this.splitter = new JpegFrameSplitter((frame) => {
this.latestFrame = frame;
this._broadcast(frame);
});
this.proc.stdout.on('data', (chunk) => this.splitter?.push(chunk));
this.proc.stderr.on('data', (d) => this._logStderr(d));
this.proc.on('exit', (code, signal) => {
console.warn(`[FFmpeg] ${this.devicePath} exited code=${code} sig=${signal}`);
if (this._stderrBuf.length) console.warn(`[FFmpeg] ${this.name} last errors:\n - ${this._stderrBuf.join('\n - ')}`);
this.proc = null;
const quick = (Date.now() - (this.startedAt || Date.now())) < 2000;
this.startedAt = null;
if (quick && !this._restarting) {
this._quickFailCount++;
this.formatIdx = (this.formatIdx + 1) % this.opts.tryFormats.length;
this.currentFormat = this.opts.tryFormats[this.formatIdx];
console.warn(`[FFmpeg] ${this.name}: quick failure -> trying next format: ${this.currentFormat}`);
} else {
this._quickFailCount = 0;
}
if (this._quickFailCount >= this._quickFailLimit) {
this._suspendedUntil = Date.now() + 60000; // 60s pause
this._quickFailCount = 0;
console.error(`[FFmpeg] ${this.name}: too many quick failures; suspending restarts for 60s`);
}
const delay = this._restarting ? 300 : Math.min(this._backoffMs, this._maxBackoffMs);
setTimeout(() => {
if (!this._restarting) this._backoffMs = Math.min(this._backoffMs * 2, this._maxBackoffMs);
else this._backoffMs = 500;
this.start();
}, delay);
this._restarting = false;
});
}
_killProcessGroup(signal = 'SIGTERM') {
if (!this.proc) return;
try {
if (process.platform !== 'win32') process.kill(-this.proc.pid, signal);
else this.proc.kill(signal);
} catch {}
}
stop() {
if (!this.proc) return;
this._restarting = false;
this._killProcessGroup('SIGTERM');
}
restart(newOpts = {}) {
this._restarting = true;
if (newOpts.input) this.opts.input = { ...this.opts.input, ...newOpts.input };
this.opts = { ...this.opts, ...newOpts, input: this.opts.input };
if (newOpts.input && Object.prototype.hasOwnProperty.call(newOpts.input, 'format')) {
const idx = this.opts.tryFormats.indexOf(this.opts.input.format);
if (idx >= 0) {
this.formatIdx = idx;
this.currentFormat = this.opts.tryFormats[this.formatIdx];
}
}
if (this.proc) this._killProcessGroup('SIGTERM'); else { this._restarting = false; this.start(); }
}
attach(ws) {
this.clients.add(ws);
if (this.latestFrame && ws.readyState === WebSocket.OPEN) {
try { ws.send(this.latestFrame, { binary: true }); } catch {}
}
ws.on('close', () => this.clients.delete(ws));
}
snapshot(toFile) {
if (!this.latestFrame) throw new Error('No frame available yet');
fs.writeFileSync(toFile, this.latestFrame);
return toFile;
}
_broadcast(frame) {
if (!this.clients.size) return;
for (const ws of this.clients) {
if (ws.readyState !== WebSocket.OPEN) continue;
if (ws.bufferedAmount > 512 * 1024) continue; // drop if back-pressured
try { ws.send(frame, { binary: true }); } catch {}
}
}
}
module.exports = { FFmpegStreamer };

View File

@@ -1,110 +0,0 @@
var isRunning = false;
var gamePadId = 0;
var gamepad = {};
let lastCheckTime = 0;
function checkGamePad() {
if(isRunning == false){return;}
const stepSize = "0.01";
const stepSizeXYZ = "0.5"; // 3 ist auch ok
const stepSizeE = "0.01";
var gp = navigator.getGamepads()[gamePadId]
var buttons = gp.buttons
var xyzSpeed = 10; // 100 geht auch
var psi = gp.axes[0];
var z = gp.axes[1];
var x = gp.axes[2];
var y = gp.axes[3];
// Dreieck zum Dreieck-Setzen
if (buttons[3].pressed) {
socketDriver.send(`G90 G1 X0 Y300 Z0 A${Math.PI/2} B${-1.0*Math.PI/2} C0 F${xyzSpeed}`);
}
if (buttons[4].pressed) {
//console.log("x=" + robot.x + " y=" + robot.y + " z=" + robot.z);
}
// X Button setzt eine Marke
if(buttons[0].pressed && (Date.now() - lastCheckTime > 500)){
lastCheckTime = Date.now()
console.log('FPoint!');
socketDriver.send('FPoint');
socketDriver.send('FShow');
}
// L1 und R1 Button to forward-backward in Point-List
if(gp.buttons[4].pressed && (Date.now() - lastCheckTime > 500)){
lastCheckTime = Date.now()
socketDriver.send('FMinus');
socketDriver.send('FShow');
}
if(gp.buttons[5].pressed && (Date.now() - lastCheckTime > 500)){
lastCheckTime = Date.now()
socketDriver.send('FPlus');
socketDriver.send('FShow');
}
if (x < -0.2) { socketDriver.send(`G91 G1 X+${stepSizeXYZ} F${xyzSpeed}`);}
if (x > 0.2) { socketDriver.send(`G91 G1 X-${stepSizeXYZ} F${xyzSpeed}`);}
if (y < -0.2) { socketDriver.send(`G91 G1 Y${stepSizeXYZ} F${xyzSpeed}`); }
if (y > 0.2) { socketDriver.send(`G91 G1 Y-${stepSizeXYZ} F${xyzSpeed}`);}
if (z < -0.2) { socketDriver.send(`G91 G1 Z${stepSizeXYZ} F${xyzSpeed}`); }
if (z > 0.2) { socketDriver.send(`G91 G1 Z-${stepSizeXYZ} F${xyzSpeed}`); }
// Greif-Richtung
// LeftRight
if(buttons[14].pressed){ socketDriver.send(`G91 G1 A${stepSize} F${xyzSpeed}`);}
if(buttons[15].pressed){ socketDriver.send(`G91 G1 A-${stepSize} F${xyzSpeed}`);}
// Up - Down
if(buttons[12].pressed){ socketDriver.send(`G91 G1 B${stepSize} F${xyzSpeed}`);}
if(buttons[13].pressed){ socketDriver.send(`G91 G1 B-${stepSize} F${xyzSpeed}`);}
// Drehung
if (psi < -0.2) { socketDriver.send(`G91 G1 C${stepSize} F${xyzSpeed}`); }
if (psi > 0.2) { socketDriver.send(`G91 G1 C-${stepSize} F${xyzSpeed}`); }
// Trigger-Buttons für Öffnen und Schliessen
if(buttons[6].pressed){socketDriver.send(`G91 G1 E-${stepSizeE} F${xyzSpeed}`);}
if(buttons[7].pressed){socketDriver.send(`G91 G1 E${stepSizeE} F${xyzSpeed}`);}
if (isRunning) { setTimeout(checkGamePad, 15);}
}
function gamepadHandler(event, connecting) {
gamepad = event.gamepad;
if (typeof gamepad === `undefined`) {
isRunning = false;
gamePadId = 0;
console.warn("GamePad kann nicht gefunden werden");
return;
}
if (connecting) {
console.log("GamePad " + event.gamepad.index + " connected");
gamePadId = gamepad.index;
isRunning = true;
setTimeout(checkGamePad, 20);
} else {
console.log("GamePad " +gamePadId +" disconnected");
isRunning = false;
gamePadId = 0;
}
}
window.addEventListener("gamepadconnected", function (e) { gamepadHandler(e, true); }, false);
window.addEventListener("gamepaddisconnected", function (e) { gamepadHandler(e, false); }, false);
document.addEventListener("touchstart", e => { console.log("TouchStart"); })

View File

@@ -1,64 +0,0 @@
document.onkeydown = checkKey;
function checkKey(e) {
e = e || window.event;
if(e.key == 'a' || e.key == 'A')
{
console.log("back to A position");
socketDriver.send(`G90 G1 X0 Y300 Z0 A${Math.PI/2} B-${Math.PI/2} C0 F100`);
}
// Hand-Winkel (Eulerwinkel)
else if(e.key == 'i' || e.key == 'I'){
socketDriver.send('G91 G1 B+0.1 F100');
}
else if(e.key == 'k' || e.key == 'K'){
socketDriver.send('G91 G1 B-0.1 F100');
}
else if(e.key == 'l' || e.key == 'L'){
socketDriver.send('G91 G1 A+0.1 F100');
}
else if(e.key == 'j' || e.key == 'J'){
socketDriver.send('G91 G1 A-0.1 F100');
}
else if(e.key == 'o' || e.key == 'O'){
socketDriver.send('G91 G1 C+0.1 F100');
}
else if(e.key == 'u' || e.key == 'U'){
socketDriver.send('G91 G1 C-0.1 F100');
}
// XYZ Koordinaten
else if(e.key == 'e' || e.key == 'E'){
socketDriver.send('G91 G1 Z+5 F100');
}
else if(e.key == 'd' || e.key == 'D'){
socketDriver.send('G91 G1 Z-5 F100');
}
else if(e.key == 's' || e.key == 'S'){
socketDriver.send('G91 G1 X5 F100');
}
else if(e.key == 'f' || e.key == 'S'){
socketDriver.send('G91 G1 X-5 F100');
}
else if(e.key == 'r' || e.key == 'R'){
socketDriver.send('G91 G1 Y5 F100');
}
else if(e.key == 'w' || e.key == 'W'){
socketDriver.send('G91 G1 Y-5 F100');
}
// File & Log-Operations
else if(e.key == ' '){
console.log('FPoint!')
socketDriver.send('FPoint');
}
else if(e.key == 'b'){
socketDriver.send('FMinus');
}
else if(e.key == 'n'){
socketDriver.send('FPlus');
}
}

View File

@@ -1,25 +0,0 @@
const protocolS = location.protocol === "https:" ? "wss://" : "ws://";
const robotURL = protocolS + location.host + "/ws/robot";
console.log("socketDriver try to connect to "+ robotURL);
const socketDriver = new WebSocket(robotURL);
socketDriver.onopen = () => { console.log("socketDriver WebSocket connected"); };
socketDriver.onmessage = (event) => {
if(event.data.toString().includes("position")){
console.log("Position: " + event.data);
}
else if(event.data.toString().includes("XYZ__FShow__XYZ")){
const content = event.data.toString().split("XYZ__FShow__XYZ")[1];
document.querySelectorAll("textarea#GCodeWindow.editor-look")[0].value = content;
}
else{
console.log('DATA SinceStartup: ' + (Date.now() - startTime).toString() +': ', event.data);
}
}
socketDriver.onclose = () => {console.log("socketDriver WebSocket is closing");};
socketDriver.onerror = (err) => { console.error("socketDriver socket error:", err); };

View File

@@ -1,72 +0,0 @@
// /public/app.js
// Small bootstrap moved out of index.html to satisfy CSP (`script-src 'self'`).
(function () {
const isSecure = location.protocol === 'https:';
const wsProto = isSecure ? 'wss' : 'ws';
const base = `${wsProto}://${location.host}`;
// Camera 0 with control channel
window.VideoService.attachStream({
url: `${base}/ws/video0`,
canvas: document.getElementById('canvas0'),
statusEl: document.getElementById('status0'),
control: {
resSelect: document.getElementById('res0'),
fpsSelect: document.getElementById('fps0'),
qSelect: document.getElementById('q0'),
applyBtn: document.getElementById('apply0'),
snapshotBtn: document.getElementById('snap0'),
startBtn: document.getElementById('start0'),
stopBtn: document.getElementById('stop0'),
snapshotOutEl: document.getElementById('snapshotLink'),
}
});
// Camera 1 (no control)
window.VideoService.attachStream({
url: `${base}/ws/video1`,
canvas: document.getElementById('canvas1'),
statusEl: document.getElementById('status1'),
});
// Run an automatic snapshot shortly after the page loads so the client
// receives the freshest annotated image/CSV on startup. This uses a
// synthetic click on the snapshot button; the handler will no-op if the
// WebSocket isn't open yet.
setTimeout(() => {
const snapBtn = document.getElementById('snap0');
if (snapBtn) snapBtn.click();
}, 200);
// Attach handlers for the Point/Up/Down buttons (no inline JS, CSP-safe)
const btnPoint = document.getElementById('btnPoint');
const btnDown = document.getElementById('btnDown');
const btnUp = document.getElementById('btnUp');
if (btnPoint) btnPoint.addEventListener('click', () => {
console.log('FPoint!');
try { socketDriver.send('FPoint'); socketDriver.send('FShow'); } catch (e) { console.error(e); }
});
if (btnDown) btnDown.addEventListener('click', () => {
console.log('FPlus');
try { socketDriver.send('FPlus'); socketDriver.send('FShow'); } catch (e) { console.error(e); }
});
if (btnUp) btnUp.addEventListener('click', () => {
console.log('FMinus');
try { socketDriver.send('FMinus'); socketDriver.send('FShow'); } catch (e) { console.error(e); }
});
// Other buttons that were previously using inline onclicks.
const btnInfo = document.getElementById('b_M114');
const btnNull = document.getElementById('b_G28');
const btnListFile = document.getElementById('btnListFile');
const btnSendGCode = document.getElementById('btnSendGCode');
if (btnInfo) btnInfo.addEventListener('click', () => { try { socketDriver.send('M114'); } catch (e) { console.error(e); } });
if (btnNull) btnNull.addEventListener('click', () => { try { socketDriver.send('G28'); } catch (e) { console.error(e); } });
if (btnListFile) btnListFile.addEventListener('click', () => { try { socketDriver.send('FShow'); } catch (e) { console.error(e); } });
if (btnSendGCode) btnSendGCode.addEventListener('click', () => {
try {
const input = document.getElementById('GKarth');
if (input && input.value) socketDriver.send(input.value);
} catch (e) { console.error(e); }
});
})();

View File

@@ -1,23 +0,0 @@
document.getElementById("b_uparrow").addEventListener('click',() => { socketDriver.send(`G91 G1 Z10 F100`);});
document.getElementById("b_downarw").addEventListener('click',() => { socketDriver.send(`G90 G1 Z-10 F100`);});
document.getElementById("b_right").addEventListener('click',() => { socketDriver.send(`G90 G1 X10 F100`);});
document.getElementById("b_left").addEventListener('click',() => { socketDriver.send(`G90 G1 X-10 F100`);});
document.getElementById("b_forward").addEventListener('click',() => { socketDriver.send(`G90 G1 Y10 F100`);});
document.getElementById("b_backward").addEventListener('click',() => { socketDriver.send(`G90 G1 Y-10 F100`);});
document.getElementById("b_aPlus").addEventListener('click',() => { socketDriver.send(`G91 G1 A0.10 F100`);});
document.getElementById("b_aMinus").addEventListener('click',() => { socketDriver.send(`G91 G1 A-0.10 F100`);});
document.getElementById("b_bPlus").addEventListener('click',() => { socketDriver.send(`G91 G1 B0.10 F100`);});
document.getElementById("b_bMinus").addEventListener('click',() => { socketDriver.send(`G91 G1 B-0.10 F100`);});
document.getElementById("b_cPlus").addEventListener('click',() => { socketDriver.send(`G91 G1 C0.10 F100`);});
document.getElementById("b_cMinus").addEventListener('click',() => { socketDriver.send(`G91 G1 C-0.10 F100`);});
document.getElementById("b_ePlus").addEventListener('click',() => { socketDriver.send(`G91 G1 E0.10 F100`);});
document.getElementById("b_eMinus").addEventListener('click',() => { socketDriver.send(`G91 G1 E-0.10 F100`);});
document.getElementById("b_default").addEventListener('click',() => { socketDriver.send(`G90 G1 X0 Y300 Z0 A${Math.PI/2} B-${Math.PI/2} C0 F100`);});
document.getElementById("b_G28").addEventListener('click',() => { socketDriver.send(`G28`);});
document.getElementById("b_M114").addEventListener('click',() => { socketDriver.send(`M114`);});

View File

@@ -1,108 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Dual Camera Stream (HTTPS + WSS)</title>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<script src="WebSocket.js" defer></script>
<script src="buttonCmd.js" defer></script>
<script src="GamePad.js" defer></script>
<script src="KeyboardInput.js" defer></script>
<link rel="stylesheet" href="indexA.css">
</head>
<body>
<h1>Robot Control</h1>
<div class="grid">
<!-- Camera 0 -->
<section class="panel">
<h2>Camera 0</h2>
<div class="video-wrap">
<canvas id="canvas0" width="1280" height="720"></canvas>
<img id="overlayImg"
src="/snapshots/snapshot_video0_1765356085372_two_cam_overlay.png">
</div>
<div id="status0" class="status">Connecting…</div>
</section>
<!-- Camera 1 -->
<section class="panel">
<h2>Camera 1</h2>
<div class="video-wrap">
<canvas id="canvas1" width="1280" height="720"></canvas>
</div>
<div id="status1" class="status">Connecting…</div>
</section>
<section class="panel">
<div class="controls">
<div style="display:flex; gap:0.5rem; align-items:end;">
<button id="snap0">Snapshot</button>
</div>
</div>
<div id="snapshotLink" class="status"></div>
<div id="csvTable" class="status"></div>
</section>
<section class="panel">
<div id="divButtons">
<h3>Controls</h3>
<button id="b_uparrow">Up</button>
<button id="b_downarw">Down</button>
</br/>
<button id="b_right">Right</button>
<button id="b_left">Left</button>
<br/>
<button id="b_forward">Forward</button>
<button id="b_backward">Backward</button>
<br/>
<button id="b_M114">Info</button>
<button id="b_G28">Null</button>
<button id="b_default">△ Default</button>
<br/>
<br/>
<button id="b_aPlus" >a+</button>
<button id="b_aMinus">a-</button>
<br/>
<button id="b_bPlus" >B+</button>
<button id="b_bMinus">B-</button>
<br/>
<button id="b_cPlus" >C+</button>
<button id="b_cMinus">C-</button>
<br/>
<button id="b_ePlus" >E+</button>
<button id="b_eMinus">E-</button>
</div>
<div id="divGCodeWindow">
<div id="GCode">
<form onsubmit="return false;"><input id="GKarth"/><button id="btnSendGCode" type="button">Send GCode</button></form>
<br/>
</div>
<button id="btnListFile" type="button">ListFile</button>
<br/>
<textarea id="GCodeWindow" class="editor-look" readonly>
</textarea>
<br/>
<button id="btnPoint">🗙 Point</button>
<button id="btnDown"></button>
<button id="btnUp"></button>
</div>
</section>
</div>
<!-- External scripts only (CSP-safe, no inline JS) -->
<script src="/readCSV.js" defer></script>
<script src="/videoService.js" defer></script>
<script src="/app.js" defer></script>
</body>
</html>

View File

@@ -1,203 +0,0 @@
:root { color-scheme: dark light; }
/* Overlay opacity is configurable here. Set to 1 to let PNG alpha be authoritative */
:root {
--overlay-opacity: 1; /* default overlay alpha applied client-side */
}
body {
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
margin: 0;
padding: 1rem;
background: #0b1020;
color: #e7eaf6;
}
h1, h2 {
margin: 0.5rem 0 0.25rem;
}
.grid {
display: grid;
gap: 1rem;
/*
grid-auto-flow: column;
grid-auto-columns: 1280px;
justify-content: start;
*/
grid-template-columns: repeat(auto-fit, minmax(1280px, 1fr));
align-items: start;
}
.panel {
background: #141b34;
border: 1px solid #242c4f;
border-radius: 12px;
padding: 1rem;
box-shadow: 0 10px 24px rgba(0,0,0,0.25);
}
.video-wrap {
position: relative;
background: #000;
border-radius: 8px;
overflow: hidden;
/* Keep a 16:9 box that doesn't stretch beyond the source resolution */
width: 100%;
max-width: 1280px;
aspect-ratio: 16 / 9;
margin: 0 auto;
}
/* Ensure canvas and overlay are sized identically and keep the same aspect ratio
so overlays align correctly when the window is resized. */
.video-wrap canvas,
.video-wrap img,
#overlayImg {
position: absolute;
inset: 0; /* top:0; right:0; bottom:0; left:0 */
width: 100%;
height: 100%;
/* Use `contain` so both layers scale uniformly and show letterboxing instead
of stretching or cropping when the container size differs from the image. */
object-fit: contain;
background: #000;
}
.video-wrap canvas {
z-index: 1;
}
.video-wrap img,
#overlayImg {
z-index: 2;
pointer-events: none; /* don't block interactions with the canvas */
opacity: var(--overlay-opacity);
}
.status {
font-size: 0.9rem;
opacity: 0.85;
margin: 0.5rem 0 0;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 0.5rem;
margin-top: 0.75rem;
}
.controls label {
display: grid;
gap: 0.25rem;
font-size: 0.9rem;
}
select, input, button {
padding: 0.5rem 0.6rem;
border-radius: 8px;
border: 1px solid #2d3766;
background: #0d1530;
color: #e7eaf6;
}
button.primary {
background: #355dff;
border-color: #355dff;
}
a {
color: #9eb8ff;
}
footer {
margin-top: 1.25rem;
opacity: 0.7;
font-size: 0.9rem;
}
.muted {
opacity: 0.8;
}
/* Ensure overlay uses contain and respects alpha channel from the PNG
(the PNG's transparency is the authoritative source of which pixels
are transparent; CSS opacity only adjusts global alpha). */
#overlayImg {
position: absolute;
inset: 0; /* shorthand for top:0; right:0; bottom:0; left:0; */
width: 100%;
height: 100%;
pointer-events: none;
opacity: var(--overlay-opacity);
object-fit: contain; /* preserve aspect ratio and don't distort */
background: transparent;
}
/* CSV table styles */
.csv-table {
width: 440px; /* 4 columns * 200px each */
border-collapse: collapse;
margin-top: 0.5rem;
table-layout: fixed; /* use fixed layout so column widths are respected */
}
.csv-table th, .csv-table td {
border: 1px solid rgba(255,255,255,0.06);
padding: 0.35rem 0.5rem;
text-align: center;
font-size: 0.9rem;
width: 110px; /* fixed column width */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.csv-table th {
background: rgba(255,255,255,0.02);
font-weight: 600;
}
.csv-table tr:nth-child(even) td {
background: rgba(255,255,255,0.01);
}
/* Right-align CSV numeric data and use tabular digits for stable column alignment */
.csv-table td { text-align: right; font-variant-numeric: tabular-nums; }
#GCodeWindow {
width: 100%;
height: 200px;
}
/* Layout: place the buttons and GCode window side-by-side in their panel.
- `#divButtons` gets a fixed ~300px width
- `#divGCodeWindow` uses the remaining width
- On small screens they stack vertically for usability */
/* Use floats for a predictable two-column layout inside the panel. */
#divButtons {
float: left;
width: 300px;
box-sizing: border-box;
margin-right: 1rem;
}
#divGCodeWindow {
display: block;
margin-left: 320px; /* 300px left column + 1rem gap */
box-sizing: border-box;
}
/* Clear floats inside panels so subsequent content flows correctly */
.panel::after {
content: "";
display: table;
clear: both;
}
@media (max-width: 800px) {
#divButtons, #divGCodeWindow {
display: block;
float: none;
width: 100%;
margin-left: 0;
margin-right: 0;
}
}

View File

@@ -1,76 +0,0 @@
// public/readCSV.js
// Fetch a CSV file and render a small table into the given container.
// Usage: window.readCSV.renderCSV(containerOrId, csvUrl)
(function () {
async function renderCSV(container, csvUrl) {
console.log("readCSV should start");
console.log('readCSV.renderCSV', container, csvUrl);
const el = (typeof container === 'string') ? document.getElementById(container) : container;
if (!el) return;
if (!csvUrl) {
el.innerHTML = '<em>No CSV URL provided</em>';
return;
}
el.innerHTML = 'Loading CSV…';
try {
const res = await fetch(csvUrl, { cache: 'no-store' });
if (!res.ok) {
el.innerHTML = `<em>CSV not found (HTTP ${res.status})</em>`;
return;
}
const text = await res.text();
const lines = text.trim().split(/\r?\n/).filter(Boolean);
if (!lines.length) {
el.innerHTML = '<em>CSV is empty</em>';
return;
}
const headers = lines[0].split(',').map(h => h.trim());
// Look for id,x,y,z columns (case-insensitive)
const lower = headers.map(h => h.toLowerCase());
const ids = {
id: lower.indexOf('id'),
x: lower.indexOf('x_mm'),
y: lower.indexOf('y_mm'),
z: lower.indexOf('z_mm')
};
// If any of these are missing, show a helpful message
if (ids.id === -1 || ids.x === -1 || ids.y === -1 || ids.z === -1) {
el.innerHTML = `<em>CSV does not contain required columns: id, x, y, z</em> ${csvUrl}`;
return;
}
// Build table
const table = document.createElement('table');
table.className = 'csv-table';
const thead = table.createTHead();
const thr = thead.insertRow();
['id', 'x [mm]', 'y [mm]', 'z [mm]'].forEach(h => {
const th = document.createElement('th');
th.textContent = h;
thr.appendChild(th);
});
const tbody = table.createTBody();
for (let i = 1; i < lines.length; i++) {
const row = lines[i].split(',').map(c => c.trim());
// Skip if row doesn't have enough columns
if (row.length < headers.length) continue;
const tr = tbody.insertRow();
['id', 'x', 'y', 'z'].forEach(k => {
const td = tr.insertCell();
td.textContent = row[ids[k]] ?? '';
});
}
el.innerHTML = ''; // clear
el.appendChild(table);
} catch (err) {
console.error('readCSV error', err);
el.innerHTML = `<em>Error loading CSV</em>`;
}
}
window.readCSV = { renderCSV };
})();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -1,9 +0,0 @@
id,x_mm,y_mm,z_mm,roll_deg,pitch_deg,yaw_deg
camera1,461.226,163.150,208.814,-115.601,2.852,108.987
camera2,368.003,-324.025,849.279,-147.633,-3.697,60.975
20,-27.787,-370.901,-104.452,-0.238,-4.375,143.811
25,-67.077,-165.677,-12.623,-2.256,0.110,87.366
50,0.002,-0.008,-0.007,-0.557,-0.254,0.228
71,140.418,0.009,0.093,0.905,-2.137,1.508
76,-204.436,-90.385,-94.522,-52.679,39.273,-87.214
101,0.016,-80.028,-0.025,1.056,-2.213,0.260
1 id x_mm y_mm z_mm roll_deg pitch_deg yaw_deg
2 camera1 461.226 163.150 208.814 -115.601 2.852 108.987
3 camera2 368.003 -324.025 849.279 -147.633 -3.697 60.975
4 20 -27.787 -370.901 -104.452 -0.238 -4.375 143.811
5 25 -67.077 -165.677 -12.623 -2.256 0.110 87.366
6 50 0.002 -0.008 -0.007 -0.557 -0.254 0.228
7 71 140.418 0.009 0.093 0.905 -2.137 1.508
8 76 -204.436 -90.385 -94.522 -52.679 39.273 -87.214
9 101 0.016 -80.028 -0.025 1.056 -2.213 0.260

View File

@@ -1,123 +0,0 @@
{
"metadata": {
"timestamp": "2025-12-17 12:29:14",
"reference_markers": [
50,
71,
101
],
"dict": "DICT_4X4_250",
"marker_size_mm": 25.0,
"rms_refs_px_cam1": 0.01816248951344627,
"rms_refs_px_cam2": 0.49688754115479944,
"description": "Two-camera joint optimization with triangulation"
},
"cameras": [
{
"id": "camera1",
"position_mm": [
461.2260243096291,
163.15047116357306,
208.81423321244827
],
"orientation_deg": {
"roll": -115.60069066047194,
"pitch": 2.8518474088280343,
"yaw": 108.98699397077662
}
},
{
"id": "camera2",
"position_mm": [
368.0032092105987,
-324.02483230064206,
849.2788823044326
],
"orientation_deg": {
"roll": -147.6328488975886,
"pitch": -3.697144586874371,
"yaw": 60.97510432458154
}
}
],
"markers": [
{
"id": 20,
"position_mm": [
-27.78722965547953,
-370.90144636184993,
-104.45169497289619
],
"orientation_deg": {
"roll": -0.23809413256188228,
"pitch": -4.374834208590023,
"yaw": 143.81085305206332
}
},
{
"id": 25,
"position_mm": [
-67.07681274414062,
-165.6768341064453,
-12.623200416564941
],
"orientation_deg": {
"roll": -2.2561967939168364,
"pitch": 0.10968211192904707,
"yaw": 87.36599578819637
}
},
{
"id": 50,
"position_mm": [
0.0022646484430879354,
-0.007531278766691685,
-0.006839004810899496
],
"orientation_deg": {
"roll": -0.5572651387919108,
"pitch": -0.2540478856154707,
"yaw": 0.22792865561204573
}
},
{
"id": 71,
"position_mm": [
140.41845703125,
0.009271804243326187,
0.092652328312397
],
"orientation_deg": {
"roll": 0.9053221063108222,
"pitch": -2.1366085994661774,
"yaw": 1.5077277441713781
}
},
{
"id": 76,
"position_mm": [
-204.43634100254326,
-90.3850951365776,
-94.52184634518557
],
"orientation_deg": {
"roll": -52.67872835725091,
"pitch": 39.27299014262584,
"yaw": -87.21449748201128
}
},
{
"id": 101,
"position_mm": [
0.01583130471408367,
-80.02842712402344,
-0.02530261129140854
],
"orientation_deg": {
"roll": 1.0558520137504372,
"pitch": -2.2130377151018776,
"yaw": 0.2601617066921372
}
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -1,7 +0,0 @@
id,x_mm,y_mm,z_mm,roll_deg,pitch_deg,yaw_deg
camera1,744.393,-11.882,212.666,-115.660,2.222,96.551
camera2,527.995,-443.448,881.443,-149.405,-0.614,30.363
25,146.987,-219.644,-14.453,-1.475,-0.713,75.762
50,-1.518,0.089,-0.276,-0.596,-1.297,0.062
71,139.840,0.042,-0.031,0.036,-1.975,-0.196
101,-2.288,-80.358,-0.673,0.487,-1.568,-0.808
1 id x_mm y_mm z_mm roll_deg pitch_deg yaw_deg
2 camera1 744.393 -11.882 212.666 -115.660 2.222 96.551
3 camera2 527.995 -443.448 881.443 -149.405 -0.614 30.363
4 25 146.987 -219.644 -14.453 -1.475 -0.713 75.762
5 50 -1.518 0.089 -0.276 -0.596 -1.297 0.062
6 71 139.840 0.042 -0.031 0.036 -1.975 -0.196
7 101 -2.288 -80.358 -0.673 0.487 -1.568 -0.808

View File

@@ -1,97 +0,0 @@
{
"metadata": {
"timestamp": "2025-12-17 12:40:45",
"reference_markers": [
50,
71,
101
],
"dict": "DICT_4X4_250",
"marker_size_mm": 25.0,
"rms_refs_px_cam1": 0.45907110184149624,
"rms_refs_px_cam2": 1.8220535514255254,
"description": "Two-camera joint optimization with triangulation"
},
"cameras": [
{
"id": "camera1",
"position_mm": [
744.3932024096008,
-11.882499535240939,
212.66641344590275
],
"orientation_deg": {
"roll": -115.66036055568036,
"pitch": 2.2216915445453633,
"yaw": 96.55059294849401
}
},
{
"id": "camera2",
"position_mm": [
527.9947219168113,
-443.4476403624862,
881.4430227800225
],
"orientation_deg": {
"roll": -149.4054202299282,
"pitch": -0.6136995548592902,
"yaw": 30.363222205495898
}
}
],
"markers": [
{
"id": 25,
"position_mm": [
146.98660278320312,
-219.6441650390625,
-14.453299522399902
],
"orientation_deg": {
"roll": -1.4751980332876062,
"pitch": -0.7130443690892287,
"yaw": 75.76221922077491
}
},
{
"id": 50,
"position_mm": [
-1.5184317827224731,
0.08936363458633423,
-0.27635297179222107
],
"orientation_deg": {
"roll": -0.5960699180502376,
"pitch": -1.296579669039671,
"yaw": 0.06174649369085999
}
},
{
"id": 71,
"position_mm": [
139.83982849121094,
0.04223456606268883,
-0.03088134340941906
],
"orientation_deg": {
"roll": 0.03560029736445153,
"pitch": -1.9746336879120467,
"yaw": -0.1957084044000817
}
},
{
"id": 101,
"position_mm": [
-2.2875027656555176,
-80.35816192626953,
-0.673177182674408
],
"orientation_deg": {
"roll": 0.4866379194238045,
"pitch": -1.5677675033219693,
"yaw": -0.8081315765533771
}
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

View File

@@ -1,8 +0,0 @@
id,x_mm,y_mm,z_mm,roll_deg,pitch_deg,yaw_deg
camera1,498.210,88.623,206.052,-119.838,2.196,102.110
camera2,334.074,-396.876,854.525,-147.250,-0.513,39.282
25,-93.825,-211.994,-12.439,-1.046,-0.406,84.805
50,-0.004,0.003,0.012,-0.106,-0.397,-0.042
71,140.045,-0.037,-0.014,0.502,-1.471,0.890
76,-347.458,-82.554,-72.496,-4.794,0.320,-131.339
101,-0.044,-80.081,-0.025,1.333,-0.936,-0.171
1 id x_mm y_mm z_mm roll_deg pitch_deg yaw_deg
2 camera1 498.210 88.623 206.052 -119.838 2.196 102.110
3 camera2 334.074 -396.876 854.525 -147.250 -0.513 39.282
4 25 -93.825 -211.994 -12.439 -1.046 -0.406 84.805
5 50 -0.004 0.003 0.012 -0.106 -0.397 -0.042
6 71 140.045 -0.037 -0.014 0.502 -1.471 0.890
7 76 -347.458 -82.554 -72.496 -4.794 0.320 -131.339
8 101 -0.044 -80.081 -0.025 1.333 -0.936 -0.171

View File

@@ -1,110 +0,0 @@
{
"metadata": {
"timestamp": "2025-12-17 12:43:26",
"reference_markers": [
50,
71,
101
],
"dict": "DICT_4X4_250",
"marker_size_mm": 25.0,
"rms_refs_px_cam1": 0.0689596563850227,
"rms_refs_px_cam2": 0.15658089852267823,
"description": "Two-camera joint optimization with triangulation"
},
"cameras": [
{
"id": "camera1",
"position_mm": [
498.210011232767,
88.62307649604217,
206.05227415389444
],
"orientation_deg": {
"roll": -119.83827305973767,
"pitch": 2.1955471129077124,
"yaw": 102.10968938445795
}
},
{
"id": "camera2",
"position_mm": [
334.0739312137807,
-396.87615758651145,
854.5248655333136
],
"orientation_deg": {
"roll": -147.24989987242014,
"pitch": -0.5126818772143938,
"yaw": 39.28200265150137
}
}
],
"markers": [
{
"id": 25,
"position_mm": [
-93.8246078491211,
-211.99365234375,
-12.43923568725586
],
"orientation_deg": {
"roll": -1.0455208006289969,
"pitch": -0.405997221444331,
"yaw": 84.80479451113972
}
},
{
"id": 50,
"position_mm": [
-0.004142458084970713,
0.0031922683119773865,
0.011907931417226791
],
"orientation_deg": {
"roll": -0.10642453044301076,
"pitch": -0.3968296567842359,
"yaw": -0.04171026817406525
}
},
{
"id": 71,
"position_mm": [
140.0451202392578,
-0.037084344774484634,
-0.014320459216833115
],
"orientation_deg": {
"roll": 0.5023078026002353,
"pitch": -1.4710306891384899,
"yaw": 0.8898461679228588
}
},
{
"id": 76,
"position_mm": [
-347.4584356767253,
-82.55446010770595,
-72.49557614124402
],
"orientation_deg": {
"roll": -4.793917356113556,
"pitch": 0.3203544844260641,
"yaw": -131.3390863237459
}
},
{
"id": 101,
"position_mm": [
-0.043663639575242996,
-80.08061218261719,
-0.024777762591838837
],
"orientation_deg": {
"roll": 1.3330315657696694,
"pitch": -0.9363310058749773,
"yaw": -0.17089002041329374
}
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

View File

@@ -1,8 +0,0 @@
id,x_mm,y_mm,z_mm,roll_deg,pitch_deg,yaw_deg
camera1,499.822,88.859,207.627,-119.927,2.190,102.072
camera2,324.617,-392.143,862.639,-147.982,-0.790,39.250
25,-93.860,-212.481,-12.595,-1.949,-0.269,83.807
50,-0.004,0.003,0.012,-0.258,-0.650,0.428
71,140.046,-0.037,-0.013,0.306,-2.118,0.609
76,-340.361,-86.238,-67.321,-4.926,2.286,-131.705
101,-0.043,-80.081,-0.024,1.357,-1.022,-0.205
1 id x_mm y_mm z_mm roll_deg pitch_deg yaw_deg
2 camera1 499.822 88.859 207.627 -119.927 2.190 102.072
3 camera2 324.617 -392.143 862.639 -147.982 -0.790 39.250
4 25 -93.860 -212.481 -12.595 -1.949 -0.269 83.807
5 50 -0.004 0.003 0.012 -0.258 -0.650 0.428
6 71 140.046 -0.037 -0.013 0.306 -2.118 0.609
7 76 -340.361 -86.238 -67.321 -4.926 2.286 -131.705
8 101 -0.043 -80.081 -0.024 1.357 -1.022 -0.205

View File

@@ -1,110 +0,0 @@
{
"metadata": {
"timestamp": "2025-12-17 12:45:50",
"reference_markers": [
50,
71,
101
],
"dict": "DICT_4X4_250",
"marker_size_mm": 25.0,
"rms_refs_px_cam1": 0.06868263740095656,
"rms_refs_px_cam2": 0.15630357297595754,
"description": "Two-camera joint optimization with triangulation"
},
"cameras": [
{
"id": "camera1",
"position_mm": [
499.822142506545,
88.85939416980528,
207.62721199733437
],
"orientation_deg": {
"roll": -119.92745477596469,
"pitch": 2.190488293707874,
"yaw": 102.07241332921645
}
},
{
"id": "camera2",
"position_mm": [
324.6169326616307,
-392.1429491628629,
862.638817313126
],
"orientation_deg": {
"roll": -147.98179815319492,
"pitch": -0.7895678131424055,
"yaw": 39.24991008226242
}
}
],
"markers": [
{
"id": 25,
"position_mm": [
-93.85962677001953,
-212.4806365966797,
-12.594773292541504
],
"orientation_deg": {
"roll": -1.9485429121680113,
"pitch": -0.2687673923849189,
"yaw": 83.80737730916069
}
},
{
"id": 50,
"position_mm": [
-0.003994773142039776,
0.003109264886006713,
0.012178612872958183
],
"orientation_deg": {
"roll": -0.25790734082294964,
"pitch": -0.6498026480254269,
"yaw": 0.428441829560328
}
},
{
"id": 71,
"position_mm": [
140.04592895507812,
-0.03738624230027199,
-0.01319770235568285
],
"orientation_deg": {
"roll": 0.3055486706690755,
"pitch": -2.1177898361268723,
"yaw": 0.6091614832951404
}
},
{
"id": 76,
"position_mm": [
-340.36073770675137,
-86.23836578004335,
-67.32056820469123
],
"orientation_deg": {
"roll": -4.926387915431779,
"pitch": 2.2860961549797616,
"yaw": -131.70545931002823
}
},
{
"id": 101,
"position_mm": [
-0.04275330901145935,
-80.08077239990234,
-0.023683607578277588
],
"orientation_deg": {
"roll": 1.356930847634746,
"pitch": -1.0223328395484648,
"yaw": -0.20516192462359795
}
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

View File

@@ -1,8 +0,0 @@
id,x_mm,y_mm,z_mm,roll_deg,pitch_deg,yaw_deg
camera1,395.016,274.958,206.045,-119.984,2.814,119.358
camera2,359.471,-267.295,863.813,-147.768,-2.538,55.530
25,-78.352,-188.494,-11.881,-1.886,0.748,102.089
50,0.003,-0.035,-0.029,-0.393,-0.996,1.561
71,140.373,-0.216,-0.009,0.798,-1.562,1.944
76,-459.707,-170.694,-97.767,-2.463,3.181,-112.929
101,0.061,-80.295,0.055,0.729,-1.048,0.758
1 id x_mm y_mm z_mm roll_deg pitch_deg yaw_deg
2 camera1 395.016 274.958 206.045 -119.984 2.814 119.358
3 camera2 359.471 -267.295 863.813 -147.768 -2.538 55.530
4 25 -78.352 -188.494 -11.881 -1.886 0.748 102.089
5 50 0.003 -0.035 -0.029 -0.393 -0.996 1.561
6 71 140.373 -0.216 -0.009 0.798 -1.562 1.944
7 76 -459.707 -170.694 -97.767 -2.463 3.181 -112.929
8 101 0.061 -80.295 0.055 0.729 -1.048 0.758

View File

@@ -1,110 +0,0 @@
{
"metadata": {
"timestamp": "2025-12-17 12:51:11",
"reference_markers": [
50,
71,
101
],
"dict": "DICT_4X4_250",
"marker_size_mm": 25.0,
"rms_refs_px_cam1": 0.6323697652065153,
"rms_refs_px_cam2": 0.6489014675423443,
"description": "Two-camera joint optimization with triangulation"
},
"cameras": [
{
"id": "camera1",
"position_mm": [
395.01563676863606,
274.9583596464833,
206.04475397752364
],
"orientation_deg": {
"roll": -119.98431855701496,
"pitch": 2.8142321507417067,
"yaw": 119.35809543020302
}
},
{
"id": "camera2",
"position_mm": [
359.47129678000016,
-267.2947756235871,
863.8126650240622
],
"orientation_deg": {
"roll": -147.76809010492943,
"pitch": -2.5376982238056183,
"yaw": 55.52992636638746
}
}
],
"markers": [
{
"id": 25,
"position_mm": [
-78.35247802734375,
-188.4938201904297,
-11.880936622619629
],
"orientation_deg": {
"roll": -1.885989573087267,
"pitch": 0.7475050867613423,
"yaw": 102.08882951321951
}
},
{
"id": 50,
"position_mm": [
0.002867116592824459,
-0.034943774342536926,
-0.02899026684463024
],
"orientation_deg": {
"roll": -0.39255711964707385,
"pitch": -0.9959750123875623,
"yaw": 1.5614975288119828
}
},
{
"id": 71,
"position_mm": [
140.3726043701172,
-0.21625664830207825,
-0.00919211097061634
],
"orientation_deg": {
"roll": 0.7984134246053606,
"pitch": -1.562078718233375,
"yaw": 1.9439997964972604
}
},
{
"id": 76,
"position_mm": [
-459.70712987851766,
-170.69410468938494,
-97.76705043570367
],
"orientation_deg": {
"roll": -2.4626449410414946,
"pitch": 3.1808466940482707,
"yaw": -112.9286987372601
}
},
{
"id": 101,
"position_mm": [
0.06145252659916878,
-80.29531860351562,
0.055326566100120544
],
"orientation_deg": {
"roll": 0.7289847565857819,
"pitch": -1.0475664492494143,
"yaw": 0.7580539936823688
}
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -1,8 +0,0 @@
id,x_mm,y_mm,z_mm,roll_deg,pitch_deg,yaw_deg
camera1,536.560,19.088,206.377,-106.622,2.737,89.850
camera2,265.697,-417.719,863.004,-147.946,-0.838,27.172
25,-105.950,-153.015,-13.182,-0.890,-0.149,73.247
50,-0.040,-0.018,-0.033,-0.879,-1.449,-0.958
71,140.010,-0.012,-0.131,0.402,-1.339,-0.189
76,-426.751,36.837,-80.533,-2.174,0.185,-140.318
101,-0.154,-80.157,-0.149,1.401,-1.417,-0.671
1 id x_mm y_mm z_mm roll_deg pitch_deg yaw_deg
2 camera1 536.560 19.088 206.377 -106.622 2.737 89.850
3 camera2 265.697 -417.719 863.004 -147.946 -0.838 27.172
4 25 -105.950 -153.015 -13.182 -0.890 -0.149 73.247
5 50 -0.040 -0.018 -0.033 -0.879 -1.449 -0.958
6 71 140.010 -0.012 -0.131 0.402 -1.339 -0.189
7 76 -426.751 36.837 -80.533 -2.174 0.185 -140.318
8 101 -0.154 -80.157 -0.149 1.401 -1.417 -0.671

View File

@@ -1,110 +0,0 @@
{
"metadata": {
"timestamp": "2025-12-17 12:52:57",
"reference_markers": [
50,
71,
101
],
"dict": "DICT_4X4_250",
"marker_size_mm": 25.0,
"rms_refs_px_cam1": 0.24427057867694885,
"rms_refs_px_cam2": 0.27827582635161247,
"description": "Two-camera joint optimization with triangulation"
},
"cameras": [
{
"id": "camera1",
"position_mm": [
536.5599698999005,
19.08812610913911,
206.37737338942568
],
"orientation_deg": {
"roll": -106.62238029916044,
"pitch": 2.73698636164054,
"yaw": 89.84958179049634
}
},
{
"id": "camera2",
"position_mm": [
265.696908061668,
-417.71859377662963,
863.0037755691174
],
"orientation_deg": {
"roll": -147.94607328148066,
"pitch": -0.8376675719210364,
"yaw": 27.17235921730388
}
}
],
"markers": [
{
"id": 25,
"position_mm": [
-105.95045471191406,
-153.015380859375,
-13.182010650634766
],
"orientation_deg": {
"roll": -0.8902363626829162,
"pitch": -0.14934531032116036,
"yaw": 73.24720041399077
}
},
{
"id": 50,
"position_mm": [
-0.040261898189783096,
-0.018160216510295868,
-0.032540079206228256
],
"orientation_deg": {
"roll": -0.8788475817184255,
"pitch": -1.4493986331576718,
"yaw": -0.9580865386218039
}
},
{
"id": 71,
"position_mm": [
140.0104522705078,
-0.011823506094515324,
-0.13130724430084229
],
"orientation_deg": {
"roll": 0.40245628021742036,
"pitch": -1.339452511723197,
"yaw": -0.18891338256930315
}
},
{
"id": 76,
"position_mm": [
-426.75066384529714,
36.836556193528715,
-80.53298420815302
],
"orientation_deg": {
"roll": -2.1735863467636007,
"pitch": 0.18540893221631718,
"yaw": -140.3178431469828
}
},
{
"id": 101,
"position_mm": [
-0.15381859242916107,
-80.15699005126953,
-0.14876669645309448
],
"orientation_deg": {
"roll": 1.401434331782987,
"pitch": -1.417352777188989,
"yaw": -0.6705647440036173
}
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -1,8 +0,0 @@
id,x_mm,y_mm,z_mm,roll_deg,pitch_deg,yaw_deg
camera1,536.132,19.475,207.265,-106.720,2.751,89.881
camera2,268.384,-412.214,863.776,-148.162,-0.584,27.306
25,-106.039,-153.152,-13.340,148.790,18.375,132.294
50,-0.040,-0.018,-0.032,-0.891,-1.547,-0.922
71,140.010,-0.012,-0.133,0.607,-1.246,0.187
76,-427.186,35.671,-80.653,-2.018,-0.109,-140.186
101,-0.155,-80.158,-0.149,1.389,-1.515,-0.634
1 id x_mm y_mm z_mm roll_deg pitch_deg yaw_deg
2 camera1 536.132 19.475 207.265 -106.720 2.751 89.881
3 camera2 268.384 -412.214 863.776 -148.162 -0.584 27.306
4 25 -106.039 -153.152 -13.340 148.790 18.375 132.294
5 50 -0.040 -0.018 -0.032 -0.891 -1.547 -0.922
6 71 140.010 -0.012 -0.133 0.607 -1.246 0.187
7 76 -427.186 35.671 -80.653 -2.018 -0.109 -140.186
8 101 -0.155 -80.158 -0.149 1.389 -1.515 -0.634

View File

@@ -1,110 +0,0 @@
{
"metadata": {
"timestamp": "2025-12-17 12:53:12",
"reference_markers": [
50,
71,
101
],
"dict": "DICT_4X4_250",
"marker_size_mm": 25.0,
"rms_refs_px_cam1": 0.246809770178914,
"rms_refs_px_cam2": 0.27880768692475444,
"description": "Two-camera joint optimization with triangulation"
},
"cameras": [
{
"id": "camera1",
"position_mm": [
536.1318738587609,
19.474736168499103,
207.26462795091985
],
"orientation_deg": {
"roll": -106.7203897853097,
"pitch": 2.7507012808548144,
"yaw": 89.88105336644603
}
},
{
"id": "camera2",
"position_mm": [
268.3843515238169,
-412.2144214770623,
863.7760290780072
],
"orientation_deg": {
"roll": -148.1621979629559,
"pitch": -0.5838459897877972,
"yaw": 27.306333846282612
}
}
],
"markers": [
{
"id": 25,
"position_mm": [
-106.03853607177734,
-153.15234375,
-13.33987045288086
],
"orientation_deg": {
"roll": 148.7903066118052,
"pitch": 18.37471840944555,
"yaw": 132.29369679166456
}
},
{
"id": 50,
"position_mm": [
-0.04027938097715378,
-0.018168868497014046,
-0.03249623626470566
],
"orientation_deg": {
"roll": -0.8911896876960713,
"pitch": -1.5474788147950507,
"yaw": -0.9216004257713646
}
},
{
"id": 71,
"position_mm": [
140.00997924804688,
-0.011874680407345295,
-0.13304609060287476
],
"orientation_deg": {
"roll": 0.6066041335238053,
"pitch": -1.2456305272454657,
"yaw": 0.1873499158076866
}
},
{
"id": 76,
"position_mm": [
-427.1856256100377,
35.67064810704457,
-80.65343499481803
],
"orientation_deg": {
"roll": -2.0175816127635513,
"pitch": -0.10919350640776791,
"yaw": -140.1864457356773
}
},
{
"id": 101,
"position_mm": [
-0.15468710660934448,
-80.15770721435547,
-0.1492648720741272
],
"orientation_deg": {
"roll": 1.3886002098868537,
"pitch": -1.5153698095294952,
"yaw": -0.6340729479437016
}
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -1,8 +0,0 @@
id,x_mm,y_mm,z_mm,roll_deg,pitch_deg,yaw_deg
camera1,536.611,18.820,206.461,-106.626,2.661,89.850
camera2,268.384,-412.214,863.776,-148.162,-0.584,27.306
25,-105.905,-153.052,-13.330,148.800,18.245,132.261
50,-0.040,-0.018,-0.032,-0.803,-1.452,-0.960
71,140.010,-0.012,-0.132,1.207,-1.367,-0.158
76,-427.186,35.671,-80.653,-2.018,-0.109,-140.186
101,-0.155,-80.158,-0.149,0.999,-1.889,-0.671
1 id x_mm y_mm z_mm roll_deg pitch_deg yaw_deg
2 camera1 536.611 18.820 206.461 -106.626 2.661 89.850
3 camera2 268.384 -412.214 863.776 -148.162 -0.584 27.306
4 25 -105.905 -153.052 -13.330 148.800 18.245 132.261
5 50 -0.040 -0.018 -0.032 -0.803 -1.452 -0.960
6 71 140.010 -0.012 -0.132 1.207 -1.367 -0.158
7 76 -427.186 35.671 -80.653 -2.018 -0.109 -140.186
8 101 -0.155 -80.158 -0.149 0.999 -1.889 -0.671

View File

@@ -1,110 +0,0 @@
{
"metadata": {
"timestamp": "2025-12-17 12:53:25",
"reference_markers": [
50,
71,
101
],
"dict": "DICT_4X4_250",
"marker_size_mm": 25.0,
"rms_refs_px_cam1": 0.2441583102735186,
"rms_refs_px_cam2": 0.27880768692475444,
"description": "Two-camera joint optimization with triangulation"
},
"cameras": [
{
"id": "camera1",
"position_mm": [
536.6112531658491,
18.81997611440784,
206.46065124784747
],
"orientation_deg": {
"roll": -106.6257561255132,
"pitch": 2.6610643225108683,
"yaw": 89.84975075090361
}
},
{
"id": "camera2",
"position_mm": [
268.3843515238169,
-412.2144214770623,
863.7760290780072
],
"orientation_deg": {
"roll": -148.1621979629559,
"pitch": -0.5838459897877972,
"yaw": 27.306333846282612
}
}
],
"markers": [
{
"id": 25,
"position_mm": [
-105.90450286865234,
-153.0524444580078,
-13.329595565795898
],
"orientation_deg": {
"roll": 148.80013975261303,
"pitch": 18.24478302172668,
"yaw": 132.26101573505235
}
},
{
"id": 50,
"position_mm": [
-0.04021511226892471,
-0.018157418817281723,
-0.032385922968387604
],
"orientation_deg": {
"roll": -0.8028611672479457,
"pitch": -1.4516990130367182,
"yaw": -0.9596821440114531
}
},
{
"id": 71,
"position_mm": [
140.010009765625,
-0.011915921233594418,
-0.13167670369148254
],
"orientation_deg": {
"roll": 1.20741490711664,
"pitch": -1.3669496548399305,
"yaw": -0.1576632529292416
}
},
{
"id": 76,
"position_mm": [
-427.1856256100377,
35.67064810704457,
-80.65343499481803
],
"orientation_deg": {
"roll": -2.0175816127635513,
"pitch": -0.10919350640776791,
"yaw": -140.1864457356773
}
},
{
"id": 101,
"position_mm": [
-0.15451067686080933,
-80.15750122070312,
-0.14881648123264313
],
"orientation_deg": {
"roll": 0.9986347088612523,
"pitch": -1.8894700986904136,
"yaw": -0.6714546601080824
}
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -1,8 +0,0 @@
id,x_mm,y_mm,z_mm,roll_deg,pitch_deg,yaw_deg
camera1,536.485,18.839,206.779,-106.660,2.662,89.850
camera2,270.078,-416.213,861.643,-147.875,-0.599,27.290
25,-106.002,-152.845,-13.395,-1.656,-0.260,74.773
50,-0.040,-0.018,-0.033,-0.803,-1.486,-0.958
71,140.010,-0.012,-0.132,0.125,-0.986,-0.226
76,-427.338,36.154,-79.282,-2.295,-0.032,-140.199
101,-0.155,-80.157,-0.149,0.998,-1.924,-0.670
1 id x_mm y_mm z_mm roll_deg pitch_deg yaw_deg
2 camera1 536.485 18.839 206.779 -106.660 2.662 89.850
3 camera2 270.078 -416.213 861.643 -147.875 -0.599 27.290
4 25 -106.002 -152.845 -13.395 -1.656 -0.260 74.773
5 50 -0.040 -0.018 -0.033 -0.803 -1.486 -0.958
6 71 140.010 -0.012 -0.132 0.125 -0.986 -0.226
7 76 -427.338 36.154 -79.282 -2.295 -0.032 -140.199
8 101 -0.155 -80.157 -0.149 0.998 -1.924 -0.670

View File

@@ -1,110 +0,0 @@
{
"metadata": {
"timestamp": "2025-12-17 12:54:28",
"reference_markers": [
50,
71,
101
],
"dict": "DICT_4X4_250",
"marker_size_mm": 25.0,
"rms_refs_px_cam1": 0.24499340599673408,
"rms_refs_px_cam2": 0.27883589939730147,
"description": "Two-camera joint optimization with triangulation"
},
"cameras": [
{
"id": "camera1",
"position_mm": [
536.4845116439085,
18.839229807721747,
206.77874626303299
],
"orientation_deg": {
"roll": -106.65991480567813,
"pitch": 2.6616683186812895,
"yaw": 89.84996530221618
}
},
{
"id": "camera2",
"position_mm": [
270.0780494525769,
-416.21266285199596,
861.643153883761
],
"orientation_deg": {
"roll": -147.87452698748913,
"pitch": -0.598847887585048,
"yaw": 27.289970873393735
}
}
],
"markers": [
{
"id": 25,
"position_mm": [
-106.00161743164062,
-152.84478759765625,
-13.39499282836914
],
"orientation_deg": {
"roll": -1.6559759933266671,
"pitch": -0.2604845235747343,
"yaw": 74.7725286291638
}
},
{
"id": 50,
"position_mm": [
-0.040440429002046585,
-0.018118301406502724,
-0.03263949975371361
],
"orientation_deg": {
"roll": -0.8029835843019023,
"pitch": -1.4858259759073,
"yaw": -0.9578783681599231
}
},
{
"id": 71,
"position_mm": [
140.0098114013672,
-0.011588250286877155,
-0.13215307891368866
],
"orientation_deg": {
"roll": 0.12497030142085656,
"pitch": -0.9856255500173754,
"yaw": -0.2263123773485252
}
},
{
"id": 76,
"position_mm": [
-427.3379087741667,
36.154104540318976,
-79.28241714985451
],
"orientation_deg": {
"roll": -2.2951599547424033,
"pitch": -0.03224401177592252,
"yaw": -140.19949710822343
}
},
{
"id": 101,
"position_mm": [
-0.1550489366054535,
-80.1570816040039,
-0.14949001371860504
],
"orientation_deg": {
"roll": 0.9983404931317552,
"pitch": -1.9235960141180923,
"yaw": -0.6696442337381485
}
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -1,8 +0,0 @@
id,x_mm,y_mm,z_mm,roll_deg,pitch_deg,yaw_deg
camera1,504.143,-126.269,207.847,-108.968,2.548,85.122
camera2,270.078,-416.213,861.643,-147.875,-0.599,27.290
25,-106.269,-152.549,-13.850,-1.588,-0.375,72.807
50,-0.060,0.071,-0.102,-0.330,-0.119,-0.470
71,139.927,0.527,-0.717,0.185,-1.331,-0.473
76,-427.338,36.154,-79.282,-2.295,-0.032,-140.199
101,-0.131,-80.113,-0.116,2.341,-2.488,-1.552
1 id x_mm y_mm z_mm roll_deg pitch_deg yaw_deg
2 camera1 504.143 -126.269 207.847 -108.968 2.548 85.122
3 camera2 270.078 -416.213 861.643 -147.875 -0.599 27.290
4 25 -106.269 -152.549 -13.850 -1.588 -0.375 72.807
5 50 -0.060 0.071 -0.102 -0.330 -0.119 -0.470
6 71 139.927 0.527 -0.717 0.185 -1.331 -0.473
7 76 -427.338 36.154 -79.282 -2.295 -0.032 -140.199
8 101 -0.131 -80.113 -0.116 2.341 -2.488 -1.552

View File

@@ -1,110 +0,0 @@
{
"metadata": {
"timestamp": "2025-12-17 12:56:33",
"reference_markers": [
50,
71,
101
],
"dict": "DICT_4X4_250",
"marker_size_mm": 25.0,
"rms_refs_px_cam1": 1.6352803769089361,
"rms_refs_px_cam2": 0.27883589939730147,
"description": "Two-camera joint optimization with triangulation"
},
"cameras": [
{
"id": "camera1",
"position_mm": [
504.1434070874974,
-126.26880184835102,
207.84717134607124
],
"orientation_deg": {
"roll": -108.96805282221369,
"pitch": 2.5480630503117383,
"yaw": 85.1220631505837
}
},
{
"id": "camera2",
"position_mm": [
270.0780494525769,
-416.21266285199596,
861.643153883761
],
"orientation_deg": {
"roll": -147.87452698748913,
"pitch": -0.598847887585048,
"yaw": 27.289970873393735
}
}
],
"markers": [
{
"id": 25,
"position_mm": [
-106.2688980102539,
-152.54945373535156,
-13.849575996398926
],
"orientation_deg": {
"roll": -1.5884234657772047,
"pitch": -0.3745665819559331,
"yaw": 72.80694636666057
}
},
{
"id": 50,
"position_mm": [
-0.0595768541097641,
0.07062801718711853,
-0.10162462294101715
],
"orientation_deg": {
"roll": -0.32981134855001165,
"pitch": -0.11945304564632257,
"yaw": -0.4700373514560054
}
},
{
"id": 71,
"position_mm": [
139.9272918701172,
0.5273333787918091,
-0.7168083786964417
],
"orientation_deg": {
"roll": 0.18548532711350751,
"pitch": -1.3314171147799696,
"yaw": -0.4728166142447713
}
},
{
"id": 76,
"position_mm": [
-427.3379087741667,
36.154104540318976,
-79.28241714985451
],
"orientation_deg": {
"roll": -2.2951599547424033,
"pitch": -0.03224401177592252,
"yaw": -140.19949710822343
}
},
{
"id": 101,
"position_mm": [
-0.13059137761592865,
-80.11270904541016,
-0.1164691299200058
],
"orientation_deg": {
"roll": 2.3411360398909618,
"pitch": -2.4881518135413483,
"yaw": -1.55157097952814
}
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -1,8 +0,0 @@
id,x_mm,y_mm,z_mm,roll_deg,pitch_deg,yaw_deg
camera1,504.045,-125.440,208.438,-109.022,2.699,85.139
camera2,266.610,-418.486,860.306,-147.821,-0.848,27.232
25,-140.292,-125.449,-97.313,2.157,-0.941,71.251
50,-0.060,0.071,-0.102,-0.757,-0.797,-0.990
71,139.929,0.526,-0.716,-0.325,-1.431,0.182
76,-433.531,41.246,-90.204,-2.749,1.515,-141.240
101,-0.131,-80.112,-0.117,1.895,-2.234,-2.024
1 id x_mm y_mm z_mm roll_deg pitch_deg yaw_deg
2 camera1 504.045 -125.440 208.438 -109.022 2.699 85.139
3 camera2 266.610 -418.486 860.306 -147.821 -0.848 27.232
4 25 -140.292 -125.449 -97.313 2.157 -0.941 71.251
5 50 -0.060 0.071 -0.102 -0.757 -0.797 -0.990
6 71 139.929 0.526 -0.716 -0.325 -1.431 0.182
7 76 -433.531 41.246 -90.204 -2.749 1.515 -141.240
8 101 -0.131 -80.112 -0.117 1.895 -2.234 -2.024

View File

@@ -1,110 +0,0 @@
{
"metadata": {
"timestamp": "2025-12-17 12:59:31",
"reference_markers": [
50,
71,
101
],
"dict": "DICT_4X4_250",
"marker_size_mm": 25.0,
"rms_refs_px_cam1": 1.6293581246511548,
"rms_refs_px_cam2": 0.2788366800146409,
"description": "Two-camera joint optimization with triangulation"
},
"cameras": [
{
"id": "camera1",
"position_mm": [
504.04506476827214,
-125.43959602101629,
208.43800067135305
],
"orientation_deg": {
"roll": -109.02184112523827,
"pitch": 2.698848284001251,
"yaw": 85.13910561624756
}
},
{
"id": "camera2",
"position_mm": [
266.6103702868428,
-418.48644100540105,
860.3060088273107
],
"orientation_deg": {
"roll": -147.82070047743667,
"pitch": -0.8481476074566079,
"yaw": 27.231785358346894
}
}
],
"markers": [
{
"id": 25,
"position_mm": [
-140.29239613974138,
-125.4489603029717,
-97.31344559021105
],
"orientation_deg": {
"roll": 2.1567827057244413,
"pitch": -0.9412219288956468,
"yaw": 71.25097767397922
}
},
{
"id": 50,
"position_mm": [
-0.05961627885699272,
0.07056063413619995,
-0.10187845677137375
],
"orientation_deg": {
"roll": -0.7565047680938701,
"pitch": -0.7966906440544459,
"yaw": -0.9895245611569333
}
},
{
"id": 71,
"position_mm": [
139.9290313720703,
0.5259146094322205,
-0.7162161469459534
],
"orientation_deg": {
"roll": -0.32549591394932065,
"pitch": -1.4313243306475538,
"yaw": 0.18202150530705868
}
},
{
"id": 76,
"position_mm": [
-433.53071815447515,
41.246156509784306,
-90.20381677655665
],
"orientation_deg": {
"roll": -2.749193832463117,
"pitch": 1.5148995579585318,
"yaw": -141.24013438395986
}
},
{
"id": 101,
"position_mm": [
-0.13099518418312073,
-80.1122055053711,
-0.11726970225572586
],
"orientation_deg": {
"roll": 1.8945374923981935,
"pitch": -2.233677355990763,
"yaw": -2.0235970532528627
}
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

Some files were not shown because too many files have changed in this diff Show More