- added region painting context and components
- can switch to target detection on region select
This commit is contained in:
@@ -1,19 +1,28 @@
|
||||
import type { CameraSettings, CameraSettingsAction } from "../../utils/types";
|
||||
|
||||
export const initialState: CameraSettings = {
|
||||
cameraMode: 0,
|
||||
mode: 0,
|
||||
imageSize: { width: 1280, height: 960 },
|
||||
regionPainter: {
|
||||
paintedCells: [],
|
||||
paintmode: "painter",
|
||||
paintedCells: new Map(),
|
||||
regions: [
|
||||
{ name: "Region 1", brushColour: "#FF0000", cells: [] },
|
||||
{ name: "Region 2", brushColour: "#00FF00", cells: [] },
|
||||
{ name: "Region 1", brushColour: "#FF0000" },
|
||||
{ name: "Region 2", brushColour: "#00FF00" },
|
||||
{ name: "Region 3", brushColour: "#0000FF" },
|
||||
],
|
||||
selectedRegionIndex: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const cameraSettingsReducer = (state: CameraSettings, action: CameraSettingsAction) => {
|
||||
switch (action.type) {
|
||||
case "SET_CAMERA_MODE":
|
||||
return {
|
||||
...state,
|
||||
cameraMode: action.payload,
|
||||
};
|
||||
case "SET_MODE":
|
||||
return {
|
||||
...state,
|
||||
@@ -24,6 +33,23 @@ export const cameraSettingsReducer = (state: CameraSettings, action: CameraSetti
|
||||
...state,
|
||||
imageSize: action.payload,
|
||||
};
|
||||
case "SET_REGION_PAINTMODE":
|
||||
return {
|
||||
...state,
|
||||
regionPainter: {
|
||||
...state.regionPainter,
|
||||
paintmode: action.payload,
|
||||
},
|
||||
};
|
||||
|
||||
case "SET_SELECTED_REGION_INDEX":
|
||||
return {
|
||||
...state,
|
||||
regionPainter: {
|
||||
...state.regionPainter,
|
||||
selectedRegionIndex: action.payload,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Formik, Form } from "formik";
|
||||
import type { CameraSettings } from "../../../../utils/types";
|
||||
|
||||
type CameraControlProps = {
|
||||
tabIndex: number;
|
||||
state: CameraSettings;
|
||||
};
|
||||
|
||||
const CameraControls = ({ tabIndex }: CameraControlProps) => {
|
||||
const CameraControls = ({ state }: CameraControlProps) => {
|
||||
console.log(state);
|
||||
const initialValues = {};
|
||||
|
||||
console.log(tabIndex);
|
||||
const handleSumbit = (values: { test?: string }) => {
|
||||
console.log(values);
|
||||
};
|
||||
|
||||
@@ -3,24 +3,32 @@ import "react-tabs/style/react-tabs.css";
|
||||
import Card from "../../../../components/ui/Card";
|
||||
import CameraControls from "../cameraControls/CameraControls";
|
||||
import { useState } from "react";
|
||||
import Region from "../region/Region";
|
||||
import { useCameraSettingsContext } from "../../../../app/context/CameraSettingsContext";
|
||||
|
||||
const CameraSetup = () => {
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
console.log(tabIndex);
|
||||
const { state, dispatch } = useCameraSettingsContext();
|
||||
return (
|
||||
<Card className="p-4">
|
||||
<Tabs selectedIndex={tabIndex} onSelect={(index) => setTabIndex(index)}>
|
||||
<Tabs
|
||||
selectedIndex={tabIndex}
|
||||
onSelect={(index) => {
|
||||
dispatch({ type: "SET_CAMERA_MODE", payload: index });
|
||||
setTabIndex(index);
|
||||
}}
|
||||
>
|
||||
<TabList>
|
||||
<Tab>Camera</Tab>
|
||||
<Tab>Regions</Tab>
|
||||
<Tab>Target Detection</Tab>
|
||||
<Tab>Crop</Tab>
|
||||
<Tab>Advanced</Tab>
|
||||
</TabList>
|
||||
<TabPanel>
|
||||
<CameraControls tabIndex={tabIndex} />
|
||||
<CameraControls state={state} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<div>Regions</div>
|
||||
<Region state={state} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<div>Crop</div>
|
||||
|
||||
@@ -21,8 +21,8 @@ const PlatePatchSetup = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sightingList?.map((sighting) => (
|
||||
<tr key={sighting.vrm} className="border-b border-gray-700/50 hover:bg-gray-700/20">
|
||||
{sightingList?.map((sighting, index) => (
|
||||
<tr key={index} className="border-b border-gray-700/50 hover:bg-gray-700/20">
|
||||
<td className="px-4 py-3 font-mono text-lg">{sighting.vrm}</td>
|
||||
<td className="px-4 py-3 text-center">{sighting.seenCount}</td>
|
||||
<td className="px-4 py-3">{sighting.timeStamp}</td>
|
||||
|
||||
118
src/features/setup/components/region/Region.tsx
Normal file
118
src/features/setup/components/region/Region.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { useCameraSettingsContext } from "../../../../app/context/CameraSettingsContext";
|
||||
import type { CameraSettings } from "../../../../utils/types";
|
||||
|
||||
type RegionProps = {
|
||||
state: CameraSettings;
|
||||
};
|
||||
|
||||
const Region = ({ state }: RegionProps) => {
|
||||
const { dispatch } = useCameraSettingsContext();
|
||||
const paintMode = state.regionPainter.paintmode;
|
||||
const regions = state.regionPainter.regions;
|
||||
|
||||
const handleChangePaintMode = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const mode = event.target.value as "painter" | "eraser";
|
||||
dispatch({ type: "SET_REGION_PAINTMODE", payload: mode });
|
||||
};
|
||||
|
||||
const handlePaintMode = (mode: "painter" | "eraser") => dispatch({ type: "SET_REGION_PAINTMODE", payload: mode });
|
||||
|
||||
const handleChangeRegion = (idx: number) => {
|
||||
dispatch({ type: "SET_SELECTED_REGION_INDEX", payload: idx });
|
||||
};
|
||||
|
||||
const handleSaveClick = () => {
|
||||
console.log(state);
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<div className="border border-gray-600 p-2 rounded-lg w-full">
|
||||
<h2 className="text-2xl mb-2">Tools</h2>
|
||||
<div className="flex flex-col">
|
||||
<label
|
||||
htmlFor="paintMode"
|
||||
className={`p-4 border rounded-lg mb-2
|
||||
${paintMode === "painter" ? "border-gray-400 bg-[#202b36]" : "bg-[#253445] border-gray-700"}
|
||||
hover:bg-[#202b36] hover:cursor-pointer`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id="paintMode"
|
||||
name="paintMode"
|
||||
onChange={handleChangePaintMode}
|
||||
checked={paintMode === "painter"}
|
||||
value="painter"
|
||||
className="sr-only"
|
||||
/>
|
||||
<span className="text-xl">Paint Mode</span>
|
||||
</label>
|
||||
<label
|
||||
htmlFor="eraseMode"
|
||||
className={`p-4 border rounded-lg mb-2
|
||||
${paintMode === "eraser" ? "border-gray-400 bg-[#202b36]" : "bg-[#253445] border-gray-700"}
|
||||
hover:bg-[#202b36] hover:cursor-pointer`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id="eraseMode"
|
||||
name="paintMode"
|
||||
onChange={handleChangePaintMode}
|
||||
checked={paintMode === "eraser"}
|
||||
value="eraser"
|
||||
className="sr-only"
|
||||
/>
|
||||
<span className="text-xl">Eraser</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border border-gray-600 p-2 rounded-lg w-full">
|
||||
<h2 className="text-2xl mb-2">Regions (Lanes)</h2>
|
||||
<div>
|
||||
{regions.map((region, idx) => {
|
||||
const inputID = `region-${idx}`;
|
||||
return (
|
||||
<label
|
||||
htmlFor={inputID}
|
||||
key={region.name}
|
||||
className={`items-center p-4 m-1 rounded-xl border flex flex-row justify-between
|
||||
${state.regionPainter.selectedRegionIndex === idx ? "border-gray-400 bg-[#202b36]" : "bg-[#253445] border-gray-700"} hover:bg-[#202b36] hover:cursor-pointer`}
|
||||
>
|
||||
<div className="flex flex-row gap-4 items-center">
|
||||
<input
|
||||
type="radio"
|
||||
id={inputID}
|
||||
checked={state.regionPainter.selectedRegionIndex === idx}
|
||||
name="region"
|
||||
className="sr-only"
|
||||
onChange={() => {
|
||||
handlePaintMode("painter");
|
||||
handleChangeRegion(idx);
|
||||
}}
|
||||
/>
|
||||
<span className="text-lg">{region.name}</span>
|
||||
<div className="w-6 h-6 rounded mt-1" style={{ backgroundColor: region.brushColour }}></div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border border-gray-600 rounded-lg p-4 flex flex-col">
|
||||
<h2 className="text-xl">Actions</h2>
|
||||
<div className="flex flex-col justify-between w-full mt-4">
|
||||
<button
|
||||
className="p-2 rounded-lg bg-blue-500 text-white w-[40%] hover:bg-blue-800 cursor-pointer"
|
||||
onClick={handleSaveClick}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button className="p-2 rounded-lg bg-gray-500 text-white w-[40%] mt-2">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Region;
|
||||
@@ -1,12 +1,31 @@
|
||||
import { Stage, Layer, Image } from "react-konva";
|
||||
import { Stage, Layer, Image, Shape } from "react-konva";
|
||||
import { useCreateVideoPreviewSnapshot } from "../../hooks/useCreatePreviewImage";
|
||||
import { useEffect, type RefObject } from "react";
|
||||
import { useEffect, useRef, type RefObject } from "react";
|
||||
import { useCameraSettingsContext } from "../../../../app/context/CameraSettingsContext";
|
||||
import type { KonvaEventObject } from "konva/lib/Node";
|
||||
|
||||
const BACKEND_WIDTH = 640;
|
||||
|
||||
const BACKEND_CELL_SIZE = 16;
|
||||
|
||||
const rows = 22.5;
|
||||
const cols = 40;
|
||||
const gap = 0;
|
||||
|
||||
const VideoFeedSetup = () => {
|
||||
const { latestBitmapRef, isLoading } = useCreateVideoPreviewSnapshot();
|
||||
const { state, dispatch } = useCameraSettingsContext();
|
||||
const cameraMode = state.cameraMode;
|
||||
const paintedCells = state.regionPainter.paintedCells;
|
||||
const paintMode = state.regionPainter.paintmode;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const paintLayerRef = useRef<any>(null);
|
||||
const size = state.imageSize;
|
||||
const selectedRegionIndex = state.regionPainter.selectedRegionIndex;
|
||||
const region = state.regionPainter.regions[selectedRegionIndex];
|
||||
|
||||
const currentScale = size.width / BACKEND_WIDTH;
|
||||
const cellSize = BACKEND_CELL_SIZE * currentScale;
|
||||
|
||||
const draw = (bmp: RefObject<ImageBitmap | null>): ImageBitmap | null => {
|
||||
if (!bmp || !bmp.current) {
|
||||
@@ -17,6 +36,45 @@ const VideoFeedSetup = () => {
|
||||
};
|
||||
const image = draw(latestBitmapRef);
|
||||
|
||||
const paintCell = (x: number, y: number) => {
|
||||
const col = Math.floor(x / (cellSize + gap));
|
||||
const row = Math.floor(y / (cellSize + gap));
|
||||
|
||||
if (row < 0 || row >= rows || col < 0 || col >= cols) return;
|
||||
|
||||
const activeRegion = region;
|
||||
if (!activeRegion) return;
|
||||
const cellKey = `${row}-${col}`;
|
||||
const currentColour = region.brushColour;
|
||||
|
||||
const map = paintedCells;
|
||||
const existing = map.get(cellKey);
|
||||
|
||||
if (paintMode === "eraser") {
|
||||
if (map.has(cellKey)) {
|
||||
map.delete(cellKey);
|
||||
paintLayerRef.current?.batchDraw();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (existing && existing.colour === currentColour) return;
|
||||
map.set(cellKey, { colour: currentColour, region: activeRegion });
|
||||
paintLayerRef.current?.batchDraw();
|
||||
};
|
||||
|
||||
const handleStageMouseDown = (e: KonvaEventObject<MouseEvent>) => {
|
||||
if (!(cameraMode === 1)) return;
|
||||
const pos = e.target.getStage()?.getPointerPosition();
|
||||
if (pos) paintCell(pos.x, pos.y);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: KonvaEventObject<MouseEvent>) => {
|
||||
if (!(cameraMode === 1)) return;
|
||||
const pos = e.target.getStage()?.getPointerPosition();
|
||||
if (pos && e.evt.buttons === 1) paintCell(pos.x, pos.y);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const updateSize = () => {
|
||||
const width = window.innerWidth * 0.48;
|
||||
@@ -31,8 +89,35 @@ const VideoFeedSetup = () => {
|
||||
if (isLoading) return <>Loading...</>;
|
||||
return (
|
||||
<div className="mt-[1%]">
|
||||
<Stage width={size.width} height={size.height}>
|
||||
<Stage width={size.width} height={size.height} onMouseDown={handleStageMouseDown} onMouseMove={handleMouseMove}>
|
||||
<Layer>{image && <Image image={image} height={size.height} width={size.width} cornerRadius={10} />}</Layer>
|
||||
<Layer ref={paintLayerRef} opacity={0.6}>
|
||||
{cameraMode === 1 && (
|
||||
<Shape
|
||||
sceneFunc={(ctx, shape) => {
|
||||
const cells = paintedCells;
|
||||
if (!cells || cells.size === 0 || !paintLayerRef.current) return;
|
||||
cells?.forEach((cell, key) => {
|
||||
const [rowStr, colStr] = key.split("-");
|
||||
const row = Number(rowStr);
|
||||
const col = Number(colStr);
|
||||
|
||||
const x = col * (cellSize + gap);
|
||||
const y = row * (cellSize + gap);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.rect(x, y, cellSize, cellSize);
|
||||
ctx.fillStyle = cell.colour;
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
ctx.fillStrokeShape(shape);
|
||||
}}
|
||||
width={size.width}
|
||||
height={size.height}
|
||||
/>
|
||||
)}
|
||||
</Layer>
|
||||
</Stage>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useVideoPreview } from "./useVideoPreview";
|
||||
import { useCameraSettingsContext } from "../../../app/context/CameraSettingsContext";
|
||||
|
||||
export const useCreateVideoPreviewSnapshot = () => {
|
||||
const { videoPreviewQuery } = useVideoPreview();
|
||||
const { state } = useCameraSettingsContext();
|
||||
const { videoPreviewQuery, targetDetectionFeedQuery } = useVideoPreview(state.cameraMode);
|
||||
const latestBitmapRef = useRef<ImageBitmap | null>(null);
|
||||
const isLoading = videoPreviewQuery?.isPending;
|
||||
const imageBlob = videoPreviewQuery?.data;
|
||||
|
||||
const isLoading = videoPreviewQuery?.isPending || targetDetectionFeedQuery?.isPending;
|
||||
let snapshot;
|
||||
if (state.cameraMode === 0) {
|
||||
snapshot = videoPreviewQuery?.data;
|
||||
} else if (state.cameraMode === 1) {
|
||||
snapshot = targetDetectionFeedQuery?.data;
|
||||
}
|
||||
const imageBlob = snapshot;
|
||||
useEffect(() => {
|
||||
async function createImageBitmapFromBlob() {
|
||||
if (!imageBlob) return;
|
||||
|
||||
@@ -9,11 +9,27 @@ const fetchVideoPreview = async () => {
|
||||
return response.blob();
|
||||
};
|
||||
|
||||
export const useVideoPreview = () => {
|
||||
const fetchTargetDectionFeed = async () => {
|
||||
const response = await fetch(`${cambase}/TargetDetectionColour-preview`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch target detection feed");
|
||||
}
|
||||
return response.blob();
|
||||
};
|
||||
|
||||
export const useVideoPreview = (mode: number) => {
|
||||
const videoPreviewQuery = useQuery({
|
||||
queryKey: ["videoPreview"],
|
||||
queryFn: fetchVideoPreview,
|
||||
refetchInterval: 100,
|
||||
enabled: mode === 0,
|
||||
});
|
||||
return { videoPreviewQuery };
|
||||
|
||||
const targetDetectionFeedQuery = useQuery({
|
||||
queryKey: ["targetDetectionFeed"],
|
||||
queryFn: fetchTargetDectionFeed,
|
||||
refetchInterval: 100,
|
||||
enabled: mode === 1,
|
||||
});
|
||||
return { videoPreviewQuery, targetDetectionFeedQuery };
|
||||
};
|
||||
|
||||
@@ -52,12 +52,25 @@ export type NpedJSON = {
|
||||
"INSURANCE STATUS": string;
|
||||
};
|
||||
|
||||
export type Region = {
|
||||
name: string;
|
||||
brushColour: string;
|
||||
};
|
||||
|
||||
export type PaintedCell = {
|
||||
colour: string;
|
||||
region: Region;
|
||||
};
|
||||
|
||||
export type CameraSettings = {
|
||||
cameraMode: number;
|
||||
mode: number;
|
||||
imageSize: { width: number; height: number };
|
||||
regionPainter: {
|
||||
paintedCells: { x: number; y: number }[];
|
||||
regions: { name: string; brushColour: string; cells: { x: number; y: number }[] }[];
|
||||
paintmode: "painter" | "eraser";
|
||||
paintedCells: Map<string, PaintedCell>;
|
||||
regions: Region[];
|
||||
selectedRegionIndex: number;
|
||||
};
|
||||
};
|
||||
export type CameraSettingsAction =
|
||||
@@ -68,7 +81,16 @@ export type CameraSettingsAction =
|
||||
| {
|
||||
type: "SET_IMAGE_SIZE";
|
||||
payload: { width: number; height: number };
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "SET_CAMERA_MODE";
|
||||
payload: number;
|
||||
}
|
||||
| {
|
||||
type: "SET_REGION_PAINTMODE";
|
||||
payload: "painter" | "eraser";
|
||||
}
|
||||
| { type: "SET_SELECTED_REGION_INDEX"; payload: number };
|
||||
|
||||
export type CameraStatus = {
|
||||
id: string;
|
||||
|
||||
Reference in New Issue
Block a user