Compare commits
	
		
			20 Commits
		
	
	
		
			zelda.upda
			...
			main
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								 | 
						6cca796646 | |
| 
							
							
								 | 
						a3e7e29897 | |
| 
							
							
								 | 
						052e385473 | |
| 
							
							
								 | 
						f770973b5e | |
| 
							
							
								 | 
						c1214662a3 | |
| 
							
							
								 | 
						db1f306145 | |
| 
							
							
								 | 
						fef22a3cf6 | |
| 
							
							
								
								 | 
						6ef757e8a2 | |
| 
							
							
								 | 
						911c4d590c | |
| 
							
							
								
								 | 
						f959cb3a00 | |
| 
							
							
								 | 
						60a165e9a3 | |
| 
							
							
								
								 | 
						3c9dfa427b | |
| 
							
							
								 | 
						8453a18466 | |
| 
							
							
								
								 | 
						c945db4c68 | |
| 
							
							
								 | 
						11769c850f | |
| 
							
							
								
								 | 
						9c6f0ee3c1 | |
| 
							
							
								 | 
						f8da45b508 | |
| 
							
							
								
								 | 
						a2f422f67d | |
| 
							
							
								 | 
						3e1b5d1ba3 | |
| 
							
							
								
								 | 
						00c356268d | 
| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
PUBLIC_URL=
 | 
			
		||||
REACT_APP_API_URL="http://127.0.0.1:5000"
 | 
			
		||||
 | 
			
		||||
REACT_APP_DEFAULTAUTH=fake
 | 
			
		||||
 | 
			
		||||
REACT_APP_API_KEY=
 | 
			
		||||
REACT_APP_AUTHDOMAIN=
 | 
			
		||||
REACT_APP_DATABASEURL=
 | 
			
		||||
REACT_APP_PROJECTID=
 | 
			
		||||
REACT_APP_STORAGEBUCKET=
 | 
			
		||||
REACT_APP_MESSAGINGSENDERID=
 | 
			
		||||
REACT_APP_APPID=
 | 
			
		||||
REACT_APP_MEASUREMENTID=
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -28,4 +28,7 @@ yarn-error.log*
 | 
			
		|||
/src/server/model_datasets
 | 
			
		||||
*.log
 | 
			
		||||
