update ui and code #1
			
				
			
		
		
		
	| 
						 | 
					@ -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  |