55package camera
66
77import (
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+
2739func 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.
7789type 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
179266func 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.
278374type 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