Skip to content

Commit 1ab734a

Browse files
authored
RSDK-11613 — Update Go camera API with new GetImages signature (#5183)
1 parent e68a336 commit 1ab734a

File tree

20 files changed

+1006
-245
lines changed

20 files changed

+1006
-245
lines changed

components/camera/camera.go

Lines changed: 117 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
package camera
66

77
import (
8+
"bytes"
89
"context"
910
"fmt"
1011
"image"
12+
"strings"
1113
"time"
1214

1315
"github.com/pkg/errors"
@@ -24,6 +26,16 @@ import (
2426
"go.viam.com/rdk/utils"
2527
)
2628

29+
// ErrMIMETypeBytesMismatch indicates that the NamedImage's mimeType does not match the image bytes header.
30+
//
31+
// For example, if the image bytes are JPEG, but the mimeType is PNG, this error will be returned.
32+
// This likely means there is a bug in the code that created the GetImages response.
33+
//
34+
// However, there may still be valid, decodeable underlying JPEG image bytes.
35+
// If you want to decode the image bytes as a JPEG regardless of the mismatch, you can recover from this error,
36+
// call the .Bytes() method, then decode the image bytes as JPEG manually with image.Decode().
37+
var ErrMIMETypeBytesMismatch = errors.New("mime_type does not match the image bytes")
38+
2739
func init() {
2840
resource.RegisterAPI(API, resource.APIRegistration[Camera]{
2941
RPCServiceServerConstructor: NewRPCServiceServer,
@@ -75,8 +87,81 @@ type Properties struct {
7587

7688
// NamedImage is a struct that associates the source from where the image came from to the Image.
7789
type NamedImage struct {
78-
Image image.Image
90+
data []byte
91+
img image.Image
7992
SourceName string
93+
mimeType string
94+
}
95+
96+
// NamedImageFromBytes constructs a NamedImage from a byte slice, source name, and mime type.
97+
func NamedImageFromBytes(data []byte, sourceName, mimeType string) (NamedImage, error) {
98+
if data == nil {
99+
return NamedImage{}, fmt.Errorf("must provide image bytes to construct a named image from bytes")
100+
}
101+
if mimeType == "" {
102+
return NamedImage{}, fmt.Errorf("must provide a mime type to construct a named image")
103+
}
104+
return NamedImage{data: data, SourceName: sourceName, mimeType: mimeType}, nil
105+
}
106+
107+
// NamedImageFromImage constructs a NamedImage from an image.Image, source name, and mime type.
108+
func NamedImageFromImage(img image.Image, sourceName, mimeType string) (NamedImage, error) {
109+
if img == nil {
110+
return NamedImage{}, fmt.Errorf("must provide image to construct a named image from image")
111+
}
112+
if mimeType == "" {
113+
return NamedImage{}, fmt.Errorf("must provide a mime type to construct a named image")
114+
}
115+
return NamedImage{img: img, SourceName: sourceName, mimeType: mimeType}, nil
116+
}
117+
118+
// Image returns the image.Image of the NamedImage.
119+
func (ni *NamedImage) Image(ctx context.Context) (image.Image, error) {
120+
if ni.img != nil {
121+
return ni.img, nil
122+
}
123+
if ni.data == nil {
124+
return nil, fmt.Errorf("no image or image bytes available")
125+
}
126+
127+
reader := bytes.NewReader(ni.data)
128+
_, header, err := image.DecodeConfig(reader)
129+
if err != nil {
130+
return nil, fmt.Errorf("could not decode image config: %w", err)
131+
}
132+
133+
if header != "" && !strings.Contains(ni.mimeType, header) {
134+
return nil, fmt.Errorf("%w: expected %s, got %s", ErrMIMETypeBytesMismatch, ni.mimeType, header)
135+
}
136+
137+
img, err := rimage.DecodeImage(ctx, ni.data, ni.mimeType)
138+
if err != nil {
139+
return nil, fmt.Errorf("could not decode bytes into image.Image: %w", err)
140+
}
141+
ni.img = img
142+
return ni.img, nil
143+
}
144+
145+
// Bytes returns the byte slice of the NamedImage.
146+
func (ni *NamedImage) Bytes(ctx context.Context) ([]byte, error) {
147+
if ni.data != nil {
148+
return ni.data, nil
149+
}
150+
if ni.img == nil {
151+
return nil, fmt.Errorf("no image or image bytes available")
152+
}
153+
154+
data, err := rimage.EncodeImage(ctx, ni.img, ni.mimeType)
155+
if err != nil {
156+
return nil, fmt.Errorf("could not encode image with encoding %s: %w", ni.mimeType, err)
157+
}
158+
ni.data = data
159+
return ni.data, nil
160+
}
161+
162+
// MimeType returns the mime type of the NamedImage.
163+
func (ni *NamedImage) MimeType() string {
164+
return ni.mimeType
80165
}
81166

82167
// ImageMetadata contains useful information about returned image bytes such as its mimetype.
@@ -139,8 +224,9 @@ type Camera interface {
139224

140225
// Images is used for getting simultaneous images from different imagers,
141226
// along with associated metadata (just timestamp for now). It's not for getting a time series of images from the same imager.
142-
// The extra parameter can be used to pass additional options to the camera resource.
143-
Images(ctx context.Context, extra map[string]interface{}) ([]NamedImage, resource.ResponseMetadata, error)
227+
// The extra parameter can be used to pass additional options to the camera resource. The filterSourceNames parameter can be used to filter
228+
// only the images from the specified source names. When unspecified, all images are returned.
229+
Images(ctx context.Context, filterSourceNames []string, extra map[string]interface{}) ([]NamedImage, resource.ResponseMetadata, error)
144230

145231
// NextPointCloud returns the next immediately available point cloud, not necessarily one
146232
// a part of a sequence. In the future, there could be streaming of point clouds.
@@ -167,7 +253,8 @@ func DecodeImageFromCamera(ctx context.Context, mimeType string, extra map[strin
167253
return img, nil
168254
}
169255

170-
// GetImageFromGetImages is a utility function to quickly implement GetImage from an already-implemented GetImages method.
256+
// GetImageFromGetImages will be deprecated after RSDK-11726.
257+
// It is a utility function to quickly implement GetImage from an already-implemented GetImages method.
171258
// It returns a byte slice and ImageMetadata, which is the same response signature as the Image method.
172259
//
173260
// If sourceName is nil, it returns the first image in the response slice.
@@ -179,30 +266,38 @@ func DecodeImageFromCamera(ctx context.Context, mimeType string, extra map[strin
179266
func GetImageFromGetImages(
180267
ctx context.Context,
181268
sourceName *string,
182-
mimeType string,
183269
cam Camera,
184270
extra map[string]interface{},
271+
filterSourceNames []string,
185272
) ([]byte, ImageMetadata, error) {
186-
images, _, err := cam.Images(ctx, extra)
273+
sourceNames := []string{}
274+
if sourceName != nil {
275+
sourceNames = append(sourceNames, *sourceName)
276+
}
277+
namedImages, _, err := cam.Images(ctx, sourceNames, extra)
187278
if err != nil {
188279
return nil, ImageMetadata{}, fmt.Errorf("could not get images from camera: %w", err)
189280
}
190-
if len(images) == 0 {
281+
if len(namedImages) == 0 {
191282
return nil, ImageMetadata{}, errors.New("no images returned from camera")
192283
}
193284

194-
// if mimeType is empty, use JPEG as default
195-
if mimeType == "" {
196-
mimeType = utils.MimeTypeJPEG
197-
}
198-
199285
var img image.Image
286+
var mimeType string
200287
if sourceName == nil {
201-
img = images[0].Image
288+
img, err = namedImages[0].Image(ctx)
289+
if err != nil {
290+
return nil, ImageMetadata{}, fmt.Errorf("could not get image from named image: %w", err)
291+
}
292+
mimeType = namedImages[0].MimeType()
202293
} else {
203-
for _, i := range images {
294+
for _, i := range namedImages {
204295
if i.SourceName == *sourceName {
205-
img = i.Image
296+
img, err = i.Image(ctx)
297+
if err != nil {
298+
return nil, ImageMetadata{}, fmt.Errorf("could not get image from named image: %w", err)
299+
}
300+
mimeType = i.MimeType()
206301
break
207302
}
208303
}
@@ -217,12 +312,13 @@ func GetImageFromGetImages(
217312

218313
imgBytes, err := rimage.EncodeImage(ctx, img, mimeType)
219314
if err != nil {
220-
return nil, ImageMetadata{}, fmt.Errorf("could not encode image: %w", err)
315+
return nil, ImageMetadata{}, fmt.Errorf("could not encode image with encoding %s: %w", mimeType, err)
221316
}
222317
return imgBytes, ImageMetadata{MimeType: mimeType}, nil
223318
}
224319

225-
// GetImagesFromGetImage is a utility function to quickly implement GetImages from an already-implemented GetImage method.
320+
// GetImagesFromGetImage will be deprecated after RSDK-11726.
321+
// It is a utility function to quickly implement GetImages from an already-implemented GetImage method.
226322
// It takes a mimeType, extra parameters, and a camera as args, and returns a slice of NamedImage and ResponseMetadata,
227323
// which is the same response signature as the Images method. We use the mimeType arg to specify
228324
// how to decode the image bytes returned from GetImage. The extra parameter is passed through to the underlying GetImage method.
@@ -249,12 +345,12 @@ func GetImagesFromGetImage(
249345
logger.Warnf("requested mime type %s, but received %s", mimeType, resMimetype)
250346
}
251347

252-
img, err := rimage.DecodeImage(ctx, resBytes, utils.WithLazyMIMEType(resMetadata.MimeType))
348+
namedImg, err := NamedImageFromBytes(resBytes, "", resMetadata.MimeType)
253349
if err != nil {
254-
return nil, resource.ResponseMetadata{}, fmt.Errorf("could not decode into image.Image: %w", err)
350+
return nil, resource.ResponseMetadata{}, fmt.Errorf("could not create named image: %w", err)
255351
}
256352

257-
return []NamedImage{{Image: img, SourceName: ""}}, resource.ResponseMetadata{CapturedAt: time.Now()}, nil
353+
return []NamedImage{namedImg}, resource.ResponseMetadata{CapturedAt: time.Now()}, nil
258354
}
259355

260356
// VideoSource is a camera that has `Stream` embedded to directly integrate with gostream.
@@ -276,7 +372,7 @@ type PointCloudSource interface {
276372

277373
// A ImagesSource is a source that can return a list of images with timestamp.
278374
type ImagesSource interface {
279-
Images(ctx context.Context, extra map[string]interface{}) ([]NamedImage, resource.ResponseMetadata, error)
375+
Images(ctx context.Context, filterSourceNames []string, extra map[string]interface{}) ([]NamedImage, resource.ResponseMetadata, error)
280376
}
281377

282378
// NewPropertiesError returns an error specific to a failure in Properties.

0 commit comments

Comments
 (0)