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 @@ + +
@@ -20,12 +27,19 @@
-
+
- - - - {{ datapoint.datetime }} + + + + +
+ {{ dp.analyze_result }} +
+ {{ dp.datetime }}
@@ -34,6 +48,7 @@
@@ -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

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