initial commit
This commit is contained in:
commit
8a4f10aeed
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@ -0,0 +1,2 @@
|
||||
./alg/opencv
|
||||
.git
|
||||
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
build
|
||||
/venv
|
||||
/api/api/static/
|
||||
/opencv
|
||||
/emtest/target
|
||||
/dataset/local
|
||||
/detection/model
|
||||
102
.gitlab-ci.yml
Normal file
102
.gitlab-ci.yml
Normal file
@ -0,0 +1,102 @@
|
||||
stages:
|
||||
- test-and-build
|
||||
- build-docker
|
||||
- deploy
|
||||
|
||||
cache:
|
||||
key: one-key-to-rule-them-all
|
||||
paths:
|
||||
- opencv/src
|
||||
- opencv/contrib
|
||||
- emtest/target
|
||||
- venv
|
||||
|
||||
test:
|
||||
stage: test-and-build
|
||||
tags:
|
||||
- i7
|
||||
before_script:
|
||||
- if ! test -d venv; then python3 -m venv venv; fi
|
||||
- source venv/bin/activate
|
||||
- pip3 install -r requirements.txt
|
||||
script:
|
||||
- make opencv
|
||||
- make -C alg qrtool
|
||||
- make test
|
||||
|
||||
build-alg:
|
||||
stage: test-and-build
|
||||
tags:
|
||||
- i7
|
||||
script:
|
||||
- make opencv
|
||||
- make build/alg/qrtool
|
||||
artifacts:
|
||||
paths:
|
||||
- build
|
||||
- alg/qrtool
|
||||
|
||||
build-web:
|
||||
stage: test-and-build
|
||||
tags:
|
||||
- i7
|
||||
before_script:
|
||||
- (cd web; npm install)
|
||||
script:
|
||||
- make web
|
||||
artifacts:
|
||||
paths:
|
||||
- build
|
||||
|
||||
build-docker:
|
||||
stage: build-docker
|
||||
tags:
|
||||
- i7
|
||||
script:
|
||||
- make docker-build
|
||||
- make docker-push
|
||||
dependencies:
|
||||
- build-web
|
||||
- build-alg
|
||||
except:
|
||||
- main
|
||||
|
||||
deploy-dev:
|
||||
stage: deploy
|
||||
tags:
|
||||
- i7
|
||||
only:
|
||||
- dev
|
||||
script:
|
||||
- make deploy-api-dev
|
||||
cache: []
|
||||
|
||||
dev-smoke:
|
||||
stage: test-and-build
|
||||
tags:
|
||||
- i7
|
||||
allow_failure: true
|
||||
script:
|
||||
- ./scripts/emcli --env dev activate 0074253255108
|
||||
- ./api/scripts/api_smoke.py -p $EMBLEM_CI_PASSWORD
|
||||
cache: []
|
||||
|
||||
deploy-prod:
|
||||
stage: deploy
|
||||
tags:
|
||||
- i7
|
||||
only:
|
||||
- main
|
||||
script:
|
||||
- make docker-push-prod
|
||||
- make deploy-api-prod
|
||||
cache: []
|
||||
|
||||
deploy-roi-worker:
|
||||
tags:
|
||||
- emblem-s1
|
||||
stage: deploy
|
||||
when: manual
|
||||
script:
|
||||
- make deploy-roi-worker
|
||||
cache: []
|
||||
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@ -0,0 +1,19 @@
|
||||
FROM ubuntu:22.04
|
||||
ADD packages.txt packages.txt
|
||||
RUN apt-get update -y && DEBIAN_FRONTEND=noninteractive apt-get install -y $(cat packages.txt)
|
||||
RUN pip3 install torch==1.13.0 torchvision==0.14.0 torchaudio==0.13.0
|
||||
ADD requirements.txt requirements.txt
|
||||
RUN pip3 install -r requirements.txt
|
||||
ADD detection /emblem/detection
|
||||
ADD alg /emblem/alg
|
||||
ADD api /emblem/api
|
||||
ADD web /emblem/web
|
||||
RUN cd /emblem/api/api && ./manage.py collectstatic --noinput
|
||||
RUN mkdir -p /emblem/log
|
||||
ADD scripts /emblem/scripts
|
||||
ADD nginx.conf /emblem/nginx.conf
|
||||
ADD dataset/topleft/topleft-0518.jpeg /tmp/topleft-test.jpg
|
||||
ADD nginx.conf /emblem/nginx.conf
|
||||
RUN cd /emblem/alg/ && ./qrtool topleft /tmp/topleft-test.jpg
|
||||
WORKDIR /emblem
|
||||
CMD /emblem/scripts/entrypoint
|
||||
144
Makefile
Normal file
144
Makefile
Normal file
@ -0,0 +1,144 @@
|
||||
.PHONY: FORCE
|
||||
|
||||
IMAGE_TAG := $(shell git rev-parse --short HEAD)
|
||||
IMAGE_REPO := registry.gitlab.com/euphon/themblem
|
||||
IMAGE_REPO_PROD := registry.cn-shenzhen.aliyuncs.com/emblem/themblem
|
||||
IMAGE := $(IMAGE_REPO):$(IMAGE_TAG)
|
||||
IMAGE_PROD := $(IMAGE_REPO_PROD):$(IMAGE_TAG)
|
||||
|
||||
ifeq ($(shell uname), Darwin)
|
||||
BUILD_SHARED_LIBS := ON
|
||||
else
|
||||
BUILD_SHARED_LIBS := OFF
|
||||
endif
|
||||
|
||||
API_FILES := \
|
||||
$(addprefix build/, \
|
||||
$(shell find -L \
|
||||
api/ip2region.db \
|
||||
api/api \
|
||||
api/scripts \
|
||||
-type f)\
|
||||
)
|
||||
|
||||
WEB_FILES := \
|
||||
$(addprefix build/, \
|
||||
$(shell find -L \
|
||||
web/dist \
|
||||
-type f)\
|
||||
)
|
||||
|
||||
DETECTION_FILES := \
|
||||
$(addprefix build/, \
|
||||
$(shell find -L \
|
||||
detection \
|
||||
-type f \
|
||||
-not -name '*.pyc' \
|
||||
) \
|
||||
)
|
||||
|
||||
SCRIPTS_FILES := \
|
||||
$(addprefix build/, \
|
||||
$(shell find -L \
|
||||
scripts \
|
||||
-type f \
|
||||
) \
|
||||
)
|
||||
|
||||
DATASET_FILES := \
|
||||
$(addprefix build/, \
|
||||
$(shell find -L \
|
||||
dataset \
|
||||
-name topleft-0518.jpeg \
|
||||
) \
|
||||
)
|
||||
|
||||
ALG_FILES := \
|
||||
$(addprefix build/, \
|
||||
$(shell find -L \
|
||||
alg/qrtool \
|
||||
alg/wechat_qrcode \
|
||||
) \
|
||||
)
|
||||
|
||||
docker-build: build/Dockerfile build/packages.txt build/requirements.txt \
|
||||
build/nginx.conf $(WEB_FILES) $(API_FILES) $(ALG_FILES) $(DETECTION_FILES) $(SCRIPTS_FILES) $(DATASET_FILES)
|
||||
find build
|
||||
docker build --network=host -t $(IMAGE) build
|
||||
|
||||
docker-push:
|
||||
docker push $(IMAGE)
|
||||
|
||||
docker-push-prod:
|
||||
docker tag $(IMAGE) $(IMAGE_PROD)
|
||||
docker push $(IMAGE_PROD)
|
||||
|
||||
web: FORCE
|
||||
cd web && npm run build
|
||||
mkdir -p build/web
|
||||
cp -r web/dist build/web/dist
|
||||
|
||||
build/%: %
|
||||
mkdir -p $(shell dirname $@)
|
||||
cp -a $^ $@
|
||||
|
||||
deploy-api-dev:
|
||||
curl -X POST https://euphon-alert-23358.famzheng.workers.dev/ -d 'Deploying Emblem API to dev: $(IMAGE)'
|
||||
kubectl --kubeconfig deploy/kubeconfig.dev set image deploy api emblem=$(IMAGE)
|
||||
kubectl --kubeconfig deploy/kubeconfig.dev rollout status --timeout=1h deploy api
|
||||
|
||||
deploy-api-prod:
|
||||
curl -X POST https://euphon-alert-23358.famzheng.workers.dev/ -d 'Deploying Emblem API to prod: $(IMAGE_PROD)'
|
||||
kubectl --kubeconfig deploy/kubeconfig.themblem set image deploy api emblem=$(IMAGE_PROD)
|
||||
kubectl --kubeconfig deploy/kubeconfig.themblem rollout status --timeout=1h deploy api
|
||||
|
||||
deploy-roi-worker:
|
||||
curl -X POST https://euphon-alert-23358.famzheng.workers.dev/ -d 'Deploying ROI Worker to emblem-s1: $(IMAGE)'
|
||||
kubectl --kubeconfig deploy/kubeconfig.emblem-s1 set image deploy roi-worker alg=$(IMAGE)
|
||||
kubectl --kubeconfig deploy/kubeconfig.emblem-s1 rollout status --timeout=1h deploy roi-worker
|
||||
|
||||
test: FORCE
|
||||
cd emtest && cargo test -- --nocapture
|
||||
make -C api test
|
||||
make -C detection test
|
||||
|
||||
OPENCV_TAG := 4.9.0
|
||||
opencv/src/LICENSE:
|
||||
rm -rf opencv/src opencv/contrib
|
||||
git clone --depth=1 https://github.com/opencv/opencv_contrib opencv/contrib -b $(OPENCV_TAG)
|
||||
git clone --depth=1 https://github.com/opencv/opencv opencv/src -b $(OPENCV_TAG)
|
||||
|
||||
opencv: opencv/src/LICENSE FORCE
|
||||
mkdir -p opencv/build/cpp opencv/install
|
||||
cd opencv/build/cpp && cmake \
|
||||
-D CMAKE_BUILD_TYPE=RELEASE \
|
||||
-D CMAKE_INSTALL_PREFIX=$(PWD)/opencv/install \
|
||||
-D OPENCV_GENERATE_PKGCONFIG=ON \
|
||||
-D BUILD_EXAMPLES=OFF \
|
||||
-D INSTALL_PYTHON_EXAMPLES=OFF \
|
||||
-D INSTALL_C_EXAMPLES=OFF \
|
||||
-D BUILD_TESTS=OFF \
|
||||
-D BUILD_PERF_TESTS=OFF \
|
||||
-D OPENCV_EXTRA_MODULES_PATH=$(PWD)/opencv/contrib/modules \
|
||||
-D BUILD_opencv_python2=OFF \
|
||||
-D BUILD_opencv_python3=OFF \
|
||||
-D WITH_PROTOBUF=ON \
|
||||
-D BUILD_SHARED_LIBS=$(BUILD_SHARED_LIBS) \
|
||||
-D WITH_GTK=OFF \
|
||||
-D WITH_TIFF=OFF \
|
||||
../../src
|
||||
$(MAKE) -C opencv/build/cpp
|
||||
$(MAKE) -C opencv/build/cpp install
|
||||
|
||||
opencv.js: opencv/src/LICENSE FORCE
|
||||
mkdir -p opencv/build/wasm
|
||||
python3 opencv/src/platforms/js/build_js.py opencv/build/wasm \
|
||||
--build_wasm \
|
||||
--enable_exception \
|
||||
--cmake_option="-DOPENCV_EXTRA_MODULES_PATH=$(PWD)/opencv/contrib/modules/" \
|
||||
--cmake_option="-DWITH_PROTOBUF=off" \
|
||||
--emscripten_dir=../emsdk/upstream/emscripten/ \
|
||||
--disable_single_file
|
||||
|
||||
alg/qrtool:
|
||||
make -C alg qrtool
|
||||
6
alg/.dockerignore
Normal file
6
alg/.dockerignore
Normal file
@ -0,0 +1,6 @@
|
||||
opencv/.git
|
||||
opencv/.cache
|
||||
opencv/euphon/build
|
||||
opencv/build_wasm
|
||||
.git
|
||||
/dataset/local
|
||||
15
alg/.gitignore
vendored
Normal file
15
alg/.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
/qrtool
|
||||
*.wasm
|
||||
*.wasm.*
|
||||
*.html
|
||||
qrtool.js
|
||||
*.dSYM
|
||||
.DS_Store
|
||||
qrtool.*.js
|
||||
/lib
|
||||
*.o
|
||||
*.pb.cc
|
||||
*.pb.h
|
||||
/dataset/local
|
||||
qrtool.zip
|
||||
/dataset/scan/*.roi.jpg
|
||||
6
alg/.ipynb_checkpoints/qr-checkpoint.ipynb
Normal file
6
alg/.ipynb_checkpoints/qr-checkpoint.ipynb
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"cells": [],
|
||||
"metadata": {},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
265
alg/Makefile
Normal file
265
alg/Makefile
Normal file
@ -0,0 +1,265 @@
|
||||
|
||||
.PHONY: FORCE default
|
||||
CV_DIR := $(shell pwd)/../opencv
|
||||
CV_INSTALL_DIR := $(CV_DIR)/install
|
||||
CV_WASM_DIR := $(CV_DIR)/build/wasm
|
||||
CXX := ccache g++
|
||||
CXXFLAGS := -O2 -std=c++17 -Wall -Werror -g -I$(CV_INSTALL_DIR)/include/opencv4
|
||||
|
||||
|
||||
ifeq ($(shell uname), Darwin)
|
||||
RPATH_FLAG := -Wl,-rpath,'@executable_path/lib'
|
||||
STATIC :=
|
||||
IMAGE_VIEWER := open
|
||||
else
|
||||
RPATH_FLAG := -Wl,-rpath,'$$ORIGIN/lib'
|
||||
STATIC := 1
|
||||
IMAGE_VIEWER := feh
|
||||
endif
|
||||
|
||||
USE_PULSAR :=
|
||||
|
||||
ENABLE_GRPC :=
|
||||
|
||||
START_GROUP := -Wl,--start-group
|
||||
END_GROUP := -Wl,--end-group
|
||||
|
||||
CV_PKG_CONFIG_PATH := $(shell pwd)/../opencv/install/lib/pkgconfig/
|
||||
|
||||
OPENCV_FLAGS := $(filter-out -lIconv::Iconv, \
|
||||
$(shell PKG_CONFIG_PATH=$(CV_PKG_CONFIG_PATH) pkg-config opencv4 --libs --cflags $(if $(STATIC), --static)) \
|
||||
)
|
||||
|
||||
default: qrtool qrtool.wx.wasm.br qrtool.web.js
|
||||
|
||||
qrtool: CXXFLAGS += -DWECHAT_QRCODE_USE_MODEL=1
|
||||
|
||||
qrtool: qrtool.cpp libqr.cpp \
|
||||
$(if $(USE_PULSAR), mq_worker.cpp) \
|
||||
base64.cpp mq_worker.h base64.h \
|
||||
http.o \
|
||||
$(if $(ENABLE_GRPC), fileprocess.o fileprocess.pb.o fileprocess.grpc.pb.o) \
|
||||
Makefile
|
||||
$(CXX) -o $@ \
|
||||
$(if $(STATIC), -static) \
|
||||
$(filter %.cpp %.o, $^) \
|
||||
-DQRTOOL_MAIN=1 \
|
||||
$(if $(USE_PULSAR), -lpulsar) \
|
||||
$(CXXFLAGS) \
|
||||
$(RPATH_FLAG) \
|
||||
$(if $(STATIC), $(START_GROUP) -ljbig) \
|
||||
$(OPENCV_FLAGS) \
|
||||
$(if $(STATIC), $(END_GROUP)) \
|
||||
-Wno-error=unused-function \
|
||||
|
||||
qrtool.zip: qrtool
|
||||
rm -rf qrtool.zip-workdir
|
||||
mkdir -p qrtool.zip-workdir
|
||||
cp qrtool qrtool.zip-workdir/qrtool.$(shell git describe --always).x86_64
|
||||
cd qrtool.zip-workdir && zip qrtool.zip qrtool.$(shell git describe --always).x86_64 && mv qrtool.zip ..
|
||||
rm -rf qrtool.zip-workdir
|
||||
|
||||
angle: qrtool
|
||||
./qrtool angle dataset/camera/warp-small.jpg
|
||||
|
||||
verify: qrtool
|
||||
./qrtool verify ../dataset/similarity/19000-roi.jpg ../dataset/similarity/19000.jpg
|
||||
./qrtool verify ../dataset/similarity/19006-roi.jpg ../dataset/similarity/19006.jpg
|
||||
# ./qrtool verify ../dataset/local/scan-data/19687-roi.jpg ../dataset/local/scan-data/19687-frame.jpg
|
||||
|
||||
verify-neg: qrtool
|
||||
./qrtool verify ../dataset/similarity/19000-roi.jpg ../dataset/similarity/19002.jpg
|
||||
./qrtool verify ../dataset/similarity/19006-roi.jpg ../dataset/similarity/19002.jpg
|
||||
|
||||
verify-test: D := ../dataset/local/scan-data/
|
||||
verify-test: qrtool FORCE
|
||||
for roi in $(shell ls $D | grep roi.jpg | sort -R | head -n 100); do \
|
||||
frame=$${roi/roi/frame}; \
|
||||
cmd="./qrtool verify $D/$$roi $D/$$frame"; \
|
||||
sim=$$($$cmd | grep similarity); \
|
||||
echo "<div class=case><img class=roi src=$$roi /> <img class=frame src=$$frame /><h1>$$sim</h1>"; \
|
||||
cat $D/$${roi/-roi.jpg/.txt}; \
|
||||
echo "</div>"; \
|
||||
done | tee $D/verify.html
|
||||
echo '<style> div.case { border: 1px solid green; padding: 1rem; margin: 1rem; } img.frame { width: 200px } img.roi { display: block }' >> $D/verify.html
|
||||
|
||||
rectify: qrtool
|
||||
./qrtool rectify dataset/camera/warp-small.jpg
|
||||
|
||||
roi: code=4295987837721
|
||||
roi: qrtool
|
||||
./qrtool roi dataset/scan/$(code).jpg
|
||||
$(IMAGE_VIEWER) dataset/scan/$(code).jpg.roi.jpg
|
||||
|
||||
topleft: qrtool
|
||||
./qrtool topleft ../dataset/scandata/18986.jpg
|
||||
|
||||
roi_bench: qrtool
|
||||
begin=$$(date +%s); parallel -j10 ./qrtool roi_bench -- dataset/batches/*; end=$$(date +%s); nfiles=$$(find dataset/batches -type f | wc -l); echo total qps: $$((nfiles / (end - begin)))
|
||||
|
||||
imdecode: qrtool
|
||||
./qrtool imdecode dataset/camera/warp-small.jpg
|
||||
|
||||
angle-bench: qrtool
|
||||
time -f %e $(SHELL) -c '\
|
||||
for i in $(shell seq 100); do \
|
||||
$(library_path_prefix) \
|
||||
./qrtool angle dataset/camera/warp-small.jpg; \
|
||||
done'
|
||||
|
||||
neg: qrtool
|
||||
set -e; \
|
||||
for img in $(wildcard dataset/negative/*.jpg); do \
|
||||
if ./qrtool angle $$img; then echo "negative image check failed: $$img"; exit 1; fi; \
|
||||
done
|
||||
|
||||
detect: qrtool
|
||||
./qrtool detect dataset/camera/warp-small.jpg
|
||||
detect2: qrtool
|
||||
./qrtool detect2 dataset/camera/warp-small.jpg
|
||||
|
||||
check: qrtool
|
||||
./qrtool check dataset/camera/warp-small.jpg
|
||||
|
||||
diagonal: qrtool
|
||||
./qrtool diagonal dataset/camera/warp-small.jpg
|
||||
|
||||
bench: qrtool
|
||||
./qrtool bench dataset/camera/warp-small.jpg
|
||||
|
||||
memory: qrtool
|
||||
valgrind ./qrtool bench dataset/camera/warp-small.jpg
|
||||
|
||||
fileprocess.o: fileprocess.grpc.pb.h
|
||||
|
||||
worker: qrtool
|
||||
./qrtool roi_worker roi
|
||||
|
||||
worker_nop: qrtool
|
||||
./qrtool roi_worker_nop roi
|
||||
|
||||
workers: qrtool
|
||||
parallel -j8 ./qrtool roi_worker -- roi roi roi roi roi roi roi roi
|
||||
|
||||
grpc: qrtool
|
||||
./qrtool grpc_server 0.0.0.0:32439
|
||||
|
||||
energy: qrtool FORCE
|
||||
$(library_path_prefix) \
|
||||
./qrtool energy dataset/roi/20231226/roi-1703563444.7468174.png
|
||||
|
||||
energy.html: qrtool FORCE
|
||||
ls dataset/roi/20231224/* | sort -R | head -n 100 | while read x; do \
|
||||
echo -n "<div>"; \
|
||||
$(library_path_prefix) \
|
||||
./qrtool energy $$x | tr -d '\n'; \
|
||||
echo -n " <img src=\"$$x\" />"; \
|
||||
echo "</div>"; \
|
||||
done | sort -rn -k 2 | tee $@
|
||||
google-chrome $@
|
||||
|
||||
hist.html: qrtool FORCE
|
||||
ls dataset/roi/20231224/* | grep -v hist | sort -R | head -n 100 | while read x; do \
|
||||
echo -n "<div>$$x"; \
|
||||
echo -n " <img src=\"$$x\" /><img height=60 width=60 src=\""; \
|
||||
$(library_path_prefix) \
|
||||
./qrtool $$x | tr -d '\n'; \
|
||||
echo "\"/></div>"; \
|
||||
done | sort -rn -k 2 | tee $@
|
||||
|
||||
dft.html: qrtool FORCE
|
||||
ls dataset/roi/20231224/* | grep -v dft | \
|
||||
while read x; do \
|
||||
echo -n "<div>$$x"; \
|
||||
echo -n " <img src=\"$$x\" /><img height=60 width=60 src=\""; \
|
||||
$(library_path_prefix) \
|
||||
./qrtool $$x | tr -d '\n'; \
|
||||
echo "\"/></div>"; \
|
||||
done | tee $@
|
||||
|
||||
dft-compare.html: qrtool FORCE
|
||||
(echo dataset/roi/20231224/roi-1703295645.003432.png; echo dataset/roi/20231224/roi-1703295645.365394.png) | \
|
||||
while read x; do \
|
||||
echo -n "<div>$$x"; \
|
||||
echo -n " <img src=\"$$x\" /><img height=60 width=60 src=\""; \
|
||||
$(library_path_prefix) \
|
||||
./qrtool $$x | tr -d '\n'; \
|
||||
echo "\"/></div>"; \
|
||||
done | sort -rn -k 2 | tee $@
|
||||
google-chrome $@
|
||||
|
||||
dft: qrtool FORCE
|
||||
$(library_path_prefix) \
|
||||
./qrtool dataset/camera/warp-small.jpg
|
||||
|
||||
qrtool.web.js: EMCC_FLAGS := \
|
||||
-O3
|
||||
|
||||
qrtool.wx.js: EMCC_FLAGS := \
|
||||
-O3 \
|
||||
--pre-js pre.wx.js \
|
||||
--post-js post.wx.js
|
||||
|
||||
SIMD128_OPTS := -msimd128
|
||||
SIMD128_OPTS :=
|
||||
|
||||
qrtool.wx.js: pre.wx.js post.wx.js
|
||||
qrtool.wx.js qrtool.web.js: qrtool_wasm.cpp libqr.cpp Makefile
|
||||
emcc \
|
||||
-o $@ -I$(CV_INSTALL_DIR)/include/opencv4 $(filter %.cpp, $^) \
|
||||
$(addprefix $(CV_WASM_DIR)/lib/, \
|
||||
libopencv_core.a \
|
||||
libopencv_dnn.a \
|
||||
libopencv_imgproc.a \
|
||||
libopencv_wechat_qrcode.a \
|
||||
) \
|
||||
$(addprefix $(CV_WASM_DIR)/3rdparty/lib/, \
|
||||
libzlib.a) \
|
||||
'-sEXPORTED_FUNCTIONS=["_qrtool_angle","_malloc","_free"]' \
|
||||
'-sEXPORTED_RUNTIME_METHODS=["ccall","cwrap","_wasm_call_ctors"]' \
|
||||
"-sMIN_CHROME_VERSION=73" \
|
||||
"-sMIN_SAFARI_VERSION=140100" \
|
||||
-sALLOW_MEMORY_GROWTH=1 \
|
||||
-sENVIRONMENT=web \
|
||||
$(SIMD128_OPTS) \
|
||||
-std=c++17 \
|
||||
-fexceptions \
|
||||
-lembind \
|
||||
-g1 \
|
||||
-sWASM=1 \
|
||||
$(EMCC_FLAGS)
|
||||
|
||||
qrtool.wx.wasm.br: qrtool.wx.js
|
||||
brotli -kf qrtool.wx.wasm
|
||||
|
||||
opencv: FORCE
|
||||
./opencv/euphon/build-cpp.sh
|
||||
|
||||
wasm: FORCE
|
||||
./opencv/euphon/build-wasm.sh
|
||||
|
||||
serve: qrtool FORCE
|
||||
$(library_path_prefix) \
|
||||
./server.py
|
||||
|
||||
deploy: FORCE
|
||||
set -e; \
|
||||
for kc in $(TARGET); do \
|
||||
echo $$kc; \
|
||||
kubectl --kubeconfig deploy/kubeconfig.$$kc set image deployment/alg alg=$(IMAGE); \
|
||||
kubectl --kubeconfig deploy/kubeconfig.$$kc rollout status deployment alg; \
|
||||
done
|
||||
|
||||
PROTO_DIR := ../../../cassia/estord/proto
|
||||
fileprocess.grpc.pb.h fileprocess.grpc.pb.cc fileprocess.pb.h fileprocess.pb.cc: $(PROTO_DIR)/fileprocess.proto
|
||||
protoc -I $(PROTO_DIR) --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` $<
|
||||
protoc -I $(PROTO_DIR) --cpp_out=. $<
|
||||
|
||||
install-scanner: qrtool.wx.wasm.br
|
||||
@cp -v qrtool.wx.js qrtool.wx.wasm.br ../scanner/assets
|
||||
@cp -v qrtool.wx.js ../scanner/worker
|
||||
|
||||
install-web: qrtool.web.wasm
|
||||
@cp -v qrtool.web.js qrtool.web.wasm ../web/public/camera-4.0/js/
|
||||
|
||||
install: install-web install-scanner
|
||||
211
alg/angle.cpp
Normal file
211
alg/angle.cpp
Normal file
@ -0,0 +1,211 @@
|
||||
#include <algorithm>
|
||||
#include "libqr.h"
|
||||
|
||||
static void clear_connected(Mat &bin, Point p)
|
||||
{
|
||||
vector<Point> q;
|
||||
q.push_back(p);
|
||||
while (q.size()) {
|
||||
auto p = q[q.size() - 1];
|
||||
q.pop_back();
|
||||
bin.at<uint8_t>(p.y, p.x) = 0;
|
||||
for (int i = -1; i <= 1; i++) {
|
||||
for (int j = -1; j <= 1; j++) {
|
||||
int nx = p.x + i;
|
||||
int ny = p.y + j;
|
||||
if (nx < 0 || nx >= bin.cols || ny < 0 || ny >= bin.rows) {
|
||||
continue;
|
||||
}
|
||||
if (bin.at<bool>(ny, nx)) {
|
||||
q.push_back(Point(nx, ny));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static
|
||||
vector<Point> find_points(Mat bin)
|
||||
{
|
||||
vector<Point> ret;
|
||||
for (int x = 0; x < bin.cols; x++) {
|
||||
for (int y = 0; y < bin.rows; y++) {
|
||||
auto p = bin.at<uint8_t>(y, x);
|
||||
if (!p) continue;
|
||||
auto point = Point(x, y);
|
||||
ret.push_back(point);
|
||||
clear_connected(bin, point);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static
|
||||
bool in_center(Mat &bin, Point &p)
|
||||
{
|
||||
int margin = bin.rows * 2 / 10;
|
||||
|
||||
return p.x > margin && p.x < bin.cols - margin && p.y > margin && p.y <= bin.rows - margin;
|
||||
}
|
||||
|
||||
static
|
||||
float distance(Point &p, Point &q)
|
||||
{
|
||||
auto xdiff = p.x - q.x;
|
||||
auto ydiff = p.y - q.y;
|
||||
return xdiff * xdiff + ydiff * ydiff;
|
||||
}
|
||||
|
||||
static
|
||||
int find_closest(Point &p, vector<Point> &points, bool left, bool top)
|
||||
{
|
||||
int ret = -1;
|
||||
for (int ii = 0; ii < points.size(); ii++) {
|
||||
auto i = points[ii];
|
||||
if (i.x == p.x && i.y == p.y) continue;
|
||||
if (left && i.x > p.x) continue;
|
||||
if (top && i.y > p.y) continue;
|
||||
if (!left && i.x <= p.x) continue;
|
||||
if (!top && i.y < p.y) continue;
|
||||
if (ret < 0 || distance(p, points[ret]) > distance(p, i)) {
|
||||
ret = ii;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static
|
||||
float find_angle(Point &p, vector<Point> &points)
|
||||
{
|
||||
// Find 4 dots in 4 quadrant (if any)
|
||||
// Then find 2 closest on y axis
|
||||
// Then calculate angle between those two
|
||||
|
||||
auto topleft = find_closest(p, points, true, true);
|
||||
auto bottomright = find_closest(p, points, false, false);
|
||||
|
||||
if (topleft < 0 || bottomright < 0)
|
||||
return -1;
|
||||
auto a = points[topleft];
|
||||
auto b = points[bottomright];
|
||||
printf("point %d %d top left %d %d, bottom right %d %d\n", p.x, p.y, a.x, a.y, b.x, b.y);
|
||||
if (a.y == b.y) return 0;
|
||||
auto ret = atan((b.x - a.x) / (b.y - a.y)) * 180.0 / CV_PI;
|
||||
if (ret < 0) ret += 90;
|
||||
if (ret > 45) ret = 90 - ret;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static
|
||||
void angle_stat(vector<float> angles, float &median, float &variance)
|
||||
{
|
||||
std::sort(angles.begin(), angles.end());
|
||||
float sum = 0;
|
||||
for (auto x: angles) {
|
||||
sum += x;
|
||||
}
|
||||
auto mid = angles.size() / 2;
|
||||
median = angles[mid];
|
||||
auto avg = sum / angles.size();
|
||||
variance = 0;
|
||||
for (auto x: angles) {
|
||||
auto diff = x - avg;
|
||||
variance += diff * diff;
|
||||
}
|
||||
}
|
||||
|
||||
float hough_lines_angle(Mat &img, string &err)
|
||||
{
|
||||
show(img);
|
||||
vector<Vec3f> lines;
|
||||
HoughLines(img, lines, 1, CV_PI / 180, 6, 0, 0);
|
||||
for (auto x: lines) {
|
||||
printf("line: %.1f %.1f %.1f\n", x[0], x[1] * 180.0 / CV_PI, x[2]);
|
||||
}
|
||||
if (!lines.size()) {
|
||||
err = "cannot find lines in image";
|
||||
return -1;
|
||||
}
|
||||
|
||||
int total_weight = 0;
|
||||
for (int i = 0; i < lines.size() && i < 5; i++) {
|
||||
total_weight += lines[i][2];
|
||||
}
|
||||
int acc = 0;
|
||||
float ret = 0;
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
acc += lines[i][2];
|
||||
if (acc >= total_weight / 2) {
|
||||
ret = lines[i][1] * 180.0 / CV_PI;
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (ret < 0) {
|
||||
ret += 90;
|
||||
}
|
||||
while (ret > 90) {
|
||||
ret -= 90;
|
||||
}
|
||||
if (ret > 45) {
|
||||
ret = 90 - ret;
|
||||
}
|
||||
printf("angle: %f\n", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
float emblem_detect_angle(Mat &gray, string &err)
|
||||
{
|
||||
Mat bin;
|
||||
const int min_points = 30;
|
||||
vector<Point> points;
|
||||
auto kernel = getStructuringElement(MORPH_ELLIPSE, Size(3, 3));
|
||||
|
||||
adaptiveThreshold(gray, bin, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY_INV, 11, 2);
|
||||
while (true) {
|
||||
// In this loop we erode a "full" image in order to get enough detached components
|
||||
points = find_points(bin.clone());
|
||||
printf("points: %zu\n", points.size());
|
||||
if (points.size() == 0) {
|
||||
err = "cannot find enough points";
|
||||
return -1;
|
||||
}
|
||||
if (points.size() > min_points) {
|
||||
break;
|
||||
}
|
||||
erode(bin, bin, kernel);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
// In this loop we further erode a "lean" image in order to get clarity until it's too much
|
||||
Mat eroded;
|
||||
erode(bin, eroded, kernel);
|
||||
auto tmp = find_points(eroded.clone());
|
||||
if (tmp.size() < min_points) {
|
||||
printf("too much\n");
|
||||
break;
|
||||
}
|
||||
bin = eroded.clone();
|
||||
}
|
||||
|
||||
return hough_lines_angle(bin, err);
|
||||
|
||||
vector<float> angles;
|
||||
for (auto p: points) {
|
||||
if (!in_center(bin, p)) {
|
||||
continue;
|
||||
}
|
||||
auto angle = find_angle(p, points);
|
||||
if (angle >= 0) {
|
||||
printf("found angle %f\n", angle);
|
||||
angles.push_back(angle);
|
||||
}
|
||||
}
|
||||
if (!angles.size()) {
|
||||
err = "cannot find point to calculate angle";
|
||||
return -1;
|
||||
}
|
||||
float med, var;
|
||||
angle_stat(angles, med, var);
|
||||
printf("med: %f, var: %f\n", med, var);
|
||||
return med;
|
||||
}
|
||||
95
alg/base64.cpp
Normal file
95
alg/base64.cpp
Normal file
@ -0,0 +1,95 @@
|
||||
#include "base64.h"
|
||||
#include <iostream>
|
||||
|
||||
static const std::string base64_chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
"0123456789+/";
|
||||
|
||||
|
||||
static inline bool is_base64(BYTE c) {
|
||||
return (isalnum(c) || (c == '+') || (c == '/'));
|
||||
}
|
||||
|
||||
std::string base64_encode(BYTE const* buf, unsigned int bufLen) {
|
||||
std::string ret;
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
BYTE char_array_3[3];
|
||||
BYTE char_array_4[4];
|
||||
|
||||
while (bufLen--) {
|
||||
char_array_3[i++] = *(buf++);
|
||||
if (i == 3) {
|
||||
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
|
||||
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
|
||||
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
|
||||
char_array_4[3] = char_array_3[2] & 0x3f;
|
||||
|
||||
for(i = 0; (i <4) ; i++)
|
||||
ret += base64_chars[char_array_4[i]];
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (i)
|
||||
{
|
||||
for(j = i; j < 3; j++)
|
||||
char_array_3[j] = '\0';
|
||||
|
||||
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
|
||||
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
|
||||
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
|
||||
char_array_4[3] = char_array_3[2] & 0x3f;
|
||||
|
||||
for (j = 0; (j < i + 1); j++)
|
||||
ret += base64_chars[char_array_4[j]];
|
||||
|
||||
while((i++ < 3))
|
||||
ret += '=';
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<BYTE> base64_decode(std::string const& encoded_string) {
|
||||
int in_len = encoded_string.size();
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
int in_ = 0;
|
||||
BYTE char_array_4[4], char_array_3[3];
|
||||
std::vector<BYTE> ret;
|
||||
|
||||
while (in_len-- && ( encoded_string[in_] != '=') && is_base64(encoded_string[in_])) {
|
||||
char_array_4[i++] = encoded_string[in_]; in_++;
|
||||
if (i ==4) {
|
||||
for (i = 0; i <4; i++)
|
||||
char_array_4[i] = base64_chars.find(char_array_4[i]);
|
||||
|
||||
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
|
||||
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
|
||||
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
|
||||
|
||||
for (i = 0; (i < 3); i++)
|
||||
ret.push_back(char_array_3[i]);
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (i) {
|
||||
for (j = i; j <4; j++)
|
||||
char_array_4[j] = 0;
|
||||
|
||||
for (j = 0; j <4; j++)
|
||||
char_array_4[j] = base64_chars.find(char_array_4[j]);
|
||||
|
||||
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
|
||||
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
|
||||
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
|
||||
|
||||
for (j = 0; (j < i - 1); j++) ret.push_back(char_array_3[j]);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
11
alg/base64.h
Normal file
11
alg/base64.h
Normal file
@ -0,0 +1,11 @@
|
||||
#ifndef _BASE64_H_
|
||||
#define _BASE64_H_
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
typedef unsigned char BYTE;
|
||||
|
||||
std::string base64_encode(BYTE const* buf, unsigned int bufLen);
|
||||
std::vector<BYTE> base64_decode(std::string const&);
|
||||
|
||||
#endif
|
||||
20
alg/deploy/kubeconfig.derby
Normal file
20
alg/deploy/kubeconfig.derby
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: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrakNDQVRlZ0F3SUJBZ0lJZW9uUVdIaE5mcGd3Q2dZSUtvWkl6ajBFQXdJd0l6RWhNQjhHQTFVRUF3d1kKYXpOekxXTnNhV1Z1ZEMxallVQXhOamsyTURjd016ZzNNQjRYRFRJek1Ea3pNREV3TXprME4xb1hEVEkwTURreQpPVEV3TXprME4xb3dNREVYTUJVR0ExVUVDaE1PYzNsemRHVnRPbTFoYzNSbGNuTXhGVEFUQmdOVkJBTVRESE41CmMzUmxiVHBoWkcxcGJqQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJHZWpuRkFjK2hPRTBtNEMKT1Z3NkVNTG85SGZJMU4vVDYrTC9zRzR0OHA0WWI5VWhiTnlhVC9HcjlwVEhpZG5zS21sT3ZiZWZPR1NSV3JlbQpEcEhzNjEyalNEQkdNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBakFmCkJnTlZIU01FR0RBV2dCVGd0NFp1aUp3YlZlZ0xUalFMSGdrVzFVR2JqREFLQmdncWhrak9QUVFEQWdOSkFEQkcKQWlFQTFXeWNZRW5WbEs2OG1GZGZBUmlKdytBUytSQ0swSkl3M2hLZXJmNlV4WE1DSVFDUU85cGROOUgxMzBOOApkUncvMHJUbHo3Q1J0ZmZObEdUdjNDeE1lRVVUb3c9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlCZHpDQ0FSMmdBd0lCQWdJQkFEQUtCZ2dxaGtqT1BRUURBakFqTVNFd0h3WURWUVFEREJock0zTXRZMnhwClpXNTBMV05oUURFMk9UWXdOekF6T0Rjd0hoY05Nak13T1RNd01UQXpPVFEzV2hjTk16TXdPVEkzTVRBek9UUTMKV2pBak1TRXdId1lEVlFRRERCaHJNM010WTJ4cFpXNTBMV05oUURFMk9UWXdOekF6T0Rjd1dUQVRCZ2NxaGtqTwpQUUlCQmdncWhrak9QUU1CQndOQ0FBUTJEUWxMMWxpM0cyU29pa0t1MGpIM2YwQzZYdWlxc1U0bVBzN0FqR1VPCnlGNnNra0hVamg2ZldPMDZBZ3NrUkdQQ3FaOFpwQjlDL2doZDlqVTl2ZnhubzBJd1FEQU9CZ05WSFE4QkFmOEUKQkFNQ0FxUXdEd1lEVlIwVEFRSC9CQVV3QXdFQi96QWRCZ05WSFE0RUZnUVU0TGVHYm9pY0cxWG9DMDQwQ3g0SgpGdFZCbTR3d0NnWUlLb1pJemowRUF3SURTQUF3UlFJaEFLdlNQVkRlTmtBZFRUR0pzNWNLRGFmSStCYUR4ZmhvCm1hM082V0hxK05JeEFpQnMxVTBsVlNRWjRYb0lZWXJ4OHBMSm5EUzVjSGI4cmRLTndaTjZEcExCSlE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
|
||||
client-key-data: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUd4WU5mWk5GWVFJRG9zRVZjM0JiVUFNcVFoV0wrNVpndHIwZ0R2SUJES0tvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWjZPY1VCejZFNFRTYmdJNVhEb1F3dWowZDhqVTM5UHI0dit3YmkzeW5oaHYxU0ZzM0pwUAo4YXYybE1lSjJld3FhVTY5dDU4NFpKRmF0NllPa2V6clhRPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
|
||||
20
alg/deploy/kubeconfig.t420
Normal file
20
alg/deploy/kubeconfig.t420
Normal file
@ -0,0 +1,20 @@
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
insecure-skip-tls-verify: true
|
||||
server: https://192.168.0.253: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: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrakNDQVRlZ0F3SUJBZ0lJTXBPbFZsSkM4YUV3Q2dZSUtvWkl6ajBFQXdJd0l6RWhNQjhHQTFVRUF3d1kKYXpOekxXTnNhV1Z1ZEMxallVQXhOekV3TURJeU9EYzRNQjRYRFRJME1ETXdPVEl5TWpFeE9Gb1hEVEkxTURNdwpPVEl5TWpFeE9Gb3dNREVYTUJVR0ExVUVDaE1PYzNsemRHVnRPbTFoYzNSbGNuTXhGVEFUQmdOVkJBTVRESE41CmMzUmxiVHBoWkcxcGJqQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJQd3FiQ0dDZkdiRWhxem0KUjBoU2ZTTEh1OWxjdEdKb2laTTZYUlNJRlJYaEZJd043V2w0NXBPbSttQm9ldG92UWMyL3hyS2kwYmhMeXhUbApRbGk2bFEyalNEQkdNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBakFmCkJnTlZIU01FR0RBV2dCUms2aXJOdDk4NzNBdnUwTjRvWmVKZ3lIVDE5akFLQmdncWhrak9QUVFEQWdOSkFEQkcKQWlFQXNRZzZxQ2lMOXpPazdTenJrbUNWSjA4Q1M4aWUzaW5FRDRNUVd0bVVVU3dDSVFEUG5RY0pNQVR2VlJoZwo4NEhOaVh2RVhWbEN1SHlOM1V6MEdrT2sveEVoK2c9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlCZHpDQ0FSMmdBd0lCQWdJQkFEQUtCZ2dxaGtqT1BRUURBakFqTVNFd0h3WURWUVFEREJock0zTXRZMnhwClpXNTBMV05oUURFM01UQXdNakk0Tnpnd0hoY05NalF3TXpBNU1qSXlNVEU0V2hjTk16UXdNekEzTWpJeU1URTQKV2pBak1TRXdId1lEVlFRRERCaHJNM010WTJ4cFpXNTBMV05oUURFM01UQXdNakk0Tnpnd1dUQVRCZ2NxaGtqTwpQUUlCQmdncWhrak9QUU1CQndOQ0FBU2dQWGRKYWRDamh5SEQ4TlRUaS9ZRitKcDZUaXNpQTRyV2Q5OUlXejdLCnM5Rm5OV2NUVHY4SHNjQkM0TVpLRkwwM0dYbFU4SENTNk9pY1ZmdU12WHFFbzBJd1FEQU9CZ05WSFE4QkFmOEUKQkFNQ0FxUXdEd1lEVlIwVEFRSC9CQVV3QXdFQi96QWRCZ05WSFE0RUZnUVVaT29xemJmZk85d0w3dERlS0dYaQpZTWgwOWZZd0NnWUlLb1pJemowRUF3SURTQUF3UlFJZ0RZQ2JubHpiaXorMVlmclZCQmV6VnVWSTB5Kzl1N3RJCks2RXFHYlFKVXAwQ0lRRHRuZjA2NFFGWExaaGtLWiszKy9KdnJieTdmYU4rVTV6ZEdyOWFCMWQ1MXc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
|
||||
client-key-data: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUc3bXZVNmlEcGZMam1lcmRzVG4wQXhIczlnL1QzUzdjb3pjU280bStJS21vQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFL0Nwc0lZSjhac1NHck9aSFNGSjlJc2U3MlZ5MFltaUprenBkRklnVkZlRVVqQTN0YVhqbQprNmI2WUdoNjJpOUJ6Yi9Hc3FMUnVFdkxGT1ZDV0xxVkRRPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
|
||||
20
alg/deploy/kubeconfig.zy
Normal file
20
alg/deploy/kubeconfig.zy
Normal file
@ -0,0 +1,20 @@
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
insecure-skip-tls-verify: true
|
||||
server: https://euphon.cloud: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: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrVENDQVRlZ0F3SUJBZ0lJVTI1b0lYUkIvM3N3Q2dZSUtvWkl6ajBFQXdJd0l6RWhNQjhHQTFVRUF3d1kKYXpOekxXTnNhV1Z1ZEMxallVQXhOekF6TURZNE5ETXpNQjRYRFRJek1USXlNREV3TXpNMU0xb1hEVEkwTVRJeApPVEV3TXpNMU0xb3dNREVYTUJVR0ExVUVDaE1PYzNsemRHVnRPbTFoYzNSbGNuTXhGVEFUQmdOVkJBTVRESE41CmMzUmxiVHBoWkcxcGJqQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJQaWs4UnBCcEVKS0NYbG8KdnB3UnJ0NEVKaHBsbUMvUW1zT3JwTUlYTC93amdnZkwrb0MvSVQ0VUtuOWZ0cmZsdlBjWEhHWVprYWgvd210QQo2OS9rUWlHalNEQkdNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBakFmCkJnTlZIU01FR0RBV2dCU1grN1IzbDR0b0luMkp1bk9Cd1VCN1E4dlpwakFLQmdncWhrak9QUVFEQWdOSUFEQkYKQWlFQXNiTjVVUVRWSnhrRFlHbDJCUDhrUkF4Tk42OXRXMzNpdlBldXZVTXViM01DSUhoUzNab3d2dU9rci9KRAoyaFZhM25mQ3BWdzNrZ0NiVjd3a3RpVGdpaHNaCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJlRENDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdFkyeHAKWlc1MExXTmhRREUzTURNd05qZzBNek13SGhjTk1qTXhNakl3TVRBek16VXpXaGNOTXpNeE1qRTNNVEF6TXpVegpXakFqTVNFd0h3WURWUVFEREJock0zTXRZMnhwWlc1MExXTmhRREUzTURNd05qZzBNek13V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFSZzlOYnRuZ1RDSzhIVnV5NHNpUzQ2dFE5b3pmY0dlclI2ZlFxbmVab0EKemlqRFdnaHhEWnNOTzVuVWVaWHpiWDgrbVdzNUIyRWtlaHZZeWhnRzY4MTVvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVWwvdTBkNWVMYUNKOWlicHpnY0ZBCmUwUEwyYVl3Q2dZSUtvWkl6ajBFQXdJRFNRQXdSZ0loQUoyamlZVGFKSHptUDJ0NjZmRlVkT3hmQjlNRnRNb2YKazN5dnNFZ1YxOWY4QWlFQXVIVUtjVHljVStzbnIxUWxhWjBoSFZ5OW53UGp3M3ZVdlVJWU9kTGZwNjA9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
|
||||
client-key-data: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUZwRG9kSU9Kbmg3UUVlYWQ4MXdHdnkzaHNEQkhjbG5NQXEzL3pzUHlaVTFvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFK0tUeEdrR2tRa29KZVdpK25CR3UzZ1FtR21XWUw5Q2F3NnVrd2hjdi9DT0NCOHY2Z0w4aApQaFFxZjErMnQrVzg5eGNjWmhtUnFIL0NhMERyMytSQ0lRPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
|
||||
84
alg/fileprocess.cpp
Normal file
84
alg/fileprocess.cpp
Normal file
@ -0,0 +1,84 @@
|
||||
#include "fileprocess.h"
|
||||
#include "fileprocess.grpc.pb.h"
|
||||
#include <grpc++/server_builder.h>
|
||||
|
||||
using namespace std;
|
||||
using namespace grpc;
|
||||
using namespace fileprocess;
|
||||
|
||||
class FileProcessServer final : public FileProcess::Service {
|
||||
|
||||
handler_fn _handle_image;
|
||||
|
||||
Status ProcessFiles(ServerContext* context, const Files* request,
|
||||
Output *resp) override {
|
||||
printf("process files\n");
|
||||
for (auto file: request->files()) {
|
||||
printf("file: %s\n", file.path().c_str());
|
||||
string output_path;
|
||||
vector<uint8_t> output;
|
||||
vector<uint8_t> input(file.data().begin(), file.data().end());
|
||||
auto r = _handle_image(file.path(), input, output_path, output);
|
||||
auto d = resp->add_files();
|
||||
if (r) {
|
||||
d->set_succeeded(false);
|
||||
d->set_error("Failed to process image");
|
||||
} else {
|
||||
d->set_succeeded(true);
|
||||
d->set_path(output_path);
|
||||
string data(output.begin(), output.end());
|
||||
d->set_data(data);
|
||||
}
|
||||
}
|
||||
printf("done\n");
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
#if 0
|
||||
Status ProcessArchive(ServerContext* context, ServerReaderWriter<Output, ArchiveFile> *stream) override {
|
||||
ArchiveFile request;
|
||||
while (stream->Read(&request)) {
|
||||
if (request.data().size() <= 0) continue;
|
||||
string output_path;
|
||||
vector<uint8_t> output;
|
||||
vector<uint8_t> input(request.data().begin(), request.data().end());
|
||||
string path = request.path();
|
||||
path += string("-files/") + request.path_in_archive();
|
||||
auto r = _handle_image(path, input, output_path, output);
|
||||
Output d;
|
||||
if (r) {
|
||||
d.set_succeeded(false);
|
||||
string error = "Failed to process image " + request.path_in_archive();
|
||||
d.set_error(error);
|
||||
} else {
|
||||
d.set_succeeded(true);
|
||||
d.set_path(output_path);
|
||||
string data(output.begin(), output.end());
|
||||
d.set_data(data);
|
||||
}
|
||||
stream->Write(d);
|
||||
}
|
||||
return Status::OK;
|
||||
}
|
||||
#endif
|
||||
|
||||
public:
|
||||
FileProcessServer(handler_fn handle_image) :
|
||||
_handle_image(handle_image)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
int run_server(const string &server_addr, handler_fn handle_image) {
|
||||
FileProcessServer service(handle_image);
|
||||
|
||||
ServerBuilder builder;
|
||||
builder.AddListeningPort(server_addr, grpc::InsecureServerCredentials());
|
||||
builder.RegisterService(&service);
|
||||
builder.SetMaxSendMessageSize(128 * 1024 * 1024);
|
||||
std::unique_ptr<Server> server(builder.BuildAndStart());
|
||||
std::cout << "Server listening on " << server_addr << std::endl;
|
||||
server->Wait();
|
||||
return 0;
|
||||
}
|
||||
|
||||
13
alg/fileprocess.h
Normal file
13
alg/fileprocess.h
Normal file
@ -0,0 +1,13 @@
|
||||
#ifndef _FILEPROCESS_H_
|
||||
#define _FILEPROCESS_H_
|
||||
#include <stdint.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
typedef int (*handler_fn)(const std::string &input_path,
|
||||
const std::vector<uint8_t> &input,
|
||||
std::string &output_path,
|
||||
std::vector<uint8_t> &output);
|
||||
|
||||
int run_server(const std::string &server_addr, handler_fn handle_image);
|
||||
|
||||
#endif
|
||||
36
alg/http.cc
Normal file
36
alg/http.cc
Normal file
@ -0,0 +1,36 @@
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
#include <stdlib.h>
|
||||
#include "httplib.h"
|
||||
#include "http.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
int start_http_server(int port, http_handle_file handle_file)
|
||||
{
|
||||
httplib::Server svr;
|
||||
|
||||
svr.Post("/roi", [handle_file](const httplib::Request &req, httplib::Response &res) {
|
||||
auto f = req.get_file_value("file");
|
||||
vector<uint8_t> input(f.content.begin(), f.content.end());
|
||||
vector<uint8_t> output;
|
||||
if (!input.size()) {
|
||||
res.status = 400;
|
||||
res.set_content("file is missing\n", "text/plain");
|
||||
} else {
|
||||
int r = handle_file(input, output);
|
||||
if (r) {
|
||||
res.status = 400;
|
||||
res.set_content("failed to process file\n", "text/plain");
|
||||
} else {
|
||||
res.status = 200;
|
||||
res.set_content((char *)&output[0], output.size(), "image/jpeg");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cout << "starting server on port " << port << endl;
|
||||
svr.listen("0.0.0.0", port);
|
||||
|
||||
return 0;
|
||||
}
|
||||
10
alg/http.h
Normal file
10
alg/http.h
Normal file
@ -0,0 +1,10 @@
|
||||
#ifndef _HTTP_H_
|
||||
#define _HTTP_H_
|
||||
#include <string>
|
||||
#include <vector>
|
||||
typedef int (*http_handle_file)(const std::vector<uint8_t> &input,
|
||||
std::vector<uint8_t> &output);
|
||||
|
||||
int start_http_server(int port, http_handle_file handle_file);
|
||||
|
||||
#endif
|
||||
9464
alg/httplib.h
Normal file
9464
alg/httplib.h
Normal file
File diff suppressed because it is too large
Load Diff
720
alg/libqr.cpp
Normal file
720
alg/libqr.cpp
Normal file
@ -0,0 +1,720 @@
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include "libqr.h"
|
||||
#include "opencv2/objdetect.hpp"
|
||||
#include "opencv2/wechat_qrcode.hpp"
|
||||
#include "string_format.h"
|
||||
using namespace std;
|
||||
using namespace cv;
|
||||
|
||||
static
|
||||
vector<Point> transform_image(Mat &in, vector<Point> qr_points, Mat &out)
|
||||
{
|
||||
Mat src = (Mat_<float>(4, 2) <<
|
||||
qr_points[0].x, qr_points[0].y,
|
||||
qr_points[1].x, qr_points[1].y,
|
||||
qr_points[2].x, qr_points[2].y,
|
||||
qr_points[3].x, qr_points[3].y
|
||||
);
|
||||
|
||||
int min_x = qr_points[0].x;
|
||||
int min_y = qr_points[0].y;
|
||||
int max_x = qr_points[0].x;
|
||||
int max_y = qr_points[0].y;
|
||||
for (auto p: qr_points) {
|
||||
min_x = min(p.x, min_x);
|
||||
min_y = min(p.y, min_y);
|
||||
max_x = max(p.x, max_x);
|
||||
max_y = max(p.y, max_y);
|
||||
}
|
||||
Mat dst = (Mat_<float>(4, 2) <<
|
||||
min_x, min_y,
|
||||
max_x, min_y,
|
||||
max_x, max_y,
|
||||
min_x, max_y);
|
||||
|
||||
Mat m = getPerspectiveTransform(src, dst);
|
||||
warpPerspective(in, out, m, in.size());
|
||||
vector<Point> ret;
|
||||
ret.push_back(Point(min_x, min_y));
|
||||
ret.push_back(Point(max_x, min_y));
|
||||
ret.push_back(Point(max_x, max_y));
|
||||
ret.push_back(Point(min_x, max_y));
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool detect_qr(ProcessState &ps, float margin_ratio, bool warp, string &err)
|
||||
{
|
||||
#if WECHAT_QRCODE_USE_MODEL
|
||||
auto wr = wechat_qrcode::WeChatQRCode(
|
||||
"wechat_qrcode/detect.prototxt",
|
||||
"wechat_qrcode/detect.caffemodel",
|
||||
"wechat_qrcode/sr.prototxt",
|
||||
"wechat_qrcode/sr.caffemodel");
|
||||
#else
|
||||
auto wr = wechat_qrcode::WeChatQRCode();
|
||||
#endif
|
||||
vector<Mat> qrs;
|
||||
auto r = wr.detectAndDecode(ps.preprocessed, qrs);
|
||||
|
||||
if (!r.size()) {
|
||||
err = "qr not detected";
|
||||
return false;
|
||||
}
|
||||
|
||||
ps.qrcode = r[0];
|
||||
auto rect = qrs[0];
|
||||
vector<Point> qr_points;
|
||||
qr_points.push_back(Point(rect.at<float>(0, 0) / ps.scale, rect.at<float>(0, 1) / ps.scale));
|
||||
qr_points.push_back(Point(rect.at<float>(1, 0) / ps.scale, rect.at<float>(1, 1) / ps.scale));
|
||||
qr_points.push_back(Point(rect.at<float>(2, 0) / ps.scale, rect.at<float>(2, 1) / ps.scale));
|
||||
qr_points.push_back(Point(rect.at<float>(3, 0) / ps.scale, rect.at<float>(3, 1) / ps.scale));
|
||||
ps.qr_points = qr_points;
|
||||
Mat warped;
|
||||
vector<Point> warped_qr_points;
|
||||
if (warp) {
|
||||
warped_qr_points = transform_image(*ps.orig, qr_points, warped);
|
||||
} else {
|
||||
warped = *ps.orig;
|
||||
warped_qr_points = qr_points;
|
||||
}
|
||||
int min_x = warped_qr_points[0].x;
|
||||
int min_y = warped_qr_points[0].y;
|
||||
int max_x = min_x;
|
||||
int max_y = min_y;
|
||||
for (auto p: warped_qr_points) {
|
||||
min_x = min(p.x, min_x);
|
||||
min_y = min(p.y, min_y);
|
||||
max_x = max(p.x, max_x);
|
||||
max_y = max(p.y, max_y);
|
||||
}
|
||||
int margin = (max_x - min_x) * margin_ratio;
|
||||
if (min_y < margin || min_x < margin || max_x + margin >= warped.cols || max_y + margin >= warped.rows) {
|
||||
err = "qr margin too small";
|
||||
return false;
|
||||
}
|
||||
int qr_width = max_x - min_x;
|
||||
int qr_height = max_y - min_y;
|
||||
if (qr_width < 200 && qr_height < 200 && qr_width < ps.orig->cols * 0.5 && qr_height < ps.orig->rows * 0.5) {
|
||||
printf("(%d, %d) in (%d, %d)\n", qr_width, qr_height, ps.orig->cols, ps.orig->rows);
|
||||
err = "qr too small";
|
||||
return false;
|
||||
}
|
||||
|
||||
Rect qr_rect(min_x, min_y, max_x - min_x, max_y - min_y);
|
||||
ps.qr_straighten = warped(qr_rect);
|
||||
Rect qr_with_margin_rect(min_x - margin, min_y - margin,
|
||||
max_x - min_x + margin * 2,
|
||||
max_y - min_y + margin * 2);
|
||||
ps.straighten = warped(qr_with_margin_rect);
|
||||
Mat g;
|
||||
cvtColor(ps.straighten, g, COLOR_BGR2GRAY);
|
||||
equalizeHist(g, g);
|
||||
|
||||
Rect dot_rect;
|
||||
dot_rect.x = 0;
|
||||
dot_rect.y = 0;
|
||||
dot_rect.width = margin / 2;
|
||||
dot_rect.height = margin / 2;
|
||||
ps.dot_area = ps.straighten(dot_rect);
|
||||
Mat dot_area_gray = g(dot_rect);
|
||||
resize(dot_area_gray, ps.dot_area_gray, Size(64, 64));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool preprocess(ProcessState &ps)
|
||||
{
|
||||
Mat gray;
|
||||
cvtColor(*ps.orig, gray, COLOR_BGR2GRAY);
|
||||
ps.scale = 1.0;
|
||||
const float size_cap = 512;
|
||||
if (ps.orig->rows > size_cap) {
|
||||
ps.scale = size_cap / ps.orig->rows;
|
||||
}
|
||||
if (ps.orig->cols > ps.orig->rows && ps.orig->cols > size_cap) {
|
||||
ps.scale = size_cap / ps.orig->cols;
|
||||
}
|
||||
resize(gray, ps.preprocessed, Size(), ps.scale, ps.scale);
|
||||
return true;
|
||||
}
|
||||
|
||||
struct EnergyGradient {
|
||||
double x;
|
||||
double y;
|
||||
};
|
||||
|
||||
static
|
||||
EnergyGradient energy_gradient(Mat &gray_img)
|
||||
{
|
||||
|
||||
Mat smd_image_x, smd_image_y, G;
|
||||
|
||||
Mat kernel_x(3, 3, CV_32F, Scalar(0));
|
||||
kernel_x.at<float>(1, 2) = -1.0;
|
||||
kernel_x.at<float>(1, 1) = 1.0;
|
||||
Mat kernel_y(3, 3, CV_32F, Scalar(0));
|
||||
kernel_y.at<float>(1, 1) = 1.0;
|
||||
kernel_y.at<float>(2, 1) = -1.0;
|
||||
filter2D(gray_img, smd_image_x, gray_img.depth(), kernel_x);
|
||||
filter2D(gray_img, smd_image_y, gray_img.depth(), kernel_y);
|
||||
|
||||
multiply(smd_image_x, smd_image_x, smd_image_x);
|
||||
multiply(smd_image_y, smd_image_y, smd_image_y);
|
||||
|
||||
EnergyGradient ret = { mean(smd_image_x)[0], mean(smd_image_y)[0], };
|
||||
return ret;
|
||||
}
|
||||
|
||||
static
|
||||
bool check_blur_by_energy_gradient(Mat &gray, string &err)
|
||||
{
|
||||
const int thres = 85;
|
||||
auto a = energy_gradient(gray);
|
||||
|
||||
float angle = 45;
|
||||
auto m = getRotationMatrix2D(Point2f(gray.cols / 2, gray.rows / 2), angle, 1.0);
|
||||
Mat rotated;
|
||||
warpAffine(gray, rotated, m, gray.size());
|
||||
|
||||
auto b = energy_gradient(rotated);
|
||||
|
||||
auto diffa = fabs(a.x - a.y);
|
||||
auto diffb = fabs(b.x - b.y);
|
||||
auto diffa_percent = 100 * diffa / max(a.x, a.y);
|
||||
auto diffb_percent = 100 * diffb / max(b.x, b.y);
|
||||
bool ret =
|
||||
((a.x > thres && a.y > thres) || (b.x > thres && b.y > thres)) &&
|
||||
diffa_percent < 15 && diffb_percent < 15;
|
||||
|
||||
cout << "energy: "
|
||||
+ to_string(a.x) + " "
|
||||
+ to_string(a.y) + " "
|
||||
+ to_string(b.x) + " "
|
||||
+ to_string(b.y) << endl;
|
||||
if (!ret) {
|
||||
err = "energy: "
|
||||
+ to_string(a.x) + " "
|
||||
+ to_string(a.y) + " "
|
||||
+ to_string(b.x) + " "
|
||||
+ to_string(b.y);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
double laplacian(Mat &gray, string &err)
|
||||
{
|
||||
int ddepth = CV_16S;
|
||||
Mat check, lap;
|
||||
GaussianBlur(gray, check, Size(5, 5), 0, 0, BORDER_DEFAULT);
|
||||
Laplacian(check, lap, ddepth, 3);
|
||||
convertScaleAbs(lap, lap);
|
||||
|
||||
Mat mean, stddev;
|
||||
meanStdDev(lap, mean, stddev);
|
||||
if (stddev.cols * stddev.rows == 1) {
|
||||
double area = gray.rows * gray.cols;
|
||||
double sd = stddev.at<double>(0, 0);
|
||||
double var = sd * sd;
|
||||
return var / area;
|
||||
}
|
||||
err = "wrong shape of stddev result";
|
||||
return -1;
|
||||
}
|
||||
|
||||
static
|
||||
bool check_blur_by_laplacian(ProcessState &ps, Mat &gray, string &err)
|
||||
{
|
||||
auto var = laplacian(gray, err);
|
||||
if (var < 0) return false;
|
||||
|
||||
ps.clarity = var;
|
||||
if (var <= ps.laplacian_thres) {
|
||||
err = string_format("image (%d x %d) too blurry: %lf <= %lf",
|
||||
gray.cols, gray.rows,
|
||||
var, ps.laplacian_thres
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static
|
||||
bool check_blur(ProcessState &ps, Mat &gray, string &err)
|
||||
{
|
||||
bool use_energy_gradient = false;
|
||||
if (use_energy_gradient) {
|
||||
return check_blur_by_energy_gradient(gray, err);
|
||||
}
|
||||
return check_blur_by_laplacian(ps, gray, err);
|
||||
}
|
||||
|
||||
#define COUNT_COMPONENTS 0
|
||||
#if COUNT_COMPONENTS
|
||||
static bool is_valid_pattern(Mat &img)
|
||||
{
|
||||
Mat labels;
|
||||
Mat stats;
|
||||
Mat centroids;
|
||||
connectedComponentsWithStats(img, labels, stats, centroids);
|
||||
int valid = 0;
|
||||
for (auto i = 0; i < stats.rows; i++) {
|
||||
int area = stats.at<int>(i, CC_STAT_AREA);
|
||||
if (area > 5) {
|
||||
valid++;
|
||||
}
|
||||
}
|
||||
|
||||
return valid > 25;
|
||||
}
|
||||
#endif
|
||||
|
||||
static
|
||||
int find_score(Mat &img)
|
||||
{
|
||||
int ret = 0;
|
||||
for (int row = 0; row < img.rows; row++) {
|
||||
int row_sum = 0;
|
||||
for (int col = 0; col < img.cols; col++) {
|
||||
auto p = img.at<bool>(row, col);
|
||||
if (p) {
|
||||
row_sum += 1;
|
||||
}
|
||||
}
|
||||
if (row_sum) {
|
||||
ret += 1;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void clear_connected(Mat &bin, Point p)
|
||||
{
|
||||
vector<Point> q;
|
||||
q.push_back(p);
|
||||
while (q.size()) {
|
||||
auto p = q[q.size() - 1];
|
||||
q.pop_back();
|
||||
bin.at<uint8_t>(p.y, p.x) = 0;
|
||||
for (int i = -1; i <= 1; i++) {
|
||||
for (int j = -1; j <= 1; j++) {
|
||||
int nx = p.x + i;
|
||||
int ny = p.y + j;
|
||||
if (nx < 0 || nx >= bin.cols || ny < 0 || ny >= bin.rows) {
|
||||
continue;
|
||||
}
|
||||
if (bin.at<bool>(ny, nx)) {
|
||||
q.push_back(Point(nx, ny));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static
|
||||
vector<Point> find_points(Mat bin)
|
||||
{
|
||||
vector<Point> ret;
|
||||
for (int x = 0; x < bin.cols; x++) {
|
||||
for (int y = 0; y < bin.rows; y++) {
|
||||
auto p = bin.at<uint8_t>(y, x);
|
||||
if (!p) continue;
|
||||
auto point = Point(x, y);
|
||||
ret.push_back(point);
|
||||
clear_connected(bin, point);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static
|
||||
int adaptive_erode(Mat &bin, Mat &eroded, string &err)
|
||||
{
|
||||
auto kernel = getStructuringElement(MORPH_ELLIPSE, Size(3, 3));
|
||||
const int min_points = 25;
|
||||
int max_erodes = 5;
|
||||
|
||||
printf("adaptiveThreshold\n");
|
||||
eroded = bin.clone();
|
||||
while (max_erodes-- > 0) {
|
||||
// In this loop we erode a "full" image in order to get enough detached components
|
||||
auto points = find_points(bin.clone());
|
||||
printf("points: %zu\n", points.size());
|
||||
if (points.size() == 0) {
|
||||
err = "cannot find enough points";
|
||||
return -1;
|
||||
}
|
||||
if (points.size() > min_points) {
|
||||
break;
|
||||
}
|
||||
erode(eroded, eroded, kernel);
|
||||
}
|
||||
|
||||
while (max_erodes-- > 0) {
|
||||
// In this loop we further erode a "lean" image in order to get clarity until it's too much
|
||||
Mat next;
|
||||
erode(eroded, next, kernel);
|
||||
auto points = find_points(next.clone());
|
||||
if (points.size() < min_points) {
|
||||
break;
|
||||
}
|
||||
eroded = next;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static
|
||||
int emblem_detect_angle(Mat &gray, bool check_orthogonal, string &err)
|
||||
{
|
||||
Mat bin;
|
||||
int min_score = gray.cols;
|
||||
int max_score = 0;
|
||||
int lowest_score_angle = -1;
|
||||
|
||||
adaptiveThreshold(gray, bin, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY_INV, 11, 2);
|
||||
|
||||
Mat inverted;
|
||||
bitwise_not(bin, inverted);
|
||||
const int MAX_ROT_ANGLE = 180;
|
||||
int scores[MAX_ROT_ANGLE] = { 0 };
|
||||
const int score_diff_thres = 5;
|
||||
|
||||
Mat eroded;
|
||||
adaptive_erode(bin, eroded, err);
|
||||
|
||||
for (int angle = 0; angle < MAX_ROT_ANGLE; angle += 1) {
|
||||
auto m = getRotationMatrix2D(Point2f(gray.cols / 2, gray.rows / 2), angle, 1.0);
|
||||
Mat rotated;
|
||||
warpAffine(eroded, rotated, m, gray.size());
|
||||
int score = find_score(rotated);
|
||||
scores[angle] = score;
|
||||
if (score < min_score) {
|
||||
lowest_score_angle = angle;
|
||||
}
|
||||
min_score = min(score, min_score);
|
||||
max_score = max(max_score, score);
|
||||
}
|
||||
if (max_score - min_score > score_diff_thres) {
|
||||
int orthogonal_angle = lowest_score_angle + 90;
|
||||
if (orthogonal_angle > 180) {
|
||||
orthogonal_angle -= 180;
|
||||
}
|
||||
int orthogonal_score = scores[orthogonal_angle];
|
||||
printf("lowest_score_angle %d, min score %d, max score %d, orthogonal_angle %d, orthogonal score: %d\n",
|
||||
lowest_score_angle, min_score, max_score, orthogonal_angle, orthogonal_score);
|
||||
lowest_score_angle = lowest_score_angle > 90 ? lowest_score_angle - 90 : lowest_score_angle;
|
||||
if (lowest_score_angle > 45)
|
||||
lowest_score_angle = 90 - lowest_score_angle;
|
||||
if (max_score - orthogonal_score > score_diff_thres || !check_orthogonal) {
|
||||
return lowest_score_angle;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool emblem_dot_angle(ProcessState &ps, InputArray in, float &angle, string &qrcode, string &err)
|
||||
{
|
||||
try {
|
||||
|
||||
ps.orig = (Mat *)in.getObj();
|
||||
preprocess(ps);
|
||||
|
||||
if (!detect_qr(ps, 0.20, true, err)) {
|
||||
err = "detect_qr: " + err;
|
||||
return false;
|
||||
}
|
||||
|
||||
qrcode = ps.qrcode;
|
||||
|
||||
if (!check_blur(ps, ps.dot_area_gray, err)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int a = emblem_detect_angle(ps.dot_area_gray, false, err);
|
||||
if (a > 0) {
|
||||
angle = a;
|
||||
return true;
|
||||
} else {
|
||||
err = "cannot detect angle";
|
||||
return false;
|
||||
}
|
||||
} catch (const std::exception &exc) {
|
||||
std::cout << exc.what() << std::endl;
|
||||
err = "exception";
|
||||
return false;
|
||||
} catch (...) {
|
||||
err = "unknown error";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static
|
||||
Mat adaptive_gray(Mat &img)
|
||||
{
|
||||
Mat ret;
|
||||
Mat mean, stddev;
|
||||
Mat channels[3];
|
||||
Mat hsv_img;
|
||||
|
||||
meanStdDev(img, mean, stddev);
|
||||
|
||||
int bgr_max_std_channel = 0;
|
||||
float bgr_max_std = stddev.at<float>(0, 0);
|
||||
for (int i = 1; i < 3; i++) {
|
||||
auto nv = stddev.at<float>(0, i);
|
||||
if (nv > bgr_max_std_channel) {
|
||||
bgr_max_std_channel = i;
|
||||
bgr_max_std = nv;
|
||||
}
|
||||
}
|
||||
cvtColor(img, hsv_img, COLOR_BGR2HSV);
|
||||
meanStdDev(img, hsv_img, stddev);
|
||||
int hsv_max_std_channel = 0;
|
||||
float hsv_max_std = stddev.at<float>(0, 0);
|
||||
for (int i = 1; i < 3; i++) {
|
||||
auto nv = stddev.at<float>(0, i);
|
||||
if (nv > hsv_max_std_channel) {
|
||||
hsv_max_std_channel = i;
|
||||
hsv_max_std = nv;
|
||||
}
|
||||
}
|
||||
if (hsv_max_std > bgr_max_std) {
|
||||
split(hsv_img, channels);
|
||||
printf("using hsv channel %d\n", hsv_max_std_channel);
|
||||
ret = channels[hsv_max_std_channel];
|
||||
} else {
|
||||
split(img, channels);
|
||||
printf("using rgb channel %d\n", bgr_max_std_channel);
|
||||
ret = channels[bgr_max_std_channel];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static
|
||||
bool cell_in_bg(int cell_x, int cell_y)
|
||||
{
|
||||
return
|
||||
(cell_x == 1 && (cell_y > 0 && cell_y < 6)) ||
|
||||
(cell_x == 2 && (cell_y == 1 || cell_y == 5)) ||
|
||||
(cell_x == 3 && (cell_y == 1 || cell_y == 5)) ||
|
||||
(cell_x == 4 && (cell_y == 1 || cell_y == 5)) ||
|
||||
(cell_x == 5 && cell_y > 0 && cell_y < 6)
|
||||
;
|
||||
}
|
||||
|
||||
static
|
||||
bool roi_in_bg(int w, int h, Point p)
|
||||
{
|
||||
int cell_x = p.x * 7 / w;
|
||||
int cell_y = p.y * 7 / h;
|
||||
return cell_in_bg(cell_x, cell_y);
|
||||
}
|
||||
|
||||
static
|
||||
void roi_mask(Mat &img, int margin_pct)
|
||||
{
|
||||
int counts[256] = { 0 };
|
||||
for (int i = 0; i < img.cols; i++) {
|
||||
for (int j = 0; j < img.rows; j++) {
|
||||
uint8_t p = img.at<uint8_t>(Point(i, j));
|
||||
counts[p]++;
|
||||
}
|
||||
}
|
||||
int cut = 20;
|
||||
int seen = 0;
|
||||
int total = img.cols * img.rows;
|
||||
int p05, p95;
|
||||
for (p05 = 0; seen < total * cut / 100 && p05 < 256; p05++) {
|
||||
seen += counts[p05];
|
||||
}
|
||||
|
||||
seen = 0;
|
||||
for (p95 = 0; seen < total * (100 - cut) / 100 && p95 < 256; p95++) {
|
||||
seen += counts[p95];
|
||||
}
|
||||
|
||||
printf("p05: %d, p95: %d\n", p05, p95);
|
||||
int cap = (p95 - p05) * margin_pct / 100;
|
||||
int min_thres = p05 + cap;
|
||||
int max_thres = p95 - cap;
|
||||
|
||||
for (int i = 0; i < img.cols; i++) {
|
||||
for (int j = 0; j < img.rows; j++) {
|
||||
auto pos = Point(i, j);
|
||||
uint8_t p = img.at<uint8_t>(pos);
|
||||
if (!roi_in_bg(img.cols, img.rows, pos)) {
|
||||
img.at<uint8_t>(pos) = 0;
|
||||
} else if (p < min_thres) {
|
||||
img.at<uint8_t>(pos) = 0;
|
||||
} else if (p > max_thres) {
|
||||
img.at<uint8_t>(pos) = 0;
|
||||
} else {
|
||||
img.at<uint8_t>(pos) = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static
|
||||
vector<float> roi_extract_features(Mat &img)
|
||||
{
|
||||
vector<int> ones(49, 0);
|
||||
vector<int> zeroes(49, 0);
|
||||
for (int i = 0; i < img.cols; i++) {
|
||||
for (int j = 0; j < img.rows; j++) {
|
||||
auto pos = Point(i, j);
|
||||
int cell_x = pos.x * 7 / img.cols;
|
||||
int cell_y = pos.y * 7 / img.rows;
|
||||
int idx = cell_y * 7 + cell_x;
|
||||
assert(idx < 49);
|
||||
|
||||
uint8_t p = img.at<uint8_t>(pos);
|
||||
if (p) {
|
||||
ones[idx]++;
|
||||
} else {
|
||||
zeroes[idx]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
printf("ones:\n");
|
||||
for (int i = 0; i < 49; i++) {
|
||||
printf("%d ", ones[i]);
|
||||
}
|
||||
printf("\n");
|
||||
vector<float> ret;
|
||||
for (int i = 0; i < 49; i++) {
|
||||
int cell_x = i % 7;
|
||||
int cell_y = i / 7;
|
||||
if (!cell_in_bg(cell_x, cell_y)) {
|
||||
continue;
|
||||
}
|
||||
if (ones[i] || zeroes[i]) {
|
||||
ret.push_back(ones[i] / (float)(ones[i] + zeroes[i]));
|
||||
} else {
|
||||
ret.push_back(0);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static
|
||||
float mean(vector<float> &a)
|
||||
{
|
||||
float sum = 0;
|
||||
|
||||
if (!a.size()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (auto x: a) {
|
||||
sum += x;
|
||||
}
|
||||
|
||||
return sum / a.size();
|
||||
}
|
||||
|
||||
static
|
||||
float covariance(vector<float> &a, vector<float> &b)
|
||||
{
|
||||
float mean_a = mean(a);
|
||||
float mean_b = mean(b);
|
||||
float ret = 0;
|
||||
|
||||
if (a.size() != b.size()) return 0;
|
||||
|
||||
for (size_t i = 0; i < a.size(); i++) {
|
||||
ret += (a[i] - mean_a) * (b[i] - mean_b);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static inline
|
||||
bool valid_point(Mat &a, Point p)
|
||||
{
|
||||
return p.x > 0 && p.x < a.cols && p.y > 0 && p.y < a.rows;
|
||||
}
|
||||
|
||||
static inline
|
||||
bool fuzzy_pixel_match(Mat &a, Point pa, Mat &b, Point pb)
|
||||
{
|
||||
if (!valid_point(a, pa) || !valid_point(b, pb)) return false;
|
||||
return a.at<uint8_t>(pa) == b.at<uint8_t>(pb);
|
||||
}
|
||||
|
||||
static
|
||||
int fuzzy_pixel_cmp(Mat &b, Mat &a)
|
||||
{
|
||||
int ret = 0;
|
||||
int w = a.cols;
|
||||
int h = a.rows;
|
||||
assert(a.cols == b.cols);
|
||||
assert(a.rows == b.rows);
|
||||
for (int i = 0; i < w; i++) {
|
||||
for (int j = 0; j < h; j++) {
|
||||
Point p(i, j);
|
||||
if (!roi_in_bg(w, h, p)) {
|
||||
ret++;
|
||||
continue;
|
||||
}
|
||||
bool same = false;
|
||||
int fuzziness = 1;
|
||||
for (int ii = -fuzziness; ii <= fuzziness; ii++) {
|
||||
for (int jj = -fuzziness; jj <= fuzziness; jj++) {
|
||||
if (fuzzy_pixel_match(a, p, b, Point(i + ii, j + jj))) {
|
||||
same = true;
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
}
|
||||
out:
|
||||
ret += same ? 1 : 0;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
double emblem_roi_similarity(SimilarityAlg alg, InputArray std_in, InputArray frame_roi_in, string &err)
|
||||
{
|
||||
Mat stdm = *(Mat *)std_in.getObj();
|
||||
Mat frame_roi = *(Mat *)frame_roi_in.getObj();
|
||||
err = "";
|
||||
|
||||
Mat frame_gray = adaptive_gray(frame_roi);
|
||||
Mat std_gray = adaptive_gray(stdm);
|
||||
|
||||
resize(frame_gray, frame_gray, std_gray.size());
|
||||
|
||||
double ret = 0;
|
||||
Mat frame = frame_gray.clone();
|
||||
Mat std = std_gray.clone();
|
||||
|
||||
roi_mask(frame, 20);
|
||||
roi_mask(std, 30);
|
||||
|
||||
double same = fuzzy_pixel_cmp(frame, std);
|
||||
double total = frame.rows * frame.cols;
|
||||
double sim = same / total;
|
||||
printf("same: %lf, total: %lf, sim: %lf\n", same, total, sim);
|
||||
|
||||
auto std_feature = roi_extract_features(std);
|
||||
auto frame_feature = roi_extract_features(frame);
|
||||
|
||||
printf("\nstd:");
|
||||
for (auto x: std_feature) {
|
||||
printf("%.2lf ", x * 100);
|
||||
}
|
||||
|
||||
printf("\nfrm:");
|
||||
for (auto x: frame_feature) {
|
||||
printf("%.2lf ", x * 100);
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
double cov = covariance(std_feature, frame_feature);
|
||||
printf("cov: %lf\n", cov);
|
||||
double t = cov * sim;
|
||||
ret = ret > t ? ret : t;
|
||||
return ret;
|
||||
}
|
||||
48
alg/libqr.h
Normal file
48
alg/libqr.h
Normal file
@ -0,0 +1,48 @@
|
||||
#ifndef LIBQR_H
|
||||
#define LIBQR_H
|
||||
#include "opencv2/highgui.hpp"
|
||||
#include "opencv2/imgproc.hpp"
|
||||
#include "opencv2/core.hpp"
|
||||
#include "opencv2/calib3d.hpp"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace cv;
|
||||
using namespace std;
|
||||
typedef Mat CvImg;
|
||||
|
||||
struct ProcessState {
|
||||
CvImg *orig;
|
||||
std::vector<Point> qr_points;
|
||||
float scale;
|
||||
Mat transform;
|
||||
CvImg preprocessed;
|
||||
CvImg straighten;
|
||||
Rect qr_rect_in_straighten;
|
||||
CvImg qr_straighten;
|
||||
CvImg dot_area;
|
||||
CvImg dot_area_gray;
|
||||
string qrcode = "";
|
||||
double clarity;
|
||||
float laplacian_thres = 0.1;
|
||||
};
|
||||
|
||||
bool preprocess(ProcessState &ps);
|
||||
bool emblem_dot_angle(ProcessState &ps, cv::InputArray in, float &angle, std::string &qrcode, std::string &err);
|
||||
bool detect_qr(ProcessState &ps, float margin_ratio, bool warp, string &err);
|
||||
enum SimilarityAlg {
|
||||
CellWeight,
|
||||
FuzzyPixelCmp,
|
||||
};
|
||||
double emblem_roi_similarity(SimilarityAlg alg, InputArray a, InputArray b, string &err);
|
||||
double laplacian(Mat &gray, string &err);
|
||||
|
||||
static inline void showimg_(const char *title, Mat &img) {
|
||||
imshow(title, img);
|
||||
waitKey(0);
|
||||
}
|
||||
|
||||
#define show(img) showimg_(#img, img)
|
||||
|
||||
|
||||
#endif
|
||||
169
alg/mq_worker.cpp
Normal file
169
alg/mq_worker.cpp
Normal file
@ -0,0 +1,169 @@
|
||||
#include <thread>
|
||||
#include <iostream>
|
||||
#include <json/json.h>
|
||||
#include "base64.h"
|
||||
#include "mq_worker.h"
|
||||
|
||||
#include <pulsar/Client.h>
|
||||
using namespace pulsar;
|
||||
using namespace std;
|
||||
|
||||
// Define a simple struct with string fields
|
||||
struct MqMessage {
|
||||
string space;
|
||||
string path;
|
||||
string result_topic;
|
||||
vector<uint8_t> bytes;
|
||||
};
|
||||
|
||||
struct Response {
|
||||
string path;
|
||||
bool succeeded;
|
||||
string result_path;
|
||||
vector<uint8_t> output;
|
||||
size_t size;
|
||||
string error;
|
||||
};
|
||||
|
||||
static
|
||||
MqMessage parse_message(const std::string& str) {
|
||||
Json::CharReaderBuilder builder;
|
||||
|
||||
Json::Value root;
|
||||
std::istringstream jsonStream(str);
|
||||
Json::parseFromStream(builder, jsonStream, &root, nullptr);
|
||||
|
||||
MqMessage msg;
|
||||
msg.space = root["space"].asString();
|
||||
msg.path = root["path"].asString();
|
||||
msg.result_topic = root["result_topic"].asString();
|
||||
msg.bytes = base64_decode(root["data_b64"].asString());
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
static
|
||||
std::string response_to_json(const Response& msg) {
|
||||
Json::Value root;
|
||||
root["path"] = msg.path;
|
||||
root["succeeded"] = msg.succeeded;
|
||||
root["size"] = msg.size;
|
||||
if (msg.error.size()) {
|
||||
root["error"] = msg.error;
|
||||
}
|
||||
Json::Value ofs;
|
||||
Json::Value of;
|
||||
of["path"] = msg.result_path;
|
||||
of["data_b64"] = base64_encode(msg.output.data(), msg.output.size());
|
||||
ofs.append(of);
|
||||
root["output_files"] = ofs;
|
||||
|
||||
Json::StreamWriterBuilder builder;
|
||||
std::string str = Json::writeString(builder, root);
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
int mq_worker(const char *topic, const char *worker_name, handler_fn handle_image) {
|
||||
Client client("pulsar://localhost:6650");
|
||||
|
||||
Producer producer;
|
||||
string prev_producer_topic;
|
||||
|
||||
Consumer consumer;
|
||||
ConsumerConfiguration config;
|
||||
config.setConsumerType(ConsumerShared);
|
||||
config.setSubscriptionInitialPosition(InitialPositionEarliest);
|
||||
Result result = client.subscribe(topic, worker_name, config, consumer);
|
||||
if (result != ResultOk) {
|
||||
cout << "Failed to subscribe: " << result << endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
Message mq_msg;
|
||||
int processed = 0;
|
||||
int failed = 0;
|
||||
while (1) {
|
||||
consumer.receive(mq_msg);
|
||||
auto payload = mq_msg.getDataAsString();
|
||||
auto msg = parse_message(payload);
|
||||
if (processed % 1000 == 0) {
|
||||
cout << processed << ": " << msg.path << " " << msg.bytes.size() << endl;
|
||||
}
|
||||
Response resp;
|
||||
resp.path = msg.path;
|
||||
resp.size = msg.bytes.size();
|
||||
resp.succeeded = true;
|
||||
int r = handle_image(msg.path,
|
||||
msg.bytes,
|
||||
resp.result_path,
|
||||
resp.output);
|
||||
if (r) {
|
||||
resp.succeeded = false;
|
||||
resp.error = string("error ") + to_string(r);
|
||||
}
|
||||
auto reply = response_to_json(resp);
|
||||
|
||||
if (prev_producer_topic != msg.result_topic) {
|
||||
Result result = client.createProducer(msg.result_topic, producer);
|
||||
if (result != ResultOk) {
|
||||
cerr << "Error creating producer: " << result << endl;
|
||||
return -1;
|
||||
}
|
||||
prev_producer_topic = msg.result_topic;
|
||||
}
|
||||
Message result_msg = MessageBuilder().setContent(reply).build();
|
||||
Result result = producer.send(result_msg);
|
||||
if (result != ResultOk) {
|
||||
cerr << "Error sending reply: " << result << endl;
|
||||
consumer.negativeAcknowledge(mq_msg);
|
||||
failed++;
|
||||
} else {
|
||||
processed++;
|
||||
consumer.acknowledge(mq_msg);
|
||||
}
|
||||
if (processed % 1000 == 0) {
|
||||
cout << "processed: " << processed << ", failed: " << failed << endl;
|
||||
}
|
||||
}
|
||||
|
||||
client.close();
|
||||
return 0;
|
||||
|
||||
}
|
||||
|
||||
#if 0
|
||||
static
|
||||
int test_pulsar_worker() {
|
||||
Client client("pulsar://localhost:6650");
|
||||
|
||||
Producer producer;
|
||||
|
||||
Result result = client.createProducer("persistent://public/default/my-topic", producer);
|
||||
if (result != ResultOk) {
|
||||
std::cout << "Error creating producer: " << result << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Send 100 messages synchronously
|
||||
int ctr = 0;
|
||||
while (ctr < 100) {
|
||||
std::string content = "msg" + std::to_string(ctr);
|
||||
Message msg = MessageBuilder().setContent(content).setProperty("x", "1").build();
|
||||
Result result = producer.send(msg);
|
||||
if (result != ResultOk) {
|
||||
std::cout << "The message " << content << " could not be sent, received code: " << result << std::endl;
|
||||
} else {
|
||||
std::cout << "The message " << content << " sent successfully" << std::endl;
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
ctr++;
|
||||
}
|
||||
|
||||
std::cout << "Finished producing synchronously!" << std::endl;
|
||||
|
||||
client.close();
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
12
alg/mq_worker.h
Normal file
12
alg/mq_worker.h
Normal file
@ -0,0 +1,12 @@
|
||||
#ifndef _MQ_WORKER_H_
|
||||
#define _MQ_WORKER_H_
|
||||
#include <stdint.h>
|
||||
#include <vector>
|
||||
|
||||
typedef int (*handler_fn)(const std::string &input_path,
|
||||
const std::vector<uint8_t> &input,
|
||||
std::string &output_path,
|
||||
std::vector<uint8_t> &output);
|
||||
int mq_worker(const char *topic, const char *worker_name, handler_fn handle_image);
|
||||
|
||||
#endif
|
||||
1
alg/post.wx.js
Normal file
1
alg/post.wx.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = Module;
|
||||
15
alg/pre.wx.js
Normal file
15
alg/pre.wx.js
Normal file
@ -0,0 +1,15 @@
|
||||
var window = {};
|
||||
var WA = WXWebAssembly;
|
||||
var WebAssembly = WA;
|
||||
WebAssembly.RuntimeErrror = Error;
|
||||
var performance = {
|
||||
now: Date.now,
|
||||
};
|
||||
Module['instantiateWasm'] = (info, receiveInstance) => {
|
||||
console.log("loading wasm...", info);
|
||||
WebAssembly.instantiate("assets/qrtool.wx.wasm.br", info).then((result) => {
|
||||
console.log("result:", result);
|
||||
var inst = result['instance'];
|
||||
receiveInstance(inst);
|
||||
});
|
||||
}
|
||||
107
alg/qr.ipynb
Normal file
107
alg/qr.ipynb
Normal file
File diff suppressed because one or more lines are too long
646
alg/qrtool.cpp
Normal file
646
alg/qrtool.cpp
Normal file
@ -0,0 +1,646 @@
|
||||
// This file is part of OpenCV project.
|
||||
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
||||
// of this distribution and at http://opencv.org/license.html.
|
||||
//
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include "opencv2/objdetect.hpp"
|
||||
#include "mq_worker.h"
|
||||
#if ENABLE_GRPC
|
||||
#include "fileprocess.h"
|
||||
#endif
|
||||
#include "http.h"
|
||||
#include "libqr.h"
|
||||
|
||||
static
|
||||
int detect_cmd(char **argv, int argc)
|
||||
{
|
||||
char *file = argv[0];
|
||||
auto orig = imread(file);
|
||||
QRCodeDetector detector;
|
||||
|
||||
Mat points;
|
||||
Mat straight;
|
||||
auto r = detector.detectAndDecode(orig, points, straight);
|
||||
printf("r: %s\n", r.c_str());
|
||||
printf("points: %d %d\n", points.rows, points.cols);
|
||||
for (int i = 0; i < points.cols; i++) {
|
||||
printf("%f ", points.at<float>(0, i));
|
||||
}
|
||||
printf("\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static
|
||||
int angle_cmd(char **argv, int argc)
|
||||
{
|
||||
char *file = argv[0];
|
||||
printf("file: %s\n", file);
|
||||
Mat orig = imread(file);
|
||||
string qrcode, err;
|
||||
float angle;
|
||||
ProcessState ps;
|
||||
auto r = emblem_dot_angle(ps, orig, angle, qrcode, err);
|
||||
|
||||
if (!r) {
|
||||
cerr << r << ":" << err << endl;
|
||||
return 1;
|
||||
}
|
||||
printf("angle: %.1f\n", angle);
|
||||
printf("qrcode: %s\n", qrcode.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
static
|
||||
int dot_cmd(char **argv, int argc)
|
||||
{
|
||||
ProcessState ps;
|
||||
char *file = argv[0];
|
||||
printf("file: %s\n", file);
|
||||
Mat orig = imread(file);
|
||||
string qrcode, err;
|
||||
float angle;
|
||||
auto r = emblem_dot_angle(ps, orig, angle, qrcode, err);
|
||||
|
||||
if (!r) {
|
||||
cerr << r << ":" << err << endl;
|
||||
return 1;
|
||||
}
|
||||
string outfile = string(file) + ".dot.jpg";
|
||||
printf("angle: %.1f\n", angle);
|
||||
printf("qrcode: %s\n", qrcode.c_str());
|
||||
printf("saving dot file: %s\n", outfile.c_str());
|
||||
imwrite(outfile, ps.dot_area);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static
|
||||
int clarity_cmd(char **argv, int argc)
|
||||
{
|
||||
string err;
|
||||
|
||||
char *file = argv[0];
|
||||
printf("file: %s\n", file);
|
||||
Mat orig = imread(file);
|
||||
Mat gray;
|
||||
cvtColor(orig, gray, COLOR_BGR2GRAY);
|
||||
auto c = laplacian(gray, err);
|
||||
printf("clarity: %lf\n", c);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static
|
||||
int rectify_cmd(char **argv, int argc)
|
||||
{
|
||||
char *file = argv[0];
|
||||
string err;
|
||||
ProcessState ps;
|
||||
Mat orig = imread(file);
|
||||
|
||||
ps.orig = &orig;
|
||||
preprocess(ps);
|
||||
|
||||
if (!detect_qr(ps, 0.20, false, err)) {
|
||||
cerr << err << endl;
|
||||
return 1;
|
||||
}
|
||||
string outfile = string(file) + ".qr.jpg";
|
||||
imwrite(outfile, ps.straighten);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static
|
||||
int topleft_cmd(char **argv, int argc)
|
||||
{
|
||||
char *file = argv[0];
|
||||
string err;
|
||||
ProcessState ps;
|
||||
Mat orig = imread(file);
|
||||
|
||||
ps.orig = &orig;
|
||||
preprocess(ps);
|
||||
|
||||
if (!detect_qr(ps, 0.02, true, err)) {
|
||||
cerr << err << endl;
|
||||
return 1;
|
||||
}
|
||||
string outfile = string(file) + ".topleft.jpg";
|
||||
Mat &base = ps.straighten;
|
||||
auto crop = Rect(0, 0, base.cols / 2, base.rows / 2);
|
||||
Mat result = base(crop);
|
||||
imwrite(outfile, result);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static
|
||||
int find_roi_start_point(Mat &bin, Point &p)
|
||||
{
|
||||
int npoints = 4;
|
||||
|
||||
for (int i = 0; i < bin.cols / 3; i++) {
|
||||
uchar sum = 0;
|
||||
for (int j = 0; j < npoints; j++) {
|
||||
int v = i + j;
|
||||
sum += bin.at<uchar>(v, v);
|
||||
}
|
||||
if (sum == 0) {
|
||||
p = Point(i, i);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
cerr << "find_roi_start_point" << endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static
|
||||
vector<int> count_black(Mat &bin, bool count_rows, int size)
|
||||
{
|
||||
vector<int> ret;
|
||||
for (int i = 0; i < size; i++) {
|
||||
int count = 0;
|
||||
for (int j = 0; j < size; j++) {
|
||||
int x = count_rows ? j : i;
|
||||
int y = count_rows ? i : j;
|
||||
if (bin.at<uchar>(y, x) == 0) count++;
|
||||
}
|
||||
ret.push_back(count);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int find_start_of_first_black_range(const vector<int> &data)
|
||||
{
|
||||
size_t i = 0;
|
||||
int m = *std::max_element(data.begin(), data.end());
|
||||
int thres = m * 50 / 100;
|
||||
while (i < data.size() - 3) {
|
||||
if (data[i] >= thres && data[i + 1] >= thres && data[i + 2] >= thres) {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (i >= data.size() - 3) return -1;
|
||||
return i;
|
||||
}
|
||||
|
||||
static int find_end_of_first_black_range(const vector<int> &data)
|
||||
{
|
||||
size_t i = 0;
|
||||
int m = *std::max_element(data.begin(), data.end());
|
||||
int thres = m * 50 / 100;
|
||||
while (i < data.size() - 3) {
|
||||
if (data[i] >= thres && data[i + 1] >= thres && data[i + 2] >= thres) {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (i >= data.size() - 3) return -1;
|
||||
while (i < data.size() - 3) {
|
||||
if (data[i] < thres || data[i + 1] < thres || data[i + 2] < thres) {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (i >= data.size() - 3) return -1;
|
||||
return i;
|
||||
}
|
||||
|
||||
static
|
||||
int find_roi_rect(Mat &bin, Point &start, Rect &rect, bool inner, string &err)
|
||||
{
|
||||
Mat visited;
|
||||
bin.copyTo(visited);
|
||||
vector<Point> q;
|
||||
q.push_back(start);
|
||||
int min_x = bin.rows, min_y = bin.rows, max_x = 0, max_y = 0;
|
||||
int orig_size = max(bin.rows, bin.cols);
|
||||
|
||||
while (q.size()) {
|
||||
Point p = q.back();
|
||||
q.pop_back();
|
||||
visited.at<uchar>(p.y, p.x) = 255;
|
||||
min_x = min(min_x, p.x);
|
||||
min_y = min(min_y, p.y);
|
||||
max_x = max(max_x, p.x);
|
||||
max_y = max(max_y, p.y);
|
||||
for (int xoff = -1; xoff <= 1; xoff++) {
|
||||
for (int yoff = -1; yoff <= 1; yoff++) {
|
||||
int x = p.x + xoff;
|
||||
int y = p.y + yoff;
|
||||
if (x >= 0 && x < visited.cols && y >= 0 && y < visited.rows) {
|
||||
auto v = visited.at<uchar>(y, x);
|
||||
if (v == 0) {
|
||||
if (q.size() >= 20000) {
|
||||
err = string("roi detected range too large: ") + to_string(q.size());
|
||||
return -1;
|
||||
}
|
||||
q.push_back(Point(x, y));
|
||||
visited.at<uchar>(y, x) = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (max_x - min_x < 50 || max_y - min_y < 50) {
|
||||
err = "detected roi outer region too small";
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto size = std::max(max_x, max_y);
|
||||
|
||||
auto row_sums = count_black(bin, true, size);
|
||||
auto col_sums = count_black(bin, false, size);
|
||||
|
||||
min_x = inner ? find_end_of_first_black_range(col_sums) : find_start_of_first_black_range(col_sums);
|
||||
min_y = inner ? find_end_of_first_black_range(row_sums) : find_start_of_first_black_range(row_sums);
|
||||
|
||||
if (min_x < 0 || min_y < 0) {
|
||||
err = "min_x or min_y is negative";
|
||||
return -1;
|
||||
}
|
||||
|
||||
// find the max values, similarly
|
||||
std::reverse(col_sums.begin(), col_sums.end());
|
||||
std::reverse(row_sums.begin(), row_sums.end());
|
||||
max_x = size - (inner ? find_end_of_first_black_range(col_sums) : find_start_of_first_black_range(col_sums));
|
||||
max_y = size - (inner ? find_end_of_first_black_range(row_sums) : find_start_of_first_black_range(row_sums));
|
||||
|
||||
if (max_x < 0 || max_y < 0) return -1;
|
||||
|
||||
if (max_x - min_x < 50 || max_y - min_y < 50) {
|
||||
err = "detected roi region too small";
|
||||
return -1;
|
||||
}
|
||||
size = (max_x - min_x + max_y - min_y) / 2;
|
||||
|
||||
if (size < orig_size / 5 || size > orig_size * 3 / 5) {
|
||||
err = "size of found region is out of valid range";
|
||||
return -1;
|
||||
}
|
||||
rect = Rect(min_x + 1, min_y + 1, size, size);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void get_bin(Mat &orig, Mat &out)
|
||||
{
|
||||
Mat gray;
|
||||
Mat filtered;
|
||||
Point start;
|
||||
Rect roi_rect;
|
||||
cvtColor(orig, gray, COLOR_BGR2GRAY);
|
||||
convertScaleAbs(gray, gray, 2);
|
||||
// bilateralFilter(gray, filtered, 9, 150, 150, BORDER_DEFAULT);
|
||||
medianBlur(gray, gray, 9);
|
||||
threshold(gray, out, 128, 255, THRESH_BINARY);
|
||||
}
|
||||
|
||||
static
|
||||
int find_roi(Mat &qr_straighten, Mat &roi, bool inner, string &err)
|
||||
{
|
||||
Mat bin;
|
||||
Rect topleft_r(0, 0, qr_straighten.cols / 2, qr_straighten.rows / 2);
|
||||
Mat topleft = qr_straighten(topleft_r);
|
||||
get_bin(topleft, bin);
|
||||
|
||||
Point start;
|
||||
Rect roi_rect;
|
||||
auto r = find_roi_start_point(bin, start);
|
||||
if (r) {
|
||||
err = "failed to find roi start point";
|
||||
return r;
|
||||
}
|
||||
r = find_roi_rect(bin, start, roi_rect, inner, err);
|
||||
if (r) {
|
||||
return r;
|
||||
}
|
||||
roi = qr_straighten(roi_rect);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int roi_process_one(const char *file, bool inner, string &err, bool warp, Mat *roi_out)
|
||||
{
|
||||
Mat roi;
|
||||
Mat orig = imread(file, IMREAD_COLOR);
|
||||
Mat qr_with_margin;
|
||||
|
||||
if (warp) {
|
||||
ProcessState ps;
|
||||
ps.orig = &orig;
|
||||
preprocess(ps);
|
||||
if (!detect_qr(ps, 0.02, true, err)) {
|
||||
cerr << err << ":" << file << endl;
|
||||
return 1;
|
||||
}
|
||||
qr_with_margin = ps.straighten;
|
||||
} else {
|
||||
qr_with_margin = orig;
|
||||
}
|
||||
if (qr_with_margin.cols <= 0 || qr_with_margin.rows <= 0) return -1;
|
||||
auto r = find_roi(qr_with_margin, roi, inner, err);
|
||||
if (r) return r;
|
||||
if (roi_out) {
|
||||
*roi_out = roi;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool is_dir(char *path)
|
||||
{
|
||||
struct stat st;
|
||||
|
||||
if (lstat(path, &st) == 0) {
|
||||
if (S_ISREG(st.st_mode)) {
|
||||
return false;
|
||||
} else if (S_ISDIR(st.st_mode)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
perror("Error in lstat");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void save_roi(string orig_file, Mat &roi)
|
||||
{
|
||||
string outfile = orig_file + ".roi.jpg";
|
||||
cout << "save: " << outfile << endl;
|
||||
imwrite(outfile, roi);
|
||||
}
|
||||
|
||||
static
|
||||
int frame_roi_cmd(char **argv, int argc)
|
||||
{
|
||||
char *file = argv[0];
|
||||
string err;
|
||||
int ret = 0;
|
||||
Mat roi;
|
||||
|
||||
cout << "frame roi processing: " << file << endl;
|
||||
ret = roi_process_one(file, false, err, true, &roi);
|
||||
if (ret) {
|
||||
cerr << "failed to process: " << file << ":" << err <<endl;
|
||||
return ret;
|
||||
}
|
||||
save_roi(file, roi);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static
|
||||
int roi_cmd(char **argv, int argc)
|
||||
{
|
||||
char *file = argv[0];
|
||||
string err;
|
||||
int ret = 0;
|
||||
|
||||
cout << "roi processing: " << file << endl;
|
||||
if (is_dir(file)) {
|
||||
for (auto const& dir_entry : filesystem::directory_iterator{file}) {
|
||||
auto path = dir_entry.path();
|
||||
Mat roi;
|
||||
if (roi_process_one(path.c_str(), false, err, false, &roi) != 0) {
|
||||
cerr << "failed: " << path << ":" << err << endl;
|
||||
ret = 1;
|
||||
}
|
||||
save_roi(file, roi);
|
||||
}
|
||||
} else {
|
||||
Mat roi;
|
||||
ret = roi_process_one(file, false, err, false, &roi);
|
||||
if (ret) {
|
||||
cerr << "failed to process: " << file << ":" << err <<endl;
|
||||
} else {
|
||||
save_roi(file, roi);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static
|
||||
int roi_bench_cmd(char **argv, int argc)
|
||||
{
|
||||
char *file = argv[0];
|
||||
int n = 0;
|
||||
string err;
|
||||
auto begin = chrono::system_clock::now();
|
||||
for (auto const& dir_entry : filesystem::directory_iterator{file}) {
|
||||
auto path = dir_entry.path();
|
||||
if (roi_process_one(path.c_str(), false, err, false, NULL) == 0) {
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
auto end = chrono::system_clock::now();
|
||||
std::chrono::duration<float> duration = end - begin;
|
||||
float seconds = duration.count();
|
||||
printf("qps: %.1f\n", n / seconds);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if USE_PULSAR
|
||||
static vector<string> split_path(const string &path)
|
||||
{
|
||||
vector<string> ret;
|
||||
string cur = "";
|
||||
for (auto x: path) {
|
||||
if (x == '/') {
|
||||
if (cur.size()) {
|
||||
ret.push_back(cur);
|
||||
cur = "";
|
||||
}
|
||||
} else {
|
||||
cur += x;
|
||||
}
|
||||
}
|
||||
if (cur.size()) {
|
||||
ret.push_back(cur);
|
||||
cur = "";
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static string join_path(const vector<string> fs)
|
||||
{
|
||||
string ret;
|
||||
const string sep = "/";
|
||||
|
||||
for (auto x: fs) {
|
||||
ret += sep + x;
|
||||
}
|
||||
if (ret.size() == 0) ret = "/";
|
||||
return ret;
|
||||
}
|
||||
|
||||
static string get_output_path(const string &path)
|
||||
{
|
||||
auto ret = split_path(path);
|
||||
ret[3] = "roi";
|
||||
return join_path(ret);
|
||||
}
|
||||
|
||||
static
|
||||
int roi_worker_handle_image(const string &input_path,
|
||||
const vector<uint8_t> &input,
|
||||
string &output_path,
|
||||
vector<uint8_t> &output)
|
||||
{
|
||||
Mat roi;
|
||||
Mat orig = imdecode(input, IMREAD_COLOR);
|
||||
|
||||
auto r = find_roi(orig, roi, true, true);
|
||||
if (r) return r;
|
||||
imencode(".jpg", roi, output);
|
||||
output_path = get_output_path(input_path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static
|
||||
int roi_worker_handle_image_nop(const string &input_path,
|
||||
const vector<uint8_t> &input,
|
||||
string &output_path,
|
||||
vector<uint8_t> &output)
|
||||
{
|
||||
output.push_back('f');
|
||||
output.push_back('o');
|
||||
output.push_back('o');
|
||||
output_path = get_output_path(input_path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static
|
||||
int roi_worker_cmd(char *topic)
|
||||
{
|
||||
string worker_name = "roi-worker-";
|
||||
worker_name += to_string(rand());
|
||||
return mq_worker(topic, "roi-worker", roi_worker_handle_image);
|
||||
}
|
||||
|
||||
static
|
||||
int roi_worker_nop_cmd(char *topic)
|
||||
{
|
||||
string worker_name = "roi-worker-";
|
||||
worker_name += to_string(rand());
|
||||
return mq_worker(topic, "roi-worker", roi_worker_handle_image_nop);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if ENABLE_GRPC
|
||||
static
|
||||
int grpc_server_cmd(char *addr)
|
||||
{
|
||||
return run_server(addr, roi_worker_handle_image);
|
||||
}
|
||||
#endif
|
||||
|
||||
static
|
||||
int http_server_handle_image(const vector<uint8_t> &input,
|
||||
vector<uint8_t> &output)
|
||||
{
|
||||
string err;
|
||||
Mat roi;
|
||||
Mat orig = imdecode(input, IMREAD_COLOR);
|
||||
|
||||
if (orig.empty()) {
|
||||
return -EINVAL;
|
||||
}
|
||||
auto r = find_roi(orig, roi, false, err);
|
||||
if (r) return r;
|
||||
imencode(".jpg", roi, output);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static
|
||||
int http_server_cmd(char **argv, int argc)
|
||||
{
|
||||
char *port = argv[0];
|
||||
return start_http_server(atoi(port), http_server_handle_image);
|
||||
}
|
||||
|
||||
static
|
||||
int verify_cmd(char **args, int nargs)
|
||||
{
|
||||
char *std_file = args[0];
|
||||
char *frame_file = args[1];
|
||||
Mat std = imread(std_file);
|
||||
Mat frame = imread(frame_file);
|
||||
Mat roi;
|
||||
int r;
|
||||
|
||||
string err;
|
||||
r = roi_process_one(frame_file, false, err, true, &roi);
|
||||
if (r) {
|
||||
printf("failed to find roi: %s\n", err.c_str());
|
||||
return r;
|
||||
}
|
||||
double s = emblem_roi_similarity(FuzzyPixelCmp, std, roi, err);
|
||||
|
||||
if (err.size()) {
|
||||
printf("err: %s\n", err.c_str());
|
||||
return 1;
|
||||
} else {
|
||||
printf("similarity: %f\n", s); return 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static
|
||||
void usage(const char *name, vector<string> &cmds)
|
||||
{
|
||||
printf("usage: %s <cmd> <arg0>\n", name);
|
||||
printf("or for 2 args: %s <cmd> <arg0> <arg1>\n", name);
|
||||
printf("possible commands:\n");
|
||||
for (auto cmd: cmds) {
|
||||
printf(" %s\n", cmd.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef QRTOOL_MAIN
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
string cmd = "help";
|
||||
if (argc > 1) {
|
||||
cmd = argv[1];
|
||||
}
|
||||
vector<string> cmds;
|
||||
|
||||
#define add_cmd(c, nargs) \
|
||||
do { \
|
||||
cmds.push_back(#c); \
|
||||
if (cmd == #c && argc >= 2 + nargs) return c##_cmd(&argv[2], argc - 2); \
|
||||
} while (0)
|
||||
|
||||
add_cmd(detect, 1);
|
||||
add_cmd(angle, 1);
|
||||
add_cmd(dot, 1);
|
||||
add_cmd(clarity, 1);
|
||||
add_cmd(rectify, 1);
|
||||
add_cmd(topleft, 1);
|
||||
add_cmd(frame_roi, 1);
|
||||
add_cmd(roi, 1);
|
||||
add_cmd(roi_bench, 1);
|
||||
#if USE_PULSAR
|
||||
add_cmd(roi_worker, 1);
|
||||
add_cmd(roi_worker_nop, 1);
|
||||
#endif
|
||||
#if ENABLE_GRPC
|
||||
add_cmd(grpc_server, 1);
|
||||
#endif
|
||||
add_cmd(http_server, 1);
|
||||
add_cmd(verify, 2);
|
||||
usage(argv[0], cmds);
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
59
alg/qrtool_wasm.cpp
Normal file
59
alg/qrtool_wasm.cpp
Normal file
@ -0,0 +1,59 @@
|
||||
#include "opencv2/highgui.hpp"
|
||||
#include "opencv2/imgproc.hpp"
|
||||
#include "opencv2/core.hpp"
|
||||
#include "opencv2/calib3d.hpp"
|
||||
#include "libqr.h"
|
||||
|
||||
using namespace cv;
|
||||
using namespace std;
|
||||
|
||||
static
|
||||
std::string make_resp(bool ok, string err, int angle = -1, string qrcode = "", double elapsed = 0)
|
||||
{
|
||||
char buf[512];
|
||||
snprintf(buf, sizeof(buf), R"({ "ok": %s, "err": "%s", "qrcode": "%s", "angle": %d, "elapsed": %lf })",
|
||||
ok ? "true" : "false",
|
||||
err.c_str(),
|
||||
qrcode.c_str(),
|
||||
angle,
|
||||
elapsed
|
||||
);
|
||||
return string(buf);
|
||||
}
|
||||
|
||||
|
||||
extern "C" {
|
||||
const char *qrtool_angle(uint8_t *data, int width, int height, uint8_t *dot_area, float camera_sensitivity) {
|
||||
ProcessState ps;
|
||||
ps.laplacian_thres = camera_sensitivity / 10.0;
|
||||
|
||||
auto start = std::chrono::system_clock::now();
|
||||
static char ret[512];
|
||||
printf("qrtool_angle, width: %d height %d\n", width, height);
|
||||
for (int i = 0; i < 16; i++) {
|
||||
printf("%d ", data[i]);
|
||||
}
|
||||
printf("\n");
|
||||
Mat orig(Size(width, height), CV_8UC4, data);
|
||||
printf("mat: %d %d\n", orig.cols, orig.rows);
|
||||
string qrcode, err;
|
||||
float angle;
|
||||
auto ok = emblem_dot_angle(ps, orig, angle, qrcode, err);
|
||||
auto end = std::chrono::system_clock::now();
|
||||
std::chrono::duration<double> elapsed = end-start;
|
||||
auto x = make_resp(ok, err, angle, qrcode, elapsed.count());
|
||||
if (dot_area) {
|
||||
if (!ps.dot_area.empty()) {
|
||||
Mat da;
|
||||
ps.dot_area.convertTo(da, CV_8UC4);
|
||||
resize(da, da, Size(32 ,32));
|
||||
memset(dot_area, 255, 32 * 32 * 4);
|
||||
memcpy(dot_area, da.ptr(), 32 * 32 * 4);
|
||||
} else {
|
||||
memset(dot_area, 55, 32 * 32 * 4);
|
||||
}
|
||||
}
|
||||
snprintf(ret, 512, "%s", x.c_str());
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
115
alg/server.py
Executable file
115
alg/server.py
Executable file
@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import json
|
||||
import argparse
|
||||
import subprocess
|
||||
import tempfile
|
||||
import base64
|
||||
from flask import Flask, request, Response
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/', methods=['GET'])
|
||||
@app.route('/alg', methods=['GET'])
|
||||
def index():
|
||||
return make_resp({})
|
||||
|
||||
def oss_save_image(data_url, file_prefix):
|
||||
ak = 'LTAI5tC2qXGxwHZUZP7DoD1A'
|
||||
sk = 'qPo9O6ZvEfqo4t8oflGEm0DoxLHJhm'
|
||||
auth = oss2.Auth(ak, sk)
|
||||
endpoint = 'oss-cn-guangzhou.aliyuncs.com'
|
||||
bucket_name = 'emblem-roi-samples'
|
||||
bucket = oss2.Bucket(auth, endpoint, bucket_name)
|
||||
|
||||
pref = 'data:image/png;base64,'
|
||||
if data_url.startswith(pref):
|
||||
decoded = base64.b64decode(data_url[len(pref):])
|
||||
fname = file_prefix + str(time.time()) + ".png"
|
||||
bucket.put_object(fname, decoded)
|
||||
|
||||
|
||||
def make_resp(content):
|
||||
resp = Response(json.dumps(content))
|
||||
resp.headers['Access-Control-Allow-Origin'] = "*"
|
||||
resp.headers['Access-Control-Allow-Methods'] = '*'
|
||||
resp.headers['Access-Control-Allow-Headers'] = '*'
|
||||
resp.headers['Content-Type'] = 'application/json'
|
||||
return resp
|
||||
|
||||
@app.route('/alg/angle', methods=['POST', 'OPTIONS'])
|
||||
def angle():
|
||||
try:
|
||||
return handle_angle_request()
|
||||
except Exception as e:
|
||||
return make_resp({
|
||||
"ok": False,
|
||||
"err": str(e),
|
||||
})
|
||||
|
||||
def handle_angle_request():
|
||||
if request.method == 'OPTIONS':
|
||||
return make_resp({})
|
||||
body = request.stream.read()
|
||||
pref = 'data:image/jpeg;base64,'
|
||||
err = "Cannot detect angle"
|
||||
angle = -1
|
||||
try:
|
||||
encoded = body.decode()
|
||||
if encoded.startswith(pref):
|
||||
decoded = base64.b64decode(encoded[len(pref):])
|
||||
else:
|
||||
decoded = body
|
||||
except:
|
||||
decoded = body
|
||||
with tempfile.NamedTemporaryFile(dir="uploads", suffix=".jpg", delete=False) as tf:
|
||||
tf.write(decoded)
|
||||
tf.flush()
|
||||
cmd = ['./qrtool', 'angle', tf.name]
|
||||
print(" ".join(cmd))
|
||||
r = subprocess.run(cmd, capture_output=True)
|
||||
if r.returncode == 0:
|
||||
lines = r.stdout.decode().splitlines()
|
||||
return make_resp({
|
||||
"ok": True,
|
||||
"qrcode": lines[0].split()[1],
|
||||
"angle": int(lines[1].split()[1])
|
||||
})
|
||||
else:
|
||||
err = r.stderr.decode()
|
||||
return make_resp({
|
||||
"ok": False,
|
||||
"err": err,
|
||||
})
|
||||
|
||||
def handle_rectify_request():
|
||||
body = request.stream.read()
|
||||
with tempfile.NamedTemporaryFile() as tf:
|
||||
tf.write(body)
|
||||
tf.flush()
|
||||
outf = tf.name + ".qr.jpg"
|
||||
cmd = ['./qrtool', 'rectify', tf.name]
|
||||
subprocess.check_call(cmd)
|
||||
with open(outf, 'rb') as of:
|
||||
resp = Response(of.read(), status=200, mimetype='image/jpeg')
|
||||
os.unlink(outf)
|
||||
return resp
|
||||
|
||||
@app.route('/alg/rectify', methods=['POST', 'OPTIONS'])
|
||||
def rectify():
|
||||
try:
|
||||
return handle_rectify_request()
|
||||
except Exception as e:
|
||||
return make_resp({
|
||||
"ok": False,
|
||||
"err": str(e),
|
||||
})
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--host", "-l", default="0.0.0.0")
|
||||
parser.add_argument("--port", "-p", type=int, default=3028)
|
||||
return parser.parse_args()
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = parse_args()
|
||||
app.run(host=args.host, port=args.port)
|
||||
11
alg/string_format.h
Normal file
11
alg/string_format.h
Normal file
@ -0,0 +1,11 @@
|
||||
template<typename ... Args>
|
||||
std::string string_format( const std::string& format, Args ... args )
|
||||
{
|
||||
int size_s = std::snprintf( nullptr, 0, format.c_str(), args ... ) + 1; // Extra space for '\0'
|
||||
if( size_s <= 0 ){ throw std::runtime_error( "Error during formatting." ); }
|
||||
auto size = static_cast<size_t>( size_s );
|
||||
std::unique_ptr<char[]> buf( new char[ size ] );
|
||||
std::snprintf( buf.get(), size, format.c_str(), args ... );
|
||||
return std::string( buf.get(), buf.get() + size - 1 ); // We don't want the '\0' inside
|
||||
}
|
||||
|
||||
BIN
alg/wechat_qrcode/detect.caffemodel
Normal file
BIN
alg/wechat_qrcode/detect.caffemodel
Normal file
Binary file not shown.
2716
alg/wechat_qrcode/detect.prototxt
Normal file
2716
alg/wechat_qrcode/detect.prototxt
Normal file
File diff suppressed because it is too large
Load Diff
BIN
alg/wechat_qrcode/sr.caffemodel
Normal file
BIN
alg/wechat_qrcode/sr.caffemodel
Normal file
Binary file not shown.
403
alg/wechat_qrcode/sr.prototxt
Normal file
403
alg/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
|
||||
}
|
||||
}
|
||||
67
alg/worker.py
Executable file
67
alg/worker.py
Executable file
@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import json
|
||||
import base64
|
||||
import tempfile
|
||||
import subprocess
|
||||
import pulsar
|
||||
|
||||
client = pulsar.Client('pulsar://localhost:6650')
|
||||
|
||||
worker_id = str(uuid.uuid4())
|
||||
producer = client.create_producer('estor')
|
||||
result_producer = None
|
||||
consumer = client.subscribe('roi', f'roi-worker-{worker_id}')
|
||||
|
||||
def import_file(fname, content):
|
||||
print('Import file', fname, len(content))
|
||||
producer.send(fname.encode() + b'\0' + content)
|
||||
|
||||
def make_roi_path(orig):
|
||||
comps = [x for x in orig.split('/') if x.strip()]
|
||||
new_comps = comps[:3] + ['roi'] + comps[4:]
|
||||
return '/' + '/'.join(new_comps)
|
||||
|
||||
def handle_qr(fname, content, result_topic):
|
||||
with tempfile.NamedTemporaryFile(suffix=".jpg") as tf:
|
||||
tf.write(content)
|
||||
tf.flush()
|
||||
cmd = ['./qrtool', 'roi', tf.name]
|
||||
subprocess.check_call(cmd)
|
||||
newfile = fname + ".roi.jpg"
|
||||
with open(tf.name + ".roi.jpg", 'rb') as f:
|
||||
roi_data = f.read()
|
||||
global result_producer
|
||||
if not result_producer or result_producer.topic() != result_topic:
|
||||
result_producer = client.create_producer(result_topic)
|
||||
roi_path = make_roi_path(fname)
|
||||
resp = {
|
||||
'path': fname,
|
||||
'succeeded': True,
|
||||
'output_files': [{
|
||||
'path': roi_path,
|
||||
'data_b64': base64.b64encode(roi_data).decode(),
|
||||
}],
|
||||
'size': len(content),
|
||||
}
|
||||
result_producer.send(json.dumps(resp).encode())
|
||||
|
||||
def roi_worker():
|
||||
while True:
|
||||
msg = consumer.receive()
|
||||
try:
|
||||
body = msg.data()
|
||||
print("Received message id='{}'".format(msg.message_id()))
|
||||
payload = json.loads(body)
|
||||
fname = payload['path']
|
||||
content = base64.b64decode(payload['data_b64'])
|
||||
handle_qr(fname, content, payload['result_topic'])
|
||||
consumer.acknowledge(msg)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
consumer.negative_acknowledge(msg)
|
||||
|
||||
# import_file("/emblem/batches/test-batch/import/test.jpg", open('/etc/fstab', 'rb').read())
|
||||
roi_worker()
|
||||
2
api/.dockerignore
Normal file
2
api/.dockerignore
Normal file
@ -0,0 +1,2 @@
|
||||
/web/node_modules
|
||||
/qrreader/target
|
||||
69
api/Makefile
Normal file
69
api/Makefile
Normal file
@ -0,0 +1,69 @@
|
||||
IMAGE_TAG ?= $(shell git describe --always)
|
||||
|
||||
.PHONY: FORCE
|
||||
|
||||
DOCKER := docker
|
||||
|
||||
all:
|
||||
$(MAKE) web
|
||||
$(MAKE) docker
|
||||
$(MAKE) push
|
||||
|
||||
docker:
|
||||
$(DOCKER) build -t emblem:$(IMAGE_TAG) .
|
||||
|
||||
push:
|
||||
for tag in $(IMAGE_TAG) $(CI_COMMIT_BRANCH); do \
|
||||
$(DOCKER) tag emblem:$(IMAGE_TAG) registry.gitlab.com/euphon/emblem:$(IMAGE_TAG) &&\
|
||||
$(DOCKER) push registry.gitlab.com/euphon/emblem:$(IMAGE_TAG); \
|
||||
done
|
||||
|
||||
docker-run:
|
||||
$(DOCKER) build --network=host -t emblem:$(IMAGE_TAG) .
|
||||
$(DOCKER) run -ti --rm -p 12345:80 emblem:$(IMAGE_TAG)
|
||||
|
||||
web: FORCE
|
||||
cd web; npm run build
|
||||
|
||||
test:
|
||||
cd api; ./manage.py migrate
|
||||
cd api; ./manage.py test tests
|
||||
|
||||
stress:
|
||||
cd api; ./manage.py test tests.stress
|
||||
|
||||
deploy: deploy-dev deploy-prod
|
||||
|
||||
deploy-dev: FORCE
|
||||
./scripts/deploy --kubeconfig deploy/kubeconfig.derby \
|
||||
--db-host postgres-postgresql.db \
|
||||
--emblem-env dev \
|
||||
-n emblem \
|
||||
-i registry.gitlab.com/euphon/emblem:$(IMAGE_TAG)
|
||||
|
||||
deploy-g: FORCE
|
||||
./scripts/deploy --kubeconfig deploy/kubeconfig.g \
|
||||
--db-host 10.42.0.1 \
|
||||
--emblem-env prod \
|
||||
-n emblem \
|
||||
-i registry.gitlab.com/euphon/emblem:$(IMAGE_TAG)
|
||||
|
||||
deploy-prod: FORCE
|
||||
./scripts/deploy --kubeconfig deploy/kubeconfig.themblem \
|
||||
--db-host 192.168.33.175 \
|
||||
--emblem-env prod \
|
||||
-n emblem \
|
||||
-i registry.gitlab.com/euphon/emblem:$(IMAGE_TAG)
|
||||
run:
|
||||
./scripts/run-tmux.sh
|
||||
|
||||
vm: FORCE vm/sys.img $(DATA_IMGS)
|
||||
q q +vblk:vm/sys.img +sd:vm/ext.img -f --no-net -- \
|
||||
-bios /usr/share/ovmf/OVMF.fd \
|
||||
-serial stdio \
|
||||
-device virtio-blk,drive=sys \
|
||||
-netdev user,id=n0,hostfwd=::10022-:22,hostfwd=::6006-:6006,hostfwd=::13000-:3000,hostfwd=::18000-:8000 \
|
||||
-device virtio-net-pci,netdev=n0 \
|
||||
$(shell for i in 0 1 2 3 4 5 6 7 8; do echo -drive file=vm/data-$$i.img,if=none,id=d$$i -device nvme,serial=NVME_$$i,drive=d$$i; done) \
|
||||
-device virtio-scsi \
|
||||
-drive file=$(EXT_IMG),if=none,id=ext0 -device scsi-hd,drive=ext0
|
||||
3
api/README.md
Normal file
3
api/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Emblem
|
||||
|
||||
The Emblem Project
|
||||
1
api/api/.gitignore
vendored
Normal file
1
api/api/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
db.sqlite3
|
||||
16
api/api/emblemapi/asgi.py
Normal file
16
api/api/emblemapi/asgi.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for emblemapi project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'emblemapi.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
181
api/api/emblemapi/settings.py
Normal file
181
api/api/emblemapi/settings.py
Normal file
@ -0,0 +1,181 @@
|
||||
"""
|
||||
Django settings for emblemapi project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 3.2.10.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.2/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
from keys import *
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-po^713agbnee6w8&ovj-9@)cyv&-&1q&v8%88(e3o)okn%de_3'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
ENV = os.environ.get("EMBLEM_ENV", "debug")
|
||||
|
||||
DEBUG = ENV in ["debug", "dev"]
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'corsheaders',
|
||||
'tastypie',
|
||||
'products',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
|
||||
ROOT_URLCONF = 'emblemapi.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'emblemapi.wsgi.application'
|
||||
|
||||
APPEND_SLASH = False
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
||||
|
||||
if os.environ.get("EMBLEM_DB_TYPE") == "postgres":
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||
'NAME': 'emblem',
|
||||
'USER': 'emblem',
|
||||
'PASSWORD': 'emblempass',
|
||||
'HOST': os.environ["EMBLEM_DB_HOST"],
|
||||
'PORT': '5432',
|
||||
},
|
||||
}
|
||||
else:
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
},
|
||||
}
|
||||
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
|
||||
'LOCATION': 'emblem_cache_table',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/3.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/3.2/howto/static-files/
|
||||
|
||||
STATIC_URL = '/api/static/'
|
||||
STATIC_ROOT = './static'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
DATA_UPLOAD_MAX_MEMORY_SIZE = 20 << 20
|
||||
|
||||
if ENV == "prod":
|
||||
OSS = aliyun_prod_key
|
||||
FEATURES_BUCKET = "emblem-features-prod"
|
||||
ARCHIVE_BUCKET = "emblem-archive-prod"
|
||||
else:
|
||||
OSS = aliyun_dev_key
|
||||
FEATURES_BUCKET = "emblem-features-dev-1"
|
||||
ARCHIVE_BUCKET = "emblem-oss-archive-dev-1"
|
||||
|
||||
IPINFO_TOKEN = '537dea9ec5c99a'
|
||||
|
||||
TOKEN_EXPIRE_MINUTES = 180
|
||||
|
||||
ADMINS = [('Fam Zheng', 'fam@euphon.net')]
|
||||
|
||||
EMAIL_HOST = 'smtpdm.aliyun.com'
|
||||
EMAIL_PORT = 465
|
||||
EMAIL_USE_SSL = True
|
||||
EMAIL_HOST_USER = 'noreply@emblem-notify.euphon.net'
|
||||
EMAIL_HOST_PASSWORD = 'N72yBNi4cJw'
|
||||
SERVER_EMAIL = EMAIL_HOST_USER
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = ['https://*.themblem.com', 'https://themblem.com']
|
||||
23
api/api/emblemapi/urls.py
Normal file
23
api/api/emblemapi/urls.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""emblemapi URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/3.2/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('api/admin/', admin.site.urls),
|
||||
path('api/', include('products.urls')),
|
||||
path('v/', include('products.v_urls')),
|
||||
]
|
||||
16
api/api/emblemapi/wsgi.py
Normal file
16
api/api/emblemapi/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for emblemapi project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'emblemapi.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
5
api/api/euphon.py
Normal file
5
api/api/euphon.py
Normal file
@ -0,0 +1,5 @@
|
||||
import requests
|
||||
|
||||
def send_alert(msg):
|
||||
url = 'https://euphon-ps-alert-lgtmbklwhe.cn-beijing.fcapp.run/Ye2ienoo'
|
||||
requests.post(url, data=msg)
|
||||
14
api/api/keys.py
Normal file
14
api/api/keys.py
Normal file
@ -0,0 +1,14 @@
|
||||
|
||||
aliyun_prod_key = {
|
||||
'access_key': 'LTAI5tC2qXGxwHZUZP7DoD1A',
|
||||
'secret': 'qPo9O6ZvEfqo4t8oflGEm0DoxLHJhm',
|
||||
'endpoint': 'https://oss-cn-shenzhen.aliyuncs.com',
|
||||
'bucket': 'emblem-prod',
|
||||
}
|
||||
|
||||
aliyun_dev_key = {
|
||||
'access_key': 'LTAI5tC2qXGxwHZUZP7DoD1A',
|
||||
'secret': 'qPo9O6ZvEfqo4t8oflGEm0DoxLHJhm',
|
||||
'endpoint': 'https://oss-eu-west-1.aliyuncs.com',
|
||||
'bucket': 'emblem-dev-1',
|
||||
}
|
||||
22
api/api/manage.py
Executable file
22
api/api/manage.py
Executable file
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'emblemapi.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
67
api/api/notify.py
Executable file
67
api/api/notify.py
Executable file
@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import argparse
|
||||
import smtplib
|
||||
from keys import *
|
||||
from email.message import EmailMessage
|
||||
from aliyunsdkcore.client import AcsClient
|
||||
from aliyunsdkcore.request import CommonRequest
|
||||
|
||||
FROM_ADDR = 'noreply@emblem-notify.euphon.net'
|
||||
|
||||
def send_email(to, subject, body):
|
||||
msg = EmailMessage()
|
||||
msg.set_content(body)
|
||||
msg['Subject'] = subject
|
||||
msg['From'] = FROM_ADDR
|
||||
msg['To'] = to
|
||||
|
||||
s = smtplib.SMTP('smtpdm.aliyun.com')
|
||||
s.login(FROM_ADDR, 'N72yBNi4cJw')
|
||||
s.send_message(msg)
|
||||
s.quit()
|
||||
|
||||
def send_sms_code(mobile, code):
|
||||
ak = aliyun_prod_key['access_key']
|
||||
sk = aliyun_prod_key['secret']
|
||||
client = AcsClient(ak, sk, 'cn-shenzhen')
|
||||
request = CommonRequest()
|
||||
request.set_accept_format('json')
|
||||
request.set_domain('dysmsapi.aliyuncs.com')
|
||||
request.set_method('POST')
|
||||
request.set_protocol_type('https') # https | http
|
||||
request.set_version('2017-05-25')
|
||||
request.set_action_name('SendSms')
|
||||
|
||||
request.add_query_param('RegionId', "cn-shenzhen")
|
||||
request.add_query_param('PhoneNumbers', mobile)
|
||||
request.add_query_param('SignName', "themblem")
|
||||
request.add_query_param('TemplateCode', 'SMS_280201599')
|
||||
request.add_query_param('TemplateParam', "{\"code\":\"%s\"}" % code)
|
||||
|
||||
print(request)
|
||||
response = client.do_action(request).decode()
|
||||
print(response)
|
||||
r = json.loads(response)
|
||||
if r.get("Code") != "OK":
|
||||
raise Exception(r["Message"])
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--to")
|
||||
parser.add_argument("--subject")
|
||||
parser.add_argument("--body", default='no content')
|
||||
parser.add_argument("--mobile")
|
||||
parser.add_argument("--verify-code", '-c')
|
||||
return parser.parse_args()
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
if args.to and args.subject:
|
||||
send_email(args.to, args.subject, args.body)
|
||||
if args.mobile and args.verify_code:
|
||||
send_sms_code(args.mobile, args.verify_code)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
0
api/api/products/__init__.py
Normal file
0
api/api/products/__init__.py
Normal file
21
api/api/products/admin.py
Normal file
21
api/api/products/admin.py
Normal file
@ -0,0 +1,21 @@
|
||||
from django.contrib import admin
|
||||
from .models import *
|
||||
|
||||
# Register your models here.
|
||||
admin.site.register(CodeBatch)
|
||||
admin.site.register(SerialCode)
|
||||
admin.site.register(Product)
|
||||
admin.site.register(Article)
|
||||
admin.site.register(AdminInfo)
|
||||
admin.site.register(SmsVerifiedAction)
|
||||
admin.site.register(ScanData)
|
||||
admin.site.register(EstorArchiveRecord)
|
||||
admin.site.register(Job)
|
||||
admin.site.register(ConsumerInfo)
|
||||
|
||||
class AuthTokenAdmin(admin.ModelAdmin):
|
||||
search_fields = ['token']
|
||||
|
||||
admin.site.register(AuthToken, AuthTokenAdmin)
|
||||
|
||||
admin.site.register(GlobalConfig)
|
||||
39
api/api/products/aliyun.py
Normal file
39
api/api/products/aliyun.py
Normal file
@ -0,0 +1,39 @@
|
||||
import json
|
||||
import oss2
|
||||
from aliyunsdkcore.client import AcsClient
|
||||
from aliyunsdkcore.request import CommonRequest
|
||||
from django.conf import settings
|
||||
|
||||
def oss_bucket(bucketname):
|
||||
oss = settings.OSS
|
||||
auth = oss2.Auth(oss['access_key'], oss['secret'])
|
||||
bname = bucketname or oss['bucket']
|
||||
bucket = oss2.Bucket(auth, oss['endpoint'], bname)
|
||||
return bucket
|
||||
|
||||
def oss_put(name, f, bucket=None):
|
||||
oss_bucket(bucket).put_object(name, f)
|
||||
|
||||
def oss_get(name, bucket=None):
|
||||
try:
|
||||
return oss_bucket(bucket).get_object(name).read()
|
||||
except oss2.exceptions.NoSuchKey:
|
||||
return None
|
||||
|
||||
def oss_sign_url(name, method='GET', bucket=None):
|
||||
return oss_bucket(bucket).sign_url(method, name.encode(), 24 * 60 * 60)
|
||||
|
||||
def oss_has(name, bucket=None):
|
||||
try:
|
||||
obj = oss_bucket(bucket).get_object(name)
|
||||
return True
|
||||
except oss2.exceptions.NoSuchKey:
|
||||
return False
|
||||
|
||||
def oss_stat(bucket=None):
|
||||
bucket = oss_bucket(bucket)
|
||||
stat = bucket.get_bucket_stat()
|
||||
return {
|
||||
'objects': stat.object_count,
|
||||
'size': stat.storage_size_in_bytes,
|
||||
}
|
||||
6
api/api/products/apps.py
Normal file
6
api/api/products/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ProductsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'products'
|
||||
45
api/api/products/article.py
Normal file
45
api/api/products/article.py
Normal file
@ -0,0 +1,45 @@
|
||||
import json
|
||||
from django.template import Template, Context
|
||||
|
||||
css_template = """
|
||||
body {
|
||||
background-color: {{ backgroun_color }};
|
||||
}
|
||||
|
||||
.mce-content-body {
|
||||
background-color: {{ backgroun_color }};
|
||||
}
|
||||
|
||||
.article {
|
||||
font-size: 1.5rem;
|
||||
text-overflow: wrap;
|
||||
color: #222;
|
||||
background-color: {{ backgroun_color }};
|
||||
}
|
||||
|
||||
.article a {
|
||||
text-decoration: none;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.article a:hover {
|
||||
text-decoration: none;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.article img {
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
"""
|
||||
|
||||
def gen_article_css(article):
|
||||
try:
|
||||
options = json.loads(article.options)
|
||||
except:
|
||||
options = {}
|
||||
t = Template(css_template)
|
||||
c = Context({
|
||||
'backgroun_color': options.get('page_bg_color', '#ffffff')
|
||||
})
|
||||
return t.render(c)
|
||||
40
api/api/products/ip2region.py
Normal file
40
api/api/products/ip2region.py
Normal file
@ -0,0 +1,40 @@
|
||||
import os
|
||||
from django.conf import settings
|
||||
import logging
|
||||
import subprocess
|
||||
from django.core.cache import cache
|
||||
|
||||
def get_client_ip(request):
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(',')[0]
|
||||
else:
|
||||
ip = request.META.get('REMOTE_ADDR')
|
||||
return ip
|
||||
|
||||
def request_to_region(request):
|
||||
ip = get_client_ip(request)
|
||||
if not ip:
|
||||
logging.warning("Cannot get client IP from request: %s" % request)
|
||||
return
|
||||
return ip_to_region(ip)
|
||||
|
||||
def ip_to_region(ip):
|
||||
ck = 'ip2region.' + ip
|
||||
region = cache.get(ck)
|
||||
if not region:
|
||||
cmd = [os.path.join(settings.BASE_DIR, "../scripts/ip2region.py"), ip]
|
||||
try:
|
||||
out = subprocess.check_output(cmd).decode()
|
||||
seen = set()
|
||||
locs = []
|
||||
for u in out.split('|'):
|
||||
us = u.strip()
|
||||
if us not in ['', '0', '中国'] and us not in seen:
|
||||
seen.add(us)
|
||||
locs.append(us)
|
||||
region = '-'.join(locs)
|
||||
except Exception as e:
|
||||
region = "N/A"
|
||||
cache.set(ck, region)
|
||||
return region
|
||||
52
api/api/products/management/commands/bindbatch.py
Normal file
52
api/api/products/management/commands/bindbatch.py
Normal file
@ -0,0 +1,52 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from products.models import *
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Update batch binding with tenant'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--batch', '-b', type=int)
|
||||
parser.add_argument('--job', '-j', type=int)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if options.get('job'):
|
||||
job = Job.objects.get(pk=options['job'])
|
||||
else:
|
||||
job = Job.objects.create(name='bind-batch.%d' % options.get('batch'))
|
||||
try:
|
||||
job.update('running', 0.0)
|
||||
b = CodeBatch.objects.get(pk=options['batch'])
|
||||
t = b.tenant
|
||||
q = SerialCode.objects.filter(batch=b)
|
||||
total = q.count()
|
||||
done = 0
|
||||
batchsize = 1000
|
||||
for objs in self.batch_iter(q, batchsize):
|
||||
if t:
|
||||
seq_num = t.alloc_seq_nums(batchsize)
|
||||
for x in objs:
|
||||
x.tenant = t
|
||||
x.seq_num = seq_num
|
||||
seq_num += 1
|
||||
else:
|
||||
for x in objs:
|
||||
x.tenant = None
|
||||
x.seq_num = None
|
||||
SerialCode.objects.bulk_update(objs, ['tenant', 'seq_num'])
|
||||
done += len(objs)
|
||||
perc = done * 100.0 / total
|
||||
job.update('running', perc)
|
||||
print(f"bound {done} codes, total {total}")
|
||||
job.update('done', 100.0)
|
||||
except Exception as e:
|
||||
job.update('error', message=str(e))
|
||||
|
||||
def batch_iter(self, q, batchsize=1000):
|
||||
this_batch = []
|
||||
for x in q.iterator():
|
||||
this_batch.append(x)
|
||||
if len(this_batch) >= batchsize:
|
||||
yield this_batch
|
||||
this_batch = []
|
||||
if this_batch:
|
||||
yield this_batch
|
||||
82
api/api/products/management/commands/codebatchop.py
Normal file
82
api/api/products/management/commands/codebatchop.py
Normal file
@ -0,0 +1,82 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from products.models import *
|
||||
from django.db import transaction
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Manage serial code in batch'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--tenant-id', '-t', required=True, type=int)
|
||||
parser.add_argument('--job', '-j', type=int)
|
||||
parser.add_argument('--all', '-A', action="store_true", help="apply to all code")
|
||||
parser.add_argument('--seq-range', "-r", help="code by seq range")
|
||||
parser.add_argument('--code-file', "-f", help="code list from file")
|
||||
parser.add_argument('--activate', "-a", action="store_true", help="activate code")
|
||||
parser.add_argument('--deactivate', "-d", action="store_true", help="deactivate code")
|
||||
parser.add_argument('--bind-product', "-b", type=int, help="bind to product by id")
|
||||
parser.add_argument('--unbind-product', "-u", action="store_true", help="unbind product")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if options.get('job'):
|
||||
job = Job.objects.get(pk=options['job'])
|
||||
else:
|
||||
job = Job.objects.create(name='bind-batch.%d' % options.get('batch'))
|
||||
try:
|
||||
job.update('running', 0.0)
|
||||
tenant = Tenant.objects.get(pk=options['tenant_id'])
|
||||
|
||||
query = self.build_query(options)
|
||||
total = query.count()
|
||||
done = 0
|
||||
prod = None
|
||||
if options.get("bind_product"):
|
||||
prod = Product.objects.get(tenant=tenant, pk=options['bind_product'])
|
||||
for batch_ids in self.iterate_batches(query, 10000):
|
||||
uq = SerialCode.objects.filter(pk__in=batch_ids)
|
||||
if options.get("activate"):
|
||||
uq.update(is_active=True)
|
||||
elif options.get("deactivate"):
|
||||
uq.update(is_active=False)
|
||||
elif options.get("unbind_product"):
|
||||
uq.update(product=None)
|
||||
elif options.get("bind_product"):
|
||||
uq.update(product=prod)
|
||||
|
||||
transaction.commit()
|
||||
done += len(batch_ids)
|
||||
|
||||
print("code batch op", done, total)
|
||||
perc = done * 100.0 / total
|
||||
job.update('running', perc)
|
||||
|
||||
job.update('done', 100.0)
|
||||
except Exception as e:
|
||||
job.update('error', message=str(e))
|
||||
raise
|
||||
|
||||
def iterate_batches(self, query, batchsize):
|
||||
total = query.count()
|
||||
cur = 0
|
||||
while cur < total:
|
||||
yield [x.pk for x in query[cur : cur + batchsize]]
|
||||
cur += batchsize
|
||||
|
||||
def build_query(self, options):
|
||||
tenant = Tenant.objects.get(pk=options['tenant_id'])
|
||||
query = SerialCode.objects.filter(tenant=tenant).order_by('pk')
|
||||
if options.get("seq_range"):
|
||||
begin, end = options['seq_range'].split(',', maxsplit=1)
|
||||
query = query.filter(seq_num__gte=int(begin),
|
||||
seq_num__lte=int(end))
|
||||
elif options.get("code_file"):
|
||||
codes = self.read_code_file(options['code_file'])
|
||||
query = query.filter(code__in=codes)
|
||||
elif options.get("all"):
|
||||
pass
|
||||
else:
|
||||
raise Exception("Code not specified")
|
||||
return query
|
||||
|
||||
def read_code_file(self, cf):
|
||||
with open(cf, 'r') as f:
|
||||
return [x for x in f.read().splitlines() if not x.strip().startswith('#')]
|
||||
42
api/api/products/management/commands/exportbatch.py
Normal file
42
api/api/products/management/commands/exportbatch.py
Normal file
@ -0,0 +1,42 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from products.models import *
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Export batch to a file'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--batch', '-b', type=int, required=True, help="batch id")
|
||||
parser.add_argument('--output', '-o', required=True, help="output file")
|
||||
parser.add_argument('--job', '-j', type=int, help="The job object to report progress")
|
||||
parser.add_argument('--pattern', '-P', default='{code}', help="The pattern of each line, where {code} is replaced with the serial code")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
batch_id = options['batch']
|
||||
if options.get('job'):
|
||||
job = Job.objects.get(pk=options['job'])
|
||||
else:
|
||||
job = Job.objects.create(name='export-batch.%d' % batch_id)
|
||||
try:
|
||||
job.update('running', 0.0)
|
||||
b = CodeBatch.objects.get(pk=batch_id)
|
||||
fn = open(options['output'], 'w', encoding='utf-8')
|
||||
ac = b.codes.all().order_by('seq_num')
|
||||
total = ac.count()
|
||||
done = 0
|
||||
for c in ac.iterator():
|
||||
line = options['pattern']
|
||||
line = line.replace('{code}', c.code)
|
||||
line = line.replace('{seq}', str(c.seq_num) if c.seq_num else "0")
|
||||
|
||||
fn.write(line + "\n")
|
||||
done += 1
|
||||
if done % 10000 == 0:
|
||||
perc = done * 100.0 / total
|
||||
print("[%d/%d %.1f%%]" % (done, total, perc))
|
||||
job.update('running', perc)
|
||||
print("all done, exported %d codes" % total)
|
||||
fn.flush()
|
||||
job.update('done', 100.0)
|
||||
fn.flush()
|
||||
except Exception as e:
|
||||
job.update('error', message=str(e))
|
||||
67
api/api/products/management/commands/exportscandata.py
Normal file
67
api/api/products/management/commands/exportscandata.py
Normal file
@ -0,0 +1,67 @@
|
||||
import time
|
||||
import shutil
|
||||
import tempfile
|
||||
import subprocess
|
||||
import requests
|
||||
import datetime
|
||||
import os
|
||||
from django.utils import timezone
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from products.models import *
|
||||
from products.aliyun import oss_sign_url
|
||||
from django.core import serializers
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Export scan data to a zip file'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--output', '-o', required=True, help="output file")
|
||||
parser.add_argument('--job', '-j', type=int, help="The job object to report progress")
|
||||
parser.add_argument('--hours', '-H', type=int, help="how many hours")
|
||||
|
||||
def download(self, url, p):
|
||||
r = requests.get(url, allow_redirects=True)
|
||||
d = os.path.dirname(p)
|
||||
os.makedirs(d, exist_ok=True)
|
||||
with open(p, 'wb') as f:
|
||||
f.write(r.content)
|
||||
|
||||
def do_export(self, job, workdir, hours, part_prog):
|
||||
subprocess.check_output(["mkdir", "-p", "images"], cwd=workdir)
|
||||
start = timezone.now() - datetime.timedelta(hours=hours)
|
||||
records = ScanData.objects.filter(datetime__gt=start)
|
||||
total = records.count() + 1
|
||||
done = 0
|
||||
print("total:", total)
|
||||
for r in records:
|
||||
url = oss_sign_url(r.image)
|
||||
p = os.path.join(workdir, 'images', r.image)
|
||||
done += 1
|
||||
self.download(url, p)
|
||||
perc = done * part_prog / total
|
||||
print("[%d/%d %.1f%%]" % (done, total, perc))
|
||||
job.update('running', perc)
|
||||
j = serializers.serialize('json', records)
|
||||
print(j)
|
||||
with open(os.path.join(workdir, 'data.json'), 'w') as f:
|
||||
f.write(j)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if options.get('job'):
|
||||
job = Job.objects.get(pk=options['job'])
|
||||
else:
|
||||
job = Job.objects.create(name='export-scan-data.%d' % time.time())
|
||||
td = None
|
||||
try:
|
||||
job.update('running', 0.0)
|
||||
td = tempfile.mkdtemp()
|
||||
self.do_export(job, td, options['hours'], 80)
|
||||
cmd = ['zip', '-r', os.path.abspath(options['output']), '.']
|
||||
print(cmd)
|
||||
subprocess.check_call(cmd, cwd=td)
|
||||
job.update('done', 100.0)
|
||||
except Exception as e:
|
||||
job.update('error', message=str(e))
|
||||
finally:
|
||||
if td:
|
||||
shutil.rmtree(td)
|
||||
59
api/api/products/management/commands/importcode.py
Normal file
59
api/api/products/management/commands/importcode.py
Normal file
@ -0,0 +1,59 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from products.models import *
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Import serial code from text file to a batch'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--batch', '-b', required=True, type=int)
|
||||
parser.add_argument('--file', '-f', required=True)
|
||||
parser.add_argument('--job', '-j', type=int)
|
||||
|
||||
def file_batch_iter(self, path, batchsize):
|
||||
this_batch = []
|
||||
for x in open(path, 'r'):
|
||||
x = x.strip()
|
||||
if x.startswith('#'):
|
||||
continue
|
||||
this_batch.append(x)
|
||||
if len(this_batch) >= batchsize:
|
||||
yield this_batch
|
||||
this_batch = []
|
||||
yield this_batch
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if options.get('job'):
|
||||
job = Job.objects.get(pk=options['job'])
|
||||
else:
|
||||
job = Job.objects.create(name='import-code.%d' % options.get('batch'))
|
||||
try:
|
||||
job.update('running', 0.0)
|
||||
fn = options['file']
|
||||
batchsize = 10000
|
||||
total = sum([len(x) for x in self.file_batch_iter(fn, batchsize)])
|
||||
done = 0
|
||||
b = CodeBatch.objects.get(pk=options['batch'])
|
||||
t = b.tenant
|
||||
for lines in self.file_batch_iter(fn, batchsize):
|
||||
objs = []
|
||||
if t:
|
||||
seq_num = t.alloc_seq_nums(batchsize)
|
||||
else:
|
||||
seq_num = None
|
||||
for code in lines:
|
||||
obj = SerialCode(code=code, batch=b, tenant=t, seq_num=seq_num, is_active=b.is_active)
|
||||
if seq_num:
|
||||
seq_num += 1
|
||||
objs.append(obj)
|
||||
done += len(objs)
|
||||
perc = done * 100.0 / total
|
||||
SerialCode.objects.bulk_create(objs, ignore_conflicts=True)
|
||||
print("[%d / %d]" % (done, total))
|
||||
job.update('running', perc)
|
||||
job.update('done', 100.0)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
job.update('error', message=str(e))
|
||||
finally:
|
||||
print("deleting", fn)
|
||||
os.unlink(fn)
|
||||
59
api/api/products/management/commands/qrrepeatalert.py
Normal file
59
api/api/products/management/commands/qrrepeatalert.py
Normal file
@ -0,0 +1,59 @@
|
||||
import json
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.utils import timezone
|
||||
from products.sendmsg import send_user_message
|
||||
from products.models import *
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Check repeated QR verify request and send alert'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('code')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
code = options['code']
|
||||
sc = SerialCode.objects.filter(code=code, is_active=True).first()
|
||||
if not sc:
|
||||
return
|
||||
trigger = False
|
||||
for a in AdminInfo.objects.filter(qr_verify_alert_rule__isnull=False):
|
||||
if self.check_one(a, sc):
|
||||
trigger = True
|
||||
if trigger:
|
||||
self.disable_code(sc)
|
||||
|
||||
def check_one(self, admin, sc):
|
||||
rule = admin.qr_verify_alert_rule
|
||||
if not rule:
|
||||
return
|
||||
rule = json.loads(rule)
|
||||
time_window_seconds = rule.get('time_window_seconds')
|
||||
repeat_threshold = rule.get('repeat_threshold')
|
||||
if not time_window_seconds or not repeat_threshold:
|
||||
return
|
||||
start = timezone.now() - datetime.timedelta(seconds=time_window_seconds)
|
||||
records = ScanData.objects.filter(code=sc.code, datetime__gte=start)
|
||||
if records.count() >= repeat_threshold:
|
||||
self.alert_one(admin, sc, records)
|
||||
return True
|
||||
|
||||
def disable_code(self, sc):
|
||||
sc.is_active = False
|
||||
sc.save()
|
||||
|
||||
def alert_one(self, admin, sc, records):
|
||||
subject = "重复验证报警: 序列码 %s 最近已重复 %d 次验证" % (sc.code, records.count())
|
||||
print(subject)
|
||||
lines = [
|
||||
"序列码: %s" % sc.code,
|
||||
"租户: %s" % (sc.tenant.username if sc.tenant else ""),
|
||||
"产品: %s" % (sc.product.name if sc.product else ""),
|
||||
"",
|
||||
"近期验证记录:"]
|
||||
for r in records:
|
||||
lines.append("%s, %s, %s" % (r.datetime, r.location, r.ip))
|
||||
|
||||
lines.append("")
|
||||
lines.append("验证码已冻结")
|
||||
|
||||
send_user_message(admin, subject, "\n".join(lines))
|
||||
11
api/api/products/management/commands/sendalert.py
Normal file
11
api/api/products/management/commands/sendalert.py
Normal file
@ -0,0 +1,11 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from euphon import send_alert
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Send alert message to euphon ops channel'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('msg', nargs="+")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
send_alert("\n".join(options['msg']))
|
||||
15
api/api/products/management/commands/sendemail.py
Normal file
15
api/api/products/management/commands/sendemail.py
Normal file
@ -0,0 +1,15 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from products.models import *
|
||||
from products.sendmsg import send_email
|
||||
from django.db import transaction
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Send a message to admin/tenant'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('to')
|
||||
parser.add_argument('subject')
|
||||
parser.add_argument('content')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
send_email(options['to'], options['subject'], options['content'])
|
||||
31
api/api/products/management/commands/sendmsg.py
Normal file
31
api/api/products/management/commands/sendmsg.py
Normal file
@ -0,0 +1,31 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from products.models import *
|
||||
from products.sendmsg import send_user_message, admin_broadcast
|
||||
from django.db import transaction
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Send a message to admin/tenant'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--all-admin', '-A', action="store_true")
|
||||
parser.add_argument('--admin', '-a')
|
||||
parser.add_argument('--tenant', '-t')
|
||||
parser.add_argument('--subject', '-s', required=True)
|
||||
parser.add_argument('--content', '-c', default='')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
admin = options['admin']
|
||||
tenant = options['tenant']
|
||||
subject = options['subject']
|
||||
content = options['content']
|
||||
whom = None
|
||||
if options.get('all_admin'):
|
||||
admin_broadcast(subject, content)
|
||||
return
|
||||
if admin:
|
||||
whom = AdminInfo.objects.get(user__username=admin)
|
||||
elif tenant:
|
||||
whom = Tenant.objects.get(username=tenant)
|
||||
else:
|
||||
raise Exception("Must specify either admin or tenant name")
|
||||
send_user_message(whom, subject, content)
|
||||
12
api/api/products/migrations/0001_initial.py
Normal file
12
api/api/products/migrations/0001_initial.py
Normal file
@ -0,0 +1,12 @@
|
||||
# Generated by Django 3.2.11 on 2022-02-04 22:03
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
146
api/api/products/migrations/0002_initial.py
Normal file
146
api/api/products/migrations/0002_initial.py
Normal file
@ -0,0 +1,146 @@
|
||||
# Generated by Django 3.2.11 on 2022-02-05 11:33
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('products', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CodeBatch',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('qr_angle', models.FloatField(default=0.0)),
|
||||
('datetime', models.DateTimeField(auto_now=True)),
|
||||
('code_prefix', models.CharField(max_length=64)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ConsumerInfo',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('create_time', models.DateTimeField(auto_now=True)),
|
||||
('username', models.CharField(max_length=128)),
|
||||
('gender', models.CharField(blank=True, max_length=128, null=True)),
|
||||
('country', models.CharField(blank=True, max_length=128, null=True)),
|
||||
('province', models.CharField(blank=True, max_length=128, null=True)),
|
||||
('city', models.CharField(blank=True, max_length=128, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Event',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('datetime', models.DateTimeField(auto_now=True)),
|
||||
('kind', models.CharField(max_length=128)),
|
||||
('params', models.TextField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PageTemplate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('template', models.TextField()),
|
||||
('params', models.TextField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Stat',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('kind', models.CharField(max_length=128)),
|
||||
('params', models.TextField()),
|
||||
('count', models.IntegerField(default=1)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Tenant',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('username', models.CharField(db_index=True, max_length=128, unique=True, verbose_name='用户名')),
|
||||
('mobile', models.CharField(blank=True, max_length=128, null=True, unique=True, verbose_name='手机号')),
|
||||
('password', models.CharField(max_length=256, verbose_name='密码')),
|
||||
('token', models.CharField(db_index=True, max_length=128, unique=True, verbose_name='API Token')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SerialCode',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(db_index=True, max_length=128, unique=True)),
|
||||
('batch', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='codes', to='products.codebatch')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ScanData',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('datetime', models.DateTimeField(auto_now=True)),
|
||||
('ip', models.CharField(max_length=64)),
|
||||
('kind', models.CharField(max_length=128)),
|
||||
('params', models.TextField()),
|
||||
('image', models.TextField()),
|
||||
('consumer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scans', to='products.consumerinfo')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProductPage',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('html', models.TextField(default='<html><body>no content</body></html>')),
|
||||
('arguments', models.TextField(blank=True, null=True)),
|
||||
('template', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='pages', to='products.pagetemplate')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Product',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=128, verbose_name='名称')),
|
||||
('description', models.TextField(verbose_name='产品描述')),
|
||||
('counterfeit_result_page', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='counterfeit_page', to='products.productpage', verbose_name='假货页面')),
|
||||
('genuine_result_page', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='genuine_page', to='products.productpage', verbose_name='产品信息页面')),
|
||||
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='products.tenant')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('tenant', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='pagetemplate',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='templates', to='products.tenant'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Media',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('mime_type', models.CharField(max_length=128)),
|
||||
('uri', models.TextField()),
|
||||
('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='media', to='products.tenant')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='codebatch',
|
||||
name='product',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='batches', to='products.product'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AdminInfo',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('token', models.CharField(db_index=True, max_length=128, null=True, unique=True, verbose_name='API Token')),
|
||||
('mobile', models.CharField(blank=True, max_length=128, null=True, unique=True, verbose_name='手机号')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='token', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
44
api/api/products/migrations/0003_auto_20220209_2025.py
Normal file
44
api/api/products/migrations/0003_auto_20220209_2025.py
Normal file
@ -0,0 +1,44 @@
|
||||
# Generated by Django 3.2.11 on 2022-02-09 20:25
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0002_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='pagetemplate',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='codebatch',
|
||||
name='code_prefix',
|
||||
field=models.CharField(max_length=64, verbose_name='序列码前缀'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='codebatch',
|
||||
name='datetime',
|
||||
field=models.DateTimeField(auto_now=True, verbose_name='创建日期'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='codebatch',
|
||||
name='product',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='batches', to='products.product', verbose_name='产品'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='codebatch',
|
||||
name='qr_angle',
|
||||
field=models.FloatField(default=0.0, verbose_name='网线角度'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='productpage',
|
||||
name='html',
|
||||
field=models.TextField(default='<span class="no-content">no content</span>'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.11 on 2022-02-11 21:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0003_auto_20220209_2025'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='tenant',
|
||||
name='password_reset_code',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, null=True),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.11 on 2022-02-11 21:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0004_tenant_password_reset_code'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='tenant',
|
||||
name='password_reset_code_expire',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
21
api/api/products/migrations/0006_systemlog.py
Normal file
21
api/api/products/migrations/0006_systemlog.py
Normal file
@ -0,0 +1,21 @@
|
||||
# Generated by Django 3.2.11 on 2022-02-23 20:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0005_tenant_password_reset_code_expire'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SystemLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('datetime', models.DateTimeField(auto_now=True)),
|
||||
('log', models.TextField()),
|
||||
],
|
||||
),
|
||||
]
|
||||
18
api/api/products/migrations/0007_alter_systemlog_datetime.py
Normal file
18
api/api/products/migrations/0007_alter_systemlog_datetime.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.11 on 2022-02-23 21:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0006_systemlog'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='systemlog',
|
||||
name='datetime',
|
||||
field=models.DateTimeField(auto_now=True, db_index=True),
|
||||
),
|
||||
]
|
||||
23
api/api/products/migrations/0008_counter.py
Normal file
23
api/api/products/migrations/0008_counter.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.12 on 2022-02-24 21:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0007_alter_systemlog_datetime'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Counter',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(db_index=True, max_length=128, unique=True)),
|
||||
('params', models.TextField(null=True)),
|
||||
('datetime', models.DateTimeField(auto_now=True)),
|
||||
('count', models.IntegerField()),
|
||||
],
|
||||
),
|
||||
]
|
||||
18
api/api/products/migrations/0009_alter_counter_name.py
Normal file
18
api/api/products/migrations/0009_alter_counter_name.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.12 on 2022-02-24 21:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0008_counter'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='counter',
|
||||
name='name',
|
||||
field=models.CharField(db_index=True, max_length=128),
|
||||
),
|
||||
]
|
||||
27
api/api/products/migrations/0010_auto_20220303_2039.py
Normal file
27
api/api/products/migrations/0010_auto_20220303_2039.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Generated by Django 3.2.12 on 2022-03-03 20:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0009_alter_counter_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='consumerinfo',
|
||||
name='platform',
|
||||
field=models.CharField(default='wechat', max_length=128),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consumerinfo',
|
||||
name='username',
|
||||
field=models.CharField(default='[匿名用户]', max_length=128),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='consumerinfo',
|
||||
unique_together={('platform', 'username')},
|
||||
),
|
||||
]
|
||||
34
api/api/products/migrations/0011_auto_20220306_1552.py
Normal file
34
api/api/products/migrations/0011_auto_20220306_1552.py
Normal file
@ -0,0 +1,34 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-06 15:52
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0010_auto_20220303_2039'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='scandata',
|
||||
name='product',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='scans', to='products.product'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pagetemplate',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, null=True, verbose_name='名称'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='description',
|
||||
field=models.TextField(verbose_name='备注'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='serialcode',
|
||||
name='code',
|
||||
field=models.CharField(db_index=True, max_length=128, unique=True, verbose_name='序列码'),
|
||||
),
|
||||
]
|
||||
18
api/api/products/migrations/0012_alter_productpage_html.py
Normal file
18
api/api/products/migrations/0012_alter_productpage_html.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-06 15:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0011_auto_20220306_1552'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='productpage',
|
||||
name='html',
|
||||
field=models.TextField(default='暂无内容'),
|
||||
),
|
||||
]
|
||||
26
api/api/products/migrations/0013_auto_20220306_1938.py
Normal file
26
api/api/products/migrations/0013_auto_20220306_1938.py
Normal file
@ -0,0 +1,26 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-06 19:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0012_alter_productpage_html'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AssetFile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('filename', models.TextField()),
|
||||
('data', models.BinaryField()),
|
||||
('mime_type', models.CharField(default='application/octet-stream', max_length=256)),
|
||||
('usage', models.CharField(max_length=256)),
|
||||
],
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Media',
|
||||
),
|
||||
]
|
||||
18
api/api/products/migrations/0014_assetfile_properties.py
Normal file
18
api/api/products/migrations/0014_assetfile_properties.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-06 19:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0013_auto_20220306_1938'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='assetfile',
|
||||
name='properties',
|
||||
field=models.TextField(default=''),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-06 20:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0014_assetfile_properties'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='assetfile',
|
||||
name='properties',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
]
|
||||
23
api/api/products/migrations/0016_auto_20220307_2001.py
Normal file
23
api/api/products/migrations/0016_auto_20220307_2001.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-07 20:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0015_alter_assetfile_properties'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='consumerinfo',
|
||||
name='platform',
|
||||
field=models.CharField(blank=True, default='wechat', max_length=128, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consumerinfo',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='用户名'),
|
||||
),
|
||||
]
|
||||
36
api/api/products/migrations/0017_auto_20220309_1937.py
Normal file
36
api/api/products/migrations/0017_auto_20220309_1937.py
Normal file
@ -0,0 +1,36 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-09 19:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0016_auto_20220307_2001'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='Event',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Stat',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='consumerinfo',
|
||||
name='city',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='consumerinfo',
|
||||
name='country',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='consumerinfo',
|
||||
name='province',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consumerinfo',
|
||||
name='ip',
|
||||
field=models.CharField(blank=True, max_length=128, null=True),
|
||||
),
|
||||
]
|
||||
23
api/api/products/migrations/0018_auto_20220309_2024.py
Normal file
23
api/api/products/migrations/0018_auto_20220309_2024.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-09 20:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0017_auto_20220309_1937'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='counter',
|
||||
name='accumulated',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='counter',
|
||||
name='datetime',
|
||||
field=models.DateTimeField(auto_now=True, db_index=True),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-09 20:34
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0018_auto_20220309_2024'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='counter',
|
||||
name='accumulated',
|
||||
),
|
||||
]
|
||||
38
api/api/products/migrations/0020_auto_20220309_2038.py
Normal file
38
api/api/products/migrations/0020_auto_20220309_2038.py
Normal file
@ -0,0 +1,38 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-09 20:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0019_remove_counter_accumulated'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='codebatch',
|
||||
name='datetime',
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name='创建日期'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consumerinfo',
|
||||
name='create_time',
|
||||
field=models.DateTimeField(auto_now_add=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='counter',
|
||||
name='datetime',
|
||||
field=models.DateTimeField(auto_now_add=True, db_index=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='scandata',
|
||||
name='datetime',
|
||||
field=models.DateTimeField(auto_now_add=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemlog',
|
||||
name='datetime',
|
||||
field=models.DateTimeField(auto_now_add=True, db_index=True),
|
||||
),
|
||||
]
|
||||
24
api/api/products/migrations/0021_auto_20220312_2238.py
Normal file
24
api/api/products/migrations/0021_auto_20220312_2238.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-12 22:38
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0020_auto_20220309_2038'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='codebatch',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='codebatch',
|
||||
name='product',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='batches', to='products.product', verbose_name='产品'),
|
||||
),
|
||||
]
|
||||
18
api/api/products/migrations/0022_codebatch_is_active.py
Normal file
18
api/api/products/migrations/0022_codebatch_is_active.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-12 22:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0021_auto_20220312_2238'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='codebatch',
|
||||
name='is_active',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
19
api/api/products/migrations/0023_codebatch_tenant.py
Normal file
19
api/api/products/migrations/0023_codebatch_tenant.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-13 08:31
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0022_codebatch_is_active'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='codebatch',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='codebatches', to='products.tenant'),
|
||||
),
|
||||
]
|
||||
45
api/api/products/migrations/0024_auto_20220315_0817.py
Normal file
45
api/api/products/migrations/0024_auto_20220315_0817.py
Normal file
@ -0,0 +1,45 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-15 08:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0023_codebatch_tenant'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='GlobalConfig',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(db_index=True, max_length=128, unique=True, verbose_name='配置名称')),
|
||||
('value', models.TextField(null=True, verbose_name='配置内容')),
|
||||
],
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='consumerinfo',
|
||||
name='gender',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tenant',
|
||||
name='welcome_page_config',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='codebatch',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='备注'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='codebatch',
|
||||
name='is_active',
|
||||
field=models.BooleanField(default=True, verbose_name='已激活'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consumerinfo',
|
||||
name='ip',
|
||||
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='IP'),
|
||||
),
|
||||
]
|
||||
28
api/api/products/migrations/0025_auto_20220315_2039.py
Normal file
28
api/api/products/migrations/0025_auto_20220315_2039.py
Normal file
@ -0,0 +1,28 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-15 20:39
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0024_auto_20220315_0817'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='assetfile',
|
||||
name='data',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='assetfile',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assets', to='products.tenant'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='assetfile',
|
||||
name='uuid',
|
||||
field=models.CharField(blank=True, max_length=256, null=True),
|
||||
),
|
||||
]
|
||||
19
api/api/products/migrations/0026_alter_assetfile_uuid.py
Normal file
19
api/api/products/migrations/0026_alter_assetfile_uuid.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-15 20:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0025_auto_20220315_2039'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='assetfile',
|
||||
name='uuid',
|
||||
field=models.CharField(default='', max_length=256),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
18
api/api/products/migrations/0027_tenant_display_name.py
Normal file
18
api/api/products/migrations/0027_tenant_display_name.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-15 22:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0026_alter_assetfile_uuid'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='tenant',
|
||||
name='display_name',
|
||||
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='显示名称'),
|
||||
),
|
||||
]
|
||||
18
api/api/products/migrations/0028_assetfile_url.py
Normal file
18
api/api/products/migrations/0028_assetfile_url.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-19 21:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0027_tenant_display_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='assetfile',
|
||||
name='url',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
23
api/api/products/migrations/0029_article.py
Normal file
23
api/api/products/migrations/0029_article.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-21 19:45
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0028_assetfile_url'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Article',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(blank=True, max_length=128, null=True, verbose_name='标题')),
|
||||
('body', models.TextField()),
|
||||
('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='articles', to='products.tenant')),
|
||||
],
|
||||
),
|
||||
]
|
||||
30
api/api/products/migrations/0030_auto_20220323_0809.py
Normal file
30
api/api/products/migrations/0030_auto_20220323_0809.py
Normal file
@ -0,0 +1,30 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-23 08:09
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0029_article'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='assetfile',
|
||||
name='datetime',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='创建日期'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='assetfile',
|
||||
name='filename',
|
||||
field=models.TextField(verbose_name='文件名'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='scandata',
|
||||
name='datetime',
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name='时间'),
|
||||
),
|
||||
]
|
||||
23
api/api/products/migrations/0031_auto_20220323_0841.py
Normal file
23
api/api/products/migrations/0031_auto_20220323_0841.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-23 08:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0030_auto_20220323_0809'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='assetfile',
|
||||
name='title',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='标题'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='assetfile',
|
||||
name='url',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='链接'),
|
||||
),
|
||||
]
|
||||
18
api/api/products/migrations/0032_assetfile_order.py
Normal file
18
api/api/products/migrations/0032_assetfile_order.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-23 09:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0031_auto_20220323_0841'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='assetfile',
|
||||
name='order',
|
||||
field=models.IntegerField(blank=True, default=1, null=True, verbose_name='顺序'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-23 09:18
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0032_assetfile_order'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='assetfile',
|
||||
old_name='url',
|
||||
new_name='link',
|
||||
),
|
||||
]
|
||||
22
api/api/products/migrations/0034_auto_20220323_2015.py
Normal file
22
api/api/products/migrations/0034_auto_20220323_2015.py
Normal file
@ -0,0 +1,22 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-23 20:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0033_rename_url_assetfile_link'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='assetfile',
|
||||
name='order',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='assetfile',
|
||||
name='position',
|
||||
field=models.IntegerField(blank=True, default=1, null=True, verbose_name='位置'),
|
||||
),
|
||||
]
|
||||
18
api/api/products/migrations/0035_assetfile_external_url.py
Normal file
18
api/api/products/migrations/0035_assetfile_external_url.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-23 20:47
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0034_auto_20220323_2015'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='assetfile',
|
||||
name='external_url',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
22
api/api/products/migrations/0036_miniprogramcontent.py
Normal file
22
api/api/products/migrations/0036_miniprogramcontent.py
Normal file
@ -0,0 +1,22 @@
|
||||
# Generated by Django 3.2.11 on 2022-03-23 21:54
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0035_assetfile_external_url'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MiniProgramContent',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('content', models.TextField()),
|
||||
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mini_program_content', to='products.tenant', unique=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user