Skip to content

Commit cf5964c

Browse files
cheuktviambotnjooma
authored
RSDK-3050 - Add get_images (#358)
Co-authored-by: viambot <[email protected]> Co-authored-by: Naveed Jooma <[email protected]>
1 parent 7432a38 commit cf5964c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1772
-1290
lines changed

examples/server/v1/components.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from viam.errors import ResourceNotFoundError
3535
from viam.media import MediaStreamWithIterator
3636
from viam.media.audio import Audio, AudioStream
37+
from viam.media.video import NamedImage
3738
from viam.operations import run_with_operation
3839
from viam.proto.common import (
3940
AnalogStatus,
@@ -45,6 +46,7 @@
4546
Pose,
4647
PoseInFrame,
4748
Vector3,
49+
ResponseMetadata,
4850
)
4951
from viam.proto.component.arm import JointPositions
5052
from viam.proto.component.audioinput import AudioChunk, AudioChunkInfo, SampleFormat
@@ -332,6 +334,9 @@ def __del__(self):
332334
async def get_image(self, mime_type: str = "", **kwargs) -> Image.Image:
333335
return self.image.copy()
334336

337+
async def get_images(self, timeout: Optional[float] = None, **kwargs) -> Tuple[List[NamedImage], ResponseMetadata]:
338+
raise NotImplementedError()
339+
335340
async def get_point_cloud(self, **kwargs) -> Tuple[bytes, str]:
336341
raise NotImplementedError()
337342

poetry.lock

Lines changed: 798 additions & 842 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/viam/components/camera/camera.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import abc
2-
from typing import Final, NamedTuple, Optional, Tuple, Union
2+
from typing import Final, List, NamedTuple, Optional, Tuple, Union
33

44
from PIL.Image import Image
55

6+
from viam.media.video import NamedImage
7+
from viam.proto.common import ResponseMetadata
68
from viam.resource.types import RESOURCE_NAMESPACE_RDK, RESOURCE_TYPE_COMPONENT, Subtype
79

810
from ..component_base import ComponentBase
@@ -48,6 +50,21 @@ async def get_image(self, mime_type: str = "", *, timeout: Optional[float] = Non
4850
"""
4951
...
5052

53+
@abc.abstractmethod
54+
async def get_images(self, *, timeout: Optional[float] = None, **kwargs) -> Tuple[List[NamedImage], ResponseMetadata]:
55+
"""Get simultaneous images from different sensors, along with associated metadata.
56+
This should not be used for getting a time series of images from the same sensor.
57+
58+
Returns:
59+
Tuple[List[NamedImage], ResponseMetadata]:
60+
- List[NamedImage]:
61+
The list of images returned from the camera system.
62+
63+
- ResponseMetadata:
64+
The metadata associated with this response
65+
"""
66+
...
67+
5168
@abc.abstractmethod
5269
async def get_point_cloud(self, *, timeout: Optional[float] = None, **kwargs) -> Tuple[bytes, str]:
5370
"""

src/viam/components/camera/client.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
from io import BytesIO
2-
from typing import Mapping, Optional, Tuple, Union
2+
from typing import List, Mapping, Optional, Tuple, Union
33

44
from grpclib.client import Channel
55
from PIL import Image
66

7-
from viam.media.video import LIBRARY_SUPPORTED_FORMATS, CameraMimeType
8-
from viam.proto.common import DoCommandRequest, DoCommandResponse
7+
from viam.media.video import LIBRARY_SUPPORTED_FORMATS, CameraMimeType, NamedImage
8+
from viam.proto.common import DoCommandRequest, DoCommandResponse, ResponseMetadata
99
from viam.proto.component.camera import (
1010
CameraServiceStub,
1111
GetImageRequest,
1212
GetImageResponse,
13+
GetImagesRequest,
14+
GetImagesResponse,
1315
GetPointCloudRequest,
1416
GetPointCloudResponse,
1517
GetPropertiesRequest,
@@ -21,6 +23,16 @@
2123
from . import Camera, RawImage
2224

2325

26+
def get_image_from_response(data: bytes, response_mime_type: str, request_mime_type: Optional[str] = None) -> Union[Image.Image, RawImage]:
27+
if request_mime_type is None:
28+
request_mime_type = response_mime_type
29+
_, is_lazy = CameraMimeType.from_lazy(request_mime_type)
30+
if is_lazy or not (CameraMimeType.is_supported(response_mime_type)):
31+
image = RawImage(data=data, mime_type=response_mime_type)
32+
return image
33+
return Image.open(BytesIO(data), formats=LIBRARY_SUPPORTED_FORMATS)
34+
35+
2436
class CameraClient(Camera, ReconfigurableResourceRPCClientBase):
2537
"""
2638
gRPC client for the Camera component
@@ -34,11 +46,18 @@ def __init__(self, name: str, channel: Channel):
3446
async def get_image(self, mime_type: str = "", *, timeout: Optional[float] = None) -> Union[Image.Image, RawImage]:
3547
request = GetImageRequest(name=self.name, mime_type=mime_type)
3648
response: GetImageResponse = await self.client.GetImage(request, timeout=timeout)
37-
_, is_lazy = CameraMimeType.from_lazy(request.mime_type)
38-
if is_lazy or not (CameraMimeType.is_supported(response.mime_type)):
39-
image = RawImage(response.image, response.mime_type)
40-
return image
41-
return Image.open(BytesIO(response.image), formats=LIBRARY_SUPPORTED_FORMATS)
49+
return get_image_from_response(response.image, response_mime_type=response.mime_type, request_mime_type=request.mime_type)
50+
51+
async def get_images(self, *, timeout: Optional[float] = None) -> Tuple[List[NamedImage], ResponseMetadata]:
52+
request = GetImagesRequest(name=self.name)
53+
response: GetImagesResponse = await self.client.GetImages(request, timeout=timeout)
54+
imgs = []
55+
for img_data in response.images:
56+
mime_type = CameraMimeType.from_proto(img_data.format)
57+
img = NamedImage(img_data.source_name, img_data.image, mime_type)
58+
imgs.append(img)
59+
resp_metadata: ResponseMetadata = response.response_metadata
60+
return imgs, resp_metadata
4261

4362
async def get_point_cloud(self, *, timeout: Optional[float] = None) -> Tuple[bytes, str]:
4463
request = GetPointCloudRequest(name=self.name, mime_type=CameraMimeType.PCD)

src/viam/components/camera/service.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@
1010
CameraServiceBase,
1111
GetImageRequest,
1212
GetImageResponse,
13+
GetImagesRequest,
14+
GetImagesResponse,
1315
GetPointCloudRequest,
1416
GetPointCloudResponse,
1517
GetPropertiesRequest,
1618
GetPropertiesResponse,
19+
Image,
1720
RenderFrameRequest,
1821
)
1922
from viam.resource.rpc_service_base import ResourceRPCServiceBase
@@ -61,6 +64,25 @@ async def GetImage(self, stream: Stream[GetImageRequest, GetImageResponse]) -> N
6164
response.image = img_bytes
6265
await stream.send_message(response)
6366

67+
async def GetImages(self, stream: Stream[GetImagesRequest, GetImagesResponse]) -> None:
68+
request = await stream.recv_message()
69+
assert request is not None
70+
name = request.name
71+
camera = self.get_resource(name)
72+
73+
timeout = stream.deadline.time_remaining() if stream.deadline else None
74+
images, metadata = await camera.get_images(timeout=timeout, metadata=stream.metadata)
75+
img_bytes_lst = []
76+
for img in images:
77+
try:
78+
fmt = img.mime_type.to_proto()
79+
img_bytes = img.data
80+
img_bytes_lst.append(Image(source_name=name, format=fmt, image=img_bytes))
81+
finally:
82+
img.close()
83+
response = GetImagesResponse(images=img_bytes_lst, response_metadata=metadata)
84+
await stream.send_message(response)
85+
6486
async def RenderFrame(self, stream: Stream[RenderFrameRequest, HttpBody]) -> None:
6587
request = await stream.recv_message()
6688
assert request is not None

src/viam/gen/app/v1/app_grpc.py

Lines changed: 6 additions & 1 deletion
Large diffs are not rendered by default.

src/viam/gen/app/v1/app_pb2.py

Lines changed: 231 additions & 227 deletions
Large diffs are not rendered by default.

src/viam/gen/app/v1/app_pb2.pyi

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,50 @@ class CreateOrganizationInviteResponse(google.protobuf.message.Message):
499499
...
500500
global___CreateOrganizationInviteResponse = CreateOrganizationInviteResponse
501501

502+
@typing_extensions.final
503+
class UpdateOrganizationInviteAuthorizationsRequest(google.protobuf.message.Message):
504+
DESCRIPTOR: google.protobuf.descriptor.Descriptor
505+
ORGANIZATION_ID_FIELD_NUMBER: builtins.int
506+
EMAIL_FIELD_NUMBER: builtins.int
507+
ADD_AUTHORIZATIONS_FIELD_NUMBER: builtins.int
508+
REMOVE_AUTHORIZATIONS_FIELD_NUMBER: builtins.int
509+
organization_id: builtins.str
510+
email: builtins.str
511+
512+
@property
513+
def add_authorizations(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Authorization]:
514+
...
515+
516+
@property
517+
def remove_authorizations(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Authorization]:
518+
...
519+
520+
def __init__(self, *, organization_id: builtins.str=..., email: builtins.str=..., add_authorizations: collections.abc.Iterable[global___Authorization] | None=..., remove_authorizations: collections.abc.Iterable[global___Authorization] | None=...) -> None:
521+
...
522+
523+
def ClearField(self, field_name: typing_extensions.Literal['add_authorizations', b'add_authorizations', 'email', b'email', 'organization_id', b'organization_id', 'remove_authorizations', b'remove_authorizations']) -> None:
524+
...
525+
global___UpdateOrganizationInviteAuthorizationsRequest = UpdateOrganizationInviteAuthorizationsRequest
526+
527+
@typing_extensions.final
528+
class UpdateOrganizationInviteAuthorizationsResponse(google.protobuf.message.Message):
529+
DESCRIPTOR: google.protobuf.descriptor.Descriptor
530+
INVITE_FIELD_NUMBER: builtins.int
531+
532+
@property
533+
def invite(self) -> global___OrganizationInvite:
534+
...
535+
536+
def __init__(self, *, invite: global___OrganizationInvite | None=...) -> None:
537+
...
538+
539+
def HasField(self, field_name: typing_extensions.Literal['invite', b'invite']) -> builtins.bool:
540+
...
541+
542+
def ClearField(self, field_name: typing_extensions.Literal['invite', b'invite']) -> None:
543+
...
544+
global___UpdateOrganizationInviteAuthorizationsResponse = UpdateOrganizationInviteAuthorizationsResponse
545+
502546
@typing_extensions.final
503547
class DeleteOrganizationInviteRequest(google.protobuf.message.Message):
504548
DESCRIPTOR: google.protobuf.descriptor.Descriptor

0 commit comments

Comments
 (0)