- added region painting context and components

- can switch to target detection on region select
This commit is contained in:
2026-01-09 16:16:28 +00:00
parent 58e9490a09
commit 1555221825
9 changed files with 308 additions and 25 deletions

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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