feat: 导入 IRSTD 研究代码与文档(小目标检测)
- 新增 irstd 核心包、scripts、tests、configs - 新增研究文档:literature_review / mathematical_analysis / novelty_matrix / experiment_plan / research_decision / research_log / manuscript_outline - 新增 evidence、results 数据与审计产物 - 新增 pyproject.toml、requirements、.gitignore
This commit is contained in:
1 parent
9b4a29c4d2
commit
04bf90c09b
35 files changed
+2220
-2
No files matched your search
@@ -0,0 +1,78 @@
|
||||
"""Calibrate/evaluate frozen candidates. No training or test threshold selection."""
|
||||
import argparse,json,hashlib
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
from irstd.core import candidate_pool,image_stats,aggregate,calibrate,components
|
||||
from irstd.data import read_manifest,read_pair,sha
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser();p.add_argument('--manifest',required=True);p.add_argument('--predictions',required=True)
|
||||
p.add_argument('--out',required=True);p.add_argument('--gate');p.add_argument('--split',choices=['dev','cal','test'],default='test')
|
||||
p.add_argument('--calibrate',action='store_true');p.add_argument('--certificate');p.add_argument('--threshold',type=float,default=.5)
|
||||
p.add_argument('--base-threshold',type=float,default=.1);p.add_argument('--cap',type=int,default=64);p.add_argument('--radius',type=float,default=3.)
|
||||
p.add_argument('--alpha',type=float,default=.1);p.add_argument('--delta',type=float,default=.05)
|
||||
p.add_argument('--risk',choices=['count','any'],default='count');p.add_argument('--pixel-baseline',action='store_true')
|
||||
p.add_argument('--calibration-method',choices=['bonferroni','fixed_sequence'],default='bonferroni')
|
||||
a=p.parse_args()
|
||||
if a.calibrate and (a.split!='cal' or a.pixel_baseline or a.certificate):p.error('Calibration requires cal split, fixed candidates and no certificate input')
|
||||
if a.pixel_baseline and (a.gate or a.certificate):p.error('Pixel baseline cannot use gate/candidate certificate')
|
||||
if not 0<=a.threshold<=1:p.error('threshold outside [0,1]')
|
||||
rows=read_manifest(a.manifest);root=Path(a.predictions)
|
||||
provenance_path=root/'provenance.json'
|
||||
if not provenance_path.exists():raise ValueError('Missing prediction provenance.json')
|
||||
provenance=json.loads(provenance_path.read_text())
|
||||
if provenance.get('manifest_sha256')!=sha(a.manifest):raise ValueError('Prediction manifest differs')
|
||||
pipeline=dict(manifest_sha256=sha(a.manifest),provenance=provenance,
|
||||
base_threshold=a.base_threshold,cap=a.cap,radius=a.radius,
|
||||
gate_sha256=sha(a.gate) if a.gate else None,
|
||||
prediction_hashes={r['id']:sha(root/(r['id']+'.npy')) for r in rows},
|
||||
input_hashes={r['id']:dict(image=sha(r['image']),mask=sha(r['mask'])) for r in rows},
|
||||
source_hashes={name:sha(Path(__file__).resolve().parents[1]/name) for name in ['irstd/core.py','irstd/models.py','scripts/evaluate.py']},
|
||||
evaluation_version=1)
|
||||
fingerprint=hashlib.sha256(json.dumps(pipeline,sort_keys=True).encode()).hexdigest()
|
||||
gate=None
|
||||
if a.gate:
|
||||
import torch
|
||||
from irstd.models import CandidateGate
|
||||
ck=torch.load(a.gate,map_location='cpu',weights_only=True);cfg=ck['config']
|
||||
for k in ('cap','base_threshold','radius'):
|
||||
if cfg[k]!=getattr(a,k):raise ValueError('Gate pipeline differs: '+k)
|
||||
if cfg['manifest_sha256']!=sha(a.manifest):raise ValueError('Gate manifest differs')
|
||||
gate=CandidateGate();gate.load_state_dict(ck['state_dict']);gate.eval()
|
||||
thresholds=np.linspace(0,1,101).tolist() if a.calibrate else [a.threshold]
|
||||
reject_all=False
|
||||
if a.certificate:
|
||||
cert=json.loads(Path(a.certificate).read_text())
|
||||
if cert['pipeline_fingerprint']!=fingerprint:raise ValueError('Certificate invalidated by changed pipeline/data')
|
||||
reject_all=cert['reject_all'];thresholds=[cert['threshold'] if not reject_all else 1.]
|
||||
selected=[r for r in rows if r['split']==a.split]
|
||||
if not selected:raise ValueError('Empty split')
|
||||
fp=[];stats=[];per_image=[]
|
||||
for r in selected:
|
||||
prob=np.load(root/(r['id']+'.npy'),allow_pickle=False);_,gt=read_pair(r)
|
||||
if prob.shape!=gt.shape:raise ValueError('Prediction shape mismatch')
|
||||
masks,features=candidate_pool(prob,a.base_threshold,a.cap)
|
||||
scores=features[:,0]
|
||||
if gate is not None and len(features):
|
||||
with torch.no_grad():scores=gate(torch.from_numpy(features)).sigmoid().numpy()
|
||||
row_stats=[]
|
||||
for t in thresholds:
|
||||
keep=[] if reject_all else [m for m,s in zip(masks,scores) if s>=t]
|
||||
if a.pixel_baseline:keep=components(prob>=t)
|
||||
row_stats.append(image_stats(keep,gt,a.radius))
|
||||
fp.append([s['fp'] for s in row_stats]);stats.append(row_stats[0])
|
||||
per_image.append(dict(id=r['id'],group=r['group'],**row_stats[0]))
|
||||
if a.calibrate:
|
||||
result=calibrate(fp,thresholds,a.cap,a.alpha,a.delta,a.risk,a.calibration_method)
|
||||
result.update(pipeline_fingerprint=fingerprint,pipeline=pipeline,
|
||||
calibration_ids=[r['id'] for r in selected],
|
||||
calibration_groups=len(set(r['group'] for r in selected)),
|
||||
assumption_notice='Conditional on iid images; group labels alone do not verify independence. Do not pool adjacent video frames.')
|
||||
else:
|
||||
result=dict(metrics=aggregate(stats),per_image=per_image,pipeline_fingerprint=fingerprint,
|
||||
split=a.split,threshold=None if reject_all else thresholds[0],reject_all=reject_all,
|
||||
certificate_used=a.certificate,mode='pixel' if a.pixel_baseline else 'fixed_candidate')
|
||||
dest=Path(a.out);dest.parent.mkdir(parents=True,exist_ok=True)
|
||||
if dest.exists():raise FileExistsError(dest)
|
||||
dest.write_text(json.dumps(result,indent=2,allow_nan=False));print(dest)
|
||||
if __name__=='__main__':main()
|
||||
Reference in new issue
Block a user