add emblem5

This commit is contained in:
Fam Zheng 2025-08-24 11:54:40 +01:00
parent b0585d16a0
commit 4f8e3973a8
43 changed files with 10665 additions and 0 deletions

1
emblem5/.cursorignore Normal file
View File

@ -0,0 +1 @@
/data

7
emblem5/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
/venv
/data
/models
/archive
/.ipynb_checkpoints
/reports
/dataset

48
emblem5/.gitlab-ci.yml Normal file
View File

@ -0,0 +1,48 @@
stages:
- build
- prepare
- train
baseimg:
stage: build
tags:
- emblem-dev2
script:
- make -C baseimg
when: manual
fetch_and_preprocess:
only:
- main
stage: prepare
tags:
- ailab
timeout: 5h
before_script:
- if ! test -d $HOME/venv; then python3 -m venv $HOME/venv; source $HOME/venv/bin/activate; pip install --upgrade pip; fi
- source $HOME/venv/bin/activate
- |
if ! cmp -s ./baseimg/requirements.txt $HOME/venv/requirements.txt; then
pip install -r ./baseimg/requirements.txt && cp ./baseimg/requirements.txt $HOME/venv/;
fi
- mkdir -p $HOME/emblem5-data
script:
- export DATA_DIR=$HOME/emblem5-data
- mkdir -p $DATA_DIR
- time make fetch
train: &train_base
only:
- main
stage: train
tags:
- ailab
before_script:
- source $HOME/venv/bin/activate
- export DATA_DIR=$HOME/emblem5-data
script:
- echo "to be implemented"
artifacts:
paths:
- models/*
expire_in: 7 days

73
emblem5/Makefile Normal file
View File

@ -0,0 +1,73 @@
.PHONY: FORCE
DATA_DIR ?= ../data
MODEL ?= models/sbs2g-20250614_1551-pos97.22-neg94.72.pt
INF ?= 99999999
METHOD ?= qrcmpnet
TRAIN_RATE ?= 0.8
SCAN_IDS ?= 80653
benchmark:
./ai/benchmark.py -m $(MODEL) -d data/samples.json -s $(SCAN_IDS)
upload:
ls -l $(MODEL)
./ai/upload.py $(MODEL)
upload-qrs:
./scripts/upload-qrs.py --dir data/qrs/tree
server_url=http://localhost:6500
# server_url=https://themblem.com
qr_verify:
echo 'pos'
for scan_id in 101378 124999; do \
curl $(server_url)/api/v5/qr_verify \
-F frame=@data/scans/$$scan_id/frame.jpg; \
done
echo 'neg'
for scan_id in 102095 105387; do \
curl $(server_url)/api/v5/qr_verify \
-F frame=@data/scans/$$scan_id/frame.jpg; \
done
fullqr:
./ai/fullqr.py
fetch:
./ai/fetch-scans.py --data-dir $(DATA_DIR)
./ai/make-sbs.py --data-dir $(DATA_DIR)
sbs:
./ai/make-sbs.py --data-dir $(DATA_DIR)
clarity: METHOD=clarity
clarity: train
VERIFY_RANGE ?= 160000-$(INF)
NCELLS ?= 1
verify:
./ai/verify.py -d $(DATA_DIR) -m $(MODEL) -r $(VERIFY_RANGE) --name $(notdir $(MODEL))-$(VERIFY_RANGE) --with-margins --ncells $(NCELLS)
verify-ue:
./ai/verify.py -d $(DATA_DIR) -m $(MODEL) -r 0-$(INF) --name $(notdir $(MODEL))-ue --labels ue
predict:
./ai/predict.py -m $(MODEL) data/gridcrop2/117803/sbs.jpg
aiweb:
cd ai/aiweb && npm run serve
server:
./ai/server.py --debug
hypertrain:
./ai/hypertrain.py --max-epochs 20 --min-epochs 10 --ntrials 20
train:
./ai/train.py --sample-rate 1 --num-epochs 100 --data-dir $(DATA_DIR) --val-codes ai/validate-codes.txt
quick:
./ai/train.py --sample-rate 0.1 --num-epochs 1 --data-dir $(DATA_DIR) --val-codes ai/validate-codes.txt

8
emblem5/ai/Dockerfile Normal file
View File

@ -0,0 +1,8 @@
FROM registry.cn-shenzhen.aliyuncs.com/emblem/baseimg:2025041707-4bc3e34
RUN mkdir -p /emblem
# TODO: use new base image that has these
RUN apt-get update && apt-get install -y libglib2.0-0t64 libgl1
RUN mkdir -p /emblem/ai
ADD . /emblem/ai/
RUN bash -c 'source /venv/bin/activate && pip install -i https://mirrors.ustc.edu.cn/pypi/simple loguru tqdm'
CMD bash -c 'source /venv/bin/activate && cd /emblem/ai && python3 server.py'

17
emblem5/ai/Makefile Normal file
View File

@ -0,0 +1,17 @@
IMG_TAG := $(shell date +%Y%m%d-%H%M%S)-$(shell git rev-parse --short HEAD)
IMG_NAME := registry.cn-shenzhen.aliyuncs.com/emblem/infer:$(IMG_TAG)
default: build push
build:
docker build -t $(IMG_NAME) .
push:
docker push $(IMG_NAME)
deploy: build push
kubectl --kubeconfig=../deploy/kubeconfig.themblem -n emblem set image deployment/infer infer=$(IMG_NAME)
kubectl --kubeconfig=../deploy/kubeconfig.themblem -n emblem rollout status --timeout=300s deployment/infer
train:
./train.py

41
emblem5/ai/browser.py Normal file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env python3
import os
import streamlit as st
import random
import json
from PIL import Image
def get_mispredicted_scans():
with open('data/verify2.log', 'r') as f:
lines = f.readlines()
for line in lines:
fields = line.split()
if len(fields) != 6:
continue
if fields[1] != fields[2]:
yield fields[0]
def main():
st.title('Browser')
# scan_ids = os.listdir('data/scans')
# to_show = sorted(scan_ids, key=lambda x: int(x), reverse=True)[:100]
to_show = list(get_mispredicted_scans())
st.write(f'to show: {len(to_show)}')
for sid in to_show:
show_scan(sid)
def show_scan(scan_id):
scan_dir = f'data/scans/{scan_id}'
mdfile = f'{scan_dir}/metadata.json'
md = json.load(open(mdfile))
if not os.path.exists(mdfile):
return
sbs = Image.open(f'{scan_dir}/sbs.jpg')
st.write(f'{scan_id}: {md["labels"]}')
st.image(sbs.resize((512, 256)))
st.divider()
if __name__ == '__main__':
main()

791
emblem5/ai/common.py Normal file
View File

@ -0,0 +1,791 @@
#!/usr/bin/env python3
import sys
import torch
import torch.nn as nn
import torch.optim as optim
from torch.amp import autocast, GradScaler
from torch.utils.data import DataLoader, Dataset, ConcatDataset
import torchvision
from PIL import Image, ImageFilter
import os
from datetime import datetime
from collections import defaultdict
import argparse
from kornia.losses.focal import FocalLoss
from kornia.augmentation import ColorJiggle
import cv2
import numpy as np
import random
import re
import shutil
import json
import tempfile
import time
import base64
import subprocess
from loguru import logger
from collections import defaultdict
from multiprocessing import Pool, cpu_count, set_start_method
from tqdm import tqdm
import importlib
concurrency = max(1, cpu_count() - 2)
def info(msg):
logger.info(msg)
def debug(msg):
logger.debug(msg)
cuda_available = torch.cuda.is_available()
device = torch.device('cuda' if cuda_available else 'cpu')
default_model = 'best_model_ep82_pos98.94_neg96.13_20250720_222102.pt'
clarity_model = 'models/clarity-ep15-pos88.14-neg92.23-20250518_164155.pt'
torch.set_float32_matmul_precision('high')
def batch_generator(labels, batch_size):
for i in range(0, len(labels), batch_size):
yield labels[i:i+batch_size]
def make_side_by_side_img(left, right):
min_width = min(left.width, right.width)
min_height = min(left.height, right.height)
left = left.resize((min_width, min_height))
right = right.resize((min_width, min_height))
ret = Image.new('RGB', (min_width * 2, min_height))
ret.paste(left, (0, 0))
ret.paste(right, (min_width, 0))
return ret
def warp_with_margin_ratio(orig, edge, corners, margin_ratio):
src_points = np.float32(corners)
dst_points = np.float32([
[edge * margin_ratio, edge * margin_ratio],
[edge * (1 - margin_ratio), edge * margin_ratio],
[edge * (1 - margin_ratio), edge * (1 - margin_ratio)],
[edge * margin_ratio, edge * (1 - margin_ratio)],
])
M = cv2.getPerspectiveTransform(src_points, dst_points)
warped = cv2.warpPerspective(np.array(orig), M, (edge, edge))
return Image.fromarray(warped)
def find_min_margin_ratio(img, corners):
min_margin = None
for i in range(4):
point = corners[i]
x = point[0]
y = point[1]
this_min = min(
x / img.width,
(img.width - x) / img.width,
y / img.height,
(img.height - y) / img.height,
)
if min_margin is None or this_min < min_margin:
min_margin = this_min
return min_margin
def make_side_by_side_img_with_margins(frame_img, std_img):
std_qr, std_corners = find_qr(std_img)
frame_qr, frame_corners = find_qr(frame_img)
if std_corners is None or frame_corners is None:
return None
edge_length = min(std_img.width, std_img.height)
margin_ratio = find_min_margin_ratio(std_img, std_corners)
std_warped = warp_with_margin_ratio(std_img, edge_length, std_corners, margin_ratio)
frame_warped = warp_with_margin_ratio(frame_img, edge_length, frame_corners, margin_ratio)
ret = Image.new('RGB', (edge_length, int(edge_length * margin_ratio)))
ret.paste(std_warped, (0, 0))
ret.paste(frame_warped, (0, int(edge_length * margin_ratio)))
return ret
def get_top_margin(img, margin_ratio):
ret = Image.new('RGB', (img.width, int(img.height * margin_ratio)))
ret.paste(img, (0, 0))
return ret
def make_top_margin_stacked_img(frame_img, std_img):
std_qr, std_corners = find_qr(std_img)
frame_qr, frame_corners = find_qr(frame_img)
if std_corners is None or frame_corners is None:
return None
edge_length = min(std_img.width, std_img.height)
margin_ratio = find_min_margin_ratio(std_img, std_corners)
std_warped = warp_with_margin_ratio(std_img, edge_length, std_corners, margin_ratio)
frame_warped = warp_with_margin_ratio(frame_img, edge_length, frame_corners, margin_ratio)
std_top_margin = get_top_margin(std_warped, margin_ratio)
frame_top_margin = get_top_margin(frame_warped, margin_ratio)
outheight = int(edge_length * margin_ratio * 2)
ret = Image.new('RGB', (edge_length, outheight))
ret.paste(std_top_margin, (0, 0))
ret.paste(frame_top_margin, (0, outheight // 2))
return ret
def make_stripe_img(left, right, nstripes):
min_width = min(left.width, right.width)
min_height = min(left.height, right.height)
ret = Image.new('RGB', (min_width * 2, min_height))
left_stripe_width = left.width // nstripes
right_stripe_width = right.width // nstripes
stripe_width = min_width // nstripes
for i in range(nstripes):
left_stripe = left.crop((i * left_stripe_width, 0, (i + 1) * left_stripe_width, left.height))
left_stripe = left_stripe.resize((stripe_width, min_height))
right_stripe = right.crop((i * right_stripe_width, 0, (i + 1) * right_stripe_width, right.height))
right_stripe = right_stripe.resize((stripe_width, min_height))
ret.paste(left_stripe, (i * stripe_width * 2, 0))
ret.paste(right_stripe, (i * stripe_width * 2 + stripe_width, 0))
return ret
def predict_multi(model, transforms, images, ncells=1):
results_per_img = ncells * ncells * 2
ret = []
with torch.no_grad():
tensors = []
for image in images:
for xcoord in range(ncells):
for ycoord in range(ncells):
img = crop_side_by_side(image, ncells, xcoord, ycoord)
img_tensor = transforms(img).to(device)
tensors.append(img_tensor)
output = model(torch.stack(tensors, dim=0))
sub_probs = torch.nn.functional.softmax(output, dim=1).cpu().numpy().tolist()
for i in range(len(images)):
probs = sub_probs[i * results_per_img:(i + 1) * results_per_img]
neg_sum = sum([x[0] for x in probs])
pos_sum = sum([x[1] for x in probs])
predicted_class = 1 if pos_sum > neg_sum else 0
ret.append((predicted_class, probs))
return ret
def predict(model, transforms, image, ncells=1):
r = predict_multi(model, transforms, [image], ncells)
return r[0]
qr_detector = cv2.wechat_qrcode_WeChatQRCode()
def find_qr(img, scale=1.0):
# Convert PIL Image to OpenCV format
orig_size = img.size
if scale < 1.0:
img_w, img_h = img.size
new_w = int(img_w * scale)
new_h = int(img_h * scale)
new_size = (new_w, new_h)
resized_img = img.resize(new_size)
else:
new_size = orig_size
resized_img = img
img_cv = cv2.cvtColor(np.array(resized_img), cv2.COLOR_RGB2BGR)
qr, corners = qr_detector.detectAndDecode(img_cv)
if not qr:
if scale > 0.05:
return find_qr(img, scale * 2 / 3)
else:
return None, None
corners = np.array(corners[0], dtype=np.float32)
corners /= scale
return qr[0], corners
def extract_qr(img):
qr, corners = find_qr(img)
if not qr:
raise Exception('No QR code found')
corners = np.array(corners, dtype=np.float32)
img_cv = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
# Define target rectangle corners (clockwise from top-left)
min_x = min(corners[:, 0])
max_x = max(corners[:, 0])
min_y = min(corners[:, 1])
max_y = max(corners[:, 1])
width = max_x - min_x
height = max_y - min_y
width = height = int(min(width, height))
dst_corners = np.array([
[0, 0],
[width, 0],
[width, height],
[0, height]
], dtype=np.float32)
matrix = cv2.getPerspectiveTransform(corners, dst_corners)
warped = cv2.warpPerspective(img_cv, matrix, (width, height))
r = Image.fromarray(cv2.cvtColor(warped, cv2.COLOR_BGR2RGB))
return qr, r
def load_model(model_path):
checkpoint = torch.load(model_path, map_location=device, weights_only=False)
model = checkpoint['model'] # Load the complete model structure
model.load_state_dict(checkpoint['model_state_dict']) # Load the weights
model.eval()
model = model.to(device)
transforms = checkpoint['transforms']
return model, transforms
def save_model(model, transforms, save_path, metadata=None):
checkpoint = {
'model': model, # Save the complete model structure
'model_state_dict': model.state_dict(),
'transforms': transforms
}
if metadata:
checkpoint['metadata'] = metadata
torch.save(checkpoint, save_path)
def make_model(model_name):
model_makers = {
'resnet': make_resnet,
'resnet18': make_resnet18,
'resnet101': make_resnet101,
'regnet': make_regnet,
'convnext': make_convnext,
'efficientnet': make_efficientnet,
'densenet': make_densenet,
'mobilenet': make_mobilenet,
}
return model_makers[model_name]()
def make_mobilenet():
weights = torchvision.models.MobileNet_V3_Small_Weights.IMAGENET1K_V1
model = torchvision.models.mobilenet_v3_small(weights=weights)
model.classifier = nn.Sequential(
nn.Dropout(p=0.3),
nn.Linear(576, 128),
nn.GELU(),
nn.Dropout(p=0.2),
nn.Linear(128, 2)
)
return model, make_generic_transforms()
def make_densenet():
weights = models.DenseNet121_Weights.IMAGENET1K_V1
model = models.densenet121(weights=weights)
model.classifier = nn.Sequential(
nn.Dropout(p=0.4),
nn.Linear(1024, 512),
nn.GELU(),
nn.Dropout(p=0.3),
nn.Linear(512, 128),
nn.GELU(),
nn.Dropout(p=0.2),
nn.Linear(128, 2)
)
return model, make_generic_transforms()
def make_efficientnet():
weights = torchvision.models.EfficientNet_B3_Weights.IMAGENET1K_V1
model = torchvision.models.efficientnet_b3(weights=weights)
model.classifier = nn.Sequential(
nn.Dropout(p=0.4),
nn.Linear(1536, 512),
nn.GELU(),
nn.Dropout(p=0.3),
nn.Linear(512, 128),
nn.GELU(),
nn.Dropout(p=0.2),
nn.Linear(128, 2)
)
return model, make_generic_transforms()
def make_convnext():
weights = torchvision.models.ConvNeXt_Base_Weights.IMAGENET1K_V1
model = torchvision.models.convnext_base(weights=weights)
model.classifier = nn.Sequential(
nn.Flatten(1), # 先 flatten (从 [B, 1024, 1, 1] 到 [B, 1024])
nn.LayerNorm(1024, eps=1e-6),
nn.Dropout(p=0.4),
nn.Linear(1024, 512),
nn.GELU(),
nn.Dropout(p=0.3),
nn.Linear(512, 128),
nn.GELU(),
nn.Dropout(p=0.2),
nn.Linear(128, 2)
)
return model, make_generic_transforms()
def make_resnet101():
weights = torchvision.models.ResNet101_Weights.IMAGENET1K_V1
model = torchvision.models.resnet101(weights=weights)
model.fc = nn.Sequential(
nn.Dropout(p=0.4),
nn.Linear(2048, 512),
nn.GELU(),
nn.Dropout(p=0.3),
nn.Linear(512, 128),
nn.GELU(),
nn.Dropout(p=0.2),
nn.Linear(128, 2)
)
return model, make_generic_transforms()
def make_resnet18():
weights = torchvision.models.ResNet18_Weights.IMAGENET1K_V1
model = torchvision.models.resnet18(weights=weights)
model.fc = nn.Sequential(
nn.Dropout(p=0.4),
nn.Linear(512, 128),
nn.GELU(),
nn.Dropout(p=0.3),
nn.Linear(128, 2)
)
return model, make_generic_transforms()
def make_resnet():
weights = torchvision.models.ResNet50_Weights.IMAGENET1K_V1
model = torchvision.models.resnet50(weights=weights)
model.fc = nn.Sequential(
nn.Dropout(p=0.4),
nn.Linear(2048, 512),
nn.GELU(),
nn.Dropout(p=0.3),
nn.Linear(512, 128),
nn.GELU(),
nn.Dropout(p=0.2),
nn.Linear(128, 2)
)
return model, make_generic_transforms()
def make_regnet():
weights = models.RegNet_Y_3_2GF_Weights.IMAGENET1K_V1
model = models.regnet_y_3_2gf(weights=weights)
model.fc = nn.Sequential(
nn.Dropout(p=0.4),
nn.Linear(1512, 512),
nn.GELU(),
nn.Dropout(p=0.3),
nn.Linear(512, 128),
nn.GELU(),
nn.Dropout(p=0.1),
nn.Linear(128, 2)
)
return model, make_generic_transforms()
def make_generic_transforms():
return torchvision.transforms.Compose([
torchvision.transforms.Resize((128, 256)),
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
class ScanDataset(Dataset):
def __init__(self, scans, transforms):
self.transforms = transforms
self.scans = scans
def __len__(self):
return len(self.scans)
def __getitem__(self, idx):
scan = self.scans[idx]
scan_id = scan['scan_id']
gridcrop2_dir = os.path.join('data', 'gridcrop2', scan_id)
sbs_file = os.path.join(gridcrop2_dir, 'sbs.jpg')
sbs_img = Image.open(sbs_file).convert('RGB')
return self.transforms(sbs_img), 1 if 'pos' in scan['labels'] else 0
def stats(self):
return {
'pos': sum(1 for scan in self.scans if 'pos' in scan['labels']),
'neg': sum(1 for scan in self.scans if 'neg' in scan['labels']),
}
def do_train(cfg):
train_dataset = cfg['train_dataset']
val_dataset = cfg['val_dataset']
batch_size = cfg['batch_size']
num_workers = cfg['num_workers']
prefetch_factor = 4
train_loader = DataLoader(
train_dataset,
batch_size=batch_size,
shuffle=True,
num_workers=num_workers,
persistent_workers=True,
prefetch_factor=prefetch_factor,
pin_memory=True,
)
val_loader = DataLoader(
val_dataset,
batch_size=batch_size*2,
shuffle=True,
num_workers=num_workers,
persistent_workers=True,
prefetch_factor=prefetch_factor,
pin_memory=True,
)
model = cfg['model']
# Set memory fraction for better GPU utilization
torch.cuda.set_per_process_memory_fraction(0.8)
# Compile model with optimized settings
model = torch.compile(model).to(device)
criterion = cfg['criterion']
optimizer = cfg['optimizer'](model)
scheduler = cfg['scheduler'](optimizer)
max_epochs = cfg['max_epochs']
# Initialize variables for early stopping
best_epoch = None
# Create GradScaler once at the start of training
scaler = torch.amp.GradScaler('cuda')
for epoch in range(max_epochs):
info(f"Starting epoch {epoch+1}/{max_epochs}")
model.train()
running_loss = 0.0
for images, labels in tqdm(train_loader, desc=f"Train {epoch+1}/{max_epochs}", unit_scale=batch_size):
images, labels = images.to(device, non_blocking=True), labels.to(device, non_blocking=True)
optimizer.zero_grad()
with torch.amp.autocast(device_type='cuda', dtype=torch.float16):
outputs = model(images)
loss = criterion(outputs, labels).mean()
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
running_loss += loss.detach().item() * len(images)
running_loss = running_loss / len(train_loader) / cfg['learning_rate']
info(f'Epoch [{epoch+1}/{max_epochs}], Loss: {running_loss:.5f}')
# 验证阶段
model.eval()
pos_correct = 0
pos_total = 0
neg_correct = 0
neg_total = 0
with torch.no_grad():
for images, labels in tqdm(val_loader, desc=f"Validating {epoch+1}/{max_epochs}", unit_scale=batch_size):
images, labels = images.to(device), labels.to(device)
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
pos_correct += ((predicted == labels) & (labels == 1)).sum().item()
pos_total += (labels == 1).sum().item()
neg_correct += ((predicted == labels) & (labels == 0)).sum().item()
neg_total += (labels == 0).sum().item()
pos_accu = pos_correct / pos_total if pos_total else 0
neg_accu = neg_correct / neg_total if neg_total else 0
total_accu = (pos_correct + neg_correct) / (pos_total + neg_total)
info(f'Pos Accu: {pos_accu:.2%} ({pos_correct}/{pos_total})')
info(f'Neg Accu: {neg_accu:.2%} ({neg_correct}/{neg_total})')
info(f'Total Accu: {total_accu:.2%} ({pos_correct + neg_correct}/{pos_total + neg_total})')
if not best_epoch or total_accu > best_epoch['total_accu']:
best_epoch = {
'epoch': epoch,
'pos_accu': pos_accu,
'neg_accu': neg_accu,
'total_accu': total_accu,
'model_state_dict': model.state_dict().copy(),
}
info(f'New best model found with total accuracy: {best_epoch["total_accu"]:.2%}')
scheduler.step(total_accu)
# Load the best model weights
if best_epoch is not None:
model.load_state_dict(best_epoch['model_state_dict'])
info(f'Loaded best model with total accuracy: {best_epoch["total_accu"]:.2%}')
return model, best_epoch
def verify_frame(model, transforms, frame_img, orig_img):
side_by_side_img = make_side_by_side_img_with_margins(frame_img, orig_img)
if side_by_side_img is None:
raise Exception("Failed to create side-by-side image with margins")
side_by_side_img = side_by_side_img.convert('RGB')
with tempfile.NamedTemporaryFile(suffix='.jpg') as f:
side_by_side_img.save(f.name)
return predict(model, transforms, Image.open(f.name).convert('RGB'))
def parse_ranges(s):
ret = []
for tr in s.split(','):
if '-' in tr:
begin, end = tr.split('-')
ret.append([int(begin), int(end)])
else:
ret.append([int(tr), int(tr)])
return ret
def in_range(x, val_range):
if not val_range:
return False
start, end = val_range
return start <= int(x) <= end
def train_model(model, train_dataset, val_dataset, max_epochs, pos_weight=0.99):
info(f"Train count: {len(train_dataset)}, val count: {len(val_dataset)}")
learning_rate = 0.001
cfg = {
'model': model,
'train_dataset': train_dataset,
'val_dataset': val_dataset,
'criterion': FocalLoss(0.25, weight=torch.Tensor([1.0 - pos_weight, pos_weight]).to(device)),
'learning_rate': learning_rate,
'optimizer': lambda model: optim.AdamW(
model.parameters(),
lr=learning_rate,
weight_decay=0.001,
betas=(0.9, 0.999),
eps=1e-8
),
'batch_size': 32,
'max_epochs': max_epochs,
'scheduler': lambda optimizer: torch.optim.lr_scheduler.ReduceLROnPlateau(
optimizer,
mode='max',
factor=0.1,
patience=3,
min_lr=1e-6
),
'num_workers': concurrency,
}
return do_train(cfg)
def motion_blur_indicator(image):
def f(img):
equalized = cv2.equalizeHist(np.array(img))
sobelx = cv2.Sobel(equalized, cv2.CV_64F, 1, 0, ksize=3)
sobely = cv2.Sobel(equalized, cv2.CV_64F, 0, 1, ksize=3)
var_x = sobelx.var()
var_y = sobely.var()
ratio = min(var_x, var_y) / max(var_x, var_y)
return ratio
return f(image) * f(image.rotate(45))
def calc_clarity(img):
gray = img.convert('L')
blurred = gray.filter(ImageFilter.GaussianBlur(radius=1.5))
equalized = cv2.equalizeHist(np.array(blurred))
lap = cv2.Laplacian(equalized, cv2.CV_64F)
return lap.var() * motion_blur_indicator(gray)
def load_codes(codes_file):
with open(codes_file, 'r') as f:
return [line.strip() for line in f.readlines() if line.strip()]
def load_cell_labels(dataset_dir):
if not os.path.exists(dataset_dir):
raise Exception(f"Dataset directory {dataset_dir} does not exist")
neg_labels = []
pos_labels = []
all_files = []
for label in ["pos", "neg"]:
for r, ds, fs in os.walk(os.path.join(dataset_dir, label)):
for f in fs:
if f.startswith('cell_') and f.endswith('.jpg'):
fp = os.path.join(r, f)
all_files.append((label, fp))
random.shuffle(all_files)
for label, fp in all_files:
jpg_file = fp
json_file = os.path.join(os.path.dirname(fp), "metadata.json")
if not os.path.exists(json_file):
continue
with open(json_file, 'r') as f:
md = json.load(f)
if 'code' not in md:
continue
code = md['code']
if label == "pos":
pos_labels.append((jpg_file, 1))
else:
neg_labels.append((jpg_file, 0))
total_pos = len(pos_labels)
total_neg = len(neg_labels)
info(f"Total positive: {total_pos}, total negative: {total_neg}")
if not total_pos:
raise Exception("No positive labels found")
if not total_neg:
raise Exception("No negative labels found")
return pos_labels, neg_labels
def crop_side_by_side(img, cells, xcoord, ycoord, jitter=False):
width = img.width // (cells * 2)
height = img.height // cells
left = img.crop((xcoord * width, ycoord * height, (xcoord + 1) * width, (ycoord + 1) * height))
right = img.crop(((xcoord + cells) * width, ycoord * height, (xcoord + cells + 1) * width, (ycoord + 1) * height))
if jitter:
movement = 0.02
left = torchvision.transforms.RandomAffine(degrees=0, translate=(movement, movement))(left)
right = torchvision.transforms.RandomAffine(degrees=0, translate=(movement, movement))(right)
ret = Image.new('RGB', (width * 2, height))
ret.paste(left, (0, 0))
ret.paste(right, (width, 0))
if jitter:
ret = torchvision.transforms.ColorJitter(brightness=0.2, saturation=0.2, hue=0.5)(ret)
return ret
def average(values):
return sum(values) / len(values)
def random_sample(values, count):
if len(values) <= count:
return values
return random.sample(values, count)
class ClarityPredictor(object):
def __init__(self, model_path=clarity_model):
self.model_path = clarity_model
self.model = None
def __call__(self, img):
if not self.model:
if not os.path.exists(self.model_path):
return None
self.model, self.transforms = load_model(self.model_path)
tensor = self.transforms(img).to(device).unsqueeze(0)
with torch.no_grad():
output = self.model(tensor)
return output.argmax(dim=1).detach().cpu().item() == 1
clarity_predictor = ClarityPredictor()
class BaseMethod(object):
def __init__(self, datadir):
self.datadir = datadir
def preprocess(self, scans):
pass
def load_scans(self, datadir, sample_rate=1.0):
self.datadir = datadir
pool = Pool(concurrency)
counts = defaultdict(int)
pos_scans = []
neg_scans = []
all_scan_ids = os.listdir(os.path.join(datadir, "scans"))
if sample_rate < 1.0:
all_scan_ids = random.sample(all_scan_ids, int(len(all_scan_ids) * sample_rate))
for x in tqdm(pool.imap(self.load_one_scan, all_scan_ids), total=len(all_scan_ids), desc="Loading dataset"):
counts[(x.get('ok'), x.get('error'), x.get('lables'))] += 1
if x.get('ok'):
labels = x['data'].get('labels', [])
if 'pos' in labels:
pos_scans.append(x['data'])
elif 'neg' in labels:
neg_scans.append(x['data'])
info(f"Counts: {counts}")
return sorted(pos_scans, key=lambda x: int(x['scan_id'])), sorted(neg_scans, key=lambda x: int(x['scan_id']))
def pre_load_one_scan(self, scan_id):
return True
def load_one_scan(self, scan_id):
if not self.pre_load_one_scan(scan_id):
return {
"ok": False,
"error": f"pre_load_one_scan: skip",
}
scan_dir = os.path.join(self.datadir, 'scans', scan_id)
if not os.path.exists(scan_dir):
return {
"ok": False,
"error": f"Scan dir does not exist",
}
std_qr_file = os.path.join(scan_dir, "std-qr.jpg")
if not os.path.exists(std_qr_file):
return {
"ok": False,
"error": f"Std QR file does not exist",
}
mdfile = os.path.join(scan_dir, "metadata.json")
if not os.path.exists(mdfile):
return {
"ok": False,
"error": f"Metadata file does not exist",
}
with open(mdfile, 'r') as f:
try:
md = json.load(f)
except Exception as e:
return {
"ok": False,
"error": f"Error loading metadata file: {e}",
}
if not md.get('code'):
return {
"ok": False,
"error": f"Code not found in metadata file",
}
if not md.get('labels'):
return {
"ok": False,
"error": f"Labels not found in metadata file",
}
if not md.get('relative_clarity'):
std_qr_img = Image.open(std_qr_file)
std_qr_clarity = calc_clarity(std_qr_img)
if not std_qr_clarity:
return {
"ok": False,
"error": f"Std QR clarity invalid",
}
frame_qr_file = os.path.join(scan_dir, "frame-qr.jpg")
if not os.path.exists(frame_qr_file):
return {
"ok": False,
"error": f"Frame QR file does not exist",
}
frame_qr_img = Image.open(frame_qr_file)
frame_qr_clarity = calc_clarity(frame_qr_img)
relative_clarity = frame_qr_clarity / std_qr_clarity
md['relative_clarity'] = relative_clarity
with open(mdfile, 'w') as f:
json.dump(md, f, indent=2)
if md['relative_clarity'] < 0.5:
return {
"ok": False,
"error": f"Relative clarity too low",
}
self.post_load_one_scan(scan_id)
return {
"ok": True,
"data": {
"scan_id": scan_id,
"std_qr_file": std_qr_file,
"code": md['code'],
"labels": md['labels'],
},
}
def train(self, dataset, epochs):
raise Exception("Not implemented")
def load_method(method_name, datadir):
mod = importlib.import_module('methods.' + method_name)
return mod.Method(datadir)
def balance_pos_and_neg(scans):
pos_scans = [s for s in scans if 'pos' in s['labels']]
neg_scans = [s for s in scans if 'neg' in s['labels']]
random.shuffle(pos_scans)
random.shuffle(neg_scans)
min_count = min(len(pos_scans), len(neg_scans))
pos = pos_scans[:min_count]
neg = neg_scans[:min_count]
info(f'balanced from pos: {len(pos_scans)} to {len(pos)}')
info(f'balanced from neg: {len(neg_scans)} to {len(neg)}')
ret = pos + neg
random.shuffle(ret)
return ret

15
emblem5/ai/download.py Executable file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env python3
import oss2
import sys
import os
oss_ak = 'LTAI5tC2qXGxwHZUZP7DoD1A'
oss_sk = 'qPo9O6ZvEfqo4t8oflGEm0DoxLHJhm'
auth = oss2.Auth(oss_ak, oss_sk)
endpoint = 'https://oss-rg-china-mainland.aliyuncs.com'
bucket = oss2.Bucket(auth, endpoint, 'emblem-models')
for x in sys.argv[1:]:
bucket.get_object_to_file(x, 'models/' + x)

149
emblem5/ai/fetch-scans.py Executable file
View File

@ -0,0 +1,149 @@
#!/usr/bin/env python3
import argparse
import os
import requests
import json
import multiprocessing as mp
from loguru import logger
import shutil
from PIL import Image
from ossclient import *
from common import *
import io
from tqdm import tqdm
import datetime
data_dir = 'data'
class ScanDataFetcher(object):
def __init__(self):
self.token = '3ebd8c33-f46e-4b06-bda8-4c0f5f5eb530'
def make_headers(self):
return {
'Authorization': f'Token {self.token}'
}
def load_local_scan_data(self):
ret = {}
scans_dir = os.path.join(data_dir, 'scans')
os.makedirs(scans_dir, exist_ok=True)
for scan_id in os.listdir(scans_dir):
scan_dir = os.path.join(scans_dir, scan_id)
if not os.path.isdir(scan_dir):
continue
fetch_state_path = os.path.join(scan_dir, 'fetch-state.json')
if not os.path.exists(fetch_state_path):
continue
metadata_path = os.path.join(scan_dir, 'metadata.json')
if not os.path.exists(metadata_path):
continue
md = json.load(open(metadata_path))
ret[md['id']] = md
return ret
def fetch(self, sample_rate=None):
local_scan_data = self.load_local_scan_data()
logger.info(f'local_scan_data: {len(local_scan_data)}')
url = 'https://themblem.com/api/v1/scan-data-labels/'
r = requests.get(url, headers=self.make_headers())
data = r.json()
fetch_backlog = []
for item in data['items']:
if 'code' not in item or 'id' not in item or not item.get('labels') or 'image' not in item:
continue
if item['id'] in local_scan_data:
local_labels = local_scan_data[item['id']]['labels']
if local_labels == item['labels']:
continue
fetch_backlog.append(item)
if sample_rate:
fetch_backlog = random.sample(fetch_backlog, int(len(fetch_backlog) * sample_rate))
logger.info(f'fetch_backlog: {len(fetch_backlog)}')
pool = mp.Pool(mp.cpu_count() * 4)
counts = defaultdict(int)
for r in tqdm(pool.imap_unordered(self.fetch_one_scan, fetch_backlog), total=len(fetch_backlog)):
counts[r] += 1
logger.info(f'counts: {counts}')
pool.close()
pool.join()
def fetch_one_scan(self, scan):
try:
self.do_fetch_one_scan(scan)
return 'ok'
except Exception as e:
scan_dir = os.path.join(data_dir, 'scans', str(scan['id']))
fetch_state_path = os.path.join(scan_dir, 'fetch-state.json')
with open(fetch_state_path, 'w') as f:
json.dump({
'status': 'error',
'timestamp': datetime.datetime.now().isoformat(),
'scan_id': scan['id'],
'labels': scan.get('labels', ''),
'error': str(e)
}, f, indent=2)
return 'error'
def do_fetch_one_scan(self, scan):
scan_dir = os.path.join(data_dir, 'scans', str(scan['id']))
os.makedirs(scan_dir, exist_ok=True)
# Check if fetch-state.json exists, if so skip this scan
fetch_state_path = os.path.join(scan_dir, 'fetch-state.json')
if os.path.exists(fetch_state_path):
return
metadata_path = os.path.join(scan_dir, 'metadata.json')
metadata_str = json.dumps(scan, indent=2)
frame_img_url = f'https://themblem.com/api/v1/oss-image/?token={self.token}&name={scan["image"]}'
frame_img_file = os.path.join(scan_dir, 'frame.jpg')
if not os.path.exists(frame_img_file):
frame_img_bytes = requests.get(frame_img_url).content
with open(frame_img_file, 'wb') as f:
f.write(frame_img_bytes)
std_img_file = os.path.join(scan_dir, 'std.jpg')
if not os.path.exists(std_img_file):
std_img = Image.open(io.BytesIO(get_qr_image_bytes(scan['code'])))
std_img.save(std_img_file)
with open(metadata_path, 'w') as f:
f.write(metadata_str)
frame_qr_img_file = os.path.join(scan_dir, 'frame-qr.jpg')
if not os.path.exists(frame_qr_img_file):
frame_img = Image.open(frame_img_file)
_, frame_qr = extract_qr(frame_img)
frame_qr.save(frame_qr_img_file)
std_qr_img_file = os.path.join(scan_dir, 'std-qr.jpg')
if not os.path.exists(std_qr_img_file):
std_img = Image.open(std_img_file)
_, std_qr = extract_qr(std_img)
std_qr.save(std_qr_img_file)
# Create fetch-state.json to mark successful completion
fetch_state = {
'status': 'completed',
'timestamp': datetime.datetime.now().isoformat(),
'scan_id': scan['id'],
'labels': scan.get('labels', ''),
}
with open(fetch_state_path, 'w') as f:
json.dump(fetch_state, f, indent=2)
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument('--data-dir', type=str, default='data')
parser.add_argument('--sample-rate', '-r', type=float)
return parser.parse_args()
def main():
args = parse_args()
global data_dir
data_dir = args.data_dir
fetcher = ScanDataFetcher()
logger.info('fetch')
fetcher.fetch(args.sample_rate)
if __name__ == "__main__":
main()

65
emblem5/ai/make-sbs.py Executable file
View File

@ -0,0 +1,65 @@
#!/usr/bin/env python3
import os
import sys
import json
import random
import argparse
from common import *
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument('--data-dir', required=True)
return parser.parse_args()
def process_scan(scan_dir):
if not os.path.isdir(scan_dir):
return "scan_dir not found"
frame_file = os.path.join(scan_dir, 'frame.jpg')
std_file = os.path.join(scan_dir, 'std.jpg')
if not os.path.exists(frame_file) or not os.path.exists(std_file):
return "frame.jpg or std.jpg not found"
sbs_file = os.path.join(scan_dir, 'sbs.jpg')
frame_qr_file = os.path.join(scan_dir, 'frame-qr.jpg')
std_qr_file = os.path.join(scan_dir, 'std-qr.jpg')
sbs_no_margin_file = os.path.join(scan_dir, 'sbs-nomargin.jpg')
try:
if not os.path.exists(sbs_file):
frame_img = Image.open(frame_file)
std_img = Image.open(std_file)
sbs_img = make_side_by_side_img_with_margins(frame_img, std_img)
if sbs_img:
sbs_img.save(sbs_file)
else:
return "make_side_by_side_img_with_margins failed"
if not os.path.exists(sbs_no_margin_file):
frame_img = Image.open(frame_file)
std_img = Image.open(std_file)
if not os.path.exists(frame_qr_file) or not os.path.exists(std_qr_file):
frame_qrcode, frame_qr_img = extract_qr(frame_img)
std_qrcode, std_qr_img = extract_qr(std_img)
frame_qr_img.save(frame_qr_file)
std_qr_img.save(std_qr_file)
else:
frame_qr_img = Image.open(frame_qr_file)
std_qr_img = Image.open(std_qr_file)
sbs_no_margin_img = make_side_by_side_img(frame_qr_img, std_qr_img)
sbs_no_margin_img.save(sbs_no_margin_file)
return "ok"
except Exception as e:
return f"error: {e}"
def main():
args = parse_args()
data_dir = args.data_dir
scans_dir = os.path.join(data_dir, 'scans')
pool = Pool(cpu_count())
scan_ids = os.listdir(scans_dir)
counts = defaultdict(int)
for result in tqdm(pool.imap(process_scan, [os.path.join(scans_dir, scan_id) for scan_id in scan_ids]), total=len(scan_ids)):
counts[result] += 1
for k, v in counts.items():
print(f"{k}: {v}")
if __name__ == '__main__':
main()

26
emblem5/ai/ossclient.py Normal file
View File

@ -0,0 +1,26 @@
import oss2
import re
oss_ak = 'LTAI5tC2qXGxwHZUZP7DoD1A'
oss_sk = 'qPo9O6ZvEfqo4t8oflGEm0DoxLHJhm'
def oss_get_object(endpoint, bucket_name, key):
auth = oss2.Auth(oss_ak, oss_sk)
bucket = oss2.Bucket(auth, endpoint, bucket_name)
return bucket.get_object(key)
def oss_put_object(endpoint, bucket_name, key, data):
auth = oss2.Auth(oss_ak, oss_sk)
bucket = oss2.Bucket(auth, endpoint, bucket_name)
bucket.put_object(key, data)
def get_qr_image_bytes(qr_code):
code_re = re.compile(r'[0-9]{6,}')
m = code_re.search(qr_code)
if not m:
return None
code = m.group(0)
prefix = code[:2]
key = f'v5/{prefix}/{code}.jpg'
obj = oss_get_object('https://oss-cn-guangzhou.aliyuncs.com', 'emblem-qrs', key)
return obj.read()

328
emblem5/ai/server.py Executable file
View File

@ -0,0 +1,328 @@
#! /usr/bin/env python3
import flask
import oss2
import os
from PIL import Image
import re
from common import *
import io
import time
from ossclient import *
import argparse
import hashlib
'''
Emblem infer service.
This provides a simple http api for torchvision model inference.
The model is downloaded from a predefined aliyun oss bucket.
'''
app = flask.Flask(__name__)
local_model_dir = 'models'
data_dir = 'data'
scans_dir = os.path.join(data_dir, 'scans')
os.makedirs(local_model_dir, exist_ok=True)
def get_file_md5(fname):
return hashlib.md5(open(fname, 'rb').read()).hexdigest()
def download_model(model_name):
model_path = os.path.join(local_model_dir, model_name)
if not os.path.exists(model_path):
obj = oss_get_object('https://oss-rg-china-mainland.aliyuncs.com', 'emblem-models', model_name)
with open(model_path, 'wb') as f:
f.write(obj.read())
return model_path, get_file_md5(model_path)
report_dir = '/tmp/emblem-reports/'
@app.route('/api/v5/report', methods=['POST'])
def report():
os.makedirs(report_dir, exist_ok=True)
for file in flask.request.files.values():
with open(os.path.join(report_dir, file.filename), 'wb') as f:
f.write(file.read())
return {
"ok": True,
}
@app.route('/api/v5/report/<name>', methods=['GET'])
def report_file(name):
return flask.send_file(os.path.join(report_dir, name))
@app.route('/api/v5/qr_verify', methods=['POST'])
def qr_verify():
try:
return do_qr_verify()
except Exception as e:
return {
"ok": False,
"error": str(e),
}
def find_best_frame(files):
best_frame = None
clarities = {}
best_clarity = 0
for file in files.values():
img = Image.open(file)
img_qrcode, img_qr = extract_qr(img)
if not img_qrcode:
continue
clarity = calc_clarity(img_qr)
clarities[file.filename] = clarity
if not best_frame or clarity > best_clarity:
best_frame = img
best_clarity = clarity
return best_frame, clarities
def do_qr_verify():
start_time = time.time()
fd = flask.request.form
model_path, model_md5 = download_model(fd.get('model', default_model))
model, transforms = load_model(model_path)
frame_image, clarities = find_best_frame(flask.request.files)
frame_qrcode, _ = extract_qr(frame_image)
std_image = Image.open(io.BytesIO(get_qr_image_bytes(frame_qrcode)))
if not std_image:
return {
"ok": False,
"error": f"No std image: {frame_qrcode}",
}
std_qrcode, _ = extract_qr(std_image)
if frame_qrcode != std_qrcode:
return {
"ok": False,
"error": "QR code mismatch",
}
predicted_class, probabilities = verify_frame(model, transforms, frame_image, std_image)
return {
"ok": True,
"result": {
"process_time": time.time() - start_time,
"predicted_class": predicted_class,
"probabilities": ', '.join([f'{v:.2%}' for k, v in probabilities]),
"clarities": clarities,
"model_md5": model_md5,
}
}
def make_qrsbs(frame_qrcode, frame_image):
qr_img = get_qr_image(frame_qrcode)
if not qr_img:
return None
ret = make_side_by_side_img(frame_image, qr_img)
with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as f:
ret.save(f.name)
with open(f.name, 'rb') as f:
return f.read()
@app.route('/api/v5/qrsbs', methods=['POST'])
def qrsb():
frame = flask.request.files['frame']
if not frame:
return {
"ok": False,
"error": "frame is required",
}
frame_image = Image.open(frame)
frame_qrcode, _ = extract_qr(frame_image)
cache_key = f'qrsbs_v1/{frame_qrcode}.jpg'
try:
qrsbs = oss_get_object('https://oss-cn-guangzhou.aliyuncs.com', 'emblem-cache', cache_key)
except Exception as e:
print(f'cache miss: {cache_key}')
qrsbs = make_qrsbs(frame_qrcode, frame_image)
if not qrsbs:
return flask.abort(404, 'QR code not found')
oss_put_object('https://oss-cn-guangzhou.aliyuncs.com', 'emblem-cache', cache_key, qrsbs)
return flask.send_file(io.BytesIO(qrsbs), mimetype='image/jpeg')
@app.route('/api/v5/infer/version', methods=['GET'])
def version():
return {'version': '1.0.0'}
@app.route('/api/v5/frame/<session_id>/<frame_id>', methods=['POST'])
def frame(session_id, frame_id):
data = flask.request.get_data()
auth = oss2.Auth(oss_ak, oss_sk)
endpoint = 'https://oss-cn-shenzhen.aliyuncs.com'
bucket = oss2.Bucket(auth, endpoint, 'emblem-frames-prod')
bucket.put_object(f'v5/{session_id}/{frame_id}', data)
return {
"ok": True,
}
def make_data_url(fname):
bs = open(fname, 'rb').read()
encoded = base64.b64encode(bs).decode()
return f'data:image/jpeg;base64,{encoded}'
def bottomcorner(img):
width = img.width
height = img.height
return img.crop((width - width // 4, height - height // 4, width, height))
def prepare_clarities(scan_dir):
frame_img = Image.open(os.path.join(scan_dir, 'frame-qr.jpg'))
std_img = Image.open(os.path.join(scan_dir, 'std-qr.jpg'))
frame_clarity = int(calc_clarity(bottomcorner(frame_img)))
std_clarity = int(calc_clarity(bottomcorner(std_img)))
return {
'relative': int(frame_clarity / std_clarity * 100),
'frame': frame_clarity,
'std': std_clarity,
}
def prepare_scan(scan, predicted_classes={}):
scan_dir = os.path.join(scans_dir, scan)
if not os.path.exists(scan_dir):
return None
files = os.listdir(scan_dir)
mdfile = os.path.join(scan_dir, 'metadata.json')
if os.path.exists(mdfile):
with open(mdfile, 'r') as f:
md = json.load(f)
else:
md = {}
frame_qr = Image.open(os.path.join(scan_dir, 'frame-qr.jpg'))
std_qr = Image.open(os.path.join(scan_dir, 'std-qr.jpg'))
return {
'name': scan,
'files': files,
'labels': md.get('labels', '').split(','),
'predicted_class': predicted_classes.get(scan),
'clarities': prepare_clarities(scan_dir),
'frame_qr_size': [frame_qr.width, frame_qr.height],
'std_qr_size': [std_qr.width, std_qr.height],
}
def match_one_term(md, term, predicted_class):
if term in ['mispredicted', 'incorrect']:
if predicted_class is None:
return False
pcname = 'pos' if predicted_class else 'neg'
labels = md.get('labels', '')
return labels and pcname not in labels
if term == 'correct':
if predicted_class is None:
return False
pcname = 'pos' if predicted_class else 'neg'
labels = md.get('labels', '')
return labels and pcname in labels
if term == 'pos':
return 'pos' in md.get('labels', '')
if term == 'neg':
return 'neg' in md.get('labels', '')
if term == 'succeeded':
return md.get('succeeded') == True
if term == 'failed':
return md.get('succeeded') == False
if re.match(r'^[0-9]+-[0-9]+$', term):
fs = term.split('-')
return int(fs[0]) <= int(md.get('id')) <= int(fs[1])
def match_query(md, q, predicted_class):
ret = True
for term in q.split() if q else []:
ret = ret and match_one_term(md, term, predicted_class)
return ret
def search_scans(q, predicted_classes):
ret = []
all_scans = os.listdir(scans_dir)
random.shuffle(all_scans)
for scan in all_scans:
scan_dir = os.path.join(scans_dir, scan)
state_file = os.path.join(scan_dir, 'fetch-state.json')
std_qr_file = os.path.join(scan_dir, 'std-qr.jpg')
if not os.path.exists(state_file) or not os.path.exists(std_qr_file):
continue
with open(os.path.join(scan_dir, 'metadata.json'), 'r') as f:
md = json.load(f)
if not match_query(md, q, predicted_classes.get(scan)):
continue
ret.append(scan)
return ret
def highlight_nongray(image):
saturation_threshold = 30
hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
s_channel = hsv_image[:, :, 1]
return Image.fromarray(s_channel)
@app.route('/api/sbs/<scan>.jpg', methods=['GET'])
def sbs(scan):
scan_dir = os.path.join(scans_dir, scan)
if not os.path.exists(scan_dir):
return {
"ok": False,
"error": f"Scan {scan} not found",
}
frame_img = Image.open(os.path.join(scan_dir, 'frame.jpg'))
std_img = Image.open(os.path.join(scan_dir, 'std.jpg'))
ret = make_side_by_side_img_with_margins(frame_img, std_img)
with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as f:
ret.save(f.name)
return flask.send_file(f.name, mimetype='image/jpeg')
@app.route('/api/allinone/<scan>.jpg', methods=['GET'])
def allinone(scan):
scan_dir = os.path.join(scans_dir, scan)
if not os.path.exists(scan_dir):
return {
"ok": False,
"error": f"Scan {scan} not found",
}
frame_img = Image.open(os.path.join(scan_dir, 'frame.jpg'))
std_img = Image.open(os.path.join(scan_dir, 'std.jpg'))
sbs_img = make_side_by_side_img_with_margins(frame_img, std_img)
ret_width = sbs_img.width
ret_height = sbs_img.height + frame_img.height
ret = Image.new('RGB', (ret_width, ret_height))
ret.paste(sbs_img, (0, 0))
ret.paste(frame_img, (0, sbs_img.height))
with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as f:
ret.save(f.name)
return flask.send_file(f.name, mimetype='image/jpeg')
@app.route('/api/scans', methods=['GET'])
def scans():
q = flask.request.args.get('filter', None)
verify_result = flask.request.args.get('verify_result', None)
if verify_result:
with open(f'data/{verify_result}', 'r') as f:
predicted_classes = json.load(f)
else:
predicted_classes = {}
scans = search_scans(q, predicted_classes)
scans = scans[:200]
return {
"scans": [prepare_scan(s, predicted_classes) for s in scans],
}
@app.route('/api/verify_results', methods=['GET'])
def verify_results():
return {
'results': [x for x in os.listdir('data') if x.endswith('.json')],
}
@app.route('/api/data/<path:path>', methods=['GET'])
def data(path):
return flask.send_file(os.path.abspath(os.path.join(data_dir, path)))
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument('--port', type=int, default=6500)
parser.add_argument('--debug', action='store_true')
return parser.parse_args()
if __name__ == '__main__':
args = parse_args()
app.run(host='0.0.0.0', port=args.port, debug=args.debug)

545
emblem5/ai/train2.py Normal file
View File

@ -0,0 +1,545 @@
#!/usr/bin/env python3
'''
all children of data/scans are scan_ids
in each scan_id, there is a file called "metadata.json"
labels key has 'pos' or 'neg'
it also has 'code' key which is a string
For the largest 20% scan_ids, we use as validation set
for all codes appearing in the validation set in all scans, we also use as validation set
the rest are training set
preprocess the sbs.jpg for both train and validation:
1. split left and right half
2. crop 3x3 of left
3. crop 3x3 of right
4. for each pair of crop, concat into grid-i-j.jpg, and apply some colorjitter
5. all the grid-i-j.jpg are used with the label
load a resnet18 model, and train it on the training set
train for 10 epochs, and print accuracy on validation set each epoch
save the model in the end.
'''
import os
import json
import random
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import Dataset, DataLoader
from torch.optim.lr_scheduler import ReduceLROnPlateau
from PIL import Image
import numpy as np
from tqdm import tqdm
import argparse
from collections import defaultdict
import multiprocessing as mp
from functools import partial
from datetime import datetime
from common import *
def process_scan_grid(scan_item, hue_jitter=0.1):
"""Process a single scan to create grid files and metadata"""
scan_id, metadata = scan_item
sample_metadata = []
sbs_path = os.path.join('data/scans', scan_id, 'sbs.jpg')
if not os.path.exists(sbs_path):
return sample_metadata
# Create grid directory if it doesn't exist
grid_dir = os.path.join('data/scans', scan_id, 'grids')
os.makedirs(grid_dir, exist_ok=True)
# Check if all grid files already exist
all_grids_exist = True
for i in range(3):
for j in range(3):
grid_filename = f'grid-{i}-{j}.jpg'
grid_path = os.path.join(grid_dir, grid_filename)
if not os.path.exists(grid_path):
all_grids_exist = False
break
if not all_grids_exist:
break
# If all grid files exist, just return metadata
if all_grids_exist:
for i in range(3):
for j in range(3):
grid_filename = f'grid-{i}-{j}.jpg'
grid_path = os.path.join(grid_dir, grid_filename)
label = 1 if 'pos' in metadata['labels'] else 0
sample_metadata.append({
'scan_id': scan_id,
'grid_path': grid_path,
'grid_i': i,
'grid_j': j,
'label': label
})
return sample_metadata
# Load the side-by-side image
sbs_img = Image.open(sbs_path).convert('RGB')
width, height = sbs_img.size
# Calculate crop dimensions
crop_width = width // 6 # width/2 / 3
crop_height = height // 3
# Generate all 3x3 grid combinations
for i in range(3):
for j in range(3):
# Calculate crop positions directly from original image
left_x = i * crop_width
right_x = (i + 3) * crop_width # Skip middle section
y = j * crop_height
# Crop directly from original image
left_crop = sbs_img.crop((left_x, y, left_x + crop_width, y + crop_height))
right_crop = sbs_img.crop((right_x, y, right_x + crop_width, y + crop_height))
# Apply color jitter only to left crop
color_jitter = transforms.ColorJitter(
brightness=0.2,
contrast=0.2,
saturation=0.2,
hue=hue_jitter
)
left_crop = color_jitter(left_crop)
# Concatenate left and right crops horizontally
grid_img = Image.new('RGB', (crop_width * 2, crop_height))
grid_img.paste(left_crop, (0, 0))
grid_img.paste(right_crop, (crop_width, 0))
# Save grid image
grid_filename = f'grid-{i}-{j}.jpg'
grid_path = os.path.join(grid_dir, grid_filename)
grid_img.save(grid_path, 'JPEG', quality=95)
# Store metadata
label = 1 if 'pos' in metadata['labels'] else 0
sample_metadata.append({
'scan_id': scan_id,
'grid_path': grid_path,
'grid_i': i,
'grid_j': j,
'label': label
})
return sample_metadata
class GridDataset(Dataset):
def __init__(self, scan_data, transform=None, num_workers=None, hue_jitter=0.1):
self.scan_data = scan_data
self.transform = transform
self.sample_metadata = []
self.hue_jitter = hue_jitter
if num_workers is None:
num_workers = min(mp.cpu_count(), 8) # Limit to 8 workers max
print(f"Preprocessing {len(scan_data)} scans to create grid files using {num_workers} workers...")
# Use multiprocessing to create grid files
with mp.Pool(processes=num_workers) as pool:
# Process all scans in parallel with hue_jitter parameter
process_func = partial(process_scan_grid, hue_jitter=self.hue_jitter)
results = list(tqdm(
pool.imap(process_func, scan_data.items()),
total=len(scan_data),
desc="Creating grid files"
))
# Collect all sample metadata
for result in results:
self.sample_metadata.extend(result)
print(f"Created {len(self.sample_metadata)} grid files")
def __len__(self):
return len(self.sample_metadata)
def __getitem__(self, idx):
# Get sample metadata
metadata = self.sample_metadata[idx]
# Load the pre-saved grid image directly
grid_img = Image.open(metadata['grid_path']).convert('RGB')
# Apply transforms
if self.transform:
grid_img = self.transform(grid_img)
return grid_img, torch.tensor(metadata['label'], dtype=torch.long)
def load_scan_data(data_dir):
"""Load all scan metadata and organize by scan_id"""
scan_data = {}
scans_dir = os.path.join(data_dir, 'scans')
if not os.path.exists(scans_dir):
raise FileNotFoundError(f"Scans directory not found: {scans_dir}")
scan_ids = [d for d in os.listdir(scans_dir)
if os.path.isdir(os.path.join(scans_dir, d))]
print(f"Loading metadata for {len(scan_ids)} scans...")
for scan_id in tqdm(scan_ids, desc="Loading scan metadata"):
metadata_path = os.path.join(scans_dir, scan_id, 'metadata.json')
if os.path.exists(metadata_path):
try:
with open(metadata_path, 'r') as f:
metadata = json.load(f)
scan_data[scan_id] = metadata
except Exception as e:
print(f"Error loading metadata for {scan_id}: {e}")
return scan_data
def split_train_val(scan_data):
"""Split data into train and validation sets"""
scan_ids = list(scan_data.keys())
print("Splitting data into train and validation sets...")
# Select 10% random scan_ids for initial validation set
val_size = int(len(scan_ids) * 0.1)
initial_val_scan_ids = random.sample(scan_ids, val_size)
print(f"Initial random split: {len(scan_ids) - val_size} train, {val_size} validation")
# Get all (code, labels) combinations that appear in the initial validation set
val_code_labels = set()
for scan_id in tqdm(initial_val_scan_ids, desc="Collecting validation code-label combinations"):
if scan_id in scan_data:
code = scan_data[scan_id].get('code')
labels = tuple(sorted(scan_data[scan_id].get('labels', [])))
if code:
val_code_labels.add((code, labels))
print(f"Found {len(val_code_labels)} unique (code, labels) combinations in validation set")
# Find all scans in train that have matching (code, labels) combinations and move them to validation
additional_val_scan_ids = set()
train_scan_ids = set(scan_ids) - set(initial_val_scan_ids)
for scan_id in tqdm(train_scan_ids, desc="Finding scans with matching code-label combinations"):
if scan_id in scan_data:
code = scan_data[scan_id].get('code')
labels = tuple(sorted(scan_data[scan_id].get('labels', [])))
# If (code, labels) combination matches, move to validation
if code and (code, labels) in val_code_labels:
additional_val_scan_ids.add(scan_id)
# Combine validation sets
all_val_scan_ids = set(initial_val_scan_ids) | additional_val_scan_ids
all_train_scan_ids = set(scan_data.keys()) - all_val_scan_ids
# Create train and validation data dictionaries
train_data = {scan_id: scan_data[scan_id] for scan_id in all_train_scan_ids}
val_data = {scan_id: scan_data[scan_id] for scan_id in all_val_scan_ids}
print(f"Final split:")
print(f" Total scans: {len(scan_data)}")
print(f" Training scans: {len(train_data)}")
print(f" Validation scans: {len(val_data)}")
return train_data, val_data
def train_model(model, train_loader, val_loader, criterion, optimizer, scheduler, device, transform, args, train_metadata=None, num_epochs=10):
"""Train the model for specified number of epochs"""
best_val_acc = 0.0
# Enable mixed precision training
scaler = torch.amp.GradScaler('cuda')
for epoch in range(num_epochs):
# Training phase
model.train()
train_loss = 0.0
train_correct = 0
train_total = 0
train_pos_correct = 0
train_pos_total = 0
train_neg_correct = 0
train_neg_total = 0
train_pbar = tqdm(train_loader, desc=f'Epoch {epoch+1}/{num_epochs} [Train]')
for batch_idx, (data, target) in enumerate(train_pbar):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
# Use mixed precision training
with torch.amp.autocast('cuda'):
output = model(data)
loss = criterion(output, target).mean()
# Scale loss and backpropagate
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
train_loss += loss.detach().item()
pred = output.argmax(dim=1, keepdim=True)
train_correct += pred.eq(target.view_as(pred)).sum().item()
train_total += target.size(0)
# Track per-class accuracy
for i in range(target.size(0)):
if target[i] == 1: # Positive class
train_pos_total += 1
if pred[i] == target[i]:
train_pos_correct += 1
else: # Negative class
train_neg_total += 1
if pred[i] == target[i]:
train_neg_correct += 1
# Clear memory after each batch
del data, target, output, loss, pred
# Calculate per-class accuracies
train_pos_acc = 100. * train_pos_correct / max(train_pos_total, 1)
train_neg_acc = 100. * train_neg_correct / max(train_neg_total, 1)
train_pbar.set_postfix({
'Loss': f'{train_loss/(batch_idx+1):.4f}',
'Acc': f'{100.*train_correct/train_total:.2f}%',
'Pos': f'{train_pos_acc:.2f}%',
'Neg': f'{train_neg_acc:.2f}%'
})
# Validation phase
model.eval()
val_loss = 0.0
val_correct = 0
val_total = 0
val_pos_correct = 0
val_pos_total = 0
val_neg_correct = 0
val_neg_total = 0
with torch.no_grad():
val_pbar = tqdm(val_loader, desc=f'Epoch {epoch+1}/{num_epochs} [Val]')
for batch_idx, (data, target) in enumerate(val_pbar):
data, target = data.to(device), target.to(device)
# Use mixed precision for validation too
with torch.amp.autocast('cuda'):
output = model(data)
loss = criterion(output, target).mean()
val_loss += loss.item()
pred = output.argmax(dim=1, keepdim=True)
val_correct += pred.eq(target.view_as(pred)).sum().item()
val_total += target.size(0)
# Track per-class accuracy
for i in range(target.size(0)):
if target[i] == 1: # Positive class
val_pos_total += 1
if pred[i] == target[i]:
val_pos_correct += 1
else: # Negative class
val_neg_total += 1
if pred[i] == target[i]:
val_neg_correct += 1
# Clear memory after each batch
del data, target, output, loss, pred
# Calculate per-class accuracies
val_pos_acc = 100. * val_pos_correct / max(val_pos_total, 1)
val_neg_acc = 100. * val_neg_correct / max(val_neg_total, 1)
val_pbar.set_postfix({
'Loss': f'{val_loss/(batch_idx+1):.4f}',
'Acc': f'{100.*val_correct/val_total:.2f}%',
'Pos': f'{val_pos_acc:.2f}%',
'Neg': f'{val_neg_acc:.2f}%'
})
# Calculate final accuracies
train_acc = 100. * train_correct / train_total
val_acc = 100. * val_correct / val_total
train_pos_acc = 100. * train_pos_correct / max(train_pos_total, 1)
train_neg_acc = 100. * train_neg_correct / max(train_neg_total, 1)
val_pos_acc = 100. * val_pos_correct / max(val_pos_total, 1)
val_neg_acc = 100. * val_neg_correct / max(val_neg_total, 1)
print(f'Epoch {epoch+1}/{num_epochs}:')
print(f' Train Loss: {train_loss/len(train_loader):.4f}, Train Acc: {train_acc:.2f}% (Pos: {train_pos_acc:.2f}%, Neg: {train_neg_acc:.2f}%)')
print(f' Val Loss: {val_loss/len(val_loader):.4f}, Val Acc: {val_acc:.2f}% (Pos: {val_pos_acc:.2f}%, Neg: {val_neg_acc:.2f}%)')
print(f' Train samples - Pos: {train_pos_total}, Neg: {train_neg_total}')
print(f' Val samples - Pos: {val_pos_total}, Neg: {val_neg_total}')
# Step the scheduler with validation accuracy
scheduler.step(val_acc)
current_lr = optimizer.param_groups[0]['lr']
print(f' Current learning rate: {current_lr:.6f}')
# Save best model
if val_acc > best_val_acc:
best_val_acc = val_acc
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
mode_suffix = "_quick" if args.quick else ""
best_model_path = f'models/best_model_ep{epoch+1}_pos{val_pos_acc:.2f}_neg{val_neg_acc:.2f}{mode_suffix}_{timestamp}.pt'
save_model(model, transform, best_model_path, train_metadata)
print(f' New best validation accuracy: {val_acc:.2f}%')
print(f' Best model saved: {best_model_path}')
return model, val_acc, val_pos_acc, val_neg_acc
def main():
parser = argparse.ArgumentParser(description='Train ResNet18 model on grid data')
parser.add_argument('--data-dir', default='data', help='Data directory')
parser.add_argument('--batch-size', type=int, default=64, help='Batch size (increased for better GPU utilization)')
parser.add_argument('--lr', type=float, default=0.001, help='Learning rate')
parser.add_argument('--epochs', type=int, default=100, help='Number of epochs')
parser.add_argument('--num-workers', type=int, default=None, help='Number of workers for preprocessing (default: auto)')
parser.add_argument('--quick', action='store_true', help='Quick mode: use only 1%% of scans for faster testing')
parser.add_argument('--hue-jitter', type=float, default=0.1, help='Hue jitter parameter for ColorJitter (default: 0.1)')
parser.add_argument('--scheduler-patience', type=int, default=3, help='Patience for ReduceLROnPlateau scheduler (default: 3)')
parser.add_argument('--scheduler-factor', type=float, default=0.1, help='Factor for ReduceLROnPlateau scheduler (default: 0.1)')
parser.add_argument('--scheduler-min-lr', type=float, default=1e-6, help='Minimum learning rate for ReduceLROnPlateau scheduler (default: 1e-6)')
args = parser.parse_args()
# Set device
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f'Using device: {device}')
print(f'Using hue jitter: {args.hue_jitter}')
# Create models directory if it doesn't exist
os.makedirs('models', exist_ok=True)
# Load scan data
print("Loading scan data...")
scan_data = load_scan_data(args.data_dir)
if not scan_data:
print("No scan data found!")
return
# Quick mode: use only 1% of scans for faster testing
if args.quick:
original_count = len(scan_data)
scan_ids = list(scan_data.keys())
# Take 1% of scans, but at least 10 scans for meaningful training
quick_count = max(10, int(len(scan_ids) * 0.005))
selected_scan_ids = random.sample(scan_ids, quick_count)
scan_data = {scan_id: scan_data[scan_id] for scan_id in selected_scan_ids}
print(f"Quick mode enabled: Using {len(scan_data)} scans out of {original_count} (1%)")
# Split into train and validation
print("Splitting data into train and validation...")
train_data, val_data = split_train_val(scan_data)
# Define transforms
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
# Create datasets
print("Creating training dataset...")
train_dataset = GridDataset(train_data, transform=transform, num_workers=args.num_workers, hue_jitter=args.hue_jitter)
print(f"Training samples: {len(train_dataset)}")
print("Creating validation dataset...")
val_dataset = GridDataset(val_data, transform=transform, num_workers=args.num_workers, hue_jitter=args.hue_jitter)
print(f"Validation samples: {len(val_dataset)}")
# Create data loaders
print("Creating data loaders...")
train_loader = DataLoader(
train_dataset,
batch_size=args.batch_size,
shuffle=True,
num_workers=8,
pin_memory=False,
persistent_workers=True,
prefetch_factor=2
)
val_loader = DataLoader(
val_dataset,
batch_size=args.batch_size,
shuffle=False,
num_workers=8,
pin_memory=False,
persistent_workers=True,
prefetch_factor=2
)
print(f"Train batches: {len(train_loader)}, Val batches: {len(val_loader)}")
# Create model
print("Creating ResNet18 model...")
# model = torchvision.models.resnet18(pretrained=True)
model, _ = load_model('models/gridcrop-resnet-ep24-pos97.29-neg75.10-20250509_051300.pt')
# Modify final layer for binary classification
# model.fc = nn.Linear(model.fc.in_features, 2)
model = model.to(device)
print(f"Model created and moved to {device}")
# Define loss function and optimizer
print("Setting up loss function and optimizer...")
# Use FocalLoss with 0.99:0.01 weights for positive/negative classes
pos_weight = 0.99
criterion = FocalLoss(0.25, weight=torch.Tensor([1.0 - pos_weight, pos_weight]).to(device))
optimizer = optim.Adam(model.parameters(), lr=args.lr)
# Create ReduceLROnPlateau scheduler
scheduler = ReduceLROnPlateau(
optimizer,
mode='max',
factor=args.scheduler_factor,
patience=args.scheduler_patience,
min_lr=args.scheduler_min_lr
)
print(f"Using Adam optimizer with lr={args.lr}")
print(f"Using FocalLoss with weights: Negative={1.0 - pos_weight:.2f}, Positive={pos_weight:.2f}")
print(f"Using ReduceLROnPlateau scheduler with factor={args.scheduler_factor}, patience={args.scheduler_patience}, min_lr={args.scheduler_min_lr}")
# Collect training metadata
train_scan_ids = list(train_data.keys())
train_codes = set()
for scan_id, metadata in train_data.items():
code = metadata.get('code')
if code:
train_codes.add(code)
train_metadata = {
'train_scan_ids': train_scan_ids,
'train_codes': list(train_codes),
'hue_jitter': args.hue_jitter,
'quick_mode': args.quick
}
print(f"Training metadata:")
print(f" Train scan IDs: {len(train_scan_ids)}")
print(f" Train codes: {len(train_codes)}")
print(f" Hue jitter: {args.hue_jitter}")
print(f" Quick mode: {args.quick}")
# Train the model
print("Starting training...")
model, final_val_acc, final_val_pos_acc, final_val_neg_acc = train_model(model, train_loader, val_loader, criterion, optimizer, scheduler, device, transform, args, train_metadata, args.epochs)
# Save final model
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
mode_suffix = "_quick" if args.quick else ""
final_model_path = f'models/final_model_ep{args.epochs}_pos{final_val_pos_acc:.2f}_neg{final_val_neg_acc:.2f}{mode_suffix}_{timestamp}.pt'
save_model(model, transform, final_model_path, train_metadata)
print(f"Training completed! Final model saved: {final_model_path}")
if __name__ == '__main__':
main()

15
emblem5/ai/upload.py Executable file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env python3
import oss2
import sys
import os
oss_ak = 'LTAI5tC2qXGxwHZUZP7DoD1A'
oss_sk = 'qPo9O6ZvEfqo4t8oflGEm0DoxLHJhm'
auth = oss2.Auth(oss_ak, oss_sk)
endpoint = 'https://oss-rg-china-mainland.aliyuncs.com'
bucket = oss2.Bucket(auth, endpoint, 'emblem-models')
for x in sys.argv[1:]:
bucket.put_object_from_file(os.path.basename(x), x)

View File

@ -0,0 +1,167 @@
9041518155696
9043391705032
9047560846288
9047175442152
9046645732164
9048448971990
9047231003796
9046667514583
9047217570340
9043862855714
9047719805291
9046067235268
9044607756016
9049438906993
9048110553084
9044313130824
9046610110726
9048196228678
9042527399330
9041865329320
9043038880530
9043211403350
9042124015937
9048747140763
9048907683811
9044055383558
9044961109172
9046289086854
9045378241113
9049268170237
9043251838574
9043425154373
9042596134496
9047999799544
9041017580514
9048457850808
9046098673325
9044596429399
9047671774248
9048108092652
9045835321309
9044679005902
9048208086422
9049703639151
9041824087492
9048993912134
9047797019579
9042176791662
9049916444625
9046735238974
9046044876640
9042964782271
9046081233017
9047151569363
9043467200041
9042652128603
9046393672383
9044036329705
9047824031869
9044526583112
9044604195977
9043745698353
9046855799440
9041870423863
9043385501906
9047466185021
9043986284948
9049268008982
9045179435607
9043885459754
9041362669101
9048090113001
9047484404980
9049495526869
9044101220760
9044101220760
9045453464619
9046473812518
9045683245091
9045913691663
9042549358206
9044446342115
9044357449196
9046629219878
9048128593651
9049834888165
9044664345848
9049588698348
9041635328789
9047205431361
9041704601110
9045204595658
9045789047781
9047522816803
9047843727318
9049482594873
9044866091316
9047760719652
4292859353771
4291283577956
4292091386770
4293067912516
4292069800308
4299999931135
4292045885091
4293959856775
4295757450024
4299348480765
4299496909661
4296911416779
4295370931573
4296920517990
4296693440435
4295217706242
4299655305592
4296536108258
4298797246935
4292384445350
4299197224943
4298223657987
4293625631612
4299656315895
4299436471684
4295932375321
1162010093987
1169941547790
1167433463200
1168073514307
1161874738818
1165899663363
1163234869247
1163522365275
1166807824551
1165813695823
1168402949263
1162255504991
1166442217897
1162733112181
1167084078863
1164804351567
1169071099109
1164135002853
1165104857536
1162802553605
1162410467884
1164659148887
1162015664585
1167309572217
1168586487143
1167825009551
1168243803466
1162683501280
1164839838856
1168003168625
1166662251448
1165064496397
1169221746833
1164728213265
1166019005564
1163925013376
1167589827603
1164868647043
1163089588202
1169765273422

349
emblem5/ai/verify2.py Executable file
View File

@ -0,0 +1,349 @@
#!/usr/bin/env python3
'''
Load the best model and run inference on all scan IDs.
For each scan, predict on all 3x3 grid images and use voting to determine the final scan label.
Calculate accuracy against the original scan labels.
'''
import os
import json
import torch
import torch.nn as nn
import torchvision.transforms as transforms
from torch.utils.data import Dataset, DataLoader
from PIL import Image
import numpy as np
from tqdm import tqdm
import argparse
import random
from collections import defaultdict
from common import *
class GridInferenceDataset(Dataset):
def __init__(self, scan_data, transform=None):
self.scan_data = scan_data
self.transform = transform
self.sample_metadata = []
print(f"Loading grid files for {len(scan_data)} scans...")
# Collect all grid files for each scan
for scan_id, metadata in tqdm(scan_data.items(), desc="Loading grid metadata"):
grid_dir = os.path.join('data/scans', scan_id, 'grids')
if not os.path.exists(grid_dir):
continue
# Check for all 9 grid files
grid_files = []
for i in range(3):
for j in range(3):
grid_filename = f'grid-{i}-{j}.jpg'
grid_path = os.path.join(grid_dir, grid_filename)
if os.path.exists(grid_path):
grid_files.append({
'scan_id': scan_id,
'grid_path': grid_path,
'grid_i': i,
'grid_j': j,
'label': 1 if 'pos' in metadata['labels'] else 0
})
# Only include scans that have all 9 grid files
if len(grid_files) == 9:
self.sample_metadata.extend(grid_files)
else:
print(f"Warning: Scan {scan_id} has {len(grid_files)}/9 grid files, skipping")
print(f"Loaded {len(self.sample_metadata)} grid files from {len(self.sample_metadata)//9} complete scans")
def __len__(self):
return len(self.sample_metadata)
def __getitem__(self, idx):
# Get sample metadata
metadata = self.sample_metadata[idx]
# Load the grid image
grid_img = Image.open(metadata['grid_path']).convert('RGB')
# Apply transforms
if self.transform:
grid_img = self.transform(grid_img)
return grid_img, torch.tensor(metadata['label'], dtype=torch.long), metadata['scan_id']
def load_scan_data(data_dir):
"""Load all scan metadata and organize by scan_id"""
scan_data = {}
scans_dir = os.path.join(data_dir, 'scans')
if not os.path.exists(scans_dir):
raise FileNotFoundError(f"Scans directory not found: {scans_dir}")
scan_ids = [d for d in os.listdir(scans_dir)
if os.path.isdir(os.path.join(scans_dir, d))]
print(f"Loading metadata for {len(scan_ids)} scans...")
for scan_id in tqdm(scan_ids, desc="Loading scan metadata"):
metadata_path = os.path.join(scans_dir, scan_id, 'metadata.json')
if os.path.exists(metadata_path):
try:
with open(metadata_path, 'r') as f:
metadata = json.load(f)
scan_data[scan_id] = metadata
except Exception as e:
print(f"Error loading metadata for {scan_id}: {e}")
return scan_data
def run_inference(model, data_loader, device):
"""Run inference on all data and collect predictions by scan_id"""
model.eval()
# Dictionary to store predictions for each scan
scan_predictions = defaultdict(list)
scan_labels = {}
# Track running accuracy
total_predictions = 0
correct_predictions = 0
pos_correct = 0
pos_total = 0
neg_correct = 0
neg_total = 0
print("Running inference...")
with torch.no_grad():
pbar = tqdm(data_loader, desc="Inference")
for batch_idx, (data, target, scan_ids) in enumerate(pbar):
data = data.to(device)
# Get predictions
output = model(data)
probabilities = torch.softmax(output, dim=1)
predictions = torch.argmax(output, dim=1)
# Move target to same device as predictions for comparison
target_device = target.to(device)
# Calculate batch accuracy
batch_correct = (predictions == target_device).sum().item()
correct_predictions += batch_correct
total_predictions += len(target)
# Track per-class accuracy
for i in range(len(target)):
if target[i] == 1: # Positive class
pos_total += 1
if predictions[i] == target_device[i]:
pos_correct += 1
else: # Negative class
neg_total += 1
if predictions[i] == target_device[i]:
neg_correct += 1
# Store predictions by scan_id
for i in range(len(scan_ids)):
scan_id = scan_ids[i]
pred = predictions[i].item()
prob = probabilities[i][1].item() # Probability of positive class
scan_predictions[scan_id].append({
'prediction': pred,
'probability': prob
})
# Store the true label (should be the same for all grids in a scan)
if scan_id not in scan_labels:
scan_labels[scan_id] = target[i].item()
# Calculate running accuracies
overall_acc = 100. * correct_predictions / total_predictions if total_predictions > 0 else 0
pos_acc = 100. * pos_correct / pos_total if pos_total > 0 else 0
neg_acc = 100. * neg_correct / neg_total if neg_total > 0 else 0
# Update progress bar with accuracy info
pbar.set_postfix({
'Overall': f'{overall_acc:.2f}%',
'Pos': f'{pos_acc:.2f}%',
'Neg': f'{neg_acc:.2f}%',
'Pos/Total': f'{pos_correct}/{pos_total}',
'Neg/Total': f'{neg_correct}/{neg_total}'
})
# Clear memory
del data, output, probabilities, predictions
return scan_predictions, scan_labels
def calculate_voting_accuracy(scan_predictions, scan_labels):
"""Calculate accuracy using voting mechanism"""
correct_predictions = 0
total_scans = 0
pos_correct = 0
pos_total = 0
neg_correct = 0
neg_total = 0
# Create log file
log_file = 'data/verify2.log'
os.makedirs(os.path.dirname(log_file), exist_ok=True)
with open(log_file, 'w') as f:
f.write("Voting Results:\n")
f.write("-" * 80 + "\n")
f.write(f"{'Scan ID':<15} {'True Label':<12} {'Vote Result':<12} {'Pos Votes':<10} {'Neg Votes':<10} {'Correct':<8}\n")
f.write("-" * 80 + "\n")
print("\nVoting Results:")
print("-" * 80)
print(f"{'Scan ID':<15} {'True Label':<12} {'Vote Result':<12} {'Pos Votes':<10} {'Neg Votes':<10} {'Correct':<8}")
print("-" * 80)
for scan_id, predictions in scan_predictions.items():
if scan_id not in scan_labels:
continue
true_label = scan_labels[scan_id]
total_scans += 1
# Count positive and negative votes
pos_votes = sum(1 for p in predictions if p['prediction'] == 1)
neg_votes = sum(1 for p in predictions if p['prediction'] == 0)
# Determine final prediction by majority vote
final_prediction = 1 if pos_votes > neg_votes else 0
# Check if prediction is correct
is_correct = final_prediction == true_label
if is_correct:
correct_predictions += 1
# Track per-class accuracy
if true_label == 1: # Positive class
pos_total += 1
if is_correct:
pos_correct += 1
else: # Negative class
neg_total += 1
if is_correct:
neg_correct += 1
# Print result
status = "" if is_correct else ""
result_line = f"{scan_id:<15} {true_label:<12} {final_prediction:<12} {pos_votes:<10} {neg_votes:<10} {status:<8}"
print(result_line)
# Write to log file
with open(log_file, 'a') as f:
f.write(result_line + "\n")
# Calculate accuracies
overall_accuracy = 100. * correct_predictions / total_scans if total_scans > 0 else 0
pos_accuracy = 100. * pos_correct / pos_total if pos_total > 0 else 0
neg_accuracy = 100. * neg_correct / neg_total if neg_total > 0 else 0
# Write summary to log file
with open(log_file, 'a') as f:
f.write("-" * 80 + "\n")
f.write(f"Overall Accuracy: {overall_accuracy:.2f}% ({correct_predictions}/{total_scans})\n")
f.write(f"Positive Accuracy: {pos_accuracy:.2f}% ({pos_correct}/{pos_total})\n")
f.write(f"Negative Accuracy: {neg_accuracy:.2f}% ({neg_correct}/{neg_total})\n")
print("-" * 80)
print(f"Overall Accuracy: {overall_accuracy:.2f}% ({correct_predictions}/{total_scans})")
print(f"Positive Accuracy: {pos_accuracy:.2f}% ({pos_correct}/{pos_total})")
print(f"Negative Accuracy: {neg_accuracy:.2f}% ({neg_correct}/{neg_total})")
return overall_accuracy, pos_accuracy, neg_accuracy
def main():
parser = argparse.ArgumentParser(description='Verify model accuracy using voting mechanism')
parser.add_argument('--data-dir', default='data', help='Data directory')
parser.add_argument('--model', default='models/final_model_ep10_pos98.54_neg78.52_20250706_143910.pt', help='Path to the model file')
parser.add_argument('--batch-size', type=int, default=128, help='Batch size for inference')
parser.add_argument('--sample', type=float, default=1.0, help='Fraction of scans to sample (0.0-1.0, default: 1.0 for all scans)')
args = parser.parse_args()
# Set device
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f'Using device: {device}')
# Load scan data
print("Loading scan data...")
scan_data = load_scan_data(args.data_dir)
if not scan_data:
print("No scan data found!")
return
# Sample scans if requested
if args.sample < 1.0:
scan_ids = list(scan_data.keys())
num_to_sample = max(1, int(len(scan_ids) * args.sample))
sampled_scan_ids = random.sample(scan_ids, num_to_sample)
scan_data = {scan_id: scan_data[scan_id] for scan_id in sampled_scan_ids}
print(f"Sampled {len(sampled_scan_ids)} scans out of {len(scan_ids)} total scans ({args.sample*100:.1f}%)")
else:
print(f"Using all {len(scan_data)} scans")
# Define transforms
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
# Create dataset
print("Creating inference dataset...")
dataset = GridInferenceDataset(scan_data, transform=transform)
if len(dataset) == 0:
print("No valid grid files found!")
return
# Create data loader
print("Creating data loader...")
data_loader = DataLoader(
dataset,
batch_size=args.batch_size,
shuffle=True,
num_workers=8,
pin_memory=False,
persistent_workers=True,
prefetch_factor=2
)
print(f"Total batches: {len(data_loader)}")
# Load model
print(f"Loading model from {args.model}...")
try:
model, _ = load_model(args.model)
model = model.to(device)
print("Model loaded successfully")
except Exception as e:
print(f"Error loading model: {e}")
return
# Run inference
scan_predictions, scan_labels = run_inference(model, data_loader, device)
# Calculate voting accuracy
overall_acc, pos_acc, neg_acc = calculate_voting_accuracy(scan_predictions, scan_labels)
print(f"\nFinal Results:")
print(f"Overall Accuracy: {overall_acc:.2f}%")
print(f"Positive Accuracy: {pos_acc:.2f}%")
print(f"Negative Accuracy: {neg_acc:.2f}%")
# Write final results to log file
with open('data/verify2.log', 'a') as f:
f.write(f"\nFinal Results:\n")
f.write(f"Overall Accuracy: {overall_acc:.2f}%\n")
f.write(f"Positive Accuracy: {pos_acc:.2f}%\n")
f.write(f"Negative Accuracy: {neg_acc:.2f}%\n")
print(f"\nResults have been written to data/verify2.log")
if __name__ == '__main__':
main()

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -0,0 +1,403 @@
layer {
name: "data"
type: "Input"
top: "data"
input_param {
shape {
dim: 1
dim: 1
dim: 224
dim: 224
}
}
}
layer {
name: "conv0"
type: "Convolution"
bottom: "data"
top: "conv0"
param {
lr_mult: 1.0
decay_mult: 1.0
}
param {
lr_mult: 1.0
decay_mult: 0.0
}
convolution_param {
num_output: 32
bias_term: true
pad: 1
kernel_size: 3
group: 1
stride: 1
weight_filler {
type: "msra"
}
}
}
layer {
name: "conv0/lrelu"
type: "ReLU"
bottom: "conv0"
top: "conv0"
relu_param {
negative_slope: 0.05000000074505806
}
}
layer {
name: "db1/reduce"
type: "Convolution"
bottom: "conv0"
top: "db1/reduce"
param {
lr_mult: 1.0
decay_mult: 1.0
}
param {
lr_mult: 1.0
decay_mult: 0.0
}
convolution_param {
num_output: 8
bias_term: true
pad: 0
kernel_size: 1
group: 1
stride: 1
weight_filler {
type: "msra"
}
}
}
layer {
name: "db1/reduce/lrelu"
type: "ReLU"
bottom: "db1/reduce"
top: "db1/reduce"
relu_param {
negative_slope: 0.05000000074505806
}
}
layer {
name: "db1/3x3"
type: "Convolution"
bottom: "db1/reduce"
top: "db1/3x3"
param {
lr_mult: 1.0
decay_mult: 1.0
}
param {
lr_mult: 1.0
decay_mult: 0.0
}
convolution_param {
num_output: 8
bias_term: true
pad: 1
kernel_size: 3
group: 8
stride: 1
weight_filler {
type: "msra"
}
}
}
layer {
name: "db1/3x3/lrelu"
type: "ReLU"
bottom: "db1/3x3"
top: "db1/3x3"
relu_param {
negative_slope: 0.05000000074505806
}
}
layer {
name: "db1/1x1"
type: "Convolution"
bottom: "db1/3x3"
top: "db1/1x1"
param {
lr_mult: 1.0
decay_mult: 1.0
}
param {
lr_mult: 1.0
decay_mult: 0.0
}
convolution_param {
num_output: 32
bias_term: true
pad: 0
kernel_size: 1
group: 1
stride: 1
weight_filler {
type: "msra"
}
}
}
layer {
name: "db1/1x1/lrelu"
type: "ReLU"
bottom: "db1/1x1"
top: "db1/1x1"
relu_param {
negative_slope: 0.05000000074505806
}
}
layer {
name: "db1/concat"
type: "Concat"
bottom: "conv0"
bottom: "db1/1x1"
top: "db1/concat"
concat_param {
axis: 1
}
}
layer {
name: "db2/reduce"
type: "Convolution"
bottom: "db1/concat"
top: "db2/reduce"
param {
lr_mult: 1.0
decay_mult: 1.0
}
param {
lr_mult: 1.0
decay_mult: 0.0
}
convolution_param {
num_output: 8
bias_term: true
pad: 0
kernel_size: 1
group: 1
stride: 1
weight_filler {
type: "msra"
}
}
}
layer {
name: "db2/reduce/lrelu"
type: "ReLU"
bottom: "db2/reduce"
top: "db2/reduce"
relu_param {
negative_slope: 0.05000000074505806
}
}
layer {
name: "db2/3x3"
type: "Convolution"
bottom: "db2/reduce"
top: "db2/3x3"
param {
lr_mult: 1.0
decay_mult: 1.0
}
param {
lr_mult: 1.0
decay_mult: 0.0
}
convolution_param {
num_output: 8
bias_term: true
pad: 1
kernel_size: 3
group: 8
stride: 1
weight_filler {
type: "msra"
}
}
}
layer {
name: "db2/3x3/lrelu"
type: "ReLU"
bottom: "db2/3x3"
top: "db2/3x3"
relu_param {
negative_slope: 0.05000000074505806
}
}
layer {
name: "db2/1x1"
type: "Convolution"
bottom: "db2/3x3"
top: "db2/1x1"
param {
lr_mult: 1.0
decay_mult: 1.0
}
param {
lr_mult: 1.0
decay_mult: 0.0
}
convolution_param {
num_output: 32
bias_term: true
pad: 0
kernel_size: 1
group: 1
stride: 1
weight_filler {
type: "msra"
}
}
}
layer {
name: "db2/1x1/lrelu"
type: "ReLU"
bottom: "db2/1x1"
top: "db2/1x1"
relu_param {
negative_slope: 0.05000000074505806
}
}
layer {
name: "db2/concat"
type: "Concat"
bottom: "db1/concat"
bottom: "db2/1x1"
top: "db2/concat"
concat_param {
axis: 1
}
}
layer {
name: "upsample/reduce"
type: "Convolution"
bottom: "db2/concat"
top: "upsample/reduce"
param {
lr_mult: 1.0
decay_mult: 1.0
}
param {
lr_mult: 1.0
decay_mult: 0.0
}
convolution_param {
num_output: 32
bias_term: true
pad: 0
kernel_size: 1
group: 1
stride: 1
weight_filler {
type: "msra"
}
}
}
layer {
name: "upsample/reduce/lrelu"
type: "ReLU"
bottom: "upsample/reduce"
top: "upsample/reduce"
relu_param {
negative_slope: 0.05000000074505806
}
}
layer {
name: "upsample/deconv"
type: "Deconvolution"
bottom: "upsample/reduce"
top: "upsample/deconv"
param {
lr_mult: 1.0
decay_mult: 1.0
}
param {
lr_mult: 1.0
decay_mult: 0.0
}
convolution_param {
num_output: 32
bias_term: true
pad: 1
kernel_size: 3
group: 32
stride: 2
weight_filler {
type: "msra"
}
}
}
layer {
name: "upsample/lrelu"
type: "ReLU"
bottom: "upsample/deconv"
top: "upsample/deconv"
relu_param {
negative_slope: 0.05000000074505806
}
}
layer {
name: "upsample/rec"
type: "Convolution"
bottom: "upsample/deconv"
top: "upsample/rec"
param {
lr_mult: 1.0
decay_mult: 1.0
}
param {
lr_mult: 1.0
decay_mult: 0.0
}
convolution_param {
num_output: 1
bias_term: true
pad: 0
kernel_size: 1
group: 1
stride: 1
weight_filler {
type: "msra"
}
}
}
layer {
name: "nearest"
type: "Deconvolution"
bottom: "data"
top: "nearest"
param {
lr_mult: 0.0
decay_mult: 0.0
}
convolution_param {
num_output: 1
bias_term: false
pad: 0
kernel_size: 2
group: 1
stride: 2
weight_filler {
type: "constant"
value: 1.0
}
}
}
layer {
name: "Crop1"
type: "Crop"
bottom: "nearest"
bottom: "upsample/rec"
top: "Crop1"
}
layer {
name: "fc"
type: "Eltwise"
bottom: "Crop1"
bottom: "upsample/rec"
top: "fc"
eltwise_param {
operation: SUM
}
}

View File

@ -0,0 +1,14 @@
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y \
build-essential \
cmake \
git \
python3-pip \
python3-venv \
libglib2.0-0 \
libgl1
RUN python3 -m venv /venv
RUN /venv/bin/pip install torch torchvision torchaudio -i https://download.pytorch.org/whl/cu121
ADD requirements.txt /tmp/requirements.txt
RUN /venv/bin/pip install --upgrade pip
RUN /venv/bin/pip install -r /tmp/requirements.txt

10
emblem5/baseimg/Makefile Normal file
View File

@ -0,0 +1,10 @@
IMG_TAG := $(shell date +%Y%m%d%H)-$(shell git rev-parse --short HEAD)
IMG_NAME := registry.cn-shenzhen.aliyuncs.com/emblem/baseimg:$(IMG_TAG)
default: build push
build:
docker build -t $(IMG_NAME) .
push:
docker push $(IMG_NAME)

View File

@ -0,0 +1,15 @@
tqdm
loguru
pillow
numpy
pandas
matplotlib
scikit-learn
scipy
seaborn
flask
oss2
requests
kornia
opencv-python
opencv-contrib-python

View File

@ -0,0 +1,12 @@
Types: deb
#URIs: http://archive.ubuntu.com/ubuntu/
URIs: http://mirrors.aliyun.com/ubuntu/
Suites: noble noble-updates noble-backports
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
Types: deb
URIs: http://mirrors.aliyun.com/ubuntu/
Suites: noble-security
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg

View File

@ -0,0 +1,114 @@
apiVersion: v1
kind: Namespace
metadata:
name: "emblem"
labels:
name: "emblem"
---
apiVersion: v1
kind: Secret
metadata:
name: regcred
namespace: emblem
data:
.dockerconfigjson: ewoJImF1dGhzIjogewoJCSJodHRwczovL2luZGV4LmRvY2tlci5pby92MS8iOiB7CgkJCSJhdXRoIjogIlptRnRlbWhsYm1jNmFuUmFOamMwWlhSMmNHNTVTMEk9IgoJCX0sCgkJInJlZ2lzdHJ5LmNuLWJlaWppbmcuYWxpeXVuY3MuY29tIjogewoJCQkiYXV0aCI6ICJaWFZ3YUc5dU9ucEZPRjVlWTJ0cWJtTktRa29xIgoJCX0sCgkJInJlZ2lzdHJ5LmNuLXNoZW56aGVuLmFsaXl1bmNzLmNvbSI6IHsKCQkJImF1dGgiOiAiWlhWd2FHOXVPbnBGT0Y1ZVkydHFibU5LUWtvcSIKCQl9LAoJCSJyZWdpc3RyeS5naXRsYWIuY29tIjogewoJCQkiYXV0aCI6ICJabUZ0ZW1obGJtYzZaMnh3WVhRdFIyWjZXbmxFUjNCMU9XZEtlVGRSTVZSUmVtWT0iCgkJfQoJfQp9Cg==
type: kubernetes.io/dockerconfigjson
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/name: server
deploy-timestamp: "${deploy_timestamp}"
name: server
namespace: emblem
spec:
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
app.kubernetes.io/name: server
template:
metadata:
labels:
app.kubernetes.io/name: server
deploy-timestamp: "${deploy_timestamp}"
spec:
imagePullSecrets:
- name: regcred
volumes:
- name: data
hostPath:
path: /data/emblem
type: Directory
containers:
- image: ${image}
imagePullPolicy: IfNotPresent
name: server
volumeMounts:
- mountPath: /emblem/data
name: data
env:
- name: EMBLEM_ENV
value: ${emblem_env}
- name: EMBLEM_DB_TYPE
value: postgres
- name: EMBLEM_DB_HOST
value: ${db_host}
resources:
requests:
memory: "2048Mi"
cpu: "1000m"
limits:
memory: "2048Mi"
cpu: "1000m"
ports:
- containerPort: 80
name: http
protocol: TCP
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: server
namespace: emblem
spec:
selector:
app.kubernetes.io/name: server
ports:
- protocol: TCP
port: 80
targetPort: http
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: server
namespace: emblem
annotations:
traefik.ingress.kubernetes.io/router.tls.certresolver: le
spec:
tls:
- hosts:
% for host in hosts:
- ${host}
% endfor
rules:
% for host in hosts:
- host: ${host}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: server
port:
number: 80
% endfor

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,20 @@
apiVersion: v1
clusters:
- cluster:
insecure-skip-tls-verify: true
server: https://derby.euphon.net:6443
name: default
contexts:
- context:
cluster: default
namespace: emblem
user: default
name: default
current-context: default
kind: Config
preferences: {}
users:
- name: default
user:
client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrRENDQVRlZ0F3SUJBZ0lJYlV6UGpWalNzYll3Q2dZSUtvWkl6ajBFQXdJd0l6RWhNQjhHQTFVRUF3d1kKYXpOekxXTnNhV1Z1ZEMxallVQXhOekl6TURVNU9UQTNNQjRYRFRJME1EZ3dOekU1TkRVd04xb1hEVEkxTURndwpOekU1TkRVd04xb3dNREVYTUJVR0ExVUVDaE1PYzNsemRHVnRPbTFoYzNSbGNuTXhGVEFUQmdOVkJBTVRESE41CmMzUmxiVHBoWkcxcGJqQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJHZjgvYlFKaU1rQUdIdUMKdG9qK0crVmsrbGgvalBvTUs1V29Zbi9xaGZBL0NrT05QMlIvTy9yci9uK2xZUERIZkMvaXVIcmx5ZUtkL1ZwTAozeVR4SVc2alNEQkdNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBakFmCkJnTlZIU01FR0RBV2dCUXdtL2pDTWozOXhEbG1aU05HNmdCTGJvL2ZPREFLQmdncWhrak9QUVFEQWdOSEFEQkUKQWlBOVREdHBIK0RaQ3g3Q2NWVXZYQVpLMFg2UUpxUldESWMrdkxIMEdRc3p0d0lnRkZObTk1R015ZXdIN3hqYQppVnhHREVjT281OG9sNDF3SUU2WFpRa2pNdEU9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdFkyeHAKWlc1MExXTmhRREUzTWpNd05UazVNRGN3SGhjTk1qUXdPREEzTVRrME5UQTNXaGNOTXpRd09EQTFNVGswTlRBMwpXakFqTVNFd0h3WURWUVFEREJock0zTXRZMnhwWlc1MExXTmhRREUzTWpNd05UazVNRGN3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFSdEtyd3hYRTB2L0FCV3FYS1dheGtGQlU0VmZuZmxGOFUwcjViUmt5cFQKRjRhVzV3N21ONkhOK01TdGo4T3BlVHBKSzJtM0VNbkE5SnFrS0ZhM3N0S0lvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVU1KdjR3akk5L2NRNVptVWpSdW9BClMyNlAzemd3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUlnUW9XYlhZT3Brb1owL1hIeGRNTXEydWJYWUNrSmo3c2IKMnJqT3UxRlhmUEVDSVFEMEpId0NsZDdrME9FK2ZVK1ZYWDVSRWdWMFFmK2gxU3RNUS9KdFBjMXVkdz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
client-key-data: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU04Q0dzMGRZNGN0MWJZaUJScGVvK1dLTmhBbzl0b1d2NHpCcmR4S3U0RWVvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWi96OXRBbUl5UUFZZTRLMmlQNGI1V1Q2V0grTStnd3JsYWhpZitxRjhEOEtRNDAvWkg4NwordXYrZjZWZzhNZDhMK0s0ZXVYSjRwMzlXa3ZmSlBFaGJnPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=

View File

@ -0,0 +1,20 @@
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUzTURneE1EazROekF3SGhjTk1qUXdNakUyTVRnMU56VXdXaGNOTXpRd01qRXpNVGcxTnpVdwpXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUzTURneE1EazROekF3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFUd1FEUnZaRDV0OFJTcnBDU0pSOG5wdVg1NS8wdkJDTTJrUnBKdlFyanUKcTY3MnJCeUhyM3RQSmRWdmRnSHpYZnM4VzhpVTdiS2RvUlk4UWNuU2xKWjNvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVStYZjFsbjBjVll5Q3QrYTFFUEdnClkzdGVNTFF3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUloQUlxWDJsczNlSkNoRnF1Vis1bklGY0R5N3RlUnhZdTMKMWJYNU1aYTkvRU9xQWlBN0M2MXZGV1B3Mk55WW83NG1pSUE2bldEMUxwVi9GOXBjVE9BaW5wdzR5Zz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
server: https://127.0.0.1:6443
name: default
contexts:
- context:
cluster: default
user: default
namespace: emblem
name: default
current-context: default
kind: Config
preferences: {}
users:
- name: default
user:
client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrVENDQVRlZ0F3SUJBZ0lJWGNzc0Y1QnpJeE13Q2dZSUtvWkl6ajBFQXdJd0l6RWhNQjhHQTFVRUF3d1kKYXpOekxXTnNhV1Z1ZEMxallVQXhOekE0TVRBNU9EY3dNQjRYRFRJME1ESXhOakU0TlRjMU1Gb1hEVEkxTURJeApOVEU0TlRjMU1Gb3dNREVYTUJVR0ExVUVDaE1PYzNsemRHVnRPbTFoYzNSbGNuTXhGVEFUQmdOVkJBTVRESE41CmMzUmxiVHBoWkcxcGJqQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJHbWlZUUdxK3E4Y3JEQkQKemwraS9kb3BaNHphQWk1cXdFQkR4ZE1Bc1dabTdUOGNXT3BoejdtMTdaV3JZcm1rOTdFWXVmci8xcm1iREdPaAprOGhsUUdXalNEQkdNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBakFmCkJnTlZIU01FR0RBV2dCUUhBNkdNMEdVSjBVN2NxeUVXS0VXNzdlVGlYakFLQmdncWhrak9QUVFEQWdOSUFEQkYKQWlCSi9XOXNFaU1JTUV5dGk3N1lDM2hud0c1eXNyeHAvWVgySFVKQUpFQlBVQUloQU4xM2M1anU5azd4a3ZlWgpVYmk1MmZNQzBIZUxUUUdNNitSblZ4VkVJKzZkCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJlRENDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdFkyeHAKWlc1MExXTmhRREUzTURneE1EazROekF3SGhjTk1qUXdNakUyTVRnMU56VXdXaGNOTXpRd01qRXpNVGcxTnpVdwpXakFqTVNFd0h3WURWUVFEREJock0zTXRZMnhwWlc1MExXTmhRREUzTURneE1EazROekF3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFUR3VZcmN6eVRsc09sUkRNMm5JRmFXUDVyeHdRRHp2amk5dkhDQ2grNnYKb1V0V3NsRWF3YnNqQjByVkZPdzYvQzZoZEgxQWNwVVE4TmM4K3RQQ2FJdFdvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVUJ3T2hqTkJsQ2RGTzNLc2hGaWhGCnUrM2s0bDR3Q2dZSUtvWkl6ajBFQXdJRFNRQXdSZ0loQU1xaTc5OHhoK3lwN3NrU3RtYmVuNFVuOGQ5bDU3SXAKck9NLzRaUU9CUnBNQWlFQXB2RUQxV3poVEVaZmt2a1hheTZrZlYvM2pLYXZEcnkrcG1LZVA0LzNpU2M9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
client-key-data: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUNjenNWazJ0ZCtzL1hVTGhNZ1BUbDVCVnpFYThPZ0hxWWViQTZRcXlOVTNvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFYWFKaEFhcjZyeHlzTUVQT1g2TDkyaWxuak5vQ0xtckFRRVBGMHdDeFptYnRQeHhZNm1IUAp1Ylh0bGF0aXVhVDNzUmk1K3YvV3Vac01ZNkdUeUdWQVpRPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=

View File

@ -0,0 +1,20 @@
apiVersion: v1
clusters:
- cluster:
insecure-skip-tls-verify: true
server: https://g.themblem.com:6443
name: default
contexts:
- context:
cluster: default
namespace: emblem
user: default
name: default
current-context: default
kind: Config
preferences: {}
users:
- name: default
user:
client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrRENDQVRlZ0F3SUJBZ0lJRnlqb0ZiODFyTDR3Q2dZSUtvWkl6ajBFQXdJd0l6RWhNQjhHQTFVRUF3d1kKYXpOekxXTnNhV1Z1ZEMxallVQXhOekEwTkRRMU16QTBNQjRYRFRJME1ERXdOVEE1TURFME5Gb1hEVEkyTURJeApOREl5TXpBMU1Gb3dNREVYTUJVR0ExVUVDaE1PYzNsemRHVnRPbTFoYzNSbGNuTXhGVEFUQmdOVkJBTVRESE41CmMzUmxiVHBoWkcxcGJqQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJFRDREb0VCK042ZjN3cHIKWUpsZmpKN2puTnQxZmIvUi9laHpVSXNqNE41Smo3UVU2Z0MrQnUyUnUwRnczL2k1ZTdrODZqUU9LVjhicDBsVAptWHRHL25TalNEQkdNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBakFmCkJnTlZIU01FR0RBV2dCVFNBRUk1dml6N1RQUlBmYUZpcm1jY0RQREVZVEFLQmdncWhrak9QUVFEQWdOSEFEQkUKQWlCeGo5NUNGSitDZzRiL1h0VEdiZlcyOWpUYUNlNDVSNHo3cndEQ3Yvd0Vud0lnUk9ZMHBFdjFMdnNZdEpnOQphZ2dEMkhTVDRMUnBlZTQrNlZhUTRkT3k0RVU9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdFkyeHAKWlc1MExXTmhRREUzTURRME5EVXpNRFF3SGhjTk1qUXdNVEExTURrd01UUTBXaGNOTXpRd01UQXlNRGt3TVRRMApXakFqTVNFd0h3WURWUVFEREJock0zTXRZMnhwWlc1MExXTmhRREUzTURRME5EVXpNRFF3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFRMituSHNmRzl2ZDcwc0RGUVh2aHFJTUZCbDlIMEh3TTFuNm8xZytEcFcKeWRwVisvSnEvSk9IZXNPT3VJWm5Pdi85bTZWemlmZE1QOTdXL1hBTWNWV0NvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVTBnQkNPYjRzKzB6MFQzMmhZcTVuCkhBend4R0V3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUlnQTdVMlVPOGdCVVNnMENEUnIzZlYwSEhDQTVxclIydzgKMkxVTnRtR0JSaXdDSVFEcVJZRXpxRHVTTXVSanlwU2JWcHYzbGNiSTIxOGhjUXV6eW1BcSs1czJQQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
client-key-data: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSURpam90Y2J6c3c0cE1rQlljZGFEQUlwVGJIRDNFY1F0L1pjbitDenpOMHdvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFUVBnT2dRSDQzcC9mQ210Z21WK01udU9jMjNWOXY5SDk2SE5RaXlQZzNrbVB0QlRxQUw0Rwo3Wkc3UVhEZitMbDd1VHpxTkE0cFh4dW5TVk9aZTBiK2RBPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=

View File

@ -0,0 +1,6 @@
{
"exclude": [
"data",
"models"
]
}

39
emblem5/scripts/copy-pos.py Executable file
View File

@ -0,0 +1,39 @@
#!/usr/bin/env python3
import os
import shutil
import json
import argparse
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument('--num-samples', '-n', type=int, default=100)
return parser.parse_args()
def main():
args = parse_args()
scan_samples = [x for x in os.listdir('data/samples') if 'scan-' in x]
scan_samples = sorted(scan_samples, reverse=True)
todo = args.num_samples
for scan in scan_samples:
if todo <= 0:
break
scan_dir = f'data/samples/{scan}'
md_file = f'data/samples/{scan}/metadata.json'
md = json.load(open(md_file))
scan_id = scan.split('-')[-1]
targetd = f'/data/emblem/dataset/pos/scans/{scan_id}'
if os.path.exists(targetd):
continue
if 'pos' in md['labels']:
full_sbs_jpg = f'{scan_dir}/full-sbs.jpg'
if os.path.exists(full_sbs_jpg):
os.makedirs(targetd, exist_ok=True)
print(f'Copying {full_sbs_jpg} => {targetd}/fullqrsbs.jpg')
shutil.copy(full_sbs_jpg, f'{targetd}/fullqrsbs.jpg')
print(f'Copying {md_file} => {targetd}/metadata.json')
shutil.copy(md_file, f'{targetd}/metadata.json')
todo -= 1
if __name__ == '__main__':
main()

175
emblem5/scripts/emcli Executable file
View File

@ -0,0 +1,175 @@
#!/usr/bin/env python3
import os
import sys
import json
import requests
import argparse
class SubCommand(object):
""" Base class of subcommand"""
help = ""
aliases = []
want_argv = False # Whether the command accepts extra arguments
envs = {
'prod': {
'server': 'https://themblem.com',
'token': '3ebd8c33-f46e-4b06-bda8-4c0f5f5eb530',
},
'dev': {
'server': 'https://dev.themblem.com',
'token': 'D91AB64B-C5CA-4B78-AC76-01722B8C8A5C',
},
}
def setup_args(self, parser):
pass
def do(self, args, argv):
"""Do command"""
print("Not implemented")
def get_env(self):
return self.envs[self.args.env]
def get_server(self):
return self.get_env()['server']
def make_headers(self):
return {
'Authorization': 'token ' + self.get_env()['token'],
}
class ActivateCommand(SubCommand):
name = "activate"
want_argv = True
help = "Activate code"
def setup_args(self, parser):
pass
def do(self, args, argv):
for i in argv:
print(i)
self.activate(i)
def activate(self, code):
server = self.get_server()
pk = self.get_pk(code)
url = f'{server}/api/v1/code/{pk}/'
r = requests.patch(url, headers=self.make_headers(), json={
'is_active': True,
})
print(r.text)
def get_pk(self, code):
server = self.get_server()
url = f'{server}/api/v1/code/?code=' + code
r = requests.get(url, headers=self.make_headers())
r = r.json()
return r['objects'][0]['id']
class GetScanDataCommand(SubCommand):
name = "get-scan-data"
want_argv = True
help = "Get scan data from api"
def setup_args(self, parser):
parser.add_argument("--output", "-o", required=True)
def do(self, args, argv):
os.makedirs(args.output, exist_ok=True)
for i in argv:
sd = self.get_scan_data(i)
with open(f"{args.output}/metadata.json", "w") as f:
f.write(json.dumps(sd, indent=2) + "\n")
with open(f"{args.output}/frame.jpg", "wb") as f:
f.write(self.get_image(sd['image']))
def get_scan_data(self, i):
server = self.get_server()
url = f'{server}/api/v1/scan-data/{i}/'
print(url)
r = requests.get(url, headers=self.make_headers())
return r.json()
def get_image(self, name):
server = self.get_server()
token = self.get_env()['token']
url = f'{server}/api/v1/oss-image/?token={token}&name={name}'
r = requests.get(url)
r.raise_for_status()
return r.content
def get_roi(self, code):
server = self.get_server()
token = self.get_env()['token']
url = f'{server}/api/v1/code-feature-roi/?token={token}&code={code}'
r = requests.get(url)
if r.status_code == 404:
return None
return r.content
class UploadRoiCommand(SubCommand):
name = "upload-roi"
want_argv = True
help = "Upload roi, filename should be {code}.jpg"
def setup_args(self, parser):
parser.add_argument("file", action="append")
def do(self, args, argv):
for f in args.file:
print(f)
url = self.get_server() + "/api/v1/code-feature-roi/"
print(url)
r = requests.post(url, headers=self.make_headers(), files={
f: open(f, 'rb'),
})
print(r.text)
class UserInfoCommand(SubCommand):
name = "userinfo"
want_argv = True
help = "Call the userinfo API"
def setup_args(self, parser):
pass
def do(self, args, argv):
server = self.get_server()
url = f'{server}/api/v1/userinfo/'
r = requests.get(url, headers=self.make_headers())
r.raise_for_status()
print(r.json())
def global_args(parser):
parser.add_argument("--env", "-E", help="Env", default="prod")
parser.add_argument("-D", "--debug", action="store_true",
help="Enable debug output")
def main():
parser = argparse.ArgumentParser()
global_args(parser)
subparsers = parser.add_subparsers(title="subcommands")
for c in SubCommand.__subclasses__():
cmd = c()
p = subparsers.add_parser(cmd.name, aliases=cmd.aliases,
help=cmd.help)
cmd.setup_args(p)
p.set_defaults(func=cmd.do, cmdobj=cmd, all=False)
args, argv = parser.parse_known_args()
if not hasattr(args, "cmdobj"):
parser.print_usage()
return 1
if args.debug:
logging.basicConfig(level=logging.DEBUG)
if argv and not args.cmdobj.want_argv:
raise Exception("Unrecognized arguments:\n" + argv[0])
args.cmdobj.args = args
r = args.func(args, argv)
return r
if __name__ == '__main__':
sys.exit(main())

23
emblem5/scripts/qrs.py Executable file
View File

@ -0,0 +1,23 @@
#!/usr/bin/env python3
import os
def mv_qr(src, dst):
basename = os.path.basename(src)
if len(basename) < 2:
raise Exception('invalid basename: %s' % basename)
prefix = basename[:2]
dd = os.path.join(dst, prefix)
os.makedirs(dd, exist_ok=True)
print("%s => %s" % (src, os.path.join(dd, basename)))
os.rename(src, os.path.join(dd, basename))
def main():
src = '/data/qrs/GYCY-241216-119-02'
dst = '/data/qrs/tree'
for r, ds, fs in os.walk(src):
for f in fs:
if f.endswith('.jpg'):
mv_qr(os.path.join(r, f), dst)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,41 @@
#!/usr/bin/env python3
import os
import sys
import shutil
import json
import random
def main():
dataset_base = '/data/dataset'
shutil.rmtree(dataset_base + '/pos')
shutil.rmtree(dataset_base + '/neg')
os.makedirs(dataset_base + '/pos', exist_ok=True)
os.makedirs(dataset_base + '/neg', exist_ok=True)
all_samples = os.listdir('data/samples')
random.shuffle(all_samples)
for sample in all_samples[:1000]:
newname = f'{sample}.jpg'
md_name = f'data/samples/{sample}/metadata.json'
pos_or_neg = None
if 'pos' in sample:
pos_or_neg = 'pos'
elif 'neg' in sample:
pos_or_neg = 'neg'
elif os.path.exists(md_name):
with open(md_name, 'r') as f:
md = json.load(f)
if 'pos' in md['labels']:
pos_or_neg = 'pos'
elif 'neg' in md['labels']:
pos_or_neg = 'neg'
if not pos_or_neg:
continue
src = f'data/samples/{sample}/full-sbs.jpg'
if not os.path.exists(src):
continue
print(src)
shutil.copy(src, os.path.join(dataset_base, pos_or_neg, newname))
if __name__ == '__main__':
main()

View File

@ -0,0 +1,29 @@
#!/usr/bin/env python3
import os
import sys
import json
import shutil
for root, dirs, files in os.walk('data/frames/v5'):
for f in files:
if not f.endswith('-side-by-side.jpg'):
continue
frame_idx = f.split('-')[0]
parent_dir = os.path.basename(root)
outd = f'/data/samples/frame-{parent_dir}-{frame_idx}'
os.makedirs(outd, exist_ok=True)
frame_file = os.path.join(root, f)
if os.path.exists(frame_file):
shutil.copy(frame_file, os.path.join(outd, 'frame.jpg'))
qr_file = os.path.join(root, f'{frame_idx}-qr.jpg')
if os.path.exists(qr_file):
shutil.copy(qr_file, os.path.join(outd, 'frame-qr.jpg'))
orig_file = os.path.join(root, f'{frame_idx}-orig.jpg')
if os.path.exists(orig_file):
shutil.copy(orig_file, os.path.join(outd, 'std.jpg'))
side_by_side_file = os.path.join(root, f'{frame_idx}-side-by-side.jpg')
if os.path.exists(side_by_side_file):
shutil.copy(side_by_side_file, os.path.join(outd, 'side-by-side.jpg'))
metadata_file = os.path.join(root, f'{frame_idx}')
shutil.copy(metadata_file, os.path.join(outd, 'metadata.json'))

View File

@ -0,0 +1,22 @@
#!/bin/bash
for scan_id in $(ls data/scans); do
echo $scan_id
outd=data/samples/scan-${scan_id}
mkdir -p data/samples/scan-${scan_id}
# data/scans/80423/80423.txt
# data/scans/80423/80423-std.jpg
# data/scans/80423/80423-qr-side-by-side.jpg
# data/scans/80423/80423-frame.jpg
# data/scans/80423/80423.json
# data/scans/80423/80423-roi.jpg
# data/scans/80423/80423-std-qr.jpg
# data/scans/80423/80423-qr.jpg
mv data/scans/${scan_id}/${scan_id}.json $outd/metadata.json
mv data/scans/${scan_id}/${scan_id}-frame.jpg $outd/frame.jpg
mv data/scans/${scan_id}/${scan_id}-qr.jpg $outd/frame-qr.jpg
mv data/scans/${scan_id}/${scan_id}-std.jpg $outd/std.jpg
mv data/scans/${scan_id}/${scan_id}-std-qr.jpg $outd/std-qr.jpg
mv data/scans/${scan_id}/${scan_id}-qr-side-by-side.jpg $outd/side-by-side.jpg
done

49
emblem5/scripts/upload-qrs.py Executable file
View File

@ -0,0 +1,49 @@
#!/usr/bin/env python3
import oss2
import sys
import os
import time
import argparse
oss_ak = 'LTAI5tC2qXGxwHZUZP7DoD1A'
oss_sk = 'qPo9O6ZvEfqo4t8oflGEm0DoxLHJhm'
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument('--dir', '-d', type=str, default='/data/qrs/tree', help='directory of qrs')
return parser.parse_args()
def all_qrs(dir):
for r, ds, fs in os.walk(dir):
for f in fs:
if f.endswith('.jpg'):
yield os.path.join(r, f)
def main():
args = parse_args()
auth = oss2.Auth(oss_ak, oss_sk)
endpoint = 'https://oss-cn-guangzhou.aliyuncs.com'
bucket = oss2.Bucket(auth, endpoint, 'emblem-qrs')
rootdir = args.dir
aqrs = list(all_qrs(rootdir))
total = len(aqrs)
done = 0
print(f'total: {total}')
for qr in aqrs:
qrcode = os.path.basename(qr).split('.')[0]
prefix = qrcode[:2]
key = f'v5/{prefix}/{qrcode}.jpg'
for i in range(3):
try:
print(f'{done}/{total} {qr} -> {key}')
bucket.put_object_from_file(key, qr)
done += 1
break
except Exception as e:
print(f'{e}')
time.sleep(1)
if __name__ == '__main__':
main()

38
emblem5/scripts/vis.py Executable file
View File

@ -0,0 +1,38 @@
#!/usr/bin/env python3
import flask
import subprocess
app = flask.Flask(__name__)
def show_frames():
cmd = '''find data/frames/v5/*neg -name '*-qr.jpg' | sort -R | head -n 100'''
lines = subprocess.check_output(cmd, shell=True).decode().splitlines()
ret = '<div class="frames">'
for line in lines:
ret += f'<img src="{line}" />'
ret += '</div>'
return ret
@app.route('/')
def index():
ret = '''
<html>
<head>
<link rel="stylesheet" href="/data/css/style.css">
</head>
<body>
'''
ret += show_frames()
ret += '''
</body>
</html>
'''
return ret
@app.route('/data/<path:path>')
def data(path):
return flask.send_file('../data/' + path)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080, debug=True)

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -0,0 +1,403 @@
layer {
name: "data"
type: "Input"
top: "data"
input_param {
shape {
dim: 1
dim: 1
dim: 224
dim: 224
}
}
}
layer {
name: "conv0"
type: "Convolution"
bottom: "data"
top: "conv0"
param {
lr_mult: 1.0
decay_mult: 1.0
}
param {
lr_mult: 1.0
decay_mult: 0.0
}
convolution_param {
num_output: 32
bias_term: true
pad: 1
kernel_size: 3
group: 1
stride: 1
weight_filler {
type: "msra"
}
}
}
layer {
name: "conv0/lrelu"
type: "ReLU"
bottom: "conv0"
top: "conv0"
relu_param {
negative_slope: 0.05000000074505806
}
}
layer {
name: "db1/reduce"
type: "Convolution"
bottom: "conv0"
top: "db1/reduce"
param {
lr_mult: 1.0
decay_mult: 1.0
}
param {
lr_mult: 1.0
decay_mult: 0.0
}
convolution_param {
num_output: 8
bias_term: true
pad: 0
kernel_size: 1
group: 1
stride: 1
weight_filler {
type: "msra"
}
}
}
layer {
name: "db1/reduce/lrelu"
type: "ReLU"
bottom: "db1/reduce"
top: "db1/reduce"
relu_param {
negative_slope: 0.05000000074505806
}
}
layer {
name: "db1/3x3"
type: "Convolution"
bottom: "db1/reduce"
top: "db1/3x3"
param {
lr_mult: 1.0
decay_mult: 1.0
}
param {
lr_mult: 1.0
decay_mult: 0.0
}
convolution_param {
num_output: 8
bias_term: true
pad: 1
kernel_size: 3
group: 8
stride: 1
weight_filler {
type: "msra"
}
}
}
layer {
name: "db1/3x3/lrelu"
type: "ReLU"
bottom: "db1/3x3"
top: "db1/3x3"
relu_param {
negative_slope: 0.05000000074505806
}
}
layer {
name: "db1/1x1"
type: "Convolution"
bottom: "db1/3x3"
top: "db1/1x1"
param {
lr_mult: 1.0
decay_mult: 1.0
}
param {
lr_mult: 1.0
decay_mult: 0.0
}
convolution_param {
num_output: 32
bias_term: true
pad: 0
kernel_size: 1
group: 1
stride: 1
weight_filler {
type: "msra"
}
}
}
layer {
name: "db1/1x1/lrelu"
type: "ReLU"
bottom: "db1/1x1"
top: "db1/1x1"
relu_param {
negative_slope: 0.05000000074505806
}
}
layer {
name: "db1/concat"
type: "Concat"
bottom: "conv0"
bottom: "db1/1x1"
top: "db1/concat"
concat_param {
axis: 1
}
}
layer {
name: "db2/reduce"
type: "Convolution"
bottom: "db1/concat"
top: "db2/reduce"
param {
lr_mult: 1.0
decay_mult: 1.0
}
param {
lr_mult: 1.0
decay_mult: 0.0
}
convolution_param {
num_output: 8
bias_term: true
pad: 0
kernel_size: 1
group: 1
stride: 1
weight_filler {
type: "msra"
}
}
}
layer {
name: "db2/reduce/lrelu"
type: "ReLU"
bottom: "db2/reduce"
top: "db2/reduce"
relu_param {
negative_slope: 0.05000000074505806
}
}
layer {
name: "db2/3x3"
type: "Convolution"
bottom: "db2/reduce"
top: "db2/3x3"
param {
lr_mult: 1.0
decay_mult: 1.0
}
param {
lr_mult: 1.0
decay_mult: 0.0
}
convolution_param {
num_output: 8
bias_term: true
pad: 1
kernel_size: 3
group: 8
stride: 1
weight_filler {
type: "msra"
}
}
}
layer {
name: "db2/3x3/lrelu"
type: "ReLU"
bottom: "db2/3x3"
top: "db2/3x3"
relu_param {
negative_slope: 0.05000000074505806
}
}
layer {
name: "db2/1x1"
type: "Convolution"
bottom: "db2/3x3"
top: "db2/1x1"
param {
lr_mult: 1.0
decay_mult: 1.0
}
param {
lr_mult: 1.0
decay_mult: 0.0
}
convolution_param {
num_output: 32
bias_term: true
pad: 0
kernel_size: 1
group: 1
stride: 1
weight_filler {
type: "msra"
}
}
}
layer {
name: "db2/1x1/lrelu"
type: "ReLU"
bottom: "db2/1x1"
top: "db2/1x1"
relu_param {
negative_slope: 0.05000000074505806
}
}
layer {
name: "db2/concat"
type: "Concat"
bottom: "db1/concat"
bottom: "db2/1x1"
top: "db2/concat"
concat_param {
axis: 1
}
}
layer {
name: "upsample/reduce"
type: "Convolution"
bottom: "db2/concat"
top: "upsample/reduce"
param {
lr_mult: 1.0
decay_mult: 1.0
}
param {
lr_mult: 1.0
decay_mult: 0.0
}
convolution_param {
num_output: 32
bias_term: true
pad: 0
kernel_size: 1
group: 1
stride: 1
weight_filler {
type: "msra"
}
}
}
layer {
name: "upsample/reduce/lrelu"
type: "ReLU"
bottom: "upsample/reduce"
top: "upsample/reduce"
relu_param {
negative_slope: 0.05000000074505806
}
}
layer {
name: "upsample/deconv"
type: "Deconvolution"
bottom: "upsample/reduce"
top: "upsample/deconv"
param {
lr_mult: 1.0
decay_mult: 1.0
}
param {
lr_mult: 1.0
decay_mult: 0.0
}
convolution_param {
num_output: 32
bias_term: true
pad: 1
kernel_size: 3
group: 32
stride: 2
weight_filler {
type: "msra"
}
}
}
layer {
name: "upsample/lrelu"
type: "ReLU"
bottom: "upsample/deconv"
top: "upsample/deconv"
relu_param {
negative_slope: 0.05000000074505806
}
}
layer {
name: "upsample/rec"
type: "Convolution"
bottom: "upsample/deconv"
top: "upsample/rec"
param {
lr_mult: 1.0
decay_mult: 1.0
}
param {
lr_mult: 1.0
decay_mult: 0.0
}
convolution_param {
num_output: 1
bias_term: true
pad: 0
kernel_size: 1
group: 1
stride: 1
weight_filler {
type: "msra"
}
}
}
layer {
name: "nearest"
type: "Deconvolution"
bottom: "data"
top: "nearest"
param {
lr_mult: 0.0
decay_mult: 0.0
}
convolution_param {
num_output: 1
bias_term: false
pad: 0
kernel_size: 2
group: 1
stride: 2
weight_filler {
type: "constant"
value: 1.0
}
}
}
layer {
name: "Crop1"
type: "Crop"
bottom: "nearest"
bottom: "upsample/rec"
top: "Crop1"
}
layer {
name: "fc"
type: "Eltwise"
bottom: "Crop1"
bottom: "upsample/rec"
top: "fc"
eltwise_param {
operation: SUM
}
}