Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RSDK-6179 - use docommand to postprocess #311

Merged
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
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
2 changes: 1 addition & 1 deletion cartofacade/capi.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ package cartofacade
#cgo CFLAGS: -I../viam-cartographer/src/carto_facade

// the libraries that need to be linked can be derived from line 258 of the build.ninja file that is autogenerated during make build
#cgo LDFLAGS: -lviam-cartographer -lcartographer -ldl -lm -labsl_hash -labsl_city -labsl_bad_optional_access -labsl_strerror -labsl_str_format_internal -labsl_synchronization -labsl_strings -labsl_throw_delegate -lcairo -llua5.3 -lstdc++ -lceres -lprotobuf -lglog -lboost_filesystem -lboost_iostreams -lpcl_io -lpcl_common -labsl_raw_hash_set
#cgo LDFLAGS: -Wl,-rpath,/opt/homebrew/lib -lviam-cartographer -lcartographer -ldl -lm -labsl_hash -labsl_city -labsl_bad_optional_access -labsl_strerror -labsl_str_format_internal -labsl_synchronization -labsl_strings -labsl_throw_delegate -lcairo -llua5.3 -lstdc++ -lceres -lprotobuf -lglog -lboost_filesystem -lboost_iostreams -lpcl_io -lpcl_common -labsl_raw_hash_set
kim-mishra marked this conversation as resolved.
Show resolved Hide resolved

#include "../viam-cartographer/src/carto_facade/carto_facade.h"
*/
Expand Down
231 changes: 231 additions & 0 deletions postprocess/postprocess.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// Package postprocess contains functionality to postprocess pointcloud maps
package postprocess

import (
"bytes"
"errors"
"image/color"
"math"

"github.com/golang/geo/r3"
"go.viam.com/rdk/pointcloud"
)

// Instruction describes the action of the postprocess step.
type Instruction int

const (
// Add is the instruction for adding points.
Add Instruction = iota
// Remove is the instruction for removing points.
Remove = iota
)

const (
fullConfidence = 100
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now i set all the added points to have 100% confidence. I did this because I figured if a user added it that number made sense. I could see an argument for having this be configurable but opted for this to simplify the DoCommand. What to others think?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using full confidence makes sense. Cannot think of a use case where we would want it to be different

removalRadius = 100 // mm
xKey = "X"
yKey = "Y"

// ToggleCommand can be used to turn postprocessing on and off.
ToggleCommand = "postprocess_toggle"
// AddCommand can be used to add points to the pointcloud map.
AddCommand = "postprocess_add"
// RemoveCommand can be used to remove points from the pointcloud map.
RemoveCommand = "postprocess_remove"
// UndoCommand can be used to undo last postprocessing step.
UndoCommand = "postprocess_undo"
)

var (
// ErrPointsNotASlice denotes that the points have not been properly formatted as a slice.
ErrPointsNotASlice = errors.New("could not parse provided points as a slice")

// ErrPointNotAMap denotes that a point has not been properly formatted as a map.
ErrPointNotAMap = errors.New("could not parse provided point as a map")

// ErrXNotProvided denotes that an X value was not provided.
ErrXNotProvided = errors.New("could X not provided")

// ErrXNotFloat64 denotes that an X value is not a float64.
ErrXNotFloat64 = errors.New("could not parse provided X as a float64")

// ErrYNotProvided denotes that a Y value was not provided.
ErrYNotProvided = errors.New("could X not provided")

// ErrYNotFloat64 denotes that an Y value is not a float64.
ErrYNotFloat64 = errors.New("could not parse provided X as a float64")

// ErrRemovingPoints denotes that something unexpected happened during removal.
ErrRemovingPoints = errors.New("unexpected number of points after removal")
randhid marked this conversation as resolved.
Show resolved Hide resolved
)

// Task can be used to construct a postprocessing step.
type Task struct {
Instruction Instruction
Points []r3.Vector
}

// ParseDoCommand parses postprocessing DoCommands into Tasks.
func ParseDoCommand(
unstructuredPoints interface{},
instruction Instruction,
) (Task, error) {
pointSlice, ok := unstructuredPoints.([]interface{})
if !ok {
return Task{}, ErrPointsNotASlice
}

task := Task{}
for _, point := range pointSlice {
pointMap, ok := point.(map[string]interface{})
if !ok {
return Task{}, ErrPointNotAMap
}

x, ok := pointMap[xKey]
randhid marked this conversation as resolved.
Show resolved Hide resolved
if !ok {
return Task{}, ErrXNotProvided
}

xFloat, ok := x.(float64)
if !ok {
return Task{}, ErrXNotFloat64
}

y, ok := pointMap[yKey]
if !ok {
return Task{}, ErrYNotProvided
}

yFloat, ok := y.(float64)
if !ok {
return Task{}, ErrXNotFloat64
}

task.Points = append(task.Points, r3.Vector{X: xFloat, Y: yFloat})
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we handle Z/make sure a user did not provide it or is this sufficient?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think not handling z is sufficient

}
return task, nil
}

/*
UpdatePointCloud iterated through a list of tasks and adds or removes points from data
and writes the updated pointcloud to updatedData.
*/
func UpdatePointCloud(
data []byte,
updatedData *[]byte,
tasks []Task,
) error {
*updatedData = append(*updatedData, data...)

// iterate through tasks and add or remove points
for _, task := range tasks {
switch task.Instruction {
case Add:
err := updatePointCloudWithAddedPoints(updatedData, task.Points)
if err != nil {
return err
}
case Remove:
err := updatePointCloudWithRemovedPoints(updatedData, task.Points)
if err != nil {
return err
}
}
}
return nil
}

func updatePointCloudWithAddedPoints(updatedData *[]byte, points []r3.Vector) error {
reader := bytes.NewReader(*updatedData)
pc, err := pointcloud.ReadPCD(reader)
if err != nil {
return err
}

for _, point := range points {
/*
Viam expects pointcloud data with fields "x y z" or "x y z rgb", and for
this to be specified in the pointcloud header in the FIELDS entry. If color
data is included in the pointcloud, Viam's services assume that the color
value encodes a confidence score for that data point. Viam expects the
confidence score to be encoded in the blue parameter of the RGB value, on a
scale from 1-100.
*/
err := pc.Set(point, pointcloud.NewColoredData(color.NRGBA{B: fullConfidence, R: math.MaxUint8}))
if err != nil {
return err
}
}

var buf bytes.Buffer
err = pointcloud.ToPCD(pc, &buf, pointcloud.PCDBinary)
if err != nil {
return err
}

// Initialize updatedData with new points
*updatedData = make([]byte, buf.Len())
updatedReader := bytes.NewReader(buf.Bytes())
_, err = updatedReader.Read(*updatedData)
if err != nil {
return err
}

return nil
}

func updatePointCloudWithRemovedPoints(updatedData *[]byte, points []r3.Vector) error {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what the best way to do this is - the way it is currently implemented the user would have to input the exact point they want to be removed. Maybe we should instead take in a point and radius and remove any points within that circle?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having a default sized radius would be fine for now.

If we wanted to cover more options then I’m wondering if there’s a good way to let users define a radius or a rectangle as options, depending on what gets sent in the do command

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated to have a default radius of 1 cm for now

reader := bytes.NewReader(*updatedData)
pc, err := pointcloud.ReadPCD(reader)
if err != nil {
return err
}

updatedPC := pointcloud.NewWithPrealloc(pc.Size() - len(points))
pointsVisited := 0

filterRemovedPoints := func(p r3.Vector, d pointcloud.Data) bool {
pointsVisited++
// Always return true so iteration continues

for _, point := range points {
// remove all points within the removalRadius from the removed points
if point.Distance(p) <= removalRadius {
return true
}
}

err := updatedPC.Set(p, d)
// end early if point cannot be set
return err == nil
}

pc.Iterate(0, 0, filterRemovedPoints)

// confirm iterate did not have to end early
if pc.Size() != pointsVisited {
/*
Note: this condition will occur if some error occurred while copying valid points
and will be how we can tell that this error occurred: err := updatedPC.Set(p, d)
*/
return ErrRemovingPoints
}

buf := bytes.Buffer{}
err = pointcloud.ToPCD(updatedPC, &buf, pointcloud.PCDBinary)
if err != nil {
return err
}

// Overwrite updatedData with new points
*updatedData = make([]byte, buf.Len())
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we check if nil? What happens if it is?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if what is nil?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*updatedData, which we pass into Read. Come to think fo it can buf.Len() ever be zero?

Copy link
Collaborator Author

@kim-mishra kim-mishra Jan 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this fmt.Print(make([]byte, 0) == nil) and it it is false, I will add a nil check!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's another one of these higher up, don't forget that one too!

updatedReader := bytes.NewReader(buf.Bytes())
_, err = updatedReader.Read(*updatedData)
if err != nil {
return err
}

return nil
}
Loading
Loading