| # Inference Guide |
|
|
| ## Scope |
|
|
| Use this guide only for PHPWind deployments you own or are explicitly |
| authorized to test. The examples perform local image inference only; they do |
| not automate account login flows or bypass access controls. |
|
|
| ## Specification |
|
|
| | Item | Value | |
| |---|---| |
| | Input name | `input` | |
| | Input shape | `[batch, 3, 64, 160]` | |
| | Input type | float32, range [0,1] (RGB) | |
| | Output name | `logits` | |
| | Output shape | `[batch, 4, 10]` | |
| | Decode | argmax per position → digit | |
| | ONNX opset | 18 | |
|
|
| Preprocessing is only three steps: RGB → resize to 160x64 (bilinear) → divide by 255. |
| No grayscale, no denoising, no mean/std normalization. |
|
|
| ## Python |
|
|
| ```bash |
| pip install onnxruntime pillow numpy |
| ``` |
|
|
| ```python |
| import numpy as np |
| import onnxruntime as ort |
| from PIL import Image |
| |
| _SESSION = None |
| |
| def get_session(model_path="model.onnx"): |
| global _SESSION |
| if _SESSION is None: |
| _SESSION = ort.InferenceSession(model_path, providers=["CPUExecutionProvider"]) |
| return _SESSION |
| |
| def solve_captcha(img_bytes_or_path, model_path="model.onnx"): |
| sess = get_session(model_path) |
| im = Image.open(img_bytes_or_path).convert("RGB").resize((160, 64), Image.BILINEAR) |
| x = np.asarray(im, dtype=np.float32) / 255.0 |
| x = x.transpose(2, 0, 1)[None] # (1,3,64,160) |
| logits = sess.run(None, {"input": x})[0] # (1,4,10) |
| return "".join(str(int(logits[0, p].argmax())) for p in range(4)) |
| |
| code = solve_captcha("captcha.png") # "4821" |
| ``` |
|
|
| ## Go (onnxruntime_go) |
| |
| Deps: `github.com/yalue/onnxruntime_go` + the onnxruntime shared library (.dylib/.so). |
|
|
| ```go |
| package main |
| |
| import ( |
| "bytes" |
| "image" |
| _ "image/png" |
| |
| "golang.org/x/image/draw" |
| ort "github.com/yalue/onnxruntime_go" |
| ) |
| |
| const ( |
| inW, inH = 160, 64 |
| nDigits = 4 |
| ) |
| |
| type Solver struct { |
| sess *ort.Session[float32] |
| in *ort.Tensor[float32] |
| out *ort.Tensor[float32] |
| } |
| |
| func NewSolver(modelPath, libPath string) (*Solver, error) { |
| if libPath != "" { ort.SetSharedLibraryPath(libPath) } |
| if err := ort.InitializeEnvironment(); err != nil { return nil, err } |
| in, err := ort.NewEmptyTensor[float32](ort.NewShape(1, 3, inH, inW)) |
| if err != nil { return nil, err } |
| out, err := ort.NewEmptyTensor[float32](ort.NewShape(1, nDigits, 10)) |
| if err != nil { in.Destroy(); return nil, err } |
| sess, err := ort.NewSession[float32](modelPath, |
| []string{"input"}, []string{"logits"}, |
| []*ort.Tensor[float32]{in}, []*ort.Tensor[float32]{out}) |
| if err != nil { in.Destroy(); out.Destroy(); return nil, err } |
| return &Solver{sess, in, out}, nil |
| } |
| |
| func (s *Solver) Solve(png []byte) (string, error) { |
| src, _, err := image.Decode(bytes.NewReader(png)) |
| if err != nil { return "", err } |
| dst := image.NewRGBA(image.Rect(0, 0, inW, inH)) |
| draw.ApproxBiLinear.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Over, nil) |
| |
| data := s.in.GetData() |
| idx := 0 |
| // NCHW layout: all R, then all G, then all B |
| for y := 0; y < inH; y++ { for x := 0; x < inW; x++ { r,_,_,_ := dst.At(x,y).RGBA(); data[idx]=float32(r>>8)/255; idx++ } } |
| for y := 0; y < inH; y++ { for x := 0; x < inW; x++ { _,g,_,_ := dst.At(x,y).RGBA(); data[idx]=float32(g>>8)/255; idx++ } } |
| for y := 0; y < inH; y++ { for x := 0; x < inW; x++ { _,_,b,_ := dst.At(x,y).RGBA(); data[idx]=float32(b>>8)/255; idx++ } } |
| |
| if err := s.sess.Run(); err != nil { return "", err } |
| got := s.out.GetData() |
| code := make([]byte, nDigits) |
| for p := 0; p < nDigits; p++ { |
| best := 0 |
| for c := 1; c < 10; c++ { if got[p*10+c] > got[p*10+best] { best = c } } |
| code[p] = '0' + byte(best) |
| } |
| return string(code), nil |
| } |
| ``` |
|
|
| > Note: `onnxruntime_go.NewSession` reads the model from a file. If you embed it with |
| > `go:embed`, use `NewSessionWithONNXData` with the bytes instead. |
| |
| ## Performance |
| |
| - Single inference: ~5-15ms (CPU, M1/modern x86) |
| - Full flow (load + preprocess + infer): ~10-30ms |
| - No GPU needed, memory < 50MB |
| |