research: Add analysis

This commit is contained in:
Fam Zheng 2025-03-02 15:58:35 +00:00
parent 35dfd23069
commit 8cf2a8a4a0
4 changed files with 254 additions and 8 deletions

23
research/fetch Executable file
View File

@ -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()

View File

@ -6,6 +6,13 @@
<button @click="selectNone">Select None</button>
<button @click="selectInverse">Inverse Selection</button>
<button @click="deleteSelected" class="delete-button">Delete Selected</button>
<button
@click="toggleSelectionMode"
:class="{ active: selectionMode }"
class="mode-button">
{{ selectionMode ? 'Selection Mode: ON' : 'Selection Mode: OFF' }}
</button>
<button @click="analyzeImages()">Analyze All</button>
<input v-model="targetFolder" placeholder="Target folder name">
<button @click="copyToFolder">Copy to Folder</button>
<div class="size-control">
@ -20,12 +27,19 @@
</div>
</div>
<div class="datapoints">
<div class="datapoint" v-for="datapoint in datapoints" :key="datapoint.path">
<div class="datapoint" v-for="dp in datapoints" :key="dp.path">
<div class="datapoint-container">
<input type="checkbox" v-model="selectedDatapoints[datapoint.path]">
<router-link target="_blank" :to="`/datapoint/${folder}/${datapoint.basename}`" @click.prevent="showModal(datapoint)">
<img :title="datapoint_title(datapoint)" class="datapoint-image" :src="datapoint.image_data_url">
<span class="datapoint-datetime">{{ datapoint.datetime }}</span>
<input type="checkbox" v-model="selectedDatapoints[dp.path]">
<router-link
target="_blank"
:to="selectionMode ? null : `/datapoint/${folder}/${dp.basename}`"
@click.prevent="handleImageClick(dp)">
<img :title="datapoint_title(dp)" class="datapoint-image" :src="dp.image_data_url">
<img v-if="dp.dot_area" class="datapoint-dot-area" :src="dp.dot_area">
<div v-if="dp.analyze_result" class="analyze-result">
{{ dp.analyze_result }}
</div>
<span class="datapoint-datetime">{{ dp.datetime }}</span>
</router-link>
</div>
</div>
@ -34,6 +48,7 @@
<!-- Add modal component -->
<div v-if="selectedImage" class="modal" @click="closeModal">
<img :src="selectedImage.image_data_url" class="modal-image">
<div class="analyze-result" v-if="selectedImage.analyze_result"> {{ selectedImage.analyze_result }}</div>
</div>
</div>
</template>
@ -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;
}
},
},
}
</script>
@ -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;
}
</style>

View File

@ -3,7 +3,7 @@
<h1>Folders</h1>
<ul>
<li v-for="folder in folders" :key="folder.folder">
<router-link :to="`/folder/${folder.folder}`">{{ folder.folder }}</router-link>
<router-link :to="`/folder/${folder.folder}`">{{ folder.folder }} ({{ folder.count }})</router-link>
</li>
</ul>
</div>

View File

@ -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)