phpwind-captcha-ocr / docs /en /INFERENCE.md
FlanChanXwO's picture
docs: focus model card on PHPWind captcha OCR
3739346 verified
|
Raw
History Blame Contribute Delete
4.01 kB

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

pip install onnxruntime pillow numpy
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).

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