Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
20b4493
wip
hexbabe Jul 29, 2025
860a40a
Make lint
hexbabe Jul 29, 2025
34a5295
Update GetImages method and use local proto replacement
hexbabe Jul 31, 2025
38ad082
Fix error
hexbabe Jul 31, 2025
330bbf4
Fix helpers; Fix server and client format vs. mimetype handling logic
hexbabe Aug 1, 2025
9f36e90
Remove unnecessary transcode in camera client.go
hexbabe Aug 4, 2025
489517e
Commit pprof changes
hexbabe Aug 5, 2025
a0edfcf
Fix client format -> mimetype conversion
hexbabe Aug 5, 2025
5292ea9
Add tentative measure for detecting mismatched mime types; Remove pprof
hexbabe Aug 14, 2025
c58ed99
Fix image_file.go Images signature
hexbabe Aug 14, 2025
694104d
Revert POC changes for stream server
hexbabe Aug 14, 2025
7168a16
Fix type assertions in client tests
hexbabe Aug 14, 2025
15d4063
Add comments about keeping mime types and registered formats in line
hexbabe Aug 18, 2025
1a22070
Add NamedImage tests
hexbabe Aug 18, 2025
6f50d95
Add Images tests in camera_test.go; Fix client test; Don't unnecessar…
hexbabe Aug 18, 2025
3df0b2d
wip
hexbabe Aug 18, 2025
8a0a27f
Add dupe check in client and server
hexbabe Aug 18, 2025
c9fb6db
Add client tests for new images signature
hexbabe Aug 18, 2025
5abd419
Add server test for new signature
hexbabe Aug 18, 2025
ba49ae5
Fix lint in some tests
hexbabe Aug 18, 2025
85ce5b9
Update image file camera to respect filters and tests correspondingly
hexbabe Aug 18, 2025
fd6faa5
Add error on unknown source name for image_file
hexbabe Aug 18, 2025
96b9118
Add back removed comment
hexbabe Aug 18, 2025
2687a4e
Remove duplicate check in client
hexbabe Aug 20, 2025
f79755f
Use guard pattern for NamedImage's Image and Bytes methods
hexbabe Aug 21, 2025
7db873d
Remove unnecessary transcodes in collectors; Remove unused func
hexbabe Aug 21, 2025
122a49b
Add export comment for ErrMIMETypeBytesMismatch
hexbabe Aug 21, 2025
5bb18d3
Update comment for accuracy
hexbabe Aug 21, 2025
641aec5
Add comment about format
hexbabe Aug 25, 2025
1fbdbc7
GetImagesFromGetImage deprecation comments
hexbabe Aug 25, 2025
2bd2f02
Address camera_test.go review
hexbabe Aug 25, 2025
9bf4078
DRY up with ImagesExactlyEqual helper
hexbabe Aug 25, 2025
dcad36a
Make encoding err more informative
hexbabe Aug 26, 2025
edf294c
Add test coverage in client_test.go
hexbabe Aug 26, 2025
319e306
Add get images list order test
hexbabe Aug 26, 2025
0c8254d
Fix error string assert now that there's a new error string
hexbabe Aug 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 117 additions & 21 deletions components/camera/camera.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
package camera