src/server/venv/*
 | 
			
		||||
__pycache__/
 | 
			
		||||
__pycache__/
 | 
			
		||||
 | 
			
		||||
my_database.db
 | 
			
		||||
latest_detect
 | 
			
		||||
							
								
								
									
										71
									
								
								README.md
								
								
								
								
							
							
						
						
									
										71
									
								
								README.md
								
								
								
								
							| 
						 | 
				
			
			@ -1,70 +1,29 @@
 | 
			
		|||
# Getting Started with Create React App
 | 
			
		||||
## Front-end server
 | 
			
		||||
 | 
			
		||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
 | 
			
		||||
### `npm install`
 | 
			
		||||
 | 
			
		||||
## Available Scripts
 | 
			
		||||
 | 
			
		||||
In the project directory, you can run:
 | 
			
		||||
Local use `env.dev`
 | 
			
		||||
 | 
			
		||||
### `npm start`
 | 
			
		||||
 | 
			
		||||
Runs the app in the development mode.\
 | 
			
		||||
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
 | 
			
		||||
## Back-end server
 | 
			
		||||
 | 
			
		||||
The page will reload when you make changes.\
 | 
			
		||||
You may also see any lint errors in the console.
 | 
			
		||||
### /src/server
 | 
			
		||||
 | 
			
		||||
### `npm test`
 | 
			
		||||
### `python main.py`
 | 
			
		||||
 | 
			
		||||
Launches the test runner in the interactive watch mode.\
 | 
			
		||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
 | 
			
		||||
Server local run at `localhost:5000`
 | 
			
		||||
 | 
			
		||||
### `npm run build`
 | 
			
		||||
### `Linus/Mac`
 | 
			
		||||
 | 
			
		||||
Builds the app for production to the `build` folder.\
 | 
			
		||||
It correctly bundles React in production mode and optimizes the build for the best performance.
 | 
			
		||||
install package
 | 
			
		||||
 | 
			
		||||
The build is minified and the filenames include the hashes.\
 | 
			
		||||
Your app is ready to be deployed!
 | 
			
		||||
### `Windows`
 | 
			
		||||
 | 
			
		||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
 | 
			
		||||
install package.\
 | 
			
		||||
Don't need to set permission, file `utils.py` command import `pwd`, `grp` and code function `ensure_correct_permissions`
 | 
			
		||||
 | 
			
		||||
### `npm run eject`
 | 
			
		||||
### Database sqlite `my_database.db`
 | 
			
		||||
 | 
			
		||||
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
 | 
			
		||||
 | 
			
		||||
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
 | 
			
		||||
 | 
			
		||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
 | 
			
		||||
 | 
			
		||||
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
 | 
			
		||||
 | 
			
		||||
## Learn More
 | 
			
		||||
 | 
			
		||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
 | 
			
		||||
 | 
			
		||||
To learn React, check out the [React documentation](https://reactjs.org/).
 | 
			
		||||
 | 
			
		||||
### Code Splitting
 | 
			
		||||
 | 
			
		||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
 | 
			
		||||
 | 
			
		||||
### Analyzing the Bundle Size
 | 
			
		||||
 | 
			
		||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
 | 
			
		||||
 | 
			
		||||
### Making a Progressive Web App
 | 
			
		||||
 | 
			
		||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
 | 
			
		||||
 | 
			
		||||
### Advanced Configuration
 | 
			
		||||
 | 
			
		||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
 | 
			
		||||
 | 
			
		||||
### Deployment
 | 
			
		||||
 | 
			
		||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
 | 
			
		||||
 | 
			
		||||
### `npm run build` fails to minify
 | 
			
		||||
 | 
			
		||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
 | 
			
		||||
When server start with run check create table `labels`.\
 | 
			
		||||
If don't exist will create and add default 2 rows `logo.cisco`, `barcode.SN`
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,21 +1,18 @@
 | 
			
		|||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="utf-8" />
 | 
			
		||||
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1" />
 | 
			
		||||
    <meta name="theme-color" content="#000000" />
 | 
			
		||||
    <meta
 | 
			
		||||
      name="description"
 | 
			
		||||
      content="Web site created using create-react-app"
 | 
			
		||||
    />
 | 
			
		||||
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
 | 
			
		||||
    <!--
 | 
			
		||||
	<head>
 | 
			
		||||
		<meta charset="utf-8" />
 | 
			
		||||
		<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
 | 
			
		||||
		<meta name="viewport" content="width=device-width, initial-scale=1" />
 | 
			
		||||
		<meta name="theme-color" content="#000000" />
 | 
			
		||||
		<meta name="description" content="Web site created using create-react-app" />
 | 
			
		||||
		<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
 | 
			
		||||
		<!--
 | 
			
		||||
      manifest.json provides metadata used when your web app is installed on a
 | 
			
		||||
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
 | 
			
		||||
    -->
 | 
			
		||||
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
 | 
			
		||||
    <!--
 | 
			
		||||
		<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
 | 
			
		||||
		<!--
 | 
			
		||||
      Notice the use of %PUBLIC_URL% in the tags above.
 | 
			
		||||
      It will be replaced with the URL of the `public` folder during the build.
 | 
			
		||||
      Only files inside the `public` folder can be referenced from the HTML.
 | 
			
		||||
| 
						 | 
				
			
			@ -24,12 +21,12 @@
 | 
			
		|||
      work correctly both with client-side routing and a non-root public URL.
 | 
			
		||||
      Learn how to configure a non-root public URL by running `npm run build`.
 | 
			
		||||
    -->
 | 
			
		||||
    <title>React App</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <noscript>You need to enable JavaScript to run this app.</noscript>
 | 
			
		||||
    <div id="root"></div>
 | 
			
		||||
    <!--
 | 
			
		||||
		<title>AI Tool</title>
 | 
			
		||||
	</head>
 | 
			
		||||
	<body>
 | 
			
		||||
		<noscript>You need to enable JavaScript to run this app.</noscript>
 | 
			
		||||
		<div id="root"></div>
 | 
			
		||||
		<!--
 | 
			
		||||
      This HTML file is a template.
 | 
			
		||||
      If you open it directly in the browser, you will see an empty page.
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -39,5 +36,5 @@
 | 
			
		|||
      To begin the development, run `npm start` or `yarn start`.
 | 
			
		||||
      To create a production bundle, use `npm run build` or `yarn build`.
 | 
			
		||||
    -->
 | 
			
		||||
  </body>
 | 
			
		||||
	</body>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										454
									
								
								src/App.js
								
								
								
								
							
							
						
						
									
										454
									
								
								src/App.js
								
								
								
								
							| 
						 | 
				
			
			@ -1,24 +1,39 @@
 | 
			
		|||
import '@mantine/core/styles.css';
 | 
			
		||||
import "@mantine/core/styles.css";
 | 
			
		||||
 | 
			
		||||
import './App.css';
 | 
			
		||||
import { Dropzone } from './pages/components/Dropzone';
 | 
			
		||||
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';
 | 
			
		||||
import { ActionIcon, AppShell, Box, Burger, Button, Group, LoadingOverlay, Modal, Progress, ScrollArea, Tabs, Text, Tooltip } from "@mantine/core";
 | 
			
		||||
import { useDisclosure, useHotkeys } from "@mantine/hooks";
 | 
			
		||||
import { notifications } from "@mantine/notifications";
 | 
			
		||||
import {
 | 
			
		||||
	IconChevronLeft,
 | 
			
		||||
	IconChevronRight,
 | 
			
		||||
	IconEdit,
 | 
			
		||||
	IconImageInPicture,
 | 
			
		||||
	IconPlus,
 | 
			
		||||
	IconRefreshDot,
 | 
			
		||||
	IconReportAnalytics,
 | 
			
		||||
	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";
 | 
			
		||||
import ModalLabel from "./pages/components/ModalLabel";
 | 
			
		||||
 | 
			
		||||
function App() {
 | 
			
		||||
	const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();
 | 
			
		||||
	const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
 | 
			
		||||
 | 
			
		||||
	const [clickData, setClickData] = useState(null);
 | 
			
		||||
	const [listLabel, setListLabel] = useState([]);
 | 
			
		||||
	const [isLoading, setIsLoading] = useState(false);
 | 
			
		||||
	const [opened, { open, close }] = useDisclosure(false);
 | 
			
		||||
	const [dataLabel, setDataLabel] = useState({});
 | 
			
		||||
	const [openConfirm, setOpenConfirm] = useState(false);
 | 
			
		||||
 | 
			
		||||
	const { images, clearImagesDetected, appendImageDetect, setImageDetected } = useImagesDetected();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -27,13 +42,28 @@ function App() {
 | 
			
		|||
	const imageLabelRef = useRef();
 | 
			
		||||
	const dropzoneRef = useRef();
 | 
			
		||||
 | 
			
		||||
	const fetchLabels = async () => {
 | 
			
		||||
		try {
 | 
			
		||||
			const response = await fetch(`${process.env.REACT_APP_API_URL}/labels`);
 | 
			
		||||
			// const response = await fetch(`http://127.0.0.1:5000/labels`);
 | 
			
		||||
			const result = await response.json();
 | 
			
		||||
			setListLabel(result);
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			console.log(error);
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	useEffect(() => {
 | 
			
		||||
		fetchLabels();
 | 
			
		||||
	}, []);
 | 
			
		||||
 | 
			
		||||
	const handleClear = () => {
 | 
			
		||||
		// const result = window.confirm('Are you want to clear ?');
 | 
			
		||||
 | 
			
		||||
		const ID_NOTI = 'handle-clear';
 | 
			
		||||
		const ID_NOTI = "handle-clear";
 | 
			
		||||
		notifications.show({
 | 
			
		||||
			id: ID_NOTI,
 | 
			
		||||
			title: 'Are you want to clear ?',
 | 
			
		||||
			title: "Are you want to clear ?",
 | 
			
		||||
			message: (
 | 
			
		||||
				<Box className="flex items-center gap-2 justify-end">
 | 
			
		||||
					<Button
 | 
			
		||||
| 
						 | 
				
			
			@ -55,11 +85,11 @@ function App() {
 | 
			
		|||
	};
 | 
			
		||||
 | 
			
		||||
	const handleDeleteImage = (data, index) => {
 | 
			
		||||
		const ID_NOTI = 'handle-delete-image';
 | 
			
		||||
		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 response = await fetch(`${process.env.REACT_APP_API_URL}/delete/${data.image_name}`, { method: "DELETE" });
 | 
			
		||||
 | 
			
		||||
				const result = await response.json();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -76,7 +106,7 @@ function App() {
 | 
			
		|||
 | 
			
		||||
		notifications.show({
 | 
			
		||||
			id: ID_NOTI,
 | 
			
		||||
			title: 'Are you want to delete ?',
 | 
			
		||||
			title: "Are you want to delete ?",
 | 
			
		||||
			message: (
 | 
			
		||||
				<Box className="flex items-center gap-2 justify-end">
 | 
			
		||||
					<Button onClick={deleteImage} color="red" size="xs">
 | 
			
		||||
| 
						 | 
				
			
			@ -132,10 +162,10 @@ function App() {
 | 
			
		|||
			processFiles: 0,
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		const worker = new Worker(new URL('./workers/detect-worker.js', import.meta.url));
 | 
			
		||||
		const worker = new Worker(new URL("./workers/detect-worker.js", import.meta.url));
 | 
			
		||||
 | 
			
		||||
		worker.onmessage = (event) => {
 | 
			
		||||
			if (event.data.status === 'success') {
 | 
			
		||||
			if (event.data.status === "success") {
 | 
			
		||||
				if (event.data?.result) {
 | 
			
		||||
					setImageDetected(event.data.result);
 | 
			
		||||
					setProgress({
 | 
			
		||||
| 
						 | 
				
			
			@ -151,11 +181,11 @@ function App() {
 | 
			
		|||
					}
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				console.error('⚠️ Lỗi:', event.data.error);
 | 
			
		||||
				console.error("⚠️ Lỗi:", event.data.error);
 | 
			
		||||
			}
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		worker.postMessage({ action: 'processImages', files });
 | 
			
		||||
		worker.postMessage({ action: "processImages", files });
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const handlePrev = () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -188,132 +218,294 @@ function App() {
 | 
			
		|||
		return true;
 | 
			
		||||
	}, [images, clickData]);
 | 
			
		||||
 | 
			
		||||
	const handleTraining = async () => {
 | 
			
		||||
		try {
 | 
			
		||||
			setIsLoading(true);
 | 
			
		||||
			setClickData(null);
 | 
			
		||||
			clearImagesDetected();
 | 
			
		||||
			const response = await fetch(`${process.env.REACT_APP_API_URL}/train`);
 | 
			
		||||
			// const response = await fetch(`http://127.0.0.1:5000/train`);
 | 
			
		||||
			const result = await response.json();
 | 
			
		||||
			if (result?.error) {
 | 
			
		||||
				notifications.show({
 | 
			
		||||
					title: "Failed",
 | 
			
		||||
					message: result?.error,
 | 
			
		||||
					color: "red",
 | 
			
		||||
				});
 | 
			
		||||
			} else
 | 
			
		||||
				notifications.show({
 | 
			
		||||
					title: "Success",
 | 
			
		||||
					message: `Training successfully!`,
 | 
			
		||||
					color: "green",
 | 
			
		||||
				});
 | 
			
		||||
			setIsLoading(false);
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			console.log(error);
 | 
			
		||||
			setIsLoading(false);
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const handleSave = async (value) => {
 | 
			
		||||
		try {
 | 
			
		||||
			setIsLoading(true);
 | 
			
		||||
			const url = `${process.env.REACT_APP_API_URL}/label/${value?.id ? "update" : "add"}`;
 | 
			
		||||
			// const url = `http://127.0.0.1:5000/label/${value?.id ? "update" : "add"}`;
 | 
			
		||||
			const response = await fetch(url, {
 | 
			
		||||
				method: "POST",
 | 
			
		||||
				headers: {
 | 
			
		||||
					"Content-Type": "application/json",
 | 
			
		||||
				},
 | 
			
		||||
				body: JSON.stringify(value),
 | 
			
		||||
			});
 | 
			
		||||
			const result = await response.json();
 | 
			
		||||
			if (!result?.success) {
 | 
			
		||||
				notifications.show({
 | 
			
		||||
					title: "Failed",
 | 
			
		||||
					message: result?.error,
 | 
			
		||||
					color: "red",
 | 
			
		||||
				});
 | 
			
		||||
			} else
 | 
			
		||||
				notifications.show({
 | 
			
		||||
					title: "Success",
 | 
			
		||||
					message: value?.id ? `Update label successfully!` : `Add label successfully!`,
 | 
			
		||||
					color: "green",
 | 
			
		||||
				});
 | 
			
		||||
			fetchLabels();
 | 
			
		||||
			setIsLoading(false);
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			console.log(error);
 | 
			
		||||
			setIsLoading(false);
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	useHotkeys([
 | 
			
		||||
		['ArrowRight', handleNext],
 | 
			
		||||
		['ArrowLeft', handlePrev],
 | 
			
		||||
		["ArrowRight", handleNext],
 | 
			
		||||
		["ArrowLeft", handlePrev],
 | 
			
		||||
	]);
 | 
			
		||||
 | 
			
		||||
	return (
 | 
			
		||||
		<div style={{ position: 'relative' }}>
 | 
			
		||||
		<div style={{ position: "relative" }}>
 | 
			
		||||
			<AppShell
 | 
			
		||||
				header={{ height: 60 }}
 | 
			
		||||
				navbar={{
 | 
			
		||||
					width: 300,
 | 
			
		||||
					breakpoint: 'sm',
 | 
			
		||||
					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);
 | 
			
		||||
												}}
 | 
			
		||||
											/>
 | 
			
		||||
										);
 | 
			
		||||
									})}
 | 
			
		||||
				<Tabs color="teal" defaultValue="detect">
 | 
			
		||||
					<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">
 | 
			
		||||
								<Tabs.List>
 | 
			
		||||
									<Tabs.Tab value="detect">
 | 
			
		||||
										<span style={{ fontSize: 20 }}>Detect</span>
 | 
			
		||||
									</Tabs.Tab>
 | 
			
		||||
									<Tabs.Tab value="config">
 | 
			
		||||
										<span style={{ fontSize: 20 }}>Config label</span>
 | 
			
		||||
									</Tabs.Tab>
 | 
			
		||||
								</Tabs.List>
 | 
			
		||||
							</Box>
 | 
			
		||||
 | 
			
		||||
							{images.length <= 0 && (
 | 
			
		||||
								<Box className="flex items-center justify-center">
 | 
			
		||||
									<span>No images to process</span>
 | 
			
		||||
							<Box className="px-4 flex items-center gap-4 w-fit">
 | 
			
		||||
								{/* <Button disabled={isLoading} onClick={() => setOpenConfirm(true)} leftSection={<IconReportAnalytics size={14} />} color="#caa32c">
 | 
			
		||||
									Training
 | 
			
		||||
								</Button> */}
 | 
			
		||||
								<Button disabled={!clickData || isLoading} onClick={handleClearSelect} leftSection={<IconRefreshDot size={14} />} color="orange">
 | 
			
		||||
									Reset select
 | 
			
		||||
								</Button>
 | 
			
		||||
								<SaveButton
 | 
			
		||||
									disabled={isLoading}
 | 
			
		||||
									onSaved={(data) => {
 | 
			
		||||
										handleNext();
 | 
			
		||||
									}}
 | 
			
		||||
									currentData={clickData}
 | 
			
		||||
									imageLabelRef={imageLabelRef}
 | 
			
		||||
								/>
 | 
			
		||||
							</Box>
 | 
			
		||||
						</Box>
 | 
			
		||||
					</AppShell.Header>
 | 
			
		||||
					<Tabs.Panel value="detect">
 | 
			
		||||
						<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 || isLoading} onClick={handleClear} leftSection={<IconTrash size={14} />} color="red">
 | 
			
		||||
										Clear
 | 
			
		||||
									</Button>
 | 
			
		||||
									<Button fullWidth disabled={images.length || isLoading} onClick={handleAddImages} leftSection={<IconImageInPicture size={14} />}>
 | 
			
		||||
										Add images
 | 
			
		||||
									</Button>
 | 
			
		||||
								</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 />
 | 
			
		||||
								<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>
 | 
			
		||||
						)}
 | 
			
		||||
					</Box>
 | 
			
		||||
				</AppShell.Navbar>
 | 
			
		||||
				<AppShell.Main style={{ position: 'relative' }}>
 | 
			
		||||
					{clickData && <ImageLabel ref={imageLabelRef} data={clickData} />}
 | 
			
		||||
						</AppShell.Navbar>
 | 
			
		||||
						<AppShell.Main style={{ position: "relative" }}>
 | 
			
		||||
							{clickData && <ImageLabel ref={imageLabelRef} data={clickData} listLabel={listLabel} />}
 | 
			
		||||
 | 
			
		||||
					<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',
 | 
			
		||||
								});
 | 
			
		||||
							<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>
 | 
			
		||||
					</Tabs.Panel>
 | 
			
		||||
					<Tabs.Panel value="config">
 | 
			
		||||
						<AppShell.Main style={{ position: "relative", paddingLeft: 0 }}>
 | 
			
		||||
							<ScrollArea h={780} className="pb-5 flex-1">
 | 
			
		||||
								<Box className="flex justify-end pe-4">
 | 
			
		||||
									<Button disabled={isLoading} leftSection={<IconPlus size={14} />} color="#1a9b20" onClick={open}>
 | 
			
		||||
										Add
 | 
			
		||||
									</Button>
 | 
			
		||||
								</Box>
 | 
			
		||||
								<div className="flex justify-center items-center">
 | 
			
		||||
									<Box w={600} className="mt-4">
 | 
			
		||||
										{listLabel.length > 0 &&
 | 
			
		||||
											listLabel?.map((item, index) => {
 | 
			
		||||
												return (
 | 
			
		||||
													<Box w={600} key={index} className="flex items-center justify-between mb-4">
 | 
			
		||||
														<Text>{item?.name}</Text>
 | 
			
		||||
														<div className="flex items-center justify-between gap-4">
 | 
			
		||||
															<Button
 | 
			
		||||
																disabled={isLoading}
 | 
			
		||||
																leftSection={<IconEdit size={14} />}
 | 
			
		||||
																color="#0659a5"
 | 
			
		||||
																onClick={() => {
 | 
			
		||||
																	open();
 | 
			
		||||
																	setDataLabel(item);
 | 
			
		||||
																}}>
 | 
			
		||||
																Edit
 | 
			
		||||
															</Button>
 | 
			
		||||
														</div>
 | 
			
		||||
													</Box>
 | 
			
		||||
												);
 | 
			
		||||
											})}
 | 
			
		||||
									</Box>
 | 
			
		||||
								</div>
 | 
			
		||||
							</ScrollArea>
 | 
			
		||||
						</AppShell.Main>
 | 
			
		||||
					</Tabs.Panel>
 | 
			
		||||
				</Tabs>
 | 
			
		||||
				<LoadingOverlay
 | 
			
		||||
					visible={isLoading}
 | 
			
		||||
					zIndex={10}
 | 
			
		||||
					overlayProps={{ radius: "sm", blur: 2 }}
 | 
			
		||||
					loaderProps={{
 | 
			
		||||
						size: "lg",
 | 
			
		||||
					}}
 | 
			
		||||
				/>
 | 
			
		||||
				<ModalLabel
 | 
			
		||||
					opened={opened}
 | 
			
		||||
					open={open}
 | 
			
		||||
					close={() => {
 | 
			
		||||
						close();
 | 
			
		||||
						setDataLabel({});
 | 
			
		||||
					}}
 | 
			
		||||
					data={dataLabel}
 | 
			
		||||
					onSave={() => {
 | 
			
		||||
						close();
 | 
			
		||||
						setDataLabel({});
 | 
			
		||||
						setIsLoading(true);
 | 
			
		||||
						handleSave(dataLabel);
 | 
			
		||||
						setTimeout(() => {
 | 
			
		||||
							setIsLoading(false);
 | 
			
		||||
						}, 2000);
 | 
			
		||||
					}}
 | 
			
		||||
					setData={setDataLabel}
 | 
			
		||||
				/>
 | 
			
		||||
 | 
			
		||||
				<Modal opened={openConfirm} onClose={() => setOpenConfirm(false)} title={"Confirm"}>
 | 
			
		||||
					<div style={{ padding: 20 }} className="flex flex-col gap-4 justify-center items-center">
 | 
			
		||||
						<div>Do you want to run training?</div>
 | 
			
		||||
						<div>Maybe you need to save some data to train!</div>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div style={{ padding: 20 }} className="flex justify-center gap-4">
 | 
			
		||||
						<Button
 | 
			
		||||
							onClick={() => {
 | 
			
		||||
								setOpenConfirm(false);
 | 
			
		||||
							}}>
 | 
			
		||||
							Cancel
 | 
			
		||||
						</Button>
 | 
			
		||||
						<Button
 | 
			
		||||
							onClick={() => {
 | 
			
		||||
								setOpenConfirm(false);
 | 
			
		||||
								handleTraining();
 | 
			
		||||
							}}
 | 
			
		||||
							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>
 | 
			
		||||
							color="#1a9b20">
 | 
			
		||||
							Confirm
 | 
			
		||||
						</Button>
 | 
			
		||||
					</div>
 | 
			
		||||
				</Modal>
 | 
			
		||||
			</AppShell>
 | 
			
		||||
		</div>
 | 
			
		||||
	);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,16 +1,16 @@
 | 
			
		|||
