-
Notifications
You must be signed in to change notification settings - Fork 18
Feature augmented slide overview #234
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ import * as experimentSlice from "../state/slices/ExperimentSlice.js"; | |
| import * as positionSlice from "../state/slices/PositionSlice.js"; | ||
| import * as objectiveSlice from "../state/slices/ObjectiveSlice.js"; | ||
| import * as focusMapSlice from "../state/slices/FocusMapSlice.js"; | ||
| import * as overviewRegSlice from "../state/slices/OverviewRegistrationSlice.js"; | ||
|
|
||
| import * as wsUtils from "./WellSelectorUtils.js"; | ||
| import apiPositionerControllerMovePositioner from "../backendapi/apiPositionerControllerMovePositioner.js"; | ||
|
|
@@ -72,6 +73,10 @@ const WellSelectorCanvas = forwardRef((props, ref) => { | |
| const objectiveState = useSelector(objectiveSlice.getObjectiveState); | ||
| const focusMapState = useSelector(focusMapSlice.getFocusMapState); | ||
| const focusMapManualPoints = useSelector(focusMapSlice.getManualPoints); | ||
| const overviewRegState = useSelector(overviewRegSlice.getOverviewRegistrationState); | ||
|
|
||
| // Preloaded overlay images cache (HTML Image objects) | ||
| const overlayImagesRef = useRef({}); | ||
|
|
||
| //################################################################################## | ||
| useImperativeHandle(ref, () => ({ | ||
|
|
@@ -146,6 +151,9 @@ const WellSelectorCanvas = forwardRef((props, ref) => { | |
| positionState, | ||
| objectiveState, | ||
| positionHistory, | ||
| overviewRegState.overlayEnabled, | ||
| overviewRegState.overlayOpacity, | ||
| overviewRegState.overlayData, | ||
| ]); | ||
|
|
||
| //################################################################################## | ||
|
|
@@ -812,12 +820,62 @@ const WellSelectorCanvas = forwardRef((props, ref) => { | |
| //------------ draw position trace | ||
| drawPositionTrace(ctx); | ||
|
|
||
| //------------ draw overview camera overlay images | ||
| drawOverviewOverlay(ctx); | ||
|
|
||
| //------------ draw focus map points overlay | ||
| drawFocusMapOverlay(ctx); | ||
|
|
||
| //ctx.restore(); | ||
| }; | ||
|
|
||
| //################################################################################## | ||
| // Draw overview camera overlay images on the well selector canvas | ||
| const drawOverviewOverlay = (ctx) => { | ||
| if (!overviewRegState.overlayEnabled) return; | ||
| const overlayData = overviewRegState.overlayData; | ||
| if (!overlayData || !overlayData.slides) return; | ||
|
|
||
| const alpha = overviewRegState.overlayOpacity; | ||
| ctx.save(); | ||
| ctx.globalAlpha = alpha; | ||
|
|
||
| Object.values(overlayData.slides).forEach((slideData) => { | ||
| if (!slideData.imageBase64 || !slideData.stageBounds) return; | ||
|
|
||
| const bounds = slideData.stageBounds; | ||
| const imgKey = slideData.slotId + "_" + (slideData.updatedAt || ""); | ||
|
|
||
| // Check if image is already cached | ||
| let cachedImg = overlayImagesRef.current[imgKey]; | ||
| if (!cachedImg) { | ||
| // Load image and cache it | ||
| const img = new Image(); | ||
| img.src = `data:${slideData.imageMimeType || "image/png"};base64,${slideData.imageBase64}`; | ||
| img.onload = () => { | ||
| overlayImagesRef.current[imgKey] = img; | ||
| // Trigger re-render by requesting animation frame | ||
| requestAnimationFrame(() => renderCanvas()); | ||
| }; | ||
| return; // Skip this slide until image is loaded | ||
|
Comment on lines
+847
to
+860
|
||
| } | ||
|
|
||
| // Convert stage bounds to canvas pixel coordinates | ||
| const px = calcPhy2Px(bounds.minX); | ||
| const py = calcPhy2Px(bounds.minY); | ||
| const pw = calcPhy2Px(bounds.width); | ||
| const ph = calcPhy2Px(bounds.height); | ||
|
|
||
| try { | ||
| ctx.drawImage(cachedImg, px, py, pw, ph); | ||
| } catch (e) { | ||
| // Image may not be loaded yet | ||
| } | ||
| }); | ||
|
|
||
| ctx.restore(); | ||
| }; | ||
|
|
||
| //################################################################################## | ||
| // Draw focus map grid points as black crosses and manual points as blue circles | ||
| const drawFocusMapOverlay = (ctx) => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import createAxiosInstance from "./createAxiosInstance"; | ||
|
|
||
| /** | ||
| * Get all overlay data for WellSelector canvas rendering. | ||
| * GET /ExperimentController/getOverviewOverlayData | ||
| */ | ||
| const apiGetOverviewOverlayData = async ( | ||
| cameraName = "", | ||
| layoutName = "Heidstar 4x Histosample" | ||
| ) => { | ||
| const axiosInstance = createAxiosInstance(); | ||
| const response = await axiosInstance.get( | ||
| "/ExperimentController/getOverviewOverlayData", | ||
| { | ||
| params: { camera_name: cameraName, layout_name: layoutName }, | ||
| } | ||
| ); | ||
| return response.data; | ||
| }; | ||
|
|
||
| export default apiGetOverviewOverlayData; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import createAxiosInstance from "./createAxiosInstance"; | ||
|
|
||
| /** | ||
| * Get overview registration wizard config with slot definitions. | ||
| * POST /ExperimentController/getOverviewRegistrationConfig | ||
| * | ||
| * Sends the frontend's current wellLayout (with offsets applied) so | ||
| * that slot corners returned by the backend match the canvas exactly. | ||
| * | ||
| * @param {object|null} layoutData - Full wellLayout object from Redux (preferred) | ||
| * @param {string} layoutName - Fallback layout name if layoutData is null | ||
| */ | ||
| const apiGetOverviewRegistrationConfig = async ( | ||
| layoutData = null, | ||
| layoutName = "Heidstar 4x Histosample" | ||
| ) => { | ||
| const axiosInstance = createAxiosInstance(); | ||
| const response = await axiosInstance.post( | ||
| "/ExperimentController/getOverviewRegistrationConfig", | ||
| { | ||
| layout_data: layoutData, | ||
| layout_name: layoutName, | ||
| } | ||
| ); | ||
| return response.data; | ||
| }; | ||
|
|
||
| export default apiGetOverviewRegistrationConfig; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import createAxiosInstance from "./createAxiosInstance"; | ||
|
|
||
| /** | ||
| * Get registration status for all slides. | ||
| * GET /ExperimentController/getOverviewRegistrationStatus | ||
| */ | ||
| const apiGetOverviewRegistrationStatus = async ( | ||
| cameraName = "", | ||
| layoutName = "Heidstar 4x Histosample" | ||
| ) => { | ||
| const axiosInstance = createAxiosInstance(); | ||
| const response = await axiosInstance.get( | ||
| "/ExperimentController/getOverviewRegistrationStatus", | ||
| { | ||
| params: { camera_name: cameraName, layout_name: layoutName }, | ||
| } | ||
| ); | ||
| return response.data; | ||
| }; | ||
|
|
||
| export default apiGetOverviewRegistrationStatus; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import createAxiosInstance from "./createAxiosInstance"; | ||
|
|
||
| /** | ||
| * Refresh overlay image for a slide using existing registration. | ||
| * POST /ExperimentController/refreshOverviewSlideImage | ||
| */ | ||
| const apiRefreshOverviewSlideImage = async ( | ||
| slotId = "1", | ||
| cameraName = "", | ||
| layoutName = "Heidstar 4x Histosample" | ||
| ) => { | ||
| const axiosInstance = createAxiosInstance(); | ||
| let url = "/ExperimentController/refreshOverviewSlideImage?"; | ||
| const params = []; | ||
| params.push(`slot_id=${encodeURIComponent(slotId)}`); | ||
| if (cameraName) params.push(`camera_name=${encodeURIComponent(cameraName)}`); | ||
| if (layoutName) params.push(`layout_name=${encodeURIComponent(layoutName)}`); | ||
| url += params.join("&"); | ||
| const response = await axiosInstance.post(url); | ||
| return response.data; | ||
| }; | ||
|
|
||
| export default apiRefreshOverviewSlideImage; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import createAxiosInstance from "./createAxiosInstance"; | ||
|
|
||
| /** | ||
| * Register a slide with corner picks – compute homography. | ||
| * POST /ExperimentController/registerOverviewSlide | ||
| */ | ||
| const apiRegisterOverviewSlide = async (registrationData) => { | ||
| const axiosInstance = createAxiosInstance(); | ||
| const response = await axiosInstance.post( | ||
| "/ExperimentController/registerOverviewSlide", | ||
| registrationData | ||
| ); | ||
| return response.data; | ||
| }; | ||
|
|
||
| export default apiRegisterOverviewSlide; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import createAxiosInstance from "./createAxiosInstance"; | ||
|
|
||
| /** | ||
| * Snap overview image for a given slot. | ||
| * POST /ExperimentController/snapOverviewImage | ||
| */ | ||
| const apiSnapOverviewImage = async (slotId = "1", cameraName = "") => { | ||
| const axiosInstance = createAxiosInstance(); | ||
| let url = "/ExperimentController/snapOverviewImage?"; | ||
| const params = []; | ||
| params.push(`slot_id=${encodeURIComponent(slotId)}`); | ||
| if (cameraName) params.push(`camera_name=${encodeURIComponent(cameraName)}`); | ||
| url += params.join("&"); | ||
| const response = await axiosInstance.post(url); | ||
| return response.data; | ||
| }; | ||
|
|
||
| export default apiSnapOverviewImage; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The overlay image cache key includes
updatedAt(slotId + "_" + updatedAt), but old cache entries are never removed. Over time (especially with repeated refreshes) this will growoverlayImagesRef.currentunbounded. Consider caching perslotIdand replacing the entry whenupdatedAtchanges, or pruning keys that are no longer present inoverlayData.slides.