|
1 | | -# openUC2 Goniometer |
| 1 | +# openUC2 Goniometer – Tutorial |
2 | 2 |
|
| 3 | +Measure sessile-drop contact angles with your **openUC2 modular microscope** running **ImSwitch** on a Raspberry Pi. |
3 | 4 |
|
| 5 | + |
4 | 6 |
|
5 | | - |
| 7 | +Beware: There are cubes inside :) |
6 | 8 |
|
7 | | - |
| 9 | + |
| 10 | + |
| 11 | +## 1. Hardware Overview |
| 12 | + |
| 13 | +``` |
| 14 | + ┌─────────────────────────────┐ |
| 15 | + │ openUC2 Cube "Stack" │ |
| 16 | + │ │ |
| 17 | + │ ┌──────────┐ │ |
| 18 | + │ │ Pipette │ (syringe or │ |
| 19 | + │ │ Holder │ dispenser) │ |
| 20 | + │ └────┬─────┘ │ |
| 21 | + │ │ droplet │ |
| 22 | + │ ══════════════ substrate │ |
| 23 | + │ │ |
| 24 | + │ ┌───────────┐ │ |
| 25 | + │ │HIK IMX178 │ USB3 camera │ |
| 26 | + │ │ CCTV Lens │ ~90° to drop│ |
| 27 | + │ │ 25 mm │ │ |
| 28 | + │ └───────────┘ │ |
| 29 | + │ │ |
| 30 | + │ ┌──────────────────────┐ │ |
| 31 | + │ │ LED Array + Diffuser │ │ |
| 32 | + │ │ (back-light) │ │ |
| 33 | + │ └──────────────────────┘ │ |
| 34 | + └─────────────────────────────┘ |
| 35 | + │ USB3 |
| 36 | + ┌─────────▼──────────┐ |
| 37 | + │ Raspberry Pi 5 │ |
| 38 | + │ running ImSwitch │ |
| 39 | + └────────────────────┘ |
| 40 | + │ Wi-Fi / Ethernet |
| 41 | + ┌─────────▼──────────┐ |
| 42 | + │ Browser (laptop) │ |
| 43 | + │ ImSwitch UI │ |
| 44 | + └────────────────────┘ |
| 45 | +``` |
| 46 | + |
| 47 | +| Component | Details | |
| 48 | +|-----------|---------| |
| 49 | +| **Platform** | openUC2 modular cube system | |
| 50 | +| **Controller** | Raspberry Pi 5 (runs ImSwitch) | |
| 51 | +| **Camera** | HIK Vision IMX178 (USB 3.0, 6.4 MP, colour/mono) | |
| 52 | +| **Illumination** | LED array + diffuser — back-lit, homogeneous | |
| 53 | +| **Dispensing** | Syringe pipette held in a cube mount; drops ~1–10 µL | |
| 54 | +| **Substrate** | Flat sample placed on the cube base plate | |
| 55 | + |
| 56 | +The camera observes the droplet from the side (macro view) so the contact line between the drop and substrate is visible as the widest horizontal extent of the drop silhouette. |
| 57 | + |
| 58 | + |
| 59 | +## 2. Software Stack |
| 60 | + |
| 61 | +``` |
| 62 | +Raspberry Pi |
| 63 | +└── ImSwitch (Python, FastAPI REST) |
| 64 | + └── GoniometerController (plugin) |
| 65 | + ├── REST API /GoniometerController/… |
| 66 | + └── OpenCV image processing pipeline |
| 67 | +
|
| 68 | +Browser |
| 69 | +└── ImSwitch Frontend (React) |
| 70 | + └── GoniometerController.js (this UI) |
| 71 | +``` |
| 72 | + |
| 73 | +ImSwitch exposes the goniometer as a REST API. |
| 74 | +The React frontend is served alongside ImSwitch and communicates over the |
| 75 | +local network. |
| 76 | + |
| 77 | + |
| 78 | +## 3. In Action |
| 79 | + |
| 80 | +The software workflow is explained in the following video. You have to connect to the Raspberry Pi's wifi or network (PW: `youseetoo`) and then navigate to the website http://192.168.4.1 and enable the Goniometer app in the side bar: |
| 81 | + |
| 82 | +<iframe width="560" height="315" src="https://www.youtube.com/embed/i9ORYjFVBrc?si=PmFjd52UPWCQaZsi" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> |
| 83 | + |
| 84 | + |
| 85 | +## 4. UI Walkthrough |
| 86 | + |
| 87 | +The code for the backend is here: |
| 88 | +https://github.com/openUC2/ImSwitch/blob/master/imswitch/imcontrol/controller/controllers/GoniometerController.py |
| 89 | + |
| 90 | +The code for the frontend is here: |
| 91 | +https://github.com/openUC2/ImSwitch/blob/master/frontend/src/components/GoniometerController.js |
| 92 | + |
| 93 | +### 4.1 Live Camera Stream |
| 94 | + |
| 95 | +The top-left card shows a **live MJPEG stream** from the HIK camera. |
| 96 | + |
| 97 | +- The stream is updated continuously — use it to position the pipette and |
| 98 | + focus the droplet before snapping. |
| 99 | +- Zoom and pan are available on the **snapped image** (bottom card), not on |
| 100 | + the live view. |
| 101 | + |
| 102 | +### 4.2 Setting a Crop ROI |
| 103 | + |
| 104 | +For best results the algorithm only sees the droplet and its reflection — crop |
| 105 | +out any background clutter. |
| 106 | + |
| 107 | +1. Click the **Crop icon** (✂) in the live stream card header. |
| 108 | + The cursor changes to a crosshair and an info banner appears. |
| 109 | +2. **Click and drag** a rectangle around the droplet (include a little |
| 110 | + background above and below). |
| 111 | +3. Release the mouse — the crop is sent to the backend and a gold dashed |
| 112 | + rectangle persists on the live view. |
| 113 | +4. To remove the crop, click the **×** on the orange "Crop active" chip, or |
| 114 | + click **Reset Crop**. |
| 115 | + |
| 116 | +> **Why crop?** The algorithm searches for the widest contour in the image. |
| 117 | +> Bubbles, pipette edges, or substrate edges that extend further than the drop |
| 118 | +> width will confuse the baseline detection. |
| 119 | +
|
| 120 | +### 4.3 Camera Settings |
| 121 | + |
| 122 | +Below the live stream you find two fields: |
| 123 | + |
| 124 | +| Field | Typical value | Effect | |
| 125 | +|-------|--------------|--------| |
| 126 | +| **Exposure (ms)** | 2 – 10 ms | Shorter = less motion blur; too short = dark image | |
| 127 | +| **Gain** | 0 – 10 | Amplifies signal; higher gain adds noise | |
| 128 | + |
| 129 | +Press **Enter** or click outside the field to apply. |
| 130 | +A well-exposed image shows a bright, uniform background and a clearly dark |
| 131 | +drop silhouette. |
| 132 | + |
| 133 | +### 4.4 Snap an Image |
| 134 | + |
| 135 | +Click the green **Snap** button. |
| 136 | + |
| 137 | +- The backend captures a full-resolution frame from the HIK camera. |
| 138 | +- If a crop ROI is active, only the cropped region is returned. |
| 139 | +- The snapped image appears in the lower card. |
| 140 | +- The raw image is also saved as a TIFF on the Raspberry Pi |
| 141 | + (`/tmp/goniometer_*.tiff` by default). |
| 142 | + |
| 143 | +### 4.5 Automatic Measurement |
8 | 144 |
|
9 | 145 |  |
10 | 146 |
|
11 | | - |
| 147 | +After snapping, switch to the **Auto** tab (right panel) and click |
| 148 | +**Measure (Auto)**. |
| 149 | + |
| 150 | +The algorithm runs in ~100 ms and overlays: |
| 151 | + |
| 152 | +- **Green line** — detected substrate baseline. |
| 153 | +- **Cyan circles** — left and right contact points. |
| 154 | +- **Cyan curves** — polynomial fit along each drop edge. |
| 155 | +- **Magenta lines** — tangent vectors at the contact points. |
| 156 | +- **Angle labels** — left angle θ_L and right angle θ_R in degrees. |
| 157 | +- **Top-left overlay** — mean angle and wetting regime |
| 158 | + (*hydrophilic* θ < 90° or *hydrophobic* θ > 90°). |
| 159 | + |
| 160 | +The results (θ_L, θ_R, baseline tilt, regime) appear in the panel on the |
| 161 | +right. |
| 162 | + |
| 163 | +### 4.6 Manual Measurement |
| 164 | + |
| 165 | + |
| 166 | + |
| 167 | +For difficult cases (very small drops, contaminated substrates) use the |
| 168 | +**Manual** tab. |
| 169 | + |
| 170 | +You place **3 points** on the snapped image: |
| 171 | + |
| 172 | +| Point | How to place | Purpose | |
| 173 | +|-------|-------------|---------| |
| 174 | +| **Baseline 1** | Click on the substrate, left of drop | Defines substrate line | |
| 175 | +| **Baseline 2** | Click on the substrate, right of drop | Defines substrate line | |
| 176 | +| **Tangent** | Click on the drop edge at the contact point | Direction of drop wall | |
| 177 | + |
| 178 | +**Step-by-step:** |
| 179 | + |
| 180 | +1. Switch to the **Manual** tab. |
| 181 | +2. Click **Place Points** to enter placement mode (cursor → crosshair). |
| 182 | +3. Click three times in the order above. |
| 183 | + Small numbered markers appear on the image. |
| 184 | +4. Click **Measure (Manual)**. |
| 185 | + The angle is computed from the angle between the baseline vector and the |
| 186 | + tangent vector. The result is shown immediately. |
| 187 | + |
| 188 | +> **Tip:** Zoom in (use the +/− buttons or scroll) and pan (click-drag) before |
| 189 | +> placing points for higher accuracy. |
| 190 | +
|
| 191 | +### 4.7 Focus Monitor |
| 192 | + |
| 193 | +Enable the **Focus Monitor** toggle in the right panel. |
| 194 | +Every 500 ms the backend computes the **Laplacian variance** (sharpness) of |
| 195 | +the live image and sends it back. |
| 196 | + |
| 197 | +A progress bar shows the value relative to the session maximum. |
| 198 | +Maximize it while turning the focus ring (or the z-stage) to get the sharpest |
| 199 | +possible image before snapping. |
| 200 | + |
| 201 | +### 4.8 Algorithm Configuration |
| 202 | + |
| 203 | +Expand the **Config** section in the right panel to tune the image-processing parameters. You probably don't need to touch them anyway. |
| 204 | + |
| 205 | +| Parameter | Default | Effect | |
| 206 | +|-----------|---------|--------| |
| 207 | +| **Canny Low** | 30 | Lower Canny hysteresis threshold — increase for noisy images | |
| 208 | +| **Canny High** | 100 | Upper Canny threshold — decrease to detect weaker edges | |
| 209 | +| **Blur Kernel Size** | 5 | Gaussian pre-blur (odd numbers only) — reduce for sharp images | |
| 210 | +| **Local Fit Fraction** | 0.15 | Fraction of drop width used for the tangent polynomial fit | |
| 211 | +| **Polynomial Degree** | 2 | Degree of the tangent polynomial (2 = parabola, higher = more flexible) | |
| 212 | +| **Tangent Delta (px)** | 1 | Finite-difference step for tangent evaluation | |
| 213 | +| **Angle Min (°)** | 1 | Measurements below this are discarded as noise | |
| 214 | +| **Angle Max (°)** | 179 | Measurements above this are discarded as noise | |
| 215 | + |
| 216 | +Click **Reset Config** to return all values to defaults. |
| 217 | +Changes are sent to the backend immediately and applied on the next measurement. |
| 218 | + |
| 219 | +### 4.9 Measurement History & Export |
| 220 | + |
| 221 | +Every successful measurement can be added to the session history: |
| 222 | + |
| 223 | +1. After a successful auto or manual measurement click **Add to History**. |
| 224 | +2. The history table shows: ID, time, mode, θ_L, θ_R. |
| 225 | +3. The panel also shows a running **mean ± std** across all recorded values. |
| 226 | +4. Use **Export CSV** or **Export JSON** to download the full table. |
| 227 | +5. Click the download icon (↓) in the image card to save the annotated result |
| 228 | + image as a PNG. |
| 229 | + |
| 230 | + |
| 231 | +## 5. Measurement Algorithm Explained |
| 232 | + |
| 233 | +The algorithm is fully automatic and adapts to the wetting regime. |
| 234 | + |
| 235 | +### 5.1 Preprocessing |
| 236 | + |
| 237 | +1. The snapped frame is down-scaled so its longest edge ≤ 800 px (speeds up |
| 238 | + processing without losing accuracy at typical droplet sizes). |
| 239 | +2. A Gaussian blur (kernel = `blur_ksize`) suppresses pixel noise. |
| 240 | + |
| 241 | +### 5.2 Edge Detection & Contour Extraction |
| 242 | + |
| 243 | +Canny edge detection produces a binary edge map. |
| 244 | +The largest contour (by bounding-box width) is assumed to be the drop |
| 245 | +silhouette including its mirror reflection in the substrate. |
| 246 | + |
| 247 | +### 5.3 Wetting Regime Detection |
| 248 | + |
| 249 | +The contour is profiled row by row to get width vs. vertical position. |
| 250 | +The profile is smoothed and the maximum-width row (equator) is found. |
| 251 | + |
| 252 | +- **Hydrophilic (θ < 90°):** No waist below the equator. The two widest |
| 253 | + points are the contact points; a tilted baseline is fitted through them. |
| 254 | +- **Hydrophobic (θ > 90°):** A width minimum (waist) exists below the equator |
| 255 | + — the drop overhangs the contact line. The waist row defines the baseline. |
| 256 | + |
| 257 | +``` |
| 258 | + Hydrophilic Hydrophobic |
| 259 | + ___ _____ |
| 260 | + / \ / \ |
| 261 | + | | ___/ \___ ← equator (widest) |
| 262 | + \ / ↑ |
| 263 | + ───┴───┴─── ────┼──────── ← waist → baseline |
| 264 | + contact pts contact pts |
| 265 | +``` |
| 266 | + |
| 267 | +### 5.4 Polynomial Tangent Fit |
| 268 | + |
| 269 | +For each contact point a short section of the drop edge |
| 270 | +(length = `fit_frac × drop_width`) is fitted with a polynomial of degree |
| 271 | +`poly_degree`. The tangent vector is evaluated by finite differences. |
| 272 | + |
| 273 | +- **Hydrophilic:** polynomial y(x) — fit in the horizontal direction. |
| 274 | +- **Hydrophobic:** polynomial x(y) — fit in the vertical direction (avoids |
| 275 | + near-vertical tangent issues). |
| 276 | + |
| 277 | +The contact angle θ is the angle between the tangent vector and the baseline. |
| 278 | + |
| 279 | + |
| 280 | +## 6. Step-by-Step Workflow |
| 281 | + |
| 282 | +``` |
| 283 | +1. Power on Pi + camera + LED array |
| 284 | +2. Open ImSwitch in browser |
| 285 | +3. Navigate to Goniometer |
| 286 | +4. Set Exposure & Gain → bright, sharp live image |
| 287 | +5. Place substrate on base plate |
| 288 | +6. Dispense droplet with pipette |
| 289 | +7. Check live view — is the full drop visible? |
| 290 | +8. ├── Yes → proceed to step 9 |
| 291 | +8. └── No → adjust pipette height / substrate position |
| 292 | +9. (Optional) Set Crop ROI around the drop |
| 293 | +10. Focus Monitor ON → maximize sharpness |
| 294 | +11. Click "Snap" |
| 295 | +12. Inspect snapped image |
| 296 | +13. ├── Drop clearly visible → Auto Measure |
| 297 | +13. └── Poor contrast / noise → adjust Canny / blur, re-snap |
| 298 | +14. Review annotated result |
| 299 | +15. ├── Correct baseline & tangents → Add to History |
| 300 | +15. └── Wrong detection → Manual Measure or retune Config |
| 301 | +16. Repeat steps 6–15 for each measurement |
| 302 | +17. Export CSV / JSON when done |
| 303 | +``` |
| 304 | + |
| 305 | + |
| 306 | +## 7. Tips for Good Results |
| 307 | + |
| 308 | + |
| 309 | + |
| 310 | + |
| 311 | +- **Back-lighting should be uniform.** The LED array + diffuser should produce a uniform bright background so the drop appears as a solid dark silhouette. Avoid reflections on the substrate surface. |
| 312 | + |
| 313 | +- **Keep the baseline horizontal.** If the substrate is tilted, the algorithm compensates (reports `baseline_tilt_deg`) but accuracy decreases for large tilts. Use the cube levelling feet. |
| 314 | + |
| 315 | +- **Crop tightly but not too tight.** Leave ~20 px of background above the top of the drop and below the substrate. The mirror reflection (below the substrate) is NOT needed — only the real drop is used. |
| 316 | + |
| 317 | + |
| 318 | +- **Bring sample to focus.** Use the micrometer stage to place the droplet in the image plane |
| 319 | + |
| 320 | + |
| 321 | + |
| 322 | +- **Repeat and average.** Dispense 5–10 drops and record each one. |
| 323 | + The mean ± std shown in the history panel is your reported contact angle. |
| 324 | + |
| 325 | +## 8. Troubleshooting |
| 326 | + |
| 327 | +| Symptom | Likely cause | Fix | |
| 328 | +|---------|-------------|-----| |
| 329 | +| No live stream | Camera not detected by ImSwitch | Check USB3 cable; verify camera name in device config | |
| 330 | +| "Detection failed" overlay | No contour found | Increase contrast (exposure, gain); check Canny thresholds | |
| 331 | +| Wildly wrong angles | Wrong contour selected (pipette, edge) | Set crop ROI to isolate only the drop | |
| 332 | +| Only one angle returned | Drop touches image border | Widen crop ROI or move drop into view | |
| 333 | +| Hydrophobic/hydrophilic wrong | Waist threshold too sensitive | Increase `waist_rise_thresh` in DEFAULT_CONFIG (backend) | |
| 334 | +| Baseline very tilted | Substrate not level | Level substrate; or use manual mode with explicit baseline | |
| 335 | +| Focus bar stuck at low value | Wrong camera name | Verify detector name in ImSwitch config matches `GoniometerController` detector key | |
| 336 | +| Export produces empty file | No measurements added to history | Click "Add to History" after each successful measurement | |
| 337 | + |
0 commit comments