From 4358857cf23d8c41541c9076b783e61e37e9d547 Mon Sep 17 00:00:00 2001 From: chk <79915315+ChKendel@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:34:43 +0200 Subject: [PATCH] =?UTF-8?q?Claude=20=C3=BCbernimmt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 29 +- config/robot.json | 468 ++++++++++++++++++ pyproject.toml | 4 +- scripts/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 1073 bytes scripts/__pycache__/pipeline.cpython-311.pyc | Bin 0 -> 11012 bytes scripts/api/__init__.py | 2 +- scripts/api/__main__.py | 2 +- scripts/api/server.py | 22 +- ...test_pipeline.cpython-311-pytest-9.0.3.pyc | Bin 0 -> 9403 bytes 9 files changed, 515 insertions(+), 12 deletions(-) create mode 100644 config/robot.json create mode 100644 scripts/__pycache__/__init__.cpython-311.pyc create mode 100644 scripts/__pycache__/pipeline.cpython-311.pyc create mode 100644 tests/__pycache__/test_pipeline.cpython-311-pytest-9.0.3.pyc diff --git a/README.md b/README.md index b246c6c..48bfe77 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,14 @@ print(resp.json()["joints"]) | `/v1/health` | GET | Status | | `/v1/config` | GET | aktive Konfiguration | +**Maschinenlesbares Schema / interaktive Doku** (automatisch von FastAPI): + +| URL | Zweck | +|-----|-------| +| `/openapi.json` | OpenAPI-Spezifikation (für Client-Generatoren) | +| `/docs` | Swagger-UI (interaktiv ausprobieren) | +| `/redoc` | ReDoc-Ansicht | + **Response:** ```json @@ -108,14 +116,27 @@ print(resp.json()["joints"]) ## Deployment (Docker / Portainer) -**Volume:** +> **Pflichtschritt zuerst:** `config/robot.json` muss als **Datei** existieren, bevor +> der Container startet. Vorlage kopieren und mit der echten Konfiguration füllen: +> +> ```bash +> cp config/robot.json.example config/robot.json +> ``` +> +> ⚠️ Fehlt die Datei, legt Docker am Mount-Pfad ein **leeres Verzeichnis** an. +> Der Server startet dann zwar, aber jeder `/v1/estimate` liefert **500** +> (`IsADirectoryError`) und `/v1/config` ebenfalls 500. Genau dann diesen +> Pflichtschritt nachholen und den Container neu starten. + +**Volume** (Pfad muss zur tatsächlichen Datei zeigen, vgl. `docker-compose.yaml`): ```yaml -- /opt/approbot/config/robot.json:/config/robot.json:ro +- ./config/robot.json:/config/robot.json:ro ``` -**Healthcheck:** +**Start & Healthcheck:** ```bash -curl http://:8446/v1/health +docker compose up -d +curl http://:8446/v1/health # {"status":"ok","version":"1.0.0"} ``` --- diff --git a/config/robot.json b/config/robot.json new file mode 100644 index 0000000..4ed30f3 --- /dev/null +++ b/config/robot.json @@ -0,0 +1,468 @@ +{ + "coordinateSystem": {"handedness": "right", "x": "right", "y": "backward", "z": "up"}, + "units": {"length": "mm", "rotation": "degree"}, + "vision_config": {"MarkerType": "DICT_4X4_250", "MarkerSize": 0.025}, + "renderingInfo": { + "width": 1280, + "height": 720, + "renderDefaults": {"width": 1280, "height": 720, "dofFStop": 11}, + "cameraPosition__1": [-10, -800, 500], + "cameraPosition__2": [-500, 300, 1200], + "cameraPosition__3": [-200, -900, 200], + "cameraPosition__4": [1200, 200, 300], + "cameraPosition_a": [-300, -800, 500], + "cameraPosition": [-200, 200, 1400], + "cameraPosition_c": [600, -500, 600], + "cameraTarget": [200, -200, 180], + "cameraUpVector": [0, 0, 1], + "lightPosition": [-500, -500, 500], + "lightTarget": [0, 0, 0], + "lightUpVector": [0, 0, 1], + "metric": "mm", + "showSkeleton": true, + "showMarkers": true, + "backgroundColor": [0.7, 0.85, 1.0], + "backgroundStrength": 0.2, + "sunEnergy": 0.35, + "areaEnergy": 120, + "exposure": -1.5, + "lensDirt": true, + "lensDirtStrength": 0.08, + "dofEnabled": true, + "dofFStop": 11.0, + "arucoDust": true, + "arucoDustStrength": 1.6, + "markerOffsetMaxMm": 4.0, + "markerOffsetSeed": 0, + "markerRotationMaxDeg": 3, + "motionBlur": true, + "motionBlurMaxPx": 5.5, + "focalErrorPct": 0.5, + "principalErrorPx": 3.0, + "residualDistortion": [0.02, -0.01], + "localizedBlur": false, + "localizedBlurStrength": 0.15, + "vignette": true, + "vignetteStrength": 0.08, + "sensorNoise": true, + "sensorNoiseStrength": 0.01, + "lensDistortion": true, + "lensDistortionStrength": 0.002, + "materials": { + "wood": {"baseColor": [0.72, 0.52, 0.33], "roughness": 0.85, "metallic": 0.0}, + "plaWhite": {"baseColor": [0.95, 0.95, 0.95], "roughness": 0.45, "metallic": 0.0}, + "steel": {"baseColor": [0.72, 0.72, 0.75], "roughness": 0.25, "metallic": 1.0}, + "powderCoatBlue": {"baseColor": [0.15, 0.25, 0.7], "roughness": 0.55, "metallic": 0.0}, + "defaultPlastic": {"baseColor": [0.95, 0.95, 0.95], "roughness": 0.4, "metallic": 0.0}, + "skeletonRed": {"baseColor": [0.85, 0.2, 0.2], "roughness": 0.35, "metallic": 0.0}, + "markerBlack": {"baseColor": [0.04, 0.04, 0.04], "roughness": 0.8, "metallic": 0.0} + }, + "skeletonDefaults": {"radius": 4, "color": [0.85, 0.2, 0.2]}, + "markerDefaults": {"size": 25, "thickness": 1, "color": [0.04, 0.04, 0.04]}, + "defaultPosition": {"x": 80, "y": 20, "z": 80, "a": -120, "b": 23, "c": 9, "e": 3} + }, + "defaultPosition__": {"x": 10, "y": 4, "z": 20, "a": 10, "b": 2, "c": 9, "e": 1}, + "defaultPosition": {"x": 50, "y": 4, "z": 176, "a": 20, "b": 60, "c": 9, "e": 5}, + "recognized": {"x": null, "y": null, "z": null, "a": null, "b": null, "c": null, "e": null}, + "constraint_rules": { + "rigid_distance": {"enabled": true, "mode": "mst", "weight": 1.0}, + "joint_axis_projection": {"enabled": true, "max_pairs": 2, "weight": 0.35}, + "chain_axis_projection": {"enabled": false, "max_depth": 3, "max_pairs": 2, "weight": 0.15}, + "axis_alignment_threshold": 0.95 + }, + "observation_weighting": {"enabled": true, "distance_weight": true, "marker_size_weight": true, "view_angle_weight": true}, + "multiview_calculation": { + "combine_mode": "mean", + "size_ref_px": 50.0, + "border_ref_px": 120.0, + "center_ref_norm": 0.01, + "sharpness_ref": 2500.0, + "homography_ref": 0.18, + "size_factor": 0.3, + "aspect_factor": 0.3, + "border_factor": 0.01, + "center_factor": 0.01, + "sharpness_factor": 0.5, + "homography_factor": 0.2, + "normal_visibility_factor": 0.01, + "spin_factor": 0.3, + "weight_floor": 0.3 + }, + "pose_estimation": { + "method": "hybrid", + "marker_observation": "corner_pose", + "use_normals": true, + "normal_weight": 100.0, + "robust_loss": "huber", + "huber_delta_mm": 8.0, + "max_iterations": 200, + "min_cameras_per_marker": 2, + "finger_block_joints": ["b", "c", "e"], + "per_link_method": {} + }, + "robot_test_poses": { + "4": {"x": 70, "y": 50, "z": -70, "a": 120, "b": 50, "c": 30, "e": 20}, + "5": {"x": 180, "y": 86, "z": -120, "a": -60, "b": 22, "c": 91, "e": 10}, + "6": {"x": 80, "y": 20, "z": 80, "a": -120, "b": 23, "c": 9, "e": 3}, + "7": {"x": 30, "y": -2, "z": 95, "a": 20, "b": 23, "c": 9, "e": 9}, + "8": {"x": 50, "y": -2, "z": 95, "a": 20, "b": 60, "c": 9, "e": 3}, + "9": {"x": 60, "y": -2, "z": 95, "a": 200, "b": 60, "c": 9, "e": 8}, + "9a": { + "x": 60, + "y": -2, + "z": 95, + "a": 200, + "b": 60, + "c": 9, + "e": 8, + "rendering": {"width": 1440, "height": 1080, "dofFStop": 11} + }, + "9b": { + "x": 60, + "y": -2, + "z": 95, + "a": 200, + "b": 60, + "c": 9, + "e": 8, + "rendering": {"width": 4896, "height": 3264, "dofFStop": 5.6} + }, + "10": {"x": 120, "y": 60, "z": -110, "a": 20, "b": 30, "c": 180, "e": 4}, + "11": {"x": 50, "y": 4, "z": 176, "a": 20, "b": 60, "c": 9, "e": 5}, + "12": {"x": 50, "y": 0, "z": 178, "a": 210, "b": 80, "c": 90, "e": 6} + }, + "test_camera_positions": { + "a": [-300, -800, 800], + "b": [300, -900, 1200], + "c": [300, -900, 400], + "d": [700, -800, 400], + "e": [1200, -900, 400], + "f": [500, -300, 1400], + "g": [-200, 200, 1400] + }, + "test_camera_targets": { + "a": [210, -100, 180], + "b": [310, -80, 180], + "c": [210, -100, 150], + "d": [210, -100, 150], + "e": [210, -100, 50], + "f": [200, -200, 180], + "g": [200, -200, 180] + }, + "movements": {"x": null, "y": null, "z": null, "a": null, "b": null, "c": null, "e": null}, + "state_pose_params": { + "numbers_of_Elements_to_consider_start": 3, + "numbers_of_Elements_to_consider_final": 5, + "solver_in_between_geometrical": false, + "solver_after_geometrical": false, + "geometric_passes_per_stage": 2, + "revolute_search_coarse_deg": 5.0, + "revolute_search_fine_deg": 1.0, + "root_pose_min_markers": 3, + "use_marker_normals_flip_tiebreak": true, + "normal_flip_weight": 0.05 + }, + "links": { + "Board": { + "parent": null, + "size": [1000, 200, 25], + "mountPosition": [0, 0, 0], + "mountRotation": [0, 0, 0], + "skeleton": {"from": [0, 0, 16], "to": [1000, 0, 16], "radius": 4, "color": [0.85, 0.2, 0.2]}, + "markers": [ + {"id": 210, "set": "Brett", "position": [20, -20, 0.3], "normal": [0, 0, 1]}, + {"id": 211, "set": "Brett", "position": [250, -10, 0.3], "normal": [0, 0, 1]}, + {"id": 215, "set": "Brett", "position": [250, -90, 0.3], "normal": [0, 0, 1]}, + {"id": 214, "set": "Brett", "position": [350, -10, 0.3], "normal": [0, 0, 1]}, + {"id": 208, "set": "Brett", "position": [350, -90, 0.3], "normal": [0, 0, 1]}, + {"id": 206, "set": "Brett", "position": [650, -10, 0.3], "normal": [0, 0, 1]}, + {"id": 205, "set": "Brett", "position": [750, -90, 0.3], "normal": [0, 0, 1]}, + {"id": 207, "set": "Brett", "position": [750, -10, 0.3], "normal": [0, 0, 1]}, + {"id": 217, "set": "Brett", "position": [650, -90, 0.3], "normal": [0, 0, 1]}, + { + "id": 46, + "set": "A0", + "position": [536.71, 185.44, -27.3], + "normal": [0, 0, 1], + "spin": 90, + "info": "is placed on a white paper, A0_60Arucos_25mm_Seet223.pdf, with the following marker placements:" + }, + {"id": 47, "set": "A0", "position": [344.23, -286.54, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 48, "set": "A0", "position": [688.69, -320.72, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 49, "set": "A0", "position": [1006.0, 158.33, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 50, "set": "A0", "position": [573.41, 211.86, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 51, "set": "A0", "position": [167.8, -172.08, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 52, "set": "A0", "position": [94.68, 208.66, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 53, "set": "A0", "position": [486.25, 212.24, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 54, "set": "A0", "position": [342.27, -330.59, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 55, "set": "A0", "position": [283.72, -262.58, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 56, "set": "A0", "position": [498.68, 168.67, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 57, "set": "A0", "position": [602.86, -364.05, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 58, "set": "A0", "position": [50.09, -218.11, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 59, "set": "A0", "position": [626.21, -278.75, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 60, "set": "A0", "position": [434.36, 283.81, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 61, "set": "A0", "position": [-22.42, 335.83, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 62, "set": "A0", "position": [404.7, -175.1, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 63, "set": "A0", "position": [777.4, -236.15, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 64, "set": "A0", "position": [-21.27, -188.23, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 65, "set": "A0", "position": [803.39, -297.37, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 66, "set": "A0", "position": [209.75, -363.23, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 67, "set": "A0", "position": [523.07, 267.04, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 68, "set": "A0", "position": [573.73, 170.64, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 69, "set": "A0", "position": [7.61, -281.21, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 70, "set": "A0", "position": [601.87, 300.33, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 71, "set": "A0", "position": [749.75, -284.01, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 72, "set": "A0", "position": [440.99, 194.32, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 73, "set": "A0", "position": [221.73, 333.11, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 74, "set": "A0", "position": [93.78, 144.5, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 75, "set": "A0", "position": [-25.7, 194.58, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 76, "set": "A0", "position": [685.21, 166.8, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 77, "set": "A0", "position": [18.19, 191.57, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 78, "set": "A0", "position": [823.11, -344.38, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 79, "set": "A0", "position": [312.3, -159.11, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 80, "set": "A0", "position": [863.59, -335.92, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 81, "set": "A0", "position": [132.14, 169.03, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 82, "set": "A0", "position": [219.16, 297.24, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 83, "set": "A0", "position": [44.16, 339.22, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 84, "set": "A0", "position": [407.49, 258.42, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 85, "set": "A0", "position": [504.58, -312.75, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 86, "set": "A0", "position": [362.89, 292.01, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 87, "set": "A0", "position": [943.63, -245.76, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 88, "set": "A0", "position": [765.87, 316.04, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 89, "set": "A0", "position": [988.02, -369.14, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 90, "set": "A0", "position": [643.17, 316.43, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 91, "set": "A0", "position": [723.35, 328.05, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 92, "set": "A0", "position": [645.09, -184.84, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 93, "set": "A0", "position": [934.88, 143.6, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 94, "set": "A0", "position": [875.7, 173.65, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 95, "set": "A0", "position": [186.04, -274.07, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 96, "set": "A0", "position": [369.77, -186.49, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 97, "set": "A0", "position": [304.35, -359.67, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 98, "set": "A0", "position": [575.27, 315.06, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 99, "set": "A0", "position": [959.16, -321.55, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 100, "set": "A0", "position": [803.25, 172.36, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 101, "set": "A0", "position": [117.7, 298.66, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 102, "set": "A0", "position": [649.69, -223.0, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 103, "set": "A0", "position": [105.71, -187.71, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 104, "set": "A0", "position": [826.71, 239.16, -27.3], "normal": [0, 0, 1], "spin": 90}, + {"id": 105, "set": "A0", "position": [524.84, -266.25, -27.3], "normal": [0, 0, 1], "spin": 90} + ], + "model": [ + { + "stlFile": "surfaces/Board.stl", + "originOfModel": [0, 0, 0], + "rotationOfModelDegree": [0, 0, -90], + "material": "wood" + }, + { + "stlFile": "surfaces/BoardRail.stl", + "originOfModel": [0, 0, 0], + "rotationOfModelDegree": [0, 0, -90], + "material": "steel" + } + ] + }, + "Base": { + "parent": "Board", + "size": [150, 200, 150], + "mountPosition": [0, 0, 0], + "mountRotation": [0, 0, 0], + "jointToParent": { + "name": "Slider", + "type": "linear", + "axis": [1, 0, 0], + "origin": [0, 0, 16], + "rotation": [0, 0, 0], + "variable": "x" + }, + "skeleton": {"from": [0, 108, 45], "to": [110, 108, 45], "radius": 4, "color": [0.2, 0.8, 0.2]}, + "markers": [], + "model": [ + { + "stlFile": "surfaces/Base.stl", + "originOfModel": [-30, 0, -35], + "rotationOfModelDegree": [0, 0, 0], + "material": "plaWhite" + } + ] + }, + "Arm1": { + "parent": "Base", + "size": [70, 250, 70], + "mountPosition": [0, 0, 0], + "mountRotation": [0, 0, 0], + "jointToParent": { + "name": "Joint1", + "type": "revolute", + "axis": [-1, 0, 0], + "origin": [110, 108, 45], + "rotation": [0, 0, 0], + "variable": "y" + }, + "skeleton": {"from": [0, 0, 0], "to": [0, -250, 0], "radius": 4, "color": [0.2, 0.2, 0.9]}, + "markers": [ + {"id": 198, "name": "aruco_198", "position": [0, -160, 35], "normal": [0, 0, 1], "size": 25, "spin": 0}, + {"id": 229, "name": "aruco_229", "position": [0, -250, 35], "normal": [0, 0, 1], "size": 25, "spin": 0}, + {"id": 242, "name": "aruco_242", "position": [0, -250, -35], "normal": [0, 0, -1], "size": 25, "spin": 0}, + {"id": 243, "name": "aruco_243", "position": [0, -285, 0], "normal": [0, -1, 0], "size": 25, "spin": 0} + ], + "model": [ + { + "stlFile": "surfaces/Holm.stl", + "originOfModel__": [-25, 29, -28.5], + "originOfModel": [-29, 25, 28.5], + "rotationOfModelDegree__": [0, 0, 0], + "rotationOfModelDegree": [180, 0, -90], + "material": "powderCoatBlue" + } + ] + }, + "Ellbow": { + "parent": "Arm1", + "mountPosition": [0, 0, 0], + "mountRotation": [0, 0, 0], + "jointToParent": { + "name": "Joint2", + "type": "revolute", + "axis": [-1, 0, 0], + "origin": [0, -250, 0], + "rotation": [0, 0, 0], + "variable": "z" + }, + "skeleton": {"from": [0, 0, 0], "to": [90, 0, 0], "radius": 4, "color": [0.9, 0.2, 0.2]}, + "model": [ + { + "stlFile": "surfaces/Ellebogen.stl", + "originOfModel": [90, 0, 0], + "rotationOfModelDegree": [0, -90, -90], + "material": "defaultPlastic" + } + ], + "markers": [ + {"id": 244, "name": "aruco_244", "position": [125, 0, 0], "normal": [1, 0, 0], "size": 25, "spin": 0}, + {"id": 245, "name": "aruco_245", "position": [90, 0, -35], "normal": [0, 0, -1], "size": 25, "spin": 0}, + {"id": 246, "name": "aruco_246", "position": [90, 0, 35], "normal": [0, 0, 1], "size": 25}, + {"id": 247, "name": "aruco_247", "position": [52.5, 0, 35], "normal": [0, 0, 1], "size": 25}, + {"id": 248, "name": "aruco_248", "position": [52.5, 0, -35], "normal": [0, 0, -1], "size": 25}, + {"id": 232, "name": "aruco_232", "position": [90, 24.75, -24.75], "normal": [0, 1, -1], "size": 25}, + {"id": 231, "name": "aruco_231", "position": [90, 24.75, 24.75], "normal": [0, 1, 1], "size": 25} + ] + }, + "Arm2": { + "parent": "Ellbow", + "mountPosition": [0, 0, 0], + "mountRotation": [0, 0, 0], + "jointToParent": { + "name": "Joint3", + "type": "revolute", + "axis": [0, -1, 0], + "origin": [90, 0, 0], + "rotation": [0, 0, 0], + "variable": "a" + }, + "skeleton": {"from": [0, 0, 0], "to": [0, -250, 0], "radius": 4, "color": [0.95, 0.85, 0.2]}, + "model": [ + { + "stlFile": "surfaces/Unterarm.stl", + "originOfModel": [0, -250, 0], + "rotationOfModelDegree": [180, 0, -90], + "material": "defaultPlastic" + } + ], + "markers": [ + {"id": 120, "position": [24.75, -112, -24.75], "normal": [1, 0, -1]}, + {"id": 122, "name": "aruco_122", "position": [-35, -112, 0], "normal": [-1, 0, 0]}, + {"id": 218, "name": "aruco_218", "position": [35, -112, 0], "normal": [1, 0, 0]}, + {"id": 113, "name": "aruco_113", "position": [0, -182, 30], "normal": [0, 0, 1]}, + {"id": 114, "name": "aruco_114", "position": [24.75, -182, -24.75], "normal": [1, 0, -1]}, + {"id": 115, "name": "aruco_115", "position": [-24.75, -182, -24.75], "normal": [-1, 0, -1]}, + {"id": 124, "name": "aruco_124", "position": [-35, -219, 0], "normal": [-1, 0, 0]}, + {"id": 219, "name": "aruco_219", "position": [35, -219, 0], "normal": [1, 0, 0]} + ] + }, + "Hand": { + "parent": "Arm2", + "mountPosition": [0, 0, 0], + "mountRotation": [0, 0, 0], + "jointToParent": { + "name": "Joint4", + "type": "revolute", + "axis": [1, 0, 0], + "origin": [0, -250, 0], + "rotation": [0, 0, 0], + "variable": "b" + }, + "skeleton": {"from": [0, 0, 0], "to": [0, -35, 0], "radius": 4, "color": [0.95, 0.55, 0.15]} + }, + "Palm": { + "parent": "Hand", + "mountPosition": [0, 0, 0], + "mountRotation": [0, 0, 0], + "jointToParent": { + "name": "Joint3", + "type": "revolute", + "axis": [0, -1, 0], + "origin": [0, 0, 0], + "rotation": [0, 0, 0], + "variable": "c" + }, + "skeleton": {"from": [-50, -35, 0], "to": [50, -35, 0], "radius": 7, "color": [0.95, 0.2, 0.2]} + }, + "FingerA": { + "parent": "Palm", + "size": [80, 60, 20], + "mountPosition": [0, 0, 0], + "mountRotation": [0, 0, 0], + "jointToParent": { + "name": "Slider", + "type": "linear", + "axis": [1, 0, 0], + "origin": [4, -35, 0], + "rotation": [0, 0, 0], + "variable": "e" + }, + "skeleton": {"from": [0, 0, 0], "to": [0, -60, 0], "radius": 4, "color": [0.2, 0.8, 0.2]}, + "markers": [ + {"id": 40, "position": [12, -24, -17.1], "normal": [-10.98, 0, -23.56]}, + {"id": 41, "position": [1.5, -2.2, 25.8], "normal": [0, -25.6, 9.5]}, + {"id": 42, "position": [13.9, -40, 0], "normal": [1, -0.35, 0.4], "spin": 27} + ], + "model": [ + { + "stlFile": "surfaces/Finger.stl", + "originOfModel": [24, 0, -9.1], + "rotationOfModelDegree": [90, -90, 0], + "material": "defaultPlastic" + } + ] + }, + "FingerB": { + "parent": "Palm", + "size": [80, 60, 20], + "mountPosition": [0, 0, 0], + "mountRotation": [0, 0, 0], + "jointToParent": { + "name": "Slider", + "type": "linear", + "axis": [-1, 0, 0], + "origin": [-4, -35, 0], + "rotation": [0, 0, 0], + "variable": "e" + }, + "skeleton": {"from": [0, 0, 0], "to": [0, -60, 0], "radius": 4, "color": [0.2, 0.8, 0.2]}, + "markers": [ + {"id": 43, "position": [-12, -24, 17.1], "normal": [10.98, 0, 23.56], "spin": 90}, + {"id": 44, "position": [-1.5, -2.2, -25.8], "normal": [0, -25.6, -9.5], "spin": 90}, + {"id": 45, "position": [-13.9, -40, 0], "normal": [-1, -0.35, -0.4], "spin": -27} + ], + "model": [ + { + "stlFile": "surfaces/Finger.stl", + "originOfModel": [-24, 0, 9.1], + "rotationOfModelDegree": [90, 90, 0], + "material": "defaultPlastic" + } + ] + } + } +} diff --git a/pyproject.toml b/pyproject.toml index d137a14..8bc3f08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ [build-system] requires = ["setuptools>=68", "wheel"] -build-backend = "setuptools.backends.legacy:build" +build-backend = "setuptools.build_meta" [project] name = "approbot-pipeline" version = "1.0.0" description = "Robot pose estimation from multi-camera ArUco images" -readme = "doc/README.md" +readme = "README.md" requires-python = ">=3.10" dependencies = [ "numpy==1.26.4", diff --git a/scripts/__pycache__/__init__.cpython-311.pyc b/scripts/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9775cfef9ad02505a1021ce451647dcfeccce53c GIT binary patch literal 1073 zcmZuxOKaOe5SDD`fzkw8=zUNj*wmICLmz~?G%rd*o0!-+)EE|7T1y*CTCuxI>XbrG z{Q;%WdkHQ59r*(}7N1Ib>Mf94P8~V6(~_>1QQ zDvRI+lsP_ySKMPgaBY#Ko6Cj)kY6c4y{~*8Awef#T=}#I=f|JGWrFq;P!@GqOSDU}4M!`bot?TTzm35)3Xvoy9a@5#6OS3DhK73{9xr4#}Ibt^Bg$Wu)?` zG>afo3COjw$58$2o)?9{16{}hVvyZS;wU&xt zS=Rgib2vQeGM7dU9r27gOrLo^ret=I#SHi%;vo(4ADI10POVavIqY34MrSe!utRWwwY%I2gTE^g15O@e!AXY4y zBX2Z#xzU7Suuttk`IEXP?}s&cllSKzKEhUGXB$GM;99dH+|-S#Q_0-K4u}Y`ePY3} zjU?ct$N?3S@raP^!t`K< z5SKfI3})j|x7;|3sWbvT>>wcIyLO4DcJ*d$xxFWmqury9-){6H9&oR}F4zHWS1Z-W zrIk2NHAkTl4ewCf9)VTv_M5_XdhJpBwh3VoQ-oM?|7!MG$lWBM&&3`5Qd+01;c}7H ob^Ri9PtRRw>9VNlPtUX`x987=`^R%Xj2CCac|4OoBK=eR0n%7R`2YX_ literal 0 HcmV?d00001 diff --git a/scripts/__pycache__/pipeline.cpython-311.pyc b/scripts/__pycache__/pipeline.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1e18e475acf0551d7bee8db9f123ea86cf2a0925 GIT binary patch literal 11012 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 PN!pg+EdDsC%*Fo#&tY6{ literal 0 HcmV?d00001 diff --git a/scripts/api/__init__.py b/scripts/api/__init__.py index c14bfc7..060e02a 100644 --- a/scripts/api/__init__.py +++ b/scripts/api/__init__.py @@ -5,7 +5,7 @@ from .server import create_app def start_server( robot_json=None, host: str = "0.0.0.0", - port: int = 8080, + port: int = 8446, ) -> None: import uvicorn app = create_app(robot_json=robot_json) diff --git a/scripts/api/__main__.py b/scripts/api/__main__.py index 10c991e..25d3fbf 100644 --- a/scripts/api/__main__.py +++ b/scripts/api/__main__.py @@ -8,7 +8,7 @@ def main() -> None: ap = argparse.ArgumentParser(description="approbot-pipeline REST-API starten") ap.add_argument("--robot", default=None, help="Pfad zu robot.json") ap.add_argument("--host", default="0.0.0.0") - ap.add_argument("--port", type=int, default=8080) + ap.add_argument("--port", type=int, default=8446) args = ap.parse_args() start_server(robot_json=args.robot, host=args.host, port=args.port) diff --git a/scripts/api/server.py b/scripts/api/server.py index cdd4396..f54a47f 100644 --- a/scripts/api/server.py +++ b/scripts/api/server.py @@ -19,6 +19,20 @@ def create_app(robot_json: str | Path | None = None) -> FastAPI: global _robot_json if robot_json: _robot_json = Path(robot_json).resolve() + # Frühe, klare Warnung statt kryptischem 500 zur Laufzeit. + # Häufige Falle: Docker legt für einen fehlenden Bind-Mount-Pfad + # ein leeres VERZEICHNIS an — dann ist _robot_json zwar vorhanden, + # aber keine Datei. + if not _robot_json.is_file(): + import warnings + grund = "ist ein Verzeichnis" if _robot_json.is_dir() else "existiert nicht" + warnings.warn( + f"robot.json {grund}: {_robot_json}. " + f"/v1/config liefert 404, /v1/estimate verlangt einen robot_json-Upload. " + f"Bei Docker: liegt die Datei wirklich am gemounteten Host-Pfad?", + RuntimeWarning, + stacklevel=2, + ) return _app @@ -32,8 +46,8 @@ def health(): @_app.get("/v1/config") def config(): - if _robot_json is None or not _robot_json.exists(): - raise HTTPException(404, "Keine robot.json konfiguriert") + if _robot_json is None or not _robot_json.is_file(): + raise HTTPException(404, "Keine robot.json konfiguriert (Datei fehlt oder ist ein Verzeichnis)") data = json.loads(_robot_json.read_text(encoding="utf-8")) return data.get("pose_estimation", {}) @@ -57,10 +71,10 @@ async def estimate( if robot_json is not None: rj_path = tmp_path / "robot.json" rj_path.write_bytes(await robot_json.read()) - elif _robot_json and _robot_json.exists(): + elif _robot_json and _robot_json.is_file(): rj_path = _robot_json else: - raise HTTPException(400, "Keine robot.json angegeben (weder Upload noch Server-Konfig)") + raise HTTPException(400, "Keine robot.json angegeben (weder Upload noch gültige Server-Konfig)") for img in images: (tmp_path / img.filename).write_bytes(await img.read()) diff --git a/tests/__pycache__/test_pipeline.cpython-311-pytest-9.0.3.pyc b/tests/__pycache__/test_pipeline.cpython-311-pytest-9.0.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..909d4cfd62d08ab7b2c6d3674be90aa905ef8408 GIT binary patch literal 9403 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