research: Add analysis
This commit is contained in:
parent
35dfd23069
commit
8cf2a8a4a0
23
research/fetch
Executable file
23
research/fetch
Executable 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()
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
Loading…
x
Reference in New Issue
Block a user