Update Config label

This commit is contained in:
nguentrungthat 2025-03-11 09:25:24 +07:00
parent 00c356268d
commit 3e1b5d1ba3
13 changed files with 733 additions and 244 deletions

4
.gitignore vendored
View File

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

View File

@ -1,21 +1,18 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta name="description" content="Web site created using create-react-app" />
name="description" <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
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 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/ 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. Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build. 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. 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. 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`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>React App</title> <title>AI Tool</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<!-- <!--
This HTML file is a template. This HTML file is a template.
If you open it directly in the browser, you will see an empty page. 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 begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`. To create a production bundle, use `npm run build` or `yarn build`.
--> -->
</body> </body>
</html> </html>

View File

@ -1,24 +1,39 @@
import '@mantine/core/styles.css'; import "@mantine/core/styles.css";
import './App.css'; import "./App.css";
import { Dropzone } from './pages/components/Dropzone'; import { Dropzone } from "./pages/components/Dropzone";
import { ActionIcon, AppShell, Box, Burger, Button, Group, LoadingOverlay, Progress, ScrollArea, Text, Tooltip } from '@mantine/core'; import { ActionIcon, AppShell, Box, Burger, Button, Group, LoadingOverlay, Modal, Progress, ScrollArea, Tabs, Text, Tooltip } from "@mantine/core";
import { useDisclosure, useHotkeys } from '@mantine/hooks'; import { useDisclosure, useHotkeys } from "@mantine/hooks";
import { notifications } from '@mantine/notifications'; import { notifications } from "@mantine/notifications";
import { IconChevronLeft, IconChevronRight, IconImageInPicture, IconRefreshDot, IconTrash } from '@tabler/icons-react'; import {
import { useEffect, useMemo, useRef, useState } from 'react'; IconChevronLeft,
import { ImageDetect } from './pages/components/ImageDetect'; IconChevronRight,
import ImageLabel from './pages/components/ImageLabel'; IconEdit,
import { useImagesDetected } from './stores/use-images-detected'; IconImageInPicture,
import { SaveButton } from './pages/components/SaveButton'; IconPlus,
import { generateClientID } from './ultils'; 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() { function App() {
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure(); const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true); const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
const [clickData, setClickData] = useState(null); 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(); const { images, clearImagesDetected, appendImageDetect, setImageDetected } = useImagesDetected();
@ -27,13 +42,28 @@ function App() {
const imageLabelRef = useRef(); const imageLabelRef = useRef();
const dropzoneRef = 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 handleClear = () => {
// const result = window.confirm('Are you want to clear ?'); // const result = window.confirm('Are you want to clear ?');
const ID_NOTI = 'handle-clear'; const ID_NOTI = "handle-clear";
notifications.show({ notifications.show({
id: ID_NOTI, id: ID_NOTI,
title: 'Are you want to clear ?', title: "Are you want to clear ?",
message: ( message: (
<Box className="flex items-center gap-2 justify-end"> <Box className="flex items-center gap-2 justify-end">
<Button <Button
@ -55,11 +85,11 @@ function App() {
}; };
const handleDeleteImage = (data, index) => { const handleDeleteImage = (data, index) => {
const ID_NOTI = 'handle-delete-image'; const ID_NOTI = "handle-delete-image";
const deleteImage = async () => { const deleteImage = async () => {
try { 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(); const result = await response.json();
@ -76,7 +106,7 @@ function App() {
notifications.show({ notifications.show({
id: ID_NOTI, id: ID_NOTI,
title: 'Are you want to delete ?', title: "Are you want to delete ?",
message: ( message: (
<Box className="flex items-center gap-2 justify-end"> <Box className="flex items-center gap-2 justify-end">
<Button onClick={deleteImage} color="red" size="xs"> <Button onClick={deleteImage} color="red" size="xs">
@ -132,10 +162,10 @@ function App() {
processFiles: 0, 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) => { worker.onmessage = (event) => {
if (event.data.status === 'success') { if (event.data.status === "success") {
if (event.data?.result) { if (event.data?.result) {
setImageDetected(event.data.result); setImageDetected(event.data.result);
setProgress({ setProgress({
@ -151,11 +181,11 @@ function App() {
} }
} }
} else { } 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 = () => { const handlePrev = () => {
@ -188,132 +218,294 @@ function App() {
return true; return true;
}, [images, clickData]); }, [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([ useHotkeys([
['ArrowRight', handleNext], ["ArrowRight", handleNext],
['ArrowLeft', handlePrev], ["ArrowLeft", handlePrev],
]); ]);
return ( return (
<div style={{ position: 'relative' }}> <div style={{ position: "relative" }}>
<AppShell <AppShell
header={{ height: 60 }} header={{ height: 60 }}
navbar={{ navbar={{
width: 300, width: 300,
breakpoint: 'sm', breakpoint: "sm",
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened }, collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
}} }}
padding="md"> padding="md">
<AppShell.Header> <Tabs color="teal" defaultValue="detect">
<Box className="flex items-center justify-between h-full"> <AppShell.Header>
<Group h="100%" px="md"> <Box className="flex items-center justify-between h-full">
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" /> <Group h="100%" px="md">
<Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" /> <Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
</Group> <Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" />
</Group>
<Box className="px-4 flex items-center gap-4 w-fit"> <Box className="px-4 flex items-center gap-4 w-fit">
<Button disabled={!clickData} onClick={handleClearSelect} leftSection={<IconRefreshDot size={14} />} color="orange"> <Tabs.List>
Reset select <Tabs.Tab value="detect">
</Button> <span style={{ fontSize: 20 }}>Detect</span>
<SaveButton </Tabs.Tab>
onSaved={(data) => { <Tabs.Tab value="config">
handleNext(); <span style={{ fontSize: 20 }}>Config label</span>
}} </Tabs.Tab>
currentData={clickData} </Tabs.List>
imageLabelRef={imageLabelRef}
/>
</Box>
</Box>
</AppShell.Header>
<AppShell.Navbar p="md">
<Box className="flex flex-col justify-between gap-2 w-full h-full">
<Box className="flex items-center justify-between gap-4 mb-2">
<Button fullWidth disabled={!images.length} onClick={handleClear} leftSection={<IconTrash size={14} />} color="red">
Clear
</Button>
<Button fullWidth disabled={images.length} onClick={handleAddImages} leftSection={<IconImageInPicture size={14} />}>
Add images
</Button>
</Box>
<ScrollArea h={780} className="pb-5 flex-1">
<Box className="flex flex-col gap-3 w-full">
{images.length > 0 &&
images.map((item, index) => {
return (
<ImageDetect
currentData={clickData}
onClick={(data) => {
console.log({ data });
setClickData(data);
}}
key={`${item?.name || item?.image_name}_${index}`}
file={item}
onDelete={(data) => {
handleDeleteImage(data, index);
}}
/>
);
})}
</Box> </Box>
<Box className="px-4 flex items-center gap-4 w-fit">
{images.length <= 0 && ( <Button disabled={isLoading} onClick={() => setOpenConfirm(true)} leftSection={<IconReportAnalytics size={14} />} color="#caa32c">
<Box className="flex items-center justify-center"> Training
<span>No images to process</span> </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> </Box>
)}
</ScrollArea>
{progress.total > 0 && ( <ScrollArea h={780} className="pb-5 flex-1">
<Box className="flex flex-col items-center justify-end w-full"> <Box className="flex flex-col gap-3 w-full">
<Text> {images.length > 0 &&
{progress.processFiles} / {progress.total} images.map((item, index) => {
</Text> return (
<Progress className="w-full" value={(progress.processFiles * 100) / progress.total} striped animated /> <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>
</Box> <AppShell.Main style={{ position: "relative" }}>
</AppShell.Navbar> {clickData && <ImageLabel ref={imageLabelRef} data={clickData} />}
<AppShell.Main style={{ position: 'relative' }}>
{clickData && <ImageLabel ref={imageLabelRef} data={clickData} />}
<Box <Box
style={{ style={{
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
gap: '10px', gap: "10px",
width: '100%', width: "100%",
flexDirection: 'column', flexDirection: "column",
}}> }}>
<Dropzone <Dropzone
openRef={dropzoneRef} openRef={dropzoneRef}
hidden={!clickData} hidden={!clickData}
onErorrFiles={(errors) => { onErorrFiles={(errors) => {
notifications.show({ notifications.show({
title: 'Invalid Images', title: "Invalid Images",
message: `There ${errors.length === 1 ? 'is' : 'are'} ${errors.length} invalid image${ message: `There ${errors.length === 1 ? "is" : "are"} ${errors.length} invalid image${
errors.length > 1 ? 's' : '' errors.length > 1 ? "s" : ""
}. Please re-check!`, }. Please re-check!`,
color: 'red', 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} color="#1a9b20">
/> Confirm
</Box> </Button>
</div>
<Box className="fixed bottom-5 right-5 flex items-center gap-4"> </Modal>
<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> </AppShell>
</div> </div>
); );

View File

@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState, forwardRef, useImperativeHandle, memo } from 'react'; import React, { useEffect, useRef, useState, forwardRef, useImperativeHandle, memo } from "react";
import { Box, LoadingOverlay } from '@mantine/core'; import { Box, LoadingOverlay } from "@mantine/core";
import PointsBlur from './PointsBlur'; import PointsBlur from "./PointsBlur";
import { generateImageUrl } from '../../ultils'; import { generateImageUrl } from "../../ultils";
const ImageLabel = forwardRef(({ data }, ref) => { const ImageLabel = forwardRef(({ data }, ref) => {
const [imageSrc, setImageSrc] = useState(null); const [imageSrc, setImageSrc] = useState(null);
@ -10,7 +10,7 @@ const ImageLabel = forwardRef(({ data }, ref) => {
const [reloading, setReloading] = useState(false); const [reloading, setReloading] = useState(false);
const [listLabel, setListLabel] = useState([]); const [listLabel, setListLabel] = useState([]);
const [infoImage, setInfoImage] = useState({}); const [infoImage, setInfoImage] = useState({});
const [imageName, setImageName] = useState(''); const [imageName, setImageName] = useState("");
// Expose blurredRegions cho component cha // Expose blurredRegions cho component cha
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
@ -33,18 +33,23 @@ const ImageLabel = forwardRef(({ data }, ref) => {
alert(data.error); alert(data.error);
} else { } else {
if (data?.points) { if (data?.points) {
blurredRegions.current = data?.points?.map((pre) => [ blurredRegions.current = data?.points?.map((pre) => {
{ x: pre.x1, y: pre.y1, label: pre.label }, const arrLabel = pre?.label?.split(".");
{ x: pre.x2, y: pre.y1, label: pre.label }, const label = arrLabel?.length > 1 ? arrLabel[1] : arrLabel[0];
{ x: pre.x2, y: pre.y2, label: pre.label }, const subLabel = arrLabel?.length > 1 ? arrLabel[0] : null;
{ x: pre.x1, y: pre.y2, label: pre.label }, 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( setListLabel(
Object.entries(data?.labels).map(([id, name]) => ({ Object.entries(data?.labels).map(([id, name]) => ({
id: Number(id), id: Number(id),
name, name,
})), }))
); );
setImageSrc(generateImageUrl(data?.image_path)); setImageSrc(generateImageUrl(data?.image_path));
setImageName(data?.image_name); setImageName(data?.image_name);

View File

@ -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;

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 PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded, reloading, setReloading, setInfoImage, listLabel = [] }) => {
const points = useRef([]); const points = useRef([]);
@ -12,7 +12,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
useEffect(() => { useEffect(() => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext("2d");
const image = new Image(); const image = new Image();
image.onload = () => { image.onload = () => {
@ -23,8 +23,12 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
setInfoImage({ width: image.width, height: image.height }); setInfoImage({ width: image.width, height: image.height });
}; };
image.crossOrigin = 'anonymous'; image.crossOrigin = "anonymous";
image.src = imageSrc; image.src = imageSrc;
setInfo({});
setIsOpen(false);
points.current = []; // Clear points after blurring
setReloading((pre) => !pre);
}, [imageSrc]); }, [imageSrc]);
useEffect(() => { useEffect(() => {
@ -39,7 +43,6 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
const handleCanvasClick = (event) => { const handleCanvasClick = (event) => {
if (!isImageLoaded.current) return; if (!isImageLoaded.current) return;
if (isOpen) return;
const { x, y } = getMousePos(canvas, event); const { x, y } = getMousePos(canvas, event);
// Check if a delete button is clicked // Check if a delete button is clicked
@ -49,14 +52,22 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
const buttonY = Math.min(region[0].y, region[1].y, region[2].y, region[3].y) - 20; const buttonY = Math.min(region[0].y, region[1].y, region[2].y, region[3].y) - 20;
if (Math.abs(buttonX - x) < 30 && Math.abs(buttonY - y) < 10) { if (Math.abs(buttonX - x) < 30 && Math.abs(buttonY - y) < 10) {
// blurredRegions.current.splice(i, 1); // blurredRegions.current.splice(i, 1);
if (info?.index === i) {
setInfo({});
setIsOpen(false);
return;
}
setInfo({ index: i, value: blurredRegions.current[i] }); setInfo({ index: i, value: blurredRegions.current[i] });
setIsOpen(true); setIsOpen(true);
redrawCanvas(i); redrawCanvas(i);
return; return;
} }
} }
if (isOpen) return;
points.current.push({ x, y, label: listLabel[0]?.name || 'cisco' }); 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); drawRedPoint(canvas, x, y);
if (points.current.length === 4) { if (points.current.length === 4) {
@ -67,11 +78,10 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
}; };
const handleCanvasMouseDown = (event) => { const handleCanvasMouseDown = (event) => {
if (isOpen) return;
const { x, y } = getMousePos(canvas, event); const { x, y } = getMousePos(canvas, event);
// Check if we clicked on an existing point
for (let i = 0; i < blurredRegions.current.length; i++) { 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++) { for (let j = 0; j < blurredRegions.current[i].length; j++) {
const point = blurredRegions.current[i][j]; const point = blurredRegions.current[i][j];
if (Math.abs(point.x - x) < 5 && Math.abs(point.y - y) < 5) { 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 // Check if we clicked inside a region
const region = blurredRegions.current[i]; const region = blurredRegions.current[i];
if (isPointInRegion(x, y, region)) { 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); setMovingRegionIndex(i);
setOffset({ x: region[0].x - x, y: region[0].y - y }); setOffset({ x: region[0].x - x, y: region[0].y - y });
return; return;
@ -110,6 +139,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
x: x + offset.x, x: x + offset.x,
y: y + offset.y, y: y + offset.y,
label: blurredRegions.current[regionIndex][pointIndex]?.label, label: blurredRegions.current[regionIndex][pointIndex]?.label,
subLabel: blurredRegions.current[regionIndex][pointIndex]?.subLabel,
}; };
redrawCanvas(); redrawCanvas();
} else if (movingRegionIndex !== null) { } else if (movingRegionIndex !== null) {
@ -125,14 +155,14 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
} }
}; };
canvas.addEventListener('mousedown', handleCanvasMouseDown); canvas.addEventListener("mousedown", handleCanvasMouseDown);
canvas.addEventListener('mouseup', handleCanvasMouseUp); canvas.addEventListener("mouseup", handleCanvasMouseUp);
canvas.addEventListener('mousemove', handleCanvasMouseMove); canvas.addEventListener("mousemove", handleCanvasMouseMove);
return () => { return () => {
canvas.removeEventListener('mousedown', handleCanvasMouseDown); canvas.removeEventListener("mousedown", handleCanvasMouseDown);
canvas.removeEventListener('mouseup', handleCanvasMouseUp); canvas.removeEventListener("mouseup", handleCanvasMouseUp);
canvas.removeEventListener('mousemove', handleCanvasMouseMove); canvas.removeEventListener("mousemove", handleCanvasMouseMove);
}; };
}, [draggingPointIndex, movingRegionIndex, offset, isOpen, info]); }, [draggingPointIndex, movingRegionIndex, offset, isOpen, info]);
@ -160,17 +190,17 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
}; };
const drawRedPoint = (canvas, x, y) => { const drawRedPoint = (canvas, x, y) => {
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext("2d");
ctx.save(); ctx.save();
ctx.beginPath(); ctx.beginPath();
ctx.arc(x, y, 5, 0, 2 * Math.PI); ctx.arc(x, y, 5, 0, 2 * Math.PI);
ctx.fillStyle = 'red'; ctx.fillStyle = "red";
ctx.fill(); ctx.fill();
ctx.restore(); ctx.restore();
}; };
const drawBorder = (canvas, points) => { const drawBorder = (canvas, points) => {
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext("2d");
ctx.save(); ctx.save();
ctx.beginPath(); ctx.beginPath();
points.forEach((point, index) => { points.forEach((point, index) => {
@ -182,7 +212,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
}); });
ctx.closePath(); ctx.closePath();
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.strokeStyle = 'blue'; ctx.strokeStyle = "blue";
ctx.stroke(); ctx.stroke();
ctx.restore(); ctx.restore();
@ -190,23 +220,26 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
}; };
const drawDeleteButton = (canvas, region, isSelected) => { const drawDeleteButton = (canvas, region, isSelected) => {
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext("2d");
const buttonX = (region[0].x + region[1].x) / 2; const buttonX = (region[0].x + region[1].x) / 2;
const buttonY = Math.min(region[0].y, region[1].y, region[2].y, region[3].y) - 20; const buttonY = Math.min(region[0].y, region[1].y, region[2].y, region[3].y) - 20;
ctx.save(); ctx.save();
ctx.fillStyle = isSelected ? 'rgb(196, 194, 61)' : 'rgb(2, 101, 182)'; ctx.fillStyle = isSelected ? "rgb(196, 194, 61)" : "rgb(2, 101, 182)";
ctx.fillRect(buttonX - 30, buttonY - 10, 60, 20); ctx.fillRect(buttonX - 30, buttonY - 10, Math.max(region[0]?.label?.length * 6 + 20, region[0]?.subLabel?.length * 5 + 10), 25);
ctx.fillStyle = 'rgb(255, 255, 255)'; ctx.fillStyle = "rgb(255, 255, 255)";
ctx.font = '13px Arial'; ctx.font = "9px Arial";
ctx.textAlign = 'center'; ctx.textAlign = "center";
ctx.fillText(region[0]?.label, buttonX, buttonY + 5); 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(); ctx.restore();
}; };
const redrawCanvas = (index) => { const redrawCanvas = (index) => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext("2d");
const image = new Image(); const image = new Image();
image.onload = () => { image.onload = () => {
@ -223,29 +256,39 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
}); });
}; };
image.crossOrigin = 'anonymous'; image.crossOrigin = "anonymous";
image.src = imageSrc; image.src = imageSrc;
}; };
return ( return (
<div style={{ position: 'relative' }}> <div style={{ position: "relative" }}>
<canvas ref={canvasRef} /> <canvas ref={canvasRef} />
{isOpen && ( {isOpen && (
<div style={{ backgroundColor: '#FFF', position: 'absolute', top: info?.value[0].y - 200, left: info?.value[0].x, zIndex: 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> <div>
<span style={{ color: '#000', fontSize: '18px', fontWeight: 'bold' }}>Select label</span> <span style={{ color: "#000", fontSize: "18px", fontWeight: "bold" }}>Select label</span>
</div> </div>
{listLabel?.map((el, i) => ( {listLabel?.map((el, i) => (
<div key={i}> <div key={i}>
<button <button
onClick={() => { 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; blurredRegions.current[info?.index] = newList;
setInfo({}); setInfo({});
setIsOpen(false); setIsOpen(false);
redrawCanvas(); redrawCanvas();
}} }}
style={{ backgroundColor: '#ccc', width: '150px', color: '#000' }}> style={{ backgroundColor: "#ccc", width: "150px", color: "#000" }}>
{el?.name} {el?.name}
</button> </button>
</div> </div>
@ -258,7 +301,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
setIsOpen(false); setIsOpen(false);
redrawCanvas(); redrawCanvas();
}} }}
style={{ backgroundColor: '#ff0000', marginBottom: '10px', width: '150px', color: '#fff' }}> style={{ backgroundColor: "#ff0000", marginBottom: "10px", width: "150px", color: "#fff" }}>
Delete Delete
</button> </button>
</div> </div>

View File

@ -1,12 +1,12 @@
import { Button, Tooltip, LoadingOverlay } from '@mantine/core'; import { Button, Tooltip, LoadingOverlay } from "@mantine/core";
import { notifications } from '@mantine/notifications'; import { notifications } from "@mantine/notifications";
import { IconImageInPicture } from '@tabler/icons-react'; import { IconImageInPicture } from "@tabler/icons-react";
import { useState } from 'react'; import { useState } from "react";
import { useImagesDetected } from '../../stores/use-images-detected'; import { useImagesDetected } from "../../stores/use-images-detected";
import { convertToBoundingBox } from '../../ultils'; import { convertToBoundingBox } from "../../ultils";
import { useHotkeys } from '@mantine/hooks'; 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 [loading, setLoading] = useState(false);
const { appendImageDetect, images } = useImagesDetected(); const { appendImageDetect, images } = useImagesDetected();
@ -14,7 +14,7 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
const handleSave = async ({ blurredRegions, infoImage, imageName, listLabel }) => { const handleSave = async ({ blurredRegions, infoImage, imageName, listLabel }) => {
try { try {
if (blurredRegions?.length > 0) { if (blurredRegions?.length > 0) {
let arrPoints = ''; let arrPoints = "";
blurredRegions?.forEach((points) => { blurredRegions?.forEach((points) => {
const img_w = infoImage?.width; const img_w = infoImage?.width;
const img_h = infoImage?.height; const img_h = infoImage?.height;
@ -31,19 +31,21 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
const height = (y_max - y_min) / img_h; const height = (y_max - y_min) / img_h;
// Get class ID // Get class ID
const class_id = listLabel.find((label) => label?.name === points[0].label)?.id || '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)}`; 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 = `${process.env.REACT_APP_API_URL}/save`;
// const url = `http://127.0.0.1:5000/save`;
const formData = new FormData(); const formData = new FormData();
formData.append('list', arrPoints); formData.append("list", arrPoints);
formData.append('imageName', imageName); formData.append("imageName", imageName);
setLoading(true); setLoading(true);
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: "POST",
body: formData, body: formData,
}); });
@ -55,7 +57,7 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
}); });
currentData.points = newPoints; currentData.points = newPoints;
currentData['isSave'] = true; currentData["isSave"] = true;
const newImages = images.map((image) => { const newImages = images.map((image) => {
if (image.image_name === imageName) { if (image.image_name === imageName) {
@ -72,9 +74,9 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
appendImageDetect(newImages); appendImageDetect(newImages);
notifications.show({ notifications.show({
title: 'Save Success', title: "Save Success",
message: `${imageName} save success`, message: `${imageName} save success`,
color: 'green', color: "green",
}); });
if (onSaved) { if (onSaved) {
@ -84,9 +86,9 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
} }
} catch (error) { } catch (error) {
notifications.show({ notifications.show({
title: 'Save Error', title: "Save Error",
message: error?.message || 'Internal Server Error', message: error?.message || "Internal Server Error",
color: 'red', color: "red",
}); });
} finally { } finally {
setLoading(false); setLoading(false);
@ -101,11 +103,11 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
handleSave(saveData); handleSave(saveData);
}; };
useHotkeys([['ctrl+S', handleSubmit]]); useHotkeys([["ctrl+S", handleSubmit]]);
return ( return (
<Tooltip label={'Ctrl + S'}> <Tooltip label={"Ctrl + S"}>
<Button className="relative ư-f" disabled={!currentData} onClick={handleSubmit} leftSection={<IconImageInPicture size={14} />}> <Button className="relative ư-f" disabled={!currentData || disabled} onClick={handleSubmit} leftSection={<IconImageInPicture size={14} />}>
Save Save
<LoadingOverlay visible={loading} loaderProps={{ size: 14 }} /> <LoadingOverlay visible={loading} loaderProps={{ size: 14 }} />
</Button> </Button>

View File

@ -5,6 +5,7 @@ from routes.save import save_bp
from routes.show import show_bp from routes.show import show_bp
from routes.delete import delete_bp from routes.delete import delete_bp
from routes.upload import upload_bp from routes.upload import upload_bp
from routes.label import config_label
app = Flask(__name__) app = Flask(__name__)
CORS(app) CORS(app)
@ -17,6 +18,7 @@ app.register_blueprint(save_bp)
app.register_blueprint(show_bp) app.register_blueprint(show_bp)
app.register_blueprint(delete_bp) app.register_blueprint(delete_bp)
app.register_blueprint(upload_bp) app.register_blueprint(upload_bp)
app.register_blueprint(config_label)
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True) app.run(debug=True)

View File

@ -9,6 +9,7 @@ import random
import cv2 import cv2
import json import json
import time import time
import subprocess
# Load mô hình YOLO # Load mô hình YOLO
model = YOLO(get_latest_model_including_today(trained_model_folder=TRAINED_MODEL_FOLDER)) model = YOLO(get_latest_model_including_today(trained_model_folder=TRAINED_MODEL_FOLDER))
@ -139,6 +140,19 @@ def reload_model():
model = YOLO(new_model_path) model = YOLO(new_model_path)
return jsonify({"message": f"Model reloaded from {new_model_path}", "status": "true"}), 200 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

141
src/server/routes/label.py Normal file
View File

@ -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 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,13 +7,31 @@ from ultralytics import YOLO
import yaml import yaml
import requests import requests
import glob import glob
import sqlite3
# 🗂️ Configure paths # 🗂️ Configure paths
DATA_SPLIT_FOLDER = "model_datasets" DATA_SPLIT_FOLDER = "model_datasets"
IMAGE_EXTENSION = ".png" IMAGE_EXTENSION = ".png"
LOG_FILE = "training_logs.log" LOG_FILE = "training_logs.log"
class_names = ["cisco", "barcode"] 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()
# 📅 Get today's and yesterday's date # 📅 Get today's and yesterday's date
today = datetime.date.today() today = datetime.date.today()
yesterday = today - datetime.timedelta(days=1) yesterday = today - datetime.timedelta(days=1)
@ -37,9 +55,9 @@ elif os.path.exists(image_folder_yesterday) and os.path.exists(label_folder_yest
label_folder = label_folder_yesterday label_folder = label_folder_yesterday
date_str = yesterday_str date_str = yesterday_str
else: else:
print(image_folder) # print(image_folder)
print(label_folder) # print(label_folder)
raise Exception("⚠️ 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 # 🏗️ Create dataset folders
dataset_folder = os.path.join(DATA_SPLIT_FOLDER, date_str) dataset_folder = os.path.join(DATA_SPLIT_FOLDER, date_str)
@ -78,7 +96,7 @@ def log_message(message: str):
def get_latest_model(trained_model_folder: str, default_model: str = "train5/weights/best.pt"): 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.""" """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): 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 return default_model
today_str = datetime.datetime.now().strftime("%Y-%m-%d") today_str = datetime.datetime.now().strftime("%Y-%m-%d")
@ -93,10 +111,10 @@ def get_latest_model(trained_model_folder: str, default_model: str = "train5/wei
for folder in subfolders: for folder in subfolders:
model_path = os.path.join(trained_model_folder, folder, "weights", "best.pt") model_path = os.path.join(trained_model_folder, folder, "weights", "best.pt")
if os.path.exists(model_path): 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 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 return default_model
def create_dataset_yaml(dataset_folder: str, classes: list): def create_dataset_yaml(dataset_folder: str, classes: list):
@ -122,10 +140,10 @@ def call_reload_model_api(base_url="http://localhost:5000"):
try: try:
response = requests.post(url, timeout=10) response = requests.post(url, timeout=10)
response_data = response.json() response_data = response.json()
log_message(f" API Response: {response_data} \n") log_message(f" API Response: {response_data} \n")
return response_data return response_data
except requests.RequestException as e: 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)} return {"error": str(e)}
def clear_images_source(): def clear_images_source():
@ -159,8 +177,8 @@ def move_files(file_list, dest_img_folder, dest_lbl_folder):
copied_images += 1 copied_images += 1
copied_labels += 1 copied_labels += 1
else: else:
log.write(f"[{datetime.datetime.now()}] ⚠️ Missing label for image: {img_file}\n") 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)) shutil.copy(img_path, os.path.join(nolable_img_folder, img_file))
@ -171,10 +189,10 @@ def train_yolo_model(pretrained_model: str, dataset_folder: str,project_name: st
dataset_yaml = os.path.join(dataset_folder, "data.yaml") dataset_yaml = os.path.join(dataset_folder, "data.yaml")
if not os.path.exists(dataset_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): 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" pretrained_model = "yolov8n.pt"
# 🚀 Tạo model YOLOv8 và load model đã train trước đó # 🚀 Tạo model YOLOv8 và load model đã train trước đó
@ -196,7 +214,7 @@ def train_yolo_model(pretrained_model: str, dataset_folder: str,project_name: st
) )
with open(LOG_FILE, "a") as log: 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() call_reload_model_api()
clear_images_source() clear_images_source()
@ -214,7 +232,7 @@ create_dataset_yaml(dataset_folder, class_names)
# 🏁 Log summary # 🏁 Log summary
with open(LOG_FILE, "a") as log: with open(LOG_FILE, "a") as log:
log.write(f"\n[{datetime.datetime.now()}] Dataset split completed\n") log.write(f"\n[{datetime.datetime.now()}] Dataset split completed\n")
log.write(f"Source folder: {image_folder}\n") log.write(f"Source folder: {image_folder}\n")
log.write(f"Dataset saved in: {dataset_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"Planned Train: {len(train_files)} images, Val: {len(val_files)} images\n")
@ -224,7 +242,7 @@ with open(LOG_FILE, "a") as log:
if(train_copied_imgs <=0 or train_copied_lbls <= 0 or val_copied_imgs <=0 or val_copied_lbls <=0): 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: with open(LOG_FILE, "a") as log:
log.write(f"\n[{datetime.datetime.now()}] Data not qualified\n") log.write(f"\n[{datetime.datetime.now()}] Data not qualified\n")
log.write("=" * 50 + "\n") log.write("=" * 50 + "\n")
else: else:
train_yolo_model(pretrained_model = get_latest_model( train_yolo_model(pretrained_model = get_latest_model(

View File

@ -1,5 +1,6 @@
export const generateImageUrl = (image_path) => { export const generateImageUrl = (image_path) => {
return process.env.REACT_APP_API_URL + `/${image_path}`; return process.env.REACT_APP_API_URL + `/${image_path}`;
// return `http://127.0.0.1:5000/${image_path}`;
}; };
export function convertToBoundingBox(points) { export function convertToBoundingBox(points) {
@ -19,6 +20,6 @@ export const randomDelay = () => {
export function generateClientID() { export function generateClientID() {
const id = Math.random().toString(36).substr(2, 9); const id = Math.random().toString(36).substr(2, 9);
localStorage.setItem('client_id', id); localStorage.setItem("client_id", id);
return id; return id;
} }

View File

@ -4,21 +4,21 @@ self.onmessage = async function (e) {
let processFiles = 0; let processFiles = 0;
if (action === 'processImages') { if (action === "processImages") {
for (const file of files) { for (const file of files) {
try { try {
const formData = new FormData(); const formData = new FormData();
formData.append('image', file, file.name); formData.append("image", file, file.name);
const response = await fetch('/api/detect_image', { const response = await fetch(`${process.env.REACT_APP_API_URL}/detect_image`, {
method: 'POST', method: "POST",
body: formData, body: formData,
}); });
const result = await response.json(); 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) { } catch (error) {
self.postMessage({ status: 'error', error: error.message }); self.postMessage({ status: "error", error: error.message });
} }
} }
} }