titi commited on
Commit
8e6b3e7
·
1 Parent(s): f6da738

initial commit

Browse files
Files changed (4) hide show
  1. README.md +98 -7
  2. app.py +230 -0
  3. core/utils.py +110 -0
  4. requirements.txt +4 -0
README.md CHANGED
@@ -1,13 +1,104 @@
1
  ---
2
- title: Pystackreg App
3
- emoji: 👁
4
- colorFrom: green
5
- colorTo: indigo
6
  sdk: gradio
7
- sdk_version: 5.29.0
8
  app_file: app.py
9
  pinned: false
10
- short_description: Web app for image stack registration using pystackreg
 
 
 
11
  ---
 
 
 
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Pystackreg Web App
3
+ emoji: 🧠
4
+ colorFrom: indigo
5
+ colorTo: cyan
6
  sdk: gradio
7
+ sdk_version: 5.25.1
8
  app_file: app.py
9
  pinned: false
10
+ tags:
11
+ - image-processing
12
+ - registration
13
+ - pystackreg
14
  ---
15
+ # 🧠 Stack Image Registration Web App
16
+ A web-based application for image stack registration powered by **Gradio** and **pystackreg**.
17
+ This tool allows users to align and stabilize multi-frame TIFF images using a variety of transformation models.
18
 