import React, { useEffect, useRef, useState, forwardRef, useImperativeHandle, memo } from 'react';
 | 
			
		||||
import { Box, LoadingOverlay } from '@mantine/core';
 | 
			
		||||
import PointsBlur from './PointsBlur';
 | 
			
		||||
import { generateImageUrl } from '../../ultils';
 | 
			
		||||
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 ImageLabel = forwardRef(({ data, listLabel }, ref) => {
 | 
			
		||||
	const [imageSrc, setImageSrc] = useState(null);
 | 
			
		||||
	const blurredRegions = useRef([]);
 | 
			
		||||
	const [loaded, setLoaded] = useState(false);
 | 
			
		||||
	const [reloading, setReloading] = useState(false);
 | 
			
		||||
	const [listLabel, setListLabel] = useState([]);
 | 
			
		||||
	// const [listLabel, setListLabel] = useState([]);
 | 
			
		||||
	const [infoImage, setInfoImage] = useState({});
 | 
			
		||||
	const [imageName, setImageName] = useState('');
 | 
			
		||||
	const [imageName, setImageName] = useState("");
 | 
			
		||||
 | 
			
		||||
	// Expose blurredRegions cho component cha
 | 
			
		||||
	useImperativeHandle(ref, () => ({
 | 
			
		||||
| 
						 | 
				
			
			@ -33,19 +33,24 @@ const ImageLabel = forwardRef(({ data }, ref) => {
 | 
			
		|||
			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 },
 | 
			
		||||
				]);
 | 
			
		||||
				blurredRegions.current = data?.points?.map((pre) => {
 | 
			
		||||
					const arrLabel = pre?.label?.split(".");
 | 
			
		||||
					const label = arrLabel?.length > 1 ? arrLabel[1] : arrLabel[0];
 | 
			
		||||
					const subLabel = arrLabel?.length > 1 ? arrLabel[0] : null;
 | 
			
		||||
					return [
 | 
			
		||||
						{ x: pre.x1, y: pre.y1, label: label, subLabel: subLabel },
 | 
			
		||||
						{ x: pre.x2, y: pre.y1, label: label, subLabel: subLabel },
 | 
			
		||||
						{ x: pre.x2, y: pre.y2, label: label, subLabel: subLabel },
 | 
			
		||||
						{ x: pre.x1, y: pre.y2, label: label, subLabel: subLabel },
 | 
			
		||||
					];
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
			setListLabel(
 | 
			
		||||
				Object.entries(data?.labels).map(([id, name]) => ({
 | 
			
		||||
					id: Number(id),
 | 
			
		||||
					name,
 | 
			
		||||
				})),
 | 
			
		||||
			);
 | 
			
		||||
			// 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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,72 @@
 | 
			
		|||
import { Modal, Button, TextInput } from "@mantine/core";
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
 | 
			
		||||
const ModalLabel = ({ opened, open, close, data, onSave, setData }) => {
 | 
			
		||||
	const [openConfirm, setOpenConfirm] = useState(false);
 | 
			
		||||
	const [isError, setIsError] = useState(false);
 | 
			
		||||
 | 
			
		||||
	useEffect(() => {
 | 
			
		||||
		if (!opened) {
 | 
			
		||||
			setIsError(false);
 | 
			
		||||
			setOpenConfirm(false);
 | 
			
		||||
		}
 | 
			
		||||
	}, [opened]);
 | 
			
		||||
 | 
			
		||||
	return (
 | 
			
		||||
		<>
 | 
			
		||||
			<Modal opened={opened} onClose={close} title={data?.id ? "Edit Label" : "Add Label"} centered>
 | 
			
		||||
				<div style={{ padding: 20 }} className="flex justify-center">
 | 
			
		||||
					<TextInput
 | 
			
		||||
						label="Input label"
 | 
			
		||||
						withAsterisk
 | 
			
		||||
						placeholder="Input placeholder"
 | 
			
		||||
						value={data?.name || ""}
 | 
			
		||||
						onChange={(event) => {
 | 
			
		||||
							if (event.currentTarget.value) setIsError(false);
 | 
			
		||||
							setData((pre) => ({ ...pre, name: event.currentTarget.value }));
 | 
			
		||||
						}}
 | 
			
		||||
						style={{ width: 300 }}
 | 
			
		||||
						error={isError ? "Invalid label" : ""}
 | 
			
		||||
					/>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div style={{ padding: 20 }} className="flex justify-center gap-4">
 | 
			
		||||
					<Button onClick={close}>Close</Button>
 | 
			
		||||
					<Button
 | 
			
		||||
						onClick={() => {
 | 
			
		||||
							if (!data?.name) {
 | 
			
		||||
								setIsError(true);
 | 
			
		||||
								return;
 | 
			
		||||
							}
 | 
			
		||||
							if (data?.id) {
 | 
			
		||||
								onSave(data);
 | 
			
		||||
								close();
 | 
			
		||||
							} else setOpenConfirm(true);
 | 
			
		||||
						}}
 | 
			
		||||
						color="#1a9b20">
 | 
			
		||||
						Save
 | 
			
		||||
					</Button>
 | 
			
		||||
				</div>
 | 
			
		||||
			</Modal>
 | 
			
		||||
 | 
			
		||||
			<Modal opened={openConfirm} onClose={() => setOpenConfirm(false)} title={"Confirm"}>
 | 
			
		||||
				<div style={{ padding: 20 }} className="flex flex-col gap-4 justify-center items-center">
 | 
			
		||||
					<div>Do you want to add new label?</div>
 | 
			
		||||
					<div style={{ color: "#f54e4e" }}>Label cannot delete after add!</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div style={{ padding: 20 }} className="flex justify-center gap-4">
 | 
			
		||||
					<Button
 | 
			
		||||
						onClick={() => {
 | 
			
		||||
							setOpenConfirm(false);
 | 
			
		||||
						}}>
 | 
			
		||||
						Cancel
 | 
			
		||||
					</Button>
 | 
			
		||||
					<Button onClick={onSave} color="#1a9b20">
 | 
			
		||||
						Confirm
 | 
			
		||||
					</Button>
 | 
			
		||||
				</div>
 | 
			
		||||
			</Modal>
 | 
			
		||||
		</>
 | 
			
		||||
	);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default ModalLabel;
 | 
			
		||||
| 
						 | 
				
			
			@ -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 points = useRef([]);
 | 
			
		||||
| 
						 | 
				
			
			@ -12,7 +12,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
			
		|||
 | 
			
		||||
	useEffect(() => {
 | 
			
		||||
		const canvas = canvasRef.current;
 | 
			
		||||
		const ctx = canvas.getContext('2d');
 | 
			
		||||
		const ctx = canvas.getContext("2d");
 | 
			
		||||
		const image = new Image();
 | 
			
		||||
 | 
			
		||||
		image.onload = () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -23,8 +23,12 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
			
		|||
			setInfoImage({ width: image.width, height: image.height });
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		image.crossOrigin = 'anonymous';
 | 
			
		||||
		image.crossOrigin = "anonymous";
 | 
			
		||||
		image.src = imageSrc;
 | 
			
		||||
		setInfo({});
 | 
			
		||||
		setIsOpen(false);
 | 
			
		||||
		points.current = []; // Clear points after blurring
 | 
			
		||||
		setReloading((pre) => !pre);
 | 
			
		||||
	}, [imageSrc]);
 | 
			
		||||
 | 
			
		||||
	useEffect(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -39,24 +43,31 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
			
		|||
 | 
			
		||||
		const handleCanvasClick = (event) => {
 | 
			
		||||
			if (!isImageLoaded.current) return;
 | 
			
		||||
			if (isOpen) return;
 | 
			
		||||
			const { x, y } = getMousePos(canvas, event);
 | 
			
		||||
 | 
			
		||||
			// Check if a delete button is clicked
 | 
			
		||||
			for (let i = 0; i < blurredRegions.current.length; i++) {
 | 
			
		||||
				const region = blurredRegions.current[i];
 | 
			
		||||
				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) - 17;
 | 
			
		||||
				if (Math.abs(buttonX - x) < 30 && Math.abs(buttonY - y) < 10) {
 | 
			
		||||
					// blurredRegions.current.splice(i, 1);
 | 
			
		||||
					if (info?.index === i) {
 | 
			
		||||
						setInfo({});
 | 
			
		||||
						setIsOpen(false);
 | 
			
		||||
						return;
 | 
			
		||||
					}
 | 
			
		||||
					setInfo({ index: i, value: blurredRegions.current[i] });
 | 
			
		||||
					setIsOpen(true);
 | 
			
		||||
					redrawCanvas(i);
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			points.current.push({ x, y, label: listLabel[0]?.name || 'cisco' });
 | 
			
		||||
			if (isOpen) return;
 | 
			
		||||
			const arrLabel = (listLabel[0]?.name || ["logo.cisco"])?.split(".");
 | 
			
		||||
			const label = arrLabel?.length > 1 ? arrLabel[1] : arrLabel[0];
 | 
			
		||||
			const subLabel = arrLabel?.length > 1 ? arrLabel[0] : null;
 | 
			
		||||
			points.current.push({ x, y, label: label, subLabel: subLabel });
 | 
			
		||||
			drawRedPoint(canvas, x, y);
 | 
			
		||||
 | 
			
		||||
			if (points.current.length === 4) {
 | 
			
		||||
| 
						 | 
				
			
			@ -67,11 +78,10 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
			
		|||
		};
 | 
			
		||||
 | 
			
		||||
		const handleCanvasMouseDown = (event) => {
 | 
			
		||||
			if (isOpen) return;
 | 
			
		||||
			const { x, y } = getMousePos(canvas, event);
 | 
			
		||||
 | 
			
		||||
			// Check if we clicked on an existing point
 | 
			
		||||
			for (let i = 0; i < blurredRegions.current.length; i++) {
 | 
			
		||||
				// Check if we clicked on an existing point
 | 
			
		||||
				for (let j = 0; j < blurredRegions.current[i].length; j++) {
 | 
			
		||||
					const point = blurredRegions.current[i][j];
 | 
			
		||||
					if (Math.abs(point.x - x) < 5 && Math.abs(point.y - y) < 5) {
 | 
			
		||||
| 
						 | 
				
			
			@ -84,6 +94,25 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
			
		|||
				// Check if we clicked inside a region
 | 
			
		||||
				const region = blurredRegions.current[i];
 | 
			
		||||
				if (isPointInRegion(x, y, region)) {
 | 
			
		||||
					// Check if a delete button is clicked
 | 
			
		||||
					for (let i = 0; i < blurredRegions.current.length; i++) {
 | 
			
		||||
						const region = blurredRegions.current[i];
 | 
			
		||||
						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;
 | 
			
		||||
						if (Math.abs(buttonX - x) < 30 && Math.abs(buttonY - y) < 10) {
 | 
			
		||||
							// blurredRegions.current.splice(i, 1);
 | 
			
		||||
							if (info?.index === i) {
 | 
			
		||||
								setInfo({});
 | 
			
		||||
								setIsOpen(false);
 | 
			
		||||
								return;
 | 
			
		||||
							}
 | 
			
		||||
							setInfo({ index: i, value: blurredRegions.current[i] });
 | 
			
		||||
							setIsOpen(true);
 | 
			
		||||
							redrawCanvas(i);
 | 
			
		||||
							return;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					if (isOpen) return;
 | 
			
		||||
					setMovingRegionIndex(i);
 | 
			
		||||
					setOffset({ x: region[0].x - x, y: region[0].y - y });
 | 
			
		||||
					return;
 | 
			
		||||
| 
						 | 
				
			
			@ -110,6 +139,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
			
		|||
					x: x + offset.x,
 | 
			
		||||
					y: y + offset.y,
 | 
			
		||||
					label: blurredRegions.current[regionIndex][pointIndex]?.label,
 | 
			
		||||
					subLabel: blurredRegions.current[regionIndex][pointIndex]?.subLabel,
 | 
			
		||||
				};
 | 
			
		||||
				redrawCanvas();
 | 
			
		||||
			} else if (movingRegionIndex !== null) {
 | 
			
		||||
| 
						 | 
				
			
			@ -125,14 +155,14 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
			
		|||
			}
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		canvas.addEventListener('mousedown', handleCanvasMouseDown);
 | 
			
		||||
		canvas.addEventListener('mouseup', handleCanvasMouseUp);
 | 
			
		||||
		canvas.addEventListener('mousemove', handleCanvasMouseMove);
 | 
			
		||||
		canvas.addEventListener("mousedown", handleCanvasMouseDown);
 | 
			
		||||
		canvas.addEventListener("mouseup", handleCanvasMouseUp);
 | 
			
		||||
		canvas.addEventListener("mousemove", handleCanvasMouseMove);
 | 
			
		||||
 | 
			
		||||
		return () => {
 | 
			
		||||
			canvas.removeEventListener('mousedown', handleCanvasMouseDown);
 | 
			
		||||
			canvas.removeEventListener('mouseup', handleCanvasMouseUp);
 | 
			
		||||
			canvas.removeEventListener('mousemove', handleCanvasMouseMove);
 | 
			
		||||
			canvas.removeEventListener("mousedown", handleCanvasMouseDown);
 | 
			
		||||
			canvas.removeEventListener("mouseup", handleCanvasMouseUp);
 | 
			
		||||
			canvas.removeEventListener("mousemove", handleCanvasMouseMove);
 | 
			
		||||
		};
 | 
			
		||||
	}, [draggingPointIndex, movingRegionIndex, offset, isOpen, info]);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -160,17 +190,17 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
			
		|||
	};
 | 
			
		||||
 | 
			
		||||
	const drawRedPoint = (canvas, x, y) => {
 | 
			
		||||
		const ctx = canvas.getContext('2d');
 | 
			
		||||
		const ctx = canvas.getContext("2d");
 | 
			
		||||
		ctx.save();
 | 
			
		||||
		ctx.beginPath();
 | 
			
		||||
		ctx.arc(x, y, 5, 0, 2 * Math.PI);
 | 
			
		||||
		ctx.fillStyle = 'red';
 | 
			
		||||
		ctx.fillStyle = "red";
 | 
			
		||||
		ctx.fill();
 | 
			
		||||
		ctx.restore();
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const drawBorder = (canvas, points) => {
 | 
			
		||||
		const ctx = canvas.getContext('2d');
 | 
			
		||||
		const ctx = canvas.getContext("2d");
 | 
			
		||||
		ctx.save();
 | 
			
		||||
		ctx.beginPath();
 | 
			
		||||
		points.forEach((point, index) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -182,7 +212,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
			
		|||
		});
 | 
			
		||||
		ctx.closePath();
 | 
			
		||||
		ctx.lineWidth = 2;
 | 
			
		||||
		ctx.strokeStyle = 'blue';
 | 
			
		||||
		ctx.strokeStyle = "blue";
 | 
			
		||||
		ctx.stroke();
 | 
			
		||||
		ctx.restore();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -190,23 +220,26 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
			
		|||
	};
 | 
			
		||||
 | 
			
		||||
	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 buttonY = Math.min(region[0].y, region[1].y, region[2].y, region[3].y) - 20;
 | 
			
		||||
 | 
			
		||||
		ctx.save();
 | 
			
		||||
		ctx.fillStyle = isSelected ? 'rgb(196, 194, 61)' : 'rgb(2, 101, 182)';
 | 
			
		||||
		ctx.fillRect(buttonX - 30, buttonY - 10, 60, 20);
 | 
			
		||||
		ctx.fillStyle = 'rgb(255, 255, 255)';
 | 
			
		||||
		ctx.font = '13px Arial';
 | 
			
		||||
		ctx.textAlign = 'center';
 | 
			
		||||
		ctx.fillText(region[0]?.label, buttonX, buttonY + 5);
 | 
			
		||||
		ctx.fillStyle = isSelected ? "rgb(196, 194, 61, 0.5)" : "rgb(2, 101, 182, 0.5)";
 | 
			
		||||
		ctx.fillRect(buttonX - 30, buttonY - 10, Math.max(region[0]?.label?.length * 6 + 20, (region[0]?.subLabel || "")?.length * 3 + 30), 25);
 | 
			
		||||
		ctx.fillStyle = "rgb(255, 255, 255)";
 | 
			
		||||
		ctx.font = "9px Arial";
 | 
			
		||||
		ctx.textAlign = "center";
 | 
			
		||||
		ctx.fillText(region[0]?.subLabel || "", buttonX - 10, buttonY);
 | 
			
		||||
		ctx.font = "11px Arial";
 | 
			
		||||
		ctx.textAlign = "center";
 | 
			
		||||
		ctx.fillText(region[0]?.label, buttonX, buttonY + 10);
 | 
			
		||||
		ctx.restore();
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const redrawCanvas = (index) => {
 | 
			
		||||
		const canvas = canvasRef.current;
 | 
			
		||||
		const ctx = canvas.getContext('2d');
 | 
			
		||||
		const ctx = canvas.getContext("2d");
 | 
			
		||||
		const image = new Image();
 | 
			
		||||
 | 
			
		||||
		image.onload = () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -223,29 +256,39 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
			
		|||
			});
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		image.crossOrigin = 'anonymous';
 | 
			
		||||
		image.crossOrigin = "anonymous";
 | 
			
		||||
		image.src = imageSrc;
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	return (
 | 
			
		||||
		<div style={{ position: 'relative' }}>
 | 
			
		||||
		<div style={{ position: "relative" }}>
 | 
			
		||||
			<canvas ref={canvasRef} />
 | 
			
		||||
			{isOpen && (
 | 
			
		||||
				<div style={{ backgroundColor: '#FFF', position: 'absolute', top: info?.value[0].y - 200, left: info?.value[0].x, zIndex: 9999 }}>
 | 
			
		||||
				<div
 | 
			
		||||
					style={{
 | 
			
		||||
						backgroundColor: "#FFF",
 | 
			
		||||
						position: "absolute",
 | 
			
		||||
						top: info?.value[0].y - 30,
 | 
			
		||||
						left: info?.value[0].x < 200 ? info?.value[0].x + 50 : info?.value[0].x - 170,
 | 
			
		||||
						zIndex: 9999,
 | 
			
		||||
					}}>
 | 
			
		||||
					<div>
 | 
			
		||||
						<span style={{ color: '#000', fontSize: '18px', fontWeight: 'bold' }}>Select label</span>
 | 
			
		||||
						<span style={{ color: "#000", fontSize: "18px", fontWeight: "bold" }}>Select label</span>
 | 
			
		||||
					</div>
 | 
			
		||||
					{listLabel?.map((el, i) => (
 | 
			
		||||
						<div key={i}>
 | 
			
		||||
							<button
 | 
			
		||||
								onClick={() => {
 | 
			
		||||
									const newList = info?.value?.map((pre) => ({ ...pre, label: el?.name }));
 | 
			
		||||
									const arrLabel = el?.name?.split(".");
 | 
			
		||||
									const label = arrLabel?.length > 1 ? arrLabel[1] : arrLabel[0];
 | 
			
		||||
									const subLabel = arrLabel?.length > 1 ? arrLabel[0] : null;
 | 
			
		||||
									const newList = info?.value?.map((pre) => ({ ...pre, label: label, subLabel: subLabel }));
 | 
			
		||||
									blurredRegions.current[info?.index] = newList;
 | 
			
		||||
									setInfo({});
 | 
			
		||||
									setIsOpen(false);
 | 
			
		||||
									redrawCanvas();
 | 
			
		||||
								}}
 | 
			
		||||
								style={{ backgroundColor: '#ccc', width: '150px', color: '#000' }}>
 | 
			
		||||
								style={{ backgroundColor: "#ccc", width: "150px", color: "#000" }}>
 | 
			
		||||
								{el?.name}
 | 
			
		||||
							</button>
 | 
			
		||||
						</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -258,7 +301,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
 | 
			
		|||
								setIsOpen(false);
 | 
			
		||||
								redrawCanvas();
 | 
			
		||||
							}}
 | 
			
		||||
							style={{ backgroundColor: '#ff0000', marginBottom: '10px', width: '150px', color: '#fff' }}>
 | 
			
		||||
							style={{ backgroundColor: "#ff0000", marginBottom: "10px", width: "150px", color: "#fff" }}>
 | 
			
		||||
							Delete
 | 
			
		||||
						</button>
 | 
			
		||||
					</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,12 @@
 | 
			
		|||
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';
 | 
			
		||||
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 }) => {
 | 
			
		||||
export const SaveButton = ({ currentData, imageLabelRef, onSaved, disabled = false }) => {
 | 
			
		||||
	const [loading, setLoading] = useState(false);
 | 
			
		||||
 | 
			
		||||
	const { appendImageDetect, images } = useImagesDetected();
 | 
			
		||||
| 
						 | 
				
			
			@ -14,7 +14,7 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
 | 
			
		|||
	const handleSave = async ({ blurredRegions, infoImage, imageName, listLabel }) => {
 | 
			
		||||
		try {
 | 
			
		||||
			if (blurredRegions?.length > 0) {
 | 
			
		||||
				let arrPoints = '';
 | 
			
		||||
				let arrPoints = "";
 | 
			
		||||
				blurredRegions?.forEach((points) => {
 | 
			
		||||
					const img_w = infoImage?.width;
 | 
			
		||||
					const img_h = infoImage?.height;
 | 
			
		||||
| 
						 | 
				
			
			@ -31,19 +31,26 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
 | 
			
		|||
					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 class_id =
 | 
			
		||||
						listLabel.find((value) => {
 | 
			
		||||
							const arrLabel = value?.name?.split(".");
 | 
			
		||||
							const label = arrLabel?.length > 1 ? arrLabel[1] : arrLabel[0];
 | 
			
		||||
							return label === points[0].label;
 | 
			
		||||
						})?.code || "0";
 | 
			
		||||
 | 
			
		||||
					const yolo_label = `${class_id} ${x_center.toFixed(6)} ${y_center.toFixed(6)} ${width.toFixed(6)} ${height.toFixed(6)}`;
 | 
			
		||||
 | 
			
		||||
					arrPoints += yolo_label + '\n';
 | 
			
		||||
					arrPoints += yolo_label + "\n";
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				const url = `${process.env.REACT_APP_API_URL}/save`;
 | 
			
		||||
				// const url = `http://127.0.0.1:5000/save`;
 | 
			
		||||
				const formData = new FormData();
 | 
			
		||||
				formData.append('list', arrPoints);
 | 
			
		||||
				formData.append('imageName', imageName);
 | 
			
		||||
				formData.append("list", arrPoints);
 | 
			
		||||
				formData.append("imageName", imageName);
 | 
			
		||||
				setLoading(true);
 | 
			
		||||
				const response = await fetch(url, {
 | 
			
		||||
					method: 'POST',
 | 
			
		||||
					method: "POST",
 | 
			
		||||
					body: formData,
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -55,7 +62,7 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
 | 
			
		|||
					});
 | 
			
		||||
 | 
			
		||||
					currentData.points = newPoints;
 | 
			
		||||
					currentData['isSave'] = true;
 | 
			
		||||
					currentData["isSave"] = true;
 | 
			
		||||
 | 
			
		||||
					const newImages = images.map((image) => {
 | 
			
		||||
						if (image.image_name === imageName) {
 | 
			
		||||
| 
						 | 
				
			
			@ -72,9 +79,9 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
 | 
			
		|||
					appendImageDetect(newImages);
 | 
			
		||||
 | 
			
		||||
					notifications.show({
 | 
			
		||||
						title: 'Save Success',
 | 
			
		||||
						title: "Save Success",
 | 
			
		||||
						message: `${imageName} save success`,
 | 
			
		||||
						color: 'green',
 | 
			
		||||
						color: "green",
 | 
			
		||||
					});
 | 
			
		||||
 | 
			
		||||
					if (onSaved) {
 | 
			
		||||
| 
						 | 
				
			
			@ -84,9 +91,9 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
 | 
			
		|||
			}
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			notifications.show({
 | 
			
		||||
				title: 'Save Error',
 | 
			
		||||
				message: error?.message || 'Internal Server Error',
 | 
			
		||||
				color: 'red',
 | 
			
		||||
				title: "Save Error",
 | 
			
		||||
				message: error?.message || "Internal Server Error",
 | 
			
		||||
				color: "red",
 | 
			
		||||
			});
 | 
			
		||||
		} finally {
 | 
			
		||||
			setLoading(false);
 | 
			
		||||
| 
						 | 
				
			
			@ -101,11 +108,11 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
 | 
			
		|||
		handleSave(saveData);
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	useHotkeys([['ctrl+S', handleSubmit]]);
 | 
			
		||||
	useHotkeys([["ctrl+S", handleSubmit]]);
 | 
			
		||||
 | 
			
		||||
	return (
 | 
			
		||||
		<Tooltip label={'Ctrl + S'}>
 | 
			
		||||
			<Button className="relative ư-f" disabled={!currentData} onClick={handleSubmit} leftSection={<IconImageInPicture size={14} />}>
 | 
			
		||||
		<Tooltip label={"Ctrl + S"}>
 | 
			
		||||
			<Button className="relative ư-f" disabled={!currentData || disabled} onClick={handleSubmit} leftSection={<IconImageInPicture size={14} />}>
 | 
			
		||||
				Save
 | 
			
		||||
				<LoadingOverlay visible={loading} loaderProps={{ size: 14 }} />
 | 
			
		||||
			</Button>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ from routes.save import save_bp
 | 
			
		|||
from routes.show import show_bp
 | 
			
		||||
from routes.delete import delete_bp
 | 
			
		||||
from routes.upload import upload_bp
 | 
			
		||||
from routes.label import config_label
 | 
			
		||||
 | 
			
		||||
app = Flask(__name__)
 | 
			
		||||
CORS(app)
 | 
			
		||||
| 
						 | 
				
			
			@ -17,6 +18,7 @@ app.register_blueprint(save_bp)
 | 
			
		|||
app.register_blueprint(show_bp)
 | 
			
		||||
app.register_blueprint(delete_bp)
 | 
			
		||||
app.register_blueprint(upload_bp)
 | 
			
		||||
app.register_blueprint(config_label)
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    app.run(debug=True)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,7 @@ import random
 | 
			
		|||
import cv2
 | 
			
		||||
import json
 | 
			
		||||
import time
 | 
			
		||||
import subprocess
 | 
			
		||||
 | 
			
		||||
# Load mô hình YOLO
 | 
			
		||||
model = YOLO(get_latest_model_including_today(trained_model_folder=TRAINED_MODEL_FOLDER))
 | 
			
		||||
| 
						 | 
				
			
			@ -110,7 +111,7 @@ def detect_image():
 | 
			
		|||
    file.save(image_path)
 | 
			
		||||
 | 
			
		||||
    # Run with model
 | 
			
		||||
    results = model(image_path, conf=0.6)  
 | 
			
		||||
    results = model(image_path, conf=0.5)  
 | 
			
		||||
    points = []
 | 
			
		||||
 | 
			
		||||
    # Result predict
 | 
			
		||||
| 
						 | 
				
			
			@ -139,6 +140,19 @@ def reload_model():
 | 
			
		|||
    model = YOLO(new_model_path)  
 | 
			
		||||
    return jsonify({"message": f"Model reloaded from {new_model_path}", "status": "true"}), 200
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@detect_bp.route('/train', methods=['GET'])
 | 
			
		||||
def run_training():
 | 
			
		||||
    try:
 | 
			
		||||
        # Run train.py as a subprocess
 | 
			
		||||
        process = subprocess.Popen(["python", "train.py"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 | 
			
		||||
        stdout, stderr = process.communicate()
 | 
			
		||||
 | 
			
		||||
        if process.returncode == 0:
 | 
			
		||||
            return jsonify({"message": "Training started successfully", "output": stdout.decode()}), 200
 | 
			
		||||
        else:
 | 
			
		||||
            return jsonify({"error": "Training failed", "details": stderr.decode()}), 500
 | 
			
		||||
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        return jsonify({"error": str(e)}), 500
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,141 @@
 | 
			
		|||
import datetime
 | 
			
		||||
from flask import Blueprint, request, jsonify, send_from_directory
 | 
			
		||||
import sqlite3
 | 
			
		||||
 | 
			
		||||
config_label = Blueprint('label', __name__)
 | 
			
		||||
# Connect to database (creates a new one if it doesn’t exist)
 | 
			
		||||
conn = sqlite3.connect("my_database.db")
 | 
			
		||||
# Create a cursor object to execute SQL commands
 | 
			
		||||
cursor = conn.cursor()
 | 
			
		||||
 | 
			
		||||
# Connect to database
 | 
			
		||||
def get_db_connection():
 | 
			
		||||
    conn = sqlite3.connect("my_database.db")
 | 
			
		||||
    conn.row_factory = sqlite3.Row  # Enables dictionary-like access to rows
 | 
			
		||||
    return conn
 | 
			
		||||
 | 
			
		||||
# Create table (only call once at startup)
 | 
			
		||||
def create_table():
 | 
			
		||||
    conn = get_db_connection()
 | 
			
		||||
    cursor = conn.cursor()
 | 
			
		||||
 | 
			
		||||
    # Check if the table exists
 | 
			
		||||
    cursor.execute("""
 | 
			
		||||
        SELECT name FROM sqlite_master WHERE type='table' AND name='labels'
 | 
			
		||||
    """)
 | 
			
		||||
    table_exists = cursor.fetchone()
 | 
			
		||||
 | 
			
		||||
    if not table_exists:
 | 
			
		||||
        # Create table if it doesn't exist
 | 
			
		||||
        cursor.execute("""
 | 
			
		||||
            CREATE TABLE labels (
 | 
			
		||||
                id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
			
		||||
                name TEXT NOT NULL,
 | 
			
		||||
                code INTEGER NOT NULL,
 | 
			
		||||
                status TEXT DEFAULT 'active',
 | 
			
		||||
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 | 
			
		||||
                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 | 
			
		||||
            )
 | 
			
		||||
        """)
 | 
			
		||||
        
 | 
			
		||||
        # Insert default values
 | 
			
		||||
        default_values = [
 | 
			
		||||
            ("logo.cisco", 0),
 | 
			
		||||
            ("barcode.SN", 1)
 | 
			
		||||
        ]
 | 
			
		||||
        cursor.executemany("INSERT INTO labels (name, code) VALUES (?, ?)", default_values)
 | 
			
		||||
        
 | 
			
		||||
        print("Table created and default values inserted.")
 | 
			
		||||
    else:
 | 
			
		||||
        print("Table already exists.")
 | 
			
		||||
 | 
			
		||||
    conn.commit()
 | 
			
		||||
    conn.close()
 | 
			
		||||
 | 
			
		||||
# Call create_table() when the script runs
 | 
			
		||||
create_table()
 | 
			
		||||
 | 
			
		||||
# Route to add a new label
 | 
			
		||||
@config_label.route('/label/add', methods=['POST'])
 | 
			
		||||
def add_label():
 | 
			
		||||
    data = request.get_json()  # Get JSON data from request
 | 
			
		||||
    name = data.get("name")
 | 
			
		||||
 | 
			
		||||
    if not name:
 | 
			
		||||
        return jsonify({"error": "Name is required"}), 400
 | 
			
		||||
 | 
			
		||||
    conn = get_db_connection()
 | 
			
		||||
    cursor = conn.cursor()
 | 
			
		||||
 | 
			
		||||
    # Get the last code value
 | 
			
		||||
    cursor.execute("SELECT MAX(code) FROM labels")
 | 
			
		||||
    last_code = cursor.fetchone()[0]
 | 
			
		||||
    new_code = (last_code + 1) if last_code is not None else 1  # Start from 1 if table is empty
 | 
			
		||||
 | 
			
		||||
    # Insert new label
 | 
			
		||||
    cursor.execute(
 | 
			
		||||
        "INSERT INTO labels (name, code) VALUES (?, ?)", 
 | 
			
		||||
        (name, new_code)
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    conn.commit()
 | 
			
		||||
    conn.close()
 | 
			
		||||
 | 
			
		||||
    return jsonify({"message": "Label added successfully", "name": name, "code": new_code, "success" : True}), 201
 | 
			
		||||
 | 
			
		||||
# Route to update a label
 | 
			
		||||
@config_label.route('/label/update', methods=['POST'])
 | 
			
		||||
def update_label():
 | 
			
		||||
    data = request.get_json()
 | 
			
		||||
    name = data.get("name")
 | 
			
		||||
    label_id = data.get("id")
 | 
			
		||||
 | 
			
		||||
    if not name or not label_id:
 | 
			
		||||
        return jsonify({"error": "Name and id are required"}), 400
 | 
			
		||||
 | 
			
		||||
    conn = get_db_connection()
 | 
			
		||||
    cursor = conn.cursor()
 | 
			
		||||
    
 | 
			
		||||
    cursor.execute(
 | 
			
		||||
        "UPDATE labels SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", 
 | 
			
		||||
        (name, label_id)
 | 
			
		||||
    )
 | 
			
		||||
    
 | 
			
		||||
    conn.commit()
 | 
			
		||||
    conn.close()
 | 
			
		||||
 | 
			
		||||
    return jsonify({"message": "Label updated successfully", "success" : True}), 200
 | 
			
		||||
 | 
			
		||||
@config_label.route('/labels', methods=['GET'])
 | 
			
		||||
def get_labels():
 | 
			
		||||
    conn = get_db_connection()
 | 
			
		||||
    cursor = conn.cursor()
 | 
			
		||||
    
 | 
			
		||||
    cursor.execute("SELECT * FROM labels WHERE status = 'active'")
 | 
			
		||||
    labels = cursor.fetchall()
 | 
			
		||||
    
 | 
			
		||||
    conn.close()
 | 
			
		||||
 | 
			
		||||
    # Convert rows to list of dictionaries
 | 
			
		||||
    label_list = [dict(row) for row in labels]
 | 
			
		||||
 | 
			
		||||
    return jsonify(label_list), 200
 | 
			
		||||
 | 
			
		||||
# Route to delete a label
 | 
			
		||||
@config_label.route('/delete/<int:label_id>', methods=['GET'])
 | 
			
		||||
def delete_label(label_id):
 | 
			
		||||
    if not label_id is None:
 | 
			
		||||
        return jsonify({"error": "ID label are required"}), 400
 | 
			
		||||
 | 
			
		||||
    conn = get_db_connection()
 | 
			
		||||
    cursor = conn.cursor()
 | 
			
		||||
    
 | 
			
		||||
    cursor.execute(
 | 
			
		||||
        "UPDATE labels SET status = 'delete' WHERE id = ?", 
 | 
			
		||||
        (label_id)
 | 
			
		||||
    )
 | 
			
		||||
    
 | 
			
		||||
    conn.commit()
 | 
			
		||||
    conn.close()
 | 
			
		||||
 | 
			
		||||
    return jsonify({"message": "Label delete successfully"}), 200
 | 
			
		||||
| 
						 | 
				
			
			@ -7,13 +7,36 @@ from ultralytics import YOLO
 | 
			
		|||
import yaml
 | 
			
		||||
import requests
 | 
			
		||||
import glob
 | 
			
		||||
import sqlite3
 | 
			
		||||
 | 
			
		||||
# 🗂️ Configure paths
 | 
			
		||||
DATA_SPLIT_FOLDER = "model_datasets"
 | 
			
		||||
IMAGE_EXTENSION = ".png"  
 | 
			
		||||
LOG_FILE = "training_logs.log"
 | 
			
		||||
LATEST_DETECT = "latest_detect"
 | 
			
		||||
LATEST_DETECT_IMAGES = "latest_detect/images"
 | 
			
		||||
LATEST_DETECT_LABELS = "latest_detect/labels"
 | 
			
		||||
image_folder=""
 | 
			
		||||
label_folder=""
 | 
			
		||||
 | 
			
		||||
def get_label_names():
 | 
			
		||||
    conn = sqlite3.connect("my_database.db")
 | 
			
		||||
    conn.row_factory = sqlite3.Row  # Enables dictionary-like access to rows
 | 
			
		||||
    # Create a cursor object to execute SQL commands
 | 
			
		||||
    cursor = conn.cursor()
 | 
			
		||||
 | 
			
		||||
    # Fetch all names from the labels table
 | 
			
		||||
    cursor.execute("SELECT name FROM labels")
 | 
			
		||||
    labels = cursor.fetchall()
 | 
			
		||||
    conn.close()
 | 
			
		||||
 | 
			
		||||
    # Convert fetched rows into a simple list of strings
 | 
			
		||||
    label_names = [row["name"] for row in labels]
 | 
			
		||||
 | 
			
		||||
    return label_names
 | 
			
		||||
 | 
			
		||||
class_names = get_label_names()
 | 
			
		||||
 | 
			
		||||
class_names = ["cisco", "barcode"]
 | 
			
		||||
# 📅 Get today's and yesterday's date
 | 
			
		||||
today = datetime.date.today()
 | 
			
		||||
yesterday = today - datetime.timedelta(days=1)
 | 
			
		||||
| 
						 | 
				
			
			@ -37,9 +60,11 @@ elif os.path.exists(image_folder_yesterday) and os.path.exists(label_folder_yest
 | 
			
		|||
    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!")
 | 
			
		||||
    # print(image_folder)
 | 
			
		||||
    # print(label_folder)
 | 
			
		||||
    with open(LOG_FILE, "a") as log:
 | 
			
		||||
        log.write(f"\nDate: {today_str}. No valid image & label folder found in the last two days!")
 | 
			
		||||
    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)
 | 
			
		||||
| 
						 | 
				
			
			@ -55,6 +80,7 @@ for folder in [train_img_folder, train_lbl_folder, val_img_folder, val_lbl_folde
 | 
			
		|||
 | 
			
		||||
# 📂 Get image list
 | 
			
		||||
image_files = [f for f in os.listdir(image_folder) if f.endswith(IMAGE_EXTENSION)]
 | 
			
		||||
label_files = [f for f in os.listdir(label_folder) if os.path.exists(label_folder)]
 | 
			
		||||
 | 
			
		||||
# 🌀 Shuffle images
 | 
			
		||||
random.shuffle(image_files)
 | 
			
		||||
| 
						 | 
				
			
			@ -66,7 +92,18 @@ split_idx = int(len(image_files) * train_ratio)
 | 
			
		|||
train_files = image_files[:split_idx]
 | 
			
		||||
val_files = image_files[split_idx:]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def copy_file_latest_training():
 | 
			
		||||
    latest_img_folder = [f for f in os.listdir(LATEST_DETECT_IMAGES) if f.endswith(IMAGE_EXTENSION)]
 | 
			
		||||
    train_files += latest_img_folder
 | 
			
		||||
    # Copy latest detect images to train folder
 | 
			
		||||
    for img_file in latest_img_folder:
 | 
			
		||||
        img_path = os.path.join(LATEST_DETECT_IMAGES, img_file)
 | 
			
		||||
        label_file = os.path.splitext(img_file)[0] + ".txt"
 | 
			
		||||
        label_path = os.path.join(LATEST_DETECT_LABELS, label_file)
 | 
			
		||||
        
 | 
			
		||||
        if os.path.exists(img_path) and os.path.exists(label_path) and not os.path.exists(os.path.join(image_folder, img_file)):
 | 
			
		||||
            shutil.copy(img_path, os.path.join(image_folder, img_file))
 | 
			
		||||
            shutil.copy(label_path, os.path.join(label_folder, label_file))
 | 
			
		||||
 | 
			
		||||
def log_message(message: str):
 | 
			
		||||
    """Ghi log vào file"""
 | 
			
		||||
| 
						 | 
				
			
			@ -78,7 +115,7 @@ def log_message(message: str):
 | 
			
		|||
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}")
 | 
			
		||||
        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")
 | 
			
		||||
| 
						 | 
				
			
			@ -86,17 +123,17 @@ def get_latest_model(trained_model_folder: str, default_model: str = "train5/wei
 | 
			
		|||
    # 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
 | 
			
		||||
        if os.path.isdir(os.path.join(trained_model_folder, f)) and (f <= today_str or f.startswith(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}")
 | 
			
		||||
            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}")
 | 
			
		||||
    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):
 | 
			
		||||
| 
						 | 
				
			
			@ -122,23 +159,49 @@ def call_reload_model_api(base_url="http://localhost:5000"):
 | 
			
		|||
    try:
 | 
			
		||||
        response = requests.post(url, timeout=10)
 | 
			
		||||
        response_data = response.json()
 | 
			
		||||
        log_message(f"✅ API Response: {response_data} \n")
 | 
			
		||||
        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")
 | 
			
		||||
        log_message(f" Error calling reload model API: {e} \n")
 | 
			
		||||
        return {"error": str(e)}
 | 
			
		||||
 | 
			
		||||
def copy_files(src_folder, dest_folder, num_files=20):
 | 
			
		||||
    if not os.path.exists(dest_folder):
 | 
			
		||||
        os.makedirs(dest_folder)
 | 
			
		||||
 | 
			
		||||
    files = sorted(
 | 
			
		||||
        glob.glob(os.path.join(src_folder, "*")),
 | 
			
		||||
        key=os.path.getmtime,  # Sort by modification time (latest first)
 | 
			
		||||
        reverse=True
 | 
			
		||||
    )[:num_files]  # Get top N files
 | 
			
		||||
 | 
			
		||||
    for file in files:
 | 
			
		||||
        try:
 | 
			
		||||
            shutil.copy(file, os.path.join(dest_folder, os.path.basename(file)))
 | 
			
		||||
            # log_message(f"Copied: {file} -> {dest_folder}")
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            log_message(f"Error copying {file}: {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]
 | 
			
		||||
    # Copy top 20 images and labels
 | 
			
		||||
    copy_files(image_folder, LATEST_DETECT_IMAGES, 20)
 | 
			
		||||
    copy_files(label_folder, LATEST_DETECT_LABELS, 20)
 | 
			
		||||
 | 
			
		||||
    # Delete all files in source folders
 | 
			
		||||
    for path in [image_folder, label_folder]:
 | 
			
		||||
        if not os.path.exists(path):
 | 
			
		||||
            log_message(f"Path does not exist: {path}")
 | 
			
		||||
            continue  
 | 
			
		||||
 | 
			
		||||
    for path in paths:
 | 
			
		||||
        for file in glob.glob(os.path.join(path, "*")):
 | 
			
		||||
            if os.path.isfile(file):  
 | 
			
		||||
                os.remove(file)
 | 
			
		||||
            if os.path.isfile(file):
 | 
			
		||||
                try:
 | 
			
		||||
                    os.remove(file)
 | 
			
		||||
                    # log_message(f"Deleted: {file}")
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    log_message(f"Error deleting {file}: {e}")
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    log_message(f"Delete source image success \n")
 | 
			
		||||
    log_message("Delete source image success \n")
 | 
			
		||||
    log_message("END " + ("=" * 20) + "\n")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -159,22 +222,30 @@ def move_files(file_list, dest_img_folder, dest_lbl_folder):
 | 
			
		|||
                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))
 | 
			
		||||
                log.write(f"[{datetime.datetime.now()}]   Missing label for image: {img_file}\n")
 | 
			
		||||
                shutil.copy(img_path, os.path.join(nolable_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):
 | 
			
		||||
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!")
 | 
			
		||||
        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'.")
 | 
			
		||||
        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 đó
 | 
			
		||||
| 
						 | 
				
			
			@ -189,47 +260,51 @@ def train_yolo_model(pretrained_model: str, dataset_folder: str,project_name: st
 | 
			
		|||
        optimizer="AdamW",
 | 
			
		||||
        lr0=lr,
 | 
			
		||||
        weight_decay=0.0005,
 | 
			
		||||
        patience=10, 
 | 
			
		||||
        patience=0, 
 | 
			
		||||
        verbose=True,
 | 
			
		||||
        project=project_name,  
 | 
			
		||||
        name=name  
 | 
			
		||||
        name=name
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    with open(LOG_FILE, "a") as log:
 | 
			
		||||
        log.write(f"\n[{datetime.datetime.now()}] ✅ Train completed\n")
 | 
			
		||||
        log.write(f"\n[{datetime.datetime.now()}]  Train completed\n")
 | 
			
		||||
 | 
			
		||||
    call_reload_model_api()
 | 
			
		||||
    clear_images_source()
 | 
			
		||||
 
 | 
			
		||||
    clear_images_source()   
 | 
			
		||||
 | 
			
		||||
if(len(label_files) >= 20):
 | 
			
		||||
    if os.path.exists(LATEST_DETECT): copy_file_latest_training()
 | 
			
		||||
 | 
			
		||||
    # 🚀 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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# 🚀 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):
 | 
			
		||||
    # 🏁 Log summary
 | 
			
		||||
    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
 | 
			
		||||
        )
 | 
			
		||||
        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 = 50,
 | 
			
		||||
            name=today_str,
 | 
			
		||||
            project_name=model_folder_name,
 | 
			
		||||
            )
 | 
			
		||||
else:
 | 
			
		||||
    log_message(f"Date: {today_str}. Need more image for training!!")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,12 @@
 | 
			
		|||
export const generateImageUrl = (image_path) => {
 | 
			
		||||
	return process.env.REACT_APP_API_URL + `/${image_path}`;
 | 
			
		||||
	// return `http://127.0.0.1:5000/${image_path}`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function convertToBoundingBox(points) {
 | 
			
		||||
	return {
 | 
			
		||||
		label: points[0].label,
 | 
			
		||||
		subLabel: points[0].subLabel || "",
 | 
			
		||||
		label: (points[0].subLabel || "") + "." + points[0].label,
 | 
			
		||||
		x1: Math.min(...points.map((p) => p.x)),
 | 
			
		||||
		x2: Math.max(...points.map((p) => p.x)),
 | 
			
		||||
		y1: Math.min(...points.map((p) => p.y)),
 | 
			
		||||
| 
						 | 
				
			
			@ -19,6 +21,6 @@ export const randomDelay = () => {
 | 
			
		|||
 | 
			
		||||
export function generateClientID() {
 | 
			
		||||
	const id = Math.random().toString(36).substr(2, 9);
 | 
			
		||||
	localStorage.setItem('client_id', id);
 | 
			
		||||
	localStorage.setItem("client_id", id);
 | 
			
		||||
	return id;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,21 +4,21 @@ self.onmessage = async function (e) {
 | 
			
		|||
 | 
			
		||||
	let processFiles = 0;
 | 
			
		||||
 | 
			
		||||
	if (action === 'processImages') {
 | 
			
		||||
	if (action === "processImages") {
 | 
			
		||||
		for (const file of files) {
 | 
			
		||||
			try {
 | 
			
		||||
				const formData = new FormData();
 | 
			
		||||
				formData.append('image', file, file.name);
 | 
			
		||||
				formData.append("image", file, file.name);
 | 
			
		||||
 | 
			
		||||
				const response = await fetch('/api/detect_image', {
 | 
			
		||||
					method: 'POST',
 | 
			
		||||
				const response = await fetch(`${process.env.REACT_APP_API_URL}/detect_image`, {
 | 
			
		||||
					method: "POST",
 | 
			
		||||
					body: formData,
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				const result = await response.json();
 | 
			
		||||
				self.postMessage({ status: 'success', result, total: files.length, processFiles: ++processFiles });
 | 
			
		||||
				self.postMessage({ status: "success", result, total: files.length, processFiles: ++processFiles });
 | 
			
		||||
			} catch (error) {
 | 
			
		||||
				self.postMessage({ status: 'error', error: error.message });
 | 
			
		||||
				self.postMessage({ status: "error", error: error.message });
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue