add emblem5
This commit is contained in:
parent
b0585d16a0
commit
4f8e3973a8
1
emblem5/.cursorignore
Normal file
1
emblem5/.cursorignore
Normal file
@ -0,0 +1 @@
|
||||
/data
|
||||
7
emblem5/.gitignore
vendored
Normal file
7
emblem5/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
/venv
|
||||
/data
|
||||
/models
|
||||
/archive
|
||||
/.ipynb_checkpoints
|
||||
/reports
|
||||
/dataset
|
||||
48
emblem5/.gitlab-ci.yml
Normal file
48
emblem5/.gitlab-ci.yml
Normal 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
73
emblem5/Makefile
Normal 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
8
emblem5/ai/Dockerfile
Normal 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
17
emblem5/ai/Makefile
Normal 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
41
emblem5/ai/browser.py
Normal 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
791
emblem5/ai/common.py
Normal 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
15
emblem5/ai/download.py
Executable 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
149
emblem5/ai/fetch-scans.py
Executable 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
65
emblem5/ai/make-sbs.py
Executable 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
26
emblem5/ai/ossclient.py
Normal 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
328
emblem5/ai/server.py
Executable 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
545
emblem5/ai/train2.py
Normal 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
15
emblem5/ai/upload.py
Executable 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)
|
||||
|
||||
167
emblem5/ai/validate-codes.txt
Normal file
167
emblem5/ai/validate-codes.txt
Normal 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
349
emblem5/ai/verify2.py
Executable 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()
|
||||
BIN
emblem5/ai/wechat_qrcode/detect.caffemodel
Normal file
BIN
emblem5/ai/wechat_qrcode/detect.caffemodel
Normal file
Binary file not shown.
2716
emblem5/ai/wechat_qrcode/detect.prototxt
Normal file
2716
emblem5/ai/wechat_qrcode/detect.prototxt
Normal file
File diff suppressed because it is too large
Load Diff
BIN
emblem5/ai/wechat_qrcode/sr.caffemodel
Normal file
BIN
emblem5/ai/wechat_qrcode/sr.caffemodel
Normal file
Binary file not shown.
403
emblem5/ai/wechat_qrcode/sr.prototxt
Normal file
403
emblem5/ai/wechat_qrcode/sr.prototxt
Normal 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
|
||||
}
|
||||
}
|
||||
14
emblem5/baseimg/Dockerfile
Normal file
14
emblem5/baseimg/Dockerfile
Normal 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
10
emblem5/baseimg/Makefile
Normal 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)
|
||||
15
emblem5/baseimg/requirements.txt
Normal file
15
emblem5/baseimg/requirements.txt
Normal file
@ -0,0 +1,15 @@
|
||||
tqdm
|
||||
loguru
|
||||
pillow
|
||||
numpy
|
||||
pandas
|
||||
matplotlib
|
||||
scikit-learn
|
||||
scipy
|
||||
seaborn
|
||||
flask
|
||||
oss2
|
||||
requests
|
||||
kornia
|
||||
opencv-python
|
||||
opencv-contrib-python
|
||||
12
emblem5/baseimg/ubuntu.sources
Normal file
12
emblem5/baseimg/ubuntu.sources
Normal 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
|
||||
114
emblem5/deploy/all.yaml.tmpl
Normal file
114
emblem5/deploy/all.yaml.tmpl
Normal 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
|
||||
1135
emblem5/deploy/elastic-agent.yml
Normal file
1135
emblem5/deploy/elastic-agent.yml
Normal file
File diff suppressed because it is too large
Load Diff
20
emblem5/deploy/kubeconfig.dev
Normal file
20
emblem5/deploy/kubeconfig.dev
Normal 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=
|
||||
20
emblem5/deploy/kubeconfig.emblem-s1
Normal file
20
emblem5/deploy/kubeconfig.emblem-s1
Normal 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=
|
||||
20
emblem5/deploy/kubeconfig.themblem
Normal file
20
emblem5/deploy/kubeconfig.themblem
Normal 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=
|
||||
6
emblem5/pyrightconfig.json
Normal file
6
emblem5/pyrightconfig.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"exclude": [
|
||||
"data",
|
||||
"models"
|
||||
]
|
||||
}
|
||||
39
emblem5/scripts/copy-pos.py
Executable file
39
emblem5/scripts/copy-pos.py
Executable 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
175
emblem5/scripts/emcli
Executable 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
23
emblem5/scripts/qrs.py
Executable 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()
|
||||
41
emblem5/scripts/refresh-dataset.py
Executable file
41
emblem5/scripts/refresh-dataset.py
Executable 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()
|
||||
29
emblem5/scripts/reorg-frames.py
Normal file
29
emblem5/scripts/reorg-frames.py
Normal 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'))
|
||||
22
emblem5/scripts/reorganize.sh
Normal file
22
emblem5/scripts/reorganize.sh
Normal 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
49
emblem5/scripts/upload-qrs.py
Executable 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
38
emblem5/scripts/vis.py
Executable 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)
|
||||
BIN
emblem5/wechat_qrcode/detect.caffemodel
Normal file
BIN
emblem5/wechat_qrcode/detect.caffemodel
Normal file
Binary file not shown.
2716
emblem5/wechat_qrcode/detect.prototxt
Normal file
2716
emblem5/wechat_qrcode/detect.prototxt
Normal file
File diff suppressed because it is too large
Load Diff
BIN
emblem5/wechat_qrcode/sr.caffemodel
Normal file
BIN
emblem5/wechat_qrcode/sr.caffemodel
Normal file
Binary file not shown.
403
emblem5/wechat_qrcode/sr.prototxt
Normal file
403
emblem5/wechat_qrcode/sr.prototxt
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user