IcecreamDetect / paddle_backend.py
ibsocr1's picture
Upload 13 files
f2798b7 verified
Raw History Blame Contribute Delete
9.07 kB
import json
import os
import shutil
import subprocess
import sys
import threading
import uuid
from pathlib import Path
from typing import Optional
ROOT = Path(__file__).resolve().parent
PD_DIR = ROOT / '.paddledetection'
PD_REPO = 'https://github.com/PaddlePaddle/PaddleDetection.git'
PD_BRANCH = os.getenv('PADDLEDETECTION_BRANCH', 'release/2.9')
_backend_lock = threading.RLock()
_training = {"running": False, "message": "not started", "error": None, "epoch": None, "log": ""}
def _run(cmd, cwd=None, timeout=None):
p = subprocess.run(cmd, cwd=cwd, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=timeout)
if p.returncode != 0:
raise RuntimeError(p.stdout[-16000:] or f'command failed: {p.returncode}')
return p.stdout
def ensure_paddledetection():
"""Clone official PaddleDetection only when first needed; no Hugging Face dependency."""
marker = PD_DIR / 'tools' / 'train.py'
if marker.exists():
return PD_DIR
PD_DIR.parent.mkdir(parents=True, exist_ok=True)
tmp = ROOT / '.paddledetection_tmp'
if tmp.exists():
shutil.rmtree(tmp, ignore_errors=True)
_run(['git', 'clone', '--depth', '1', '--branch', PD_BRANCH, PD_REPO, str(tmp)])
if marker.exists():
return PD_DIR
if PD_DIR.exists():
shutil.rmtree(PD_DIR, ignore_errors=True)
tmp.rename(PD_DIR)
return PD_DIR
def paddle_status():
try:
import paddle
return {"installed": True, "version": paddle.__version__, "device": paddle.device.get_device(), "cuda": bool(paddle.is_compiled_with_cuda())}
except Exception as e:
return {"installed": False, "error": str(e)}
def prepare_config(dataset_root: Path, classes_file: Path, output_dir: Path,
epochs: int, batch_size: int, learning_rate: float):
"""Create a native PaddleDetection RT-DETR R50vd config."""
pd = ensure_paddledetection()
cfg_dir = pd / 'configs' / 'rtdetr'
ds_cfg = pd / 'configs' / 'datasets' / 'icecream_coco.yml'
model_cfg = cfg_dir / 'icecream_rtdetr_r50vd.yml'
cfg_dir.mkdir(parents=True, exist_ok=True)
classes = [x.strip() for x in classes_file.read_text(encoding='utf-8').splitlines() if x.strip()]
if not classes:
raise RuntimeError('No classes configured.')
train_dir = (dataset_root / 'train').resolve().as_posix()
val_dir = (dataset_root / 'val').resolve().as_posix()
save_dir = output_dir.resolve().as_posix()
ds_cfg.write_text(f"""metric: COCO
num_classes: {len(classes)}
TrainDataset:
!COCODataSet
image_dir: images
anno_path: annotations.json
dataset_dir: {train_dir}
data_fields: ['image', 'gt_bbox', 'gt_class', 'is_crowd']
EvalDataset:
!COCODataSet
image_dir: images
anno_path: annotations.json
dataset_dir: {val_dir}
TestDataset:
!ImageFolder
anno_path: annotations.json
dataset_dir: {val_dir}
""", encoding='utf-8')
model_cfg.write_text(f"""_BASE_: [
'../datasets/icecream_coco.yml',
'../runtime.yml',
'_base_/optimizer_6x.yml',
'_base_/rtdetr_r50vd.yml',
'_base_/rtdetr_reader.yml',
]
architecture: DETR
num_classes: {len(classes)}
use_gpu: true
find_unused_parameters: True
log_iter: 1
save_dir: {save_dir}
weights: {save_dir}/model_final
pretrain_weights: https://paddledet.bj.bcebos.com/models/pretrained/ResNet50_vd_ssld_v2_pretrained.pdparams
epoch: {int(epochs)}
LearningRate:
base_lr: {float(learning_rate)}
TrainReader:
batch_size: {int(batch_size)}
worker_num: 0
use_shared_memory: false
EvalReader:
batch_size: 1
worker_num: 0
TestReader:
batch_size: 1
worker_num: 0
""", encoding='utf-8')
return pd, model_cfg, classes
def train(dataset_root: Path, classes_file: Path, output_dir: Path, epochs: int, batch_size: int, learning_rate: float):
if 'transformers' in sys.modules or 'torch' in sys.modules:
raise RuntimeError(
'Wrong training engine loaded: this package uses PaddleDetection RT-DETR, '
'not PyTorch or Hugging Face Transformers. Restart with this package.'
)
global _training
with _backend_lock:
if _training['running']:
return _training
_training = {"running": True, "message": "preparing PaddleDetection", "error": None, "epoch": None, "log": ""}
try:
pd, cfg, classes = prepare_config(dataset_root, classes_file, output_dir, epochs, batch_size, learning_rate)
output_dir.mkdir(parents=True, exist_ok=True)
status = paddle_status()
if not status.get('installed'):
raise RuntimeError(f'PaddlePaddle is not installed: {status.get("error")}')
if not status.get('cuda'):
raise RuntimeError(
'GPU PaddlePaddle is not available. '
f'Paddle device={status.get("device")}. Install the GPU PaddlePaddle build '
'matching the cloud CUDA runtime.'
)
env = os.environ.copy()
env['CUDA_VISIBLE_DEVICES'] = env.get('CUDA_VISIBLE_DEVICES', '0')
env['FLAGS_selected_gpus'] = env.get('FLAGS_selected_gpus', '0')
cmd = [os.sys.executable, '-u', 'tools/train.py', '-c', str(cfg), '--eval', '-o', 'use_gpu=true']
_training['message'] = 'training RT-DETR with PaddleDetection'
proc = subprocess.Popen(cmd, cwd=pd, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1)
lines = []
for line in proc.stdout:
lines.append(line.rstrip())
if len(lines) > 250:
lines = lines[-250:]
_training['log'] = '\n'.join(lines[-80:])
low = line.lower()
if 'epoch' in low:
import re
m = re.search(r'epoch[^0-9]*(\d+)', low)
if m:
_training['epoch'] = int(m.group(1))
code = proc.wait()
if code != 0:
raise RuntimeError('\n'.join(lines[-120:]) or f'PaddleDetection training exited with {code}')
final = output_dir / 'model_final.pdparams'
if not final.exists():
# Some PaddleDetection versions save model_final as a prefix with .pdparams.
candidates = list(output_dir.glob('model_final*'))
if not candidates:
raise RuntimeError('Training finished but model_final was not produced.')
(output_dir / 'classes.json').write_text(json.dumps({str(i): c for i, c in enumerate(classes)}, indent=2), encoding='utf-8')
_training = {"running": False, "message": "training complete", "error": None, "epoch": int(epochs), "log": '\n'.join(lines[-80:])}
except Exception as e:
_training = {"running": False, "message": "training failed", "error": str(e), "epoch": _training.get('epoch'), "log": _training.get('log', '')}
return _training
def training_status():
return dict(_training)
def model_ready(model_dir: Path):
return any((model_dir / name).exists() for name in ('model_final.pdparams', 'model_final.pdiparams'))
def predict(image_path: Path, model_dir: Path, threshold: float, classes_file: Path, output_dir: Path):
pd = ensure_paddledetection()
if not model_ready(model_dir):
raise RuntimeError('No trained model yet. Train the model first.')
cfg = pd / 'configs' / 'rtdetr' / 'icecream_rtdetr_r50vd.yml'
if not cfg.exists():
raise RuntimeError('PaddleDetection model config is missing; train once to generate it.')
output_dir.mkdir(parents=True, exist_ok=True)
for p in output_dir.iterdir():
if p.is_file():
p.unlink()
cmd = [os.sys.executable, '-u', 'tools/infer.py', '-c', str(cfg), '--infer_img', str(image_path), '--output_dir', str(output_dir), '--draw_threshold', str(threshold), '--save_threshold', str(threshold), '--save_results=True', '--visualize=True', '-o', f'use_gpu=true', f'weights={str(model_dir / "model_final")}']
log = _run(cmd, cwd=pd, timeout=int(os.getenv('INFER_TIMEOUT', '300')))
bbox_file = output_dir / 'bbox.json'
rows = json.loads(bbox_file.read_text(encoding='utf-8')) if bbox_file.exists() else []
classes = [x.strip() for x in classes_file.read_text(encoding='utf-8').splitlines() if x.strip()]
detections = []
for r in rows:
cid = int(r.get('category_id', 0))
detections.append({
'class': classes[cid] if 0 <= cid < len(classes) else str(cid),
'class_id': cid,
'confidence': round(float(r.get('score', 0.0)), 4),
'box': [round(float(r['bbox'][0]), 2), round(float(r['bbox'][1]), 2), round(float(r['bbox'][0] + r['bbox'][2]), 2), round(float(r['bbox'][1] + r['bbox'][3]), 2)]
})
counts = {}
for d in detections:
counts[d['class']] = counts.get(d['class'], 0) + 1
visual = next((p for p in output_dir.iterdir() if p.suffix.lower() in {'.jpg','.jpeg','.png'}), None)
return {'total': len(detections), 'counts': dict(sorted(counts.items())), 'detections': detections, 'visual_path': str(visual) if visual else None, 'log': log[-4000:]}