From 9ec5c075f581e1a1e651990c15b861198669ab67 Mon Sep 17 00:00:00 2001 From: Naomi Pentrel <5212232+npentrel@users.noreply.github.com> Date: Fri, 12 Sep 2025 14:09:01 +0200 Subject: [PATCH 1/3] DOCS-4341: Update run inference --- .../data-management/tensor-output.png | Bin 0 -> 83773 bytes docs/data-ai/ai/run-inference.md | 141 +++++++------ .../reference/triggers-configuration.md | 36 ++-- .../run-inference.snippet.run-inference.go | 177 ++++++++++++++++ .../run-inference.snippet.run-inference.py | 91 +++++++++ ...sion-service.snippet.run-vision-service.go | 75 +++++++ ...sion-service.snippet.run-vision-service.py | 43 ++++ ...sion-service.snippet.run-vision-service.ts | 46 +++++ .../examples/fleet-api/fleet-issue-1.go | 88 ++++++++ .../examples/fleet-api/fleet-issue-2.go | 76 +++++++ .../examples/fleet-api/fleet-issue-3.go | 168 ++++++++++++++++ .../examples/fleet-api/fleet-issue-4.go | 168 ++++++++++++++++ static/include/examples/go.mod | 11 + static/include/examples/go.sum | 7 + .../examples/run-inference/run-inference.go | 189 ++++++++++++++++++ .../examples/run-inference/run-inference.py | 103 ++++++++++ .../run-inference/run-vision-service.go | 97 +++++++++ .../run-inference/run-vision-service.py | 63 ++++++ .../run-inference/run-vision-service.ts | 95 +++++++++ 19 files changed, 1591 insertions(+), 83 deletions(-) create mode 100644 assets/tutorials/data-management/tensor-output.png create mode 100644 static/include/examples-generated/run-inference.snippet.run-inference.go create mode 100644 static/include/examples-generated/run-inference.snippet.run-inference.py create mode 100644 static/include/examples-generated/run-vision-service.snippet.run-vision-service.go create mode 100644 static/include/examples-generated/run-vision-service.snippet.run-vision-service.py create mode 100644 static/include/examples-generated/run-vision-service.snippet.run-vision-service.ts create mode 100644 static/include/examples/fleet-api/fleet-issue-1.go create mode 100644 static/include/examples/fleet-api/fleet-issue-2.go create mode 100644 static/include/examples/fleet-api/fleet-issue-3.go create mode 100644 static/include/examples/fleet-api/fleet-issue-4.go create mode 100644 static/include/examples/run-inference/run-inference.go create mode 100644 static/include/examples/run-inference/run-inference.py create mode 100644 static/include/examples/run-inference/run-vision-service.go create mode 100644 static/include/examples/run-inference/run-vision-service.py create mode 100644 static/include/examples/run-inference/run-vision-service.ts diff --git a/assets/tutorials/data-management/tensor-output.png b/assets/tutorials/data-management/tensor-output.png new file mode 100644 index 0000000000000000000000000000000000000000..246814efe4069ff7ccd11009c7d4bfc99f075222 GIT binary patch literal 83773 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 literal 0 HcmV?d00001 diff --git a/docs/data-ai/ai/run-inference.md b/docs/data-ai/ai/run-inference.md index 92cdbd5068..0bc9ab9483 100644 --- a/docs/data-ai/ai/run-inference.md +++ b/docs/data-ai/ai/run-inference.md @@ -15,100 +15,111 @@ aliases: - /ml/vision/segmentation/ - /ml/vision/ - /get-started/quickstarts/detect-people/ -description: "Run inference on a model with a vision service or an SDK." +description: "Run machine learning inference locally on your robot or remotely in the cloud using vision services, ML model services, or SDKs." +date: "2025-09-12" --- Inference is the process of generating output from a machine learning (ML) model. -With Viam, you can run inference to generate the following kinds of output: -- object detection (using bounding boxes) -- classification (using tags) - -You can run inference locally on a Viam machine, or remotely in the Viam cloud. +You can run inference [locally on a Viam machine](#machine-inference), or [remotely in the Viam cloud](#cloud-inference). ## Machine inference -You can use `viam-server` to deploy and run ML models directly on your machines. - -You can run inference on your machine in the following ways: - -- with a vision service -- manually in application logic with an SDK +When you have [deployed an ML model](/data-ai/ai/deploy/) on your machine, you can run inference on your machine [directly with the ML model service](#using-an-ml-model-service-directly) or [using a vision service](#using-a-vision-service) that interprets the inferences. Entry-level devices such as the Raspberry Pi 4 can run small ML models, such as TensorFlow Lite (TFLite). -More powerful hardware, including the Jetson Xavier or Raspberry Pi 5 with an AI HAT+, can process larger AI models, including TensorFlow and ONNX. +More powerful hardware, including the Jetson Xavier or Raspberry Pi 5 with an AI HAT+, can process larger models, including TensorFlow and ONNX. +If your hardware does not support the model you want to run, see [Cloud inference](#cloud-inference). + +### Using an ML model service directly {{< tabs >}} -{{% tab name="Vision service" %}} +{{% tab name="Web UI" %}} -Vision services apply an ML model to a stream of images from a camera to generate bounding boxes or classifications. +1. Visit your machine's **CONFIGURE** or **CONTROL** page. +1. Expand the **TEST** area of the ML model service panel to view the tensor output. -{{}} +{{< imgproc src="/tutorials/data-management/tensor-output.png" alt="Example tensor output" resize="x1000" class="shadow imgzoom fill" >}} + +{{% /tab %}} +{{% tab name="Python" %}} + +The following code passes an image to an ML model service, and uses the [`Infer`](/dev/reference/apis/services/ml/#infer) method to make inferences: + +{{< read-code-snippet file="/static/include/examples-generated/run-inference.snippet.run-inference.py" lang="py" class="line-numbers linkable-line-numbers" data-line="82-85" >}} + +{{% /tab %}} +{{% tab name="Go" %}} + +The following code passes an image to an ML model service, and uses the [`Infer`](/dev/reference/apis/services/ml/#infer) method to make inferences: + +{{< read-code-snippet file="/static/include/examples-generated/run-inference.snippet.run-inference.go" lang="go" class="line-numbers linkable-line-numbers" data-line="166-171" >}} + +{{% /tab %}} +{{< /tabs >}} + +### Using a vision service + +Vision services apply an ML model to a stream of images from a camera to: -{{% alert title="Tip" color="tip" %}} -Some vision services include their own ML models, and thus do not require a deployed ML model. -If your vision service does not include an ML model, you must [deploy an ML model to your machine](/data-ai/ai/deploy/) to use that service. -{{% /alert %}} +- detect objects (using bounding boxes) +- classify (using tags) To use a vision service: -1. Visit your machine's **CONFIGURE** page. +1. Navigate to your machine's **CONFIGURE** page. 1. Click the **+** icon next to your main machine part and select **Component or service**. -1. Type in the name of the service and select a vision service. -1. If your vision service does not include an ML model, [deploy an ML model to your machine](/data-ai/ai/deploy/) to use that service. -1. Configure the service based on your use case. -1. To view the deployed vision service, use the live detection feed. - The feed shows an overlay of detected objects or classifications on top of a live camera feed. - On the **CONFIGURE** or **CONTROL** pages for your machine, expand the **Test** area of the service panel to view the feed. +1. Select a vision service. +1. Configure your vision service. + If your vision service does not include an ML model, you need to [deploy an ML model to your machine](/data-ai/ai/deploy/) and select it when configuring your vision service. - {{< imgproc src="/tutorials/data-management/blue-star.png" alt="Detected blue star" resize="x200" class="shadow" >}} - {{< imgproc src="/tutorials/filtered-camera-module/viam-figure-preview.png" alt="Detection of a viam figure with a confidence score of 0.97" resize="x200" class="shadow" >}} +{{% expand "Click to search available vision services" %}} -For instance, you could use [`viam:vision:mlmodel`](/operate/reference/services/vision/mlmodel/) with the `EfficientDet-COCO` ML model to detect a variety of objects, including people, bicycles, and apples, in a camera feed. - -Alternatively, you could use [`viam-soleng:vision:openalpr`](https://app.viam.com/module/viam-soleng/viamalpr) to detect license plates in images. -Since this service includes its own ML model, there is no need to configure a separate ML model. +{{}} -After adding a vision service, you can use a vision service API method with a classifier or a detector to get inferences programmatically. -For more information, see the APIs for [ML Model](/dev/reference/apis/services/ml/) and [Vision](/dev/reference/apis/services/vision/). +{{% /expand%}} -{{% /tab %}} -{{% tab name="SDK" %}} +{{% expand "Click to view example vision services" %}} -With the Viam SDK, you can pass image data to an ML model service, read the output annotations, and react to output in your own code. -Use the [`Infer`](/dev/reference/apis/services/ml/#infer) method of the ML Model API to make inferences. + +| Example | Description | +| ------- | ----------- | +| Detect a variety of objects | Use the [`viam:vision:mlmodel`](/operate/reference/services/vision/mlmodel/) vision service with the `EfficientDet-COCO` ML model to detect a variety of objects, including people, bicycles, and apples, in a camera feed. | +| Detect license plates | Use the [`viam-soleng:vision:openalpr`](https://app.viam.com/module/viam-soleng/viamalpr) vision service to detect license plates in images. This service includes its own ML model. | -For example: +{{% /expand%}} {{< tabs >}} -{{% tab name="Python" %}} +{{% tab name="Web UI" %}} -```python {class="line-numbers linkable-line-numbers"} -import numpy as np +1. Visit your machine's **CONFIGURE** or **CONTROL** page. +1. Expand the **TEST** area of the vision service panel. -my_mlmodel = MLModelClient.from_robot(robot=machine, name="my_mlmodel_service") + The feed shows an overlay of detected objects or classifications on top of a live camera feed. + + {{< imgproc src="/tutorials/data-management/blue-star.png" alt="Detected blue star" resize="x200" class="shadow" >}} + {{< imgproc src="/tutorials/filtered-camera-module/viam-figure-preview.png" alt="Detection of a viam figure with a confidence score of 0.97" resize="x200" class="shadow" >}} -image_data = np.zeros((1, 384, 384, 3), dtype=np.uint8) +{{% /tab %}} +{{% tab name="Python" %}} -# Create the input tensors dictionary -input_tensors = { - "image": image_data -} +The following code passes an image from a camera to a vision service and uses the [`GetClassifications`](/dev/reference/apis/services/vision/#GetClassifications) method: -output_tensors = await my_mlmodel.infer(input_tensors) -``` +{{< read-code-snippet file="/static/include/examples-generated/run-vision-service.snippet.run-vision-service.py" lang="py" class="line-numbers linkable-line-numbers" data-line="36-37" >}} {{% /tab %}} {{% tab name="Go" %}} -```go {class="line-numbers linkable-line-numbers"} -input_tensors := ml.Tensors{"0": tensor.New(tensor.WithShape(1, 2, 3), tensor.WithBacking([]int{1, 2, 3, 4, 5, 6}))} +The following code passes an image from a camera to a vision service and uses the [`GetClassifications`](/dev/reference/apis/services/vision/#GetClassifications) method: -output_tensors, err := myMLModel.Infer(context.Background(), input_tensors) -``` +{{< read-code-snippet file="/static/include/examples-generated/run-vision-service.snippet.run-vision-service.go" lang="go" class="line-numbers linkable-line-numbers" data-line="66-69" >}} {{% /tab %}} -{{< /tabs >}} +{{% tab name="TypeScript" %}} + +The following code passes an image from a camera to a vision service and uses the [`GetClassifications`](/dev/reference/apis/services/vision/#GetClassifications) method: + +{{< read-code-snippet file="/static/include/examples-generated/run-vision-service.snippet.run-vision-service.ts" lang="ts" class="line-numbers linkable-line-numbers" data-line="32-38" >}} {{% /tab %}} {{< /tabs >}} @@ -116,19 +127,17 @@ output_tensors, err := myMLModel.Infer(context.Background(), input_tensors) ## Cloud inference Cloud inference enables you to run machine learning models in the Viam cloud, instead of on a local machine. -Cloud inference often provides more computing power than edge devices, so you can benefit from: +Cloud inference provides more computing power than edge devices, enabling you to run more computationally-intensive models or achieve faster inference times. -- larger, more accurate models -- faster inference times +You can run cloud inference using any TensorFlow and TensorFlow Lite model in the Viam registry, including unlisted models owned by or shared with you. -You can run cloud inference using any TensorFlow and TensorFlow Lite model in the Viam registry, including private models owned by or shared with your organization. - -To run cloud inference, you must pass +To run cloud inference, you must pass the following: - the binary data ID and organization of the data you want to run inference on - the name, version, and organization of the model you want to use for inference -The [`viam infer`](/dev/tools/cli/#infer) CLI command runs inference in the cloud on a piece of data using the specified ML model: +You can obtain the binary data ID from the [**DATA** tab](https://app.viam.com/data/view) and the organization ID by running the CLI command `viam org list`. +You can find the model information on the [**MODELS** tab](https://app.viam.com/models). ```sh {class="command-line" data-prompt="$" data-output="2-18"} viam infer --binary-data-id --model-name --model-org-id --model-version "2025-04-14T16-38-25" --org-id @@ -151,5 +160,7 @@ Bounding Box Format: [x_min, y_min, x_max, y_max] No annotations. ``` -`infer` returns a list of detected classes or bounding boxes depending on the output of the ML model you specified, as well as a list of confidence values for those classes or boxes. -This method returns bounding box output using proportional coordinates between 0 and 1, with the origin `(0, 0)` in the top left of the image and `(1, 1)` in the bottom right. +The command returns a list of detected classes or bounding boxes depending on the output of the ML model you specified, as well as a list of confidence values for those classes or boxes. +The bounding box output uses proportional coordinates between 0 and 1, with the origin `(0, 0)` in the top left of the image and `(1, 1)` in the bottom right. + +For more information, see [`viam infer`](/dev/tools/cli/#infer). diff --git a/docs/data-ai/reference/triggers-configuration.md b/docs/data-ai/reference/triggers-configuration.md index e69f6b2c83..e087e8bcf2 100644 --- a/docs/data-ai/reference/triggers-configuration.md +++ b/docs/data-ai/reference/triggers-configuration.md @@ -47,24 +47,24 @@ The following template demonstrates the structure of a JSON configuration for a The following template demonstrates the structure of a JSON configuration for a trigger that alerts when data syncs to the Viam cloud: ```json {class="line-numbers linkable-line-numbers"} - "triggers": [ - { - "name": "", - "event": { - "type": "part_data_ingested", - "data_ingested": { - "data_types": ["binary", "tabular", "file", "unspecified"] - } - }, - "notifications": [ - { - "type": "", - "value": "", - "seconds_between_notifications": - } - ] - } - ] +"triggers": [ + { + "name": "", + "event": { + "type": "part_data_ingested", + "data_ingested": { + "data_types": ["binary", "tabular", "file", "unspecified"] + } + }, + "notifications": [ + { + "type": "", + "value": "", + "seconds_between_notifications": + } + ] + } +] ``` ### Conditional trigger template diff --git a/static/include/examples-generated/run-inference.snippet.run-inference.go b/static/include/examples-generated/run-inference.snippet.run-inference.go new file mode 100644 index 0000000000..9fe5c28070 --- /dev/null +++ b/static/include/examples-generated/run-inference.snippet.run-inference.go @@ -0,0 +1,177 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "image" + "image/jpeg" + + "gorgonia.org/tensor" + + "go.viam.com/rdk/logging" + "go.viam.com/rdk/ml" + "go.viam.com/rdk/robot/client" + "go.viam.com/rdk/components/camera" + "go.viam.com/rdk/services/mlmodel" + "go.viam.com/rdk/utils" + "go.viam.com/utils/rpc" +) + +func main() { + apiKey := "" + apiKeyID := "" + machineAddress := "" + mlModelName := "" + cameraName := "" + + + logger := logging.NewDebugLogger("client") + ctx := context.Background() + + machine, err := client.New( + context.Background(), + machineAddress, + logger, + client.WithDialOptions(rpc.WithEntityCredentials( + apiKeyID, + rpc.Credentials{ + Type: rpc.CredentialsTypeAPIKey, + Payload: apiKey, + })), + ) + if err != nil { + logger.Fatal(err) + } + + // Capture image from camera + cam, err := camera.FromRobot(machine, cameraName) + if err != nil { + logger.Fatal(err) + } + + imageData, _, err := cam.Image(ctx, utils.MimeTypeJPEG, nil) + if err != nil { + logger.Fatal(err) + } + + // Decode the image data to get the actual image + img, err := jpeg.Decode(bytes.NewReader(imageData)) + if err != nil { + logger.Fatal(err) + } + + // Get ML model metadata to understand input requirements + mlModel, err := mlmodel.FromRobot(machine, mlModelName) + if err != nil { + logger.Fatal(err) + } + + metadata, err := mlModel.Metadata(ctx) + if err != nil { + logger.Fatal(err) + } + + // Get expected input shape and type from metadata + var expectedShape []int + var expectedDtype tensor.Dtype + var expectedName string + + if len(metadata.Inputs) > 0 { + inputInfo := metadata.Inputs[0] + expectedShape = inputInfo.Shape + expectedName = inputInfo.Name + + // Convert data type string to tensor.Dtype + switch inputInfo.DataType { + case "uint8": + expectedDtype = tensor.Uint8 + case "float32": + expectedDtype = tensor.Float32 + default: + expectedDtype = tensor.Float32 // Default to float32 + } + } else { + logger.Fatal("No input info found in model metadata") + } + + // Resize image to expected dimensions + bounds := img.Bounds() + width := bounds.Dx() + height := bounds.Dy() + + // Extract expected dimensions + if len(expectedShape) != 4 || expectedShape[0] != 1 || expectedShape[3] != 3 { + logger.Fatal("Unexpected input shape format") + } + + expectedHeight := expectedShape[1] + expectedWidth := expectedShape[2] + + // Create a new image with the expected dimensions + resizedImg := image.NewRGBA(image.Rect(0, 0, expectedWidth, expectedHeight)) + + // Simple nearest neighbor resize + for y := 0; y < expectedHeight; y++ { + for x := 0; x < expectedWidth; x++ { + srcX := x * width / expectedWidth + srcY := y * height / expectedHeight + resizedImg.Set(x, y, img.At(srcX, srcY)) + } + } + + // Convert image to tensor data + tensorData := make([]float32, 1*expectedHeight*expectedWidth*3) + idx := 0 + + for y := 0; y < expectedHeight; y++ { + for x := 0; x < expectedWidth; x++ { + r, g, b, _ := resizedImg.At(x, y).RGBA() + + // Convert from 16-bit to 8-bit and normalize to [0, 1] for float32 + if expectedDtype == tensor.Float32 { + tensorData[idx] = float32(r>>8) / 255.0 // R + tensorData[idx+1] = float32(g>>8) / 255.0 // G + tensorData[idx+2] = float32(b>>8) / 255.0 // B + } else { + // For uint8, we need to create a uint8 slice + logger.Fatal("uint8 tensor creation not implemented in this example") + } + idx += 3 + } + } + + // Create input tensor + var inputTensor tensor.Tensor + if expectedDtype == tensor.Float32 { + inputTensor = tensor.New( + tensor.WithShape(1, expectedHeight, expectedWidth, 3), + tensor.WithBacking(tensorData), + tensor.Of(tensor.Float32), + ) + } else { + logger.Fatal("Only float32 tensors are supported in this example") + } + + // Convert tensor.Tensor to *tensor.Dense for ml.Tensors + denseTensor, ok := inputTensor.(*tensor.Dense) + if !ok { + logger.Fatal("Failed to convert inputTensor to *tensor.Dense") + } + + inputTensors := ml.Tensors{ + expectedName: denseTensor, + } + + outputTensors, err := mlModel.Infer(ctx, inputTensors) + if err != nil { + logger.Fatal(err) + } + + fmt.Printf("Output tensors: %v\n", outputTensors) + + err = machine.Close(ctx) + if err != nil { + logger.Fatal(err) + } +} diff --git a/static/include/examples-generated/run-inference.snippet.run-inference.py b/static/include/examples-generated/run-inference.snippet.run-inference.py new file mode 100644 index 0000000000..bccf0a8b60 --- /dev/null +++ b/static/include/examples-generated/run-inference.snippet.run-inference.py @@ -0,0 +1,91 @@ +import asyncio +import numpy as np + +from PIL import Image +from viam.components.camera import Camera +from viam.media.utils.pil import viam_to_pil_image +from viam.robot.client import RobotClient +from viam.services.mlmodel import MLModelClient + +# Configuration constants – replace with your actual values +API_KEY = "" # API key, find or create in your organization settings +API_KEY_ID = "" # API key ID, find or create in your organization settings +MACHINE_ADDRESS = "" # the address of the machine you want to capture images from +ML_MODEL_NAME = "" # the name of the ML model you want to use +CAMERA_NAME = "" # the name of the camera you want to capture images from + + +async def connect_machine() -> RobotClient: + """Establish a connection to the robot using the robot address.""" + machine_opts = RobotClient.Options.with_api_key( + api_key=API_KEY, + api_key_id=API_KEY_ID + ) + return await RobotClient.at_address(MACHINE_ADDRESS, machine_opts) + +async def main() -> int: + machine = await connect_machine() + + camera = Camera.from_robot(machine, CAMERA_NAME) + ml_model = MLModelClient.from_robot(machine, ML_MODEL_NAME) + + # Get ML model metadata to understand input requirements + metadata = await ml_model.metadata() + + # Capture image + image_frame = await camera.get_image() + + # Convert ViamImage to PIL Image first + pil_image = viam_to_pil_image(image_frame) + # Convert PIL Image to numpy array + image_array = np.array(pil_image) + + # Get expected input shape from metadata + expected_shape = list(metadata.input_info[0].shape) + expected_dtype = metadata.input_info[0].data_type + expected_name = metadata.input_info[0].name + + if not expected_shape: + print("No input info found for 'image'") + return 1 + + if len(expected_shape) == 4 and expected_shape[0] == 1 and expected_shape[3] == 3: + expected_height = expected_shape[1] + expected_width = expected_shape[2] + + # Resize to expected dimensions + if image_array.shape[:2] != (expected_height, expected_width): + pil_image_resized = pil_image.resize((expected_width, expected_height)) + image_array = np.array(pil_image_resized) + else: + print(f"Unexpected input shape format.") + return 1 + + # Add batch dimension and ensure correct shape + image_data = np.expand_dims(image_array, axis=0) + + # Ensure the data type matches expected type + if expected_dtype == "uint8": + image_data = image_data.astype(np.uint8) + elif expected_dtype == "float32": + # Convert to float32 and normalize to [0, 1] range + image_data = image_data.astype(np.float32) / 255.0 + else: + # Default to float32 with normalization + image_data = image_data.astype(np.float32) / 255.0 + + # Create the input tensors dictionary + input_tensors = { + expected_name: image_data + } + + output_tensors = await ml_model.infer(input_tensors) + print(f"Output tensors:") + for key, value in output_tensors.items(): + print(f"{key}: shape={value.shape}, dtype={value.dtype}") + + await machine.close() + return 0 + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/static/include/examples-generated/run-vision-service.snippet.run-vision-service.go b/static/include/examples-generated/run-vision-service.snippet.run-vision-service.go new file mode 100644 index 0000000000..58e321d716 --- /dev/null +++ b/static/include/examples-generated/run-vision-service.snippet.run-vision-service.go @@ -0,0 +1,75 @@ +package main + +import ( + "context" + "fmt" + "image/jpeg" + "bytes" + + "go.viam.com/rdk/logging" + "go.viam.com/rdk/robot/client" + "go.viam.com/rdk/services/vision" + "go.viam.com/rdk/components/camera" + "go.viam.com/rdk/utils" + "go.viam.com/utils/rpc" +) + +func main() { + apiKey := "" + apiKeyID := "" + machineAddress := "" + classifierName := "" + cameraName := "" + + + logger := logging.NewDebugLogger("client") + ctx := context.Background() + + machine, err := client.New( + context.Background(), + machineAddress, + logger, + client.WithDialOptions(rpc.WithEntityCredentials( + apiKeyID, + rpc.Credentials{ + Type: rpc.CredentialsTypeAPIKey, + Payload: apiKey, + })), + ) + if err != nil { + logger.Fatal(err) + } + + // Capture image from camera + cam, err := camera.FromRobot(machine, cameraName) + if err != nil { + logger.Fatal(err) + } + + imageData, _, err := cam.Image(ctx, utils.MimeTypeJPEG, nil) + if err != nil { + logger.Fatal(err) + } + + // Convert binary data to image.Image + img, err := jpeg.Decode(bytes.NewReader(imageData)) + if err != nil { + logger.Fatal(err) + } + + // Get classifications using the image + classifier, err := vision.FromRobot(machine, classifierName) + if err != nil { + logger.Fatal(err) + } + + classifications, err := classifier.Classifications(ctx, img, 2, nil) + if err != nil { + logger.Fatal(err) + } + + err = machine.Close(ctx) + if err != nil { + logger.Fatal(err) + } +} diff --git a/static/include/examples-generated/run-vision-service.snippet.run-vision-service.py b/static/include/examples-generated/run-vision-service.snippet.run-vision-service.py new file mode 100644 index 0000000000..6d367a2d1a --- /dev/null +++ b/static/include/examples-generated/run-vision-service.snippet.run-vision-service.py @@ -0,0 +1,43 @@ +import asyncio +import numpy as np + +from PIL import Image +from viam.components.camera import Camera +from viam.media.utils.pil import viam_to_pil_image +from viam.robot.client import RobotClient +from viam.services.vision import VisionClient + +# Configuration constants – replace with your actual values +API_KEY = "" # API key, find or create in your organization settings +API_KEY_ID = "" # API key ID, find or create in your organization settings +MACHINE_ADDRESS = "" # the address of the machine you want to capture images from +CLASSIFIER_NAME = "" # the name of the classifier you want to use +CAMERA_NAME = "" # the name of the camera you want to capture images from + + +async def connect_machine() -> RobotClient: + """Establish a connection to the robot using the robot address.""" + machine_opts = RobotClient.Options.with_api_key( + api_key=API_KEY, + api_key_id=API_KEY_ID + ) + return await RobotClient.at_address(MACHINE_ADDRESS, machine_opts) + +async def main() -> int: + machine = await connect_machine() + + camera = Camera.from_robot(machine, CAMERA_NAME) + classifier = VisionClient.from_robot(machine, CLASSIFIER_NAME) + + # Capture image + image_frame = await camera.get_image(mime_type="image/jpeg") + + # Get tags using the ViamImage (not the PIL image) + tags = await classifier.get_classifications( + image=image_frame, image_format="image/jpeg", count=2) + + await machine.close() + return 0 + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/static/include/examples-generated/run-vision-service.snippet.run-vision-service.ts b/static/include/examples-generated/run-vision-service.snippet.run-vision-service.ts new file mode 100644 index 0000000000..98b7f98857 --- /dev/null +++ b/static/include/examples-generated/run-vision-service.snippet.run-vision-service.ts @@ -0,0 +1,46 @@ +import { createRobotClient, RobotClient, VisionClient, CameraClient } from "@viamrobotics/sdk"; + +// Configuration constants – replace with your actual values +let API_KEY = ""; // API key, find or create in your organization settings +let API_KEY_ID = ""; // API key ID, find or create in your organization settings +let MACHINE_ADDRESS = ""; // the address of the machine you want to capture images from +let CLASSIFIER_NAME = ""; // the name of the classifier you want to use +let CAMERA_NAME = ""; // the name of the camera you want to capture images from + +async function connectMachine(): Promise { + // Establish a connection to the robot using the machine address + return await createRobotClient({ + host: MACHINE_ADDRESS, + credentials: { + type: 'api-key', + payload: API_KEY, + authEntity: API_KEY_ID, + }, + signalingAddress: 'https://app.viam.com:443', + }); +} + +async function main(): Promise { + const machine = await connectMachine(); + const camera = new CameraClient(machine, CAMERA_NAME); + const classifier = new VisionClient(machine, CLASSIFIER_NAME); + + // Capture image + const imageFrame = await camera.getImage(); + + // Get tags using the image + const tags = await classifier.getClassifications( + imageFrame, + imageFrame.width ?? 0, + imageFrame.height ?? 0, + imageFrame.mimeType ?? "", + 2 + ); + + return 0; +} + +main().catch((error) => { + console.error("Script failed:", error); + process.exit(1); +}); diff --git a/static/include/examples/fleet-api/fleet-issue-1.go b/static/include/examples/fleet-api/fleet-issue-1.go new file mode 100644 index 0000000000..2cbd55efd2 --- /dev/null +++ b/static/include/examples/fleet-api/fleet-issue-1.go @@ -0,0 +1,88 @@ +package main + +import ( + "context" + "fmt" + "os" + + "go.viam.com/rdk/app" + "go.viam.com/rdk/logging" +) + +// Configuration constants – replace with your actual values +var ( + API_KEY = "" // API key, find or create in your organization settings + API_KEY_ID = "" // API key ID, find or create in your organization settings + ORG_ID = "" // Organization ID, find or create in your organization settings +) + +func main() { + ORG_ID = os.Getenv("TEST_ORG_ID") + API_KEY = os.Getenv("VIAM_API_KEY") + API_KEY_ID = os.Getenv("VIAM_API_KEY_ID") + logger := logging.NewDebugLogger("client") + ctx := context.Background() + + // Make a ViamClient + viamClient, err := app.CreateViamClientWithAPIKey( + ctx, app.Options{}, API_KEY, API_KEY_ID, logger) + if err != nil { + logger.Fatal(err) + } + defer viamClient.Close() + + // Instantiate an AppClient called "cloud" + // to run fleet management API methods on + cloud := viamClient.AppClient() + + // ISSUE 1: CREATE & DELETE REGISTRY ITEM + + // Create registry item + err = cloud.CreateRegistryItem(ctx, ORG_ID, "new-registry-item-12", app.PackageTypeMLModel) + if err != nil { + logger.Fatal(err) + } + + // Get number of registry items + registryItems, err := cloud.ListRegistryItems( + ctx, + &ORG_ID, + []app.PackageType{app.PackageTypeMLModel}, + []app.Visibility{app.VisibilityPrivate, app.VisibilityPublic}, + []string{"linux/any"}, + []app.RegistryItemStatus{app.RegistryItemStatusPublished}, + &app.ListRegistryItemsOptions{}) + if err != nil { + logger.Fatal(err) + } + numRegistryItems := len(registryItems) + fmt.Println("Number of registry items:", numRegistryItems) + if numRegistryItems <= 0 { + logger.Fatal("Expected > 0 registry items") + } + + // DOES NOT SEEM TO DELETE THE REGISTRY ITEM + // Delete registry item + err = cloud.DeleteRegistryItem(ctx, "docs-test:new-registry-item-12") + if err != nil { + logger.Fatal(err) + } + + // Get number of registry items + registryItems, err = cloud.ListRegistryItems( + ctx, + &ORG_ID, + []app.PackageType{app.PackageTypeMLModel}, + []app.Visibility{app.VisibilityPrivate, app.VisibilityPublic}, + []string{"linux/any"}, + []app.RegistryItemStatus{app.RegistryItemStatusPublished}, + &app.ListRegistryItemsOptions{}) + if err != nil { + logger.Fatal(err) + } + fmt.Println("Number of registry items:", len(registryItems)) + if numRegistryItems - 1 != len(registryItems) { + logger.Fatal("Expected 1 fewer registry item after deletion") + } + +} diff --git a/static/include/examples/fleet-api/fleet-issue-2.go b/static/include/examples/fleet-api/fleet-issue-2.go new file mode 100644 index 0000000000..c12eb0558f --- /dev/null +++ b/static/include/examples/fleet-api/fleet-issue-2.go @@ -0,0 +1,76 @@ +package main + +import ( + "context" + "fmt" + "os" + + "go.viam.com/rdk/app" + "go.viam.com/rdk/logging" +) + +// Configuration constants – replace with your actual values +var ( + API_KEY = "" // API key, find or create in your organization settings + API_KEY_ID = "" // API key ID, find or create in your organization settings + ORG_ID = "" // Organization ID, find or create in your organization settings + LOCATION_ID = "" // Location ID, find or create in your organization settings +) + +func main() { + // :remove-start: + ORG_ID = os.Getenv("TEST_ORG_ID") + API_KEY = os.Getenv("VIAM_API_KEY") + API_KEY_ID = os.Getenv("VIAM_API_KEY_ID") + LOCATION_ID = "pg5q3j3h95" + // :remove-end: + logger := logging.NewDebugLogger("client") + ctx := context.Background() + + // Make a ViamClient + viamClient, err := app.CreateViamClientWithAPIKey( + ctx, app.Options{}, API_KEY, API_KEY_ID, logger) + if err != nil { + logger.Fatal(err) + } + defer viamClient.Close() + + // Instantiate an AppClient called "cloud" + // to run fleet management API methods on + cloud := viamClient.AppClient() + + // ISSUE 2: Add role + // User ID: d3bcb264-1a60-406b-8a79-9e43da4f3c9d + // 2025-09-02T10:31:21.526Z ERROR client fleet-api/fleet-issues.go:113 rpc error: code = InvalidArgument desc = requestID=9806ca33a0007d462e545f16a7cc0ec0: provided invalid authorization id 'location_owner' for resource type 'location' and authorization type 'owner' + + memberList, _, err := cloud.ListOrganizationMembers(ctx, ORG_ID) + if err != nil { + logger.Fatal(err) + } + + // Add role + userID := memberList[1].UserID + fmt.Println("User ID:", userID) + + err = cloud.AddRole( + ctx, + ORG_ID, + userID, + app.AuthRoleOwner, + app.AuthResourceTypeLocation, + LOCATION_ID, + ) + if err != nil { + logger.Fatal(err) + } + + // Change role + fmt.Printf(userID) + err = cloud.ChangeRole(ctx, &app.Authorization{}, ORG_ID, userID, app.AuthRoleOwner, app.AuthResourceTypeOrganization, ORG_ID) + if err != nil { + logger.Fatal(err) + } + + // d3bcb264-1a60-406b-8a79-9e43da4f3c9d2025-09-02T10:33:43.956Z ERROR client fleet-api/fleet-issues.go:122 rpc error: code = InvalidArgument desc = requestID=df8a74809aad4dfd156a7c2bad698d23: missing required 'identity_id' + +} diff --git a/static/include/examples/fleet-api/fleet-issue-3.go b/static/include/examples/fleet-api/fleet-issue-3.go new file mode 100644 index 0000000000..1bda39812b --- /dev/null +++ b/static/include/examples/fleet-api/fleet-issue-3.go @@ -0,0 +1,168 @@ +package main + +import ( + "context" + "fmt" + "os" + + "go.viam.com/rdk/app" + "go.viam.com/rdk/logging" +) + +// Configuration constants – replace with your actual values +var ( + API_KEY = "" // API key, find or create in your organization settings + API_KEY_ID = "" // API key ID, find or create in your organization settings + ORG_ID = "" // Organization ID, find or create in your organization settings + EMAIL_ADDRESS = "" // Email address of the user to get the user id for + LOCATION_ID = "" // Location ID, find or create in your organization settings +) + +func main() { + // :remove-start: + ORG_ID = os.Getenv("TEST_ORG_ID") + API_KEY = os.Getenv("VIAM_API_KEY") + API_KEY_ID = os.Getenv("VIAM_API_KEY_ID") + LOCATION_ID = "pg5q3j3h95" + // :remove-end: + logger := logging.NewDebugLogger("client") + ctx := context.Background() + + // Make a ViamClient + viamClient, err := app.CreateViamClientWithAPIKey( + ctx, app.Options{}, API_KEY, API_KEY_ID, logger) + if err != nil { + logger.Fatal(err) + } + defer viamClient.Close() + + // Instantiate an AppClient called "cloud" + // to run fleet management API methods on + cloud := viamClient.AppClient() + + // ISSUE 1: CREATE & DELETE REGISTRY ITEM + + // // Create registry item + // err = cloud.CreateRegistryItem(ctx, ORG_ID, "new-registry-item-12", app.PackageTypeMLModel) + // if err != nil { + // logger.Fatal(err) + // } + + // // Get number of registry items + // registryItems, err := cloud.ListRegistryItems( + // ctx, + // &ORG_ID, + // []app.PackageType{app.PackageTypeMLModel}, + // []app.Visibility{app.VisibilityPrivate, app.VisibilityPublic}, + // []string{"linux/any"}, + // []app.RegistryItemStatus{app.RegistryItemStatusPublished}, + // &app.ListRegistryItemsOptions{}) + // if err != nil { + // logger.Fatal(err) + // } + // numRegistryItems := len(registryItems) + // fmt.Println("Number of registry items:", numRegistryItems) + // if numRegistryItems <= 0 { + // logger.Fatal("Expected > 0 registry items") + // } + + // // DOES NOT SEEM TO DELETE THE REGISTRY ITEM + // // Delete registry item + // err = cloud.DeleteRegistryItem(ctx, "docs-test:new-registry-item-12") + // if err != nil { + // logger.Fatal(err) + // } + + // // Get number of registry items + // registryItems, err = cloud.ListRegistryItems( + // ctx, + // &ORG_ID, + // []app.PackageType{app.PackageTypeMLModel}, + // []app.Visibility{app.VisibilityPrivate, app.VisibilityPublic}, + // []string{"linux/any"}, + // []app.RegistryItemStatus{app.RegistryItemStatusPublished}, + // &app.ListRegistryItemsOptions{}) + // if err != nil { + // logger.Fatal(err) + // } + // fmt.Println("Number of registry items:", len(registryItems)) + // if numRegistryItems - 1 != len(registryItems) { + // logger.Fatal("Expected 1 fewer registry item after deletion") + // } + + // ISSUE 2: Add role + // User ID: d3bcb264-1a60-406b-8a79-9e43da4f3c9d + // 2025-09-02T10:31:21.526Z ERROR client fleet-api/fleet-issues.go:113 rpc error: code = InvalidArgument desc = requestID=9806ca33a0007d462e545f16a7cc0ec0: provided invalid authorization id 'location_owner' for resource type 'location' and authorization type 'owner' + + memberList, _, err := cloud.ListOrganizationMembers(ctx, ORG_ID) + if err != nil { + logger.Fatal(err) + } + + // Add role + userID := memberList[1].UserID + fmt.Println("User ID:", userID) + + // err = cloud.AddRole( + // ctx, + // ORG_ID, + // userID, + // app.AuthRoleOwner, + // app.AuthResourceTypeLocation, + // LOCATION_ID, + // ) + // if err != nil { + // logger.Fatal(err) + // } + + // Change role + fmt.Printf(userID) + // err = cloud.ChangeRole(ctx, &app.Authorization{}, ORG_ID, userID, app.AuthRoleOwner, app.AuthResourceTypeOrganization, ORG_ID) + // if err != nil { + // logger.Fatal(err) + // } + + // d3bcb264-1a60-406b-8a79-9e43da4f3c9d2025-09-02T10:33:43.956Z ERROR client fleet-api/fleet-issues.go:122 rpc error: code = InvalidArgument desc = requestID=df8a74809aad4dfd156a7c2bad698d23: missing required 'identity_id' + + // ISSUE 3: It seems we can't create keys because we can't create APIKeyAuthorization structs + + // Create API key + // Since APIKeyAuthorization has unexported fields, we can't construct it directly + // apiKey, apiKeyID, err := cloud.CreateKey(ctx, ORG_ID, []app.APIKeyAuthorization{{ + // Role: app.AuthRoleOwner, + // ResourceType: app.AuthResourceTypeLocation, + // ResourceID: LOCATION_ID, + // }}, "mytestkey") + // if err != nil { + // logger.Fatal(err) + // } + // if apiKey == "" { + // logger.Fatal("API key should not be empty") + // } + // if apiKeyID == "" { + // logger.Fatal("API key ID should not be empty") + // } + + // ISSUE 4: RenameKey does not return the key ID + apiKeyID2, apiKey2, err := cloud.CreateKeyFromExistingKeyAuthorizations(ctx, newAPIKeyID) + if err != nil { + logger.Fatal(err) + } + if apiKey2 == "" { + logger.Fatal("API key 2 should not be empty") + } + if apiKeyID2 == "" { + logger.Fatal("API key ID 2 should not be empty") + } + fmt.Printf("API key id 2: %+v\n", apiKeyID2) + + apiKeyID3, apiKeyName3, err := cloud.RenameKey(ctx, apiKeyID2, "mytestkey2newName") + if err != nil { + logger.Fatal(err) + } + if apiKeyName3 != "mytestkey2newName" { + logger.Fatal("API key 3 should be renamed") + } + fmt.Printf("API key id 3: %+v\n", apiKeyID3) + +} diff --git a/static/include/examples/fleet-api/fleet-issue-4.go b/static/include/examples/fleet-api/fleet-issue-4.go new file mode 100644 index 0000000000..1bda39812b --- /dev/null +++ b/static/include/examples/fleet-api/fleet-issue-4.go @@ -0,0 +1,168 @@ +package main + +import ( + "context" + "fmt" + "os" + + "go.viam.com/rdk/app" + "go.viam.com/rdk/logging" +) + +// Configuration constants – replace with your actual values +var ( + API_KEY = "" // API key, find or create in your organization settings + API_KEY_ID = "" // API key ID, find or create in your organization settings + ORG_ID = "" // Organization ID, find or create in your organization settings + EMAIL_ADDRESS = "" // Email address of the user to get the user id for + LOCATION_ID = "" // Location ID, find or create in your organization settings +) + +func main() { + // :remove-start: + ORG_ID = os.Getenv("TEST_ORG_ID") + API_KEY = os.Getenv("VIAM_API_KEY") + API_KEY_ID = os.Getenv("VIAM_API_KEY_ID") + LOCATION_ID = "pg5q3j3h95" + // :remove-end: + logger := logging.NewDebugLogger("client") + ctx := context.Background() + + // Make a ViamClient + viamClient, err := app.CreateViamClientWithAPIKey( + ctx, app.Options{}, API_KEY, API_KEY_ID, logger) + if err != nil { + logger.Fatal(err) + } + defer viamClient.Close() + + // Instantiate an AppClient called "cloud" + // to run fleet management API methods on + cloud := viamClient.AppClient() + + // ISSUE 1: CREATE & DELETE REGISTRY ITEM + + // // Create registry item + // err = cloud.CreateRegistryItem(ctx, ORG_ID, "new-registry-item-12", app.PackageTypeMLModel) + // if err != nil { + // logger.Fatal(err) + // } + + // // Get number of registry items + // registryItems, err := cloud.ListRegistryItems( + // ctx, + // &ORG_ID, + // []app.PackageType{app.PackageTypeMLModel}, + // []app.Visibility{app.VisibilityPrivate, app.VisibilityPublic}, + // []string{"linux/any"}, + // []app.RegistryItemStatus{app.RegistryItemStatusPublished}, + // &app.ListRegistryItemsOptions{}) + // if err != nil { + // logger.Fatal(err) + // } + // numRegistryItems := len(registryItems) + // fmt.Println("Number of registry items:", numRegistryItems) + // if numRegistryItems <= 0 { + // logger.Fatal("Expected > 0 registry items") + // } + + // // DOES NOT SEEM TO DELETE THE REGISTRY ITEM + // // Delete registry item + // err = cloud.DeleteRegistryItem(ctx, "docs-test:new-registry-item-12") + // if err != nil { + // logger.Fatal(err) + // } + + // // Get number of registry items + // registryItems, err = cloud.ListRegistryItems( + // ctx, + // &ORG_ID, + // []app.PackageType{app.PackageTypeMLModel}, + // []app.Visibility{app.VisibilityPrivate, app.VisibilityPublic}, + // []string{"linux/any"}, + // []app.RegistryItemStatus{app.RegistryItemStatusPublished}, + // &app.ListRegistryItemsOptions{}) + // if err != nil { + // logger.Fatal(err) + // } + // fmt.Println("Number of registry items:", len(registryItems)) + // if numRegistryItems - 1 != len(registryItems) { + // logger.Fatal("Expected 1 fewer registry item after deletion") + // } + + // ISSUE 2: Add role + // User ID: d3bcb264-1a60-406b-8a79-9e43da4f3c9d + // 2025-09-02T10:31:21.526Z ERROR client fleet-api/fleet-issues.go:113 rpc error: code = InvalidArgument desc = requestID=9806ca33a0007d462e545f16a7cc0ec0: provided invalid authorization id 'location_owner' for resource type 'location' and authorization type 'owner' + + memberList, _, err := cloud.ListOrganizationMembers(ctx, ORG_ID) + if err != nil { + logger.Fatal(err) + } + + // Add role + userID := memberList[1].UserID + fmt.Println("User ID:", userID) + + // err = cloud.AddRole( + // ctx, + // ORG_ID, + // userID, + // app.AuthRoleOwner, + // app.AuthResourceTypeLocation, + // LOCATION_ID, + // ) + // if err != nil { + // logger.Fatal(err) + // } + + // Change role + fmt.Printf(userID) + // err = cloud.ChangeRole(ctx, &app.Authorization{}, ORG_ID, userID, app.AuthRoleOwner, app.AuthResourceTypeOrganization, ORG_ID) + // if err != nil { + // logger.Fatal(err) + // } + + // d3bcb264-1a60-406b-8a79-9e43da4f3c9d2025-09-02T10:33:43.956Z ERROR client fleet-api/fleet-issues.go:122 rpc error: code = InvalidArgument desc = requestID=df8a74809aad4dfd156a7c2bad698d23: missing required 'identity_id' + + // ISSUE 3: It seems we can't create keys because we can't create APIKeyAuthorization structs + + // Create API key + // Since APIKeyAuthorization has unexported fields, we can't construct it directly + // apiKey, apiKeyID, err := cloud.CreateKey(ctx, ORG_ID, []app.APIKeyAuthorization{{ + // Role: app.AuthRoleOwner, + // ResourceType: app.AuthResourceTypeLocation, + // ResourceID: LOCATION_ID, + // }}, "mytestkey") + // if err != nil { + // logger.Fatal(err) + // } + // if apiKey == "" { + // logger.Fatal("API key should not be empty") + // } + // if apiKeyID == "" { + // logger.Fatal("API key ID should not be empty") + // } + + // ISSUE 4: RenameKey does not return the key ID + apiKeyID2, apiKey2, err := cloud.CreateKeyFromExistingKeyAuthorizations(ctx, newAPIKeyID) + if err != nil { + logger.Fatal(err) + } + if apiKey2 == "" { + logger.Fatal("API key 2 should not be empty") + } + if apiKeyID2 == "" { + logger.Fatal("API key ID 2 should not be empty") + } + fmt.Printf("API key id 2: %+v\n", apiKeyID2) + + apiKeyID3, apiKeyName3, err := cloud.RenameKey(ctx, apiKeyID2, "mytestkey2newName") + if err != nil { + logger.Fatal(err) + } + if apiKeyName3 != "mytestkey2newName" { + logger.Fatal("API key 3 should be renamed") + } + fmt.Printf("API key id 3: %+v\n", apiKeyID3) + +} diff --git a/static/include/examples/go.mod b/static/include/examples/go.mod index 21ab063a30..400d67071f 100644 --- a/static/include/examples/go.mod +++ b/static/include/examples/go.mod @@ -6,6 +6,7 @@ require ( github.com/golang/geo v0.0.0-20230421003525-6adc56603217 go.viam.com/rdk v0.91.0 go.viam.com/utils v0.1.164 + gorgonia.org/tensor v0.9.24 ) require ( @@ -21,6 +22,7 @@ require ( git.sr.ht/~sbinet/gg v0.6.0 // indirect github.com/a8m/envsubst v1.4.2 // indirect github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect + github.com/apache/arrow/go/arrow v0.0.0-20201229220542-30ce2eb5d4dc // indirect github.com/aybabtme/uniplot v0.0.0-20151203143629-039c559e5e7e // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/bep/debounce v1.2.1 // indirect @@ -31,6 +33,8 @@ require ( github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/chenzhekl/goply v0.0.0-20190930133256-258c2381defd // indirect + github.com/chewxy/hm v1.0.0 // indirect + github.com/chewxy/math32 v1.0.8 // indirect github.com/cloudwego/base64x v0.1.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect @@ -52,12 +56,14 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/gonuts/binary v0.2.0 // indirect + github.com/google/flatbuffers v2.0.6+incompatible // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/s2a-go v0.1.8 // indirect github.com/google/uuid v1.6.0 // indirect @@ -125,6 +131,7 @@ require ( github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xfmoulet/qoi v0.2.0 // indirect + github.com/xtgo/set v1.0.0 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/zhuyie/golzf v0.0.0-20161112031142-8387b0307ade // indirect github.com/zitadel/oidc/v3 v3.37.0 // indirect @@ -144,6 +151,7 @@ require ( go.uber.org/zap v1.27.0 // indirect go.viam.com/api v0.1.475 // indirect go.viam.com/test v1.2.4 // indirect + go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect golang.org/x/arch v0.19.0 // indirect golang.org/x/crypto v0.41.0 // indirect golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect @@ -156,6 +164,7 @@ require ( golang.org/x/text v0.28.0 // indirect golang.org/x/time v0.6.0 // indirect golang.org/x/tools v0.35.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect gonum.org/v1/gonum v0.16.0 // indirect gonum.org/v1/plot v0.15.2 // indirect google.golang.org/api v0.196.0 // indirect @@ -166,5 +175,7 @@ require ( google.golang.org/protobuf v1.36.8 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gorgonia.org/vecf32 v0.9.0 // indirect + gorgonia.org/vecf64 v0.9.0 // indirect nhooyr.io/websocket v1.8.7 // indirect ) diff --git a/static/include/examples/go.sum b/static/include/examples/go.sum index 185e91cedd..2567260a78 100644 --- a/static/include/examples/go.sum +++ b/static/include/examples/go.sum @@ -138,6 +138,7 @@ github.com/chenzhekl/goply v0.0.0-20190930133256-258c2381defd h1:S0onsSZ3RawTrm4 github.com/chenzhekl/goply v0.0.0-20190930133256-258c2381defd/go.mod h1:P2dOeu3SNXtjA5VOH7tF0AnGm/eYrst9YA89b36c35I= github.com/chewxy/hm v1.0.0 h1:zy/TSv3LV2nD3dwUEQL2VhXeoXbb9QkpmdRAVUFiA6k= github.com/chewxy/hm v1.0.0/go.mod h1:qg9YI4q6Fkj/whwHR1D+bOGeF7SniIP40VweVepLjg0= +github.com/chewxy/math32 v1.0.0/go.mod h1:Miac6hA1ohdDUTagnvJy/q+aNnEk16qWUdb8ZVhvCN0= github.com/chewxy/math32 v1.0.8 h1:fU5E4Ec4Z+5RtRAi3TovSxUjQPkgRh+HbP7tKB2OFbM= github.com/chewxy/math32 v1.0.8/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -365,6 +366,7 @@ github.com/gonuts/binary v0.2.0 h1:caITwMWAoQWlL0RNvv2lTU/AHqAJlVuu6nZmNgfbKW4= github.com/gonuts/binary v0.2.0/go.mod h1:kM+CtBrCGDSKdv8WXTuCUsw+loiy8f/QEI8YCCC0M/E= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v2.0.6+incompatible h1:XHFReMv7nFFusa+CEokzWbzaYocKXI6C7hdU5Kgh9Lw= github.com/google/flatbuffers v2.0.6+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -844,6 +846,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -1090,6 +1093,7 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -1168,6 +1172,7 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1354,6 +1359,7 @@ google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200911024640-645f7a48b24f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU= google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4= @@ -1383,6 +1389,7 @@ google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v0.0.0-20200910201057-6591123024b3/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/static/include/examples/run-inference/run-inference.go b/static/include/examples/run-inference/run-inference.go new file mode 100644 index 0000000000..22d65f794c --- /dev/null +++ b/static/include/examples/run-inference/run-inference.go @@ -0,0 +1,189 @@ +// :snippet-start: run-inference +package main + +import ( + "bytes" + "context" + "fmt" + "image" + "image/jpeg" + + "gorgonia.org/tensor" + // :remove-start: + "os" + // :remove-end: + + "go.viam.com/rdk/logging" + "go.viam.com/rdk/ml" + "go.viam.com/rdk/robot/client" + "go.viam.com/rdk/components/camera" + "go.viam.com/rdk/services/mlmodel" + "go.viam.com/rdk/utils" + "go.viam.com/utils/rpc" +) + +func main() { + apiKey := "" + apiKeyID := "" + machineAddress := "" + mlModelName := "" + cameraName := "" + + // :remove-start: + apiKey = os.Getenv("VIAM_API_KEY") + apiKeyID = os.Getenv("VIAM_API_KEY_ID") + machineAddress = "auto-machine-main.pg5q3j3h95.viam.cloud" + cameraName = "camera-1" + mlModelName = "mlmodel-1" + // :remove-end: + + logger := logging.NewDebugLogger("client") + ctx := context.Background() + + machine, err := client.New( + context.Background(), + machineAddress, + logger, + client.WithDialOptions(rpc.WithEntityCredentials( + apiKeyID, + rpc.Credentials{ + Type: rpc.CredentialsTypeAPIKey, + Payload: apiKey, + })), + ) + if err != nil { + logger.Fatal(err) + } + + // Capture image from camera + cam, err := camera.FromRobot(machine, cameraName) + if err != nil { + logger.Fatal(err) + } + + imageData, _, err := cam.Image(ctx, utils.MimeTypeJPEG, nil) + if err != nil { + logger.Fatal(err) + } + + // Decode the image data to get the actual image + img, err := jpeg.Decode(bytes.NewReader(imageData)) + if err != nil { + logger.Fatal(err) + } + + // Get ML model metadata to understand input requirements + mlModel, err := mlmodel.FromRobot(machine, mlModelName) + if err != nil { + logger.Fatal(err) + } + + metadata, err := mlModel.Metadata(ctx) + if err != nil { + logger.Fatal(err) + } + + // Get expected input shape and type from metadata + var expectedShape []int + var expectedDtype tensor.Dtype + var expectedName string + + if len(metadata.Inputs) > 0 { + inputInfo := metadata.Inputs[0] + expectedShape = inputInfo.Shape + expectedName = inputInfo.Name + + // Convert data type string to tensor.Dtype + switch inputInfo.DataType { + case "uint8": + expectedDtype = tensor.Uint8 + case "float32": + expectedDtype = tensor.Float32 + default: + expectedDtype = tensor.Float32 // Default to float32 + } + } else { + logger.Fatal("No input info found in model metadata") + } + + // Resize image to expected dimensions + bounds := img.Bounds() + width := bounds.Dx() + height := bounds.Dy() + + // Extract expected dimensions + if len(expectedShape) != 4 || expectedShape[0] != 1 || expectedShape[3] != 3 { + logger.Fatal("Unexpected input shape format") + } + + expectedHeight := expectedShape[1] + expectedWidth := expectedShape[2] + + // Create a new image with the expected dimensions + resizedImg := image.NewRGBA(image.Rect(0, 0, expectedWidth, expectedHeight)) + + // Simple nearest neighbor resize + for y := 0; y < expectedHeight; y++ { + for x := 0; x < expectedWidth; x++ { + srcX := x * width / expectedWidth + srcY := y * height / expectedHeight + resizedImg.Set(x, y, img.At(srcX, srcY)) + } + } + + // Convert image to tensor data + tensorData := make([]float32, 1*expectedHeight*expectedWidth*3) + idx := 0 + + for y := 0; y < expectedHeight; y++ { + for x := 0; x < expectedWidth; x++ { + r, g, b, _ := resizedImg.At(x, y).RGBA() + + // Convert from 16-bit to 8-bit and normalize to [0, 1] for float32 + if expectedDtype == tensor.Float32 { + tensorData[idx] = float32(r>>8) / 255.0 // R + tensorData[idx+1] = float32(g>>8) / 255.0 // G + tensorData[idx+2] = float32(b>>8) / 255.0 // B + } else { + // For uint8, we need to create a uint8 slice + logger.Fatal("uint8 tensor creation not implemented in this example") + } + idx += 3 + } + } + + // Create input tensor + var inputTensor tensor.Tensor + if expectedDtype == tensor.Float32 { + inputTensor = tensor.New( + tensor.WithShape(1, expectedHeight, expectedWidth, 3), + tensor.WithBacking(tensorData), + tensor.Of(tensor.Float32), + ) + } else { + logger.Fatal("Only float32 tensors are supported in this example") + } + + // Convert tensor.Tensor to *tensor.Dense for ml.Tensors + denseTensor, ok := inputTensor.(*tensor.Dense) + if !ok { + logger.Fatal("Failed to convert inputTensor to *tensor.Dense") + } + + inputTensors := ml.Tensors{ + expectedName: denseTensor, + } + + outputTensors, err := mlModel.Infer(ctx, inputTensors) + if err != nil { + logger.Fatal(err) + } + + fmt.Printf("Output tensors: %v\n", outputTensors) + + err = machine.Close(ctx) + if err != nil { + logger.Fatal(err) + } +} +// :snippet-end: \ No newline at end of file diff --git a/static/include/examples/run-inference/run-inference.py b/static/include/examples/run-inference/run-inference.py new file mode 100644 index 0000000000..b45202b6f1 --- /dev/null +++ b/static/include/examples/run-inference/run-inference.py @@ -0,0 +1,103 @@ +# :snippet-start: run-inference +import asyncio +# :remove-start: +import os +# :remove-end: +import numpy as np + +from PIL import Image +from viam.components.camera import Camera +from viam.media.utils.pil import viam_to_pil_image +from viam.robot.client import RobotClient +from viam.services.mlmodel import MLModelClient + +# Configuration constants – replace with your actual values +API_KEY = "" # API key, find or create in your organization settings +API_KEY_ID = "" # API key ID, find or create in your organization settings +MACHINE_ADDRESS = "" # the address of the machine you want to capture images from +ML_MODEL_NAME = "" # the name of the ML model you want to use +CAMERA_NAME = "" # the name of the camera you want to capture images from + +# :remove-start: +API_KEY = os.environ["VIAM_API_KEY"] +API_KEY_ID = os.environ["VIAM_API_KEY_ID"] +MACHINE_ADDRESS = "auto-machine-main.pg5q3j3h95.viam.cloud" +CAMERA_NAME = "camera-1" +ML_MODEL_NAME = "mlmodel-1" +# :remove-end: + +async def connect_machine() -> RobotClient: + """Establish a connection to the robot using the robot address.""" + machine_opts = RobotClient.Options.with_api_key( + api_key=API_KEY, + api_key_id=API_KEY_ID + ) + return await RobotClient.at_address(MACHINE_ADDRESS, machine_opts) + +async def main() -> int: + machine = await connect_machine() + + camera = Camera.from_robot(machine, CAMERA_NAME) + ml_model = MLModelClient.from_robot(machine, ML_MODEL_NAME) + + # Get ML model metadata to understand input requirements + metadata = await ml_model.metadata() + + # Capture image + image_frame = await camera.get_image() + + # Convert ViamImage to PIL Image first + pil_image = viam_to_pil_image(image_frame) + # Convert PIL Image to numpy array + image_array = np.array(pil_image) + + # Get expected input shape from metadata + expected_shape = list(metadata.input_info[0].shape) + expected_dtype = metadata.input_info[0].data_type + expected_name = metadata.input_info[0].name + + if not expected_shape: + print("No input info found for 'image'") + return 1 + + if len(expected_shape) == 4 and expected_shape[0] == 1 and expected_shape[3] == 3: + expected_height = expected_shape[1] + expected_width = expected_shape[2] + + # Resize to expected dimensions + if image_array.shape[:2] != (expected_height, expected_width): + pil_image_resized = pil_image.resize((expected_width, expected_height)) + image_array = np.array(pil_image_resized) + else: + print(f"Unexpected input shape format.") + return 1 + + # Add batch dimension and ensure correct shape + image_data = np.expand_dims(image_array, axis=0) + + # Ensure the data type matches expected type + if expected_dtype == "uint8": + image_data = image_data.astype(np.uint8) + elif expected_dtype == "float32": + # Convert to float32 and normalize to [0, 1] range + image_data = image_data.astype(np.float32) / 255.0 + else: + # Default to float32 with normalization + image_data = image_data.astype(np.float32) / 255.0 + + # Create the input tensors dictionary + input_tensors = { + expected_name: image_data + } + + output_tensors = await ml_model.infer(input_tensors) + print(f"Output tensors:") + for key, value in output_tensors.items(): + print(f"{key}: shape={value.shape}, dtype={value.dtype}") + + await machine.close() + return 0 + +if __name__ == "__main__": + asyncio.run(main()) +# :snippet-end: \ No newline at end of file diff --git a/static/include/examples/run-inference/run-vision-service.go b/static/include/examples/run-inference/run-vision-service.go new file mode 100644 index 0000000000..13473e6259 --- /dev/null +++ b/static/include/examples/run-inference/run-vision-service.go @@ -0,0 +1,97 @@ +// :snippet-start: run-vision-service +package main + +import ( + "context" + "fmt" + "image/jpeg" + "bytes" + // :remove-start: + "os" + // :remove-end: + + "go.viam.com/rdk/logging" + "go.viam.com/rdk/robot/client" + "go.viam.com/rdk/services/vision" + "go.viam.com/rdk/components/camera" + "go.viam.com/rdk/utils" + "go.viam.com/utils/rpc" +) + +func main() { + apiKey := "" + apiKeyID := "" + machineAddress := "" + classifierName := "" + cameraName := "" + + // :remove-start: + apiKey = os.Getenv("VIAM_API_KEY") + apiKeyID = os.Getenv("VIAM_API_KEY_ID") + machineAddress = "auto-machine-main.pg5q3j3h95.viam.cloud" + cameraName = "camera-1" + classifierName = "classifier-1" + // :remove-end: + + logger := logging.NewDebugLogger("client") + ctx := context.Background() + + machine, err := client.New( + context.Background(), + machineAddress, + logger, + client.WithDialOptions(rpc.WithEntityCredentials( + apiKeyID, + rpc.Credentials{ + Type: rpc.CredentialsTypeAPIKey, + Payload: apiKey, + })), + ) + if err != nil { + logger.Fatal(err) + } + + // Capture image from camera + cam, err := camera.FromRobot(machine, cameraName) + if err != nil { + logger.Fatal(err) + } + + imageData, _, err := cam.Image(ctx, utils.MimeTypeJPEG, nil) + if err != nil { + logger.Fatal(err) + } + + // Convert binary data to image.Image + img, err := jpeg.Decode(bytes.NewReader(imageData)) + if err != nil { + logger.Fatal(err) + } + + // Get classifications using the image + classifier, err := vision.FromRobot(machine, classifierName) + if err != nil { + logger.Fatal(err) + } + + classifications, err := classifier.Classifications(ctx, img, 2, nil) + if err != nil { + logger.Fatal(err) + } + + // :remove-start: + if len(classifications) == 0 { + fmt.Println("No tags found") + return + } else { + for _, classification := range classifications { + fmt.Printf("Found tag: %s\n", classification.Label()) + } + } + // :remove-end: + err = machine.Close(ctx) + if err != nil { + logger.Fatal(err) + } +} +// :snippet-end: \ No newline at end of file diff --git a/static/include/examples/run-inference/run-vision-service.py b/static/include/examples/run-inference/run-vision-service.py new file mode 100644 index 0000000000..47794a3a44 --- /dev/null +++ b/static/include/examples/run-inference/run-vision-service.py @@ -0,0 +1,63 @@ +# :snippet-start: run-vision-service +import asyncio +# :remove-start: +import os +# :remove-end: +import numpy as np + +from PIL import Image +from viam.components.camera import Camera +from viam.media.utils.pil import viam_to_pil_image +from viam.robot.client import RobotClient +from viam.services.vision import VisionClient + +# Configuration constants – replace with your actual values +API_KEY = "" # API key, find or create in your organization settings +API_KEY_ID = "" # API key ID, find or create in your organization settings +MACHINE_ADDRESS = "" # the address of the machine you want to capture images from +CLASSIFIER_NAME = "" # the name of the classifier you want to use +CAMERA_NAME = "" # the name of the camera you want to capture images from + +# :remove-start: +API_KEY = os.environ["VIAM_API_KEY"] +API_KEY_ID = os.environ["VIAM_API_KEY_ID"] +MACHINE_ADDRESS = "auto-machine-main.pg5q3j3h95.viam.cloud" +CAMERA_NAME = "camera-1" +CLASSIFIER_NAME = "classifier-1" +# :remove-end: + +async def connect_machine() -> RobotClient: + """Establish a connection to the robot using the robot address.""" + machine_opts = RobotClient.Options.with_api_key( + api_key=API_KEY, + api_key_id=API_KEY_ID + ) + return await RobotClient.at_address(MACHINE_ADDRESS, machine_opts) + +async def main() -> int: + machine = await connect_machine() + + camera = Camera.from_robot(machine, CAMERA_NAME) + classifier = VisionClient.from_robot(machine, CLASSIFIER_NAME) + + # Capture image + image_frame = await camera.get_image(mime_type="image/jpeg") + + # Get tags using the ViamImage (not the PIL image) + tags = await classifier.get_classifications( + image=image_frame, image_format="image/jpeg", count=2) + + # :remove-start: + if not len(tags): + print("No tags found") + return 1 + else: + for tag in tags: + print(f"Found tag: {tag.class_name}") + # :remove-end: + await machine.close() + return 0 + +if __name__ == "__main__": + asyncio.run(main()) +# :snippet-end: \ No newline at end of file diff --git a/static/include/examples/run-inference/run-vision-service.ts b/static/include/examples/run-inference/run-vision-service.ts new file mode 100644 index 0000000000..358426226d --- /dev/null +++ b/static/include/examples/run-inference/run-vision-service.ts @@ -0,0 +1,95 @@ +// :snippet-start: run-vision-service +import { createRobotClient, RobotClient, VisionClient, CameraClient } from "@viamrobotics/sdk"; +// :remove-start: +import pkg from "@koush/wrtc"; +const { + RTCPeerConnection, + RTCSessionDescription, + RTCIceCandidate, + MediaStream, + MediaStreamTrack +} = pkg; + +// Set up global WebRTC classes +global.RTCPeerConnection = RTCPeerConnection; +global.RTCSessionDescription = RTCSessionDescription; +global.RTCIceCandidate = RTCIceCandidate; +global.MediaStream = MediaStream; +global.MediaStreamTrack = MediaStreamTrack; +// :remove-end: + +// Configuration constants – replace with your actual values +let API_KEY = ""; // API key, find or create in your organization settings +let API_KEY_ID = ""; // API key ID, find or create in your organization settings +let MACHINE_ADDRESS = ""; // the address of the machine you want to capture images from +let CLASSIFIER_NAME = ""; // the name of the classifier you want to use +let CAMERA_NAME = ""; // the name of the camera you want to capture images from +// :remove-start: +API_KEY = process.env.VIAM_API_KEY || ""; +API_KEY_ID = process.env.VIAM_API_KEY_ID || ""; +MACHINE_ADDRESS = "auto-machine-main.pg5q3j3h95.viam.cloud"; +CAMERA_NAME = "camera-1"; +CLASSIFIER_NAME = "classifier-1"; +// :remove-end: + +async function connectMachine(): Promise { + // Establish a connection to the robot using the machine address + return await createRobotClient({ + host: MACHINE_ADDRESS, + credentials: { + type: 'api-key', + payload: API_KEY, + authEntity: API_KEY_ID, + }, + signalingAddress: 'https://app.viam.com:443', + }); +} + +async function main(): Promise { + const machine = await connectMachine(); + const camera = new CameraClient(machine, CAMERA_NAME); + const classifier = new VisionClient(machine, CLASSIFIER_NAME); + + // Capture image + const imageFrame = await camera.getImage(); + + // Get tags using the image + const tags = await classifier.getClassifications( + imageFrame, + imageFrame.width ?? 0, + imageFrame.height ?? 0, + imageFrame.mimeType ?? "", + 2 + ); + + // :remove-start: + if (tags.length === 0) { + console.log("No tags found"); + return 1; + } else { + for (const tag of tags) { + console.log(`Found tag: ${tag.className}`); + } + } + + // Force exit after cleanup + process.exit(0); + // :remove-end: + return 0; +} + +// :remove-start: +// Run the script with timeout +const timeout = setTimeout(() => { + console.log("Script timed out, forcing exit"); + process.exit(1); +}, 20000); // 10 second timeout +// :remove-end: +main().catch((error) => { + // :remove-start: + clearTimeout(timeout); + // :remove-end: + console.error("Script failed:", error); + process.exit(1); +}); +// :snippet-end: \ No newline at end of file From 582c9cc1463a6951f94677b1fb6bbff05d4c9966 Mon Sep 17 00:00:00 2001 From: Naomi Pentrel <5212232+npentrel@users.noreply.github.com> Date: Fri, 12 Sep 2025 14:21:31 +0200 Subject: [PATCH 2/3] Fix links --- docs/data-ai/ai/run-inference.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/data-ai/ai/run-inference.md b/docs/data-ai/ai/run-inference.md index 0bc9ab9483..c0e13020e0 100644 --- a/docs/data-ai/ai/run-inference.md +++ b/docs/data-ai/ai/run-inference.md @@ -103,21 +103,21 @@ To use a vision service: {{% /tab %}} {{% tab name="Python" %}} -The following code passes an image from a camera to a vision service and uses the [`GetClassifications`](/dev/reference/apis/services/vision/#GetClassifications) method: +The following code passes an image from a camera to a vision service and uses the [`GetClassifications`](/dev/reference/apis/services/vision/#getclassifications) method: {{< read-code-snippet file="/static/include/examples-generated/run-vision-service.snippet.run-vision-service.py" lang="py" class="line-numbers linkable-line-numbers" data-line="36-37" >}} {{% /tab %}} {{% tab name="Go" %}} -The following code passes an image from a camera to a vision service and uses the [`GetClassifications`](/dev/reference/apis/services/vision/#GetClassifications) method: +The following code passes an image from a camera to a vision service and uses the [`GetClassifications`](/dev/reference/apis/services/vision/#getclassifications) method: {{< read-code-snippet file="/static/include/examples-generated/run-vision-service.snippet.run-vision-service.go" lang="go" class="line-numbers linkable-line-numbers" data-line="66-69" >}} {{% /tab %}} {{% tab name="TypeScript" %}} -The following code passes an image from a camera to a vision service and uses the [`GetClassifications`](/dev/reference/apis/services/vision/#GetClassifications) method: +The following code passes an image from a camera to a vision service and uses the [`GetClassifications`](/dev/reference/apis/services/vision/#getclassifications) method: {{< read-code-snippet file="/static/include/examples-generated/run-vision-service.snippet.run-vision-service.ts" lang="ts" class="line-numbers linkable-line-numbers" data-line="32-38" >}} From 4b58fa0c5ab3cec06fc8f2be0061f53ad228894f Mon Sep 17 00:00:00 2001 From: Naomi Pentrel <5212232+npentrel@users.noreply.github.com> Date: Fri, 19 Sep 2025 08:06:07 +0200 Subject: [PATCH 3/3] Improve --- docs/data-ai/ai/run-inference.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/data-ai/ai/run-inference.md b/docs/data-ai/ai/run-inference.md index c0e13020e0..a5e9a047a8 100644 --- a/docs/data-ai/ai/run-inference.md +++ b/docs/data-ai/ai/run-inference.md @@ -60,7 +60,8 @@ The following code passes an image to an ML model service, and uses the [`Infer` ### Using a vision service -Vision services apply an ML model to a stream of images from a camera to: +There are a range of vision services that interpret images. +Some vision services apply an ML model to a stream of images from a camera to: - detect objects (using bounding boxes) - classify (using tags) @@ -70,6 +71,7 @@ To use a vision service: 1. Navigate to your machine's **CONFIGURE** page. 1. Click the **+** icon next to your main machine part and select **Component or service**. 1. Select a vision service. + For more information on the vision service, see its entry in the [Viam registry](https://app.viam.com/registry). 1. Configure your vision service. If your vision service does not include an ML model, you need to [deploy an ML model to your machine](/data-ai/ai/deploy/) and select it when configuring your vision service.