Commit 557ea847 authored by Andrey Filippov's avatar Andrey Filippov

Working on global LMA pose fitting and autonomous program run by the

agent.
parent 85eab198
......@@ -21,6 +21,12 @@ HTTP MCP endpoints and usage notes for controlling imagej-elphel via MCP.
Path: `scripts/mcp-http-howto.md`
### eyesis-host-daemon-howto.md
Host daemon workflow that removes repeated approval prompts by using a local file queue.
Path: `scripts/eyesis-host-daemon-howto.md`
### email_send.py
Send email via SSH+sendmail on community.elphel.com by default (set ELPHEL_SEND_MODE=smtp to force SMTP).
......@@ -185,3 +191,132 @@ Dependencies:
- rag venv
Tags: rag, query, wrapper
## eyesis_mcp_ctl.sh
Lifecycle controller for Eyesis_Correction in MCP mode (start/stop/restart/status/logs/wait).
Path: `scripts/eyesis_mcp_ctl.sh`
Example:
```bash
/home/elphel/git/imagej-elphel/scripts/eyesis_mcp_ctl.sh start --configdir /media/elphel/btrfs-data/lwir16-proc/NC/config --rebuild
```
Inputs:
- command: start|stop|restart|status|logs|wait
- optional flags: --configdir/--config/--port/--rebuild/--log
- environment overrides: EYESIS_MCP_*
Outputs:
- Background JVM process managed by pid file
- MCP readiness checks and status output
- Log file at attic/session-logs/eyesis_mcp.log by default
Dependencies:
- mvn
- curl
Tags: mcp, lifecycle, launcher, automation
## mcp_http.sh
Thin MCP HTTP wrapper for status/dialog/button/value/submit/interrupt operations.
Path: `scripts/mcp_http.sh`
Example:
```bash
/home/elphel/git/imagej-elphel/scripts/mcp_http.sh button --label "Restore"
```
Inputs:
- subcommand: status|dialog|button|set|submit|interrupt|confirm-stop
- subcommand flags like --label/--value/--id/--ok
- optional --base and --timeout (or EYESIS_MCP_BASE_URL/EYESIS_MCP_TIMEOUT_SEC)
Outputs:
- Raw JSON response from MCP HTTP endpoint
Dependencies:
- curl
Tags: mcp, http, automation, wrapper
## eyesis_host_daemon.py
Host-side queue daemon that executes whitelisted lifecycle/MCP wrapper commands in desktop session context.
Path: `scripts/eyesis_host_daemon.py`
Example:
```bash
/home/elphel/git/imagej-elphel/scripts/eyesis_host_daemon.py submit --action ctl -- status
```
Inputs:
- serve mode with queue dir and poll interval
- submit mode: action ctl|mcp and args after --
- read mode: request id
Outputs:
- JSON response files in queue responses/
- stdout/stderr passthrough for submit --wait-sec
Dependencies:
- python3
Tags: daemon, queue, automation, mcp, lifecycle
## eyesis_host_daemon_ctl.sh
Start/stop/status/logs helper for eyesis_host_daemon.py.
Path: `scripts/eyesis_host_daemon_ctl.sh`
Example:
```bash
/home/elphel/git/imagej-elphel/scripts/eyesis_host_daemon_ctl.sh start
```
Inputs:
- command: start|stop|restart|status|logs
- optional flags: --queue-dir/--log/--poll-ms/--force
Outputs:
- Background daemon process and PID file
- Daemon status and log streaming
Dependencies:
- python3
Tags: daemon, control, automation
## eyesis_host_submit.sh
Convenience wrapper to submit daemon requests for ctl/mcp actions.
Path: `scripts/eyesis_host_submit.sh`
Example:
```bash
/home/elphel/git/imagej-elphel/scripts/eyesis_host_submit.sh mcp status
```
Inputs:
- action: ctl or mcp
- subcommand arguments for selected action
Outputs:
- Command output from daemon response
Dependencies:
- python3
- eyesis_host_daemon.py
Tags: daemon, client, automation, mcp, lifecycle
......@@ -6,6 +6,11 @@
"path": "scripts/mcp-http-howto.md",
"purpose": "HTTP MCP endpoints and usage notes for controlling imagej-elphel via MCP."
},
{
"name": "eyesis-host-daemon-howto.md",
"path": "scripts/eyesis-host-daemon-howto.md",
"purpose": "Host daemon workflow that removes repeated approval prompts by using a local file queue."
},
{
"name": "email_send.py",
"path": "scripts/email_send.py",
......@@ -136,6 +141,110 @@
],
"owner": "codex",
"created": "2026-02-04"
},
{
"name": "eyesis_mcp_ctl.sh",
"path": "scripts/eyesis_mcp_ctl.sh",
"purpose": "Lifecycle controller for Eyesis_Correction in MCP mode (start/stop/restart/status/logs/wait).",
"inputs": [
"command: start|stop|restart|status|logs|wait",
"optional flags: --configdir/--config/--port/--rebuild/--log",
"environment overrides: EYESIS_MCP_*"
],
"outputs": [
"Background JVM process managed by pid file",
"MCP readiness checks and status output",
"Log file at attic/session-logs/eyesis_mcp.log by default"
],
"example": "/home/elphel/git/imagej-elphel/scripts/eyesis_mcp_ctl.sh start --configdir /media/elphel/btrfs-data/lwir16-proc/NC/config --rebuild",
"tags": ["mcp", "lifecycle", "launcher", "automation"],
"dependencies": [
"mvn",
"curl"
],
"owner": "codex",
"created": "2026-02-23"
},
{
"name": "mcp_http.sh",
"path": "scripts/mcp_http.sh",
"purpose": "Thin MCP HTTP wrapper for status/dialog/button/value/submit/interrupt operations.",
"inputs": [
"subcommand: status|dialog|button|set|submit|interrupt|confirm-stop",
"subcommand flags like --label/--value/--id/--ok",
"optional --base and --timeout (or EYESIS_MCP_BASE_URL/EYESIS_MCP_TIMEOUT_SEC)"
],
"outputs": [
"Raw JSON response from MCP HTTP endpoint"
],
"example": "/home/elphel/git/imagej-elphel/scripts/mcp_http.sh button --label \"Restore\"",
"tags": ["mcp", "http", "automation", "wrapper"],
"dependencies": [
"curl"
],
"owner": "codex",
"created": "2026-02-23"
},
{
"name": "eyesis_host_daemon.py",
"path": "scripts/eyesis_host_daemon.py",
"purpose": "Host-side queue daemon that executes whitelisted lifecycle/MCP wrapper commands in desktop session context.",
"inputs": [
"serve mode with queue dir and poll interval",
"submit mode: action ctl|mcp and args after --",
"read mode: request id"
],
"outputs": [
"JSON response files in queue responses/",
"stdout/stderr passthrough for submit --wait-sec"
],
"example": "/home/elphel/git/imagej-elphel/scripts/eyesis_host_daemon.py submit --action ctl -- status",
"tags": ["daemon", "queue", "automation", "mcp", "lifecycle"],
"dependencies": [
"python3"
],
"owner": "codex",
"created": "2026-02-23"
},
{
"name": "eyesis_host_daemon_ctl.sh",
"path": "scripts/eyesis_host_daemon_ctl.sh",
"purpose": "Start/stop/status/logs helper for eyesis_host_daemon.py.",
"inputs": [
"command: start|stop|restart|status|logs",
"optional flags: --queue-dir/--log/--poll-ms/--force"
],
"outputs": [
"Background daemon process and PID file",
"Daemon status and log streaming"
],
"example": "/home/elphel/git/imagej-elphel/scripts/eyesis_host_daemon_ctl.sh start",
"tags": ["daemon", "control", "automation"],
"dependencies": [
"python3"
],
"owner": "codex",
"created": "2026-02-23"
},
{
"name": "eyesis_host_submit.sh",
"path": "scripts/eyesis_host_submit.sh",
"purpose": "Convenience wrapper to submit daemon requests for ctl/mcp actions.",
"inputs": [
"action: ctl or mcp",
"subcommand arguments for selected action"
],
"outputs": [
"Command output from daemon response"
],
"example": "/home/elphel/git/imagej-elphel/scripts/eyesis_host_submit.sh mcp status",
"tags": ["daemon", "client", "automation", "mcp", "lifecycle"],
"dependencies": [
"python3",
"eyesis_host_daemon.py"
],
"owner": "codex",
"created": "2026-02-23"
}
]
}
# Eyesis Host Daemon HOWTO
This daemon avoids repeated Codex approval prompts by running host-side command execution in your desktop session.
Codex then submits requests through a file queue in the workspace.
## Why use it
- No per-command approval prompts after daemon is started.
- Works across machines with one-time startup.
- Keeps command scope restricted to existing wrappers:
- `scripts/eyesis_mcp_ctl.sh`
- `scripts/mcp_http.sh`
## Components
- `scripts/eyesis_host_daemon.py`:
- `serve`: daemon loop
- `submit`: enqueue a request and optionally wait for response
- `scripts/eyesis_host_daemon_ctl.sh`:
- start/stop/restart/status/logs for daemon itself
- `scripts/eyesis_host_submit.sh`:
- convenience wrapper around `submit`
## One-time start (per login)
```bash
/home/elphel/git/imagej-elphel/scripts/eyesis_host_daemon_ctl.sh start
```
Check status:
```bash
/home/elphel/git/imagej-elphel/scripts/eyesis_host_daemon_ctl.sh status
```
View daemon log:
```bash
/home/elphel/git/imagej-elphel/scripts/eyesis_host_daemon_ctl.sh logs --lines 120
```
Real-time tails in a separate console:
```bash
tail -n 200 -F /home/elphel/git/imagej-elphel/attic/session-logs/eyesis_mcp.log
tail -n 100 -F /home/elphel/git/imagej-elphel/attic/session-logs/eyesis_host_daemon.log
```
## Request examples
Daemon-backed lifecycle request:
```bash
/home/elphel/git/imagej-elphel/scripts/eyesis_host_submit.sh ctl status
/home/elphel/git/imagej-elphel/scripts/eyesis_host_submit.sh ctl start --configdir /media/elphel/btrfs-data/lwir16-proc/NC/config --wait-sec 120
```
Daemon-backed MCP request:
```bash
/home/elphel/git/imagej-elphel/scripts/eyesis_host_submit.sh mcp status
/home/elphel/git/imagej-elphel/scripts/eyesis_host_submit.sh mcp button --label "Restore"
```
## Queue and logs
Default queue directory:
- `attic/session-logs/eyesis-host-daemon/`
Subdirectories:
- `requests/` incoming jobs
- `processing/` in-progress jobs
- `responses/` completed results
- `archive/` processed request files
Daemon log:
- `attic/session-logs/eyesis_host_daemon.log`
Eyesis console log (from lifecycle script):
- `attic/session-logs/eyesis_mcp.log`
## Stop daemon
```bash
/home/elphel/git/imagej-elphel/scripts/eyesis_host_daemon_ctl.sh stop
```
Force stop if needed:
```bash
/home/elphel/git/imagej-elphel/scripts/eyesis_host_daemon_ctl.sh stop --force
```
## New computer setup
1. Copy/clone repo.
2. Build once (`mvn -DskipTests clean package`) from your normal user session.
3. Start daemon (`eyesis_host_daemon_ctl.sh start`).
4. Use `eyesis_host_submit.sh` for run control.
#!/usr/bin/env python3
"""
Host-side Eyesis automation daemon.
Purpose:
- Run this daemon once in the user's desktop session.
- Codex (sandboxed) submits JSON requests via files in a shared queue dir.
- Daemon executes a restricted command set on the host (GUI/session-visible context).
Usage:
scripts/eyesis_host_daemon.py serve [--queue-dir DIR] [--poll-ms 200]
scripts/eyesis_host_daemon.py submit --action {ctl,mcp} -- <args...> [--queue-dir DIR] [--wait-sec 120]
scripts/eyesis_host_daemon.py read --id REQUEST_ID [--queue-dir DIR]
Queue layout (default): attic/session-logs/eyesis-host-daemon/
requests/*.json
processing/*.json
responses/*.json
archive/*.json
"""
from __future__ import annotations
import argparse
import datetime as dt
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import time
import uuid
REPO_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_QUEUE = REPO_ROOT / "attic" / "session-logs" / "eyesis-host-daemon"
CTL_SCRIPT = REPO_ROOT / "scripts" / "eyesis_mcp_ctl.sh"
MCP_SCRIPT = REPO_ROOT / "scripts" / "mcp_http.sh"
ALLOWED_CTL = {"start", "stop", "restart", "status", "wait", "logs"}
ALLOWED_MCP = {"status", "dialog", "button", "set", "submit", "interrupt", "confirm-stop"}
def now_iso() -> str:
return dt.datetime.now(dt.timezone.utc).isoformat()
def ensure_dirs(base: Path) -> dict[str, Path]:
dirs = {
"base": base,
"requests": base / "requests",
"processing": base / "processing",
"responses": base / "responses",
"archive": base / "archive",
}
for p in dirs.values():
p.mkdir(parents=True, exist_ok=True)
return dirs
def atomic_write_json(path: Path, payload: dict) -> None:
tmp = path.with_suffix(path.suffix + ".tmp")
tmp.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
os.replace(tmp, path)
def validate_request(req: dict) -> tuple[bool, str]:
action = req.get("action")
args = req.get("args")
if action not in {"ctl", "mcp"}:
return False, "action must be 'ctl' or 'mcp'"
if not isinstance(args, list) or not all(isinstance(x, str) for x in args):
return False, "args must be a list of strings"
if not args:
return False, "args must not be empty"
first = args[0]
if action == "ctl" and first not in ALLOWED_CTL:
return False, f"unsupported ctl subcommand: {first}"
if action == "mcp" and first not in ALLOWED_MCP:
return False, f"unsupported mcp subcommand: {first}"
return True, ""
def build_command(action: str, args: list[str]) -> list[str]:
if action == "ctl":
return [str(CTL_SCRIPT), *args]
return [str(MCP_SCRIPT), *args]
def process_one(req_path: Path, dirs: dict[str, Path]) -> None:
proc_path = dirs["processing"] / req_path.name
os.replace(req_path, proc_path)
req_id = req_path.stem
started = now_iso()
result: dict = {
"id": req_id,
"started_at": started,
}
try:
req = json.loads(proc_path.read_text(encoding="utf-8"))
result["request"] = req
ok, err = validate_request(req)
if not ok:
result.update({"ok": False, "exit_code": 2, "error": err, "stdout": "", "stderr": ""})
else:
timeout = req.get("timeout_sec", 300)
if not isinstance(timeout, int) or timeout < 1 or timeout > 3600:
timeout = 300
cmd = build_command(req["action"], req["args"])
cp = subprocess.run(
cmd,
cwd=str(REPO_ROOT),
text=True,
capture_output=True,
timeout=timeout,
check=False,
)
result.update(
{
"ok": cp.returncode == 0,
"exit_code": cp.returncode,
"stdout": cp.stdout,
"stderr": cp.stderr,
"command": cmd,
}
)
except subprocess.TimeoutExpired as e:
result.update(
{
"ok": False,
"exit_code": 124,
"error": f"timeout after {e.timeout}s",
"stdout": e.stdout or "",
"stderr": e.stderr or "",
}
)
except Exception as e:
result.update(
{
"ok": False,
"exit_code": 1,
"error": f"daemon exception: {e}",
"stdout": "",
"stderr": "",
}
)
finally:
result["finished_at"] = now_iso()
resp_path = dirs["responses"] / f"{req_id}.json"
atomic_write_json(resp_path, result)
shutil.move(str(proc_path), str(dirs["archive"] / proc_path.name))
def cmd_serve(args: argparse.Namespace) -> int:
dirs = ensure_dirs(Path(args.queue_dir).resolve())
poll_s = max(0.05, args.poll_ms / 1000.0)
print(f"eyesis-host-daemon: serving {dirs['base']}")
print(f"eyesis-host-daemon: ctl script={CTL_SCRIPT}")
print(f"eyesis-host-daemon: mcp script={MCP_SCRIPT}")
sys.stdout.flush()
while True:
reqs = sorted(dirs["requests"].glob("*.json"))
if not reqs:
time.sleep(poll_s)
continue
for req in reqs:
process_one(req, dirs)
def submit_request(queue_dir: Path, action: str, req_args: list[str], timeout_sec: int) -> str:
dirs = ensure_dirs(queue_dir)
req_id = uuid.uuid4().hex
payload = {
"id": req_id,
"created_at": now_iso(),
"action": action,
"args": req_args,
"timeout_sec": timeout_sec,
"cwd": str(REPO_ROOT),
}
req_path = dirs["requests"] / f"{req_id}.json"
atomic_write_json(req_path, payload)
return req_id
def wait_response(queue_dir: Path, req_id: str, wait_sec: int) -> dict | None:
resp_path = queue_dir / "responses" / f"{req_id}.json"
deadline = time.time() + max(1, wait_sec)
while time.time() < deadline:
if resp_path.exists():
return json.loads(resp_path.read_text(encoding="utf-8"))
time.sleep(0.1)
return None
def cmd_submit(args: argparse.Namespace) -> int:
queue_dir = Path(args.queue_dir).resolve()
req_id = submit_request(queue_dir, args.action, args.req_args, args.timeout_sec)
print(req_id)
if args.wait_sec <= 0:
return 0
resp = wait_response(queue_dir, req_id, args.wait_sec)
if resp is None:
print(json.dumps({"ok": False, "id": req_id, "error": "timeout waiting response"}, indent=2))
return 124
# print command outputs in a terminal-friendly way
stdout = resp.get("stdout", "")
stderr = resp.get("stderr", "")
if stdout:
sys.stdout.write(stdout)
if not stdout.endswith("\n"):
sys.stdout.write("\n")
if stderr:
sys.stderr.write(stderr)
if not stderr.endswith("\n"):
sys.stderr.write("\n")
return int(resp.get("exit_code", 1))
def cmd_read(args: argparse.Namespace) -> int:
queue_dir = Path(args.queue_dir).resolve()
resp_path = queue_dir / "responses" / f"{args.id}.json"
if not resp_path.exists():
print(f"response not found: {resp_path}", file=sys.stderr)
return 1
print(resp_path.read_text(encoding="utf-8"), end="")
return 0
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(description="Eyesis host daemon")
sub = p.add_subparsers(dest="cmd", required=True)
p_serve = sub.add_parser("serve", help="Run daemon loop")
p_serve.add_argument("--queue-dir", default=str(DEFAULT_QUEUE))
p_serve.add_argument("--poll-ms", type=int, default=200)
p_serve.set_defaults(func=cmd_serve)
p_submit = sub.add_parser("submit", help="Submit one request")
p_submit.add_argument("--queue-dir", default=str(DEFAULT_QUEUE))
p_submit.add_argument("--action", choices=["ctl", "mcp"], required=True)
p_submit.add_argument("--wait-sec", type=int, default=120)
p_submit.add_argument("--timeout-sec", type=int, default=300)
p_submit.add_argument("req_args", nargs=argparse.REMAINDER, help="Request args after '--'")
p_submit.set_defaults(func=cmd_submit)
p_read = sub.add_parser("read", help="Read response by id")
p_read.add_argument("--queue-dir", default=str(DEFAULT_QUEUE))
p_read.add_argument("--id", required=True)
p_read.set_defaults(func=cmd_read)
return p
def main() -> int:
parser = build_parser()
args = parser.parse_args()
if getattr(args, "cmd", "") == "submit":
if args.req_args and args.req_args[0] == "--":
args.req_args = args.req_args[1:]
if not args.req_args:
print("submit requires request args after '--'", file=sys.stderr)
return 2
return args.func(args)
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
DAEMON="${REPO_ROOT}/scripts/eyesis_host_daemon.py"
QUEUE_DIR="${EYESIS_HOST_QUEUE_DIR:-${REPO_ROOT}/attic/session-logs/eyesis-host-daemon}"
STATE_DIR="${QUEUE_DIR}"
PID_FILE="${STATE_DIR}/daemon.pid"
LOG_FILE="${EYESIS_HOST_DAEMON_LOG:-${REPO_ROOT}/attic/session-logs/eyesis_host_daemon.log}"
POLL_MS="${EYESIS_HOST_DAEMON_POLL_MS:-200}"
FORCE=0
LINES=80
usage() {
cat <<'USAGE'
Usage:
scripts/eyesis_host_daemon_ctl.sh start [--queue-dir DIR] [--log FILE] [--poll-ms N]
scripts/eyesis_host_daemon_ctl.sh stop [--force] [--queue-dir DIR]
scripts/eyesis_host_daemon_ctl.sh restart [options]
scripts/eyesis_host_daemon_ctl.sh status [--queue-dir DIR]
scripts/eyesis_host_daemon_ctl.sh logs [--lines N] [--log FILE]
Environment overrides:
EYESIS_HOST_QUEUE_DIR, EYESIS_HOST_DAEMON_LOG, EYESIS_HOST_DAEMON_POLL_MS
USAGE
}
is_running() {
[[ -f "${PID_FILE}" ]] || return 1
local pid
pid="$(cat "${PID_FILE}" 2>/dev/null || true)"
[[ -n "${pid}" ]] || return 1
kill -0 "${pid}" 2>/dev/null
}
clear_stale() {
if [[ -f "${PID_FILE}" ]] && ! is_running; then
rm -f "${PID_FILE}"
fi
}
start_daemon() {
mkdir -p "${QUEUE_DIR}" "${REPO_ROOT}/attic/session-logs"
clear_stale
if is_running; then
echo "daemon: running (pid=$(cat "${PID_FILE}"))"
return 0
fi
nohup "${DAEMON}" serve --queue-dir "${QUEUE_DIR}" --poll-ms "${POLL_MS}" >>"${LOG_FILE}" 2>&1 &
echo $! >"${PID_FILE}"
sleep 0.3
if is_running; then
echo "daemon: started (pid=$(cat "${PID_FILE}"))"
echo "queue: ${QUEUE_DIR}"
echo "log: ${LOG_FILE}"
return 0
fi
echo "daemon: failed to start" >&2
return 1
}
stop_daemon() {
clear_stale
if ! is_running; then
echo "daemon: stopped"
return 0
fi
local pid
pid="$(cat "${PID_FILE}")"
kill -TERM "${pid}" 2>/dev/null || true
for _ in $(seq 1 20); do
if ! kill -0 "${pid}" 2>/dev/null; then
rm -f "${PID_FILE}"
echo "daemon: stopped"
return 0
fi
sleep 0.2
done
if [[ "${FORCE}" -eq 1 ]]; then
kill -KILL "${pid}" 2>/dev/null || true
rm -f "${PID_FILE}"
echo "daemon: force-stopped"
return 0
fi
echo "daemon: stop timeout; use --force" >&2
return 1
}
status_daemon() {
clear_stale
if is_running; then
echo "daemon: running (pid=$(cat "${PID_FILE}"))"
else
echo "daemon: stopped"
fi
echo "queue: ${QUEUE_DIR}"
echo "log: ${LOG_FILE}"
}
show_logs() {
[[ -f "${LOG_FILE}" ]] || { echo "log file not found: ${LOG_FILE}" >&2; return 1; }
tail -n "${LINES}" -f "${LOG_FILE}"
}
[[ $# -ge 1 ]] || { usage; exit 2; }
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
usage
exit 0
fi
cmd="$1"
shift
while [[ $# -gt 0 ]]; do
case "$1" in
--queue-dir)
QUEUE_DIR="$2"; STATE_DIR="$2"; PID_FILE="${STATE_DIR}/daemon.pid"; shift 2 ;;
--log)
LOG_FILE="$2"; shift 2 ;;
--poll-ms)
POLL_MS="$2"; shift 2 ;;
--force)
FORCE=1; shift ;;
--lines)
LINES="$2"; shift 2 ;;
-h|--help)
usage; exit 0 ;;
*)
echo "Unknown option: $1" >&2; usage; exit 2 ;;
esac
done
case "${cmd}" in
start) start_daemon ;;
stop) stop_daemon ;;
restart) stop_daemon || true; start_daemon ;;
status) status_daemon ;;
logs) show_logs ;;
*) echo "Unknown command: ${cmd}" >&2; usage; exit 2 ;;
esac
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
CLIENT="${REPO_ROOT}/scripts/eyesis_host_daemon.py"
QUEUE_DIR="${EYESIS_HOST_QUEUE_DIR:-${REPO_ROOT}/attic/session-logs/eyesis-host-daemon}"
WAIT_SEC="${EYESIS_HOST_WAIT_SEC:-120}"
TIMEOUT_SEC="${EYESIS_HOST_TIMEOUT_SEC:-300}"
usage() {
cat <<'USAGE'
Usage:
scripts/eyesis_host_submit.sh ctl <args...>
scripts/eyesis_host_submit.sh mcp <args...>
Examples:
scripts/eyesis_host_submit.sh ctl status
scripts/eyesis_host_submit.sh ctl start --configdir /media/elphel/btrfs-data/lwir16-proc/NC/config --wait-sec 120
scripts/eyesis_host_submit.sh mcp button --label "Restore"
Environment:
EYESIS_HOST_QUEUE_DIR, EYESIS_HOST_WAIT_SEC, EYESIS_HOST_TIMEOUT_SEC
USAGE
}
[[ $# -ge 2 ]] || { usage; exit 2; }
action="$1"; shift
case "${action}" in
ctl|mcp) ;;
*) echo "action must be ctl or mcp" >&2; usage; exit 2 ;;
esac
exec "${CLIENT}" submit \
--queue-dir "${QUEUE_DIR}" \
--action "${action}" \
--wait-sec "${WAIT_SEC}" \
--timeout-sec "${TIMEOUT_SEC}" \
-- "$@"
This diff is collapsed.
......@@ -9,6 +9,22 @@ The server auto-starts when `Eyesis_Correction` runs.
Default:
- URL: `http://127.0.0.1:48888`
Lifecycle helper script:
- `scripts/eyesis_mcp_ctl.sh` can manage process lifecycle from terminal:
- `start` / `stop` / `restart` / `status` / `logs` / `wait`
- example:
```bash
/home/elphel/git/imagej-elphel/scripts/eyesis_mcp_ctl.sh start \
--configdir /media/elphel/btrfs-data/lwir16-proc/NC/config --rebuild
```
- by default it uses the same VM settings as the Eclipse run profile from `AGENTS.md`
and waits for MCP readiness.
Host daemon mode (recommended for no-repeat approvals):
- `scripts/eyesis_host_daemon_ctl.sh start` starts a queue-based host daemon in your desktop session.
- `scripts/eyesis_host_submit.sh ctl ...` and `scripts/eyesis_host_submit.sh mcp ...` submit requests through that daemon.
- details: `scripts/eyesis-host-daemon-howto.md`.
Optional JVM system properties:
- `-Delphel.mcp.port=PORT` (override port, default 48888)
- `-Delphel.mcp.mode=true|false` (enable/disable MCP dialog mode; default true)
......
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${EYESIS_MCP_BASE_URL:-http://127.0.0.1:48888}"
TIMEOUT="${EYESIS_MCP_TIMEOUT_SEC:-10}"
usage() {
cat <<'USAGE'
Usage:
scripts/mcp_http.sh status
scripts/mcp_http.sh dialog
scripts/mcp_http.sh button --label TEXT
scripts/mcp_http.sh set --label TEXT --value TEXT [--id UUID]
scripts/mcp_http.sh submit --ok 0|1 [--id UUID]
scripts/mcp_http.sh interrupt --confirm 0|1 [--asap 0|1]
scripts/mcp_http.sh confirm-stop --stop 0|1
Options:
--base URL MCP base URL (default: $EYESIS_MCP_BASE_URL or http://127.0.0.1:48888)
--timeout SEC curl max-time seconds (default: $EYESIS_MCP_TIMEOUT_SEC or 10)
-h, --help Show help
Examples:
scripts/mcp_http.sh status
scripts/mcp_http.sh button --label "Restore"
scripts/mcp_http.sh set --label "Selected path" --value "/path/file.corr-xml"
scripts/mcp_http.sh submit --ok 1
USAGE
}
cmd_get() {
local path="$1"
curl -sS --fail --max-time "${TIMEOUT}" "${BASE_URL}${path}"
}
cmd_post() {
local path="$1"
shift
curl -sS --fail --max-time "${TIMEOUT}" -X POST "$@" "${BASE_URL}${path}"
}
require_nonempty() {
local name="$1"
local value="$2"
if [[ -z "${value}" ]]; then
echo "Missing required ${name}" >&2
exit 2
fi
}
if [[ $# -lt 1 ]]; then
usage
exit 2
fi
subcmd="$1"
shift
# parse optional common flags before subcommand-specific flags
while [[ $# -gt 0 ]]; do
case "$1" in
--base)
BASE_URL="$2"
shift 2
;;
--timeout)
TIMEOUT="$2"
shift 2
;;
--)
shift
break
;;
*)
break
;;
esac
done
case "${subcmd}" in
status)
cmd_get "/mcp/status"
;;
dialog)
cmd_get "/mcp/dialog"
;;
button)
label=""
while [[ $# -gt 0 ]]; do
case "$1" in
--label)
label="$2"
shift 2
;;
*)
echo "Unknown option for button: $1" >&2
exit 2
;;
esac
done
require_nonempty "--label" "${label}"
cmd_post "/mcp/button" --data-urlencode "label=${label}"
;;
set)
label=""
value=""
id=""
while [[ $# -gt 0 ]]; do
case "$1" in
--label)
label="$2"
shift 2
;;
--value)
value="$2"
shift 2
;;
--id)
id="$2"
shift 2
;;
*)
echo "Unknown option for set: $1" >&2
exit 2
;;
esac
done
require_nonempty "--label" "${label}"
require_nonempty "--value" "${value}"
if [[ -n "${id}" ]]; then
cmd_post "/mcp/dialog/values" --data-urlencode "id=${id}" --data-urlencode "label=${label}" --data-urlencode "value=${value}"
else
cmd_post "/mcp/dialog/values" --data-urlencode "label=${label}" --data-urlencode "value=${value}"
fi
;;
submit)
ok=""
id=""
while [[ $# -gt 0 ]]; do
case "$1" in
--ok)
ok="$2"
shift 2
;;
--id)
id="$2"
shift 2
;;
*)
echo "Unknown option for submit: $1" >&2
exit 2
;;
esac
done
require_nonempty "--ok" "${ok}"
if [[ -n "${id}" ]]; then
cmd_post "/mcp/dialog/submit" --data-urlencode "id=${id}" --data-urlencode "ok=${ok}"
else
cmd_post "/mcp/dialog/submit" --data-urlencode "ok=${ok}"
fi
;;
interrupt)
confirm=""
asap="0"
while [[ $# -gt 0 ]]; do
case "$1" in
--confirm)
confirm="$2"
shift 2
;;
--asap)
asap="$2"
shift 2
;;
*)
echo "Unknown option for interrupt: $1" >&2
exit 2
;;
esac
done
require_nonempty "--confirm" "${confirm}"
cmd_post "/mcp/interrupt" --data-urlencode "confirm=${confirm}" --data-urlencode "asap=${asap}"
;;
confirm-stop)
stop=""
while [[ $# -gt 0 ]]; do
case "$1" in
--stop)
stop="$2"
shift 2
;;
*)
echo "Unknown option for confirm-stop: $1" >&2
exit 2
;;
esac
done
require_nonempty "--stop" "${stop}"
cmd_post "/mcp/interrupt/confirm" --data-urlencode "stop=${stop}"
;;
-h|--help)
usage
;;
*)
echo "Unknown subcommand: ${subcmd}" >&2
usage
exit 2
;;
esac
......@@ -42,6 +42,7 @@ import com.elphel.imagej.lwir.LwirReaderParameters;
import com.elphel.imagej.tileprocessor.BiQuadParameters;
import com.elphel.imagej.tileprocessor.ImageDtt;
import com.elphel.imagej.tileprocessor.ImageDttParameters;
import com.elphel.imagej.tileprocessor.IntersceneGlobalLmaParameters;
import com.elphel.imagej.tileprocessor.IntersceneLmaParameters;
import com.elphel.imagej.tileprocessor.IntersceneMatchParameters;
import com.elphel.imagej.tileprocessor.LWIRWorldParameters;
......@@ -1179,6 +1180,7 @@ public class CLTParameters {
public OpticalFlowParameters ofp = new OpticalFlowParameters();
public IntersceneMatchParameters imp = new IntersceneMatchParameters();
public IntersceneLmaParameters ilp = new IntersceneLmaParameters();
public IntersceneGlobalLmaParameters iglp = new IntersceneGlobalLmaParameters();
public InterNoiseParameters inp = new InterNoiseParameters();
public LWIRWorldParameters lwp = new LWIRWorldParameters();
......@@ -2333,6 +2335,7 @@ public class CLTParameters {
ofp.setProperties (prefix+"_ofp_", properties);
imp.setProperties (prefix+"_imp_", properties);
ilp.setProperties (prefix+"_ilp_", properties);
iglp.setProperties (prefix+"_iglp_", properties);
inp.setProperties (prefix+"_inp_", properties);
lwp.setProperties (prefix+"_lwp_", properties);
......@@ -3402,6 +3405,7 @@ public class CLTParameters {
ofp.getProperties (prefix+"_ofp_", properties);
imp.getProperties (prefix+"_imp_", properties);
ilp.getProperties (prefix+"_ilp_", properties);
iglp.getProperties (prefix+"_iglp_", properties);
inp.getProperties (prefix+"_inp_", properties);
lwp.getProperties (prefix+"_lwp_", properties);
}
......@@ -4898,6 +4902,9 @@ public class CLTParameters {
gd.addTab ("Inter-LMA", "parameters for the interscene LMA fitting");
this.ilp.dialogQuestions(gd);
gd.addTab ("Inter-Global-LMA", "parameters for the interscene global LMA fitting");
this.iglp.dialogQuestions(gd);
gd.addTab ("Inter-Noise", "parameters for the interscene noise testing");
this.inp.dialogQuestions(gd);
......@@ -5960,6 +5967,7 @@ public class CLTParameters {
this.imp.dialogAnswers(gd);
this.lwp.dialogAnswers(gd);
this.ilp.dialogAnswers(gd);
this.iglp.dialogAnswers(gd);
this.inp.dialogAnswers(gd);
this.debug_initial_discriminate= gd.getNextBoolean();
......
......@@ -505,6 +505,7 @@ public class Eyesis_Correction implements PlugIn, ActionListener {
jpanelPostProcessing2, jpanelPostProcessing3, jpanelDct1, jpanelClt1, jpanelClt2, jpanelClt3, jpanelClt4,
jpanelClt5, jpanelClt5aux, jpanelClt_GPU, jpanelLWIR, jpanelLWIR16, jpanelLWIRWorld, jpanelOrange;
private boolean commandFromMcp = false;
private volatile boolean shutdownRequested = false;
// EyesisTopFrame eyesisTopFrame = new EyesisTopFrame();
// System.out.println("Launched EyesisTopFrame");
......@@ -600,6 +601,7 @@ public class Eyesis_Correction implements PlugIn, ActionListener {
addJButton("Restore", jpanel6, color_restore); // , "Restore configuration");
addJButton("Stop", jpanel6, color_stop);
addJButton("Abort", jpanel6, color_stop);
addJButton("Exit", jpanel6, color_stop);
plugInJFrame.add(jpanel6);
......@@ -971,6 +973,10 @@ public class Eyesis_Correction implements PlugIn, ActionListener {
this.commandFromMcp = false;
this.SYNC_COMMAND.isRunning = false;
this.SYNC_COMMAND.stopRequested.set(0);
if (this.shutdownRequested) {
shutdownApplication();
break;
}
}
}
......@@ -1144,6 +1150,18 @@ public class Eyesis_Correction implements PlugIn, ActionListener {
if (label == null) {
return;
}
if (label.equals("Exit")) {
this.shutdownRequested = true;
if (this.SYNC_COMMAND.isRunning) {
// Request immediate stop; main loop will exit after current command unwinds.
this.SYNC_COMMAND.confirm = false;
this.SYNC_COMMAND.stopRequested.set(SyncCommand.STOP_ASAP);
return;
}
// If idle, exit immediately to avoid notify/wait race in the command loop.
shutdownApplication();
return;
}
if (label.equals("Abort")) {
this.SYNC_COMMAND.stopRequested.set(1);
this.SYNC_COMMAND.confirm = true;
......@@ -1165,6 +1183,26 @@ public class Eyesis_Correction implements PlugIn, ActionListener {
}
}
private void shutdownApplication() {
if (DEBUG_LEVEL > -3) {
System.out.println("Exit requested, shutting down application");
}
try {
if (this.plugInJFrame != null) {
this.plugInJFrame.dispose();
this.plugInJFrame = null;
}
} catch (Exception e) {
System.out.println("Failed to dispose Eyesis frame: " + e.getMessage());
}
ImageJ imagej = IJ.getInstance();
if (imagej != null) {
imagej.quit();
} else {
System.exit(0);
}
}
// codex 2026-01-25: MCP status accessors
public boolean isSyncRunning() {
return this.SYNC_COMMAND.isRunning;
......@@ -1383,6 +1421,12 @@ public class Eyesis_Correction implements PlugIn, ActionListener {
CLT_PARAMETERS.batch_run = false;
if (label == null)
return;
if (label.equals("Exit")) {
if (DEBUG_LEVEL > -3) {
System.out.println("Exit command received");
}
return;
}
if ((CLT_PARAMETERS != null) && (CLT_PARAMETERS.lwir != null)) {
String LOG_LEVEL;
switch (CLT_PARAMETERS.lwir.getDebugLevel()) {
......
......@@ -14,6 +14,7 @@ import java.util.Map;
import com.elphel.imagej.correction.Eyesis_Correction;
import com.elphel.imagej.mcp.McpFsAccess;
import com.elphel.imagej.tileprocessor.IntersceneGlobalRefine;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
......@@ -605,6 +606,8 @@ public class McpServer {
private String buildStatusJson() {
int stopRequested = owner.getSyncStopRequested();
IntersceneGlobalRefine.ProgressSnapshot progress = IntersceneGlobalRefine.getProgressSnapshot();
long now = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
sb.append("{");
sb.append("\"running\":").append(owner.isSyncRunning());
......@@ -612,10 +615,85 @@ public class McpServer {
sb.append(",\"confirmPending\":").append(owner.isSyncConfirmPending());
sb.append(",\"confirmConvenient\":").append(owner.isSyncConfirmConvenient());
sb.append(",\"buttonLabel\":\"").append(jsonEscape(owner.getSyncButtonLabel())).append("\"");
sb.append(",\"globalLma\":");
appendGlobalLmaJson(
sb,
progress,
now);
sb.append("}");
return sb.toString();
}
private static void appendGlobalLmaJson(
StringBuilder sb,
IntersceneGlobalRefine.ProgressSnapshot progress,
long nowMs) {
if (progress == null) {
sb.append("null");
return;
}
long elapsedMs = 0L;
if (progress.startedMs > 0L) {
long stopTs = (progress.active || (progress.finishedMs <= 0L)) ? nowMs : progress.finishedMs;
elapsedMs = Math.max(0L, stopTs - progress.startedMs);
}
sb.append("{");
sb.append("\"active\":").append(progress.active);
sb.append(",\"status\":\"").append(jsonEscape(progress.status)).append("\"");
sb.append(",\"stage\":\"").append(jsonEscape(progress.stage)).append("\"");
sb.append(",\"startedMs\":").append(progress.startedMs);
sb.append(",\"updatedMs\":").append(progress.updatedMs);
sb.append(",\"finishedMs\":").append(progress.finishedMs);
sb.append(",\"elapsedMs\":").append(elapsedMs);
sb.append(",\"centerIndex\":").append(progress.centerIndex);
sb.append(",\"firstScene\":").append(progress.firstScene);
sb.append(",\"lastScene\":").append(progress.lastScene);
sb.append(",\"outerIterations\":").append(progress.outerIterations);
sb.append(",\"innerIterations\":").append(progress.innerIterations);
sb.append(",\"outer\":").append(progress.outer);
sb.append(",\"inner\":").append(progress.inner);
sb.append(",\"cachedPairs\":").append(progress.cachedPairs);
sb.append(",\"correlationSolved\":").append(progress.correlationSolved);
sb.append(",\"correlationFailed\":").append(progress.correlationFailed);
sb.append(",\"tilesPerPair\":").append(progress.tilesPerPair);
sb.append(",\"solvedScenes\":").append(progress.solvedScenes);
sb.append(",\"failedScenes\":").append(progress.failedScenes);
sb.append(",\"solvedPairs\":").append(progress.solvedPairs);
sb.append(",\"failedPairs\":").append(progress.failedPairs);
sb.append(",\"pcgIter\":").append(progress.pcgIter);
sb.append(",\"avgPairRms\":");
appendJsonNumber(sb, progress.avgPairRms);
sb.append(",\"avgPairRmsPure\":");
appendJsonNumber(sb, progress.avgPairRmsPure);
sb.append(",\"maxDelta\":");
appendJsonNumber(sb, progress.maxDelta);
sb.append(",\"lambda\":");
appendJsonNumber(sb, progress.lambda);
sb.append(",\"lpfSqSum\":");
appendJsonNumber(sb, progress.lpfSqSum);
sb.append(",\"lpfWeightSum\":");
appendJsonNumber(sb, progress.lpfWeightSum);
sb.append(",\"lpf\":{");
sb.append("\"x\":");
appendJsonNumber(sb, progress.lpfX);
sb.append(",\"y\":");
appendJsonNumber(sb, progress.lpfY);
sb.append(",\"a\":");
appendJsonNumber(sb, progress.lpfA);
sb.append(",\"t\":");
appendJsonNumber(sb, progress.lpfT);
sb.append("}");
sb.append("}");
}
private static void appendJsonNumber(StringBuilder sb, double value) {
if (Double.isFinite(value)) {
sb.append(value);
} else {
sb.append("null");
}
}
private String buildDialogJson() {
McpDialogSession session = McpDialogRegistry.getCurrent();
if (session == null) {
......
......@@ -194,6 +194,8 @@ public class ErsCorrection extends GeometryCorrection {
public static final int [] DP_ZR_INDICES = {DP_DSZ,DP_DSRL};
public static final int [] DP_AT_INDICES = {DP_DSAZ,DP_DSTL};
public static final int [] DP_ATT_ERS_INDICES = {DP_DSVAZ,DP_DSVTL};
public static final int [] DP_XYZATR = {DP_DSX,DP_DSY,DP_DSZ, DP_DSAZ, DP_DSTL, DP_DSRL};
public static final RotationConvention ROT_CONV = RotationConvention.FRAME_TRANSFORM;
static final double THRESHOLD = 1E-10;
......
......@@ -329,7 +329,9 @@ public class EstimateSceneRange {
boolean sfm_filter = clt_parameters.imp.sfm_filter; //true; // use SfM filtering if available
double sfm_minmax = clt_parameters.imp.sfm_minmax; //10.0; // minimal value of the SfM gain maximum to consider available
double sfm_fracmax = clt_parameters.imp.sfm_fracmax; // 0.75; // minimal fraction of the SfM maximal gain
double sfm_fracall = clt_parameters.imp.sfm_fracall; // 0.3; // minimal relative area of the SfM-e
double sfm_minabs = clt_parameters.imp.sfm_minabs; //5.0; // SFM gain threshold to use min_ref_str_sfm as minimal reliable strength
double sfm_fracall = clt_parameters.imp.sfm_fracall; // 0.3; // minimal relative area of the SfM-enabled tiles (do not apply filter if less)
double min_ref_str_sfm = clt_parameters.imp.min_ref_str_sfm; // 0.1; // reliable strength when sfm gain is available
boolean use_ims_rotation = clt_parameters.imp.use_quat_corr; // use internally (probably deprecated - not)
boolean inertial_only = clt_parameters.imp.inertial_only; // use internally
double fmg_max_quad = clt_parameters.imp.fmg_max_quad; // estimate offset by 4 points (rooll-aware, 25% from center) if center offset is too small
......@@ -455,7 +457,9 @@ public class EstimateSceneRange {
sfm_filter, // boolean sfm_filter, // use SfM filtering if available
sfm_minmax, // double sfm_minmax, // minimal value of the SfM gain maximum to consider available
sfm_fracmax, // double sfm_fracmax,// minimal fraction of the SfM maximal gain
sfm_fracall, // double sfm_fracall,// minimal relative area of the SfM-enabled tiles (do not apply filter if less)
sfm_minabs, // double sfm_minabs,
sfm_fracall, // double sfm_fracall,// minimal relative area of the SfM-enabled tiles (do not apply filter if less)
min_ref_str_sfm, // double min_ref_str_sfm,
reduced_strength, // if not null will return >0 if had to reduce strength (no change if did not reduce)
debugLevel); // int debugLevel)
if (reduced_strength[0] > 0) {
......
package com.elphel.imagej.tileprocessor;
import com.elphel.imagej.cameras.CLTParameters;
/**
* Classic-LMA structured global refinement path.
*
* <p>Phase-1 implementation keeps behavior stable by delegating numeric solve to
* {@link IntersceneGlobalRefine} while exposing a minimal {@code prepareLMA/getFxDerivs/lmaStep/runLma}
* skeleton for incremental migration to the classic block-based style.
*/
public class IntersceneGlobalLmaRefine {
private final CLTParameters cltParameters;
private final QuadCLT[] quadCLTs;
private final QuadCLT centerCLT;
private final int centerIndex;
private final int earliestScene;
private final int lastScene;
private final double[][][] scenesXyzatr;
private final double[][][] scenesXyzatrPull;
private final boolean[] paramSelect;
private final double[] paramRegweights;
private final double[] paramLpf;
private final double[] centerDisparity;
private final boolean[] reliableRef;
private final boolean disableErs;
private final double mbMaxGain;
private final IntersceneGlobalRefine.Options options;
private IntersceneGlobalLmaRefine(
final CLTParameters cltParameters,
final QuadCLT[] quadCLTs,
final QuadCLT centerCLT,
final int centerIndex,
final int earliestScene,
final int lastScene,
final double[][][] scenesXyzatr,
final double[][][] scenesXyzatrPull,
final boolean[] paramSelect,
final double[] paramRegweights,
final double[] paramLpf,
final double[] centerDisparity,
final boolean[] reliableRef,
final boolean disableErs,
final double mbMaxGain,
final IntersceneGlobalRefine.Options options) {
this.cltParameters = cltParameters;
this.quadCLTs = quadCLTs;
this.centerCLT = centerCLT;
this.centerIndex = centerIndex;
this.earliestScene = earliestScene;
this.lastScene = lastScene;
this.scenesXyzatr = scenesXyzatr;
this.scenesXyzatrPull = scenesXyzatrPull;
this.paramSelect = paramSelect;
this.paramRegweights = paramRegweights;
this.paramLpf = paramLpf;
this.centerDisparity = centerDisparity;
this.reliableRef = reliableRef;
this.disableErs = disableErs;
this.mbMaxGain = mbMaxGain;
this.options = options;
}
public static IntersceneGlobalRefine.Result refineAllToReference(
final CLTParameters cltParameters,
final QuadCLT[] quadCLTs,
final QuadCLT centerCLT,
final int centerIndex,
final int earliestScene,
final int lastScene,
final double[][][] scenesXyzatr,
final double[][][] scenesXyzatrPull,
final boolean[] paramSelect,
final double[] paramRegweights,
final double[] paramLpf,
final double[] centerDisparity,
final boolean[] reliableRef,
final boolean disableErs,
final double mbMaxGain,
final IntersceneGlobalRefine.Options options,
final int debugLevel) {
final IntersceneGlobalLmaRefine solver = new IntersceneGlobalLmaRefine(
cltParameters,
quadCLTs,
centerCLT,
centerIndex,
earliestScene,
lastScene,
scenesXyzatr,
scenesXyzatrPull,
paramSelect,
paramRegweights,
paramLpf,
centerDisparity,
reliableRef,
disableErs,
mbMaxGain,
options);
solver.prepareLMA(debugLevel);
return solver.runLma(debugLevel);
}
/**
* Prepare solver state for the classic LMA-structured implementation.
*/
private void prepareLMA(final int debugLevel) {
if (debugLevel > -4) {
System.out.println(
"IntersceneGlobalLmaRefine: prepareLMA() phase-1 wrapper enabled; " +
"numeric solve delegated to IntersceneGlobalRefine");
}
}
/**
* Placeholder for future application-specific residual/Jacobian assembly blocks.
*/
@SuppressWarnings("unused")
private double[] getFxDerivs(
final double[] vector,
final double[][] jt,
final int debugLevel) {
return null;
}
/**
* Placeholder for future classic LMA step implementation.
*/
@SuppressWarnings("unused")
private boolean[] lmaStep(
final double lambda,
final double rmsDiff,
final int debugLevel) {
return new boolean[] {false, false};
}
/**
* Run global refinement. Phase-1 delegates to the current sparse/banded solver.
*/
private IntersceneGlobalRefine.Result runLma(final int debugLevel) {
return IntersceneGlobalRefine.refineAllToReference(
cltParameters,
quadCLTs,
centerCLT,
centerIndex,
earliestScene,
lastScene,
scenesXyzatr,
scenesXyzatrPull,
paramSelect,
paramRegweights,
paramLpf,
centerDisparity,
reliableRef,
disableErs,
mbMaxGain,
options,
debugLevel);
}
}
......@@ -614,6 +614,68 @@ public class IntersceneLma {
initial_rms = last_rms.clone();
good_or_bad_rms = this.last_rms.clone();
}
/**
* Recalculate local Jacobian/residual at a new linearization point while keeping the previously
* prepared {@code y_vector} fixed. This follows the same semantics as iterative LMA steps:
* target observations stay constant, only {@code f(x)} and {@code J(x)} are updated.
*
* @param scene_xyzatr0 current scene pose {{x,y,z},{a,t,r}}
* @param ref_xyzatr current reference pose {{x,y,z},{a,t,r}}
* @param scene_QuadClt current scene object
* @param reference_QuadClt current reference scene object
* @param debug_level debug verbosity
* @return true if residual vector was successfully recomputed
*/
public boolean relinearizeAt(
final double [][] scene_xyzatr0,
final double [][] ref_xyzatr,
final QuadCLT scene_QuadClt,
final QuadCLT reference_QuadClt,
final int debug_level) {
if ((y_vector == null) || (weights == null) || (par_indices == null) || (par_mask == null)) {
return false;
}
scenesCLT = new QuadCLT [] {reference_QuadClt, scene_QuadClt};
final ErsCorrection ers_ref = reference_QuadClt.getErsCorrection();
final ErsCorrection ers_scene = scene_QuadClt.getErsCorrection();
final double [] scene_xyz = (scene_xyzatr0 != null) ? scene_xyzatr0[0] : ers_scene.camera_xyz;
final double [] scene_atr = (scene_xyzatr0 != null) ? scene_xyzatr0[1] : ers_scene.camera_atr;
final double [] reference_xyz = (ref_xyzatr != null) ? ref_xyzatr[0] : ers_ref.camera_xyz;
final double [] reference_atr = (ref_xyzatr != null) ? ref_xyzatr[1] : ers_ref.camera_xyz;
final double [] full_parameters_vector = new double [] {
0.0, 0.0, 0.0,
ers_ref.ers_watr_center_dt[0], ers_ref.ers_watr_center_dt[1], ers_ref.ers_watr_center_dt[2],
ers_ref.ers_wxyz_center_dt[0], ers_ref.ers_wxyz_center_dt[1], ers_ref.ers_wxyz_center_dt[2],
reference_atr[0], reference_atr[1], reference_atr[2],
reference_xyz[0], reference_xyz[1], reference_xyz[2],
ers_scene.ers_watr_center_dt[0], ers_scene.ers_watr_center_dt[1], ers_scene.ers_watr_center_dt[2],
ers_scene.ers_wxyz_center_dt[0], ers_scene.ers_wxyz_center_dt[1], ers_scene.ers_wxyz_center_dt[2],
scene_atr[0], scene_atr[1], scene_atr[2],
scene_xyz[0], scene_xyz[1], scene_xyz[2]};
parameters_full = full_parameters_vector.clone();
if ((parameters_vector == null) || (parameters_vector.length != par_indices.length)) {
parameters_vector = new double [par_indices.length];
}
for (int i = 0; i < par_indices.length; i++) {
parameters_vector[i] = full_parameters_vector[par_indices[i]];
}
if ((last_jt == null) || (last_jt.length != parameters_vector.length)) {
last_jt = new double [parameters_vector.length][];
}
final double [] fx = getFxDerivs(
parameters_vector, // double [] vector,
last_jt, // final double [][] jt, // should be null or initialized with [vector.length][]
scene_QuadClt, // final QuadCLT scene_QuadClt,
reference_QuadClt, // final QuadCLT reference_QuadClt,
debug_level); // final int debug_level)
last_rms = new double [2];
last_ymfx = getYminusFxWeighted(
fx, // final double [] fx,
last_rms);// final double [] rms_fp // null or [2]
good_or_bad_rms = (last_rms == null) ? null : last_rms.clone();
return last_ymfx != null;
}
public int runLma( // <0 - failed, >=0 iteration number (1 - immediately)
double lambda, // 0.1
......
......@@ -639,8 +639,10 @@ min_str_neib_fpn 0.35
public boolean use_combo_reliable = true; // use combo dsi if available for reliable tiles
public boolean ref_need_lma = true; // need LMA output for reliable tiles (no combo available)
public boolean ref_need_lma_combo = true; // need LMA output for reliable tiles (when combo is available)
public double min_ref_str = 0.33; // 0.22; // For orientations: use only tiles of the reference scene DSI_MAIN is stronger
public double min_ref_str_lma = 0.7; // 0.22; // For orientations: use only tiles of the reference scene DSI_MAIN is stronger
public double min_ref_str = 0.33; // 0.22; // For orientations: use only tiles of the reference scene DSI_MAIN is stronger
public double min_ref_str_lma = 0.7; // 0.22; // For orientations: use only tiles of the reference scene DSI_MAIN is stronger
// public double min_ref_str_sfm = 0.1; // reliable strength when sfm gain is available
public double min_ref_frac = 0.2; // 0.22; if fraction number of reliable tiles is less than this, use best possible
public boolean save_reliables = true; // save reliable tiles as a multi-slice image
......@@ -653,9 +655,11 @@ min_str_neib_fpn 0.35
// SfM-related filtering (remove tiles without SfM)
public boolean sfm_filter = true; // use SfM filtering if available
public double sfm_minmax = 3.0; // 10.0 // minimal value of the SfM gain maximum to consider available
public double sfm_fracmax = 0.75; // minimal fraction of the SfM maximal gain
public double sfm_minmax = 20; // 3.0; // 10.0 // minimal value of the SfM gain maximum to consider available
public double sfm_fracmax = 0.1; // .75; // minimal fraction of the SfM maximal gain
public double sfm_minabs = 5.0; // minimal sfm gain
public double sfm_fracall = 0.3; // minimal relative area of the SfM-enabled tiles (do not apply filter if less)
public double min_ref_str_sfm = 0.1; // reliable strength when sfm gain is available
public int pix_step = 4; // Azimuth/tilt search step in pixels
......@@ -2406,8 +2410,12 @@ min_str_neib_fpn 0.35
"Minimal value of the SfM gain maximum to consider SfM available.");
gd.addNumericField("Threshold fraction of the SfM maximum", this.sfm_fracmax, 5,7,"",
"Threshold (minimal) fraction of the SfM maximal gain to enable tile.");
gd.addNumericField("Min. SFM gain for SFM reliable strength",this.sfm_minabs, 5,7,"",
"SFM gain threshold to use min_ref_str_sfm as minimal reliable strength.");
gd.addNumericField("Minimal relative SfM area", this.sfm_fracall, 5,7,"",
"Minimal relative area of the SfM-enabled tiles (do not apply filter if less).");
gd.addNumericField("Minimal SfM-based reliable strength", this.min_ref_str_sfm, 5,7,"",
"Reliable strength when sfm gain is available .");
......@@ -4160,7 +4168,7 @@ min_str_neib_fpn 0.35
this.min_ref_str = gd.getNextNumber();
this.min_ref_str_lma = gd.getNextNumber();
this.min_ref_frac = gd.getNextNumber();
this.save_reliables = gd.getNextBoolean();
this.save_reliables = gd.getNextBoolean();
this.ref_smooth = gd.getNextBoolean();
ref_smooth_always = gd.getNextBoolean();
......@@ -4169,7 +4177,9 @@ min_str_neib_fpn 0.35
this.sfm_filter = gd.getNextBoolean();
this.sfm_minmax = gd.getNextNumber();
this.sfm_fracmax = gd.getNextNumber();
this.sfm_minabs = gd.getNextNumber();
this.sfm_fracall = gd.getNextNumber();
this.min_ref_str_sfm = gd.getNextNumber();
this.pix_step = (int) gd.getNextNumber();
this.search_rad = (int) gd.getNextNumber();
this.maybe_sum = gd.getNextNumber();
......@@ -5454,7 +5464,9 @@ min_str_neib_fpn 0.35
properties.setProperty(prefix+"sfm_filter", this.sfm_filter+""); // boolean
properties.setProperty(prefix+"sfm_minmax", this.sfm_minmax+""); // double
properties.setProperty(prefix+"sfm_fracmax", this.sfm_fracmax+""); // double
properties.setProperty(prefix+"sfm_minabs", this.sfm_minabs+""); // double
properties.setProperty(prefix+"sfm_fracall", this.sfm_fracall+""); // double
properties.setProperty(prefix+"min_ref_str_sfm", this.min_ref_str_sfm+""); // double
properties.setProperty(prefix+"pix_step", this.pix_step+""); // int
......@@ -6674,7 +6686,9 @@ min_str_neib_fpn 0.35
if (properties.getProperty(prefix+"sfm_filter")!=null) this.sfm_filter=Boolean.parseBoolean(properties.getProperty(prefix+"sfm_filter"));
if (properties.getProperty(prefix+"sfm_minmax")!=null) this.sfm_minmax=Double.parseDouble(properties.getProperty(prefix+"sfm_minmax"));
if (properties.getProperty(prefix+"sfm_fracmax")!=null) this.sfm_fracmax=Double.parseDouble(properties.getProperty(prefix+"sfm_fracmax"));
if (properties.getProperty(prefix+"sfm_minabs")!=null) this.sfm_minabs=Double.parseDouble(properties.getProperty(prefix+"sfm_minabs"));
if (properties.getProperty(prefix+"sfm_fracall")!=null) this.sfm_fracall=Double.parseDouble(properties.getProperty(prefix+"sfm_fracall"));
if (properties.getProperty(prefix+"min_ref_str_sfm")!=null) this.min_ref_str_sfm=Double.parseDouble(properties.getProperty(prefix+"min_ref_str_sfm"));
if (properties.getProperty(prefix+"pix_step")!=null) this.pix_step=Integer.parseInt(properties.getProperty(prefix+"pix_step"));
if (properties.getProperty(prefix+"search_rad")!=null) this.search_rad=Integer.parseInt(properties.getProperty(prefix+"search_rad"));
......@@ -7900,6 +7914,7 @@ min_str_neib_fpn 0.35
imp.ref_need_lma_combo = this.ref_need_lma_combo;
imp.min_ref_str = this.min_ref_str;
imp.min_ref_str_lma = this.min_ref_str_lma;
imp.min_ref_frac = this.min_ref_frac;
imp.save_reliables = this.save_reliables;
......@@ -7911,7 +7926,9 @@ min_str_neib_fpn 0.35
imp.sfm_filter = this.sfm_filter;
imp.sfm_minmax = this.sfm_minmax;
imp.sfm_fracmax = this.sfm_fracmax;
imp.sfm_minabs = this.sfm_minabs;
imp.sfm_fracall = this.sfm_fracall;
imp.min_ref_str_sfm = this.min_ref_str_sfm;
imp.pix_step = this.pix_step;
imp.search_rad = this.search_rad;
......@@ -8734,6 +8751,15 @@ min_str_neib_fpn 0.35
return s;
}
public static boolean [] StringToBooleans(String s, boolean [] dflt){
boolean [] b = StringToBooleans(s);
if (b.length != dflt.length) {
return dflt.clone();
} else {
return b;
}
}
public static boolean [] StringToBooleans(String s) {
return StringToBooleans(s, -1);
}
......
......@@ -2046,12 +2046,12 @@ public class QuadCLTCPU {
* If this scene is a reference scene, then return array of {first_index, last_index}.
* If this scene is not a reference - return null.
* @param scenes array of scenes (QuadCLTCPU instances)
* @return a pair of {first_index, last_index}
* @return a pair of {first_index, last_index, this_index}
*/
public int [] getFirstLastIndex(QuadCLTCPU [] scenes) { // should be ordered accending ts
int [] fl = null;
if ((timestamp_first != null) && (timestamp_last != null)) {
fl = new int [] {-1,-1};
fl = new int [] {-1,-1,-1};
if (scenes != null) {
int i = 0;
for (; i < scenes.length; i++) if (scenes[i] != null) {
......@@ -2060,6 +2060,12 @@ public class QuadCLTCPU {
break;
}
}
for (; i < scenes.length; i++) if (scenes[i] != null) {
if (this.getImageName().equals(scenes[i].getImageName())) {
fl[2] = i; // reference index
break;
}
}
for (; i < scenes.length; i++) if (scenes[i] != null) {
if (timestamp_last.equals(scenes[i].getImageName())) {
fl[1] = i;
......@@ -2071,6 +2077,20 @@ public class QuadCLTCPU {
return fl;
}
public int getRefIndex(String [] scene_names) { // should be ordered accending ts
String ts = timestamp_reference;
if (scene_names != null) {
for (int i=0; i < scene_names.length; i++) if (scene_names[i] != null) {
if (ts.equals(scene_names[i])) {
return i;
}
}
}
return -1;
}
public int [] getFirstLastIndex(String [] scene_names) { // should be ordered accending ts
int [] fl = null;
if ((timestamp_first != null) && (timestamp_last != null)) {
......@@ -5241,7 +5261,9 @@ public class QuadCLTCPU {
boolean sfm_filter,
double sfm_minmax,
double sfm_fracmax,
double sfm_fracall,
double sfm_minabs,
double sfm_fracall,
double min_ref_str_sfm,
double [] reduced_strength, // if not null will return >0 if had to reduce strength (no change if did not reduce)
int debugLevel
) {
......@@ -5270,7 +5292,9 @@ public class QuadCLTCPU {
sfm_filter, // boolean sfm_filter,
sfm_minmax, // double sfm_minmax,
sfm_fracmax, // double sfm_fracmax,
sfm_fracall, // double sfm_fracall,
sfm_minabs, // double sfm_minabs,
sfm_fracall, // double sfm_fracall,
min_ref_str_sfm, // double min_ref_str_sfm,
reduced_strength, // double [] reduced_strength, // if not null will return >0 if had to reduce strength (no change if did not reduce)
disparity_lma, // double [] disparity_lma,
strength, // double [] strength,
......@@ -5286,7 +5310,9 @@ public class QuadCLTCPU {
boolean sfm_filter,
double sfm_minmax,
double sfm_fracmax,
double sfm_fracall,
double sfm_minabs,
double sfm_fracall,
double min_ref_str_sfm,
double [] reduced_strength, // if not null will return >0 if had to reduce strength (no change if did not reduce)
double [] disparity_lma,
double [] strength,
......@@ -5319,24 +5345,30 @@ public class QuadCLTCPU {
if (sfm_gain[i] > sfm_max) sfm_max = sfm_gain[i];
}
if (sfm_max > sfm_minmax) {
double sfm_thresh = sfm_max * sfm_fracmax;
double sfm_thresh = Math.max(sfm_minabs, sfm_max * sfm_fracmax);
boolean [] sfm_good = new boolean [reliable.length];
int num_sfm_gain = 0;
for (int i = 0; i < reliable.length; i++) {
if (sfm_gain[i] >= sfm_thresh) {
sfm_good[i] = true;
num_sfm_gain++;
if (strength[i] >= min_ref_str_sfm) {
sfm_good[i] = true;
num_sfm_gain++;
}
}
}
if (num_sfm_gain > (sfm_fracall * reliable.length)) {
if (num_sfm_gain > (sfm_fracall * reliable.length)) { // enough tiles have SfM -> discard all others
// set threshold to sfm-based threshold
// min_strength = min_ref_str_sfm; // not needed, it is not used
if (debugLevel > -3) {
System.out.println("getReliableTiles(): Using SfM filter");
}
for (int i = 0; i < reliable.length; i++) {
reliable[i] = sfm_good[i];
if (!sfm_good[i]) { // sfm_gain[i] <= 0){
reliable[i] = false;
// reliable[i] = false;
strength[i] = 0.0;
}
}
}
}
......@@ -5349,7 +5381,7 @@ public class QuadCLTCPU {
if (num_reliable < min_reliable) { // not enough, select best tiles, ignoring LMA
double max_str = 0;
for (double s:strength) if ((s > max_str) )max_str= s; // NaN OK
for (double s:strength) if ((s > max_str) ) max_str= s; // NaN OK
if (max_str == 0) return null;
int [] hist = new int[NUM_BINS];
int num_gt0 = 0;
......@@ -6023,6 +6055,7 @@ LogTee.clearSceneLog(); // stop per‑scene logging
(new String[]{"disparity","strength", "R","B","G"});
}
@Deprecated
public void setDSRBG(
CLTParameters clt_parameters,
int threadsMax, // maximal number of threads to launch
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment