diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..de651b9 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,31 @@ +name: Run lint & unit tests + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + name: "Run unit tests" + runs-on: [buildjet-2vcpu-ubuntu-2204-arm] + container: + image: ghcr.io/viamrobotics/ocean-prefilter:arm64 + options: --platform linux/arm64 + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.21' + + + + - name: Run lint + run: make lint + + - name: Run unit tests + run: make test \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0afb554..59d8ab5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ ocean-prefilter *.swp +module.tar.gz +ocean-prefilter-appimage \ No newline at end of file diff --git a/Makefile b/Makefile index ea8135c..2ccc85f 100644 --- a/Makefile +++ b/Makefile @@ -69,3 +69,16 @@ docker-amd64-ci: BUILD_PUSH = --push docker-amd64-ci: $(BUILD_CMD) +# Test target for running Go tests +test: + LD_LIBRARY_PATH=/usr/local/lib:/usr/lib go test ./oceanprefilter + + +# Docker image and container details +DOCKER_IMAGE := ghcr.io/viamrobotics/ocean-prefilter:arm64 + +# Lint rule +lint: + GOFLAGS="-buildvcs=false" golangci-lint run --timeout 10m ./ + + diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..061efbc --- /dev/null +++ b/build.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# check if Docker is installed +if ! command -v docker &> /dev/null; then + echo "Docker is not installed. Please install Docker and try again." + exit 1 +fi + +# check the architecture +ARCH=$(uname -m) + +# pull the Docker image and log in +DOCKER_IMAGE="ghcr.io/viamrobotics/ocean-prefilter:$ARCH" + +docker pull $DOCKER_IMAGE + +if [ $? -ne 0 ]; then + echo "Failed to pull Docker image: $DOCKER_IMAGE" + exit 1 +fi + +docker run --rm \ + -e ARCH_TAG=$ARCH \ + -v "$(pwd)":/workspace \ + -w /workspace \ + $DOCKER_IMAGE \ + /bin/bash -c "make ocean-prefilter && make ocean-prefilter-appimage && make module.tar.gz" + diff --git a/etc/Dockerfile.debian.bookworm b/etc/Dockerfile.debian.bookworm index 5a01f73..b9e616c 100644 --- a/etc/Dockerfile.debian.bookworm +++ b/etc/Dockerfile.debian.bookworm @@ -125,4 +125,7 @@ RUN cd /root/opt/src && \ # install appimage-builder RUN pip3 install --break-system-packages git+https://github.com/AppImageCrafters/appimage-builder.git@61c8ddde9ef44b85d7444bbe79d80b44a6a5576d +# golangci-lint installation here +RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /usr/local/bin v1.59.1 + ENV PATH="/usr/local/go/bin:${PATH}" diff --git a/go.mod b/go.mod index e33ba51..36fdc78 100644 --- a/go.mod +++ b/go.mod @@ -2,13 +2,16 @@ module github.com/viamrobotics/ocean-prefilter go 1.21.7 +toolchain go1.21.12 + require ( github.com/Elvenson/xgboost-go v0.1.4 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/pkg/errors v0.9.1 go.viam.com/rdk v0.28.0 + go.viam.com/test v1.1.1-0.20220913152726-5da9916c08a2 go.viam.com/utils v0.1.79 - gocv.io/x/gocv v0.36.1 + gocv.io/x/gocv v0.37.0 ) require ( @@ -48,6 +51,7 @@ require ( github.com/go-gl/mathgl v1.0.0 // indirect github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9 // indirect github.com/go-pdf/fpdf v0.6.0 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect @@ -57,7 +61,8 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/gonuts/binary v0.2.0 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/flatbuffers v2.0.8+incompatible // indirect + github.com/google/go-cmp v0.5.9 // indirect github.com/google/s2a-go v0.1.4 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect @@ -68,8 +73,11 @@ require ( github.com/improbable-eng/grpc-web v0.15.0 // indirect github.com/jedib0t/go-pretty/v6 v6.4.6 // indirect github.com/jhump/protoreflect v1.15.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/kellydunn/golang-geo v0.7.0 // indirect github.com/klauspost/compress v1.16.5 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/kylelemons/go-gypsy v1.0.0 // indirect github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect @@ -107,11 +115,13 @@ require ( github.com/pion/webrtc/v3 v3.2.36 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rs/cors v1.9.0 // indirect github.com/sergi/go-diff v1.3.1 // indirect github.com/smartystreets/assertions v1.13.0 // indirect github.com/srikrsna/protoc-gen-gotag v0.6.2 // indirect github.com/stretchr/testify v1.9.0 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect github.com/viam-labs/go-libjpeg v0.3.1 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect @@ -128,7 +138,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.24.0 // indirect go.viam.com/api v0.1.302 // indirect - go.viam.com/test v1.1.1-0.20220913152726-5da9916c08a2 // indirect + golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.23.0 // indirect golang.org/x/exp v0.0.0-20230725012225-302865e7556b // indirect golang.org/x/image v0.18.0 // indirect diff --git a/go.sum b/go.sum index 9120dc5..bf1ac6d 100644 --- a/go.sum +++ b/go.sum @@ -259,6 +259,8 @@ github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3n github.com/fullstorydev/grpcurl v1.8.6 h1:WylAwnPauJIofYSHqqMTC1eEfUIzqzevXyogBxnQquo= github.com/fullstorydev/grpcurl v1.8.6/go.mod h1:WhP7fRQdhxz2TkL97u+TCb505sxfH78W1usyoB3tepw= github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gen2brain/malgo v0.11.21 h1:qsS4Dh6zhZgmvAW5CtKRxDjQzHbc2NJlBG9eE0tgS8w= github.com/gen2brain/malgo v0.11.21/go.mod h1:f9TtuN7DVrXMiV/yIceMeWpvanyVzJQMlBecJFVMxww= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -305,12 +307,15 @@ github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNI github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8= github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-restruct/restruct v1.2.0-alpha.0.20210525045353-983b86fa188e h1:PIFVUcdZ9OADg9XAsN0I8OzUzmYXHU+2msP2X7ST/fo= github.com/go-restruct/restruct v1.2.0-alpha.0.20210525045353-983b86fa188e/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk= @@ -417,8 +422,8 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/certificate-transparency-go v1.1.1/go.mod h1:FDKqPvSXawb2ecErVRrD+nfy23RCzyl7eqVCEmlT1Zs= -github.com/google/flatbuffers v2.0.6+incompatible h1:XHFReMv7nFFusa+CEokzWbzaYocKXI6C7hdU5Kgh9Lw= -github.com/google/flatbuffers v2.0.6+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v2.0.8+incompatible h1:ivUb1cGomAB101ZM1T0nOiWz9pSrTMoa9+EiY7igmkM= +github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -566,8 +571,9 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= @@ -592,16 +598,18 @@ github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYs github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -613,8 +621,9 @@ github.com/kylelemons/go-gypsy v1.0.0/go.mod h1:chkXM0zjdpXOiqkCW1XcCHDfjfk14PH2 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+sBqGZrRkMwPgg= github.com/ldez/gomoddirectives v0.2.1/go.mod h1:sGicqkRgBOg//JfpXwkB9Hj0X5RyJ7mlACM5B9f6Me4= -github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= @@ -711,8 +720,9 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU= @@ -841,6 +851,7 @@ github.com/pion/turn/v2 v2.1.3 h1:pYxTVWG2gpC97opdRc5IGsQ1lJ9O/IlNhkzj7MMrGAA= github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= github.com/pion/webrtc/v3 v3.2.36 h1:RM/miAv0M4TrhhS7h2mcZXt44K68WmpVDkUOgz2l2l8= github.com/pion/webrtc/v3 v3.2.36/go.mod h1:wWQz1PuKNSNK4VrJJNpPN3vZmKEi4zA6i2ynaQOlxIU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -902,6 +913,9 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= @@ -1008,11 +1022,11 @@ github.com/u2takey/ffmpeg-go v0.4.1 h1:l5ClIwL3N2LaH1zF3xivb3kP2HW95eyG5xhHE1JdZ github.com/u2takey/ffmpeg-go v0.4.1/go.mod h1:ruZWkvC1FEiUNjmROowOAps3ZcWxEiOpFoHCvk97kGc= github.com/u2takey/go-utils v0.3.1 h1:TaQTgmEZZeDHQFYfd+AdUT1cT4QJgJn/XVPELhHw4ys= github.com/u2takey/go-utils v0.3.1/go.mod h1:6e+v5vEZ/6gu12w/DC2ixZdZtCrNokVxD0JUklcqdCs= -github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ultraware/funlen v0.0.3/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= @@ -1113,12 +1127,13 @@ go.viam.com/utils v0.1.79 h1:rLxMPuJifGaDqQgGsfgzy60dMz6P44n5oEBbLEdggaM= go.viam.com/utils v0.1.79/go.mod h1:3xcBBlLsRX2eWpWuQrFJ7UVxsgXwJg8Fx30Rt/kp52g= go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 h1:WJhcL4p+YeDxmZWg141nRm7XC8IDmhz7lk5GpadO1Sg= go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= -gocv.io/x/gocv v0.36.1 h1:6XkEaPOk7h/umjy+MXgSEtSeCIgcPJhccUjrJFhjdTY= -gocv.io/x/gocv v0.36.1/go.mod h1:lmS802zoQmnNvXETpmGriBqWrENPei2GxYx5KUxJsMA= +gocv.io/x/gocv v0.37.0 h1:sISHvnApErjoJodz1Dxb8UAkFdITOB3vXGslbVu6Knk= +gocv.io/x/gocv v0.37.0/go.mod h1:lmS802zoQmnNvXETpmGriBqWrENPei2GxYx5KUxJsMA= goji.io v2.0.2+incompatible h1:uIssv/elbKRLznFUy3Xj4+2Mz/qKhek/9aZQDUMae7c= goji.io v2.0.2+incompatible/go.mod h1:sbqFwrtqZACxLBTQcdgVjFh54yGVCvwq8+w49MVMMIk= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -1340,6 +1355,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/meta.json b/meta.json index 8027ace..33c58ab 100644 --- a/meta.json +++ b/meta.json @@ -9,5 +9,9 @@ "model": "viam-labs:vision:ocean-prefilter" } ], + "build": { + "build": "./build.sh", + "arch": ["linux/arm64", "linux/amd64"] + }, "entrypoint": "ocean-prefilter-appimage" } diff --git a/oceanprefilter/histogram_compare.go b/oceanprefilter/histogram_compare.go new file mode 100644 index 0000000..7aedb94 --- /dev/null +++ b/oceanprefilter/histogram_compare.go @@ -0,0 +1,155 @@ +package oceanprefilter + +import ( + "fmt" + "image" + "math" + + "github.com/pkg/errors" +) + +const NVSplit = 4 +const NHSplit = 4 + +type Bucket struct { + Count int +} + +type Histogram struct { + Count int + Buckets []Bucket + spacing float64 +} + +func Hist(buckets int, minVal, maxVal float64, input []float64) Histogram { + // make evenly spaced buckets between min and max + // values outside of min and max are discarded + space := (maxVal - minVal) / float64(buckets) + bkts := make([]Bucket, buckets) + hist := Histogram{ + Count: 0, + Buckets: bkts, + spacing: space, + } + for _, val := range input { + if val >= maxVal || val < minVal { // skip over out of range values + continue + } + bucketIndex := int((val - minVal) / space) + if bucketIndex < 0 || bucketIndex > len(hist.Buckets)-1 { + panic(fmt.Sprintf("value of %v out of range between (%v, %v)", val, maxVal, minVal)) + } + hist.Count++ + hist.Buckets[bucketIndex].Count++ + } + return hist +} + +func histogramChangeFilter( + oldHists []Histogram, newImg image.Image, rc runConfig, +) (bool, []Histogram, error) { + firstHist := false + thresh := rc.threshold + if len(oldHists) == 0 { + firstHist = true // get the data for the first histogram + } + // find the horizon, take the average y value + linePoints, err := findHorizonLine(newImg) + if err != nil { + return false, nil, err + } + if len(linePoints) < 2 { + return false, nil, errors.New("function to find the horizon line returned less than 2 points") + } + cropY := int(math.Max(float64(linePoints[0].Y), float64(linePoints[1].Y))) + if rc.debug { + rc.logger.Debugf("found horizon at y = %v", cropY) + } + if cropY >= (newImg.Bounds().Max.Y-1) || cropY <= 1 { + return false, nil, errors.Errorf("could not find horizon in image. Got a horizon value of y = %v", cropY) + } + imgs, err := splitUpImage(newImg, rc.excludedZone, cropY, NHSplit, NVSplit) + if err != nil { + return false, nil, err + } + newHists := []Histogram{} // a histogram for each RGB channel + trigger := false + if !firstHist && len(imgs) != len(oldHists) { + return false, nil, errors.New("image changed drastically, cannot evaluate histogram difference. Can be caused by large amounts of motion") + } + for i, img := range imgs { + resultHists := createGrayHistograms(img) + for j, h := range resultHists { + newHist := h + splitTrigger := false + if !firstHist { + oldHist := oldHists[i*len(resultHists)+j] + if len(oldHist.Buckets) != len(newHist.Buckets) { + return false, nil, errors.Errorf("hists should have same number of buckets, old hist: %v, new hist: %v", len(oldHist.Buckets), len(newHist.Buckets)) + } + splitTrigger = histogramTrigger(oldHist, newHist, thresh) + } + newHists = append(newHists, newHist) + if splitTrigger { + trigger = true + } + } + } + return trigger, newHists, nil +} + +// take in an old image histogram, a new image, and return a trigger bool and the new image histogram +// returns a bool based on the thresh value +func histogramTrigger(oldHist, newHist Histogram, thresh float64) bool { + //result := basicCompare(oldHist, newHist) + ecdf1 := histogramToECDF(oldHist) + ecdf2 := histogramToECDF(newHist) + result := kolmogorovSmirnovTest(ecdf1, ecdf2) + trigger := false + if result >= thresh { + trigger = true + } + return trigger +} + +func createGrayHistograms(pic image.Image) []Histogram { + hists := make([]Histogram, 0, 1) + img := toGray(pic) + pix := []float64{} + for x := 0; x < img.Bounds().Dx(); x++ { + for y := 0; y < img.Bounds().Dy(); y++ { + c := img.GrayAt(x, y) + pix = append(pix, float64(c.Y)) + } + } + hists = append(hists, Hist(32, 0, 256, pix)) // 32 bin, 8 values in each bin in 255 total + return hists +} + +// histogramToECDF converts a histogram to an empirical cumulative distribution function (ECDF) +// Assumes histogram bins are evenly distributed over the data range +func histogramToECDF(hist Histogram) []float64 { + total := float64(hist.Count) + ecdf := make([]float64, len(hist.Buckets)) + cumulativeCount := 0.0 + if total == 0 { + return ecdf + } + for i, bkt := range hist.Buckets { + cumulativeCount += float64(bkt.Count) + ecdf[i] = cumulativeCount / total + } + return ecdf +} + +// kolmogorovSmirnovTest computes the Kolmogorov-Smirnov statistic for two ECDFs +func kolmogorovSmirnovTest(ecdf1, ecdf2 []float64) float64 { + maxDiff := 0.0 + for i := range ecdf1 { + diff := math.Abs(ecdf1[i] - ecdf2[i]) + if diff > maxDiff { + maxDiff = diff + } + } + return maxDiff +} diff --git a/oceanprefilter/oceanprefilter_test.go b/oceanprefilter/oceanprefilter_test.go new file mode 100644 index 0000000..10e2861 --- /dev/null +++ b/oceanprefilter/oceanprefilter_test.go @@ -0,0 +1,248 @@ +package oceanprefilter + +import ( + "context" + "image" + "image/color" + "sync/atomic" + "testing" + "unsafe" + + "go.viam.com/rdk/services/vision" + "go.viam.com/rdk/vision/classification" + "go.viam.com/rdk/vision/viscapture" + "go.viam.com/test" +) + +// MockImage creates a mock RGBA image for testing purposes. +func MockImage(width, height int) *image.RGBA { + img := image.NewRGBA(image.Rect(0, 0, width, height)) + // Fill the image with a solid color (e.g., white) + drawColor := color.RGBA{R: 255, G: 255, B: 255, A: 255} + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + img.Set(x, y, drawColor) + } + } + return img +} + +func TestConfigValidate(t *testing.T) { + // Test case where camera name is empty + cfg := &Config{ + CameraName: "", + } + path := "test_path" + dependencies, err := cfg.Validate(path) + test.That(t, dependencies, test.ShouldBeNil) + test.That(t, err.Error(), test.ShouldEqual, `expected "camera_name" attribute for object tracker "test_path"`) + + // Test case where detector name is empty + cfg = &Config{ + CameraName: "camera1", + DetectorName: "", + } + path = "test_path" + dependencies, err = cfg.Validate(path) + test.That(t, dependencies, test.ShouldResemble, []string{"camera1"}) + test.That(t, err, test.ShouldBeNil) + + // Test case where both camera name and detector name are provided + cfg = &Config{ + CameraName: "camera1", + DetectorName: "detector1", + } + path = "test_path" + dependencies, err = cfg.Validate(path) + test.That(t, dependencies, test.ShouldResemble, []string{"camera1", "detector1"}) + test.That(t, err, test.ShouldBeNil) +} + +func TestClassificationsFromCamera(t *testing.T) { + pf := &prefilter{ + camName: "configuredCamera", + triggerFlag: &atomic.Bool{}, + cancelContext: context.Background(), + } + ctx := context.Background() + cameraName := "testCamera" + + // Test case where camera name does not match + classifications, err := pf.ClassificationsFromCamera(ctx, cameraName, 1, nil) + test.That(t, classifications, test.ShouldBeNil) + test.That(t, err.Error(), test.ShouldEqual, "camera name given to method, testCamera is not the same as configured camera configuredCamera") + + // Test case where context is done + cancelledCtx, cancel := context.WithCancel(ctx) + cancel() + classifications, err = pf.ClassificationsFromCamera(cancelledCtx, "configuredCamera", 1, nil) + test.That(t, classifications, test.ShouldBeNil) + test.That(t, err.Error(), test.ShouldEqual, "module might be configuring: context canceled") + + // Test case where internal context is done + cancelledInternalCtx, internalCancel := context.WithCancel(ctx) + pf.cancelContext = cancelledInternalCtx + internalCancel() + classifications, err = pf.ClassificationsFromCamera(ctx, "configuredCamera", 1, nil) + test.That(t, classifications, test.ShouldBeNil) + test.That(t, err.Error(), test.ShouldEqual, "lost connection with background camera stream loop: context canceled") + + // Test case where trigger flag is not set + pf.cancelContext = context.Background() + classifications, err = pf.ClassificationsFromCamera(ctx, "configuredCamera", 1, nil) + test.That(t, classifications, test.ShouldBeEmpty) + test.That(t, err, test.ShouldBeNil) + + // Test case where trigger flag is set + pf.triggerFlag.Store(true) + classifications, err = pf.ClassificationsFromCamera(ctx, "configuredCamera", 1, nil) + expectedClassifications := classification.Classifications{ + classification.NewClassification(1.0, "TRIGGER"), + } + test.That(t, classifications, test.ShouldResemble, expectedClassifications) + test.That(t, err, test.ShouldBeNil) +} + +func TestClassifications(t *testing.T) { + pf := &prefilter{ + triggerFlag: &atomic.Bool{}, + cancelContext: context.Background(), + } + + ctx := context.Background() + img := image.NewRGBA(image.Rect(0, 0, 100, 100)) + + // Test case where context is canceled + cancelledCtx, cancel := context.WithCancel(ctx) + cancel() + classifications, err := pf.Classifications(cancelledCtx, img, 1, nil) + test.That(t, classifications, test.ShouldBeNil) + test.That(t, err.Error(), test.ShouldEqual, "module might be configuring: context canceled") + + // Test case where internal context is canceled + cancelledInternalCtx, internalCancel := context.WithCancel(ctx) + pf.cancelContext = cancelledInternalCtx + internalCancel() + classifications, err = pf.Classifications(ctx, img, 1, nil) + test.That(t, classifications, test.ShouldBeNil) + test.That(t, err.Error(), test.ShouldEqual, "lost connection with background camera stream loop: context canceled") + + // Test case where trigger flag is not set + pf.cancelContext = context.Background() + classifications, err = pf.Classifications(ctx, img, 1, nil) + test.That(t, classifications, test.ShouldBeEmpty) + test.That(t, err, test.ShouldBeNil) + + // Test case where trigger flag is set + pf.triggerFlag.Store(true) + classifications, err = pf.Classifications(ctx, img, 1, nil) + expectedClassifications := classification.Classifications{ + classification.NewClassification(1.0, "TRIGGER"), + } + test.That(t, classifications, test.ShouldResemble, expectedClassifications) + test.That(t, err, test.ShouldBeNil) +} + +func TestGetProperties(t *testing.T) { + // Create a mock prefilter instance + pf := &prefilter{ + properties: vision.Properties{ + ClassificationSupported: true, + DetectionSupported: false, + ObjectPCDsSupported: false, + }, + } + + // Mock context and extra parameters + ctx := context.Background() + extra := map[string]interface{}{} + + // Call the GetProperties method + properties, err := pf.GetProperties(ctx, extra) + + // Assert the returned properties + test.That(t, properties, test.ShouldNotBeNil) + test.That(t, *properties, test.ShouldResemble, pf.properties) + + // Assert no error returned + test.That(t, err, test.ShouldBeNil) +} + + +func TestCaptureAllFromCamera(t *testing.T) { + stubImage := image.NewRGBA(image.Rect(0, 0, 100, 100)) + imgInterface := image.Image(stubImage) // Convert *image.RGBA to image.Image interface + imgPtr := unsafe.Pointer(&imgInterface) + + // Mock image dimensions + mockWidth := 100 + mockHeight := 100 + + // Create a mock image + mockImg := image.NewRGBA(image.Rect(0, 0, mockWidth, mockHeight)) + + pf := &prefilter{ + camName: "configuredCamera", + triggerFlag: &atomic.Bool{}, + cancelContext: context.Background(), + } + + ctx := context.Background() + + atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&pf.currImg)), unsafe.Pointer(&mockImg)) + + // Test case where context is canceled + cancelledCtx, cancel := context.WithCancel(ctx) + cancel() + capture, err := pf.CaptureAllFromCamera(cancelledCtx, "configuredCamera", viscapture.CaptureOptions{ReturnImage: true, ReturnClassifications: true}, nil) + test.That(t, capture.Image, test.ShouldBeEmpty) + test.That(t, capture.Classifications, test.ShouldBeEmpty) + test.That(t, err.Error(), test.ShouldEqual, "context canceled") + + // Test case where internal context is canceled + cancelledInternalCtx, internalCancel := context.WithCancel(ctx) + pf.cancelContext = cancelledInternalCtx + internalCancel() + capture, err = pf.CaptureAllFromCamera(ctx, "configuredCamera", viscapture.CaptureOptions{ReturnImage: true, ReturnClassifications: true}, nil) + test.That(t, capture.Image, test.ShouldBeEmpty) + test.That(t, capture.Classifications, test.ShouldBeEmpty) + test.That(t, err.Error(), test.ShouldEqual, "context canceled") + + // Test case where camera name does not match + pf.cancelContext = context.Background() + capture, err = pf.CaptureAllFromCamera(ctx, "incorrectCamera", viscapture.CaptureOptions{ReturnImage: true, ReturnClassifications: true}, nil) + test.That(t, capture.Image, test.ShouldBeEmpty) + test.That(t, capture.Classifications, test.ShouldBeEmpty) + test.That(t, err.Error(), test.ShouldEqual, "Camera name \"incorrectCamera\" given to CaptureAllFromCamera is not the same as configured camera \"configuredCamera\"") + + // Test case where no image or classifications are requested + capture, err = pf.CaptureAllFromCamera(ctx, "configuredCamera", viscapture.CaptureOptions{}, nil) + test.That(t, capture.Image, test.ShouldBeEmpty) + test.That(t, capture.Classifications, test.ShouldBeEmpty) + test.That(t, err, test.ShouldBeNil) + + // Test case where only image is requested + pf.triggerFlag.Store(true) + atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&pf.currImg)), imgPtr) + capture, err = pf.CaptureAllFromCamera(ctx, "configuredCamera", viscapture.CaptureOptions{ReturnImage: true}, nil) + test.That(t, capture.Image, test.ShouldResemble, stubImage) + test.That(t, capture.Classifications, test.ShouldBeEmpty) + test.That(t, err, test.ShouldBeNil) + + // Test case where only classifications are requested + capture, err = pf.CaptureAllFromCamera(ctx, "configuredCamera", viscapture.CaptureOptions{ReturnClassifications: true}, nil) + expectedClassifications := viscapture.VisCapture{ + Classifications: classification.Classifications{ + classification.NewClassification(1.0, triggerClassName), + }, + } + test.That(t, capture.Image, test.ShouldBeNil) + test.That(t, capture.Classifications, test.ShouldResemble, expectedClassifications.Classifications) + test.That(t, err, test.ShouldBeNil) + + // Test case where both image and classifications are requested + capture, err = pf.CaptureAllFromCamera(ctx, "configuredCamera", viscapture.CaptureOptions{ReturnImage: true, ReturnClassifications: true}, nil) + test.That(t, capture.Image, test.ShouldResemble, stubImage) + test.That(t, capture.Classifications, test.ShouldResemble, expectedClassifications.Classifications) + test.That(t, err, test.ShouldBeNil) +}