Spaces:
Sleeping
Sleeping
tyrwh
commited on
Commit
·
1b2b626
1
Parent(s):
9315b87
Adding Keyence folder handling
Browse files- app.py +44 -10
- nemaquant.py +52 -61
- static/script.js +297 -20
- templates/index.html +4 -3
app.py
CHANGED
|
@@ -64,14 +64,45 @@ def process_images():
|
|
| 64 |
saved_files = []
|
| 65 |
error_files = []
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
if not saved_files:
|
| 77 |
return jsonify({"error": f"No valid files uploaded. Invalid files: {error_files}"}), 400
|
|
@@ -82,6 +113,9 @@ def process_images():
|
|
| 82 |
elif input_mode == 'single':
|
| 83 |
input_target = str(saved_files[0])
|
| 84 |
img_mode_arg = 'file'
|
|
|
|
|
|
|
|
|
|
| 85 |
else:
|
| 86 |
return jsonify({"error": f"Invalid input mode: {input_mode}"}), 400
|
| 87 |
|
|
@@ -91,11 +125,11 @@ def process_images():
|
|
| 91 |
cmd = [
|
| 92 |
sys.executable,
|
| 93 |
str(APP_ROOT / 'nemaquant.py'),
|
|
|
|
| 94 |
'-i', input_target,
|
| 95 |
'-w', str(WEIGHTS_FILE),
|
| 96 |
'-o', str(output_csv),
|
| 97 |
-
'-a', str(annotated_dir)
|
| 98 |
-
'--conf', confidence
|
| 99 |
]
|
| 100 |
|
| 101 |
print(f"[{job_id}] Prepared command: {' '.join(cmd)}")
|
|
|
|
| 64 |
saved_files = []
|
| 65 |
error_files = []
|
| 66 |
|
| 67 |
+
# For keyence mode, validate XY subdirectory structure but flatten files
|
| 68 |
+
if input_mode == 'keyence':
|
| 69 |
+
xy_subdirs = set()
|
| 70 |
+
# First pass: validate XY subdirectory structure
|
| 71 |
+
for file in files:
|
| 72 |
+
if file and file.filename:
|
| 73 |
+
relative_path = file.filename
|
| 74 |
+
if '/' in relative_path:
|
| 75 |
+
first_dir = relative_path.split('/')[0]
|
| 76 |
+
if re.match(r'^XY[0-9][0-9]$', first_dir):
|
| 77 |
+
xy_subdirs.add(first_dir)
|
| 78 |
+
|
| 79 |
+
# Validate that we found XY subdirectories
|
| 80 |
+
if not xy_subdirs:
|
| 81 |
+
return jsonify({
|
| 82 |
+
"error": "Keyence mode requires folder structure with XY[0-9][0-9] subdirectories (e.g., XY01/, XY02/). No such subdirectories found in uploaded folder."
|
| 83 |
+
}), 400
|
| 84 |
+
|
| 85 |
+
# Second pass: save files with flattened structure
|
| 86 |
+
for file in files:
|
| 87 |
+
if file and allowed_file(file.filename):
|
| 88 |
+
# Extract just the filename, ignoring the folder structure
|
| 89 |
+
filename = secure_filename(Path(file.filename).name)
|
| 90 |
+
save_path = job_output_dir / filename
|
| 91 |
+
file.save(str(save_path))
|
| 92 |
+
saved_files.append(save_path)
|
| 93 |
+
elif file:
|
| 94 |
+
error_files.append(file.filename)
|
| 95 |
+
|
| 96 |
+
else:
|
| 97 |
+
# Original file handling for non-keyence modes
|
| 98 |
+
for file in files:
|
| 99 |
+
if file and allowed_file(file.filename):
|
| 100 |
+
filename = secure_filename(file.filename)
|
| 101 |
+
save_path = job_output_dir / filename
|
| 102 |
+
file.save(str(save_path))
|
| 103 |
+
saved_files.append(save_path)
|
| 104 |
+
elif file:
|
| 105 |
+
error_files.append(file.filename)
|
| 106 |
|
| 107 |
if not saved_files:
|
| 108 |
return jsonify({"error": f"No valid files uploaded. Invalid files: {error_files}"}), 400
|
|
|
|
| 113 |
elif input_mode == 'single':
|
| 114 |
input_target = str(saved_files[0])
|
| 115 |
img_mode_arg = 'file'
|
| 116 |
+
elif input_mode == 'keyence':
|
| 117 |
+
input_target = str(job_output_dir)
|
| 118 |
+
img_mode_arg = 'keyence'
|
| 119 |
else:
|
| 120 |
return jsonify({"error": f"Invalid input mode: {input_mode}"}), 400
|
| 121 |
|
|
|
|
| 125 |
cmd = [
|
| 126 |
sys.executable,
|
| 127 |
str(APP_ROOT / 'nemaquant.py'),
|
| 128 |
+
'-m', img_mode_arg,
|
| 129 |
'-i', input_target,
|
| 130 |
'-w', str(WEIGHTS_FILE),
|
| 131 |
'-o', str(output_csv),
|
| 132 |
+
'-a', str(annotated_dir)
|
|
|
|
| 133 |
]
|
| 134 |
|
| 135 |
print(f"[{job_id}] Prepared command: {' '.join(cmd)}")
|
nemaquant.py
CHANGED
|
@@ -11,10 +11,11 @@ from pathlib import Path
|
|
| 11 |
from ultralytics import YOLO
|
| 12 |
from glob import glob
|
| 13 |
import re
|
| 14 |
-
import sys
|
| 15 |
|
| 16 |
def options():
|
| 17 |
-
parser = argparse.ArgumentParser(description="Nematode egg image processing with
|
|
|
|
| 18 |
parser.add_argument("-i", "--img", help="Target image directory or image (REQUIRED)", required=True)
|
| 19 |
parser.add_argument('-w', '--weights', help='Weights file for use with YOLO11 model')
|
| 20 |
parser.add_argument("-o","--output", help="Name of results file. If no file is specified, one will be created from the key file name")
|
|
@@ -24,21 +25,25 @@ def options():
|
|
| 24 |
args = parser.parse_args()
|
| 25 |
return args
|
| 26 |
|
| 27 |
-
# TODO - maybe rework this from a function to custom argparse.Action() subclasses?
|
| 28 |
def check_args():
|
| 29 |
args = options()
|
| 30 |
# basic checks on target file validity
|
| 31 |
args.imgpath = Path(args.img)
|
| 32 |
if not args.imgpath.exists():
|
| 33 |
raise Exception("Target %s is not a valid path" % args.img)
|
| 34 |
-
if
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
# if no weights file, try using the default weights.pt
|
| 44 |
if not args.weights:
|
|
@@ -49,55 +54,39 @@ def check_args():
|
|
| 49 |
else:
|
| 50 |
raise Exception('No weights file specified and default weights.pt not found in script directory')
|
| 51 |
|
| 52 |
-
#
|
| 53 |
-
if
|
| 54 |
-
|
| 55 |
-
if
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
print('%s valid images found' % len(potential_images))
|
| 62 |
-
args.xy_mode = False
|
| 63 |
-
args.subimage_paths = potential_images
|
| 64 |
else:
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
if args.key:
|
| 72 |
-
args.keypath = Path(args.key)
|
| 73 |
-
if not args.keypath.exists():
|
| 74 |
-
raise Exception('Specified key file does not exist: %s' % args.keypath)
|
| 75 |
-
if args.keypath.suffix != '.csv':
|
| 76 |
-
raise Exception("Specified key file is not a .csv: %s" % args.keypath)
|
| 77 |
else:
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
if len(potential_keys) == 0:
|
| 81 |
-
raise Exception("No .csv files found in target folder %s, please check directory" % args.img)
|
| 82 |
-
if len(potential_keys) > 1:
|
| 83 |
-
raise Exception("Multiple .csv files found in target folder %s, please specify which one to use")
|
| 84 |
-
else:
|
| 85 |
-
args.keypath = potential_keys[0]
|
| 86 |
-
args.key = str(potential_keys[0])
|
| 87 |
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
else:
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
if args.xy_mode:
|
| 97 |
-
args.output = '%s_eggcounts.csv' % args.keypath.stem
|
| 98 |
-
else:
|
| 99 |
-
args.output = '%s_eggcounts.csv' % args.imgpath.stem
|
| 100 |
-
args.outpath = Path(args.output)
|
| 101 |
|
| 102 |
# finally, check the target dir to save annotated images in
|
| 103 |
if args.annotated:
|
|
@@ -154,8 +143,9 @@ def main():
|
|
| 154 |
tmp_filenames = []
|
| 155 |
# single-image mode
|
| 156 |
if args.img_mode == 'file':
|
|
|
|
| 157 |
img = cv2.imread(str(args.imgpath))
|
| 158 |
-
results = model.predict(img, imgsz = 1440, max_det=1000, verbose=False, conf=
|
| 159 |
result = results[0]
|
| 160 |
box_classes = [result.names[int(x)] for x in result.boxes.cls]
|
| 161 |
# NOTE - filtering by class is not necessary, but would make this easier to extend to multi-class models
|
|
@@ -171,8 +161,9 @@ def main():
|
|
| 171 |
cv2.imwrite(str(annot_path), annot)
|
| 172 |
print('Saving annotations to %s...' % str(annot_path))
|
| 173 |
# multi-image mode, runs differently depending on whether you have /XY00/ subdirectories
|
| 174 |
-
elif args.img_mode
|
| 175 |
-
|
|
|
|
| 176 |
total_subdirs = len(args.subdir_paths)
|
| 177 |
for i, subdir in enumerate(args.subdir_paths):
|
| 178 |
# Report progress
|
|
@@ -200,7 +191,7 @@ def main():
|
|
| 200 |
impath = candidate_img_paths[0]
|
| 201 |
# get the actual output
|
| 202 |
img = cv2.imread(str(impath))
|
| 203 |
-
results = model.predict(img, imgsz = 1440, verbose=False, conf=
|
| 204 |
result = results[0]
|
| 205 |
box_classes = [result.names[int(x)] for x in result.boxes.cls]
|
| 206 |
egg_xy = [x.cpu().numpy().astype(np.int32) for i,x in enumerate(result.boxes.xyxy) if box_classes[i] == 'egg']
|
|
@@ -237,7 +228,7 @@ def main():
|
|
| 237 |
sys.stdout.flush() # Flush output buffer
|
| 238 |
|
| 239 |
img = cv2.imread(str(impath))
|
| 240 |
-
results = model.predict(img, imgsz = 1440, verbose=False, conf=
|
| 241 |
result = results[0]
|
| 242 |
box_classes = [result.names[int(x)] for x in result.boxes.cls]
|
| 243 |
egg_xy = [x.cpu().numpy().astype(np.int32) for i,x in enumerate(result.boxes.xyxy) if box_classes[i] == 'egg']
|
|
|
|
| 11 |
from ultralytics import YOLO
|
| 12 |
from glob import glob
|
| 13 |
import re
|
| 14 |
+
import sys
|
| 15 |
|
| 16 |
def options():
|
| 17 |
+
parser = argparse.ArgumentParser(description="Nematode egg image processing with YOLO11 model.")
|
| 18 |
+
parser.add_argument("-m", "--img_mode", help="Mode to run", required=True)
|
| 19 |
parser.add_argument("-i", "--img", help="Target image directory or image (REQUIRED)", required=True)
|
| 20 |
parser.add_argument('-w', '--weights', help='Weights file for use with YOLO11 model')
|
| 21 |
parser.add_argument("-o","--output", help="Name of results file. If no file is specified, one will be created from the key file name")
|
|
|
|
| 25 |
args = parser.parse_args()
|
| 26 |
return args
|
| 27 |
|
|
|
|
| 28 |
def check_args():
|
| 29 |
args = options()
|
| 30 |
# basic checks on target file validity
|
| 31 |
args.imgpath = Path(args.img)
|
| 32 |
if not args.imgpath.exists():
|
| 33 |
raise Exception("Target %s is not a valid path" % args.img)
|
| 34 |
+
# check if img_mode is specified and valid
|
| 35 |
+
if args.img_mode:
|
| 36 |
+
valid_modes = ['dir', 'file', 'keyence']
|
| 37 |
+
if args.img_mode not in valid_modes:
|
| 38 |
+
raise Exception(f"img_mode must be one of: {', '.join(valid_modes)}")
|
| 39 |
+
# check for potential images in the target directory
|
| 40 |
+
if args.img_mode in ['dir','keyence']:
|
| 41 |
+
potential_images = [x for x in args.imgpath.iterdir() if x.suffix.lower() in ['.tif','.tiff','.jpg','.jpeg','.png']]
|
| 42 |
+
if len(potential_images) == 0:
|
| 43 |
+
raise Exception('No valid images (.png, .tif, .tiff, .jpeg, .jpg) in target folder %s' % args.img)
|
| 44 |
+
else:
|
| 45 |
+
print('%s valid images found' % len(potential_images))
|
| 46 |
+
args.subimage_paths = potential_images
|
| 47 |
|
| 48 |
# if no weights file, try using the default weights.pt
|
| 49 |
if not args.weights:
|
|
|
|
| 54 |
else:
|
| 55 |
raise Exception('No weights file specified and default weights.pt not found in script directory')
|
| 56 |
|
| 57 |
+
# for /XY00/ subdirectories, we require a valid key
|
| 58 |
+
# ensure that either a key is specified, or if a single .csv exists in the target dir, use that
|
| 59 |
+
if args.img_mode == 'keyence':
|
| 60 |
+
if args.key:
|
| 61 |
+
args.keypath = Path(args.key)
|
| 62 |
+
if not arg.keypath.exists():
|
| 63 |
+
raise Exception('Specified key file does not exist: %s' % args.keypath)
|
| 64 |
+
if args.keypath.suffix != '.csv':
|
| 65 |
+
raise Exception("Specified key file is not a .csv: %s" % args.keypath)
|
|
|
|
|
|
|
|
|
|
| 66 |
else:
|
| 67 |
+
print('Running on /XY00/ subdirectories but no key specified. Looking for key file...')
|
| 68 |
+
potential_keys = list(args.imgpath.glob('*.csv'))
|
| 69 |
+
if len(potential_keys) == 0:
|
| 70 |
+
raise Exception("No .csv files found in target folder %s, please check directory" % args.img)
|
| 71 |
+
if len(potential_keys) > 1:
|
| 72 |
+
raise Exception("Multiple .csv files found in target folder %s, please specify which one to use")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
else:
|
| 74 |
+
args.keypath = potential_keys[0]
|
| 75 |
+
args.key = str(potential_keys[0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
+
# if path to results file is specified, ensure it is .csv
|
| 78 |
+
if args.output:
|
| 79 |
+
args.outpath = Path(args.output)
|
| 80 |
+
if args.outpath.suffix != '.csv':
|
| 81 |
+
raise Exception("Specified output file is not a .csv: %s" % args.outpath)
|
| 82 |
+
else:
|
| 83 |
+
# for XY00 subdirs, name it after the required key file
|
| 84 |
+
# for an image directory, name it after the directory
|
| 85 |
+
if args.img_mode == 'keyence':
|
| 86 |
+
args.output = '%s_eggcounts.csv' % args.keypath.stem
|
| 87 |
else:
|
| 88 |
+
args.output = '%s_eggcounts.csv' % args.imgpath.stem
|
| 89 |
+
args.outpath = Path(args.output)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
# finally, check the target dir to save annotated images in
|
| 92 |
if args.annotated:
|
|
|
|
| 143 |
tmp_filenames = []
|
| 144 |
# single-image mode
|
| 145 |
if args.img_mode == 'file':
|
| 146 |
+
# imread then apply model, one-step predict() can't handle TIFF
|
| 147 |
img = cv2.imread(str(args.imgpath))
|
| 148 |
+
results = model.predict(img, imgsz = 1440, max_det=1000, verbose=False, conf=0.05)
|
| 149 |
result = results[0]
|
| 150 |
box_classes = [result.names[int(x)] for x in result.boxes.cls]
|
| 151 |
# NOTE - filtering by class is not necessary, but would make this easier to extend to multi-class models
|
|
|
|
| 161 |
cv2.imwrite(str(annot_path), annot)
|
| 162 |
print('Saving annotations to %s...' % str(annot_path))
|
| 163 |
# multi-image mode, runs differently depending on whether you have /XY00/ subdirectories
|
| 164 |
+
elif args.img_mode in ['dir', 'keyence']:
|
| 165 |
+
subdir_paths = []
|
| 166 |
+
if args.img_mode == 'keyence':
|
| 167 |
total_subdirs = len(args.subdir_paths)
|
| 168 |
for i, subdir in enumerate(args.subdir_paths):
|
| 169 |
# Report progress
|
|
|
|
| 191 |
impath = candidate_img_paths[0]
|
| 192 |
# get the actual output
|
| 193 |
img = cv2.imread(str(impath))
|
| 194 |
+
results = model.predict(img, imgsz = 1440, verbose=False, conf=0.05)
|
| 195 |
result = results[0]
|
| 196 |
box_classes = [result.names[int(x)] for x in result.boxes.cls]
|
| 197 |
egg_xy = [x.cpu().numpy().astype(np.int32) for i,x in enumerate(result.boxes.xyxy) if box_classes[i] == 'egg']
|
|
|
|
| 228 |
sys.stdout.flush() # Flush output buffer
|
| 229 |
|
| 230 |
img = cv2.imread(str(impath))
|
| 231 |
+
results = model.predict(img, imgsz = 1440, verbose=False, conf=0.05)
|
| 232 |
result = results[0]
|
| 233 |
box_classes = [result.names[int(x)] for x in result.boxes.cls]
|
| 234 |
egg_xy = [x.cpu().numpy().astype(np.int32) for i,x in enumerate(result.boxes.xyxy) if box_classes[i] == 'egg']
|
static/script.js
CHANGED
|
@@ -59,12 +59,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 59 |
fileInput.setAttribute('multiple', '');
|
| 60 |
inputModeHelp.textContent = 'Choose one or more image files for processing';
|
| 61 |
uploadText.textContent = 'Drag and drop images here or click to browse';
|
| 62 |
-
} else {
|
| 63 |
fileInput.setAttribute('webkitdirectory', '');
|
| 64 |
fileInput.setAttribute('directory', '');
|
| 65 |
fileInput.removeAttribute('multiple');
|
| 66 |
inputModeHelp.textContent = 'Select a folder containing images to process';
|
| 67 |
uploadText.textContent = 'Click to select a folder containing images';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
}
|
| 69 |
// Clear any existing files
|
| 70 |
fileInput.value = '';
|
|
@@ -75,17 +81,104 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 75 |
// File Upload Handling
|
| 76 |
function handleFiles(files) {
|
| 77 |
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/tif'];
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
}
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
const invalidFiles = Array.from(files).filter(file => !allowedTypes.includes(file.type));
|
| 87 |
|
| 88 |
-
if
|
|
|
|
| 89 |
logStatus(`Warning: Skipped ${invalidFiles.length} invalid files. Only PNG, JPG, and TIFF are supported.`);
|
| 90 |
invalidFiles.forEach(file => {
|
| 91 |
logStatus(`- Skipped: ${file.name} (invalid type: ${file.type || 'unknown'})`);
|
|
@@ -169,14 +262,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 169 |
});
|
| 170 |
|
| 171 |
function updateUploadState(validFileCount) {
|
| 172 |
-
|
| 173 |
-
if (!
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
startProcessingBtn.disabled = true;
|
| 178 |
} else {
|
| 179 |
-
// Show the count here
|
| 180 |
uploadText.textContent = `${validFileCount} image${validFileCount === 1 ? '' : 's'} selected`;
|
| 181 |
startProcessingBtn.disabled = validFileCount === 0;
|
| 182 |
}
|
|
@@ -195,7 +291,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 195 |
|
| 196 |
// Processing
|
| 197 |
startProcessingBtn.addEventListener('click', async () => {
|
| 198 |
-
|
|
|
|
| 199 |
if (!files || files.length === 0) {
|
| 200 |
logStatus('Error: No files selected.');
|
| 201 |
return;
|
|
@@ -284,7 +381,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 284 |
|
| 285 |
const totalImages = Array.from(fileInput.files).filter(file => {
|
| 286 |
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/tif'];
|
| 287 |
-
if (inputMode.value === 'folder') {
|
| 288 |
return allowedTypes.includes(file.type) &&
|
| 289 |
file.webkitRelativePath &&
|
| 290 |
!file.webkitRelativePath.startsWith('.');
|
|
@@ -311,7 +408,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 311 |
updateProgress(data.progress || 0, data.status);
|
| 312 |
|
| 313 |
// Update image counter based on progress percentage
|
| 314 |
-
if (data.status === '
|
| 315 |
const processedImages = Math.floor((data.progress / 90) * totalImages); // 90 is the max progress before completion
|
| 316 |
imageCounter.textContent = `${processedImages} of ${totalImages} images`;
|
| 317 |
}
|
|
@@ -378,7 +475,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 378 |
progressText.textContent = message;
|
| 379 |
|
| 380 |
// Clear the image counter when not processing
|
| 381 |
-
if (message === '
|
| 382 |
imageCounter.textContent = '';
|
| 383 |
}
|
| 384 |
}
|
|
@@ -922,7 +1019,187 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 922 |
imageInfo.innerHTML = infoText;
|
| 923 |
}
|
| 924 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 925 |
// Initialize
|
| 926 |
updateUploadState();
|
| 927 |
logStatus('Application ready');
|
| 928 |
-
});
|
|
|
|
| 59 |
fileInput.setAttribute('multiple', '');
|
| 60 |
inputModeHelp.textContent = 'Choose one or more image files for processing';
|
| 61 |
uploadText.textContent = 'Drag and drop images here or click to browse';
|
| 62 |
+
} else if (mode === 'folder') {
|
| 63 |
fileInput.setAttribute('webkitdirectory', '');
|
| 64 |
fileInput.setAttribute('directory', '');
|
| 65 |
fileInput.removeAttribute('multiple');
|
| 66 |
inputModeHelp.textContent = 'Select a folder containing images to process';
|
| 67 |
uploadText.textContent = 'Click to select a folder containing images';
|
| 68 |
+
} else if (mode === 'keyence') {
|
| 69 |
+
fileInput.setAttribute('webkitdirectory', '');
|
| 70 |
+
fileInput.setAttribute('directory', '');
|
| 71 |
+
fileInput.removeAttribute('multiple');
|
| 72 |
+
inputModeHelp.textContent = 'Select a Keyence output folder. Folder should have subdirectories of format XY01, XY02, etc.';
|
| 73 |
+
uploadText.textContent = 'Click to select a Keyence output folder';
|
| 74 |
}
|
| 75 |
// Clear any existing files
|
| 76 |
fileInput.value = '';
|
|
|
|
| 81 |
// File Upload Handling
|
| 82 |
function handleFiles(files) {
|
| 83 |
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/tif'];
|
| 84 |
+
// --- Keyence folder validation ---
|
| 85 |
+
if (inputMode.value === 'keyence') {
|
| 86 |
+
// Collect all unique first-level subdirectory names from webkitRelativePath
|
| 87 |
+
const subdirs = new Set();
|
| 88 |
+
Array.from(files).forEach(file => {
|
| 89 |
+
if (file.webkitRelativePath) {
|
| 90 |
+
const parts = file.webkitRelativePath.split('/');
|
| 91 |
+
if (parts.length > 2) { // Parent/Subdir/File
|
| 92 |
+
subdirs.add(parts[1]);
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
});
|
| 96 |
+
// Check for at least one subdir matching XY[0-9]{2}
|
| 97 |
+
const xyPattern = /^XY\d{2}$/;
|
| 98 |
+
const hasXY = Array.from(subdirs).some(name => xyPattern.test(name));
|
| 99 |
+
if (!hasXY) {
|
| 100 |
+
// Print the first 5 values in subdirs for debugging
|
| 101 |
+
const subdirArr = Array.from(subdirs).slice(0, 5);
|
| 102 |
+
logStatus('First 5 subdirectories found: ' + (subdirArr.length ? subdirArr.join(', ') : '[none]'));
|
| 103 |
+
logStatus('Error: The selected folder does not contain any subdirectory named like "XY01", "XY02", etc. Please select a valid Keyence output folder.');
|
| 104 |
+
fileList.innerHTML = '';
|
| 105 |
+
startProcessingBtn.disabled = true;
|
| 106 |
+
return;
|
| 107 |
}
|
| 108 |
+
// --- Check for .csv file(s) ---
|
| 109 |
+
const csvFiles = Array.from(files).filter(file => file.name.toLowerCase().endsWith('.csv'));
|
| 110 |
+
if (csvFiles.length === 1) {
|
| 111 |
+
// Show a custom dialog with multiple options
|
| 112 |
+
showSingleCsvDialog(csvFiles[0]);
|
| 113 |
+
return;
|
| 114 |
+
} else if (csvFiles.length === 0) {
|
| 115 |
+
const wantToUpload = window.confirm('No CSV key file was found in the folder. Would you like to supply a CSV key file now?');
|
| 116 |
+
if (wantToUpload) {
|
| 117 |
+
// Create a hidden file input for CSV upload
|
| 118 |
+
let csvInput = document.createElement('input');
|
| 119 |
+
csvInput.type = 'file';
|
| 120 |
+
csvInput.accept = '.csv,.CSV';
|
| 121 |
+
csvInput.style.display = 'none';
|
| 122 |
+
document.body.appendChild(csvInput);
|
| 123 |
+
csvInput.addEventListener('change', function() {
|
| 124 |
+
if (csvInput.files && csvInput.files.length === 1) {
|
| 125 |
+
window.selectedKeyenceCsv = csvInput.files[0];
|
| 126 |
+
logStatus(`CSV key file supplied: ${csvInput.files[0].name}`);
|
| 127 |
+
} else {
|
| 128 |
+
window.selectedKeyenceCsv = null;
|
| 129 |
+
logStatus('No CSV key file supplied.');
|
| 130 |
+
}
|
| 131 |
+
document.body.removeChild(csvInput);
|
| 132 |
+
});
|
| 133 |
+
csvInput.click();
|
| 134 |
+
} else {
|
| 135 |
+
window.selectedKeyenceCsv = null;
|
| 136 |
+
logStatus('No CSV key file will be used.');
|
| 137 |
+
}
|
| 138 |
+
} else {
|
| 139 |
+
// Multiple CSVs found, show a custom dialog for selection
|
| 140 |
+
showCsvSelectionDialog(csvFiles);
|
| 141 |
+
return; // Wait for user selection before proceeding
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
let validFiles;
|
| 145 |
+
if (inputMode.value === 'keyence') {
|
| 146 |
+
// Only analyze the first valid image file (alphabetically) in each subdirectory
|
| 147 |
+
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/tif'];
|
| 148 |
+
// Map: subdir name -> array of files in that subdir
|
| 149 |
+
const subdirFiles = {};
|
| 150 |
+
Array.from(files).forEach(file => {
|
| 151 |
+
if (file.webkitRelativePath) {
|
| 152 |
+
const parts = file.webkitRelativePath.split('/');
|
| 153 |
+
if (parts.length > 2) { // Parent/Subdir/File
|
| 154 |
+
const subdir = parts[1];
|
| 155 |
+
if (!subdirFiles[subdir]) subdirFiles[subdir] = [];
|
| 156 |
+
if (allowedTypes.includes(file.type) && !file.webkitRelativePath.startsWith('.')) {
|
| 157 |
+
subdirFiles[subdir].push(file);
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
});
|
| 162 |
+
// For each subdir, pick the first file alphabetically
|
| 163 |
+
validFiles = Object.values(subdirFiles).map(filesArr => {
|
| 164 |
+
return filesArr.sort((a, b) => a.name.localeCompare(b.name))[0];
|
| 165 |
+
}).filter(Boolean); // Remove undefined
|
| 166 |
+
} else {
|
| 167 |
+
validFiles = Array.from(files).filter(file => {
|
| 168 |
+
if (inputMode.value === 'folder') {
|
| 169 |
+
return allowedTypes.includes(file.type) &&
|
| 170 |
+
file.webkitRelativePath &&
|
| 171 |
+
!file.webkitRelativePath.startsWith('.');
|
| 172 |
+
}
|
| 173 |
+
return allowedTypes.includes(file.type);
|
| 174 |
+
});
|
| 175 |
+
}
|
| 176 |
+
filteredValidFiles = validFiles;
|
| 177 |
+
|
| 178 |
const invalidFiles = Array.from(files).filter(file => !allowedTypes.includes(file.type));
|
| 179 |
|
| 180 |
+
// Only print invalid file warnings if not in Keyence mode
|
| 181 |
+
if (invalidFiles.length > 0 && inputMode.value !== 'keyence') {
|
| 182 |
logStatus(`Warning: Skipped ${invalidFiles.length} invalid files. Only PNG, JPG, and TIFF are supported.`);
|
| 183 |
invalidFiles.forEach(file => {
|
| 184 |
logStatus(`- Skipped: ${file.name} (invalid type: ${file.type || 'unknown'})`);
|
|
|
|
| 262 |
});
|
| 263 |
|
| 264 |
function updateUploadState(validFileCount) {
|
| 265 |
+
// Use filteredValidFiles for enabling/disabling the button
|
| 266 |
+
if (!filteredValidFiles || filteredValidFiles.length === 0) {
|
| 267 |
+
if (inputMode.value === 'folder') {
|
| 268 |
+
uploadText.textContent = 'Click to select a folder containing images';
|
| 269 |
+
} else if (inputMode.value === 'keyence') {
|
| 270 |
+
uploadText.textContent = 'Click to select a Keyence output folder';
|
| 271 |
+
} else {
|
| 272 |
+
uploadText.textContent = 'Drag and drop images here or click to browse';
|
| 273 |
+
}
|
| 274 |
startProcessingBtn.disabled = true;
|
| 275 |
} else {
|
|
|
|
| 276 |
uploadText.textContent = `${validFileCount} image${validFileCount === 1 ? '' : 's'} selected`;
|
| 277 |
startProcessingBtn.disabled = validFileCount === 0;
|
| 278 |
}
|
|
|
|
| 291 |
|
| 292 |
// Processing
|
| 293 |
startProcessingBtn.addEventListener('click', async () => {
|
| 294 |
+
// Use filteredValidFiles for processing
|
| 295 |
+
const files = filteredValidFiles;
|
| 296 |
if (!files || files.length === 0) {
|
| 297 |
logStatus('Error: No files selected.');
|
| 298 |
return;
|
|
|
|
| 381 |
|
| 382 |
const totalImages = Array.from(fileInput.files).filter(file => {
|
| 383 |
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/tif'];
|
| 384 |
+
if (inputMode.value === 'folder' || inputMode.value === 'keyence') {
|
| 385 |
return allowedTypes.includes(file.type) &&
|
| 386 |
file.webkitRelativePath &&
|
| 387 |
!file.webkitRelativePath.startsWith('.');
|
|
|
|
| 408 |
updateProgress(data.progress || 0, data.status);
|
| 409 |
|
| 410 |
// Update image counter based on progress percentage
|
| 411 |
+
if (data.status === 'Running' || data.status === 'Starting') {
|
| 412 |
const processedImages = Math.floor((data.progress / 90) * totalImages); // 90 is the max progress before completion
|
| 413 |
imageCounter.textContent = `${processedImages} of ${totalImages} images`;
|
| 414 |
}
|
|
|
|
| 475 |
progressText.textContent = message;
|
| 476 |
|
| 477 |
// Clear the image counter when not processing
|
| 478 |
+
if (message === 'Please upload an image' || message === 'Processing complete' || message.startsWith('Error')) {
|
| 479 |
imageCounter.textContent = '';
|
| 480 |
}
|
| 481 |
}
|
|
|
|
| 1019 |
imageInfo.innerHTML = infoText;
|
| 1020 |
}
|
| 1021 |
|
| 1022 |
+
// Custom dialog for selecting among multiple CSV files
|
| 1023 |
+
function showCsvSelectionDialog(csvFiles) {
|
| 1024 |
+
// Create overlay
|
| 1025 |
+
const overlay = document.createElement('div');
|
| 1026 |
+
overlay.style.position = 'fixed';
|
| 1027 |
+
overlay.style.top = 0;
|
| 1028 |
+
overlay.style.left = 0;
|
| 1029 |
+
overlay.style.width = '100vw';
|
| 1030 |
+
overlay.style.height = '100vh';
|
| 1031 |
+
overlay.style.background = 'rgba(0,0,0,0.4)';
|
| 1032 |
+
overlay.style.zIndex = 9999;
|
| 1033 |
+
overlay.style.display = 'flex';
|
| 1034 |
+
overlay.style.alignItems = 'center';
|
| 1035 |
+
overlay.style.justifyContent = 'center';
|
| 1036 |
+
|
| 1037 |
+
// Create dialog box
|
| 1038 |
+
const dialog = document.createElement('div');
|
| 1039 |
+
dialog.style.background = '#fff';
|
| 1040 |
+
dialog.style.padding = '24px 32px';
|
| 1041 |
+
dialog.style.borderRadius = '8px';
|
| 1042 |
+
dialog.style.boxShadow = '0 2px 16px rgba(0,0,0,0.2)';
|
| 1043 |
+
dialog.style.minWidth = '320px';
|
| 1044 |
+
dialog.innerHTML = `<h3>Select a CSV key file</h3><p>Multiple CSV files were found. Please select one to use as the key file:</p>`;
|
| 1045 |
+
|
| 1046 |
+
// List CSV files as buttons
|
| 1047 |
+
csvFiles.forEach((file, idx) => {
|
| 1048 |
+
const btn = document.createElement('button');
|
| 1049 |
+
btn.textContent = file.name;
|
| 1050 |
+
btn.style.display = 'block';
|
| 1051 |
+
btn.style.margin = '8px 0';
|
| 1052 |
+
btn.onclick = () => {
|
| 1053 |
+
window.selectedKeyenceCsv = file;
|
| 1054 |
+
logStatus(`CSV key file selected: ${file.name}`);
|
| 1055 |
+
document.body.removeChild(overlay);
|
| 1056 |
+
};
|
| 1057 |
+
dialog.appendChild(btn);
|
| 1058 |
+
});
|
| 1059 |
+
|
| 1060 |
+
// Option: Use a different .csv
|
| 1061 |
+
const otherBtn = document.createElement('button');
|
| 1062 |
+
otherBtn.textContent = 'Use a different .csv';
|
| 1063 |
+
otherBtn.style.display = 'block';
|
| 1064 |
+
otherBtn.style.margin = '16px 0 8px 0';
|
| 1065 |
+
otherBtn.onclick = () => {
|
| 1066 |
+
let csvInput = document.createElement('input');
|
| 1067 |
+
csvInput.type = 'file';
|
| 1068 |
+
csvInput.accept = '.csv,.CSV';
|
| 1069 |
+
csvInput.style.display = 'none';
|
| 1070 |
+
document.body.appendChild(csvInput);
|
| 1071 |
+
csvInput.addEventListener('change', function() {
|
| 1072 |
+
if (csvInput.files && csvInput.files.length === 1) {
|
| 1073 |
+
window.selectedKeyenceCsv = csvInput.files[0];
|
| 1074 |
+
logStatus(`CSV key file supplied: ${csvInput.files[0].name}`);
|
| 1075 |
+
} else {
|
| 1076 |
+
window.selectedKeyenceCsv = null;
|
| 1077 |
+
logStatus('No CSV key file supplied.');
|
| 1078 |
+
}
|
| 1079 |
+
document.body.removeChild(csvInput);
|
| 1080 |
+
document.body.removeChild(overlay);
|
| 1081 |
+
});
|
| 1082 |
+
csvInput.click();
|
| 1083 |
+
};
|
| 1084 |
+
dialog.appendChild(otherBtn);
|
| 1085 |
+
|
| 1086 |
+
// Option: No key file
|
| 1087 |
+
const noneBtn = document.createElement('button');
|
| 1088 |
+
noneBtn.textContent = 'No key file';
|
| 1089 |
+
noneBtn.style.display = 'block';
|
| 1090 |
+
noneBtn.style.margin = '8px 0';
|
| 1091 |
+
noneBtn.onclick = () => {
|
| 1092 |
+
window.selectedKeyenceCsv = null;
|
| 1093 |
+
logStatus('No CSV key file will be used.');
|
| 1094 |
+
document.body.removeChild(overlay);
|
| 1095 |
+
};
|
| 1096 |
+
dialog.appendChild(noneBtn);
|
| 1097 |
+
|
| 1098 |
+
// Cancel button
|
| 1099 |
+
const cancelBtn = document.createElement('button');
|
| 1100 |
+
cancelBtn.textContent = 'Cancel';
|
| 1101 |
+
cancelBtn.style.display = 'block';
|
| 1102 |
+
cancelBtn.style.margin = '8px 0';
|
| 1103 |
+
cancelBtn.onclick = () => {
|
| 1104 |
+
// Do not change selection, just close dialog
|
| 1105 |
+
logStatus('CSV key file selection cancelled.');
|
| 1106 |
+
document.body.removeChild(overlay);
|
| 1107 |
+
};
|
| 1108 |
+
dialog.appendChild(cancelBtn);
|
| 1109 |
+
|
| 1110 |
+
overlay.appendChild(dialog);
|
| 1111 |
+
document.body.appendChild(overlay);
|
| 1112 |
+
}
|
| 1113 |
+
|
| 1114 |
+
// Custom dialog for single CSV file found
|
| 1115 |
+
function showSingleCsvDialog(csvFile) {
|
| 1116 |
+
const overlay = document.createElement('div');
|
| 1117 |
+
overlay.style.position = 'fixed';
|
| 1118 |
+
overlay.style.top = 0;
|
| 1119 |
+
overlay.style.left = 0;
|
| 1120 |
+
overlay.style.width = '100vw';
|
| 1121 |
+
overlay.style.height = '100vh';
|
| 1122 |
+
overlay.style.background = 'rgba(0,0,0,0.4)';
|
| 1123 |
+
overlay.style.zIndex = 9999;
|
| 1124 |
+
overlay.style.display = 'flex';
|
| 1125 |
+
overlay.style.alignItems = 'center';
|
| 1126 |
+
overlay.style.justifyContent = 'center';
|
| 1127 |
+
|
| 1128 |
+
const dialog = document.createElement('div');
|
| 1129 |
+
dialog.style.background = '#fff';
|
| 1130 |
+
dialog.style.padding = '24px 32px';
|
| 1131 |
+
dialog.style.borderRadius = '8px';
|
| 1132 |
+
dialog.style.boxShadow = '0 2px 16px rgba(0,0,0,0.2)';
|
| 1133 |
+
dialog.style.minWidth = '320px';
|
| 1134 |
+
dialog.innerHTML = `<h3>CSV File Found</h3><p>A CSV file named <strong>${csvFile.name}</strong> was found in the folder. Would you like to use this as the key file for this folder?</p>`;
|
| 1135 |
+
|
| 1136 |
+
// Yes button
|
| 1137 |
+
const yesBtn = document.createElement('button');
|
| 1138 |
+
yesBtn.textContent = 'Yes';
|
| 1139 |
+
yesBtn.style.display = 'block';
|
| 1140 |
+
yesBtn.style.margin = '8px 0';
|
| 1141 |
+
yesBtn.onclick = () => {
|
| 1142 |
+
window.selectedKeyenceCsv = csvFile;
|
| 1143 |
+
logStatus(`CSV key file selected: ${csvFile.name}`);
|
| 1144 |
+
document.body.removeChild(overlay);
|
| 1145 |
+
};
|
| 1146 |
+
dialog.appendChild(yesBtn);
|
| 1147 |
+
|
| 1148 |
+
// Use a different .csv file
|
| 1149 |
+
const otherBtn = document.createElement('button');
|
| 1150 |
+
otherBtn.textContent = 'Use a different .csv file';
|
| 1151 |
+
otherBtn.style.display = 'block';
|
| 1152 |
+
otherBtn.style.margin = '8px 0';
|
| 1153 |
+
otherBtn.onclick = () => {
|
| 1154 |
+
let csvInput = document.createElement('input');
|
| 1155 |
+
csvInput.type = 'file';
|
| 1156 |
+
csvInput.accept = '.csv,.CSV';
|
| 1157 |
+
csvInput.style.display = 'none';
|
| 1158 |
+
document.body.appendChild(csvInput);
|
| 1159 |
+
csvInput.addEventListener('change', function() {
|
| 1160 |
+
if (csvInput.files && csvInput.files.length === 1) {
|
| 1161 |
+
window.selectedKeyenceCsv = csvInput.files[0];
|
| 1162 |
+
logStatus(`CSV key file supplied: ${csvInput.files[0].name}`);
|
| 1163 |
+
} else {
|
| 1164 |
+
window.selectedKeyenceCsv = null;
|
| 1165 |
+
logStatus('No CSV key file supplied.');
|
| 1166 |
+
}
|
| 1167 |
+
document.body.removeChild(csvInput);
|
| 1168 |
+
document.body.removeChild(overlay);
|
| 1169 |
+
});
|
| 1170 |
+
csvInput.click();
|
| 1171 |
+
};
|
| 1172 |
+
dialog.appendChild(otherBtn);
|
| 1173 |
+
|
| 1174 |
+
// Do not use a key file
|
| 1175 |
+
const noneBtn = document.createElement('button');
|
| 1176 |
+
noneBtn.textContent = 'Do not use a key file';
|
| 1177 |
+
noneBtn.style.display = 'block';
|
| 1178 |
+
noneBtn.style.margin = '8px 0';
|
| 1179 |
+
noneBtn.onclick = () => {
|
| 1180 |
+
window.selectedKeyenceCsv = null;
|
| 1181 |
+
logStatus('No CSV key file will be used.');
|
| 1182 |
+
document.body.removeChild(overlay);
|
| 1183 |
+
};
|
| 1184 |
+
dialog.appendChild(noneBtn);
|
| 1185 |
+
|
| 1186 |
+
// Cancel button
|
| 1187 |
+
const cancelBtn = document.createElement('button');
|
| 1188 |
+
cancelBtn.textContent = 'Cancel';
|
| 1189 |
+
cancelBtn.style.display = 'block';
|
| 1190 |
+
cancelBtn.style.margin = '8px 0';
|
| 1191 |
+
cancelBtn.onclick = () => {
|
| 1192 |
+
// Do not change selection, just close dialog
|
| 1193 |
+
logStatus('CSV key file selection cancelled.');
|
| 1194 |
+
document.body.removeChild(overlay);
|
| 1195 |
+
};
|
| 1196 |
+
dialog.appendChild(cancelBtn);
|
| 1197 |
+
|
| 1198 |
+
overlay.appendChild(dialog);
|
| 1199 |
+
document.body.appendChild(overlay);
|
| 1200 |
+
}
|
| 1201 |
+
|
| 1202 |
// Initialize
|
| 1203 |
updateUploadState();
|
| 1204 |
logStatus('Application ready');
|
| 1205 |
+
});
|
templates/index.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
| 12 |
<i class="ri-microscope-line"></i>
|
| 13 |
NemaQuant
|
| 14 |
<small style="display: block; font-size: 1rem; font-weight: normal; color: var(--text-muted);">
|
| 15 |
-
Automated
|
| 16 |
</small>
|
| 17 |
</h1>
|
| 18 |
|
|
@@ -26,6 +26,7 @@
|
|
| 26 |
<select id="input-mode" name="input-mode" class="form-control">
|
| 27 |
<option value="files">Select Image(s)</option>
|
| 28 |
<option value="folder">Select Folder</option>
|
|
|
|
| 29 |
</select>
|
| 30 |
<small class="input-help" style="color: var(--text-muted); margin-top: 0.25rem; display: block;">
|
| 31 |
<span id="input-mode-help">Choose one or more image files for processing</span>
|
|
@@ -50,11 +51,11 @@
|
|
| 50 |
<div class="form-group">
|
| 51 |
<label for="confidence-threshold">
|
| 52 |
Confidence Threshold
|
| 53 |
-
<i class="ri-information-line" data-tooltip="Recommended range: 0.
|
| 54 |
</label>
|
| 55 |
<div class="range-with-value">
|
| 56 |
<input type="range" id="confidence-threshold" name="confidence-threshold"
|
| 57 |
-
min="0" max="
|
| 58 |
<span id="confidence-value">0.6</span>
|
| 59 |
</div>
|
| 60 |
</div>
|
|
|
|
| 12 |
<i class="ri-microscope-line"></i>
|
| 13 |
NemaQuant
|
| 14 |
<small style="display: block; font-size: 1rem; font-weight: normal; color: var(--text-muted);">
|
| 15 |
+
Automated nematode egg detection and counting
|
| 16 |
</small>
|
| 17 |
</h1>
|
| 18 |
|
|
|
|
| 26 |
<select id="input-mode" name="input-mode" class="form-control">
|
| 27 |
<option value="files">Select Image(s)</option>
|
| 28 |
<option value="folder">Select Folder</option>
|
| 29 |
+
<option value="keyence">Keyence Output Folder</option>
|
| 30 |
</select>
|
| 31 |
<small class="input-help" style="color: var(--text-muted); margin-top: 0.25rem; display: block;">
|
| 32 |
<span id="input-mode-help">Choose one or more image files for processing</span>
|
|
|
|
| 51 |
<div class="form-group">
|
| 52 |
<label for="confidence-threshold">
|
| 53 |
Confidence Threshold
|
| 54 |
+
<i class="ri-information-line" data-tooltip="Recommended range: 0.4 - 0.7. Higher values produce fewer false positives but more false negatives, while lower values produce fewer false negatives but more false positives."></i>
|
| 55 |
</label>
|
| 56 |
<div class="range-with-value">
|
| 57 |
<input type="range" id="confidence-threshold" name="confidence-threshold"
|
| 58 |
+
min="0.05" max="0.95" step="0.05" value="0.6">
|
| 59 |
<span id="confidence-value">0.6</span>
|
| 60 |
</div>
|
| 61 |
</div>
|