import (
"bytes"
"context"
"fmt"
"image"
"strings"
"time"

"github.com/pkg/errors"
Expand All @@ -24,6 +26,16 @@ import (
"go.viam.com/rdk/utils"
)

// ErrMIMETypeBytesMismatch indicates that the NamedImage's mimeType does not match the image bytes header.
//
// For example, if the image bytes are JPEG, but the mimeType is PNG, this error will be returned.
// This likely means there is a bug in the code that created the GetImages response.
//
// However, there may still be valid, decodeable underlying JPEG image bytes.
// If you want to decode the image bytes as a JPEG regardless of the mismatch, you can recover from this error,
// call the .Bytes() method, then decode the image bytes as JPEG manually with image.Decode().
var ErrMIMETypeBytesMismatch = errors.New("mime_type does not match the image bytes")

func init() {
resource.RegisterAPI(API, resource.APIRegistration[Camera]{
RPCServiceServerConstructor: NewRPCServiceServer,
Expand Down Expand Up @@ -75,8 +87,81 @@ type Properties struct {

// NamedImage is a struct that associates the source from where the image came from to the Image.
type NamedImage struct {
Image image.Image
data []byte
img image.Image
SourceName string
mimeType string
}

// NamedImageFromBytes constructs a NamedImage from a byte slice, source name, and mime type.
func NamedImageFromBytes(data []byte, sourceName, mimeType string) (NamedImage, error) {
if data == nil {
return NamedImage{}, fmt.Errorf("must provide image bytes to construct a named image from bytes")
}
if mimeType == "" {
return NamedImage{}, fmt.Errorf("must provide a mime type to construct a named image")
}
return NamedImage{data: data, SourceName: sourceName, mimeType: mimeType}, nil
}

// NamedImageFromImage constructs a NamedImage from an image.Image, source name, and mime type.
func NamedImageFromImage(img image.Image, sourceName, mimeType string) (NamedImage, error) {
if img == nil {
return NamedImage{}, fmt.Errorf("must provide image to construct a named image from image")
}
if mimeType == "" {
return NamedImage{}, fmt.Errorf("must provide a mime type to construct a named image")
}
return NamedImage{img: img, SourceName: sourceName, mimeType: mimeType}, nil
}

// Image returns the image.Image of the NamedImage.
func (ni *NamedImage) Image(ctx context.Context) (image.Image, error) {
if ni.img != nil {
return ni.img, nil
}
if ni.data == nil {
return nil, fmt.Errorf("no image or image bytes available")
}

reader := bytes.NewReader(ni.data)
_, header, err := image.DecodeConfig(reader)
if err != nil {
return nil, fmt.Errorf("could not decode image config: %w", err)
}

if header != "" && !strings.Contains(ni.mimeType, header) {
return nil, fmt.Errorf("%w: expected %s, got %s", ErrMIMETypeBytesMismatch, ni.mimeType, header)
}

img, err := rimage.DecodeImage(ctx, ni.data, ni.mimeType)
if err != nil {
return nil, fmt.Errorf("could not decode bytes into image.Image: %w", err)
}
ni.img = img
return ni.img, nil
}

// Bytes returns the byte slice of the NamedImage.
func (ni *NamedImage) Bytes(ctx context.Context) ([]byte, error) {
if ni.data != nil {
return ni.data, nil
}
if ni.img == nil {
return nil, fmt.Errorf("no image or image bytes available")
}

data, err := rimage.EncodeImage(ctx, ni.img, ni.mimeType)
if err != nil {
return nil, fmt.Errorf("could not encode image with encoding %s: %w", ni.mimeType, err)
}
ni.data = data
return ni.data, nil
}

// MimeType returns the mime type of the NamedImage.
func (ni *NamedImage) MimeType() string {
return ni.mimeType
}

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

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

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

// GetImageFromGetImages is a utility function to quickly implement GetImage from an already-implemented GetImages method.
// GetImageFromGetImages will be deprecated after RSDK-11726.
// It is a utility function to quickly implement GetImage from an already-implemented GetImages method.
// It returns a byte slice and ImageMetadata, which is the same response signature as the Image method.
//
// If sourceName is nil, it returns the first image in the response slice.
Expand All @@ -179,30 +266,38 @@ func DecodeImageFromCamera(ctx context.Context, mimeType string, extra map[strin
func GetImageFromGetImages(
ctx context.Context,
sourceName *string,
mimeType string,
cam Camera,
extra map[string]interface{},
filterSourceNames []string,
) ([]byte, ImageMetadata, error) {
images, _, err := cam.Images(ctx, extra)
sourceNames := []string{}
if sourceName != nil {
sourceNames = append(sourceNames, *sourceName)
}
namedImages, _, err := cam.Images(ctx, sourceNames, extra)
if err != nil {
return nil, ImageMetadata{}, fmt.Errorf("could not get images from camera: %w", err)
}
if len(images) == 0 {
if len(namedImages) == 0 {
return nil, ImageMetadata{}, errors.New("no images returned from camera")
}

// if mimeType is empty, use JPEG as default
if mimeType == "" {
mimeType = utils.MimeTypeJPEG
}

var img image.Image
var mimeType string
if sourceName == nil {
img = images[0].Image
img, err = namedImages[0].Image(ctx)
if err != nil {
return nil, ImageMetadata{}, fmt.Errorf("could not get image from named image: %w", err)
}
mimeType = namedImages[0].MimeType()
} else {
for _, i := range images {
for _, i := range namedImages {
if i.SourceName == *sourceName {
img = i.Image
img, err = i.Image(ctx)
if err != nil {
return nil, ImageMetadata{}, fmt.Errorf("could not get image from named image: %w", err)
}
mimeType = i.MimeType()
break
}
}
Expand All @@ -217,12 +312,13 @@ func GetImageFromGetImages(

imgBytes, err := rimage.EncodeImage(ctx, img, mimeType)
if err != nil {
return nil, ImageMetadata{}, fmt.Errorf("could not encode image: %w", err)
return nil, ImageMetadata{}, fmt.Errorf("could not encode image with encoding %s: %w", mimeType, err)
}
return imgBytes, ImageMetadata{MimeType: mimeType}, nil
}

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

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

return []NamedImage{{Image: img, SourceName: ""}}, resource.ResponseMetadata{CapturedAt: time.Now()}, nil
return []NamedImage{namedImg}, resource.ResponseMetadata{CapturedAt: time.Now()}, nil
}

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

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

// NewPropertiesError returns an error specific to a failure in Properties.
Expand Down
Loading
Loading