napari-tmidas 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,324 @@
1
+ # processing_functions/careamics_denoising.py
2
+ """
3
+ Processing functions for denoising images using CAREamics.
4
+
5
+ This module provides functionality to denoise images using various models from CAREamics,
6
+ including Noise2Void (N2V) and CARE models. The functions support both 2D and 3D data.
7
+
8
+ The functions will automatically create and manage a dedicated environment for CAREamics
9
+ if it's not already installed in the main environment.
10
+ """
11
+ import os
12
+
13
+ import numpy as np
14
+
15
+ from napari_tmidas._registry import BatchProcessingRegistry
16
+
17
+ # Import the environment manager for CAREamics
18
+ from napari_tmidas.processing_functions.careamics_env_manager import (
19
+ create_careamics_env,
20
+ is_env_created,
21
+ run_careamics_in_env,
22
+ )
23
+
24
+ # Check if CAREamics is directly available in current environment
25
+ try:
26
+ from careamics import CAREamist
27
+
28
+ CAREAMICS_AVAILABLE = True
29
+ USE_DEDICATED_ENV = False
30
+ print("CAREamics found in current environment, using direct import")
31
+ except ImportError:
32
+ CAREAMICS_AVAILABLE = False
33
+ USE_DEDICATED_ENV = True
34
+ print(
35
+ "CAREamics not found in current environment, will use dedicated environment"
36
+ )
37
+
38
+
39
+ @BatchProcessingRegistry.register(
40
+ name="CAREamics Denoise (N2V/CARE)",
41
+ suffix="_denoised",
42
+ description="Denoise images using CAREamics (Noise2Void or CARE model)",
43
+ parameters={
44
+ "checkpoint_path": {
45
+ "type": str,
46
+ "default": "",
47
+ "description": "Path to the CAREamics model checkpoint file (.ckpt)",
48
+ },
49
+ "tile_size_x": {
50
+ "type": int,
51
+ "default": 32,
52
+ "min": 16,
53
+ "max": 512,
54
+ "description": "Tile size in X dimension",
55
+ },
56
+ "tile_size_y": {
57
+ "type": int,
58
+ "default": 32,
59
+ "min": 16,
60
+ "max": 512,
61
+ "description": "Tile size in Y dimension",
62
+ },
63
+ "tile_size_z": {
64
+ "type": int,
65
+ "default": 0,
66
+ "min": 0,
67
+ "max": 256,
68
+ "description": "Tile size in Z dimension (for 3D data)",
69
+ },
70
+ "batch_size": {
71
+ "type": int,
72
+ "default": 1,
73
+ "min": 1,
74
+ "max": 16,
75
+ "description": "Batch size for prediction",
76
+ },
77
+ "use_tta": {
78
+ "type": bool,
79
+ "default": True,
80
+ "description": "Use test-time augmentation for better results",
81
+ },
82
+ "force_dedicated_env": {
83
+ "type": bool,
84
+ "default": False,
85
+ "description": "Force using dedicated environment even if CAREamics is available",
86
+ },
87
+ },
88
+ )
89
+ def careamics_denoise(
90
+ image: np.ndarray,
91
+ checkpoint_path: str = "",
92
+ tile_size_z: int = 64,
93
+ tile_size_y: int = 64,
94
+ tile_size_x: int = 64,
95
+ tile_overlap_z: int = 8,
96
+ tile_overlap_y: int = 8,
97
+ tile_overlap_x: int = 8,
98
+ batch_size: int = 1,
99
+ use_tta: bool = True,
100
+ force_dedicated_env: bool = False,
101
+ ) -> np.ndarray:
102
+ """
103
+ Denoise images using CAREamics models.
104
+
105
+ This function loads a CAREamics model from a checkpoint file and uses it to denoise
106
+ the input image. The function supports both 2D and 3D data and handles tiling for
107
+ processing large images efficiently.
108
+
109
+ If CAREamics is not installed in the main environment, a dedicated virtual environment
110
+ will be automatically created and managed.
111
+
112
+ Parameters:
113
+ -----------
114
+ image : numpy.ndarray
115
+ Input image to denoise
116
+ checkpoint_path : str
117
+ Path to the CAREamics model checkpoint file (.ckpt)
118
+ tile_size_z : int
119
+ Tile size in Z dimension (for 3D data)
120
+ tile_size_y : int
121
+ Tile size in Y dimension
122
+ tile_size_x : int
123
+ Tile size in X dimension
124
+ tile_overlap_z : int
125
+ Tile overlap in Z dimension (for 3D data)
126
+ tile_overlap_y : int
127
+ Tile overlap in Y dimension
128
+ tile_overlap_x : int
129
+ Tile overlap in X dimension
130
+ batch_size : int
131
+ Batch size for prediction
132
+ use_tta : bool
133
+ Use test-time augmentation for better results
134
+ force_dedicated_env : bool
135
+ Force using dedicated environment even if CAREamics is available
136
+
137
+ Returns:
138
+ --------
139
+ numpy.ndarray
140
+ Denoised image with the same dimensions as the input
141
+ """
142
+ # Verify checkpoint path
143
+ if not checkpoint_path or not os.path.exists(checkpoint_path):
144
+ print(f"Checkpoint file not found: {checkpoint_path}")
145
+ print("Please provide a valid checkpoint file path.")
146
+ return image
147
+
148
+ # Determine whether to use dedicated environment
149
+ use_env = force_dedicated_env or USE_DEDICATED_ENV
150
+
151
+ careamics_denoise.thread_safe = False
152
+
153
+ if use_env:
154
+ print("Using dedicated CAREamics environment...")
155
+
156
+ # First check if the environment exists, create if not
157
+ if not is_env_created():
158
+ print(
159
+ "Creating dedicated CAREamics environment (this may take a few minutes)..."
160
+ )
161
+ create_careamics_env()
162
+ print("Environment created successfully.")
163
+
164
+ # Prepare arguments for the CAREamics function
165
+ args = {
166
+ "image": image,
167
+ "checkpoint_path": checkpoint_path,
168
+ "tile_size_z": tile_size_z,
169
+ "tile_size_y": tile_size_y,
170
+ "tile_size_x": tile_size_x,
171
+ "tile_overlap_z": tile_overlap_z,
172
+ "tile_overlap_y": tile_overlap_y,
173
+ "tile_overlap_x": tile_overlap_x,
174
+ "batch_size": batch_size,
175
+ "use_tta": use_tta,
176
+ }
177
+
178
+ # Calculate tile overlap automatically (e.g., 25% of tile size)
179
+ def compute_overlap(tile_size, fraction=0.25):
180
+ # Ensure overlap is at least 1 and less than tile size
181
+ overlap = max(1, int(tile_size * fraction))
182
+ return min(overlap, tile_size - 1)
183
+
184
+ # Inside careamics_denoise, after parsing tile sizes:
185
+ tile_overlap_z = (
186
+ compute_overlap(tile_size_z) if len(image.shape) >= 3 else None
187
+ )
188
+ tile_overlap_y = compute_overlap(tile_size_y)
189
+ tile_overlap_x = compute_overlap(tile_size_x)
190
+
191
+ # Run CAREamics in the dedicated environment
192
+ print("Running CAREamics in dedicated environment...")
193
+ return run_careamics_in_env("predict", args)
194
+
195
+ else:
196
+ print("Running CAREamics in current environment...")
197
+ # Use CAREamics directly in the current environment
198
+ try:
199
+ print(f"Loading CAREamics model from: {checkpoint_path}")
200
+ # Initialize the CAREamist model
201
+ careamist = CAREamist(
202
+ checkpoint_path, os.path.dirname(checkpoint_path)
203
+ )
204
+
205
+ # Determine dimensionality
206
+ is_3d = len(image.shape) >= 3
207
+
208
+ if is_3d:
209
+ print(f"Processing 3D data with shape: {image.shape}")
210
+ # Determine axes based on dimensionality
211
+ if len(image.shape) == 3:
212
+ # ZYX format
213
+ axes = "ZYX"
214
+ tile_size = (tile_size_z, tile_size_y, tile_size_x)
215
+ tile_overlap = (
216
+ tile_overlap_z,
217
+ tile_overlap_y,
218
+ tile_overlap_x,
219
+ )
220
+ print(f"Using axes configuration: {axes}")
221
+ elif len(image.shape) == 4:
222
+ # Assuming TZYX format
223
+ axes = "TZYX"
224
+ tile_size = (tile_size_z, tile_size_y, tile_size_x)
225
+ tile_overlap = (
226
+ tile_overlap_z,
227
+ tile_overlap_y,
228
+ tile_overlap_x,
229
+ )
230
+ print(f"Using axes configuration: {axes}")
231
+ else:
232
+ # Unknown format, try to handle it
233
+ print(
234
+ f"Warning: Unusual data shape: {image.shape}. Defaulting to 'TZYX'"
235
+ )
236
+ axes = "TZYX"
237
+ tile_size = (tile_size_z, tile_size_y, tile_size_x)
238
+ tile_overlap = (
239
+ tile_overlap_z,
240
+ tile_overlap_y,
241
+ tile_overlap_x,
242
+ )
243
+ else:
244
+ print(f"Processing 2D data with shape: {image.shape}")
245
+ # 2D data
246
+ if len(image.shape) == 2:
247
+ # YX format
248
+ axes = "YX"
249
+ tile_size = (tile_size_y, tile_size_x)
250
+ tile_overlap = (tile_overlap_y, tile_overlap_x)
251
+ print(f"Using axes configuration: {axes}")
252
+ else:
253
+ # Unknown format, try to handle it
254
+ print(
255
+ f"Warning: Unusual data shape: {image.shape}. Defaulting to 'YX'"
256
+ )
257
+ axes = "YX"
258
+ tile_size = (tile_size_y, tile_size_x)
259
+ tile_overlap = (tile_overlap_y, tile_overlap_x)
260
+
261
+ # Run the prediction
262
+ print(
263
+ f"Running prediction with tile size: {tile_size}, overlap: {tile_overlap}"
264
+ )
265
+ print(f"Using batch size: {batch_size}, TTA: {use_tta}")
266
+
267
+ prediction = careamist.predict(
268
+ source=image,
269
+ tile_size=tile_size,
270
+ tile_overlap=tile_overlap,
271
+ axes=axes,
272
+ batch_size=batch_size,
273
+ tta=use_tta,
274
+ )
275
+
276
+ # # Handle output shape
277
+ # if prediction.shape != image.shape:
278
+ # print(f"Warning: Prediction shape {prediction.shape} differs from input shape {image.shape}")
279
+ # prediction = np.squeeze(prediction)
280
+
281
+ # # If shapes still don't match, try to reshape
282
+ # if prediction.shape != image.shape:
283
+ # print(f"Warning: Shapes still don't match after squeezing. Using original dimensions.")
284
+ # try:
285
+ # prediction = prediction.reshape(image.shape)
286
+ # except ValueError:
287
+ # print("Error: Could not reshape prediction to match input shape.")
288
+ # return image
289
+
290
+ # print(f"Denoising completed. Output shape: {prediction.shape}")
291
+ return prediction
292
+
293
+ except (RuntimeError, ValueError, ImportError) as e:
294
+ import traceback
295
+
296
+ print(
297
+ f"Error during CAREamics denoising in current environment: {str(e)}"
298
+ )
299
+ traceback.print_exc()
300
+
301
+ # If we haven't already tried using the dedicated environment, try that as a fallback
302
+ if not force_dedicated_env:
303
+ print(
304
+ "Attempting fallback to dedicated CAREamics environment..."
305
+ )
306
+ args = {
307
+ "image": image,
308
+ "checkpoint_path": checkpoint_path,
309
+ "tile_size_z": tile_size_z,
310
+ "tile_size_y": tile_size_y,
311
+ "tile_size_x": tile_size_x,
312
+ "tile_overlap_z": tile_overlap_z,
313
+ "tile_overlap_y": tile_overlap_y,
314
+ "tile_overlap_x": tile_overlap_x,
315
+ "batch_size": batch_size,
316
+ "use_tta": use_tta,
317
+ }
318
+
319
+ if not is_env_created():
320
+ create_careamics_env()
321
+
322
+ return run_careamics_in_env("predict", args)
323
+
324
+ return None
@@ -0,0 +1,339 @@
1
+ # processing_functions/careamics_env_manager.py
2
+ """
3
+ This module manages a dedicated virtual environment for CAREamics.
4
+ """
5
+
6
+ import contextlib
7
+ import os
8
+ import platform
9
+ import shutil
10
+ import subprocess
11
+ import tempfile
12
+ import venv
13
+
14
+ import tifffile
15
+
16
+ # Define the environment directory in user's home folder
17
+ ENV_DIR = os.path.join(
18
+ os.path.expanduser("~"), ".napari-tmidas", "envs", "careamics"
19
+ )
20
+
21
+
22
+ def is_careamics_installed():
23
+ """Check if CAREamics is installed in the current environment."""
24
+ try:
25
+ import importlib.util
26
+
27
+ return importlib.util.find_spec("careamics") is not None
28
+ except ImportError:
29
+ return False
30
+
31
+
32
+ def is_env_created():
33
+ """Check if the dedicated environment exists."""
34
+ env_python = get_env_python_path()
35
+ return os.path.exists(env_python)
36
+
37
+
38
+ def get_env_python_path():
39
+ """Get the path to the Python executable in the environment."""
40
+ if platform.system() == "Windows":
41
+ return os.path.join(ENV_DIR, "Scripts", "python.exe")
42
+ else:
43
+ return os.path.join(ENV_DIR, "bin", "python")
44
+
45
+
46
+ def create_careamics_env():
47
+ """Create a dedicated virtual environment for CAREamics."""
48
+ # Ensure the environment directory exists
49
+ os.makedirs(os.path.dirname(ENV_DIR), exist_ok=True)
50
+
51
+ # Remove existing environment if it exists
52
+ if os.path.exists(ENV_DIR):
53
+ shutil.rmtree(ENV_DIR)
54
+
55
+ print(f"Creating CAREamics environment at {ENV_DIR}...")
56
+
57
+ # Create a new virtual environment
58
+ venv.create(ENV_DIR, with_pip=True)
59
+
60
+ # Path to the Python executable in the new environment
61
+ env_python = get_env_python_path()
62
+
63
+ # Upgrade pip first
64
+ print("Upgrading pip...")
65
+ subprocess.check_call(
66
+ [env_python, "-m", "pip", "install", "--upgrade", "pip"]
67
+ )
68
+
69
+ # Install PyTorch first for compatibility
70
+ # Try to detect if CUDA is available
71
+ cuda_available = False
72
+ try:
73
+ import torch
74
+
75
+ cuda_available = torch.cuda.is_available()
76
+ print(f"CUDA is {'available' if cuda_available else 'not available'}")
77
+ except ImportError:
78
+ print("PyTorch not detected in main environment")
79
+
80
+ if cuda_available:
81
+ # Install PyTorch with CUDA support
82
+ print("Installing PyTorch with CUDA support...")
83
+ subprocess.check_call(
84
+ [env_python, "-m", "pip", "install", "torch", "torchvision"]
85
+ )
86
+ else:
87
+ # Install PyTorch without CUDA
88
+ print("Installing PyTorch without CUDA support...")
89
+ subprocess.check_call(
90
+ [env_python, "-m", "pip", "install", "torch", "torchvision"]
91
+ )
92
+
93
+ # Install CAREamics and dependencies
94
+ print("Installing CAREamics in the dedicated environment...")
95
+ subprocess.check_call(
96
+ [env_python, "-m", "pip", "install", "careamics[tensorboard]"]
97
+ )
98
+
99
+ # Install tifffile for image handling
100
+ subprocess.check_call(
101
+ [env_python, "-m", "pip", "install", "tifffile", "numpy"]
102
+ )
103
+
104
+ # Check if installation was successful
105
+ try:
106
+ # Run a simple script to verify CAREamics is installed and working
107
+ check_script = """
108
+ import sys
109
+ try:
110
+ import careamics
111
+ print(f"CAREamics version: {careamics.__version__}")
112
+ from careamics import CAREamist
113
+ print("CAREamist imported successfully")
114
+ import torch
115
+ print(f"PyTorch version: {torch.__version__}")
116
+ print(f"CUDA available: {torch.cuda.is_available()}")
117
+ if torch.cuda.is_available():
118
+ print(f"CUDA version: {torch.version.cuda}")
119
+ print(f"GPU: {torch.cuda.get_device_name(0)}")
120
+ print("SUCCESS: CAREamics environment is working correctly")
121
+ except Exception as e:
122
+ print(f"ERROR: {str(e)}")
123
+ sys.exit(1)
124
+ """
125
+ with tempfile.NamedTemporaryFile(
126
+ mode="w", suffix=".py", delete=False
127
+ ) as temp:
128
+ temp.write(check_script)
129
+ temp_path = temp.name
130
+
131
+ try:
132
+ result = subprocess.run(
133
+ [env_python, temp_path],
134
+ check=True,
135
+ capture_output=True,
136
+ text=True,
137
+ )
138
+ print(result.stdout)
139
+ if "SUCCESS" in result.stdout:
140
+ print(
141
+ "CAREamics environment created and verified successfully."
142
+ )
143
+ else:
144
+ print(
145
+ "WARNING: CAREamics environment created but verification uncertain."
146
+ )
147
+ finally:
148
+ os.unlink(temp_path)
149
+
150
+ except subprocess.CalledProcessError as e:
151
+ print(f"Error during CAREamics installation: {e}")
152
+ print(f"Output: {e.output}")
153
+ print(f"Errors: {e.stderr}")
154
+ raise RuntimeError("Failed to create CAREamics environment") from e
155
+
156
+ return env_python
157
+
158
+
159
+ def run_careamics_in_env(func_name, args_dict):
160
+ """
161
+ Run CAREamics in a dedicated environment.
162
+
163
+ Parameters:
164
+ -----------
165
+ func_name : str
166
+ Name of the CAREamics function to run (e.g., 'predict')
167
+ args_dict : dict
168
+ Dictionary of arguments for CAREamics function
169
+
170
+ Returns:
171
+ --------
172
+ numpy.ndarray
173
+ Denoised image
174
+ """
175
+ # Ensure the environment exists
176
+ if not is_env_created():
177
+ create_careamics_env()
178
+
179
+ # Prepare temporary files
180
+ with tempfile.NamedTemporaryFile(
181
+ suffix=".tif", delete=False
182
+ ) as input_file, tempfile.NamedTemporaryFile(
183
+ suffix=".tif", delete=False
184
+ ) as output_file, tempfile.NamedTemporaryFile(
185
+ mode="w", suffix=".py", delete=False
186
+ ) as script_file:
187
+
188
+ # Save input image to temp file
189
+ tifffile.imwrite(
190
+ input_file.name, args_dict["image"], compression="zlib"
191
+ )
192
+
193
+ # Create a temporary script to run CAREamics
194
+ script = f"""
195
+ import sys
196
+ import os
197
+ import numpy as np
198
+ import tifffile
199
+ from pathlib import Path
200
+
201
+ try:
202
+ from careamics import CAREamist
203
+
204
+ # Load input image
205
+ print(f"Loading image from {{os.path.basename('{input_file.name}')}}")
206
+ image = tifffile.imread('{input_file.name}')
207
+ print(f"Input image shape: {{image.shape}}, dtype: {{image.dtype}}")
208
+
209
+ # Load model
210
+ print(f"Loading model from {{os.path.basename('{args_dict['checkpoint_path']}')}}")
211
+ model = CAREamist('{args_dict['checkpoint_path']}',os.path.dirname('{args_dict['checkpoint_path']}'))
212
+
213
+ # Determine dimensionality
214
+ dims = len(image.shape)
215
+ is_3d = dims >= 3
216
+
217
+ if is_3d:
218
+ print(f"Processing 3D data with shape: {{image.shape}}")
219
+ # Determine axes based on dimensionality
220
+ if dims == 3:
221
+ # ZYX format
222
+ axes = "ZYX"
223
+ tile_size = ({args_dict.get('tile_size_z', 64)},
224
+ {args_dict.get('tile_size_y', 64)},
225
+ {args_dict.get('tile_size_x', 64)})
226
+ tile_overlap = ({args_dict.get('tile_overlap_z', 32)},
227
+ {args_dict.get('tile_overlap_y', 32)},
228
+ {args_dict.get('tile_overlap_x', 32)})
229
+ elif dims == 4:
230
+ # Assuming TZYX format
231
+ axes = "TZYX"
232
+ tile_size = ({args_dict.get('tile_size_z', 64)},
233
+ {args_dict.get('tile_size_y', 64)},
234
+ {args_dict.get('tile_size_x', 64)})
235
+ tile_overlap = ({args_dict.get('tile_overlap_z', 32)},
236
+ {args_dict.get('tile_overlap_y', 32)},
237
+ {args_dict.get('tile_overlap_x', 32)})
238
+ else:
239
+ print(f"Warning: Unusual data shape: {{image.shape}}. Defaulting to 'TZYX'")
240
+ axes = "TZYX"
241
+ tile_size = ({args_dict.get('tile_size_z', 64)},
242
+ {args_dict.get('tile_size_y', 64)},
243
+ {args_dict.get('tile_size_x', 64)})
244
+ tile_overlap = ({args_dict.get('tile_overlap_z', 32)},
245
+ {args_dict.get('tile_overlap_y', 32)},
246
+ {args_dict.get('tile_overlap_x', 32)})
247
+ else:
248
+ print(f"Processing 2D data with shape: {{image.shape}}")
249
+ # 2D data
250
+ if dims == 2:
251
+ # YX format
252
+ axes = "YX"
253
+ tile_size = ({args_dict.get('tile_size_y', 64)},
254
+ {args_dict.get('tile_size_x', 64)})
255
+ tile_overlap = ({args_dict.get('tile_overlap_y', 32)},
256
+ {args_dict.get('tile_overlap_x', 32)})
257
+ else:
258
+ print(f"Warning: Unusual data shape: {{image.shape}}. Defaulting to 'YX'")
259
+ axes = "YX"
260
+ tile_size = ({args_dict.get('tile_size_y', 64)},
261
+ {args_dict.get('tile_size_x', 64)})
262
+ tile_overlap = ({args_dict.get('tile_overlap_y', 32)},
263
+ {args_dict.get('tile_overlap_x', 32)})
264
+
265
+ print(f"Using axes: {{axes}}, tile_size: {{tile_size}}, tile_overlap: {{tile_overlap}}")
266
+
267
+ # Run the prediction
268
+ print("Starting CAREamics prediction...")
269
+ prediction = model.predict(
270
+ source=image,
271
+ tile_size=tile_size,
272
+ tile_overlap=tile_overlap,
273
+ axes=axes,
274
+ batch_size={args_dict.get('batch_size', 1)},
275
+ tta={args_dict.get('use_tta', True)},
276
+ )
277
+
278
+ # # Handle output shape
279
+ # if prediction.shape != image.shape:
280
+ # print(f"Warning: Prediction shape {{prediction.shape}} differs from input shape {{image.shape}}")
281
+ # prediction = np.squeeze(prediction)
282
+
283
+ # # If shapes still don't match, try to reshape
284
+ # if prediction.shape != image.shape:
285
+ # print(f"Warning: Shapes still don't match after squeezing. Using original dimensions.")
286
+
287
+ # print(f"Denoising completed. Output shape: {{prediction.shape}}")
288
+
289
+ # Save result
290
+ print(f"Saving result to {{os.path.basename('{output_file.name}')}}")
291
+ tifffile.imwrite('{output_file.name}', prediction, compression="zlib")
292
+ print("Done!")
293
+
294
+ # Exit with success code
295
+ sys.exit(0)
296
+
297
+ except Exception as e:
298
+ import traceback
299
+ print(f"Error in CAREamics processing: {{e}}")
300
+ traceback.print_exc()
301
+ sys.exit(1)
302
+ """
303
+
304
+ # Write script
305
+ script_file.write(script)
306
+ script_file.flush()
307
+
308
+ try:
309
+ # Run the script in the dedicated environment
310
+ env_python = get_env_python_path()
311
+ result = subprocess.run(
312
+ [env_python, script_file.name], capture_output=True, text=True
313
+ )
314
+
315
+ # Print output
316
+ print(result.stdout)
317
+
318
+ # Check for errors
319
+ if result.returncode != 0:
320
+ print("Error in CAREamics processing:")
321
+ print(result.stderr)
322
+ raise RuntimeError(
323
+ f"CAREamics denoising failed with error code {result.returncode}"
324
+ )
325
+
326
+ # Read and return the results
327
+ denoised_image = tifffile.imread(output_file.name)
328
+ return denoised_image
329
+
330
+ except (RuntimeError, subprocess.CalledProcessError) as e:
331
+ print(f"Error in CAREamics processing: {e}")
332
+ # Return original image in case of error
333
+ return args_dict["image"]
334
+
335
+ finally:
336
+ # Clean up temporary files
337
+ for file_path in [input_file.name, output_file.name, script_file.name]:
338
+ with contextlib.suppress(FileNotFoundError, PermissionError):
339
+ os.unlink(file_path)