Initial orientator
This commit is contained in:
181
internal/detector/detector.go
Normal file
181
internal/detector/detector.go
Normal file
@@ -0,0 +1,181 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user