Compare commits

..

No commits in common. "main" and "zelda.update-ui-and-code" have entirely different histories.

15 changed files with 350 additions and 876 deletions

View File

@ -1,14 +0,0 @@
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=

5
.gitignore vendored
View File

@ -28,7 +28,4 @@ yarn-error.log*
/src/server/model_datasets
*.log
src/server/venv/*
__pycache__/
my_database.db
latest_detect
__pycache__/

View File

@ -1,29 +1,70 @@
## Front-end server
# Getting Started with Create React App
### `npm install`
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
Local use `env.dev`
## Available Scripts
In the project directory, you can run:
### `npm start`
## Back-end server
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
### /src/server
The page will reload when you make changes.\
You may also see any lint errors in the console.
### `python main.py`
### `npm test`
Server local run at `localhost:5000`
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.
### `Linus/Mac`
### `npm run build`
install package
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
### `Windows`
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
install package.\
Don't need to set permission, file `utils.py` command import `pwd`, `grp` and code function `ensure_correct_permissions`
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### Database sqlite `my_database.db`
### `npm run eject`
When server start with run check create table `labels`.\
If don't exist will create and add default 2 rows `logo.cisco`, `barcode.SN`
**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)

View File

@ -1,18 +1,21 @@
<!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.
@ -21,12 +24,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>AI Tool</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
<title>React App</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.
@ -36,5 +39,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>

View File

@ -1,39 +1,24 @@
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, 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";
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() {
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();
@ -42,28 +27,13 @@ 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
@ -85,11 +55,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();
@ -106,7 +76,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">
@ -162,10 +132,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({
@ -181,11 +151,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 = () => {
@ -218,294 +188,132 @@ 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">
<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>
<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>
<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>
</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>
</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>
<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>
)}
{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>
</AppShell.Navbar>
<AppShell.Main style={{ position: "relative" }}>
{clickData && <ImageLabel ref={imageLabelRef} data={clickData} listLabel={listLabel} />}
)}
</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>
</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();
<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',
});
}}
color="#1a9b20">
Confirm
</Button>
</div>
</Modal>
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>
);

View File

@ -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, listLabel }, ref) => {
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 [listLabel, setListLabel] = useState([]);
const [infoImage, setInfoImage] = useState({});
const [imageName, setImageName] = useState("");
const [imageName, setImageName] = useState('');
// Expose blurredRegions cho component cha
useImperativeHandle(ref, () => ({
@ -33,24 +33,19 @@ const ImageLabel = forwardRef(({ data, listLabel }, ref) => {
alert(data.error);
} else {
if (data?.points) {
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 },
];
});
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,
// }))
// );
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);

View File

@ -1,72 +0,0 @@
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;

View File

@ -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,12 +23,8 @@ 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(() => {
@ -43,31 +39,24 @@ 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) - 17;
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;
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 });
points.current.push({ x, y, label: listLabel[0]?.name || 'cisco' });
drawRedPoint(canvas, x, y);
if (points.current.length === 4) {
@ -78,10 +67,11 @@ 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) {
@ -94,25 +84,6 @@ 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;
@ -139,7 +110,6 @@ 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) {
@ -155,14 +125,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]);
@ -190,17 +160,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) => {
@ -212,7 +182,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
});
ctx.closePath();
ctx.lineWidth = 2;
ctx.strokeStyle = "blue";
ctx.strokeStyle = 'blue';
ctx.stroke();
ctx.restore();
@ -220,26 +190,23 @@ 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, 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.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.restore();
};
const redrawCanvas = (index) => {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
const ctx = canvas.getContext('2d');
const image = new Image();
image.onload = () => {
@ -256,39 +223,29 @@ 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 - 30,
left: info?.value[0].x < 200 ? info?.value[0].x + 50 : info?.value[0].x - 170,
zIndex: 9999,
}}>
<div style={{ backgroundColor: '#FFF', position: 'absolute', top: info?.value[0].y - 200, left: info?.value[0].x, 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 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 }));
const newList = info?.value?.map((pre) => ({ ...pre, label: el?.name }));
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>
@ -301,7 +258,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>

View File

@ -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, disabled = false }) => {
export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
const [loading, setLoading] = useState(false);
const { appendImageDetect, images } = useImagesDetected();
@ -14,7 +14,7 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved, disabled = fal
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,26 +31,19 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved, disabled = fal
const height = (y_max - y_min) / img_h;
// Get class ID
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 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";
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,
});
@ -62,7 +55,7 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved, disabled = fal
});
currentData.points = newPoints;
currentData["isSave"] = true;
currentData['isSave'] = true;
const newImages = images.map((image) => {
if (image.image_name === imageName) {
@ -79,9 +72,9 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved, disabled = fal
appendImageDetect(newImages);
notifications.show({
title: "Save Success",
title: 'Save Success',
message: `${imageName} save success`,
color: "green",
color: 'green',
});
if (onSaved) {
@ -91,9 +84,9 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved, disabled = fal
}
} 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);
@ -108,11 +101,11 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved, disabled = fal
handleSave(saveData);
};
useHotkeys([["ctrl+S", handleSubmit]]);
useHotkeys([['ctrl+S', handleSubmit]]);
return (
<Tooltip label={"Ctrl + S"}>
<Button className="relative ư-f" disabled={!currentData || disabled} onClick={handleSubmit} leftSection={<IconImageInPicture size={14} />}>
<Tooltip label={'Ctrl + S'}>
<Button className="relative ư-f" disabled={!currentData} onClick={handleSubmit} leftSection={<IconImageInPicture size={14} />}>
Save
<LoadingOverlay visible={loading} loaderProps={{ size: 14 }} />
</Button>

View File

@ -5,7 +5,6 @@ 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)
@ -18,7 +17,6 @@ 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)

View File

@ -9,7 +9,6 @@ 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))
@ -111,7 +110,7 @@ def detect_image():
file.save(image_path)
# Run with model
results = model(image_path, conf=0.5)
results = model(image_path, conf=0.6)
points = []
# Result predict
@ -140,19 +139,6 @@ 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

View File

@ -1,141 +0,0 @@
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 doesnt 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

View File

@ -7,36 +7,13 @@ 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)
@ -60,11 +37,9 @@ 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)
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!")
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)
@ -80,7 +55,6 @@ 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)
@ -92,18 +66,7 @@ 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"""
@ -115,7 +78,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")
@ -123,17 +86,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 or f.startswith(today_str))
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}")
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):
@ -159,49 +122,23 @@ 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():
# 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
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):
try:
os.remove(file)
# log_message(f"Deleted: {file}")
except Exception as e:
log_message(f"Error deleting {file}: {e}")
if os.path.isfile(file):
os.remove(file)
log_message("Delete source image success \n")
log_message(f"Delete source image success \n")
log_message("END " + ("=" * 20) + "\n")
@ -222,30 +159,22 @@ 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(nolable_img_folder, img_file))
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):
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 đó
@ -260,51 +189,47 @@ def train_yolo_model(
optimizer="AdamW",
lr0=lr,
weight_decay=0.0005,
patience=0,
patience=10,
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()
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)
clear_images_source()
# 🏁 Log summary
# 🚀 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()}] 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,
)
log.write(f"\n[{datetime.datetime.now()}] ❌ Data not qualified\n")
log.write("=" * 50 + "\n")
else:
log_message(f"Date: {today_str}. Need more image for training!!")
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
)

View File

@ -1,12 +1,10 @@
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 {
subLabel: points[0].subLabel || "",
label: (points[0].subLabel || "") + "." + points[0].label,
label: 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)),
@ -21,6 +19,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;
}

View File

@ -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(`${process.env.REACT_APP_API_URL}/detect_image`, {
method: "POST",
const response = await fetch('/api/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 });
}
}
}