Skip to content

Commit e4d85cd

Browse files
authored
[RSDK-9928] - Add preloaded images to image_file camera (viamrobotics#4830)
1 parent 8d7ef31 commit e4d85cd

File tree

4 files changed

+150
-19
lines changed

4 files changed

+150
-19
lines changed

components/camera/fake/fakedata.go

+14
Large diffs are not rendered by default.

components/camera/fake/image_file.go

+74-11
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
package fake
44

55
import (
6+
"bytes"
67
"context"
8+
"encoding/base64"
79
"errors"
810
"fmt"
911
"image"
12+
"image/jpeg"
1013
"time"
1114

1215
"go.viam.com/rdk/components/camera"
@@ -36,9 +39,16 @@ func init() {
3639
}
3740

3841
func newCamera(ctx context.Context, name resource.Name, newConf *fileSourceConfig, logger logging.Logger) (camera.Camera, error) {
39-
videoSrc := &fileSource{newConf.Color, newConf.Depth, newConf.PointCloud, newConf.CameraParameters}
42+
videoSrc := &fileSource{
43+
ColorFN: newConf.Color,
44+
DepthFN: newConf.Depth,
45+
PointCloudFN: newConf.PointCloud,
46+
Intrinsics: newConf.CameraParameters,
47+
PreloadedImage: newConf.PreloadedImage,
48+
}
49+
4050
imgType := camera.ColorStream
41-
if newConf.Color == "" {
51+
if newConf.Color == "" && newConf.PreloadedImage == "" {
4252
imgType = camera.DepthStream
4353
}
4454
cameraModel := camera.NewPinholeModelWithBrownConradyDistortion(newConf.CameraParameters, newConf.DistortionParameters)
@@ -56,10 +66,11 @@ func newCamera(ctx context.Context, name resource.Name, newConf *fileSourceConfi
5666

5767
// fileSource stores the paths to a color and depth image and a pointcloud.
5868
type fileSource struct {
59-
ColorFN string
60-
DepthFN string
61-
PointCloudFN string
62-
Intrinsics *transform.PinholeCameraIntrinsics
69+
ColorFN string
70+
DepthFN string
71+
PointCloudFN string
72+
Intrinsics *transform.PinholeCameraIntrinsics
73+
PreloadedImage string
6374
}
6475

6576
// fileSourceConfig is the attribute struct for fileSource.
@@ -69,6 +80,7 @@ type fileSourceConfig struct {
6980
Color string `json:"color_image_file_path,omitempty"`
7081
Depth string `json:"depth_image_file_path,omitempty"`
7182
PointCloud string `json:"pointcloud_file_path,omitempty"`
83+
PreloadedImage string `json:"preloaded_image,omitempty"` // can be "pizza", "dog", or "crowd"
7284
}
7385

7486
// Validate ensures all parts of the config are valid.
@@ -81,23 +93,41 @@ func (c fileSourceConfig) Validate(path string) ([]string, error) {
8193
}
8294
}
8395

96+
if c.PreloadedImage != "" {
97+
switch c.PreloadedImage {
98+
case "pizza", "dog", "crowd":
99+
// valid options
100+
default:
101+
return nil, fmt.Errorf("preloaded_image must be one of: pizza, dog, crowd. Got: %s", c.PreloadedImage)
102+
}
103+
}
104+
84105
return []string{}, nil
85106
}
86107

87108
// Read returns just the RGB image if it is present, or the depth map if the RGB image is not present.
88109
func (fs *fileSource) Read(ctx context.Context) (image.Image, func(), error) {
89-
if fs.ColorFN == "" && fs.DepthFN == "" {
110+
if fs.ColorFN == "" && fs.DepthFN == "" && fs.PreloadedImage == "" {
90111
return nil, nil, errors.New("no image file to read, so not implemented")
91112
}
92-
if fs.ColorFN == "" { // only depth info
113+
if fs.ColorFN == "" && fs.PreloadedImage == "" { // only depth info
93114
img, err := rimage.NewDepthMapFromFile(context.Background(), fs.DepthFN)
94115
if err != nil {
95116
return nil, nil, err
96117
}
97118
return img, func() {}, err
98119
}
99120

100-
img, err := rimage.NewImageFromFile(fs.ColorFN)
121+
var img *rimage.Image
122+
var err error
123+
124+
// Get image from preloaded image or file
125+
if fs.PreloadedImage != "" {
126+
img, err = getPreloadedImage(fs.PreloadedImage)
127+
} else {
128+
img, err = rimage.NewImageFromFile(fs.ColorFN)
129+
}
130+
101131
if err != nil {
102132
return nil, nil, err
103133
}
@@ -123,24 +153,35 @@ func (fs *fileSource) Read(ctx context.Context) (image.Image, func(), error) {
123153

124154
// Images returns the saved color and depth image if they are present.
125155
func (fs *fileSource) Images(ctx context.Context) ([]camera.NamedImage, resource.ResponseMetadata, error) {
126-
if fs.ColorFN == "" && fs.DepthFN == "" {
127-
return nil, resource.ResponseMetadata{}, errors.New("no image file to read, so not implemented")
156+
if fs.ColorFN == "" && fs.DepthFN == "" && fs.PreloadedImage == "" {
157+
return nil, resource.ResponseMetadata{}, errors.New("no image files to read, so not implemented")
128158
}
129159
imgs := []camera.NamedImage{}
160+
161+
if fs.PreloadedImage != "" {
162+
img, err := getPreloadedImage(fs.PreloadedImage)
163+
if err != nil {
164+
return nil, resource.ResponseMetadata{}, err
165+
}
166+
imgs = append(imgs, camera.NamedImage{img, "preloaded"})
167+
}
168+
130169
if fs.ColorFN != "" {
131170
img, err := rimage.NewImageFromFile(fs.ColorFN)
132171
if err != nil {
133172
return nil, resource.ResponseMetadata{}, err
134173
}
135174
imgs = append(imgs, camera.NamedImage{img, "color"})
136175
}
176+
137177
if fs.DepthFN != "" {
138178
dm, err := rimage.NewDepthMapFromFile(context.Background(), fs.DepthFN)
139179
if err != nil {
140180
return nil, resource.ResponseMetadata{}, err
141181
}
142182
imgs = append(imgs, camera.NamedImage{dm, "depth"})
143183
}
184+
144185
ts := time.Now()
145186
return imgs, resource.ResponseMetadata{CapturedAt: ts}, nil
146187
}
@@ -227,3 +268,25 @@ func (ss *StaticSource) NextPointCloud(ctx context.Context) (pointcloud.PointClo
227268
func (ss *StaticSource) Close(ctx context.Context) error {
228269
return nil
229270
}
271+
272+
// getPreloadedImage returns one of the preloaded images based on the name.
273+
func getPreloadedImage(name string) (*rimage.Image, error) {
274+
var imageBase64 []byte
275+
switch name {
276+
case "pizza":
277+
imageBase64 = pizzaBase64
278+
case "dog":
279+
imageBase64 = dogBase64
280+
case "crowd":
281+
imageBase64 = crowdBase64
282+
default:
283+
return nil, fmt.Errorf("unknown preloaded image: %s", name)
284+
}
285+
286+
d := base64.NewDecoder(base64.StdEncoding, bytes.NewReader(imageBase64))
287+
img, err := jpeg.Decode(d)
288+
if err != nil {
289+
return nil, fmt.Errorf("failed to decode image: %w", err)
290+
}
291+
return rimage.ConvertImage(img), nil
292+
}

components/camera/fake/image_file_test.go

+62
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,65 @@ func TestColorOddResolution(t *testing.T) {
114114
err = cam.Close(ctx)
115115
test.That(t, err, test.ShouldBeNil)
116116
}
117+
118+
func TestPreloadedImages(t *testing.T) {
119+
ctx := context.Background()
120+
logger := logging.NewTestLogger(t)
121+
preloadedImages := []string{"pizza", "dog", "crowd"}
122+
123+
for _, imgName := range preloadedImages {
124+
t.Run(imgName, func(t *testing.T) {
125+
cfg := &fileSourceConfig{PreloadedImage: imgName}
126+
cam, err := newCamera(ctx, resource.Name{API: camera.API}, cfg, logger)
127+
test.That(t, err, test.ShouldBeNil)
128+
129+
img, err := camera.DecodeImageFromCamera(ctx, utils.MimeTypeRawRGBA, nil, cam)
130+
test.That(t, err, test.ShouldBeNil)
131+
test.That(t, img, test.ShouldNotBeNil)
132+
133+
bounds := img.Bounds()
134+
test.That(t, bounds.Dx() > 0, test.ShouldBeTrue)
135+
test.That(t, bounds.Dy() > 0, test.ShouldBeTrue)
136+
137+
namedImages, metadata, err := cam.Images(ctx)
138+
test.That(t, err, test.ShouldBeNil)
139+
test.That(t, len(namedImages), test.ShouldEqual, 1)
140+
test.That(t, namedImages[0].SourceName, test.ShouldEqual, "preloaded")
141+
test.That(t, metadata.CapturedAt.IsZero(), test.ShouldBeFalse)
142+
143+
jpegBytes, mime, err := cam.Image(ctx, utils.MimeTypeJPEG, nil)
144+
test.That(t, err, test.ShouldBeNil)
145+
test.That(t, mime.MimeType, test.ShouldEqual, utils.MimeTypeJPEG)
146+
test.That(t, len(jpegBytes) > 0, test.ShouldBeTrue)
147+
148+
err = cam.Close(ctx)
149+
test.That(t, err, test.ShouldBeNil)
150+
})
151+
}
152+
153+
colorImgPath := artifact.MustPath("vision/objectdetection/detection_test.jpg")
154+
cfg := &fileSourceConfig{
155+
PreloadedImage: "pizza",
156+
Color: colorImgPath,
157+
}
158+
cam, err := newCamera(ctx, resource.Name{API: camera.API}, cfg, logger)
159+
test.That(t, err, test.ShouldBeNil)
160+
161+
// Should return both images
162+
namedImages, _, err := cam.Images(ctx)
163+
test.That(t, err, test.ShouldBeNil)
164+
test.That(t, len(namedImages), test.ShouldEqual, 2)
165+
test.That(t, namedImages[0].SourceName, test.ShouldEqual, "preloaded")
166+
test.That(t, namedImages[1].SourceName, test.ShouldEqual, "color")
167+
168+
cameraImg, err := camera.DecodeImageFromCamera(ctx, utils.MimeTypeRawRGBA, nil, cam)
169+
test.That(t, err, test.ShouldBeNil)
170+
preloadedImg, err := getPreloadedImage("pizza")
171+
test.That(t, err, test.ShouldBeNil)
172+
diff, _, err := rimage.CompareImages(cameraImg, preloadedImg)
173+
test.That(t, err, test.ShouldBeNil)
174+
test.That(t, diff, test.ShouldEqual, 0)
175+
176+
err = cam.Close(ctx)
177+
test.That(t, err, test.ShouldBeNil)
178+
}

components/camera/fake/worlddata.go

-8
This file was deleted.

0 commit comments

Comments
 (0)