Skip to content

Commit 26cc216

Browse files
committed
feat: add blob-detection example notebook
1 parent c6c2d3b commit 26cc216

File tree

1 file changed

+345
-0
lines changed

1 file changed

+345
-0
lines changed
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Blob Detection Workflow\n",
8+
"\n",
9+
"This example shows a compact image-analysis pipeline that detects bright blobs in two sample images using DataJoint. It demonstrates:\n",
10+
"\n",
11+
"- Seeding a small `Image` manual table with two entries of standard images from `skimage.data`.\n",
12+
"- Defining multiple parameter sets for blob detection in a lookup table `BlobParamSet`\n",
13+
"- Defining a computed master table `Detection` together with its nested part table `Detection.Blob`.\n",
14+
"- Populating the master, which automatically inserts all part rows inside the same transaction.\n",
15+
"- Visualizing the results by drawing detection circles on the images.\n",
16+
"- Visually selecting the optimal parameter set for each image and saving the selection in a manual table `SelectDetection`.\n",
17+
"\n",
18+
"Along the way we illustrate why master-part relationships are ideal for computational workflows: the master stores aggregate results and the parts hold per-feature detail, all created atomically.\n"
19+
]
20+
},
21+
{
22+
"cell_type": "markdown",
23+
"metadata": {},
24+
"source": [
25+
"## Setup\n",
26+
"\n",
27+
"Load the required images and display them for reference.\n"
28+
]
29+
},
30+
{
31+
"cell_type": "code",
32+
"execution_count": null,
33+
"metadata": {},
34+
"outputs": [],
35+
"source": [
36+
"%xmode minimal"
37+
]
38+
},
39+
{
40+
"cell_type": "code",
41+
"execution_count": null,
42+
"metadata": {},
43+
"outputs": [],
44+
"source": [
45+
"%matplotlib inline\n",
46+
"import matplotlib.pyplot as plt\n",
47+
"from skimage import data\n",
48+
"from skimage.feature import blob_doh\n",
49+
"from skimage.color import rgb2gray\n"
50+
]
51+
},
52+
{
53+
"cell_type": "code",
54+
"execution_count": null,
55+
"metadata": {},
56+
"outputs": [],
57+
"source": [
58+
"import datajoint as dj\n",
59+
"\n",
60+
"schema = dj.Schema(db_prefix + 'blob_detection')"
61+
]
62+
},
63+
{
64+
"cell_type": "code",
65+
"execution_count": null,
66+
"metadata": {},
67+
"outputs": [],
68+
"source": [
69+
"@schema\n",
70+
"class Image(dj.Manual):\n",
71+
" definition = \"\"\"\n",
72+
" image_id : int\n",
73+
" ---\n",
74+
" image_name : varchar(30)\n",
75+
" image : longblob\n",
76+
" \"\"\"\n",
77+
"\n",
78+
"Image.insert(\n",
79+
" (\n",
80+
" (1, \"hubble deep field\", rgb2gray(data.hubble_deep_field())),\n",
81+
" (2, \"human mitosis\", data.human_mitosis()/255.0)\n",
82+
" ), skip_duplicates=True\n",
83+
");"
84+
]
85+
},
86+
{
87+
"cell_type": "code",
88+
"execution_count": null,
89+
"metadata": {},
90+
"outputs": [],
91+
"source": [
92+
"fig, axs = plt.subplots(1, 2, figsize=(10, 5))\n",
93+
"for ax, image, title in zip(axs, *Image.fetch(\"image\", \"image_name\")):\n",
94+
" ax.imshow(image, cmap=\"gray_r\")\n",
95+
" ax.axis('off')\n",
96+
" ax.axis('equal')\n",
97+
" ax.set_title(title)\n"
98+
]
99+
},
100+
{
101+
"cell_type": "code",
102+
"execution_count": null,
103+
"metadata": {},
104+
"outputs": [],
105+
"source": [
106+
"@schema\n",
107+
"class BlobParamSet(dj.Lookup):\n",
108+
" definition = \"\"\"\n",
109+
" blob_paramset : int\n",
110+
" ---\n",
111+
" min_sigma : float\n",
112+
" max_sigma : float\n",
113+
" threshold : float\n",
114+
" \"\"\"\n",
115+
" contents = [\n",
116+
" (1, 2.0, 6.0, 0.001),\n",
117+
" (2, 3.0, 8.0, 0.002),\n",
118+
" (3, 4.0, 20.0, 0.01),\n",
119+
" ]\n",
120+
"\n",
121+
"\n",
122+
"@schema\n",
123+
"class Detection(dj.Computed):\n",
124+
" definition = \"\"\"\n",
125+
" -> Image\n",
126+
" -> BlobParamSet\n",
127+
" ---\n",
128+
" nblobs : int\n",
129+
" \"\"\"\n",
130+
"\n",
131+
" class Blob(dj.Part):\n",
132+
" definition = \"\"\"\n",
133+
" -> master\n",
134+
" blob_id : int\n",
135+
" ---\n",
136+
" x : float\n",
137+
" y : float\n",
138+
" r : float\n",
139+
" \"\"\"\n",
140+
"\n",
141+
" def make(self, key):\n",
142+
" # fetch inputs\n",
143+
" img = (Image & key).fetch1(\"image\")\n",
144+
" params = (BlobParamSet & key).fetch1()\n",
145+
"\n",
146+
" # compute results\n",
147+
" blobs = blob_doh(\n",
148+
" img, \n",
149+
" min_sigma=params['min_sigma'], \n",
150+
" max_sigma=params['max_sigma'], \n",
151+
" threshold=params['threshold'])\n",
152+
"\n",
153+
" # insert master and parts\n",
154+
" self.insert1(dict(key, nblobs=len(blobs)))\n",
155+
" self.Blob.insert(\n",
156+
" (dict(key, blob_id=i, x=x, y=y, r=r)\n",
157+
" for i, (x, y, r) in enumerate(blobs)))"
158+
]
159+
},
160+
{
161+
"cell_type": "code",
162+
"execution_count": null,
163+
"metadata": {},
164+
"outputs": [],
165+
"source": [
166+
"dj.Diagram(schema)"
167+
]
168+
},
169+
{
170+
"cell_type": "code",
171+
"execution_count": null,
172+
"metadata": {},
173+
"outputs": [],
174+
"source": [
175+
"Detection.populate(display_progress=True)"
176+
]
177+
},
178+
{
179+
"cell_type": "code",
180+
"execution_count": null,
181+
"metadata": {},
182+
"outputs": [],
183+
"source": [
184+
"Detection()"
185+
]
186+
},
187+
{
188+
"cell_type": "markdown",
189+
"metadata": {},
190+
"source": [
191+
"## Parameter sets\n",
192+
"\n",
193+
"Define a small lookup table of blob-detection parameters.\n"
194+
]
195+
},
196+
{
197+
"cell_type": "code",
198+
"execution_count": null,
199+
"metadata": {},
200+
"outputs": [],
201+
"source": [
202+
"fix, axes = plt.subplots(2, 3, figsize=(10, 6))\n",
203+
"for ax, key in zip(axes.ravel(), Detection.fetch(\"KEY\", order_by=\"image_id, blob_paramset\")):\n",
204+
" img = (Image & key).fetch1(\"image\")\n",
205+
" ax.imshow(img, cmap=\"gray_r\")\n",
206+
" ax.axis('off')\n",
207+
" ax.axis('equal')\n",
208+
" ax.set_title(str(key), fontsize=10)\n",
209+
" for x, y, r in zip(*(Detection.Blob & key).fetch(\"y\", \"x\", \"r\")):\n",
210+
" c = plt.Circle((x, y), r*1.2, color='r', alpha=0.5, fill=False)\n",
211+
" ax.add_patch(c)\n",
212+
"plt.suptitle(\"Detected blobs - all paramsets\")\n",
213+
"plt.tight_layout()"
214+
]
215+
},
216+
{
217+
"cell_type": "code",
218+
"execution_count": null,
219+
"metadata": {},
220+
"outputs": [],
221+
"source": [
222+
"@schema\n",
223+
"class SelectDetection(dj.Manual):\n",
224+
" definition = \"\"\"\n",
225+
" -> Image\n",
226+
" ---\n",
227+
" -> Detection\n",
228+
" \"\"\"\n",
229+
" "
230+
]
231+
},
232+
{
233+
"cell_type": "code",
234+
"execution_count": null,
235+
"metadata": {},
236+
"outputs": [],
237+
"source": [
238+
"SelectDetection.insert1(dict(image_id=1, blob_paramset=3))\n",
239+
"SelectDetection.insert1(dict(image_id=2, blob_paramset=1))"
240+
]
241+
},
242+
{
243+
"cell_type": "code",
244+
"execution_count": null,
245+
"metadata": {},
246+
"outputs": [],
247+
"source": [
248+
"dj.Diagram(schema)"
249+
]
250+
},
251+
{
252+
"cell_type": "code",
253+
"execution_count": null,
254+
"metadata": {},
255+
"outputs": [],
256+
"source": [
257+
"fix, axes = plt.subplots(1, 2, figsize=(8, 4))\n",
258+
"for ax, key in zip(axes.ravel(), SelectDetection.fetch(as_dict=True, order_by=\"image_id\")):\n",
259+
" img = (Image & key).fetch1(\"image\")\n",
260+
" ax.imshow(img, cmap=\"gray_r\")\n",
261+
" ax.axis('off')\n",
262+
" ax.axis('equal')\n",
263+
" ax.set_title(str(key), fontsize=10)\n",
264+
" for x, y, r in zip(*(Detection.Blob & key).fetch(\"y\", \"x\", \"r\")):\n",
265+
" c = plt.Circle((x, y), r*1.2, color='r', alpha=0.5, fill=False)\n",
266+
" ax.add_patch(c)\n",
267+
"plt.suptitle(\"Selected detections\", fontsize=16)\n",
268+
"plt.tight_layout()\n"
269+
]
270+
},
271+
{
272+
"cell_type": "markdown",
273+
"metadata": {},
274+
"source": [
275+
"## Detection master and part tables\n",
276+
"\n",
277+
"`Detection` is a computed table. When `populate()` runs, its `make()` method:\n",
278+
"\n",
279+
"1. Fetches the image and parameter set.\n",
280+
"2. Runs `skimage.feature.blob_doh` to compute blobs.\n",
281+
"3. Inserts one master row with the blob count.\n",
282+
"4. Inserts one `Detection.Blob` part row per blob (containing coordinates and radius).\n",
283+
"\n",
284+
"If any insert fails, the transaction is rolled back so master and parts stay synchronized.\n"
285+
]
286+
},
287+
{
288+
"cell_type": "markdown",
289+
"metadata": {},
290+
"source": [
291+
"## Results\n",
292+
"\n",
293+
"Populate the detection table and display both the master summary and the per-blob annotations.\n"
294+
]
295+
},
296+
{
297+
"cell_type": "markdown",
298+
"metadata": {},
299+
"source": [
300+
"## Takeaways\n",
301+
"\n",
302+
"- Master-part tables capture the structure “one job → many detailed results”.\n",
303+
"- Downstream analyses depend only on the master (`-> Detection`) yet can access part details when needed.\n",
304+
"- Populating the master guarantees atomic creation of all associated parts, preserving workflow integrity.\n"
305+
]
306+
},
307+
{
308+
"cell_type": "code",
309+
"execution_count": null,
310+
"metadata": {},
311+
"outputs": [],
312+
"source": [
313+
"schema.drop() # drop the schema for re-generating the tutorial from scratch."
314+
]
315+
},
316+
{
317+
"cell_type": "code",
318+
"execution_count": null,
319+
"metadata": {},
320+
"outputs": [],
321+
"source": []
322+
}
323+
],
324+
"metadata": {
325+
"kernelspec": {
326+
"display_name": "base",
327+
"language": "python",
328+
"name": "python3"
329+
},
330+
"language_info": {
331+
"codemirror_mode": {
332+
"name": "ipython",
333+
"version": 3
334+
},
335+
"file_extension": ".py",
336+
"mimetype": "text/x-python",
337+
"name": "python",
338+
"nbconvert_exporter": "python",
339+
"pygments_lexer": "ipython3",
340+
"version": "3.13.2"
341+
}
342+
},
343+
"nbformat": 4,
344+
"nbformat_minor": 2
345+
}

0 commit comments

Comments
 (0)