diff --git a/research/fetch b/research/fetch
new file mode 100755
index 0000000..d60c762
--- /dev/null
+++ b/research/fetch
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+
+import argparse
+import subprocess
+
+def parse_args():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--date", "-d", type=str)
+ return parser.parse_args()
+
+def main():
+ args = parse_args()
+ remote_dir = f'/data/emblem-research/camera-frame'
+ if args.date:
+ remote_dir += f'/{args.date}'
+ # we cannot rsync to data/json because we will update there after sync, and
+ # the next sync will overwrite the updated files
+ local_dir = f'data/raw/camera-frame'
+ cmd = ['rsync', '-avz', f'zy:{remote_dir}', local_dir]
+ subprocess.run(cmd)
+
+if __name__ == "__main__":
+ main()
diff --git a/research/research-tools/src/views/FolderView.vue b/research/research-tools/src/views/FolderView.vue
index 43024b9..1ccaadd 100644
--- a/research/research-tools/src/views/FolderView.vue
+++ b/research/research-tools/src/views/FolderView.vue
@@ -6,6 +6,13 @@
+
+
-
+
@@ -34,6 +48,7 @@
![]()
+
{{ selectedImage.analyze_result }}
@@ -50,6 +65,9 @@ export default {
targetFolder: '',
imageSize: parseInt(localStorage.getItem('imageSize')) || 200,
selectedImage: null,
+ selectionMode: false,
+ isAnalyzing: false,
+ showDropdown: false,
}
},
watch: {
@@ -140,6 +158,44 @@ export default {
closeModal() {
this.selectedImage = null;
},
+ toggleSelectionMode() {
+ this.selectionMode = !this.selectionMode;
+ },
+ handleImageClick(datapoint) {
+ if (this.selectionMode) {
+ this.selectedDatapoints[datapoint.path] = !this.selectedDatapoints[datapoint.path];
+ } else {
+ this.showModal(datapoint);
+ }
+ },
+ toggleDropdown() {
+ if (!this.isAnalyzing) {
+ this.showDropdown = !this.showDropdown;
+ }
+ },
+ async analyzeImages() {
+ this.showDropdown = false;
+ this.isAnalyzing = true;
+ try {
+ for (var i in this.datapoints) {
+ const datapoint = this.datapoints[i];
+ const response = await fetch('/api/analyze', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(datapoint),
+ });
+
+ const result = await response.json();
+ this.datapoints[i] = result;
+ }
+ } catch (error) {
+ alert(`Error analyzing image: ${error}`);
+ } finally {
+ this.isAnalyzing = false;
+ }
+ },
},
}
@@ -151,6 +207,8 @@ export default {
}
.datapoint-container {
+ border: 1px solid green;
+ padding: 5px;
position: relative;
}
@@ -227,4 +285,64 @@ div.datapoints img {
max-height: 90vh;
object-fit: contain;
}
+
+.mode-button {
+ background-color: #666;
+ color: white;
+}
+
+.mode-button.active {
+ background-color: #4CAF50;
+}
+
+.dropdown {
+ position: relative;
+ display: inline-block;
+}
+
+.dropdown-button {
+ background-color: #2196F3;
+ color: white;
+}
+
+.dropdown-button:disabled {
+ background-color: #90CAF9;
+ cursor: not-allowed;
+}
+
+.dropdown-content {
+ position: absolute;
+ background-color: #f9f9f9;
+ min-width: 160px;
+ box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
+ z-index: 1;
+ border-radius: 4px;
+}
+
+.dropdown-content a {
+ color: black;
+ padding: 12px 16px;
+ text-decoration: none;
+ display: block;
+}
+
+.dropdown-content a:hover {
+ background-color: #f1f1f1;
+}
+
+.qr-info {
+ bottom: 5px;
+ right: 5px;
+ background-color: rgba(0, 0, 0, 0.7);
+ color: white;
+ padding: 1rem;
+ border-radius: 3px;
+ font-size: 14px;
+ width: 200px;
+ text-wrap: wrap;
+}
+
+.qr-info > div {
+ margin: 2px 0;
+}
\ No newline at end of file
diff --git a/research/research-tools/src/views/FoldersView.vue b/research/research-tools/src/views/FoldersView.vue
index 40c0328..fbed4b1 100644
--- a/research/research-tools/src/views/FoldersView.vue
+++ b/research/research-tools/src/views/FoldersView.vue
@@ -3,7 +3,7 @@
Folders
-
- {{ folder.folder }}
+ {{ folder.folder }} ({{ folder.count }})
diff --git a/research/server.py b/research/server.py
index 78a3c62..5ebef31 100755
--- a/research/server.py
+++ b/research/server.py
@@ -1,4 +1,5 @@
#!/usr/bin/env python3
+import subprocess
from flask import Flask, request, jsonify, send_file
from datetime import datetime
import os
@@ -7,6 +8,9 @@ import uuid
import shutil
import base64
from io import BytesIO
+import tempfile
+import cv2
+import numpy as np
app = Flask(__name__)
@@ -17,7 +21,7 @@ def image_data_url_to_binary(image_data_url):
if image_data_url.startswith('data:image/jpeg;base64,'):
return base64.b64decode(image_data_url.split(',')[1]), 'image/jpeg'
elif image_data_url.startswith('data:image/png;base64,'):
- return base64.b64decode(image_data_url.split(',')[1]), 'png'
+ return base64.b64decode(image_data_url.split(',')[1]), 'image/png'
else:
raise ValueError(f"Unsupported image data URL: {image_data_url}")
@@ -58,6 +62,42 @@ def folders():
"folders": ret,
}
+@app.route("/api/qrtool", methods=["POST"])
+def qrtool():
+ data = request.get_json()
+ image_data_url = data.get("image_data_url")
+ binary, mimetype = image_data_url_to_binary(image_data_url)
+ if mimetype == 'image/png':
+ suffix = ".png"
+ elif mimetype == 'image/jpeg':
+ suffix = ".jpg"
+ else:
+ raise ValueError(f"Unsupported image type: {mimetype}")
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as f:
+ f.write(binary)
+ qrtool = os.path.abspath("../alg/qrtool")
+ if not os.path.exists(qrtool):
+ return {
+ "ok": False,
+ "error": f"qrtool not found: {qrtool}",
+ }
+ cmd = ["./qrtool", data['cmd'], f.name]
+ print(cmd)
+ try:
+ sp = subprocess.Popen(cmd, cwd=os.path.dirname(qrtool), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ stdout, stderr = sp.communicate()
+
+ return {
+ "exit_code": sp.returncode,
+ "stdout": stdout.decode('utf-8'),
+ "stderr": stderr.decode('utf-8'),
+ }
+ except Exception as e:
+ return {
+ "ok": False,
+ "error": str(e),
+ }
+
@app.route('/api/datapoints')
def datapoints():
folder = request.args.get('folder')
@@ -111,7 +151,7 @@ def do_copy_to_folder(paths, target_folder):
dst = os.path.join(DATA_DIR, "json", target_folder)
os.makedirs(dst, exist_ok=True)
print(f"Copying {src} to {dst}")
- shutil.copy(src, dst)
+ shutil.copy2(src, dst)
@app.route('/api/copy-to-folder', methods=['POST'])
def copy_to_folder():
@@ -124,5 +164,70 @@ def copy_to_folder():
"message": "Datapoints copied to folder",
}
+def do_qrtool(image, cmd):
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as f:
+ image.save(f.name)
+ qrtool = os.path.abspath("../alg/qrtool")
+ cmd = ["./qrtool", cmd, f.name]
+ print(cmd)
+ sp = subprocess.Popen(cmd, cwd=os.path.dirname(qrtool), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ stdout, stderr = sp.communicate()
+ return stdout.decode('utf-8')
+
+def calc_sharpness(image_data_url):
+ image = image_data_url_to_image(image_data_url)
+ # 转换为灰度图像
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
+
+ # 高斯滤波
+ blurred = cv2.GaussianBlur(gray, (5, 5), 0)
+
+ # 计算Sobel梯度
+ sobelx = cv2.Sobel(blurred, cv2.CV_64F, 1, 0, ksize=3)
+ sobely = cv2.Sobel(blurred, cv2.CV_64F, 0, 1, ksize=3)
+
+ # 计算梯度幅值
+ magnitude = np.sqrt(sobelx**2 + sobely**2)
+
+ # 计算清晰度
+ sharpness = np.mean(magnitude) / (image.shape[0] * image.shape[1]) * 1000
+ return sharpness
+
+def image_data_url_to_image(image_data_url):
+ binary, mimetype = image_data_url_to_binary(image_data_url)
+ return cv2.imdecode(np.frombuffer(binary, np.uint8), cv2.IMREAD_COLOR)
+
+def get_image_data_url(path):
+ mimetype = "image/jpeg"
+ if path.endswith(".png"):
+ mimetype = "image/png"
+ with open(path, "rb") as f:
+ binary = f.read()
+ return "data:" + mimetype + ";base64," + base64.b64encode(binary).decode('utf-8')
+
+def get_image_dot_area(image):
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as f:
+ cv2.imwrite(f.name, image)
+ qrtool = os.path.abspath("../alg/qrtool")
+ cmd = ["./qrtool", "dot", f.name]
+ if 0 == subprocess.call(cmd, cwd=os.path.dirname(qrtool)):
+ return get_image_data_url(f.name + ".dot.jpg")
+
+def analyze_datapoint(data):
+ image = image_data_url_to_image(data['image_data_url'])
+ # data["clarity"] = do_qrtool(image, "clarity").split()[-1]
+ # data["analyze_result"] = data
+ data["analyze_result"] = {}
+ data["dot_area"] = get_image_dot_area(image)
+ if data["dot_area"]:
+ data["analyze_result"]["dot_area_sharpness"] = calc_sharpness(data["dot_area"])
+ return data
+
+@app.route('/api/analyze', methods=['POST'])
+def analyze():
+ data = request.get_json()
+ analyze_datapoint(data)
+ return data
+
if __name__ == '__main__':
app.run(host='0.0.0.0', port=26966, debug=True)
\ No newline at end of file