diff --git a/src/officesense_vision/.gitignore b/src/officesense_vision/.gitignore new file mode 100644 index 0000000..f0d14ef --- /dev/null +++ b/src/officesense_vision/.gitignore @@ -0,0 +1,8 @@ +# dlib compile cache +.docker-cache/ + +# face recognition models +models/ + +# executable +output/ diff --git a/src/officesense_vision/cameraSession/Dockerfile b/src/officesense_vision/cameraSession/Dockerfile new file mode 100644 index 0000000..c876026 --- /dev/null +++ b/src/officesense_vision/cameraSession/Dockerfile @@ -0,0 +1,55 @@ +ARG BUILDPLATFORM=linux/amd64 +FROM --platform=$BUILDPLATFORM golang:1.26-bookworm AS builder + +# Install dlib deps +RUN dpkg --add-architecture arm64 && \ + apt-get update && apt-get install -y \ + g++-aarch64-linux-gnu \ + libdlib-dev:arm64 \ + libjpeg-dev:arm64 \ + libblas-dev:arm64 \ + libopenblas-dev:arm64 \ + libatlas-base-dev:arm64 \ + liblapack-dev:arm64 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Compiler wrapper for arm64 +RUN printf '#!/bin/bash\nexec aarch64-linux-gnu-gcc "${@//-march=native/-march=armv8-a}"\n' \ + > /usr/local/bin/arm64-gcc && \ + printf '#!/bin/bash\nexec aarch64-linux-gnu-g++ "${@//-march=native/-march=armv8-a}"\n' \ + > /usr/local/bin/arm64-g++ && \ + chmod +x /usr/local/bin/arm64-gcc /usr/local/bin/arm64-g++ + +# Compiler variables for cross-compile to arm64 +ENV CGO_ENABLED=1 \ + GOOS=linux \ + GOARCH=arm64 \ + CC=/usr/local/bin/arm64-gcc \ + CXX=/usr/local/bin/arm64-g++ \ + PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig + +COPY go.mod go.sum ./ +RUN go mod download + +# Pre-compile all CGO deps into cache +RUN go build -v ./... 2>/dev/null || \ + go list -m all && \ + CGO_ENABLED=1 GOOS=linux GOARCH=arm64 \ + go build -gcflags="-trimpath" -v \ + $(go list -f '{{if .CgoFiles}}{{.ImportPath}}{{end}}' ./...) 2>/dev/null; exit 0 + +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg \ + go mod download + +COPY . . + +# Build - reuse CGO cache +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg \ + go build -o cameraSession + +FROM scratch AS export +COPY --from=builder /app/cameraSession /cameraSession \ No newline at end of file diff --git a/src/officesense_vision/cameraSession/Makefile b/src/officesense_vision/cameraSession/Makefile new file mode 100644 index 0000000..f87430d --- /dev/null +++ b/src/officesense_vision/cameraSession/Makefile @@ -0,0 +1,10 @@ +all: + docker buildx build --platform linux/amd64 --cache-from type=local,src=./.docker-cache --cache-to type=local,dest=./.docker-cache --target export --output type=local,dest=./output . + +models: + wget https://github.com/Kagami/go-face-testdata/raw/master/models/shape_predictor_5_face_landmarks.dat + wget https://github.com/Kagami/go-face-testdata/raw/master/models/dlib_face_recognition_resnet_model_v1.dat + wget https://github.com/Kagami/go-face-testdata/raw/master/models/mmod_human_face_detector.dat + +clean: + rm -r output/ diff --git a/src/officesense_vision/cameraSession/cameraSession.go b/src/officesense_vision/cameraSession/cameraSession.go new file mode 100644 index 0000000..2f64c24 --- /dev/null +++ b/src/officesense_vision/cameraSession/cameraSession.go @@ -0,0 +1,163 @@ +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "log" + "os" + "sync" + + "github.com/Kagami/go-face" + "github.com/blackjack/webcam" +) + +type InputPacket struct { + UUID string `json:"uuid"` + Descriptor face.Descriptor `json:"descriptor"` +} + +type ResultPacket struct { + UUID string `json:"uuid"` + Verified bool `json:"verified"` +} + +var ( + rec *face.Recognizer + mtx sync.Mutex + results = make([]ResultPacket, 0) +) + +func captureCameraFrame() ([]byte, error) { + cam, err := webcam.Open("/dev/video0") + if err != nil { + return nil, err + } + defer cam.Close() + + format := webcam.PixelFormat(uint32(0x47504a4d)) // MJPG + + _, _, _, err = cam.SetImageFormat(format, 1280, 720) + if err != nil { + return nil, fmt.Errorf("camera format error: %v", err) + } + + err = cam.StartStreaming() + if err != nil { + return nil, err + } + defer cam.StopStreaming() + + for { + err = cam.WaitForFrame(5) + if err != nil { + continue + } + + frame, err := cam.ReadFrame() + if err != nil { + return nil, err + } + + if len(frame) != 0 { + result := make([]byte, len(frame)) + copy(result, frame) + return result, nil + } + } +} + +// verfication thread +func verifyFace(packet InputPacket, descriptors []face.Descriptor, wg *sync.WaitGroup) { + defer wg.Done() + + const threshold = 0.35 + + verified := false + + for _, descriptor := range descriptors { + distance := face.SquaredEuclideanDistance(packet.Descriptor, descriptor) + + if distance < threshold { + verified = true + break + } + } + + mtx.Lock() + results = append(results, ResultPacket{ + UUID: packet.UUID, + Verified: verified, + }) + mtx.Unlock() +} + +func main() { + var err error + + // initialize recognizer + rec, err = face.NewRecognizer("./models") + if err != nil { + log.Fatalf("failed to initialize recognizer: %v", err) + } + defer rec.Close() + + // capture image + cameraImage, err := captureCameraFrame() + if err != nil { + log.Fatalf("failed to read camera: %v", err) + } + + // recognize faces from camera input + faces, err := rec.RecognizeCNN(cameraImage) + if err != nil { + log.Fatalf("recognition error: %v", err) + } + + if len(faces) == 0 { + fmt.Println("no face found.") + return + } + + var descriptors []face.Descriptor + + // extract descriptors to array + for _, f := range faces { + descriptors = append(descriptors, f.Descriptor) + } + + // read stdin + scanner := bufio.NewScanner(os.Stdin) + var wg sync.WaitGroup + + for scanner.Scan() { + line := scanner.Bytes() + + var packet InputPacket + + err := json.Unmarshal(line, &packet) + if err != nil { + log.Printf("invalid json packet: %v", err) + continue + } + + wg.Add(1) + + go verifyFace(packet, descriptors, &wg) + } + + if err := scanner.Err(); err != nil { + log.Printf("stdin error: %v", err) + } + + // wait threads to finish + wg.Wait() + + // print results to stdout + output, err := json.Marshal(results) + if err != nil { + log.Fatalf("failed to marshal results: %v", err) + } + + fmt.Println(string(output)) +} diff --git a/src/officesense_vision/cameraSession/go.mod b/src/officesense_vision/cameraSession/go.mod new file mode 100644 index 0000000..0dfa768 --- /dev/null +++ b/src/officesense_vision/cameraSession/go.mod @@ -0,0 +1,10 @@ +module OfficeSense/cameraSession + +go 1.26.3 + +require ( + github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e + github.com/blackjack/webcam v0.6.1 +) + +require golang.org/x/sys v0.14.0 // indirect diff --git a/src/officesense_vision/cameraSession/go.sum b/src/officesense_vision/cameraSession/go.sum new file mode 100644 index 0000000..13712c1 --- /dev/null +++ b/src/officesense_vision/cameraSession/go.sum @@ -0,0 +1,6 @@ +github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e h1:lqIUFzxaqyYqUn4MhzAvSAh4wIte/iLNcIEWxpT/qbc= +github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e/go.mod h1:9wdDJkRgo3SGTcFwbQ7elVIQhIr2bbBjecuY7VoqmPU= +github.com/blackjack/webcam v0.6.1 h1:K0T6Q0zto23U99gNAa5q/hFoye6uGcKr2aE6hFoxVoE= +github.com/blackjack/webcam v0.6.1/go.mod h1:zs+RkUZzqpFPHPiwBZ6U5B34ZXXe9i+SiHLKnnukJuI= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/src/officesense_vision/extractEmbeddings/Dockerfile b/src/officesense_vision/extractEmbeddings/Dockerfile new file mode 100644 index 0000000..f7e4fd3 --- /dev/null +++ b/src/officesense_vision/extractEmbeddings/Dockerfile @@ -0,0 +1,55 @@ +ARG BUILDPLATFORM=linux/amd64 +FROM --platform=$BUILDPLATFORM golang:1.26-bookworm AS builder + +# Install dlib deps +RUN dpkg --add-architecture arm64 && \ + apt-get update && apt-get install -y \ + g++-aarch64-linux-gnu \ + libdlib-dev:arm64 \ + libjpeg-dev:arm64 \ + libblas-dev:arm64 \ + libopenblas-dev:arm64 \ + libatlas-base-dev:arm64 \ + liblapack-dev:arm64 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Compiler wrapper for arm64 +RUN printf '#!/bin/bash\nexec aarch64-linux-gnu-gcc "${@//-march=native/-march=armv8-a}"\n' \ + > /usr/local/bin/arm64-gcc && \ + printf '#!/bin/bash\nexec aarch64-linux-gnu-g++ "${@//-march=native/-march=armv8-a}"\n' \ + > /usr/local/bin/arm64-g++ && \ + chmod +x /usr/local/bin/arm64-gcc /usr/local/bin/arm64-g++ + +# Compiler variables for cross-compile to arm64 +ENV CGO_ENABLED=1 \ + GOOS=linux \ + GOARCH=arm64 \ + CC=/usr/local/bin/arm64-gcc \ + CXX=/usr/local/bin/arm64-g++ \ + PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig + +COPY go.mod go.sum ./ +RUN go mod download + +# Pre-compile all CGO deps into cache +RUN go build -v ./... 2>/dev/null || \ + go list -m all && \ + CGO_ENABLED=1 GOOS=linux GOARCH=arm64 \ + go build -gcflags="-trimpath" -v \ + $(go list -f '{{if .CgoFiles}}{{.ImportPath}}{{end}}' ./...) 2>/dev/null; exit 0 + +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg \ + go mod download + +COPY . . + +# Build - reuse CGO cache +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg \ + go build -o extractEmbeddings + +FROM scratch AS export +COPY --from=builder /app/extractEmbeddings /extractEmbeddings \ No newline at end of file diff --git a/src/officesense_vision/extractEmbeddings/Makefile b/src/officesense_vision/extractEmbeddings/Makefile new file mode 100644 index 0000000..f87430d --- /dev/null +++ b/src/officesense_vision/extractEmbeddings/Makefile @@ -0,0 +1,10 @@ +all: + docker buildx build --platform linux/amd64 --cache-from type=local,src=./.docker-cache --cache-to type=local,dest=./.docker-cache --target export --output type=local,dest=./output . + +models: + wget https://github.com/Kagami/go-face-testdata/raw/master/models/shape_predictor_5_face_landmarks.dat + wget https://github.com/Kagami/go-face-testdata/raw/master/models/dlib_face_recognition_resnet_model_v1.dat + wget https://github.com/Kagami/go-face-testdata/raw/master/models/mmod_human_face_detector.dat + +clean: + rm -r output/ diff --git a/src/officesense_vision/extractEmbeddings/extractEmbeddings.go b/src/officesense_vision/extractEmbeddings/extractEmbeddings.go new file mode 100644 index 0000000..436d7fe --- /dev/null +++ b/src/officesense_vision/extractEmbeddings/extractEmbeddings.go @@ -0,0 +1,126 @@ +package main + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "image" + "image/jpeg" + "io" + + "os" + + "github.com/Kagami/go-face" + "golang.org/x/image/draw" +) + +type Result struct { + Success bool `json:"success"` + Descriptor face.Descriptor `json:"descriptor"` + Error string `json:"error"` +} + +var res Result + +func atExit() { + resString, err := json.Marshal(res) + if err != nil { + os.Exit(1) + } + + fmt.Println(string(resString)) + + os.Exit(0) +} + +func resizeIfNeeded(img image.Image, max int) image.Image { + b := img.Bounds() + w := b.Dx() + h := b.Dy() + + if w <= max && h <= max { + return img + } + + var scale float64 + if w > h { + scale = float64(max) / float64(w) + } else { + scale = float64(max) / float64(h) + } + + newW := int(float64(w) * scale) + newH := int(float64(h) * scale) + + dst := image.NewRGBA(image.Rect(0, 0, newW, newH)) + draw.BiLinear.Scale(dst, dst.Bounds(), img, b, draw.Over, nil) + + return dst +} + +func main() { + defer atExit() + + // Read input image from stdin in Base64 encoding + input, err := io.ReadAll(os.Stdin) + if err != nil { + res.Success = false + res.Error = "[STDIN]: " + err.Error() + return + } + + // Decode Base64 + image, err := base64.StdEncoding.DecodeString(string(input)) + if err != nil { + res.Success = false + res.Error = "[BASE64]: " + err.Error() + return + } + + imageJPEG, err := jpeg.Decode(bytes.NewReader(image)) + if err != nil { + res.Success = false + res.Error = "[JPEG][DECODE]: " + err.Error() + return + } + + imageJPEG = resizeIfNeeded(imageJPEG, 600) + + var buf bytes.Buffer + err = jpeg.Encode(&buf, imageJPEG, &jpeg.Options{Quality: 90}) + if err != nil { + res.Success = false + res.Error = "[JPEG][ENCODE]: " + err.Error() + return + } + + image = buf.Bytes() + + // Create recognizer - provide ./models dir + rec, err := face.NewRecognizer("models") + if err != nil { + res.Success = false + res.Error = "[DLIB][INIT]: " + err.Error() + return + } + defer rec.Close() + + faces, err := rec.RecognizeCNN(image) + if err != nil { + res.Success = false + res.Error = "[DLIB][CNN]: " + err.Error() + return + } else if len(faces) == 0 { + res.Success = false + res.Error = "[DLIB][CNN]: No face found in image." + return + } else if len(faces) != 1 { + res.Success = false + res.Error = "[DLIB][CNN]: Multiple faces found in image." + return + } + + res.Success = true + res.Descriptor = faces[0].Descriptor +} diff --git a/src/officesense_vision/extractEmbeddings/go.mod b/src/officesense_vision/extractEmbeddings/go.mod new file mode 100644 index 0000000..11258c0 --- /dev/null +++ b/src/officesense_vision/extractEmbeddings/go.mod @@ -0,0 +1,8 @@ +module OfficeSense/extractEmbeddings + +go 1.26.3 + +require ( + github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e + golang.org/x/image v0.41.0 +) diff --git a/src/officesense_vision/extractEmbeddings/go.sum b/src/officesense_vision/extractEmbeddings/go.sum new file mode 100644 index 0000000..8164ae0 --- /dev/null +++ b/src/officesense_vision/extractEmbeddings/go.sum @@ -0,0 +1,4 @@ +github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e h1:lqIUFzxaqyYqUn4MhzAvSAh4wIte/iLNcIEWxpT/qbc= +github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e/go.mod h1:9wdDJkRgo3SGTcFwbQ7elVIQhIr2bbBjecuY7VoqmPU= +golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo= +golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=