Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e1a7d0eca | ||
|
|
1ac5ddbf33 | ||
|
|
8dffdc9bf9 | ||
|
|
23116f6a19 | ||
|
|
2badcb92e2 | ||
|
|
8abae7b9ba | ||
|
|
1bce98e083 |
+2
-1
@@ -50,9 +50,10 @@
|
|||||||
### Completed
|
### Completed
|
||||||
- Tag & Scanner CLI for configuration & persistent config
|
- Tag & Scanner CLI for configuration & persistent config
|
||||||
- Core script (MQTT, location estimation, Store in Redis)
|
- Core script (MQTT, location estimation, Store in Redis)
|
||||||
|
- NGSI-LD public api
|
||||||
|
- public dashboard (Node-RED)
|
||||||
|
|
||||||
### In Progress
|
### In Progress
|
||||||
- NGSI-LD data for public API
|
|
||||||
- Admin API with CRUD functions
|
- Admin API with CRUD functions
|
||||||
|
|
||||||
### Next Steps
|
### Next Steps
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# dlib compile cache
|
||||||
|
.docker-cache/
|
||||||
|
|
||||||
|
# face recognition models
|
||||||
|
models/
|
||||||
|
|
||||||
|
# executable
|
||||||
|
output/
|
||||||
@@ -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
|
||||||
@@ -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/
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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=
|
||||||
@@ -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
|
||||||
@@ -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/
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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=
|
||||||
Reference in New Issue
Block a user