Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
I
imagej-elphel
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
3
Issues
3
List
Board
Labels
Milestones
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Commits
Issue Boards
Open sidebar
Elphel
imagej-elphel
Commits
479b64e0
Commit
479b64e0
authored
May 04, 2026
by
Andrey Filippov
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: Implement CuasTargetsAnalyze MCP endpoint and fix calcMatchingTargetsLengths bug
parent
ca8ad02a
Changes
4
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
556 additions
and
29 deletions
+556
-29
mcp_compare_targets.py
scripts/mcp_compare_targets.py
+265
-0
Eyesis_Correction.java
.../java/com/elphel/imagej/correction/Eyesis_Correction.java
+11
-10
CuasMotion.java
src/main/java/com/elphel/imagej/cuas/CuasMotion.java
+242
-19
McpServer.java
src/main/java/com/elphel/imagej/mcp/McpServer.java
+38
-0
No files found.
scripts/mcp_compare_targets.py
0 → 100755
View file @
479b64e0
#!/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
())
src/main/java/com/elphel/imagej/correction/Eyesis_Correction.java
View file @
479b64e0
...
...
@@ -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
(),
""
);
}
}
src/main/java/com/elphel/imagej/cuas/CuasMotion.java
View file @
479b64e0
This diff is collapsed.
Click to expand it.
src/main/java/com/elphel/imagej/mcp/McpServer.java
View file @
479b64e0
...
...
@@ -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
""
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment