update ui and code
|
|
@ -1,5 +1,5 @@
|
||||||
PUBLIC_URL=
|
PUBLIC_URL=
|
||||||
REACT_APP_API_URL=
|
REACT_APP_API_URL="/api"
|
||||||
|
|
||||||
REACT_APP_DEFAULTAUTH=fake
|
REACT_APP_DEFAULTAUTH=fake
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,3 +24,8 @@ yarn-error.log*
|
||||||
|
|
||||||
/src/server/label
|
/src/server/label
|
||||||
/src/server/images
|
/src/server/images
|
||||||
|
/src/server/trained_models
|
||||||
|
/src/server/model_datasets
|
||||||
|
*.log
|
||||||
|
src/server/venv/*
|
||||||
|
__pycache__/
|
||||||
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
|
@ -4,12 +4,18 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"-": "0.0.1",
|
"-": "0.0.1",
|
||||||
|
"@mantine/core": "^7.17.1",
|
||||||
|
"@mantine/dropzone": "^7.17.1",
|
||||||
|
"@mantine/hooks": "^7.17.1",
|
||||||
|
"@mantine/notifications": "^7.17.1",
|
||||||
|
"@tabler/icons-react": "^3.30.0",
|
||||||
"@testing-library/jest-dom": "^5.17.0",
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"assert": "^2.1.0",
|
"assert": "^2.1.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"child_process": "^1.0.2",
|
"child_process": "^1.0.2",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
"fs": "0.0.1-security",
|
"fs": "0.0.1-security",
|
||||||
"numpy": "0.0.1",
|
"numpy": "0.0.1",
|
||||||
"opencv-build": "^0.1.9",
|
"opencv-build": "^0.1.9",
|
||||||
|
|
@ -24,7 +30,8 @@
|
||||||
"web-vitals": "^2.1.4",
|
"web-vitals": "^2.1.4",
|
||||||
"webpack": "^5.96.1",
|
"webpack": "^5.96.1",
|
||||||
"webpack-cli": "^5.1.4",
|
"webpack-cli": "^5.1.4",
|
||||||
"webpack-dev-server": "^5.1.0"
|
"webpack-dev-server": "^5.1.0",
|
||||||
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
|
|
@ -52,5 +59,10 @@
|
||||||
},
|
},
|
||||||
"opencv4nodejs": {
|
"opencv4nodejs": {
|
||||||
"disableAutoBuild": "1"
|
"disableAutoBuild": "1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
|
"postcss-simple-vars": "^7.0.1",
|
||||||
|
"tailwindcss": "^3.4.17"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
'postcss-preset-mantine': {},
|
||||||
|
'postcss-simple-vars': {
|
||||||
|
variables: {
|
||||||
|
'mantine-breakpoint-xs': '36em',
|
||||||
|
'mantine-breakpoint-sm': '48em',
|
||||||
|
'mantine-breakpoint-md': '62em',
|
||||||
|
'mantine-breakpoint-lg': '75em',
|
||||||
|
'mantine-breakpoint-xl': '88em',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
|
|
@ -0,0 +1,106 @@
|
||||||
|
task: detect
|
||||||
|
mode: train
|
||||||
|
model: yolov8n.pt
|
||||||
|
data: model_datasets/2025-03-05/data.yaml
|
||||||
|
epochs: 50
|
||||||
|
time: null
|
||||||
|
patience: 10
|
||||||
|
batch: 16
|
||||||
|
imgsz: 640
|
||||||
|
save: true
|
||||||
|
save_period: -1
|
||||||
|
cache: false
|
||||||
|
device: null
|
||||||
|
workers: 8
|
||||||
|
project: null
|
||||||
|
name: train
|
||||||
|
exist_ok: false
|
||||||
|
pretrained: true
|
||||||
|
optimizer: AdamW
|
||||||
|
verbose: true
|
||||||
|
seed: 0
|
||||||
|
deterministic: true
|
||||||
|
single_cls: false
|
||||||
|
rect: false
|
||||||
|
cos_lr: false
|
||||||
|
close_mosaic: 10
|
||||||
|
resume: false
|
||||||
|
amp: true
|
||||||
|
fraction: 1.0
|
||||||
|
profile: false
|
||||||
|
freeze: null
|
||||||
|
multi_scale: false
|
||||||
|
overlap_mask: true
|
||||||
|
mask_ratio: 4
|
||||||
|
dropout: 0.0
|
||||||
|
val: true
|
||||||
|
split: val
|
||||||
|
save_json: false
|
||||||
|
save_hybrid: false
|
||||||
|
conf: null
|
||||||
|
iou: 0.7
|
||||||
|
max_det: 300
|
||||||
|
half: false
|
||||||
|
dnn: false
|
||||||
|
plots: true
|
||||||
|
source: null
|
||||||
|
vid_stride: 1
|
||||||
|
stream_buffer: false
|
||||||
|
visualize: false
|
||||||
|
augment: false
|
||||||
|
agnostic_nms: false
|
||||||
|
classes: null
|
||||||
|
retina_masks: false
|
||||||
|
embed: null
|
||||||
|
show: false
|
||||||
|
save_frames: false
|
||||||
|
save_txt: false
|
||||||
|
save_conf: false
|
||||||
|
save_crop: false
|
||||||
|
show_labels: true
|
||||||
|
show_conf: true
|
||||||
|
show_boxes: true
|
||||||
|
line_width: null
|
||||||
|
format: torchscript
|
||||||
|
keras: false
|
||||||
|
optimize: false
|
||||||
|
int8: false
|
||||||
|
dynamic: false
|
||||||
|
simplify: true
|
||||||
|
opset: null
|
||||||
|
workspace: null
|
||||||
|
nms: false
|
||||||
|
lr0: 0.001
|
||||||
|
lrf: 0.01
|
||||||
|
momentum: 0.937
|
||||||
|
weight_decay: 0.0005
|
||||||
|
warmup_epochs: 3.0
|
||||||
|
warmup_momentum: 0.8
|
||||||
|
warmup_bias_lr: 0.1
|
||||||
|
box: 7.5
|
||||||
|
cls: 0.5
|
||||||
|
dfl: 1.5
|
||||||
|
pose: 12.0
|
||||||
|
kobj: 1.0
|
||||||
|
nbs: 64
|
||||||
|
hsv_h: 0.015
|
||||||
|
hsv_s: 0.7
|
||||||
|
hsv_v: 0.4
|
||||||
|
degrees: 0.0
|
||||||
|
translate: 0.1
|
||||||
|
scale: 0.5
|
||||||
|
shear: 0.0
|
||||||
|
perspective: 0.0
|
||||||
|
flipud: 0.0
|
||||||
|
fliplr: 0.5
|
||||||
|
bgr: 0.0
|
||||||
|
mosaic: 1.0
|
||||||
|
mixup: 0.0
|
||||||
|
copy_paste: 0.0
|
||||||
|
copy_paste_mode: flip
|
||||||
|
auto_augment: randaugment
|
||||||
|
erasing: 0.4
|
||||||
|
crop_fraction: 1.0
|
||||||
|
cfg: null
|
||||||
|
tracker: botsort.yaml
|
||||||
|
save_dir: /home/work/projects/YoLo/runs/detect/train
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
task: detect
|
||||||
|
mode: train
|
||||||
|
model: yolov8n.pt
|
||||||
|
data: model_datasets/2025-03-05/data.yaml
|
||||||
|
epochs: 50
|
||||||
|
time: null
|
||||||
|
patience: 10
|
||||||
|
batch: 16
|
||||||
|
imgsz: 640
|
||||||
|
save: true
|
||||||
|
save_period: -1
|
||||||
|
cache: false
|
||||||
|
device: null
|
||||||
|
workers: 8
|
||||||
|
project: null
|
||||||
|
name: train2
|
||||||
|
exist_ok: false
|
||||||
|
pretrained: true
|
||||||
|
optimizer: AdamW
|
||||||
|
verbose: true
|
||||||
|
seed: 0
|
||||||
|
deterministic: true
|
||||||
|
single_cls: false
|
||||||
|
rect: false
|
||||||
|
cos_lr: false
|
||||||
|
close_mosaic: 10
|
||||||
|
resume: false
|
||||||
|
amp: true
|
||||||
|
fraction: 1.0
|
||||||
|
profile: false
|
||||||
|
freeze: null
|
||||||
|
multi_scale: false
|
||||||
|
overlap_mask: true
|
||||||
|
mask_ratio: 4
|
||||||
|
dropout: 0.0
|
||||||
|
val: true
|
||||||
|
split: val
|
||||||
|
save_json: false
|
||||||
|
save_hybrid: false
|
||||||
|
conf: null
|
||||||
|
iou: 0.7
|
||||||
|
max_det: 300
|
||||||
|
half: false
|
||||||
|
dnn: false
|
||||||
|
plots: true
|
||||||
|
source: null
|
||||||
|
vid_stride: 1
|
||||||
|
stream_buffer: false
|
||||||
|
visualize: false
|
||||||
|
augment: false
|
||||||
|
agnostic_nms: false
|
||||||
|
classes: null
|
||||||
|
retina_masks: false
|
||||||
|
embed: null
|
||||||
|
show: false
|
||||||
|
save_frames: false
|
||||||
|
save_txt: false
|
||||||
|
save_conf: false
|
||||||
|
save_crop: false
|
||||||
|
show_labels: true
|
||||||
|
show_conf: true
|
||||||
|
show_boxes: true
|
||||||
|
line_width: null
|
||||||
|
format: torchscript
|
||||||
|
keras: false
|
||||||
|
optimize: false
|
||||||
|
int8: false
|
||||||
|
dynamic: false
|
||||||
|
simplify: true
|
||||||
|
opset: null
|
||||||
|
workspace: null
|
||||||
|
nms: false
|
||||||
|
lr0: 0.001
|
||||||
|
lrf: 0.01
|
||||||
|
momentum: 0.937
|
||||||
|
weight_decay: 0.0005
|
||||||
|
warmup_epochs: 3.0
|
||||||
|
warmup_momentum: 0.8
|
||||||
|
warmup_bias_lr: 0.1
|
||||||
|
box: 7.5
|
||||||
|
cls: 0.5
|
||||||
|
dfl: 1.5
|
||||||
|
pose: 12.0
|
||||||
|
kobj: 1.0
|
||||||
|
nbs: 64
|
||||||
|
hsv_h: 0.015
|
||||||
|
hsv_s: 0.7
|
||||||
|
hsv_v: 0.4
|
||||||
|
degrees: 0.0
|
||||||
|
translate: 0.1
|
||||||
|
scale: 0.5
|
||||||
|
shear: 0.0
|
||||||
|
perspective: 0.0
|
||||||
|
flipud: 0.0
|
||||||
|
fliplr: 0.5
|
||||||
|
bgr: 0.0
|
||||||
|
mosaic: 1.0
|
||||||
|
mixup: 0.0
|
||||||
|
copy_paste: 0.0
|
||||||
|
copy_paste_mode: flip
|
||||||
|
auto_augment: randaugment
|
||||||
|
erasing: 0.4
|
||||||
|
crop_fraction: 1.0
|
||||||
|
cfg: null
|
||||||
|
tracker: botsort.yaml
|
||||||
|
save_dir: /home/work/projects/YoLo/runs/detect/train2
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
task: detect
|
||||||
|
mode: train
|
||||||
|
model: yolov8n.pt
|
||||||
|
data: model_datasets/2025-03-05/data.yaml
|
||||||
|
epochs: 50
|
||||||
|
time: null
|
||||||
|
patience: 10
|
||||||
|
batch: 16
|
||||||
|
imgsz: 640
|
||||||
|
save: true
|
||||||
|
save_period: -1
|
||||||
|
cache: false
|
||||||
|
device: null
|
||||||
|
workers: 8
|
||||||
|
project: null
|
||||||
|
name: train3
|
||||||
|
exist_ok: false
|
||||||
|
pretrained: true
|
||||||
|
optimizer: AdamW
|
||||||
|
verbose: true
|
||||||
|
seed: 0
|
||||||
|
deterministic: true
|
||||||
|
single_cls: false
|
||||||
|
rect: false
|
||||||
|
cos_lr: false
|
||||||
|
close_mosaic: 10
|
||||||
|
resume: false
|
||||||
|
amp: true
|
||||||
|
fraction: 1.0
|
||||||
|
profile: false
|
||||||
|
freeze: null
|
||||||
|
multi_scale: false
|
||||||
|
overlap_mask: true
|
||||||
|
mask_ratio: 4
|
||||||
|
dropout: 0.0
|
||||||
|
val: true
|
||||||
|
split: val
|
||||||
|
save_json: false
|
||||||
|
save_hybrid: false
|
||||||
|
conf: null
|
||||||
|
iou: 0.7
|
||||||
|
max_det: 300
|
||||||
|
half: false
|
||||||
|
dnn: false
|
||||||
|
plots: true
|
||||||
|
source: null
|
||||||
|
vid_stride: 1
|
||||||
|
stream_buffer: false
|
||||||
|
visualize: false
|
||||||
|
augment: false
|
||||||
|
agnostic_nms: false
|
||||||
|
classes: null
|
||||||
|
retina_masks: false
|
||||||
|
embed: null
|
||||||
|
show: false
|
||||||
|
save_frames: false
|
||||||
|
save_txt: false
|
||||||
|
save_conf: false
|
||||||
|
save_crop: false
|
||||||
|
show_labels: true
|
||||||
|
show_conf: true
|
||||||
|
show_boxes: true
|
||||||
|
line_width: null
|
||||||
|
format: torchscript
|
||||||
|
keras: false
|
||||||
|
optimize: false
|
||||||
|
int8: false
|
||||||
|
dynamic: false
|
||||||
|
simplify: true
|
||||||
|
opset: null
|
||||||
|
workspace: null
|
||||||
|
nms: false
|
||||||
|
lr0: 0.001
|
||||||
|
lrf: 0.01
|
||||||
|
momentum: 0.937
|
||||||
|
weight_decay: 0.0005
|
||||||
|
warmup_epochs: 3.0
|
||||||
|
warmup_momentum: 0.8
|
||||||
|
warmup_bias_lr: 0.1
|
||||||
|
box: 7.5
|
||||||
|
cls: 0.5
|
||||||
|
dfl: 1.5
|
||||||
|
pose: 12.0
|
||||||
|
kobj: 1.0
|
||||||
|
nbs: 64
|
||||||
|
hsv_h: 0.015
|
||||||
|
hsv_s: 0.7
|
||||||
|
hsv_v: 0.4
|
||||||
|
degrees: 0.0
|
||||||
|
translate: 0.1
|
||||||
|
scale: 0.5
|
||||||
|
shear: 0.0
|
||||||
|
perspective: 0.0
|
||||||
|
flipud: 0.0
|
||||||
|
fliplr: 0.5
|
||||||
|
bgr: 0.0
|
||||||
|
mosaic: 1.0
|
||||||
|
mixup: 0.0
|
||||||
|
copy_paste: 0.0
|
||||||
|
copy_paste_mode: flip
|
||||||
|
auto_augment: randaugment
|
||||||
|
erasing: 0.4
|
||||||
|
crop_fraction: 1.0
|
||||||
|
cfg: null
|
||||||
|
tracker: botsort.yaml
|
||||||
|
save_dir: /home/work/projects/YoLo/runs/detect/train3
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
epoch,time,train/box_loss,train/cls_loss,train/dfl_loss,metrics/precision(B),metrics/recall(B),metrics/mAP50(B),metrics/mAP50-95(B),val/box_loss,val/cls_loss,val/dfl_loss,lr/pg0,lr/pg1,lr/pg2
|
||||||
|
1,60.4197,0,100.785,0,0,0,0,0,0,35.8662,0,0.09901,1e-05,1e-05
|
||||||
|
2,119.599,0,62.3699,0,0,0,0,0,0,22.7946,0,0.0970294,2.9406e-05,2.9406e-05
|
||||||
|
|
After Width: | Height: | Size: 382 KiB |
|
After Width: | Height: | Size: 304 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
|
@ -0,0 +1,106 @@
|
||||||
|
task: detect
|
||||||
|
mode: train
|
||||||
|
model: yolov8n.pt
|
||||||
|
data: model_datasets/2025-03-05/data.yaml
|
||||||
|
epochs: 2
|
||||||
|
time: null
|
||||||
|
patience: 10
|
||||||
|
batch: 16
|
||||||
|
imgsz: 640
|
||||||
|
save: true
|
||||||
|
save_period: -1
|
||||||
|
cache: false
|
||||||
|
device: null
|
||||||
|
workers: 8
|
||||||
|
project: null
|
||||||
|
name: train4
|
||||||
|
exist_ok: false
|
||||||
|
pretrained: true
|
||||||
|
optimizer: AdamW
|
||||||
|
verbose: true
|
||||||
|
seed: 0
|
||||||
|
deterministic: true
|
||||||
|
single_cls: false
|
||||||
|
rect: false
|
||||||
|
cos_lr: false
|
||||||
|
close_mosaic: 10
|
||||||
|
resume: false
|
||||||
|
amp: true
|
||||||
|
fraction: 1.0
|
||||||
|
profile: false
|
||||||
|
freeze: null
|
||||||
|
multi_scale: false
|
||||||
|
overlap_mask: true
|
||||||
|
mask_ratio: 4
|
||||||
|
dropout: 0.0
|
||||||
|
val: true
|
||||||
|
split: val
|
||||||
|
save_json: false
|
||||||
|
save_hybrid: false
|
||||||
|
conf: null
|
||||||
|
iou: 0.7
|
||||||
|
max_det: 300
|
||||||
|
half: false
|
||||||
|
dnn: false
|
||||||
|
plots: true
|
||||||
|
source: null
|
||||||
|
vid_stride: 1
|
||||||
|
stream_buffer: false
|
||||||
|
visualize: false
|
||||||
|
augment: false
|
||||||
|
agnostic_nms: false
|
||||||
|
classes: null
|
||||||
|
retina_masks: false
|
||||||
|
embed: null
|
||||||
|
show: false
|
||||||
|
save_frames: false
|
||||||
|
save_txt: false
|
||||||
|
save_conf: false
|
||||||
|
save_crop: false
|
||||||
|
show_labels: true
|
||||||
|
show_conf: true
|
||||||
|
show_boxes: true
|
||||||
|
line_width: null
|
||||||
|
format: torchscript
|
||||||
|
keras: false
|
||||||
|
optimize: false
|
||||||
|
int8: false
|
||||||
|
dynamic: false
|
||||||
|
simplify: true
|
||||||
|
opset: null
|
||||||
|
workspace: null
|
||||||
|
nms: false
|
||||||
|
lr0: 0.001
|
||||||
|
lrf: 0.01
|
||||||
|
momentum: 0.937
|
||||||
|
weight_decay: 0.0005
|
||||||
|
warmup_epochs: 3.0
|
||||||
|
warmup_momentum: 0.8
|
||||||
|
warmup_bias_lr: 0.1
|
||||||
|
box: 7.5
|
||||||
|
cls: 0.5
|
||||||
|
dfl: 1.5
|
||||||
|
pose: 12.0
|
||||||
|
kobj: 1.0
|
||||||
|
nbs: 64
|
||||||
|
hsv_h: 0.015
|
||||||
|
hsv_s: 0.7
|
||||||
|
hsv_v: 0.4
|
||||||
|
degrees: 0.0
|
||||||
|
translate: 0.1
|
||||||
|
scale: 0.5
|
||||||
|
shear: 0.0
|
||||||
|
perspective: 0.0
|
||||||
|
flipud: 0.0
|
||||||
|
fliplr: 0.5
|
||||||
|
bgr: 0.0
|
||||||
|
mosaic: 1.0
|
||||||
|
mixup: 0.0
|
||||||
|
copy_paste: 0.0
|
||||||
|
copy_paste_mode: flip
|
||||||
|
auto_augment: randaugment
|
||||||
|
erasing: 0.4
|
||||||
|
crop_fraction: 1.0
|
||||||
|
cfg: null
|
||||||
|
tracker: botsort.yaml
|
||||||
|
save_dir: /home/work/projects/YoLo/runs/detect/train4
|
||||||
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
|
@ -0,0 +1,106 @@
|
||||||
|
task: detect
|
||||||
|
mode: train
|
||||||
|
model: yolov8n.pt
|
||||||
|
data: model_datasets/2025-03-05/data.yaml
|
||||||
|
epochs: 2
|
||||||
|
time: null
|
||||||
|
patience: 10
|
||||||
|
batch: 16
|
||||||
|
imgsz: 640
|
||||||
|
save: true
|
||||||
|
save_period: -1
|
||||||
|
cache: false
|
||||||
|
device: null
|
||||||
|
workers: 8
|
||||||
|
project: null
|
||||||
|
name: train5
|
||||||
|
exist_ok: false
|
||||||
|
pretrained: true
|
||||||
|
optimizer: AdamW
|
||||||
|
verbose: true
|
||||||
|
seed: 0
|
||||||
|
deterministic: true
|
||||||
|
single_cls: false
|
||||||
|
rect: false
|
||||||
|
cos_lr: false
|
||||||
|
close_mosaic: 10
|
||||||
|
resume: false
|
||||||
|
amp: true
|
||||||
|
fraction: 1.0
|
||||||
|
profile: false
|
||||||
|
freeze: null
|
||||||
|
multi_scale: false
|
||||||
|
overlap_mask: true
|
||||||
|
mask_ratio: 4
|
||||||
|
dropout: 0.0
|
||||||
|
val: true
|
||||||
|
split: val
|
||||||
|
save_json: false
|
||||||
|
save_hybrid: false
|
||||||
|
conf: null
|
||||||
|
iou: 0.7
|
||||||
|
max_det: 300
|
||||||
|
half: false
|
||||||
|
dnn: false
|
||||||
|
plots: true
|
||||||
|
source: null
|
||||||
|
vid_stride: 1
|
||||||
|
stream_buffer: false
|
||||||
|
visualize: false
|
||||||
|
augment: false
|
||||||
|
agnostic_nms: false
|
||||||
|
classes: null
|
||||||
|
retina_masks: false
|
||||||
|
embed: null
|
||||||
|
show: false
|
||||||
|
save_frames: false
|
||||||
|
save_txt: false
|
||||||
|
save_conf: false
|
||||||
|
save_crop: false
|
||||||
|
show_labels: true
|
||||||
|
show_conf: true
|
||||||
|
show_boxes: true
|
||||||
|
line_width: null
|
||||||
|
format: torchscript
|
||||||
|
keras: false
|
||||||
|
optimize: false
|
||||||
|
int8: false
|
||||||
|
dynamic: false
|
||||||
|
simplify: true
|
||||||
|
opset: null
|
||||||
|
workspace: null
|
||||||
|
nms: false
|
||||||
|
lr0: 0.001
|
||||||
|
lrf: 0.01
|
||||||
|
momentum: 0.937
|
||||||
|
weight_decay: 0.0005
|
||||||
|
warmup_epochs: 3.0
|
||||||
|
warmup_momentum: 0.8
|
||||||
|
warmup_bias_lr: 0.1
|
||||||
|
box: 7.5
|
||||||
|
cls: 0.5
|
||||||
|
dfl: 1.5
|
||||||
|
pose: 12.0
|
||||||
|
kobj: 1.0
|
||||||
|
nbs: 64
|
||||||
|
hsv_h: 0.015
|
||||||
|
hsv_s: 0.7
|
||||||
|
hsv_v: 0.4
|
||||||
|
degrees: 0.0
|
||||||
|
translate: 0.1
|
||||||
|
scale: 0.5
|
||||||
|
shear: 0.0
|
||||||
|
perspective: 0.0
|
||||||
|
flipud: 0.0
|
||||||
|
fliplr: 0.5
|
||||||
|
bgr: 0.0
|
||||||
|
mosaic: 1.0
|
||||||
|
mixup: 0.0
|
||||||
|
copy_paste: 0.0
|
||||||
|
copy_paste_mode: flip
|
||||||
|
auto_augment: randaugment
|
||||||
|
erasing: 0.4
|
||||||
|
crop_fraction: 1.0
|
||||||
|
cfg: null
|
||||||
|
tracker: botsort.yaml
|
||||||
|
save_dir: /home/work/projects/YoLo/runs/detect/train5
|
||||||
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
epoch,time,train/box_loss,train/cls_loss,train/dfl_loss,metrics/precision(B),metrics/recall(B),metrics/mAP50(B),metrics/mAP50-95(B),val/box_loss,val/cls_loss,val/dfl_loss,lr/pg0,lr/pg1,lr/pg2
|
||||||
|
1,13.2159,2.49424,4.97029,2.25232,0.0014,0.25,0.00121,0.00012,1.88201,4.79522,2.07429,0.1,0,0
|
||||||
|
2,25.7522,2.40923,4.29302,2.0057,0.00145,0.25,0.00124,0.00025,1.98576,4.55288,2.05854,0.0990051,5.05e-06,5.05e-06
|
||||||
|
|
After Width: | Height: | Size: 269 KiB |
|
After Width: | Height: | Size: 244 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
|
@ -1,13 +1,320 @@
|
||||||
import logo from "./logo.svg";
|
import '@mantine/core/styles.css';
|
||||||
import "./App.css";
|
|
||||||
import Main from "./pages/index";
|
import './App.css';
|
||||||
|
import { Dropzone } from './pages/components/Dropzone';
|
||||||
|
|
||||||
|
import { ActionIcon, AppShell, Box, Burger, Button, Group, LoadingOverlay, Progress, ScrollArea, Text, Tooltip } from '@mantine/core';
|
||||||
|
import { useDisclosure, useHotkeys } from '@mantine/hooks';
|
||||||
|
import { notifications } from '@mantine/notifications';
|
||||||
|
import { IconChevronLeft, IconChevronRight, IconImageInPicture, IconRefreshDot, IconTrash } from '@tabler/icons-react';
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { ImageDetect } from './pages/components/ImageDetect';
|
||||||
|
import ImageLabel from './pages/components/ImageLabel';
|
||||||
|
import { useImagesDetected } from './stores/use-images-detected';
|
||||||
|
import { SaveButton } from './pages/components/SaveButton';
|
||||||
|
import { generateClientID } from './ultils';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();
|
||||||
|
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
||||||
|
|
||||||
|
const [clickData, setClickData] = useState(null);
|
||||||
|
|
||||||
|
const { images, clearImagesDetected, appendImageDetect, setImageDetected } = useImagesDetected();
|
||||||
|
|
||||||
|
const [progress, setProgress] = useState({ total: 0, processFiles: 0 });
|
||||||
|
|
||||||
|
const imageLabelRef = useRef();
|
||||||
|
const dropzoneRef = useRef();
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
// const result = window.confirm('Are you want to clear ?');
|
||||||
|
|
||||||
|
const ID_NOTI = 'handle-clear';
|
||||||
|
notifications.show({
|
||||||
|
id: ID_NOTI,
|
||||||
|
title: 'Are you want to clear ?',
|
||||||
|
message: (
|
||||||
|
<Box className="flex items-center gap-2 justify-end">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setClickData(null);
|
||||||
|
clearImagesDetected();
|
||||||
|
notifications.hide(ID_NOTI);
|
||||||
|
}}
|
||||||
|
color="red"
|
||||||
|
size="xs">
|
||||||
|
Ok
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => notifications.hide(ID_NOTI)} size="xs">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteImage = (data, index) => {
|
||||||
|
const ID_NOTI = 'handle-delete-image';
|
||||||
|
|
||||||
|
const deleteImage = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${process.env.REACT_APP_API_URL}/delete/${data.image_name}`, { method: 'DELETE' });
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
setClickData((prevClick) => (prevClick?.image_name === data.image_name ? null : prevClick));
|
||||||
|
|
||||||
|
const newImages = images.filter((image) => image.image_name !== data.image_name);
|
||||||
|
appendImageDetect(newImages);
|
||||||
|
|
||||||
|
notifications.hide(ID_NOTI);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
notifications.show({
|
||||||
|
id: ID_NOTI,
|
||||||
|
title: 'Are you want to delete ?',
|
||||||
|
message: (
|
||||||
|
<Box className="flex items-center gap-2 justify-end">
|
||||||
|
<Button onClick={deleteImage} color="red" size="xs">
|
||||||
|
Ok
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => notifications.hide(ID_NOTI)} size="xs">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddImages = () => {
|
||||||
|
// setClickData(null);
|
||||||
|
|
||||||
|
if (dropzoneRef?.current) {
|
||||||
|
dropzoneRef.current();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearSelect = () => {
|
||||||
|
setClickData(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
if (!images.length) return;
|
||||||
|
|
||||||
|
if (!clickData) {
|
||||||
|
setClickData(images[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const findItem = images.find((item) => item?.image_name === clickData?.image_name);
|
||||||
|
|
||||||
|
if (!findItem) return;
|
||||||
|
|
||||||
|
const index = images.indexOf(findItem);
|
||||||
|
|
||||||
|
const nextIndex = index + 1;
|
||||||
|
|
||||||
|
if (index === -1 || nextIndex >= images.length) return;
|
||||||
|
|
||||||
|
console.log(images[nextIndex]);
|
||||||
|
setClickData(images[nextIndex]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpload = async (files) => {
|
||||||
|
if (!files) return;
|
||||||
|
|
||||||
|
setProgress({
|
||||||
|
total: files.length,
|
||||||
|
processFiles: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const worker = new Worker(new URL('./workers/detect-worker.js', import.meta.url));
|
||||||
|
|
||||||
|
worker.onmessage = (event) => {
|
||||||
|
if (event.data.status === 'success') {
|
||||||
|
if (event.data?.result) {
|
||||||
|
setImageDetected(event.data.result);
|
||||||
|
setProgress({
|
||||||
|
total: event.data.total,
|
||||||
|
processFiles: event.data.processFiles,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (event.data.total === event.data.processFiles) {
|
||||||
|
setProgress({
|
||||||
|
total: 0,
|
||||||
|
processFiles: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('⚠️ Lỗi:', event.data.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.postMessage({ action: 'processImages', files });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrev = () => {
|
||||||
|
if (!images.length) return;
|
||||||
|
const findItem = images.find((item) => item?.image_name === clickData?.image_name);
|
||||||
|
|
||||||
|
if (!findItem) return;
|
||||||
|
|
||||||
|
const index = images.indexOf(findItem);
|
||||||
|
const prevIndex = index - 1;
|
||||||
|
|
||||||
|
if (index <= 0) return;
|
||||||
|
|
||||||
|
setClickData(images[prevIndex]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showNext = useMemo(() => {
|
||||||
|
if (!images.length) return false;
|
||||||
|
|
||||||
|
if (clickData?.image_name === images[images.length - 1]?.image_name) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, [images, clickData]);
|
||||||
|
|
||||||
|
const showPrev = useMemo(() => {
|
||||||
|
if (!images.length || !clickData) return false;
|
||||||
|
|
||||||
|
if (clickData?.image_name === images[0]?.image_name) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, [images, clickData]);
|
||||||
|
|
||||||
|
useHotkeys([
|
||||||
|
['ArrowRight', handleNext],
|
||||||
|
['ArrowLeft', handlePrev],
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div style={{ position: 'relative' }}>
|
||||||
<header className="App-header">
|
<AppShell
|
||||||
<Main />
|
header={{ height: 60 }}
|
||||||
</header>
|
navbar={{
|
||||||
|
width: 300,
|
||||||
|
breakpoint: 'sm',
|
||||||
|
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
|
||||||
|
}}
|
||||||
|
padding="md">
|
||||||
|
<AppShell.Header>
|
||||||
|
<Box className="flex items-center justify-between h-full">
|
||||||
|
<Group h="100%" px="md">
|
||||||
|
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
|
||||||
|
<Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" />
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Box className="px-4 flex items-center gap-4 w-fit">
|
||||||
|
<Button disabled={!clickData} onClick={handleClearSelect} leftSection={<IconRefreshDot size={14} />} color="orange">
|
||||||
|
Reset select
|
||||||
|
</Button>
|
||||||
|
<SaveButton
|
||||||
|
onSaved={(data) => {
|
||||||
|
handleNext();
|
||||||
|
}}
|
||||||
|
currentData={clickData}
|
||||||
|
imageLabelRef={imageLabelRef}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</AppShell.Header>
|
||||||
|
<AppShell.Navbar p="md">
|
||||||
|
<Box className="flex flex-col justify-between gap-2 w-full h-full">
|
||||||
|
<Box className="flex items-center justify-between gap-4 mb-2">
|
||||||
|
<Button fullWidth disabled={!images.length} onClick={handleClear} leftSection={<IconTrash size={14} />} color="red">
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
<Button fullWidth disabled={images.length} onClick={handleAddImages} leftSection={<IconImageInPicture size={14} />}>
|
||||||
|
Add images
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<ScrollArea h={780} className="pb-5 flex-1">
|
||||||
|
<Box className="flex flex-col gap-3 w-full">
|
||||||
|
{images.length > 0 &&
|
||||||
|
images.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<ImageDetect
|
||||||
|
currentData={clickData}
|
||||||
|
onClick={(data) => {
|
||||||
|
console.log({ data });
|
||||||
|
setClickData(data);
|
||||||
|
}}
|
||||||
|
key={`${item?.name || item?.image_name}_${index}`}
|
||||||
|
file={item}
|
||||||
|
onDelete={(data) => {
|
||||||
|
handleDeleteImage(data, index);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{images.length <= 0 && (
|
||||||
|
<Box className="flex items-center justify-center">
|
||||||
|
<span>No images to process</span>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
{progress.total > 0 && (
|
||||||
|
<Box className="flex flex-col items-center justify-end w-full">
|
||||||
|
<Text>
|
||||||
|
{progress.processFiles} / {progress.total}
|
||||||
|
</Text>
|
||||||
|
<Progress className="w-full" value={(progress.processFiles * 100) / progress.total} striped animated />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</AppShell.Navbar>
|
||||||
|
<AppShell.Main style={{ position: 'relative' }}>
|
||||||
|
{clickData && <ImageLabel ref={imageLabelRef} data={clickData} />}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '10px',
|
||||||
|
width: '100%',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}>
|
||||||
|
<Dropzone
|
||||||
|
openRef={dropzoneRef}
|
||||||
|
hidden={!clickData}
|
||||||
|
onErorrFiles={(errors) => {
|
||||||
|
notifications.show({
|
||||||
|
title: 'Invalid Images',
|
||||||
|
message: `There ${errors.length === 1 ? 'is' : 'are'} ${errors.length} invalid image${
|
||||||
|
errors.length > 1 ? 's' : ''
|
||||||
|
}. Please re-check!`,
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onFilesChange={handleUpload}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className="fixed bottom-5 right-5 flex items-center gap-4">
|
||||||
|
<Tooltip label={'Left (<)'}>
|
||||||
|
<ActionIcon disabled={!showPrev} onClick={handlePrev} size={'lg'}>
|
||||||
|
<IconChevronLeft size={20} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label={'Right (>)'}>
|
||||||
|
<ActionIcon disabled={!showNext} onClick={handleNext} size={'lg'}>
|
||||||
|
<IconChevronRight size={20} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
sans-serif;
|
||||||
sans-serif;
|
-webkit-font-smoothing: antialiased;
|
||||||
-webkit-font-smoothing: antialiased;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||||
monospace;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,24 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
import '@mantine/dropzone/styles.css';
|
||||||
|
import '@mantine/notifications/styles.css';
|
||||||
|
import { Notifications } from '@mantine/notifications';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import reportWebVitals from './reportWebVitals';
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
import { createTheme, MantineProvider } from '@mantine/core';
|
||||||
|
|
||||||
|
const theme = createTheme({
|
||||||
|
/** Put your mantine theme override here */
|
||||||
|
});
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<MantineProvider theme={theme} defaultColorScheme="dark">
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>
|
|
||||||
|
<Notifications />
|
||||||
|
</MantineProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
import { Group, Text } from '@mantine/core';
|
||||||
|
import { Dropzone as Dz, IMAGE_MIME_TYPE } from '@mantine/dropzone';
|
||||||
|
import { IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
export const Dropzone = forwardRef(({ onErorrFiles, onFilesChange, onReject, hidden, openRef }, ref) => {
|
||||||
|
const handleDrop = async (files) => {
|
||||||
|
const checkImageSize = (file) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
const objectUrl = URL.createObjectURL(file);
|
||||||
|
|
||||||
|
img.src = objectUrl;
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
if (img.width >= 500 && img.height >= 500) {
|
||||||
|
resolve(file);
|
||||||
|
} else {
|
||||||
|
reject(`${file.name} is too small (${img.width}x${img.height})`);
|
||||||
|
}
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = () => {
|
||||||
|
reject(`${file.name} is not a valid image.`);
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await Promise.allSettled(files.map(checkImageSize));
|
||||||
|
|
||||||
|
const acceptedFiles = results.filter((res) => res.status === 'fulfilled').map((res) => res.value);
|
||||||
|
const errors = results.filter((res) => res.status === 'rejected').map((res) => `❌ ${res.reason}`);
|
||||||
|
|
||||||
|
if (errors.length > 0 && onErorrFiles) {
|
||||||
|
onErorrFiles(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onFilesChange) {
|
||||||
|
onFilesChange(acceptedFiles, files);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Unexpected error:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dz
|
||||||
|
display={!hidden ? 'none' : 'flex'}
|
||||||
|
openRef={openRef}
|
||||||
|
ref={ref}
|
||||||
|
style={{ width: '100%', height: '400px', display: 'flex', justifyContent: 'center' }}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onReject={onReject}
|
||||||
|
accept={IMAGE_MIME_TYPE}>
|
||||||
|
<Group justify="center" gap="xl" mih={220} h={'100%'} style={{ pointerEvents: 'none' }}>
|
||||||
|
<Dz.Accept>
|
||||||
|
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
|
</Dz.Accept>
|
||||||
|
<Dz.Reject>
|
||||||
|
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
|
</Dz.Reject>
|
||||||
|
<Dz.Idle>
|
||||||
|
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dz.Idle>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text size="xl" inline>
|
||||||
|
Drag images here or click to select files
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" inline mt={7}>
|
||||||
|
Attach as many files as you like, each file should not exceed 5mb
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Dz>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { ActionIcon, Avatar, Box, Indicator, LoadingOverlay } from '@mantine/core';
|
||||||
|
import { generateImageUrl, randomDelay } from '../../ultils';
|
||||||
|
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useImagesDetected } from '../../stores/use-images-detected';
|
||||||
|
import { IconTrash } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
export const ImageDetect = ({ file, onClick, currentData, onDelete, onDetected }) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [data, setData] = useState();
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const ref = useRef(null);
|
||||||
|
const [isDetected, setIsDetected] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!file?.points) return;
|
||||||
|
|
||||||
|
setData(file);
|
||||||
|
}, [file]);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (isVisible || currentData?.image_name !== file?.image_name) return;
|
||||||
|
|
||||||
|
// ref.current?.scrollIntoView({
|
||||||
|
// behavior: 'smooth',
|
||||||
|
// block: 'center',
|
||||||
|
// inline: 'nearest',
|
||||||
|
// });
|
||||||
|
// }, [currentData, isVisible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
setIsVisible(entry.isIntersecting);
|
||||||
|
},
|
||||||
|
{ threshold: 1 },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ref.current) observer.observe(ref.current);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (ref.current) observer.unobserve(ref.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
display={isDetected ? 'none' : 'flex'}
|
||||||
|
ref={ref}
|
||||||
|
onClick={data?.points && onClick ? () => onClick(data) : undefined}
|
||||||
|
className={classNames('flex items-center justify-between gap-3 rounded-md p-3 max-w-full relative', {
|
||||||
|
['hover:bg-gray-900 cursor-pointer']: !loading,
|
||||||
|
['cursor-not-allowed']: loading,
|
||||||
|
['bg-gray-900']: currentData?.image_name === data?.image_name,
|
||||||
|
})}>
|
||||||
|
<Box className="flex items-center gap-3 w-full flex-1 max-w-full">
|
||||||
|
<Indicator color="red" disabled={!!data?.isSave}>
|
||||||
|
<Avatar size={'md'} src={generateImageUrl(data?.image_path)} radius="sm" />
|
||||||
|
</Indicator>
|
||||||
|
<span className="truncate flex-1 w-[140px]">{data?.image_name || ''}</span>
|
||||||
|
</Box>
|
||||||
|
<ActionIcon
|
||||||
|
color="red"
|
||||||
|
onClick={
|
||||||
|
onDelete
|
||||||
|
? (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDelete(data);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}>
|
||||||
|
<IconTrash size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
|
||||||
|
<LoadingOverlay
|
||||||
|
visible={loading}
|
||||||
|
zIndex={10}
|
||||||
|
overlayProps={{ radius: 'sm', blur: 2 }}
|
||||||
|
loaderProps={{
|
||||||
|
size: 'xs',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
import React, { useEffect, useRef, useState, forwardRef, useImperativeHandle, memo } from 'react';
|
||||||
|
import { Box, LoadingOverlay } from '@mantine/core';
|
||||||
|
import PointsBlur from './PointsBlur';
|
||||||
|
import { generateImageUrl } from '../../ultils';
|
||||||
|
|
||||||
|
const ImageLabel = forwardRef(({ data }, ref) => {
|
||||||
|
const [imageSrc, setImageSrc] = useState(null);
|
||||||
|
const blurredRegions = useRef([]);
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
const [reloading, setReloading] = useState(false);
|
||||||
|
const [listLabel, setListLabel] = useState([]);
|
||||||
|
const [infoImage, setInfoImage] = useState({});
|
||||||
|
const [imageName, setImageName] = useState('');
|
||||||
|
|
||||||
|
// Expose blurredRegions cho component cha
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
getBlurredRegions: () => blurredRegions.current,
|
||||||
|
setBlurredRegions: (regions) => {
|
||||||
|
blurredRegions.current = regions;
|
||||||
|
},
|
||||||
|
getSaveData: () => {
|
||||||
|
return {
|
||||||
|
blurredRegions: blurredRegions.current,
|
||||||
|
imageName,
|
||||||
|
infoImage,
|
||||||
|
listLabel,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.error) {
|
||||||
|
alert(data.error);
|
||||||
|
} else {
|
||||||
|
if (data?.points) {
|
||||||
|
blurredRegions.current = data?.points?.map((pre) => [
|
||||||
|
{ x: pre.x1, y: pre.y1, label: pre.label },
|
||||||
|
{ x: pre.x2, y: pre.y1, label: pre.label },
|
||||||
|
{ x: pre.x2, y: pre.y2, label: pre.label },
|
||||||
|
{ x: pre.x1, y: pre.y2, label: pre.label },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
setListLabel(
|
||||||
|
Object.entries(data?.labels).map(([id, name]) => ({
|
||||||
|
id: Number(id),
|
||||||
|
name,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
setImageSrc(generateImageUrl(data?.image_path));
|
||||||
|
setImageName(data?.image_name);
|
||||||
|
setTimeout(() => setReloading((pre) => !pre), 1000);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className="w-full h-full flex items-center justify-center relative">
|
||||||
|
<PointsBlur
|
||||||
|
imageSrc={imageSrc}
|
||||||
|
blurredRegions={blurredRegions}
|
||||||
|
loaded={loaded}
|
||||||
|
setLoaded={setLoaded}
|
||||||
|
reloading={reloading}
|
||||||
|
setReloading={setReloading}
|
||||||
|
setInfoImage={setInfoImage}
|
||||||
|
listLabel={listLabel}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default memo(ImageLabel);
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded, reloading, setReloading, setInfoImage, listLabel = [] }) => {
|
const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded, reloading, setReloading, setInfoImage, listLabel = [] }) => {
|
||||||
const points = useRef([]);
|
const points = useRef([]);
|
||||||
|
|
@ -12,7 +12,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext('2d');
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
|
|
||||||
image.onload = () => {
|
image.onload = () => {
|
||||||
|
|
@ -23,7 +23,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
|
||||||
setInfoImage({ width: image.width, height: image.height });
|
setInfoImage({ width: image.width, height: image.height });
|
||||||
};
|
};
|
||||||
|
|
||||||
image.crossOrigin = "anonymous";
|
image.crossOrigin = 'anonymous';
|
||||||
image.src = imageSrc;
|
image.src = imageSrc;
|
||||||
}, [imageSrc]);
|
}, [imageSrc]);
|
||||||
|
|
||||||
|
|
@ -56,7 +56,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
points.current.push({ x, y, label: listLabel[0]?.name || "cisco" });
|
points.current.push({ x, y, label: listLabel[0]?.name || 'cisco' });
|
||||||
drawRedPoint(canvas, x, y);
|
drawRedPoint(canvas, x, y);
|
||||||
|
|
||||||
if (points.current.length === 4) {
|
if (points.current.length === 4) {
|
||||||
|
|
@ -125,14 +125,14 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
canvas.addEventListener("mousedown", handleCanvasMouseDown);
|
canvas.addEventListener('mousedown', handleCanvasMouseDown);
|
||||||
canvas.addEventListener("mouseup", handleCanvasMouseUp);
|
canvas.addEventListener('mouseup', handleCanvasMouseUp);
|
||||||
canvas.addEventListener("mousemove", handleCanvasMouseMove);
|
canvas.addEventListener('mousemove', handleCanvasMouseMove);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
canvas.removeEventListener("mousedown", handleCanvasMouseDown);
|
canvas.removeEventListener('mousedown', handleCanvasMouseDown);
|
||||||
canvas.removeEventListener("mouseup", handleCanvasMouseUp);
|
canvas.removeEventListener('mouseup', handleCanvasMouseUp);
|
||||||
canvas.removeEventListener("mousemove", handleCanvasMouseMove);
|
canvas.removeEventListener('mousemove', handleCanvasMouseMove);
|
||||||
};
|
};
|
||||||
}, [draggingPointIndex, movingRegionIndex, offset, isOpen, info]);
|
}, [draggingPointIndex, movingRegionIndex, offset, isOpen, info]);
|
||||||
|
|
||||||
|
|
@ -160,17 +160,17 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawRedPoint = (canvas, x, y) => {
|
const drawRedPoint = (canvas, x, y) => {
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext('2d');
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, 5, 0, 2 * Math.PI);
|
ctx.arc(x, y, 5, 0, 2 * Math.PI);
|
||||||
ctx.fillStyle = "red";
|
ctx.fillStyle = 'red';
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawBorder = (canvas, points) => {
|
const drawBorder = (canvas, points) => {
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext('2d');
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
points.forEach((point, index) => {
|
points.forEach((point, index) => {
|
||||||
|
|
@ -182,7 +182,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
|
||||||
});
|
});
|
||||||
ctx.closePath();
|
ctx.closePath();
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
ctx.strokeStyle = "blue";
|
ctx.strokeStyle = 'blue';
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
|
|
@ -190,23 +190,23 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawDeleteButton = (canvas, region, isSelected) => {
|
const drawDeleteButton = (canvas, region, isSelected) => {
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext('2d');
|
||||||
const buttonX = (region[0].x + region[1].x) / 2;
|
const buttonX = (region[0].x + region[1].x) / 2;
|
||||||
const buttonY = Math.min(region[0].y, region[1].y, region[2].y, region[3].y) - 20;
|
const buttonY = Math.min(region[0].y, region[1].y, region[2].y, region[3].y) - 20;
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.fillStyle = isSelected ? "rgb(196, 194, 61)" : "rgb(2, 101, 182)";
|
ctx.fillStyle = isSelected ? 'rgb(196, 194, 61)' : 'rgb(2, 101, 182)';
|
||||||
ctx.fillRect(buttonX - 30, buttonY - 10, 60, 20);
|
ctx.fillRect(buttonX - 30, buttonY - 10, 60, 20);
|
||||||
ctx.fillStyle = "rgb(255, 255, 255)";
|
ctx.fillStyle = 'rgb(255, 255, 255)';
|
||||||
ctx.font = "13px Arial";
|
ctx.font = '13px Arial';
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = 'center';
|
||||||
ctx.fillText(region[0]?.label, buttonX, buttonY + 5);
|
ctx.fillText(region[0]?.label, buttonX, buttonY + 5);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
const redrawCanvas = (index) => {
|
const redrawCanvas = (index) => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext('2d');
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
|
|
||||||
image.onload = () => {
|
image.onload = () => {
|
||||||
|
|
@ -223,17 +223,17 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
image.crossOrigin = "anonymous";
|
image.crossOrigin = 'anonymous';
|
||||||
image.src = imageSrc;
|
image.src = imageSrc;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: "relative" }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<canvas ref={canvasRef} />
|
<canvas ref={canvasRef} />
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div style={{ backgroundColor: "#FFF", position: "absolute", top: info?.value[0].y - 200, left: info?.value[0].x, zIndex: 10 }}>
|
<div style={{ backgroundColor: '#FFF', position: 'absolute', top: info?.value[0].y - 200, left: info?.value[0].x, zIndex: 9999 }}>
|
||||||
<div>
|
<div>
|
||||||
<span style={{ color: "#000", fontSize: "18px", fontWeight: "bold" }}>Select label</span>
|
<span style={{ color: '#000', fontSize: '18px', fontWeight: 'bold' }}>Select label</span>
|
||||||
</div>
|
</div>
|
||||||
{listLabel?.map((el, i) => (
|
{listLabel?.map((el, i) => (
|
||||||
<div key={i}>
|
<div key={i}>
|
||||||
|
|
@ -245,7 +245,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
redrawCanvas();
|
redrawCanvas();
|
||||||
}}
|
}}
|
||||||
style={{ backgroundColor: "#ccc", width: "150px", color: "#000" }}>
|
style={{ backgroundColor: '#ccc', width: '150px', color: '#000' }}>
|
||||||
{el?.name}
|
{el?.name}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -258,7 +258,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
redrawCanvas();
|
redrawCanvas();
|
||||||
}}
|
}}
|
||||||
style={{ backgroundColor: "#ff0000", marginBottom: "10px", width: "150px", color: "#fff" }}>
|
style={{ backgroundColor: '#ff0000', marginBottom: '10px', width: '150px', color: '#fff' }}>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { Button, Tooltip, LoadingOverlay } from '@mantine/core';
|
||||||
|
import { notifications } from '@mantine/notifications';
|
||||||
|
import { IconImageInPicture } from '@tabler/icons-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useImagesDetected } from '../../stores/use-images-detected';
|
||||||
|
import { convertToBoundingBox } from '../../ultils';
|
||||||
|
import { useHotkeys } from '@mantine/hooks';
|
||||||
|
|
||||||
|
export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const { appendImageDetect, images } = useImagesDetected();
|
||||||
|
|
||||||
|
const handleSave = async ({ blurredRegions, infoImage, imageName, listLabel }) => {
|
||||||
|
try {
|
||||||
|
if (blurredRegions?.length > 0) {
|
||||||
|
let arrPoints = '';
|
||||||
|
blurredRegions?.forEach((points) => {
|
||||||
|
const img_w = infoImage?.width;
|
||||||
|
const img_h = infoImage?.height;
|
||||||
|
|
||||||
|
// Get bounding box
|
||||||
|
const x_min = Math.min(...points.map((p) => p.x));
|
||||||
|
const y_min = Math.min(...points.map((p) => p.y));
|
||||||
|
const x_max = Math.max(...points.map((p) => p.x));
|
||||||
|
const y_max = Math.max(...points.map((p) => p.y));
|
||||||
|
|
||||||
|
const x_center = (x_min + x_max) / 2 / img_w;
|
||||||
|
const y_center = (y_min + y_max) / 2 / img_h;
|
||||||
|
const width = (x_max - x_min) / img_w;
|
||||||
|
const height = (y_max - y_min) / img_h;
|
||||||
|
|
||||||
|
// Get class ID
|
||||||
|
const class_id = listLabel.find((label) => label?.name === points[0].label)?.id || '0';
|
||||||
|
|
||||||
|
const yolo_label = `${class_id} ${x_center.toFixed(6)} ${y_center.toFixed(6)} ${width.toFixed(6)} ${height.toFixed(6)}`;
|
||||||
|
|
||||||
|
arrPoints += yolo_label + '\n';
|
||||||
|
});
|
||||||
|
const url = `${process.env.REACT_APP_API_URL}/save`;
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('list', arrPoints);
|
||||||
|
formData.append('imageName', imageName);
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
const newPoints = blurredRegions.map((points) => {
|
||||||
|
return convertToBoundingBox(points);
|
||||||
|
});
|
||||||
|
|
||||||
|
currentData.points = newPoints;
|
||||||
|
currentData['isSave'] = true;
|
||||||
|
|
||||||
|
const newImages = images.map((image) => {
|
||||||
|
if (image.image_name === imageName) {
|
||||||
|
return {
|
||||||
|
...image,
|
||||||
|
points: newPoints,
|
||||||
|
isSave: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...image };
|
||||||
|
});
|
||||||
|
|
||||||
|
appendImageDetect(newImages);
|
||||||
|
|
||||||
|
notifications.show({
|
||||||
|
title: 'Save Success',
|
||||||
|
message: `${imageName} save success`,
|
||||||
|
color: 'green',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onSaved) {
|
||||||
|
onSaved({ ...currentData });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notifications.show({
|
||||||
|
title: 'Save Error',
|
||||||
|
message: error?.message || 'Internal Server Error',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
const saveData = imageLabelRef.current?.getSaveData();
|
||||||
|
|
||||||
|
if (!saveData) return;
|
||||||
|
|
||||||
|
handleSave(saveData);
|
||||||
|
};
|
||||||
|
|
||||||
|
useHotkeys([['ctrl+S', handleSubmit]]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip label={'Ctrl + S'}>
|
||||||
|
<Button className="relative ư-f" disabled={!currentData} onClick={handleSubmit} leftSection={<IconImageInPicture size={14} />}>
|
||||||
|
Save
|
||||||
|
<LoadingOverlay visible={loading} loaderProps={{ size: 14 }} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
import React, { useRef, useState } from "react";
|
|
||||||
import PointsBlur from "./components/PointsBlur";
|
|
||||||
|
|
||||||
const Main = () => {
|
|
||||||
const [imageSrc, setImageSrc] = useState(null);
|
|
||||||
const blurredRegions = useRef([]);
|
|
||||||
const [loaded, setLoaded] = useState(false);
|
|
||||||
const [reloading, setReloading] = useState(false);
|
|
||||||
const [listLabel, setListLabel] = useState([]);
|
|
||||||
const [infoImage, setInfoImage] = useState({});
|
|
||||||
const [imageName, setImageName] = useState("");
|
|
||||||
|
|
||||||
const handleFileChange = async (event) => {
|
|
||||||
const file = event.target.files[0];
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("image", file);
|
|
||||||
blurredRegions.current = [];
|
|
||||||
setLoaded(false);
|
|
||||||
try {
|
|
||||||
const url = "http://localhost:5000/detect_image";
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.error) {
|
|
||||||
alert(data.error);
|
|
||||||
} else {
|
|
||||||
if (data?.points) {
|
|
||||||
blurredRegions.current = data?.points?.map((pre) => [
|
|
||||||
{ x: pre.x1, y: pre.y1, label: pre.label },
|
|
||||||
{ x: pre.x2, y: pre.y1, label: pre.label },
|
|
||||||
{ x: pre.x2, y: pre.y2, label: pre.label },
|
|
||||||
{ x: pre.x1, y: pre.y2, label: pre.label },
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
setListLabel(
|
|
||||||
Object.entries(data?.labels).map(([id, name]) => ({
|
|
||||||
id: Number(id),
|
|
||||||
name,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
setImageSrc("http://localhost:5000/" + data?.image_path);
|
|
||||||
setImageName(data?.image_name);
|
|
||||||
setTimeout(() => setReloading((pre) => !pre), 1000);
|
|
||||||
console.log(data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error detecting QR code:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
if (blurredRegions.current?.length > 0) {
|
|
||||||
let arrPoints = "";
|
|
||||||
blurredRegions.current?.forEach((points) => {
|
|
||||||
const img_w = infoImage?.width;
|
|
||||||
const img_h = infoImage?.height;
|
|
||||||
|
|
||||||
// Get bounding box
|
|
||||||
const x_min = Math.min(...points.map((p) => p.x));
|
|
||||||
const y_min = Math.min(...points.map((p) => p.y));
|
|
||||||
const x_max = Math.max(...points.map((p) => p.x));
|
|
||||||
const y_max = Math.max(...points.map((p) => p.y));
|
|
||||||
|
|
||||||
const x_center = (x_min + x_max) / 2 / img_w;
|
|
||||||
const y_center = (y_min + y_max) / 2 / img_h;
|
|
||||||
const width = (x_max - x_min) / img_w;
|
|
||||||
const height = (y_max - y_min) / img_h;
|
|
||||||
|
|
||||||
// Get class ID
|
|
||||||
const class_id = listLabel.find((label) => label?.name === points[0].label)?.id || "0";
|
|
||||||
|
|
||||||
const yolo_label = `${class_id} ${x_center.toFixed(6)} ${y_center.toFixed(6)} ${width.toFixed(6)} ${height.toFixed(6)}`;
|
|
||||||
|
|
||||||
arrPoints += yolo_label + "\n";
|
|
||||||
});
|
|
||||||
const url = "http://localhost:5000/save";
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("list", arrPoints);
|
|
||||||
formData.append("imageName", imageName);
|
|
||||||
await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<input type="file" accept="image/*" onChange={handleFileChange} />
|
|
||||||
<button style={{}} onClick={handleSave}>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<PointsBlur
|
|
||||||
imageSrc={imageSrc}
|
|
||||||
blurredRegions={blurredRegions}
|
|
||||||
loaded={loaded}
|
|
||||||
setLoaded={setLoaded}
|
|
||||||
reloading={reloading}
|
|
||||||
setReloading={setReloading}
|
|
||||||
setInfoImage={setInfoImage}
|
|
||||||
listLabel={listLabel}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Main;
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Cấu hình thư mục lưu trữ ảnh
|
||||||
|
IMAGE_FOLDER = "images"
|
||||||
|
LABEL_FOLDER = "label"
|
||||||
|
TEMP_FOLDER = "temp"
|
||||||
|
TRAINED_MODEL_FOLDER = "trained_models"
|
||||||
|
PRETRAINED_MODEL = "train5/weights/best.pt"
|
||||||
|
|
||||||
|
# Tạo thư mục nếu chưa có
|
||||||
|
for folder in [IMAGE_FOLDER, LABEL_FOLDER, TEMP_FOLDER]:
|
||||||
|
os.makedirs(folder, exist_ok=True)
|
||||||
|
Before Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
|
@ -1,20 +0,0 @@
|
||||||
1 0.846875 0.679733 0.102500 0.046706
|
|
||||||
1 0.399687 0.411593 0.091875 0.032527
|
|
||||||
1 0.394375 0.494579 0.096250 0.036697
|
|
||||||
1 0.380000 0.703086 0.107500 0.041701
|
|
||||||
1 0.384062 0.592160 0.104375 0.041701
|
|
||||||
0 0.751563 0.582152 0.038125 0.031693
|
|
||||||
0 0.302187 0.718098 0.043125 0.031693
|
|
||||||
0 0.324688 0.505421 0.038125 0.028357
|
|
||||||
0 0.725938 0.486239 0.036875 0.028357
|
|
||||||
0 0.334375 0.416597 0.035000 0.025855
|
|
||||||
0 0.774687 0.688907 0.039375 0.036697
|
|
||||||
1 0.399375 0.333194 0.090000 0.032527
|
|
||||||
0 0.310000 0.603003 0.038750 0.031693
|
|
||||||
0 0.697187 0.325271 0.033125 0.020017
|
|
||||||
0 0.703125 0.401168 0.032500 0.025021
|
|
||||||
0 0.336875 0.337364 0.035000 0.022519
|
|
||||||
1 0.792500 0.481234 0.092500 0.033361
|
|
||||||
1 0.820937 0.572560 0.100625 0.040867
|
|
||||||
1 0.767188 0.397832 0.075000 0.023353
|
|
||||||
1 0.757813 0.321518 0.073750 0.027523
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
1 0.846875 0.679733 0.102500 0.046706
|
|
||||||
1 0.399687 0.411593 0.091875 0.032527
|
|
||||||
1 0.394375 0.494579 0.096250 0.036697
|
|
||||||
1 0.380000 0.703086 0.107500 0.041701
|
|
||||||
1 0.384062 0.592160 0.104375 0.041701
|
|
||||||
0 0.751563 0.582152 0.038125 0.031693
|
|
||||||
0 0.302187 0.718098 0.043125 0.031693
|
|
||||||
0 0.324688 0.505421 0.038125 0.028357
|
|
||||||
0 0.725938 0.486239 0.036875 0.028357
|
|
||||||
0 0.334375 0.416597 0.035000 0.025855
|
|
||||||
0 0.773438 0.689741 0.039375 0.036697
|
|
||||||
1 0.401875 0.336530 0.090000 0.032527
|
|
||||||
0 0.310000 0.603003 0.038750 0.031693
|
|
||||||
0 0.697187 0.325271 0.033125 0.020017
|
|
||||||
0 0.703125 0.401168 0.032500 0.025021
|
|
||||||
1 0.792500 0.481234 0.092500 0.033361
|
|
||||||
1 0.820937 0.572560 0.100625 0.040867
|
|
||||||
1 0.759687 0.325271 0.080000 0.028357
|
|
||||||
1 0.766250 0.399917 0.076875 0.032527
|
|
||||||
1 0.337187 0.341952 0.031250 0.026689
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
1 0.726563 0.259766 0.156250 0.050781
|
|
||||||
0 0.503418 0.502604 0.045898 0.093750
|
|
||||||
0 0.444336 0.773438 0.050781 0.098958
|
|
||||||
1 0.324219 0.819661 0.164063 0.061198
|
|
||||||
1 0.787109 0.660156 0.162109 0.054688
|
|
||||||
1 0.387207 0.535156 0.166992 0.049479
|
|
||||||
1 0.795410 0.522135 0.159180 0.052083
|
|
||||||
1 0.740723 0.800781 0.151367 0.054688
|
|
||||||
0 0.455078 0.638672 0.046875 0.095052
|
|
||||||
1 0.769531 0.389974 0.154297 0.050781
|
|
||||||
1 0.333984 0.677083 0.167969 0.054688
|
|
||||||
1 0.321777 0.264323 0.161133 0.046875
|
|
||||||
0 0.853027 0.755859 0.047852 0.092448
|
|
||||||
1 0.354492 0.398438 0.166016 0.046875
|
|
||||||
0 0.473145 0.367839 0.045898 0.092448
|
|
||||||
0 0.905273 0.487630 0.042969 0.095052
|
|
||||||
0 0.897949 0.620443 0.043945 0.097656
|
|
||||||
0 0.837402 0.231120 0.043945 0.095052
|
|
||||||
0 0.439941 0.236979 0.045898 0.093750
|
|
||||||
1 0.908691 0.358073 0.051758 0.093750
|
|
||||||
|
|
@ -1,93 +1,22 @@
|
||||||
from flask import Flask, request, jsonify, send_from_directory
|
from flask import Flask
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
import cv2
|
from routes.detect import detect_bp
|
||||||
import numpy as np
|
from routes.save import save_bp
|
||||||
from pyzbar.pyzbar import decode
|
from routes.show import show_bp
|
||||||
from ultralytics import YOLO
|
from routes.delete import delete_bp
|
||||||
import datetime
|
from routes.upload import upload_bp
|
||||||
from PIL import Image
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Load model đã huấn luyện
|
|
||||||
model = YOLO("train5/weights/best.pt")
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
CORS(app)
|
CORS(app)
|
||||||
|
|
||||||
# Ensure the directory exists
|
|
||||||
os.makedirs("label", exist_ok=True)
|
|
||||||
os.makedirs("images", exist_ok=True)
|
|
||||||
|
|
||||||
@app.route('/detect_image', methods=['POST'])
|
app.config["MAX_CONTENT_LENGTH"] = 100 * 1024 * 1024
|
||||||
def detect_image():
|
|
||||||
file = request.files.get('image')
|
|
||||||
if not file:
|
|
||||||
return jsonify({"error": "No file uploaded"}), 400
|
|
||||||
|
|
||||||
# Open the image using PIL
|
app.register_blueprint(detect_bp)
|
||||||
try:
|
app.register_blueprint(save_bp)
|
||||||
image = Image.open(file)
|
app.register_blueprint(show_bp)
|
||||||
except Exception as e:
|
app.register_blueprint(delete_bp)
|
||||||
return jsonify({"error": "Invalid image file", "details": str(e)}), 400
|
app.register_blueprint(upload_bp)
|
||||||
|
|
||||||
# Check image size
|
|
||||||
min_width, min_height = 500, 500
|
|
||||||
if image.width < min_width or image.height < min_height:
|
|
||||||
return jsonify({"error": f"Image is too small. Minimum size is {min_width}x{min_height} pixels."}), 400
|
|
||||||
|
|
||||||
# Reset file pointer before saving
|
|
||||||
file.seek(0)
|
|
||||||
|
|
||||||
basename = "detect_image"
|
|
||||||
suffix = datetime.datetime.now().strftime("%y%m%d_%H%M%S")
|
|
||||||
image_name = "_".join([basename, suffix]) # e.g. 'detect_image_120508_171442'
|
|
||||||
image_path = os.path.join("images", image_name) + ".png"
|
|
||||||
file.save(image_path)
|
|
||||||
# Chạy mô hình để dự đoán
|
|
||||||
results = model(image_path, conf=0.6) # Hạ conf xuống để giữ nhiều phát hiện hơn
|
|
||||||
points = []
|
|
||||||
# Xử lý kết quả dự đoán
|
|
||||||
for r in results:
|
|
||||||
for box in r.boxes:
|
|
||||||
x1, y1, x2, y2 = map(int, box.xyxy[0]) # Lấy tọa độ bbox
|
|
||||||
# print(x1, y1, x2, y2)
|
|
||||||
# print(model.names)
|
|
||||||
conf = float(box.conf[0]) # Độ tin cậy
|
|
||||||
cls = int(box.cls[0]) # Nhãn class ID
|
|
||||||
# label = f"{model.names[cls]} {conf:.2f}" # Tạo nhãn hiển thị
|
|
||||||
|
|
||||||
points.append({'label': model.names[cls],'x1':x1, "y1":y1, "x2":x2, "y2":y2})
|
|
||||||
result = {
|
|
||||||
"image_name": image_name,
|
|
||||||
"image_path": image_path,
|
|
||||||
"labels": model.names,
|
|
||||||
"points": points
|
|
||||||
}
|
|
||||||
return jsonify(result)
|
|
||||||
|
|
||||||
# Serve images from the "images" directory
|
|
||||||
@app.route('/images/<filename>')
|
|
||||||
def get_image(filename):
|
|
||||||
return send_from_directory("images", filename)
|
|
||||||
|
|
||||||
@app.route('/save', methods=['POST'])
|
|
||||||
def save_data():
|
|
||||||
# Get form data
|
|
||||||
arr_points = request.form.get("list")
|
|
||||||
image_name = request.form.get("imageName")
|
|
||||||
|
|
||||||
if not arr_points or not image_name:
|
|
||||||
return jsonify({"error": "Missing required fields"}), 400
|
|
||||||
|
|
||||||
# Define the file path
|
|
||||||
file_path = os.path.join("label", f"{image_name}.txt")
|
|
||||||
arr_points=arr_points.replace("\r\n", "\n")
|
|
||||||
# Save the data to a text file
|
|
||||||
with open(file_path, "w") as file:
|
|
||||||
file.write(arr_points) # Write the points directly to the file
|
|
||||||
|
|
||||||
|
|
||||||
return jsonify({"success": True, "list": arr_points, "imageName": image_name})
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(debug=True)
|
app.run(debug=True)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
```bash
|
||||||
|
|
||||||
|
nohup gunicorn -w 1 -b 127.0.0.1:5000 main:app > output.log 2>&1 &
|
||||||
|
ps aux | grep gunicorn
|
||||||
|
|
||||||
|
flask --app main.py --debug run
|
||||||
|
|
||||||
|
source venv/bin/activate.fish
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from config import LABEL_FOLDER, IMAGE_FOLDER
|
||||||
|
delete_bp = Blueprint('delete', __name__)
|
||||||
|
|
||||||
|
@delete_bp.route('/delete/<image_name>', methods=['DELETE'])
|
||||||
|
def delete_image(image_name):
|
||||||
|
|
||||||
|
today_str = datetime.datetime.now().strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
image_path = os.path.join(IMAGE_FOLDER,today_str, image_name)
|
||||||
|
|
||||||
|
label_path = os.path.join(LABEL_FOLDER,today_str, f"{image_name}.txt")
|
||||||
|
|
||||||
|
if os.path.exists(image_path):
|
||||||
|
os.remove(image_path)
|
||||||
|
else:
|
||||||
|
return jsonify({"error": "Image not found"}), 404
|
||||||
|
|
||||||
|
if os.path.exists(label_path):
|
||||||
|
os.remove(label_path)
|
||||||
|
|
||||||
|
return jsonify({"message": "Image and label deleted successfully", "status": "true"})
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
from flask import Blueprint, request, jsonify, Response, stream_with_context
|
||||||
|
from PIL import Image
|
||||||
|
from ultralytics import YOLO
|
||||||
|
from config import IMAGE_FOLDER, PRETRAINED_MODEL, TRAINED_MODEL_FOLDER, TEMP_FOLDER
|
||||||
|
from utils import ensure_correct_permissions, get_latest_model_including_today
|
||||||
|
import random
|
||||||
|
import cv2
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Load mô hình YOLO
|
||||||
|
model = YOLO(get_latest_model_including_today(trained_model_folder=TRAINED_MODEL_FOLDER))
|
||||||
|
|
||||||
|
detect_bp = Blueprint('detect', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
connected_clients = {} # Lưu client_id và kết nối
|
||||||
|
|
||||||
|
@detect_bp.route('/detect_images', methods=['POST'])
|
||||||
|
def detect_images():
|
||||||
|
files = request.files.getlist('images') # Nhận nhiều ảnh từ request
|
||||||
|
if not files or len(files) == 0:
|
||||||
|
return jsonify({"error": "No files uploaded"}), 400
|
||||||
|
|
||||||
|
results_list = [] # Danh sách kết quả cho từng ảnh
|
||||||
|
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
try:
|
||||||
|
# Kiểm tra file hợp lệ không
|
||||||
|
image = Image.open(file)
|
||||||
|
|
||||||
|
# Check kích thước ảnh
|
||||||
|
min_width, min_height = 500, 500
|
||||||
|
if image.width < min_width or image.height < min_height:
|
||||||
|
results_list.append({"filename": file.filename, "error": f"Image too small ({image.width}x{image.height})"})
|
||||||
|
continue # Bỏ qua ảnh lỗi
|
||||||
|
|
||||||
|
# Reset file pointer trước khi lưu
|
||||||
|
file.seek(0)
|
||||||
|
|
||||||
|
# Tạo tên file duy nhất
|
||||||
|
suffix = datetime.datetime.now().strftime("%y%m%d_%H%M%S")
|
||||||
|
image_name = f"detect_{suffix}_{file.filename}"
|
||||||
|
image_path = os.path.join("images", image_name)
|
||||||
|
file.save(image_path)
|
||||||
|
|
||||||
|
# Chạy mô hình YOLO để phát hiện vật thể
|
||||||
|
results = model(image_path, conf=0.6)
|
||||||
|
|
||||||
|
points = []
|
||||||
|
for r in results:
|
||||||
|
for box in r.boxes:
|
||||||
|
x1, y1, x2, y2 = map(int, box.xyxy[0]) # Lấy tọa độ bbox
|
||||||
|
conf = float(box.conf[0]) # Độ tin cậy
|
||||||
|
cls = int(box.cls[0]) # Nhãn class ID
|
||||||
|
points.append({'label': model.names[cls], 'x1': x1, "y1": y1, "x2": x2, "y2": y2})
|
||||||
|
|
||||||
|
results_list.append({
|
||||||
|
"filename": file.filename,
|
||||||
|
"image_name": image_name,
|
||||||
|
"image_path": image_path,
|
||||||
|
"labels": model.names,
|
||||||
|
"points": points
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
results_list.append({"filename": file.filename, "error": str(e)})
|
||||||
|
|
||||||
|
return jsonify({"results": results_list})
|
||||||
|
|
||||||
|
|
||||||
|
@detect_bp.route('/detect_image', methods=['POST'])
|
||||||
|
def detect_image():
|
||||||
|
file = request.files.get('image')
|
||||||
|
if not file:
|
||||||
|
return jsonify({"error": "No file uploaded"}), 400
|
||||||
|
|
||||||
|
# Open the image using PIL
|
||||||
|
try:
|
||||||
|
image = Image.open(file)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": "Invalid image file", "details": str(e)}), 400
|
||||||
|
|
||||||
|
# Check image size
|
||||||
|
min_width, min_height = 500, 500
|
||||||
|
if image.width < min_width or image.height < min_height:
|
||||||
|
return jsonify({"error": f"Image is too small. Minimum size is {min_width}x{min_height} pixels."}), 400
|
||||||
|
|
||||||
|
# Reset file pointer before saving
|
||||||
|
file.seek(0)
|
||||||
|
|
||||||
|
# 📌 Get time today
|
||||||
|
today_str = datetime.datetime.now().strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
# 📌 Create folder images/today if not exits
|
||||||
|
save_dir = os.path.join(IMAGE_FOLDER, today_str)
|
||||||
|
os.makedirs(save_dir, exist_ok=True)
|
||||||
|
ensure_correct_permissions(save_dir)
|
||||||
|
# 📌 Create radom image name
|
||||||
|
basename = "detect_image"
|
||||||
|
suffix = datetime.datetime.now().strftime("%H%M%S") # Chỉ lấy giờ phút giây
|
||||||
|
random_number = random.randint(100000, 999999)
|
||||||
|
image_name = f"{basename}_{suffix}_{random_number}.png"
|
||||||
|
image_path = os.path.join(save_dir, image_name)
|
||||||
|
|
||||||
|
# 📌 Save image to folder today time
|
||||||
|
file.save(image_path)
|
||||||
|
|
||||||
|
# Run with model
|
||||||
|
results = model(image_path, conf=0.6)
|
||||||
|
points = []
|
||||||
|
|
||||||
|
# Result predict
|
||||||
|
for r in results:
|
||||||
|
for box in r.boxes:
|
||||||
|
x1, y1, x2, y2 = map(int, box.xyxy[0])
|
||||||
|
conf = float(box.conf[0])
|
||||||
|
cls = int(box.cls[0])
|
||||||
|
|
||||||
|
points.append({'label': model.names[cls], 'x1': x1, "y1": y1, "x2": x2, "y2": y2})
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"image_name": image_name,
|
||||||
|
"image_path": image_path,
|
||||||
|
"labels": model.names,
|
||||||
|
"points": points
|
||||||
|
}
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
|
||||||
|
@detect_bp.route('/reload_model', methods=['POST'])
|
||||||
|
def reload_model():
|
||||||
|
global model
|
||||||
|
new_model_path = get_latest_model_including_today(trained_model_folder=TRAINED_MODEL_FOLDER)
|
||||||
|
print(new_model_path)
|
||||||
|
model = YOLO(new_model_path)
|
||||||
|
return jsonify({"message": f"Model reloaded from {new_model_path}", "status": "true"}), 200
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import os
|
||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from config import LABEL_FOLDER
|
||||||
|
import datetime
|
||||||
|
from utils import ensure_correct_permissions
|
||||||
|
save_bp = Blueprint('save', __name__)
|
||||||
|
|
||||||
|
@save_bp.route('/save', methods=['POST'])
|
||||||
|
def save_data():
|
||||||
|
arr_points = request.form.get("list")
|
||||||
|
image_name = request.form.get("imageName")
|
||||||
|
|
||||||
|
if not arr_points or not image_name:
|
||||||
|
return jsonify({"error": "Missing required fields"}), 400
|
||||||
|
|
||||||
|
today_str = datetime.datetime.now().strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
today_folder = os.path.join(LABEL_FOLDER, today_str)
|
||||||
|
os.makedirs(today_folder, exist_ok=True)
|
||||||
|
ensure_correct_permissions(today_folder)
|
||||||
|
|
||||||
|
file_name = os.path.splitext(image_name)[0]
|
||||||
|
file_path = os.path.join(today_folder, f"{file_name}.txt")
|
||||||
|
arr_points = arr_points.replace("\r\n", "\n")
|
||||||
|
|
||||||
|
with open(file_path, "w") as file:
|
||||||
|
file.write(arr_points)
|
||||||
|
|
||||||
|
return jsonify({"success": True, "list": arr_points, "imageName": image_name})
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
from flask import Blueprint, request, jsonify, send_from_directory
|
||||||
|
from PIL import Image
|
||||||
|
from ultralytics import YOLO
|
||||||
|
from config import IMAGE_FOLDER
|
||||||
|
|
||||||
|
show_bp = Blueprint('show', __name__)
|
||||||
|
|
||||||
|
@show_bp.route('/images/<date>/<filename>')
|
||||||
|
def get_image(date, filename):
|
||||||
|
image_path = os.path.join("images", date)
|
||||||
|
|
||||||
|
if not os.path.exists(image_path):
|
||||||
|
return jsonify({"error": "Image not found"}), 404
|
||||||
|
|
||||||
|
return send_from_directory(image_path, filename)
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import os
|
||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from config import TEMP_FOLDER
|
||||||
|
import datetime
|
||||||
|
from utils import ensure_correct_permissions
|
||||||
|
upload_bp = Blueprint('upload', __name__)
|
||||||
|
|
||||||
|
@upload_bp.route('/upload_images', methods=['POST'])
|
||||||
|
def upload_images():
|
||||||
|
files = request.files.getlist("images")
|
||||||
|
if not files:
|
||||||
|
return jsonify({"error": "No files received"}), 400
|
||||||
|
|
||||||
|
saved_files = []
|
||||||
|
skipped_files = []
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
file_path = os.path.join(TEMP_FOLDER, file.filename)
|
||||||
|
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
skipped_files.append(file.filename)
|
||||||
|
continue
|
||||||
|
|
||||||
|
file.save(file_path)
|
||||||
|
saved_files.append(file.filename)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"message": "Upload completed!",
|
||||||
|
"saved": saved_files,
|
||||||
|
"skipped": skipped_files
|
||||||
|
}), 200
|
||||||
|
|
@ -0,0 +1,235 @@
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import datetime
|
||||||
|
import random
|
||||||
|
from config import IMAGE_FOLDER, LABEL_FOLDER, TRAINED_MODEL_FOLDER, PRETRAINED_MODEL
|
||||||
|
from ultralytics import YOLO
|
||||||
|
import yaml
|
||||||
|
import requests
|
||||||
|
import glob
|
||||||
|
|
||||||
|
# 🗂️ Configure paths
|
||||||
|
DATA_SPLIT_FOLDER = "model_datasets"
|
||||||
|
IMAGE_EXTENSION = ".png"
|
||||||
|
LOG_FILE = "training_logs.log"
|
||||||
|
|
||||||
|
class_names = ["cisco", "barcode"]
|
||||||
|
# 📅 Get today's and yesterday's date
|
||||||
|
today = datetime.date.today()
|
||||||
|
yesterday = today - datetime.timedelta(days=1)
|
||||||
|
|
||||||
|
today_str = today.strftime("%Y-%m-%d")
|
||||||
|
yesterday_str = yesterday.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
image_folder_today = os.path.join(IMAGE_FOLDER, today_str)
|
||||||
|
image_folder_yesterday = os.path.join(IMAGE_FOLDER, yesterday_str)
|
||||||
|
|
||||||
|
label_folder_today = os.path.join(LABEL_FOLDER, today_str)
|
||||||
|
label_folder_yesterday = os.path.join(LABEL_FOLDER, yesterday_str)
|
||||||
|
|
||||||
|
# 🔍 Validate folder existence
|
||||||
|
if os.path.exists(image_folder_today) and os.path.exists(label_folder_today):
|
||||||
|
image_folder = image_folder_today
|
||||||
|
label_folder = label_folder_today
|
||||||
|
date_str = today_str
|
||||||
|
elif os.path.exists(image_folder_yesterday) and os.path.exists(label_folder_yesterday):
|
||||||
|
image_folder = image_folder_yesterday
|
||||||
|
label_folder = label_folder_yesterday
|
||||||
|
date_str = yesterday_str
|
||||||
|
else:
|
||||||
|
print(image_folder)
|
||||||
|
print(label_folder)
|
||||||
|
raise Exception("⚠️ No valid image & label folder found in the last two days!")
|
||||||
|
|
||||||
|
# 🏗️ Create dataset folders
|
||||||
|
dataset_folder = os.path.join(DATA_SPLIT_FOLDER, date_str)
|
||||||
|
model_folder_name = os.path.join(TRAINED_MODEL_FOLDER)
|
||||||
|
nolable_img_folder = os.path.join(dataset_folder, "nolable")
|
||||||
|
train_img_folder = os.path.join(dataset_folder, "train", "images")
|
||||||
|
train_lbl_folder = os.path.join(dataset_folder, "train", "labels")
|
||||||
|
val_img_folder = os.path.join(dataset_folder, "val", "images")
|
||||||
|
val_lbl_folder = os.path.join(dataset_folder, "val", "labels")
|
||||||
|
|
||||||
|
for folder in [train_img_folder, train_lbl_folder, val_img_folder, val_lbl_folder, nolable_img_folder]:
|
||||||
|
os.makedirs(folder, exist_ok=True)
|
||||||
|
|
||||||
|
# 📂 Get image list
|
||||||
|
image_files = [f for f in os.listdir(image_folder) if f.endswith(IMAGE_EXTENSION)]
|
||||||
|
|
||||||
|
# 🌀 Shuffle images
|
||||||
|
random.shuffle(image_files)
|
||||||
|
|
||||||
|
# 📊 Split ratio
|
||||||
|
train_ratio = 0.8
|
||||||
|
split_idx = int(len(image_files) * train_ratio)
|
||||||
|
|
||||||
|
train_files = image_files[:split_idx]
|
||||||
|
val_files = image_files[split_idx:]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def log_message(message: str):
|
||||||
|
"""Ghi log vào file"""
|
||||||
|
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
log_entry = f"[{timestamp}] {message}\n"
|
||||||
|
with open(LOG_FILE, "a") as log:
|
||||||
|
log.write(log_entry)
|
||||||
|
|
||||||
|
def get_latest_model(trained_model_folder: str, default_model: str = "train5/weights/best.pt"):
|
||||||
|
"""Tìm model gần nhất (trước ngày hôm nay), nếu không có thì dùng model mặc định."""
|
||||||
|
if not os.path.exists(trained_model_folder):
|
||||||
|
log_message(f"⚠️ Folder {trained_model_folder} does not exist. Using default model: {default_model}")
|
||||||
|
return default_model
|
||||||
|
|
||||||
|
today_str = datetime.datetime.now().strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
# Lấy danh sách các thư mục theo ngày, loại bỏ thư mục của ngày hôm nay
|
||||||
|
subfolders = [
|
||||||
|
f for f in os.listdir(trained_model_folder)
|
||||||
|
if os.path.isdir(os.path.join(trained_model_folder, f)) and f < today_str
|
||||||
|
]
|
||||||
|
subfolders = sorted(subfolders, reverse=True) # Sắp xếp giảm dần (mới nhất trước)
|
||||||
|
|
||||||
|
for folder in subfolders:
|
||||||
|
model_path = os.path.join(trained_model_folder, folder, "weights", "best.pt")
|
||||||
|
if os.path.exists(model_path):
|
||||||
|
log_message(f"✅ Found latest model: {model_path}")
|
||||||
|
return model_path
|
||||||
|
|
||||||
|
log_message(f"⚠️ No valid models found in {trained_model_folder}. Using default model: {default_model}")
|
||||||
|
return default_model
|
||||||
|
|
||||||
|
def create_dataset_yaml(dataset_folder: str, classes: list):
|
||||||
|
dataset_path = os.path.abspath(dataset_folder)
|
||||||
|
data_yaml_path = os.path.join(dataset_folder, "data.yaml")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"train": os.path.join(dataset_path, "train", "images"),
|
||||||
|
"val": os.path.join(dataset_path, "val", "images"),
|
||||||
|
"test": os.path.join(dataset_path, "test", "images"), # Nếu có test set
|
||||||
|
"nc": len(classes),
|
||||||
|
"names": list(classes)
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(data_yaml_path, "w") as f:
|
||||||
|
yaml.dump(data, f, default_flow_style=True)
|
||||||
|
|
||||||
|
return data_yaml_path
|
||||||
|
|
||||||
|
def call_reload_model_api(base_url="http://localhost:5000"):
|
||||||
|
"""Gọi API để reload model"""
|
||||||
|
url = f"{base_url}/reload_model"
|
||||||
|
try:
|
||||||
|
response = requests.post(url, timeout=10)
|
||||||
|
response_data = response.json()
|
||||||
|
log_message(f"✅ API Response: {response_data} \n")
|
||||||
|
return response_data
|
||||||
|
except requests.RequestException as e:
|
||||||
|
log_message(f"❌ Error calling reload model API: {e} \n")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
def clear_images_source():
|
||||||
|
paths = [image_folder, label_folder]
|
||||||
|
# paths = [image_folder, label_folder, train_img_folder, train_lbl_folder, val_img_folder, val_lbl_folder]
|
||||||
|
|
||||||
|
for path in paths:
|
||||||
|
for file in glob.glob(os.path.join(path, "*")):
|
||||||
|
if os.path.isfile(file):
|
||||||
|
os.remove(file)
|
||||||
|
|
||||||
|
|
||||||
|
log_message(f"Delete source image success \n")
|
||||||
|
log_message("END " + ("=" * 20) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
# 🚀 Function to move files and log process
|
||||||
|
def move_files(file_list, dest_img_folder, dest_lbl_folder):
|
||||||
|
copied_images = 0
|
||||||
|
copied_labels = 0
|
||||||
|
|
||||||
|
with open(LOG_FILE, "a") as log:
|
||||||
|
for img_file in file_list:
|
||||||
|
img_path = os.path.join(image_folder, img_file)
|
||||||
|
label_file = os.path.splitext(img_file)[0] + ".txt"
|
||||||
|
label_path = os.path.join(label_folder, label_file)
|
||||||
|
|
||||||
|
if os.path.exists(img_path) and os.path.exists(label_path):
|
||||||
|
shutil.copy(img_path, os.path.join(dest_img_folder, img_file))
|
||||||
|
shutil.copy(label_path, os.path.join(dest_lbl_folder, label_file))
|
||||||
|
copied_images += 1
|
||||||
|
copied_labels += 1
|
||||||
|
else:
|
||||||
|
log.write(f"[{datetime.datetime.now()}] ⚠️ Missing label for image: {img_file}\n")
|
||||||
|
shutil.copy(img_path, os.path.join(nolabel_img_folder, img_file))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return copied_images, copied_labels
|
||||||
|
|
||||||
|
|
||||||
|
def train_yolo_model(pretrained_model: str, dataset_folder: str,project_name: str, name: str, epochs: int = 50, batch_size: int = 16, img_size: int = 640, lr: float = 0.001):
|
||||||
|
dataset_yaml = os.path.join(dataset_folder, "data.yaml")
|
||||||
|
|
||||||
|
if not os.path.exists(dataset_yaml):
|
||||||
|
raise FileNotFoundError(f"⚠️ Not found file {dataset_yaml}. Plases check datasets!")
|
||||||
|
|
||||||
|
if not os.path.exists(pretrained_model):
|
||||||
|
log_message(f"⚠️ Model not found {pretrained_model}. Start train with 'yolov8n.pt'.")
|
||||||
|
pretrained_model = "yolov8n.pt"
|
||||||
|
|
||||||
|
# 🚀 Tạo model YOLOv8 và load model đã train trước đó
|
||||||
|
model = YOLO(pretrained_model)
|
||||||
|
|
||||||
|
# 🔥 Bắt đầu training
|
||||||
|
model.train(
|
||||||
|
data=dataset_yaml,
|
||||||
|
epochs=epochs,
|
||||||
|
batch=batch_size,
|
||||||
|
imgsz=img_size,
|
||||||
|
optimizer="AdamW",
|
||||||
|
lr0=lr,
|
||||||
|
weight_decay=0.0005,
|
||||||
|
patience=10,
|
||||||
|
verbose=True,
|
||||||
|
project=project_name,
|
||||||
|
name=name
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(LOG_FILE, "a") as log:
|
||||||
|
log.write(f"\n[{datetime.datetime.now()}] ✅ Train completed\n")
|
||||||
|
|
||||||
|
call_reload_model_api()
|
||||||
|
clear_images_source()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 🚀 Copy files
|
||||||
|
train_copied_imgs, train_copied_lbls = move_files(train_files, train_img_folder, train_lbl_folder)
|
||||||
|
val_copied_imgs, val_copied_lbls = move_files(val_files, val_img_folder, val_lbl_folder)
|
||||||
|
|
||||||
|
# Create yml file
|
||||||
|
create_dataset_yaml(dataset_folder, class_names)
|
||||||
|
|
||||||
|
|
||||||
|
# 🏁 Log summary
|
||||||
|
with open(LOG_FILE, "a") as log:
|
||||||
|
log.write(f"\n[{datetime.datetime.now()}] ✅ Dataset split completed\n")
|
||||||
|
log.write(f"Source folder: {image_folder}\n")
|
||||||
|
log.write(f"Dataset saved in: {dataset_folder}\n")
|
||||||
|
log.write(f"Planned Train: {len(train_files)} images, Val: {len(val_files)} images\n")
|
||||||
|
log.write(f"Actual Train: {train_copied_imgs} images, {train_copied_lbls} labels\n")
|
||||||
|
log.write(f"Actual Val: {val_copied_imgs} images, {val_copied_lbls} labels\n")
|
||||||
|
|
||||||
|
|
||||||
|
if(train_copied_imgs <=0 or train_copied_lbls <= 0 or val_copied_imgs <=0 or val_copied_lbls <=0):
|
||||||
|
with open(LOG_FILE, "a") as log:
|
||||||
|
log.write(f"\n[{datetime.datetime.now()}] ❌ Data not qualified\n")
|
||||||
|
log.write("=" * 50 + "\n")
|
||||||
|
else:
|
||||||
|
train_yolo_model(pretrained_model = get_latest_model(
|
||||||
|
trained_model_folder=TRAINED_MODEL_FOLDER,
|
||||||
|
default_model=PRETRAINED_MODEL),
|
||||||
|
dataset_folder = dataset_folder, epochs = 2, name=today_str, project_name=model_folder_name
|
||||||
|
)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
VENV_PATH="/home/work/projects/YoLo/src/server/venv"
|
||||||
|
PROJECT_DIR="/home/work/projects/YoLo/src/server"
|
||||||
|
TRAIN_SCRIPT="train.py"
|
||||||
|
LOCK_FILE="/tmp/train_model.lock"
|
||||||
|
LOG_FILE="$PROJECT_DIR/train.log"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "🚀 $(date) - Start running train.py"
|
||||||
|
|
||||||
|
# Chuyển vào thư mục làm việc
|
||||||
|
cd "$PROJECT_DIR" || { echo "❌ Failed to cd into $PROJECT_DIR"; exit 1; }
|
||||||
|
|
||||||
|
echo "📌 Current directory: $(pwd)"
|
||||||
|
|
||||||
|
# Kích hoạt môi trường ảo
|
||||||
|
source "$VENV_PATH/bin/activate"
|
||||||
|
|
||||||
|
# Chạy script training
|
||||||
|
python3 "$TRAIN_SCRIPT"
|
||||||
|
|
||||||
|
echo "✅ $(date) - Training completed!"
|
||||||
|
} 2>&1 | flock -n "$LOCK_FILE" tee -a "$LOG_FILE"
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
0
|
|
||||||
1
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
0 0.304375 0.714345 0.043750 0.032527
|
|
||||||
1 0.380937 0.706422 0.095625 0.031693
|
|
||||||
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 251 KiB After Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 468 KiB After Width: | Height: | Size: 468 KiB |
|
Before Width: | Height: | Size: 466 KiB After Width: | Height: | Size: 466 KiB |
|
Before Width: | Height: | Size: 370 KiB After Width: | Height: | Size: 370 KiB |
|
Before Width: | Height: | Size: 389 KiB After Width: | Height: | Size: 389 KiB |
|
Before Width: | Height: | Size: 388 KiB After Width: | Height: | Size: 388 KiB |
|
Before Width: | Height: | Size: 589 KiB After Width: | Height: | Size: 589 KiB |
|
Before Width: | Height: | Size: 479 KiB After Width: | Height: | Size: 479 KiB |
|
Before Width: | Height: | Size: 488 KiB After Width: | Height: | Size: 488 KiB |