Сортировочная ячейка Isaac Sim: CV-пайплайн и меши товаров

Замкнутый контур "поток -> CV -> механика": товары идут по конвейеру с шагом 700 мм,
класс определяется стереопайплайном во время движения, пушер и плуг реагируют физически.

Состав:
* control_test/ - ячейка и CV. run_sorting_cv.py + cv_worker.py (два процесса, потому что
  torch внутри Isaac роняет сцену), cell.py (физика лент, плуга, пушера), measure_plane.py
  (замер габаритов), README.md и .memory.md с замерами, проблемами и ловушками
* robozon_sorter/ - модули симуляции, scripts/ - утилиты, scene/ - сцены
* assets/ - меши товаров, плуг, объекты Objaverse

Бейзлайн CV: DEFOM-Stereo vitl, вход 480, iters 24, кроп зоны осмотра, без сегментации.
На потоке 700 мм - классы 8/9, габариты MAE 32.8 мм, 469 мс на товар при такте 700 мс.

Веса моделей (4.5 ГБ) и пропсы конвейера NVIDIA (274 МБ) не включены - источники и
команды скачивания в MODELS.md. Выход прогонов (captures/, runtime/) не включён:
воспроизводится.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
dasha_f
2026-08-01 13:07:24 +00:00
parent 6ce460378a
commit 0d32f32db0
342 changed files with 18000 additions and 0 deletions
+316
View File
@@ -0,0 +1,316 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Send Python code to a running Isaac Sim instance via the python_server TCP socket.
Usage:
# Inline code
python isaacsim_send.py 'print("hello")'
# From stdin (pipe or heredoc)
echo 'print("hello")' | python isaacsim_send.py
# Send a .py file
python isaacsim_send.py --file path/to/script.py
# Send a file with injected variables
python isaacsim_send.py --file script.py --arg output_path=/tmp/shot.png --arg width=1920
# Custom host/port/timeout
python isaacsim_send.py --host 127.0.0.1 --port 8226 --timeout 120 'print("hello")'
# Raw JSON output (default is formatted human-readable)
python isaacsim_send.py --raw 'print("hello")'
# --file scripts are isolated by default (wrapped in a function scope).
# Use --context for persistent named namespaces:
python isaacsim_send.py --context recording --file setup.py
python isaacsim_send.py --context recording 'print(my_var)'
# JSON envelope mode: named context, injected args, per-request timeout
python isaacsim_send.py --context my_session --execution-timeout 30 'x = 1'
python isaacsim_send.py --args-json '{"x": 42}' 'print(x)'
# Fire-and-forget: get immediate ACK and task_id
python isaacsim_send.py --fire-and-forget 'import time; time.sleep(5)'
# Introspection queries
python isaacsim_send.py --introspect status
python isaacsim_send.py --introspect contexts
python isaacsim_send.py --introspect tasks
Exit codes:
0 - Execution succeeded (status: "ok")
1 - Execution failed (status: "error") or connection error
"""
import argparse
import asyncio
import json
import sys
async def send_and_receive(host: str, port: int, source: str, timeout: float = 60.0) -> dict:
"""Send Python source code or a JSON envelope and return the parsed JSON response."""
reader, writer = await asyncio.wait_for(asyncio.open_connection(host, port), timeout=timeout)
writer.write(source.encode())
writer.write_eof()
data = await asyncio.wait_for(reader.read(), timeout=timeout)
writer.close()
return json.loads(data.decode())
def _inject_args(source: str, args: list[str]) -> str:
"""Prepend variable assignments for --arg key=value pairs."""
if not args:
return source
lines = []
for arg in args:
key, _, value = arg.partition("=")
key = key.strip()
value = value.strip()
try:
parsed = eval(value, {"__builtins__": {}}) # noqa: S307
lines.append(f"{key} = {repr(parsed)}")
except Exception:
lines.append(f'{key} = "{value}"')
return "\n".join(lines) + "\n" + source
def _wrap_isolated(source: str, args: list[str]) -> str:
"""Wrap script in an async function scope to isolate from executor state.
The python_server executor shares global state between connections.
Wrapping in a function prevents variable leakage between calls.
Top-level `await` expressions are supported inside the wrapper.
"""
# Build argument assignments
arg_lines = []
for arg in args:
key, _, value = arg.partition("=")
key = key.strip()
value = value.strip()
try:
parsed = eval(value, {"__builtins__": {}}) # noqa: S307
arg_lines.append(f" {key} = {repr(parsed)}")
except Exception:
arg_lines.append(f' {key} = "{value}"')
# Indent the source
indented = "\n".join(" " + line for line in source.splitlines())
parts = ["async def _isolated_script():"]
if arg_lines:
parts.extend(arg_lines)
parts.append(indented)
parts.append("")
parts.append("await _isolated_script()")
return "\n".join(parts)
def _parse_args_kv(arg_list: list[str]) -> dict:
"""Convert a list of ``key=value`` strings to a dict with type inference."""
result = {}
for arg in arg_list:
key, _, value = arg.partition("=")
key = key.strip()
value = value.strip()
try:
result[key] = eval(value, {"__builtins__": {}}) # noqa: S307
except Exception:
result[key] = value
return result
def _needs_envelope(args: argparse.Namespace) -> bool:
"""Return whether any flag requires the JSON envelope format."""
return bool(
args.json_envelope
or args.context
or args.fire_and_forget
or args.execution_timeout is not None
or args.args_json
or args.introspect
)
async def main() -> None:
parser = argparse.ArgumentParser(description="Send Python code to Isaac Sim python_server")
parser.add_argument("code", nargs="?", help="Python code to execute (reads stdin if omitted)")
parser.add_argument("--file", "-f", help="Path to a Python file to send instead of inline code")
parser.add_argument(
"--arg", "-a", action="append", default=[], help="Inject key=value as a global variable (use with --file)"
)
parser.add_argument("--host", default="127.0.0.1", help="Server host (default: 127.0.0.1)")
parser.add_argument("--port", type=int, default=8226, help="Server port (default: 8226)")
parser.add_argument("--timeout", type=float, default=60.0, help="Client TCP timeout in seconds (default: 60)")
parser.add_argument("--raw", action="store_true", help="Print raw JSON instead of formatted output")
parser.add_argument(
"--no-isolate",
action="store_true",
help="Don't wrap --file scripts in isolated scope (use for state persistence)",
)
# JSON envelope options
envelope_group = parser.add_argument_group("JSON envelope options")
envelope_group.add_argument("--json-envelope", "-j", action="store_true", help="Force JSON envelope mode")
envelope_group.add_argument(
"--context", "-c", help="Named execution context (creates or reuses a persistent namespace)"
)
envelope_group.add_argument(
"--fire-and-forget",
"--ff",
action="store_true",
dest="fire_and_forget",
help="Fire-and-forget mode: receive immediate ACK with task_id, code runs in background",
)
envelope_group.add_argument(
"--execution-timeout",
"-t",
type=float,
metavar="SECONDS",
dest="execution_timeout",
help="Per-request server-side execution timeout (0 = no limit; different from --timeout)",
)
envelope_group.add_argument(
"--args-json",
metavar="JSON",
help="Inject variables as a JSON object string, e.g. '{\"x\": 42}'",
)
envelope_group.add_argument(
"--introspect",
metavar="COMMAND",
help=(
"Run an introspection query instead of executing code. "
"Commands: status, contexts, context, tasks, task, delete_context"
),
)
args = parser.parse_args()
# Handle introspection queries
if args.introspect:
envelope: dict = {"introspect": args.introspect}
if args.context:
envelope["context"] = args.context
if args.code:
# Allow passing a task_id as a positional argument for 'task' queries
envelope["task_id"] = args.code
source = json.dumps(envelope)
try:
result = await send_and_receive(args.host, args.port, source, args.timeout)
except ConnectionRefusedError:
print(f"Error: Cannot connect to Isaac Sim at {args.host}:{args.port}", file=sys.stderr)
sys.exit(1)
except asyncio.TimeoutError:
print(f"Error: Timeout after {args.timeout}s waiting for response", file=sys.stderr)
sys.exit(1)
if args.raw:
print(json.dumps(result, indent=2))
else:
if result.get("status") == "ok":
print(json.dumps(result.get("result"), indent=2))
else:
print(f"ERROR: {result.get('result', result)}", file=sys.stderr)
sys.exit(1)
return
# Build code string
if args.file:
with open(args.file) as f:
source_code = f.read()
if args.no_isolate or _needs_envelope(args):
source_code = _inject_args(source_code, args.arg)
else:
source_code = _wrap_isolated(source_code, args.arg)
elif args.code:
source_code = args.code
if not _needs_envelope(args):
source_code = _inject_args(source_code, args.arg)
else:
source_code = sys.stdin.read()
if not source_code.strip():
print("Error: No code provided", file=sys.stderr)
sys.exit(1)
# Build the final request: JSON envelope or raw Python
if _needs_envelope(args):
envelope = {"code": source_code}
if args.context:
envelope["context"] = args.context
if args.fire_and_forget:
envelope["fire_and_forget"] = True
if args.execution_timeout is not None:
envelope["timeout"] = args.execution_timeout
# Merge args from --arg key=value pairs and --args-json
merged_args: dict = _parse_args_kv(args.arg)
if args.args_json:
try:
json_args = json.loads(args.args_json)
if isinstance(json_args, dict):
merged_args.update(json_args)
else:
print("Error: --args-json must be a JSON object", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as exc:
print(f"Error: Invalid --args-json: {exc}", file=sys.stderr)
sys.exit(1)
if merged_args:
envelope["args"] = merged_args
source = json.dumps(envelope)
else:
source = source_code
try:
result = await send_and_receive(args.host, args.port, source, args.timeout)
except ConnectionRefusedError:
print(f"Error: Cannot connect to Isaac Sim at {args.host}:{args.port}", file=sys.stderr)
print("Make sure Isaac Sim is running with isaacsim.code_editor.python_server enabled.", file=sys.stderr)
sys.exit(1)
except asyncio.TimeoutError:
print(f"Error: Timeout after {args.timeout}s waiting for response", file=sys.stderr)
sys.exit(1)
if args.raw:
print(json.dumps(result, indent=2))
else:
# Fire-and-forget: show task_id prominently
if result.get("fire_and_forget"):
print(f"Task submitted. task_id: {result.get('task_id')}")
sys.exit(0)
output = result.get("output", "")
if output:
print(output)
if "result" in result and result["result"] is not None:
print(f"=> {result['result']}")
if result.get("status") == "error":
print(f"\nERROR [{result.get('ename', '?')}]: {result.get('evalue', '?')}", file=sys.stderr)
for tb in result.get("traceback", []):
print(tb, file=sys.stderr)
if result.get("elapsed_seconds") is not None:
print(f"(elapsed: {result['elapsed_seconds']:.2f}s)", file=sys.stderr)
sys.exit(0 if result.get("status") == "ok" else 1)
if __name__ == "__main__":
asyncio.run(main())