-
Notifications
You must be signed in to change notification settings - Fork 128
Utilties-Hardware Autofocus #800
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
Conversation
that combines a shutter and a camera into an autofocus device.
autofocus device that uses an existing camera, shutter and focus stage to image a reflection spot, can calibrate the movement of this spot as a function of the postion of the z stage, then move the stage to place that spot back in the desired position whenever a full focus is desired. There is code to discern 2 spots (top and bottom of coverslip_ and to pick the correct one (at least when both spots are visible).
Saves all values in calibration file, inclusing camera ROI and binning. Needs extensive documentation.
on current position. Now always subtracts a dark iameg from each image with IR. More reliable recognition of top and bottom reflection.
…ly grouped in the UI.
…ramData/Micro-Manager
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.
Pull request overview
This pull request adds a hardware-based autofocus device adapter for Micro-Manager that combines a camera, shutter, and Z-stage to achieve focus by detecting and tracking IR reflection spots. The implementation includes calibration, continuous focus monitoring, and offset measurement capabilities.
Key Changes:
- New AutoFocus device class with image analysis and calibration algorithms
- Integration with existing AutoFocusStage device for automatic registration
- OpenCV 2.4.13.6 dependency added for image processing capabilities
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 25 comments.
Show a summary per file
| File | Description |
|---|---|
| DeviceAdapters/Utilities/Utilities.vcxproj | Added OpenCV include paths, library directories, and dependencies for both Debug and Release configurations |
| DeviceAdapters/Utilities/Utilities.vcxproj.filters | Added AutoFocus.cpp to project source files filter |
| DeviceAdapters/Utilities/Utilities.h | Added error codes, threading/OpenCV includes, and AutoFocus class declaration with calibration data structures |
| DeviceAdapters/Utilities/Utilities.cpp | Registered new AutoFocus device in module initialization |
| DeviceAdapters/Utilities/AutoFocusStage.cpp | Added registration/unregistration logic to connect with AutoFocus device |
| DeviceAdapters/Utilities/AutoFocus.cpp | Complete implementation (2189 lines) including initialization, calibration, image analysis, continuous focus thread, and JSON-based calibration persistence |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (ret != DEVICE_OK) | ||
| { | ||
| // Try to restore original position | ||
| pStage->SetPositionUm(startPos); |
Copilot
AI
Dec 16, 2025
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.
Incorrect error handling on SetPositionUm failure. When pStage->SetPositionUm(targetPos) fails, the code attempts to call pStage->SetPositionUm(startPos) but doesn't check if this second call succeeds. If the stage is in a bad state, this could fail too. Additionally, it should restore to originalPos, not startPos.
| pStage->SetPositionUm(startPos); | |
| int restoreRet = pStage->SetPositionUm(originalPos); | |
| if (restoreRet != DEVICE_OK) { | |
| // Optionally, log both errors or return a combined error code | |
| // For now, return the restoration error code to signal a more severe problem | |
| return restoreRet; | |
| } |
| ImgBuffer resultImage(lightImage.Width(), lightImage.Height(), lightImage.Depth()); | ||
| const unsigned char* lightPixels = lightImage.GetPixels(); | ||
| const unsigned char* darkPixels = darkImage.GetPixels(); | ||
| unsigned int numPixels = lightImage.Width() * lightImage.Height(); |
Copilot
AI
Dec 16, 2025
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.
Potential integer overflow in image pixel calculations. When calculating numPixels as width * height, there's no check that this multiplication doesn't overflow an unsigned int. For very large images (e.g., 65536 x 65536 pixels), this could overflow. Consider using size_t or adding overflow checks.
|
|
||
| shutter->SetOpen(true); | ||
| CDeviceUtils::SleepMs(10); // wait for shutter to open | ||
| // Snap image with shutter open | ||
| camera->SnapImage(); | ||
| ImgBuffer lightImage; | ||
| ret = GetImageFromBuffer(lightImage); | ||
| shutter->SetOpen(false); |
Copilot
AI
Dec 16, 2025
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.
Shutter left open after SnapAndAnalyze completes. The function opens the shutter at line 968, and while it closes it at line 974, if any error occurs between these lines (like at line 966 or 973 where GetImageFromBuffer could fail but isn't checked), the shutter will remain open. The shutter should be closed in all code paths, preferably using RAII or ensuring cleanup even on error paths.
| shutter->SetOpen(true); | |
| CDeviceUtils::SleepMs(10); // wait for shutter to open | |
| // Snap image with shutter open | |
| camera->SnapImage(); | |
| ImgBuffer lightImage; | |
| ret = GetImageFromBuffer(lightImage); | |
| shutter->SetOpen(false); | |
| if (ret != DEVICE_OK) | |
| return ret; | |
| // RAII guard to ensure shutter is closed | |
| struct ShutterGuard { | |
| MM::Shutter* shutter_; | |
| bool closed_; | |
| ShutterGuard(MM::Shutter* s) : shutter_(s), closed_(false) {} | |
| void Close() { if (!closed_ && shutter_) { shutter_->SetOpen(false); closed_ = true; } } | |
| ~ShutterGuard() { Close(); } | |
| } shutterGuard(shutter); | |
| shutter->SetOpen(true); | |
| CDeviceUtils::SleepMs(10); // wait for shutter to open | |
| // Snap image with shutter open | |
| camera->SnapImage(); | |
| ImgBuffer lightImage; | |
| ret = GetImageFromBuffer(lightImage); | |
| if (ret != DEVICE_OK) | |
| return ret; | |
| // shutter will be closed by shutterGuard destructor |
| #define ERR_AUTOFOCUS_NOT_SUPPORTED 10012 | ||
| #define ERR_NO_PHYSICAL_STAGE 10013 | ||
| #define ERR_NO_SHUTTER_DEVICE_FOUND 10014 | ||
| #define ERR_TAGET_TOO_HIGH 10015 |
Copilot
AI
Dec 16, 2025
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.
Typo in error code constant name. "TAGET" should be "TARGET".
| #define ERR_TAGET_TOO_HIGH 10015 | |
| #define ERR_TARGET_TOO_HIGH 10015 |
| const double MIN_SLOPE = 1e-6; // Threshold for effectively zero slope | ||
| const double INVALID_Z = -1.0e10; // Sentinel value | ||
|
|
||
| // bool xValid = fabs(cal.slopeX) > MIN_SLOPE; |
Copilot
AI
Dec 16, 2025
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.
Commented out code should be removed. Line 1814 contains a duplicate variable assignment that's commented out. If this was intentional for debugging, it should be removed for production code.
| // bool xValid = fabs(cal.slopeX) > MIN_SLOPE; |
| SetErrorText(ERR_NO_SHUTTER_DEVICE_FOUND, "No Shutter device found. Please select a valid shutter in the Shutter property."); | ||
| SetErrorText(ERR_NO_AUTOFOCUS_DEVICE, "No AutoFocus Device selected"); | ||
| SetErrorText(ERR_NO_AUTOFOCUS_DEVICE_FOUND, "No AutoFocus Device loaded"); | ||
| SetErrorText(ERR_TAGET_TOO_HIGH, "Target position exceeds allowed maximum. Likely error in autofocus"); |
Copilot
AI
Dec 16, 2025
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 error message refers to the misspelled constant ERR_TAGET_TOO_HIGH. This should be ERR_TARGET_TOO_HIGH to match the corrected constant name.
| SetErrorText(ERR_TAGET_TOO_HIGH, "Target position exceeds allowed maximum. Likely error in autofocus"); | |
| SetErrorText(ERR_TARGET_TOO_HIGH, "Target position exceeds allowed maximum. Likely error in autofocus"); |
| double track1X = 0.0; | ||
| double track1Y = 0.0; | ||
| double track2X = 0.0; | ||
| double track2Y = 0.0;; |
Copilot
AI
Dec 16, 2025
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.
Unnecessary semicolon after variable declaration. This creates an empty statement and should be removed.
| double track2Y = 0.0;; | |
| double track2Y = 0.0; |
| binning_ = pCam->GetBinning(); | ||
| if (binning_ <= 0) | ||
| binning_ = 1; | ||
| GetCoreCallback()->OnPropertyChanged(this, "Binning", CDeviceUtils::ConvertToString(binning_)); |
Copilot
AI
Dec 16, 2025
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.
Inconsistent property name reference. The code calls OnPropertyChanged with "Binning" but the property was created as "Camera_Binning" on line 210. This will cause the property change notification to fail silently.
| GetCoreCallback()->OnPropertyChanged(this, "Binning", CDeviceUtils::ConvertToString(binning_)); | |
| GetCoreCallback()->OnPropertyChanged(this, "Camera_Binning", CDeviceUtils::ConvertToString(binning_)); |
| GetCoreCallback()->OnPropertyChanged(this, "ROI-X", CDeviceUtils::ConvertToString((long)roiX_)); | ||
| GetCoreCallback()->OnPropertyChanged(this, "ROI-Y", CDeviceUtils::ConvertToString((long)roiY_)); | ||
| GetCoreCallback()->OnPropertyChanged(this, "ROI-Width", CDeviceUtils::ConvertToString((long)roiWidth_)); | ||
| GetCoreCallback()->OnPropertyChanged(this, "ROI-Height", CDeviceUtils::ConvertToString((long)roiHeight_)); |
Copilot
AI
Dec 16, 2025
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.
Inconsistent property name references. The code calls OnPropertyChanged with "ROI-X", "ROI-Y", "ROI-Width", and "ROI-Height", but the properties were created with "Camera_ROI-X", "Camera_ROI-Y", "Camera_ROI-Width", and "Camera_ROI-Height" on lines 194, 198, 202, and 206. This will cause the property change notifications to fail silently.
| GetCoreCallback()->OnPropertyChanged(this, "ROI-X", CDeviceUtils::ConvertToString((long)roiX_)); | |
| GetCoreCallback()->OnPropertyChanged(this, "ROI-Y", CDeviceUtils::ConvertToString((long)roiY_)); | |
| GetCoreCallback()->OnPropertyChanged(this, "ROI-Width", CDeviceUtils::ConvertToString((long)roiWidth_)); | |
| GetCoreCallback()->OnPropertyChanged(this, "ROI-Height", CDeviceUtils::ConvertToString((long)roiHeight_)); | |
| GetCoreCallback()->OnPropertyChanged(this, "Camera_ROI-X", CDeviceUtils::ConvertToString((long)roiX_)); | |
| GetCoreCallback()->OnPropertyChanged(this, "Camera_ROI-Y", CDeviceUtils::ConvertToString((long)roiY_)); | |
| GetCoreCallback()->OnPropertyChanged(this, "Camera_ROI-Width", CDeviceUtils::ConvertToString((long)roiWidth_)); | |
| GetCoreCallback()->OnPropertyChanged(this, "Camera_ROI-Height", CDeviceUtils::ConvertToString((long)roiHeight_)); |
| // Snap image with shutter open | ||
| camera->SnapImage(); | ||
| ImgBuffer lightImage; | ||
| ret = GetImageFromBuffer(lightImage); |
Copilot
AI
Dec 16, 2025
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.
Missing check for return value. The function GetImageFromBuffer is called but its return value is assigned to ret but never checked. If the image retrieval fails, the code continues to use potentially invalid lightImage data.
| ret = GetImageFromBuffer(lightImage); | |
| ret = GetImageFromBuffer(lightImage); | |
| if (ret != DEVICE_OK) | |
| return ret; |
|
As you already know from our previous chats, very excited to see reflection-based focus maintenance with software in the loop! Some questions and discussion points:
I agree that it's good to make this generally usable (as opposed to making it only work with Cephla devices), but at the same time I feel that is's specialized and complex enough that it would be best placed somewhere other than in Utilities whose existing devices have very simple input-output relationships (it's nice to be able to use that simple explanation for Utilities devices). Another reason to prefer a separate device adapter is that the code is large enough that it could already benefit from being split into multiple files (would be good to separate the data processing from the device adapter so that it can be unit tested, and even more so if additional algorithms are added in the future). This would crowd and complicate Utilities. I would maybe also suggest giving the name some thought, because this is actually focus maintenance, which is superior to autofocus (which usually means focusing based on the viewfinder image). (Yes, I'm aware many people don't make the distinction, but....) I bit of history is that we used to have a |
|
Superseded by #803 |
Combines a camera, shutter, and Zstage into a hardware autofocus. Works with the Cephla microscope on my desk.