Merge pull request 'feature/targetdetection' (#7) from feature/targetdetection into develop

Reviewed-on: #7
This commit is contained in:
2025-11-28 09:21:03 +00:00
13 changed files with 313 additions and 136 deletions

View File

@@ -1,5 +1,5 @@
export const wsConfig = { export const wsConfig = {
infoBar: "ws://100.115.148.59/websocket-infobar", infoBar: "ws://100.115.125.56/websocket-infobar",
}; };
export type SocketKey = keyof typeof wsConfig; export type SocketKey = keyof typeof wsConfig;

View File

@@ -0,0 +1,16 @@
import { createContext, useContext } from "react";
import type { CameraFeedAction, CameraFeedState } from "../../types/types";
type CameraFeedContextType = {
state: CameraFeedState;
// check and refactor
dispatch: (state: CameraFeedAction) => void;
};
export const CameraFeedContext = createContext<CameraFeedContextType | null>(null);
export const useCameraFeedContext = () => {
const ctx = useContext(CameraFeedContext);
if (!ctx) throw new Error("useCameraFeedContext must be used inside <CameraFeedContext.Provider>");
return ctx;
};

View File

@@ -1,11 +1,14 @@
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
import { QueryProvider } from "./QueryProviders"; import { QueryProvider } from "./QueryProviders";
import { WebSocketProvider } from "./WebSocketProvider"; import { WebSocketProvider } from "./WebSocketProvider";
import { CameraFeedProvider } from "./CameraFeedProvider";
export const AppProviders = ({ children }: PropsWithChildren) => { export const AppProviders = ({ children }: PropsWithChildren) => {
return ( return (
<QueryProvider> <QueryProvider>
<WebSocketProvider>{children}</WebSocketProvider> <CameraFeedProvider>
<WebSocketProvider>{children}</WebSocketProvider>
</CameraFeedProvider>
</QueryProvider> </QueryProvider>
); );
}; };

View File

@@ -0,0 +1,9 @@
import { useReducer, type ReactNode } from "react";
import { CameraFeedContext } from "../context/CameraFeedContext";
import { initialState, reducer } from "../reducers/cameraFeedReducer";
export const CameraFeedProvider = ({ children }: { children: ReactNode }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return <CameraFeedContext.Provider value={{ state, dispatch }}>{children}</CameraFeedContext.Provider>;
};

View File

@@ -0,0 +1,95 @@
import type { CameraFeedAction, CameraFeedState, PaintedCell } from "../../types/types";
export const initialState: CameraFeedState = {
cameraFeedID: "A",
paintedCells: {
A: new Map<string, PaintedCell>(),
B: new Map<string, PaintedCell>(),
C: new Map<string, PaintedCell>(),
},
regionsByCamera: {
A: [
{ name: "Region 1", brushColour: "#ff0000" },
{ name: "Region 2", brushColour: "#00ff00" },
{ name: "Region 3", brushColour: "#0400ff" },
],
B: [
{ name: "Region 1", brushColour: "#ff0000" },
{ name: "Region 2", brushColour: "#00ff00" },
],
C: [{ name: "Region 1", brushColour: "#ff0000" }],
},
selectedRegionIndex: 0,
modeByCamera: {
A: "brush",
B: "brush",
C: "brush",
},
};
export function reducer(state: CameraFeedState, action: CameraFeedAction) {
switch (action.type) {
case "SET_CAMERA_FEED":
return {
...state,
cameraFeedID: action.payload,
};
case "CHANGE_MODE":
return {
...state,
modeByCamera: {
...state.modeByCamera,
[action.payload.cameraFeedID]: action.payload.mode,
},
};
case "SET_SELECTED_REGION_INDEX":
return {
...state,
selectedRegionIndex: action.payload,
};
case "SET_SELECTED_REGION_COLOUR":
return {
...state,
regionsByCamera: {
...state.regionsByCamera,
[action.payload.cameraFeedID]: state.regionsByCamera[action.payload.cameraFeedID].map((region) =>
region.name === action.payload.regionName ? { ...region, brushColour: action.payload.newColour } : region,
),
},
};
case "ADD_NEW_REGION":
return {
...state,
regionsByCamera: {
...state.regionsByCamera,
[action.payload.cameraFeedID]: [
...state.regionsByCamera[action.payload.cameraFeedID],
{ name: action.payload.regionName, brushColour: action.payload.brushColour },
],
},
};
case "REMOVE_REGION":
console.log(action.payload);
return {
...state,
regionsByCamera: {
...state.regionsByCamera,
[action.payload.cameraFeedID]: state.regionsByCamera[action.payload.cameraFeedID].filter(
(region) => region.name !== action.payload.regionName,
),
},
};
case "RESET_PAINTED_CELLS":
return {
...state,
paintedCells: {
...state.paintedCells,
[state.cameraFeedID]: new Map<string, PaintedCell>(),
},
};
default:
return state;
}
}

View File

@@ -1,52 +1,17 @@
import { useRef, useState } from "react"; import { useState } from "react";
import VideoFeedGridPainter from "./Video/VideoFeedGridPainter"; import VideoFeedGridPainter from "./Video/VideoFeedGridPainter";
import CameraSettings from "./CameraSettings/CameraSettings"; import CameraSettings from "./CameraSettings/CameraSettings";
import type { PaintedCell, Region } from "../../../types/types";
import PlatePatch from "./PlatePatch/PlatePatch"; import PlatePatch from "./PlatePatch/PlatePatch";
const CameraGrid = () => { const CameraGrid = () => {
const [regions, setRegions] = useState<Region[]>([
{ name: "Region 1", brushColour: "#ff0000" },
{ name: "Region 2", brushColour: "#00ff00" },
{ name: "Region 3", brushColour: "#0400ff" },
]);
const [selectedRegionIndex, setSelectedRegionIndex] = useState(0);
const [mode, setMode] = useState("");
const [tabIndex, setTabIndex] = useState(0); const [tabIndex, setTabIndex] = useState(0);
const updateRegionColour = (index: number, newColour: string) => {
setRegions((prev) => prev.map((r, i) => (i === index ? { ...r, brushColour: newColour } : r)));
};
const paintedCellsRef = useRef<Map<string, PaintedCell>>(new Map());
return ( return (
<div className="grid grid-cols-1 md:grid-cols-5 md:grid-rows-5 max-h-screen"> <div className="grid grid-cols-1 md:grid-cols-5 md:grid-rows-5 max-h-screen">
<VideoFeedGridPainter <VideoFeedGridPainter />
regions={regions} <CameraSettings tabIndex={tabIndex} setTabIndex={setTabIndex} />
selectedRegionIndex={selectedRegionIndex}
mode={mode}
paintedCells={paintedCellsRef}
/>
<CameraSettings
regions={regions}
selectedRegionIndex={selectedRegionIndex}
onSelectRegion={setSelectedRegionIndex}
onChangeRegionColour={updateRegionColour}
mode={mode}
onSelectMode={setMode}
tabIndex={tabIndex}
setTabIndex={setTabIndex}
paintedCells={paintedCellsRef}
onAddRegion={() => {
setRegions((prev) => [...prev, { name: `Region ${prev.length + 1}`, brushColour: "#ffffff" }]);
}}
OnRemoveRegion={() => {
setRegions((prev) => prev.filter((_, i) => i !== selectedRegionIndex));
setSelectedRegionIndex((prev) => (prev > 0 ? prev - 1 : 0));
}}
/>
<PlatePatch /> <PlatePatch />
</div> </div>
); );

View File

@@ -0,0 +1,61 @@
import { Tabs, Tab, TabList, TabPanel } from "react-tabs";
import { useEffect } from "react";
import { useCameraFeedContext } from "../../../../app/context/CameraFeedContext";
import RegionSelector from "./RegionSelector";
type CameraPanelProps = {
tabIndex: number;
};
const CameraPanel = ({ tabIndex }: CameraPanelProps) => {
const { state, dispatch } = useCameraFeedContext();
const cameraFeedID = state.cameraFeedID;
const regions = state.regionsByCamera[cameraFeedID];
const selectedRegionIndex = state.selectedRegionIndex;
const mode = state.modeByCamera[cameraFeedID];
useEffect(() => {
const mapIndextoCameraId = () => {
switch (tabIndex) {
case 0:
return "A";
case 1:
return "B";
case 2:
return "C";
default:
return "A";
}
};
const cameraId = mapIndextoCameraId();
dispatch({ type: "SET_CAMERA_FEED", payload: cameraId });
}, [dispatch, tabIndex]);
return (
<Tabs>
<TabList>
<Tab>Target Detection</Tab>
<Tab>Camera Controls</Tab>
</TabList>
<TabPanel>
<RegionSelector
regions={regions}
selectedRegionIndex={selectedRegionIndex}
mode={mode}
cameraFeedID={cameraFeedID}
/>
</TabPanel>
<TabPanel>
<div className="p-4">
<h2 className="text-lg font-semibold mb-4">Camera Controls</h2>
<p>Controls for camera {cameraFeedID} will go here.</p>
</div>
</TabPanel>
</Tabs>
);
};
export default CameraPanel;

View File

@@ -1,37 +1,14 @@
import Card from "../../../../ui/Card"; import Card from "../../../../ui/Card";
import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
import "react-tabs/style/react-tabs.css"; import "react-tabs/style/react-tabs.css";
import RegionSelector from "./RegionSelector"; import CameraPanel from "./CameraPanel";
import type { PaintedCell, Region } from "../../../../types/types";
import type { RefObject } from "react";
type CameraSettingsProps = { type CameraSettingsProps = {
regions: Region[];
selectedRegionIndex: number;
onSelectRegion: (index: number) => void;
onChangeRegionColour: (index: number, colour: string) => void;
mode: string;
onSelectMode: (mode: string) => void;
setTabIndex: (tabIndex: number) => void; setTabIndex: (tabIndex: number) => void;
tabIndex: number; tabIndex: number;
paintedCells: RefObject<Map<string, PaintedCell>>;
onAddRegion: () => void;
OnRemoveRegion: () => void;
}; };
const CameraSettings = ({ const CameraSettings = ({ tabIndex, setTabIndex }: CameraSettingsProps) => {
regions,
selectedRegionIndex,
onSelectRegion,
onChangeRegionColour,
mode,
onSelectMode,
tabIndex,
setTabIndex,
paintedCells,
onAddRegion,
OnRemoveRegion,
}: CameraSettingsProps) => {
return ( return (
<Card className="p-4 col-span-3 row-span-5 col-start-3 md:col-span-3 md:row-span-5 max-h-screen overflow-auto"> <Card className="p-4 col-span-3 row-span-5 col-start-3 md:col-span-3 md:row-span-5 max-h-screen overflow-auto">
<Tabs <Tabs
@@ -40,32 +17,18 @@ const CameraSettings = ({
onSelect={(index) => setTabIndex(index)} onSelect={(index) => setTabIndex(index)}
> >
<TabList> <TabList>
<Tab>Target Detection</Tab> <Tab>Camera A</Tab>
<Tab>Camera 1</Tab> <Tab>Camera B</Tab>
<Tab>Camera 2</Tab> <Tab>Camera C</Tab>
<Tab>Camera 3</Tab>
</TabList> </TabList>
<TabPanel> <TabPanel>
<RegionSelector <CameraPanel tabIndex={tabIndex} />
regions={regions}
selectedRegionIndex={selectedRegionIndex}
onSelectRegion={onSelectRegion}
onChangeRegionColour={onChangeRegionColour}
mode={mode}
onSelectMode={onSelectMode}
paintedCells={paintedCells}
onAddRegion={onAddRegion}
OnRemoveRegion={OnRemoveRegion}
/>
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>
<div>Camera details {tabIndex}</div> <CameraPanel tabIndex={tabIndex} />
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>
<div>Camera details {tabIndex}</div> <CameraPanel tabIndex={tabIndex} />
</TabPanel>
<TabPanel>
<div>Camera details {tabIndex}</div>
</TabPanel> </TabPanel>
</Tabs> </Tabs>
</Card> </Card>

View File

@@ -1,45 +1,57 @@
import ColourPicker from "./ColourPicker";
import type { PaintedCell, Region } from "../../../../types/types"; import type { PaintedCell, Region } from "../../../../types/types";
import type { RefObject } from "react"; import ColourPicker from "./ColourPicker";
import { useCameraFeedContext } from "../../../../app/context/CameraFeedContext";
type RegionSelectorProps = { type RegionSelectorProps = {
regions: Region[]; regions: Region[];
selectedRegionIndex: number; selectedRegionIndex: number;
onSelectRegion: (index: number) => void;
onChangeRegionColour: (index: number, colour: string) => void;
mode: string; mode: string;
onSelectMode: (mode: string) => void; cameraFeedID: "A" | "B" | "C";
paintedCells: RefObject<Map<string, PaintedCell>>;
onAddRegion: () => void;
OnRemoveRegion: () => void;
}; };
const RegionSelector = ({ const RegionSelector = ({ regions, selectedRegionIndex, mode, cameraFeedID }: RegionSelectorProps) => {
regions, const { dispatch } = useCameraFeedContext();
selectedRegionIndex,
onSelectRegion,
onChangeRegionColour,
mode,
onSelectMode,
paintedCells,
onAddRegion,
OnRemoveRegion,
}: RegionSelectorProps) => {
const handleChange = (e: { target: { value: string } }) => { const handleChange = (e: { target: { value: string } }) => {
onSelectMode(e.target.value); dispatch({ type: "CHANGE_MODE", payload: { cameraFeedID: cameraFeedID, mode: e.target.value } });
}; };
const handleAddClick = () => { const handleAddRegionClick = () => {
onAddRegion(); const regionName = `Region ${regions.length + 1}`;
dispatch({
type: "ADD_NEW_REGION",
payload: { cameraFeedID: cameraFeedID, regionName: regionName, brushColour: "#ffffff" },
});
}; };
const handleResetClick = () => { const handleResetRegion = () => {
const map = paintedCells.current; dispatch({
map.clear(); type: "RESET_PAINTED_CELLS",
payload: { cameraFeedID: cameraFeedID, paintedCells: new Map<string, PaintedCell>() },
});
}; };
const handleRemoveClick = () => { const handleRemoveClick = () => {
OnRemoveRegion(); dispatch({
type: "REMOVE_REGION",
payload: { cameraFeedID: cameraFeedID, regionName: regions[selectedRegionIndex].name },
});
};
const handleModeChange = (newMode: string) => {
dispatch({ type: "CHANGE_MODE", payload: { cameraFeedID: cameraFeedID, mode: newMode } });
};
const handleRegionSelect = (index: number) => {
dispatch({ type: "SET_SELECTED_REGION_INDEX", payload: index });
};
const handleRegionColourChange = (index: number, newColour: string) => {
const regionName = regions[index].name;
dispatch({
type: "SET_SELECTED_REGION_COLOUR",
payload: { cameraFeedID: cameraFeedID, regionName: regionName, newColour: newColour },
});
}; };
return ( return (
@@ -84,7 +96,7 @@ const RegionSelector = ({
<div className="p-2 border border-gray-600 rounded-lg flex flex-col"> <div className="p-2 border border-gray-600 rounded-lg flex flex-col">
<h2 className="text-2xl mb-2">Region Select</h2> <h2 className="text-2xl mb-2">Region Select</h2>
<> <>
{regions.map((region, idx) => { {regions?.map((region, idx) => {
const isSelected = selectedRegionIndex === idx; const isSelected = selectedRegionIndex === idx;
const inputId = `region-${idx}`; const inputId = `region-${idx}`;
return ( return (
@@ -102,20 +114,20 @@ const RegionSelector = ({
name="region" name="region"
className="sr-only" className="sr-only"
onChange={() => { onChange={() => {
onSelectMode("painter"); handleModeChange("painter");
onSelectRegion(idx); handleRegionSelect(idx);
}} }}
/> />
<span className="text-xl">{region.name}</span> <span className="text-xl">{region.name}</span>
</div> </div>
<ColourPicker colour={region.brushColour} setColour={(c: string) => onChangeRegionColour(idx, c)} /> <ColourPicker colour={region.brushColour} setColour={(c: string) => handleRegionColourChange(idx, c)} />
<p className="text-slate-400">{region.brushColour}</p> <p className="text-slate-400">{region.brushColour}</p>
</label> </label>
); );
})} })}
</> </>
<div className=" mx-auto flex flex-row gap-4 mt-4"> <div className=" mx-auto flex flex-row gap-4 mt-4">
<button className="border border-blue-900 bg-blue-700 px-4 rounded-md" onClick={handleAddClick}> <button className="border border-blue-900 bg-blue-700 px-4 rounded-md" onClick={handleAddRegionClick}>
Add Region Add Region
</button> </button>
<button className="border border-red-900 px-4 rounded-md" onClick={handleRemoveClick}> <button className="border border-red-900 px-4 rounded-md" onClick={handleRemoveClick}>
@@ -128,10 +140,10 @@ const RegionSelector = ({
<div className="flex flex-col"> <div className="flex flex-col">
<h2 className="text-2xl mb-2">Actions</h2> <h2 className="text-2xl mb-2">Actions</h2>
<button <button
onClick={handleResetClick} onClick={handleResetRegion}
className="mt-2 px-4 py-2 border border-red-600 rounded-md text-white bg-red-600 w-full md:w-[40%] hover:bg-red-700 hover:cursor-pointer" className="mt-2 px-4 py-2 border border-red-600 rounded-md text-white bg-red-600 w-full md:w-[40%] hover:bg-red-700 hover:cursor-pointer"
> >
Reset Regions Reset Region
</button> </button>
</div> </div>
</div> </div>

View File

@@ -2,22 +2,22 @@ import { useEffect, useRef, useState, type RefObject } from "react";
import { Stage, Layer, Image, Shape } from "react-konva"; import { Stage, Layer, Image, Shape } from "react-konva";
import type { KonvaEventObject } from "konva/lib/Node"; import type { KonvaEventObject } from "konva/lib/Node";
import { useCreateVideoSnapshot } from "../../hooks/useGetvideoSnapshots"; import { useCreateVideoSnapshot } from "../../hooks/useGetvideoSnapshots";
import type { PaintedCell, Region } from "../../../../types/types";
import Card from "../../../../ui/Card"; import Card from "../../../../ui/Card";
import { useCameraFeedContext } from "../../../../app/context/CameraFeedContext";
const rows = 40; const rows = 40;
const cols = 40; const cols = 40;
const size = 20; const size = 20;
const gap = 0; const gap = 0;
type VideoFeedGridPainterProps = { const VideoFeedGridPainter = () => {
regions: Region[]; const { state } = useCameraFeedContext();
selectedRegionIndex: number; const cameraFeedID = state.cameraFeedID;
mode: string; const paintedCells = state.paintedCells[cameraFeedID];
paintedCells: RefObject<Map<string, PaintedCell>>; const regions = state.regionsByCamera[cameraFeedID];
}; const selectedRegionIndex = state.selectedRegionIndex;
const mode = state.modeByCamera[cameraFeedID];
const VideoFeedGridPainter = ({ regions, selectedRegionIndex, mode, paintedCells }: VideoFeedGridPainterProps) => {
const { latestBitmapRef, isloading } = useCreateVideoSnapshot(); const { latestBitmapRef, isloading } = useCreateVideoSnapshot();
const [stageSize, setStageSize] = useState({ width: 740, height: 460 }); const [stageSize, setStageSize] = useState({ width: 740, height: 460 });
const isDrawingRef = useRef(false); const isDrawingRef = useRef(false);
@@ -47,7 +47,7 @@ const VideoFeedGridPainter = ({ regions, selectedRegionIndex, mode, paintedCells
const key = `${row}-${col}`; const key = `${row}-${col}`;
const currentColour = regions[selectedRegionIndex].brushColour; const currentColour = regions[selectedRegionIndex].brushColour;
const map = paintedCells.current; const map = paintedCells;
const existing = map.get(key); const existing = map.get(key);
if (mode === "eraser") { if (mode === "eraser") {
@@ -121,7 +121,7 @@ const VideoFeedGridPainter = ({ regions, selectedRegionIndex, mode, paintedCells
<Layer ref={paintLayerRef} opacity={0.6}> <Layer ref={paintLayerRef} opacity={0.6}>
<Shape <Shape
sceneFunc={(ctx, shape) => { sceneFunc={(ctx, shape) => {
const cells = paintedCells.current; const cells = paintedCells;
cells.forEach((cell, key) => { cells.forEach((cell, key) => {
const [rowStr, colStr] = key.split("-"); const [rowStr, colStr] = key.split("-");
const row = Number(rowStr); const row = Number(rowStr);

View File

@@ -1,7 +1,8 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { CAMBASE } from "../../../utils/config";
const getfeed = async () => { const getfeed = async (cameraFeedID: "A" | "B" | "C" | null) => {
const response = await fetch(`http://100.115.148.59/TargetDetectionColour-preview`, { const response = await fetch(`${CAMBASE}TargetDetectionColour${cameraFeedID}-preview`, {
signal: AbortSignal.timeout(300000), signal: AbortSignal.timeout(300000),
cache: "no-store", cache: "no-store",
}); });
@@ -11,10 +12,10 @@ const getfeed = async () => {
return response.blob(); return response.blob();
}; };
export const useGetVideoFeed = () => { export const useGetVideoFeed = (cameraFeedID: "A" | "B" | "C" | null) => {
const videoQuery = useQuery({ const videoQuery = useQuery({
queryKey: ["getfeed"], queryKey: ["getfeed", cameraFeedID],
queryFn: getfeed, queryFn: () => getfeed(cameraFeedID),
refetchInterval: 500, refetchInterval: 500,
}); });

View File

@@ -1,9 +1,12 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { useGetVideoFeed } from "./useGetVideoFeed"; import { useGetVideoFeed } from "./useGetVideoFeed";
import { useCameraFeedContext } from "../../../app/context/CameraFeedContext";
export const useCreateVideoSnapshot = () => { export const useCreateVideoSnapshot = () => {
const { state } = useCameraFeedContext();
const cameraFeedID = state?.cameraFeedID;
const latestBitmapRef = useRef<ImageBitmap | null>(null); const latestBitmapRef = useRef<ImageBitmap | null>(null);
const { videoQuery } = useGetVideoFeed(); const { videoQuery } = useGetVideoFeed(cameraFeedID);
const snapShot = videoQuery?.data; const snapShot = videoQuery?.data;
const isloading = videoQuery.isPending; const isloading = videoQuery.isPending;

View File

@@ -95,3 +95,52 @@ export type OptionalBOF2LaneIDs = {
LID2?: string; LID2?: string;
LID3?: string; LID3?: string;
}; };
export type CameraFeedState = {
cameraFeedID: "A" | "B" | "C";
paintedCells: {
A: Map<string, PaintedCell>;
B: Map<string, PaintedCell>;
C: Map<string, PaintedCell>;
};
regionsByCamera: {
A: Region[];
B: Region[];
C: Region[];
};
selectedRegionIndex: number;
modeByCamera: {
A: string;
B: string;
C: string;
};
tabIndex?: number;
};
export type CameraFeedAction =
| {
type: "SET_CAMERA_FEED";
payload: "A" | "B" | "C";
}
| {
type: "CHANGE_MODE";
payload: { cameraFeedID: "A" | "B" | "C"; mode: string };
}
| { type: "SET_SELECTED_REGION_INDEX"; payload: number }
| {
type: "SET_SELECTED_REGION_COLOUR";
payload: { cameraFeedID: "A" | "B" | "C"; regionName: string; newColour: string };
}
| {
type: "ADD_NEW_REGION";
payload: { cameraFeedID: "A" | "B" | "C"; regionName: string; brushColour: string };
}
| {
type: "REMOVE_REGION";
payload: { cameraFeedID: "A" | "B" | "C"; regionName: string };
}
| {
type: "RESET_PAINTED_CELLS";
payload: { cameraFeedID: "A" | "B" | "C"; paintedCells: Map<string, PaintedCell> };
};