19
+ <p align="center">
20
+ <img src="https://raw.githubusercontent.com/qchapp/pystackreg-app/refs/heads/master/images/app.png" height="500">
21
+ </p>
22
+
23
+ ---
24
+
25
+ ## 🚀 Try the App
26
+ The application is running on [Hugging Face](https://huggingface.co/), try it using this [link](https://huggingface.co/spaces/qchapp/pystackreg-app)!
27
+
28
+ ---
29
+
30
+ ## 🛠️ Installation
31
+ We recommend performing the installation in a clean Python environment.
32
+
33
+ This app requires `python>=3.10`. To install dependencies, run:
34
+
35
+ ```sh
36
+ pip install -r requirements.txt
37
+ ```
38
+
39
+ ---
40
+
41
+ ## ▶️ Usage
42
+ To run the app locally:
43
+
44
+ ```sh
45
+ python app.py
46
+ ```
47
+
48
+ Then open your browser and go to: [http://localhost:7860](http://localhost:7860)
49
+
50
+ ---
51
+
52
+ ## 🔍 About Stack Registration
53
+ This app uses the [pystackreg](https://github.com/glichtner/pystackreg) library, a Python port of the TurboReg/StackReg algorithms.
54
+ It supports several transformation models for alignment:
55
+ - Translation
56
+ - Rigid Body
57
+ - Scaled Rotation
58
+ - Affine
59
+ - Bilinear
60
+
61
+ ---
62
+
63
+ ## 📂 Features
64
+ This application provides three core registration modes:
65
+
66
+ 1. **📚 Reference-Based Alignment**
67
+ Align all frames within a stack to a selected reference frame — either from the same stack or an external 3D image.
68
+
69
+ 2. **🎯 Stack-Based Alignment**
70
+ Align every frame in one stack to the first frame of another reference stack.
71
+
72
+ 3. **🧩 Frame-to-Frame Alignment**
73
+ Align a single frame to another frame within the same stack.
74
+
75
+ By default, the app uses the **Rigid Body** transformation mode for all alignment tasks.
76
+ If needed, users can enable **Advanced Settings** in each tab to select from other transformation models, such as Translation, Affine, or Bilinear.
77
+
78
+ Each mode offers:
79
+ - 🔍 Interactive image preview
80
+ - 🧭 Frame-by-frame navigation
81
+ - 💾 Downloadable aligned results
82
+ - ⚙️ Customizable transformation models via advanced options
83
+
84
+ ---
85
+
86
+ ### 📚 Credits
87
+
88
+ - **App Author**: [Quentin Chappuis](https://github.com/qchapp)
89
+ Developed the Gradio-based web interface and integrated `pystackreg` for image stack registration.
90
+
91
+ - **Core Registration Library**: [pystackreg](https://github.com/glichtner/pystackreg)
92
+ A Python port of the StackReg plugin, written by [Gregor Lichtenberg](https://github.com/glichtner).
93
+
94
+ - **Original Algorithm Author**: Philippe Thévenaz (EPFL)
95
+ The core algorithm was originally developed by Philippe Thévenaz and is described in the following publication:
96
+
97
+ > P. Thévenaz, U.E. Ruttimann, M. Unser.
98
+ > *A Pyramid Approach to Subpixel Registration Based on Intensity*.
99
+ > IEEE Transactions on Image Processing, vol. 7, no. 1, pp. 27–41, January 1998.
100
+ > [View paper](http://bigwww.epfl.ch/publications/thevenaz9801.html)
101
+
102
+ For more information, visit the [Biomedical Imaging Group at EPFL](http://bigwww.epfl.ch/).
103
+
104
+ ---
app.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import numpy as np
3
+ from PIL import Image
4
+ from pystackreg import StackReg
5
+ import imageio.v2 as iio
6
+ import tifffile
7
+ import tempfile
8
+
9
+ from core.utils import *
10
+
11
+ # Globals
12
+ original_frames, aligned_frames = [], []
13
+ ref_frames, mov_frames, reg_frames = [], [], []
14
+ custom_stack, reg_result = [], None
15
+ full_stack, full_aligned = [], []
16
+
17
+ # reset
18
+ def reset_intra_stack():
19
+ global original_frames, aligned_frames
20
+ original_frames, aligned_frames = [], []
21
+ return [None, gr.update(value=0, minimum=0, maximum=0), None, gr.update(value=0, minimum=0, maximum=0), None, gr.update(value=0, minimum=0, maximum=0), None, gr.update(value=0, minimum=0, maximum=0), None]
22
+
23
+ def reset_reference_based():
24
+ global ref_frames, mov_frames, reg_frames
25
+ ref_frames, mov_frames, reg_frames = [], [], []
26
+ return [None, None, None, gr.update(value=0, minimum=0, maximum=0), None, gr.update(value=0, minimum=0, maximum=0), None]
27
+
28
+ def reset_frame_to_frame():
29
+ global custom_stack, reg_result
30
+ custom_stack, reg_result = [], None
31
+ return [None, gr.update(value=0, minimum=0, maximum=0), gr.update(value=0, minimum=0, maximum=0), None, None]
32
+
33
+
34
+ # Registration Logic
35
+ def intra_stack_align(f, ref_idx, ext_file, ext_idx, mode):
36
+ global original_frames, aligned_frames
37
+ stack = load_stack(f)
38
+ original_frames = [Image.fromarray(fr) for fr in stack]
39
+ sr = StackReg(get_sr_mode(mode))
40
+
41
+ if ext_file:
42
+ ext_stack = load_stack(ext_file)
43
+ ref = ext_stack[ext_idx]
44
+ else:
45
+ ref = stack[ref_idx]
46
+
47
+ aligned = [sr.register_transform(ref, fr) for fr in stack]
48
+ aligned = normalize_stack(np.stack(aligned))
49
+ aligned_frames = [upscale(Image.fromarray(fr)) for fr in aligned]
50
+
51
+ path = tempfile.NamedTemporaryFile(suffix=".tif", delete=False).name
52
+ tifffile.imwrite(path, aligned, photometric='minisblack')
53
+
54
+ return (
55
+ original_frames[0],
56
+ gr.update(value=0, maximum=len(original_frames) - 1),
57
+ aligned_frames[0],
58
+ gr.update(value=0, maximum=len(aligned_frames) - 1),
59
+ path
60
+ )
61
+
62
+ def reference_align(ref_file, mov_file, mode):
63
+ global ref_frames, mov_frames, reg_frames
64
+ ref_stack = load_stack(ref_file)
65
+ mov_stack = load_stack(mov_file)
66
+ ref_frames = [Image.fromarray(f) for f in ref_stack]
67
+ mov_frames = [Image.fromarray(f) for f in mov_stack]
68
+ sr = StackReg(get_sr_mode(mode))
69
+ aligned = [sr.register_transform(ref_stack[0], f) for f in mov_stack]
70
+ aligned = normalize_stack(np.stack(aligned))
71
+ reg_frames = [upscale(Image.fromarray(f)) for f in aligned]
72
+ path = tempfile.NamedTemporaryFile(suffix=".tif", delete=False).name
73
+ tifffile.imwrite(path, aligned, photometric='minisblack')
74
+ return ref_frames[0], gr.update(value=0, maximum=len(ref_frames)-1), \
75
+ reg_frames[0], gr.update(value=0, maximum=len(reg_frames)-1), path
76
+
77
+ def frame_to_frame_align(file, ref_idx, mov_idx, mode):
78
+ global custom_stack, reg_result
79
+ stack = load_stack(file)
80
+ custom_stack = [Image.fromarray(f) for f in stack]
81
+ sr = StackReg(get_sr_mode(mode))
82
+ aligned = sr.register_transform(stack[ref_idx], stack[mov_idx])
83
+ aligned = normalize_stack(np.stack([aligned]))[0]
84
+ reg_result = Image.fromarray(aligned)
85
+ path = tempfile.NamedTemporaryFile(suffix=".tif", delete=False).name
86
+ tifffile.imwrite(path, aligned[np.newaxis, ...], photometric='minisblack')
87
+ return reg_result, path
88
+
89
+
90
+ # Interface
91
+ with gr.Blocks() as demo:
92
+ gr.Markdown("# 🧠 Pystackreg Web Application")
93
+ gr.Markdown(citation_markdown)
94
+
95
+ with gr.Accordion("📘 How to Use This App (Click to Expand)", open=False):
96
+ gr.Markdown(documentation_markdown)
97
+
98
+ with gr.Tab("📚 Reference-Based Alignment"):
99
+ with gr.Row():
100
+ file_input = gr.File(label="Upload Stack to Register")
101
+ use_ext_ref = gr.Checkbox(label="Use external reference stack")
102
+
103
+ with gr.Row():
104
+ ref_slider = gr.Slider(label="Reference Frame (from uploaded stack)", minimum=0, maximum=0, value=0, step=1, visible=True)
105
+ ext_ref_file = gr.File(label="Upload External Reference Stack (.tif)", visible=False)
106
+ ext_ref_slider = gr.Slider(label="Reference Frame (from external stack)", minimum=0, maximum=0, value=0, step=1, visible=False)
107
+
108
+ use_ext_ref.change(
109
+ lambda use_ext: (
110
+ gr.update(visible=not use_ext),
111
+ gr.update(visible=use_ext),
112
+ gr.update(visible=use_ext)
113
+ ),
114
+ use_ext_ref,
115
+ [ref_slider, ext_ref_file, ext_ref_slider]
116
+ )
117
+
118
+ ext_ref_file.change(
119
+ lambda f: gr.update(value=0, maximum=len(iio.mimread(f)) - 1) if f else gr.update(value=0, maximum=0),
120
+ ext_ref_file,
121
+ ext_ref_slider
122
+ )
123
+
124
+ with gr.Row():
125
+ show_advanced = gr.Checkbox(label="Show Advanced Settings", value=False)
126
+ mode_dropdown = gr.Dropdown(["TRANSLATION", "RIGID_BODY", "SCALED_ROTATION", "AFFINE", "BILINEAR"],
127
+ value="RIGID_BODY", label="Transformation Mode", visible=False)
128
+
129
+ show_advanced.change(lambda v: gr.update(visible=v), show_advanced, mode_dropdown)
130
+ run_btn = gr.Button("▶️ Align Stack")
131
+
132
+ with gr.Row():
133
+ original_image = gr.Image(label="Original Frame")
134
+ aligned_image = gr.Image(label="Aligned Frame")
135
+
136
+ with gr.Row():
137
+ original_slider = gr.Slider(label="Browse Original Stack", minimum=0, maximum=0, value=0, step=1)
138
+ aligned_slider = gr.Slider(label="Browse Aligned Stack", minimum=0, maximum=0, value=0, step=1)
139
+
140
+ download = gr.File(label="Download")
141
+
142
+ file_input.change(
143
+ lambda f: gr.update(value=0, maximum=len(iio.mimread(f)) - 1) if f else gr.update(value=0, maximum=0),
144
+ file_input,
145
+ ref_slider
146
+ )
147
+
148
+ run_btn.click(
149
+ intra_stack_align,
150
+ [file_input, ref_slider, ext_ref_file, ext_ref_slider, mode_dropdown],
151
+ [original_image, original_slider, aligned_image, aligned_slider, download]
152
+ )
153
+
154
+ original_slider.change(lambda i: original_frames[i] if 0 <= i < len(original_frames) else None, original_slider, original_image)
155
+ aligned_slider.change(lambda i: aligned_frames[i] if 0 <= i < len(aligned_frames) else None, aligned_slider, aligned_image)
156
+
157
+ reset_btn = gr.Button("🔄 Reset Tab")
158
+ reset_btn.click(
159
+ reset_intra_stack,
160
+ outputs=[
161
+ file_input, ref_slider, ext_ref_file, ext_ref_slider,
162
+ original_image, original_slider, aligned_image, aligned_slider, download
163
+ ]
164
+ )
165
+
166
+ with gr.Tab("🎯 Stack-Based Alignment"):
167
+ with gr.Row():
168
+ ref_input = gr.File(label="Reference Stack")
169
+ mov_input = gr.File(label="Moving Stack")
170
+
171
+ with gr.Row():
172
+ show_adv_ref = gr.Checkbox(label="Show Advanced Settings", value=False)
173
+ mode_dropdown_ref = gr.Dropdown(["TRANSLATION", "RIGID_BODY", "SCALED_ROTATION", "AFFINE", "BILINEAR"],
174
+ value="RIGID_BODY", label="Transformation Mode", visible=False)
175
+
176
+ show_adv_ref.change(lambda v: gr.update(visible=v), show_adv_ref, mode_dropdown_ref)
177
+ ref_btn = gr.Button("▶️ Register")
178
+
179
+ with gr.Row():
180
+ ref_image = gr.Image(label="Reference Frame")
181
+ reg_image = gr.Image(label="Registered Frame")
182
+
183
+ with gr.Row():
184
+ ref_slider = gr.Slider(label="Browse Ref", minimum=0, maximum=0, value=0, step=1)
185
+ reg_slider = gr.Slider(label="Browse Reg", minimum=0, maximum=0, value=0, step=1)
186
+
187
+ download_ref = gr.File(label="Download")
188
+
189
+ ref_btn.click(reference_align, [ref_input, mov_input, mode_dropdown_ref],
190
+ [ref_image, ref_slider, reg_image, reg_slider, download_ref])
191
+ ref_slider.change(lambda i: ref_frames[i] if 0 <= i < len(ref_frames) else None, ref_slider, ref_image)
192
+ reg_slider.change(lambda i: reg_frames[i] if 0 <= i < len(reg_frames) else None, reg_slider, reg_image)
193
+
194
+ reset_ref_btn = gr.Button("🔄 Reset Tab")
195
+ reset_ref_btn.click(
196
+ reset_reference_based,
197
+ outputs=[ref_input, mov_input, ref_image, ref_slider, reg_image, reg_slider, download_ref]
198
+ )
199
+
200
+
201
+ with gr.Tab("🧩 Frame-to-Frame Alignment"):
202
+ with gr.Row():
203
+ frame_file = gr.File(label="Upload Stack")
204
+ ref_idx = gr.Slider(label="Reference Frame", minimum=0, maximum=0, value=0, step=1)
205
+ mov_idx = gr.Slider(label="Moving Frame", minimum=0, maximum=0, value=0, step=1)
206
+
207
+ with gr.Row():
208
+ show_adv_ftf = gr.Checkbox(label="Show Advanced Settings", value=False)
209
+ mode_dropdown_ftf = gr.Dropdown(["TRANSLATION", "RIGID_BODY", "SCALED_ROTATION", "AFFINE", "BILINEAR"],
210
+ value="RIGID_BODY", label="Transformation Mode", visible=False)
211
+
212
+ show_adv_ftf.change(lambda v: gr.update(visible=v), show_adv_ftf, mode_dropdown_ftf)
213
+ frame_btn = gr.Button("▶️ Register Frame")
214
+ frame_output = gr.Image(label="Registered Output")
215
+ download_ftf = gr.File(label="Download")
216
+
217
+ frame_file.change(
218
+ lambda f: [gr.update(value=0, maximum=len(iio.mimread(f)) - 1)]*2 if f else [gr.update(value=0, maximum=0)]*2,
219
+ frame_file, [ref_idx, mov_idx]
220
+ )
221
+ frame_btn.click(frame_to_frame_align, [frame_file, ref_idx, mov_idx, mode_dropdown_ftf], [frame_output, download_ftf])
222
+
223
+ reset_ftf_btn = gr.Button("🔄 Reset Tab")
224
+ reset_ftf_btn.click(
225
+ reset_frame_to_frame,
226
+ outputs=[frame_file, ref_idx, mov_idx, frame_output, download_ftf]
227
+ )
228
+
229
+ if __name__ == "__main__":
230
+ demo.launch()
core/utils.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pystackreg import StackReg
2
+ import numpy as np
3
+ import imageio.v2 as iio
4
+ from PIL import Image
5
+
6
+ def get_sr_mode(mode_str): return {
7
+ "TRANSLATION": StackReg.TRANSLATION,
8
+ "RIGID_BODY": StackReg.RIGID_BODY,
9
+ "SCALED_ROTATION": StackReg.SCALED_ROTATION,
10
+ "AFFINE": StackReg.AFFINE,
11
+ "BILINEAR": StackReg.BILINEAR,
12
+ }.get(mode_str, StackReg.RIGID_BODY)
13
+
14
+ def normalize_stack(stack):
15
+ norm_stack = []
16
+ for frame in stack:
17
+ f = frame.astype(np.float32)
18
+ low, high = np.percentile(f, (1, 99))
19
+ f = np.clip(f, low, high)
20
+ f = (f - f.min()) / (np.ptp(f) + 1e-8) * 255 if np.ptp(f) > 0 else np.zeros_like(f)
21
+ norm_stack.append(f.astype(np.uint8))
22
+ return np.stack(norm_stack)
23
+
24
+ def upscale(image, factor=3):
25
+ return image.resize((image.width * factor, image.height * factor), Image.NEAREST)
26
+
27
+ def load_stack(file):
28
+ stack = np.array(iio.mimread(file))
29
+ if stack.ndim == 4 and stack.shape[-1] == 3:
30
+ stack = np.mean(stack, axis=-1)
31
+ return normalize_stack(stack)
32
+
33
+
34
+ ##### Markdowns #####
35
+
36
+ citation_markdown = """
37
+ ### 📘 Credits & Acknowledgments
38
+
39
+ Register TIFF stacks using multiple alignment strategies.
40
+
41
+ **App Author**: [Quentin Chappuis](https://github.com/qchapp)
42
+ **Pystackreg Author**: [Gregor Lichtenberg](https://github.com/glichtner)
43
+ 🔗 [Pystackreg on GitHub](https://github.com/glichtner/pystackreg)
44
+
45
+ **Original Algorithm Author**: Philippe Thévenaz (EPFL)
46
+ The core algorithm was originally developed by Philippe Thévenaz and is described in the following publication:
47
+
48
+ > P. Thévenaz, U.E. Ruttimann, M. Unser
49
+ > *A Pyramid Approach to Subpixel Registration Based on Intensity*
50
+ > *IEEE Transactions on Image Processing*, vol. 7, no. 1, pp. 27–41, January 1998.
51
+ > 🔗 [View paper](http://bigwww.epfl.ch/publications/thevenaz9801.html)
52
+ """
53
+
54
+
55
+ documentation_markdown = """
56
+ ### Overview
57
+
58
+ This app provides three registration modes for 2D TIFF image stacks using the `pystackreg` library.
59
+
60
+ ---
61
+
62
+ ### 📚 Tab 1: Reference-Based Alignment
63
+
64
+ Register a stack using a reference frame. You can:
65
+ - Use a frame from the **same stack** as reference
66
+ - Or upload an **external reference stack**
67
+
68
+ 1. Upload the stack you want to align.
69
+ 2. (Optional) Check "Use external reference stack" to align to a frame from another file.
70
+ 3. Choose the reference frame using the slider.
71
+ 4. (Optional) Choose transformation mode.
72
+ 5. Click **▶️ Align Stack**.
73
+ 6. Use sliders to browse original/aligned results and download the output.
74
+
75
+ ---
76
+
77
+ ### 🎯 Tab 2: Stack-Based Alignment
78
+
79
+ Align one stack (moving) to another (reference).
80
+
81
+ 1. Upload both **reference** and **moving** stacks.
82
+ 2. (Optional) Choose transformation mode.
83
+ 3. Click **▶️ Register** to align.
84
+ 4. Browse and download registered stack.
85
+
86
+ ---
87
+
88
+ ### 🧩 Tab 3: Frame-to-Frame Alignment
89
+
90
+ Align a **single frame to another** from the same stack.
91
+
92
+ 1. Upload a stack.
93
+ 2. Select **reference** and **moving** frames using sliders.
94
+ 3. Choose transformation mode.
95
+ 4. Click **▶️ Register Frame**.
96
+ 5. View/download the result.
97
+
98
+ ---
99
+
100
+ ### 🔄 Reset Buttons
101
+
102
+ Each tab includes a **Reset Tab** button that clears inputs, outputs, and internal state.
103
+
104
+ ---
105
+
106
+ ### 🧠 Credits
107
+
108
+ App developed by **Quentin Chapuis**
109
+ Library: [`pystackreg`](https://github.com/glichtner/pystackreg) by **Georg Lichtenberg**
110
+ """
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio==5.25.1
2
+ pystackreg
3
+ tifffile
4
+ imageio