Commit 479b64e0 authored by Andrey Filippov's avatar Andrey Filippov

feat: Implement CuasTargetsAnalyze MCP endpoint and fix calcMatchingTargetsLengths bug

parent ca8ad02a
#!/usr/bin/env python3
"""Compare CUAS target sections through the ImageJ MCP HTTP endpoint."""
import argparse
import json
import math
import sys
from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple
import requests
DEFAULT_URL = "http://127.0.0.1:48888/mcp/cuas/targets"
PARAMS: List[Tuple[str, int]] = [
("RSLT_X", 0),
("RSLT_Y", 1),
("RSLT_VX", 16),
("RSLT_VY", 17),
("RSLT_VSTR", 18),
("RSLT_BX", 20),
("RSLT_BY", 21),
("RSLT_AX", 22),
("RSLT_AY", 23),
("RSLT_MISMATCH_BEFORE", 24),
("RSLT_MISMATCH_AFTER", 25),
("RSLT_MISMATCH_DIRS", 26),
("RSLT_MATCH_LENGTH", 27),
("RSLT_BEFORE_LENGTH", 28),
("RSLT_AFTER_LENGTH", 29),
("RSLT_SEQ_TRAVEL", 30),
("RSLT_MSCORE", 31),
("RSLT_QMATCH", 36),
("RSLT_QMATCH_LEN", 37),
("RSLT_QTRAVEL", 38),
("RSLT_QSCORE", 39),
("RSLT_SLOW", 41),
("RSLT_FAIL", 43),
("RSLT_GLOBAL", 48),
]
PARAM_BY_INDEX = {index: name for name, index in PARAMS}
CORE_FIELDS = [
"RSLT_MATCH_LENGTH",
"RSLT_BEFORE_LENGTH",
"RSLT_AFTER_LENGTH",
"RSLT_MISMATCH_BEFORE",
"RSLT_MISMATCH_AFTER",
"RSLT_SEQ_TRAVEL",
"RSLT_QMATCH",
"RSLT_QMATCH_LEN",
"RSLT_QTRAVEL",
"RSLT_QSCORE",
"RSLT_GLOBAL",
]
TargetKey = Tuple[int, int, int, int]
TargetRecord = Dict[str, Any]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Query BEFORE and AFTER CUAS target TIFFs through /mcp/cuas/targets "
"and print per-target matching length changes."
)
)
parser.add_argument("--file-before", required=True, help="Path to TARGETS_SINGLE-FIRST TIFF")
parser.add_argument("--file-after", required=True, help="Path to MATCHING_LENGTHS2 TIFF")
parser.add_argument("--tx", type=int, required=True, help="Tile X coordinate")
parser.add_argument("--ty", type=int, required=True, help="Tile Y coordinate")
parser.add_argument("--seq0", type=int, required=True, help="First sequence index")
parser.add_argument("--seq1", type=int, required=True, help="Last sequence index")
parser.add_argument("--tw", type=int, default=1, help="Tile-section width; default: 1")
parser.add_argument("--th", type=int, default=1, help="Tile-section height; default: 1")
parser.add_argument("--url", default=DEFAULT_URL, help=f"MCP endpoint URL; default: {DEFAULT_URL}")
parser.add_argument("--timeout", type=float, default=60.0, help="HTTP timeout in seconds")
parser.add_argument(
"--json",
action="store_true",
help="Print normalized JSON instead of the text report",
)
return parser.parse_args()
def query_targets(
url: str,
target_path: str,
args: argparse.Namespace,
) -> Mapping[str, Any]:
payload = {
"target_path": target_path,
"queries": [
{
"mode": "section",
"tx": args.tx,
"ty": args.ty,
"tw": args.tw,
"th": args.th,
"nseq0": args.seq0,
"nseq1": args.seq1,
"skip_empty": False,
"params": [index for _, index in PARAMS],
}
],
}
response = requests.post(url, json=payload, timeout=args.timeout)
response.raise_for_status()
data = response.json()
if not data.get("ok", False):
raise RuntimeError(f"MCP request failed for {target_path}: {data}")
return data
def normalize_response(data: Mapping[str, Any]) -> Dict[TargetKey, TargetRecord]:
results = data.get("results") or []
if not results:
return {}
section = results[0]
if "error" in section:
raise RuntimeError(str(section["error"]))
records: Dict[TargetKey, TargetRecord] = {}
for seq in section.get("sequences", []):
nseq = int(seq["nseq"])
for tile in seq.get("tiles", []):
tx = int(tile["tx"])
ty = int(tile["ty"])
for ntarg, target in enumerate(tile.get("targets", [])):
named = rename_params(target)
named.update({"nseq": nseq, "tx": tx, "ty": ty, "ntarg": ntarg})
records[(nseq, tx, ty, ntarg)] = named
return records
def rename_params(target: Mapping[str, Any]) -> TargetRecord:
renamed: TargetRecord = {}
for key, value in target.items():
try:
numeric_key = int(key)
except (TypeError, ValueError):
renamed[key] = value
continue
renamed[PARAM_BY_INDEX.get(numeric_key, key)] = value
return renamed
def fmt_value(value: Any) -> str:
if value is None:
return "-"
if isinstance(value, float):
if math.isnan(value):
return "NaN"
if math.isinf(value):
return "Inf" if value > 0 else "-Inf"
return f"{value:.6g}"
return str(value)
def fmt_delta(before: Any, after: Any) -> str:
if before is None or after is None:
return ""
if isinstance(before, (int, float)) and isinstance(after, (int, float)):
delta = after - before
if delta == 0:
return ""
return f" ({delta:+.6g})"
if before != after:
return " (changed)"
return ""
def iter_changed_fields(before: Optional[TargetRecord], after: Optional[TargetRecord]) -> Iterable[str]:
for field in CORE_FIELDS:
bval = before.get(field) if before else None
aval = after.get(field) if after else None
if bval != aval:
yield f"{field}: {fmt_value(bval)} -> {fmt_value(aval)}{fmt_delta(bval, aval)}"
def print_report(
before: Mapping[TargetKey, TargetRecord],
after: Mapping[TargetKey, TargetRecord],
args: argparse.Namespace,
) -> None:
keys = sorted(set(before) | set(after))
print(f"Endpoint: {args.url}")
print(f"BEFORE: {args.file_before}")
print(f"AFTER: {args.file_after}")
print(f"Section: tx={args.tx}, ty={args.ty}, tw={args.tw}, th={args.th}, seq={args.seq0}..{args.seq1}")
print(f"Targets: before={len(before)}, after={len(after)}, union={len(keys)}")
print()
if not keys:
print("No targets returned for this section.")
return
for key in keys:
nseq, tx, ty, ntarg = key
btarget = before.get(key)
atarget = after.get(key)
status = "changed"
if btarget is None:
status = "after-only"
elif atarget is None:
status = "before-only"
elif not list(iter_changed_fields(btarget, atarget)):
status = "unchanged"
print(f"nseq={nseq} tile=({tx},{ty}) ntarg={ntarg} [{status}]")
print(" kinematics:")
for field in ("RSLT_BX", "RSLT_BY", "RSLT_AX", "RSLT_AY", "RSLT_VX", "RSLT_VY", "RSLT_VSTR", "RSLT_SLOW", "RSLT_FAIL"):
bval = btarget.get(field) if btarget else None
aval = atarget.get(field) if atarget else None
print(f" {field}: {fmt_value(bval)} -> {fmt_value(aval)}{fmt_delta(bval, aval)}")
changed = list(iter_changed_fields(btarget, atarget))
if changed:
print(" matching:")
for line in changed:
print(f" {line}")
else:
print(" matching: no core-field changes")
print()
def jsonable_records(records: Mapping[TargetKey, TargetRecord]) -> Dict[str, TargetRecord]:
return {
f"{nseq}:{tx}:{ty}:{ntarg}": record
for (nseq, tx, ty, ntarg), record in sorted(records.items())
}
def main() -> int:
args = parse_args()
try:
before_raw = query_targets(args.url, args.file_before, args)
after_raw = query_targets(args.url, args.file_after, args)
before = normalize_response(before_raw)
after = normalize_response(after_raw)
except requests.RequestException as exc:
print(f"HTTP error querying MCP endpoint: {exc}", file=sys.stderr)
return 2
except (ValueError, RuntimeError, KeyError, TypeError) as exc:
print(f"Error processing MCP response: {exc}", file=sys.stderr)
return 3
if args.json:
print(
json.dumps(
{"before": jsonable_records(before), "after": jsonable_records(after)},
indent=2,
sort_keys=True,
default=str,
)
)
else:
print_report(before, after, args)
return 0
if __name__ == "__main__":
raise SystemExit(main())
......@@ -13503,9 +13503,10 @@ public class Eyesis_Correction implements PlugIn, ActionListener {
String pluginsDir = url.substring(5, url.length() - clazz.getName().length() - 6);
System.setProperty("plugins.dir", pluginsDir);
// start ImageJ
if (!GraphicsEnvironment.isHeadless()) {
new ImageJ();
}
// run the plugin
IJ.runPlugIn(clazz.getName(), "");
}
}
......@@ -105,6 +105,7 @@ public class McpServer {
server.createContext("/mcp/fs/glob", new FsGlobHandler());
server.createContext("/mcp/fs/csvcol", new FsCsvColHandler());
server.createContext("/mcp/rag/query", new RagQueryHandler());
server.createContext("/mcp/cuas/targets", new CuasTargetsHandler());
server.setExecutor(null);
server.start();
if (Eyesis_Correction.MCP_DEBUG_LEVEL >= Eyesis_Correction.MINIMAL_DEBUG_MCP) {
......@@ -831,6 +832,43 @@ public class McpServer {
}
}
private class CuasTargetsHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
try {
java.io.InputStream is = exchange.getRequestBody();
java.io.ByteArrayOutputStream buffer = new java.io.ByteArrayOutputStream();
int nRead;
byte[] data = new byte[1024];
while ((nRead = is.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
buffer.flush();
String requestBody = new String(buffer.toByteArray(), StandardCharsets.UTF_8);
org.json.simple.parser.JSONParser parser = new org.json.simple.parser.JSONParser();
org.json.simple.JSONObject req = (org.json.simple.JSONObject) parser.parse(requestBody);
String targetPath = (String) req.get("target_path");
org.json.simple.JSONArray queries = (org.json.simple.JSONArray) req.get("queries");
if (targetPath == null || queries == null) {
sendJson(exchange, 400, "{\"ok\":false,\"error\":\"Missing target_path or queries\"}");
return;
}
org.json.simple.JSONArray results = com.elphel.imagej.cuas.CuasMotion.targetsAnalyzeMCP(targetPath, queries);
org.json.simple.JSONObject response = new org.json.simple.JSONObject();
response.put("ok", true);
response.put("results", results);
sendJson(exchange, 200, response.toJSONString());
} catch (Exception e) {
String detail = jsonEscape(e.getMessage());
sendJson(exchange, 500, "{\"ok\":false,\"error\":\"Failed to process request\",\"detail\":\"" + detail + "\"}");
}
}
}
private static String jsonEscape(String value) {
if (value == null) {
return "";
......
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