182 lines
4.1 KiB
Go
182 lines
4.1 KiB
Go
package detector
|
|
|
|
import (
|
|
_ "embed"
|
|
"fmt"
|
|
"image"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sync"
|
|
|
|
ort "github.com/yalue/onnxruntime_go"
|
|
)
|
|
|
|
// Angles map to model's [2:6] logits in the same order as the Python reference:
|
|
// indices [0,1,2,3] -> degrees [0, 270, 180, 90].
|
|
var dedocMap = [4]int{0, 270, 180, 90}
|
|
|
|
//go:embed assets/dedoc_orientation.onnx
|
|
var modelBytes []byte
|
|
|
|
type Detector struct {
|
|
session *ort.AdvancedSession
|
|
input *ort.Tensor[float32]
|
|
output *ort.Tensor[float32]
|
|
mu sync.Mutex // ONNX session is not goroutine-safe under concurrent Run
|
|
}
|
|
|
|
var (
|
|
once sync.Once
|
|
instance *Detector
|
|
initErr error
|
|
)
|
|
|
|
// Get returns the lazily-initialized singleton detector.
|
|
func Get() (*Detector, error) {
|
|
once.Do(func() {
|
|
instance, initErr = newDetector()
|
|
})
|
|
return instance, initErr
|
|
}
|
|
|
|
func newDetector() (*Detector, error) {
|
|
libPath, err := extractRuntimeLib()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("extract runtime: %w", err)
|
|
}
|
|
ort.SetSharedLibraryPath(libPath)
|
|
if err := ort.InitializeEnvironment(); err != nil {
|
|
return nil, fmt.Errorf("init ort env: %w", err)
|
|
}
|
|
|
|
modelPath, err := extractModel()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("extract model: %w", err)
|
|
}
|
|
|
|
inputs, outputs, err := ort.GetInputOutputInfo(modelPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("inspect model: %w", err)
|
|
}
|
|
if len(inputs) == 0 || len(outputs) == 0 {
|
|
return nil, fmt.Errorf("model has no inputs/outputs")
|
|
}
|
|
|
|
inShape := ort.NewShape(1, 3, ImgSize, ImgSize)
|
|
inTensor, err := ort.NewEmptyTensor[float32](inShape)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
outShape := ort.NewShape(1, 6)
|
|
outTensor, err := ort.NewEmptyTensor[float32](outShape)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
opts, err := ort.NewSessionOptions()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer opts.Destroy()
|
|
_ = opts.SetIntraOpNumThreads(runtime.NumCPU())
|
|
|
|
sess, err := ort.NewAdvancedSession(
|
|
modelPath,
|
|
[]string{inputs[0].Name},
|
|
[]string{outputs[0].Name},
|
|
[]ort.ArbitraryTensor{inTensor},
|
|
[]ort.ArbitraryTensor{outTensor},
|
|
opts,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create session: %w", err)
|
|
}
|
|
|
|
return &Detector{
|
|
session: sess,
|
|
input: inTensor,
|
|
output: outTensor,
|
|
}, nil
|
|
}
|
|
|
|
// Predict returns the rotation needed to right the image and the confidence.
|
|
// Output angle ∈ {0, 90, 180, 270}; meaning matches the Python reference.
|
|
func (d *Detector) Predict(img image.Image) (angle int, confidence float64, err error) {
|
|
if img == nil {
|
|
return 0, 0, fmt.Errorf("nil image")
|
|
}
|
|
data := Preprocess(img)
|
|
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
dst := d.input.GetData()
|
|
copy(dst, data)
|
|
|
|
if err := d.session.Run(); err != nil {
|
|
return 0, 0, fmt.Errorf("ort run: %w", err)
|
|
}
|
|
logits := d.output.GetData() // shape [1,6]
|
|
// Use only orientation logits at index [2:6].
|
|
probs := softmax4(logits[2:6])
|
|
idx := 0
|
|
best := probs[0]
|
|
for i := 1; i < 4; i++ {
|
|
if probs[i] > best {
|
|
best = probs[i]
|
|
idx = i
|
|
}
|
|
}
|
|
return dedocMap[idx], float64(best), nil
|
|
}
|
|
|
|
func softmax4(x []float32) [4]float32 {
|
|
var max float32 = x[0]
|
|
for i := 1; i < 4; i++ {
|
|
if x[i] > max {
|
|
max = x[i]
|
|
}
|
|
}
|
|
var sum float32
|
|
var e [4]float32
|
|
for i := 0; i < 4; i++ {
|
|
e[i] = float32(math.Exp(float64(x[i] - max)))
|
|
sum += e[i]
|
|
}
|
|
for i := 0; i < 4; i++ {
|
|
e[i] /= sum
|
|
}
|
|
return e
|
|
}
|
|
|
|
func extractRuntimeLib() (string, error) {
|
|
return materialize("runtime", runtimeLibFileName, runtimeLibBytes)
|
|
}
|
|
|
|
func extractModel() (string, error) {
|
|
return materialize("model", "dedoc_orientation.onnx", modelBytes)
|
|
}
|
|
|
|
// materialize writes embedded bytes to the user cache dir, reusing the existing
|
|
// file if its size already matches.
|
|
func materialize(subdir, name string, data []byte) (string, error) {
|
|
cacheDir, err := os.UserCacheDir()
|
|
if err != nil {
|
|
cacheDir = os.TempDir()
|
|
}
|
|
dir := filepath.Join(cacheDir, "orientator", subdir, fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH))
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return "", err
|
|
}
|
|
out := filepath.Join(dir, name)
|
|
if st, err := os.Stat(out); err == nil && int(st.Size()) == len(data) {
|
|
return out, nil
|
|
}
|
|
if err := os.WriteFile(out, data, 0o644); err != nil {
|
|
return "", err
|
|
}
|
|
return out, nil
|
|
}
|