|
47 | 47 | io_logger = logging.getLogger(__name__) |
48 | 48 |
|
49 | 49 | def logger_setup(cp_path=".cellpose", logfile_name="run.log", stdout_file_replacement=None): |
| 50 | + """Set up logging to a file and stdout (or a file replacement). |
| 51 | +
|
| 52 | + Creates the log directory if it doesn't exist, removes any existing log |
| 53 | + file, and configures the root logger to write INFO-level and above messages |
| 54 | + to both a log file and stdout (or a replacement file). |
| 55 | +
|
| 56 | + Parameters |
| 57 | + ---------- |
| 58 | + cp_path : str, optional |
| 59 | + Directory name under the user's home directory for log output. |
| 60 | + Default is ".cellpose". |
| 61 | + logfile_name : str, optional |
| 62 | + Name of the log file created inside cp_path. Default is "run.log". |
| 63 | + stdout_file_replacement : str or None, optional |
| 64 | + If provided, log output is written to this file path instead of stdout. |
| 65 | +
|
| 66 | + Returns |
| 67 | + ------- |
| 68 | + logger : logging.Logger |
| 69 | + Configured logger for this module. Only INFO and above messages are |
| 70 | + emitted by default. To enable debug output, call |
| 71 | + ``logger.setLevel(logging.DEBUG)`` on the returned logger. |
| 72 | +
|
| 73 | + Notes |
| 74 | + ----- |
| 75 | + The log file is deleted and recreated on each call. |
| 76 | + """ |
50 | 77 | cp_dir = pathlib.Path.home().joinpath(cp_path) |
51 | 78 | cp_dir.mkdir(exist_ok=True) |
52 | 79 | log_file = cp_dir.joinpath(logfile_name) |
@@ -189,6 +216,28 @@ def imread(filename): |
189 | 216 | if not ND2: |
190 | 217 | io_logger.critical("ERROR: need to 'pip install nd2' to load in .nd2 file") |
191 | 218 | return None |
| 219 | + else: |
| 220 | + with nd2.ND2File(filename) as nd2_file: |
| 221 | + img = nd2_file.asarray() |
| 222 | + sizes = nd2_file.sizes |
| 223 | + |
| 224 | + kept_axes = [nd2.AXIS.Y, nd2.AXIS.X, nd2.AXIS.CHANNEL, nd2.AXIS.Z] |
| 225 | + # For multi-dimensional data (T, P, etc.), take first frame/position |
| 226 | + # Work backwards through axes to avoid index shifting |
| 227 | + for i, (ax_name, size) in reversed(list(enumerate(sizes.items()))): |
| 228 | + # Keep Y, X, C, Z; remove or reduce everything else |
| 229 | + if ax_name not in kept_axes: |
| 230 | + if size > 1: |
| 231 | + io_logger.warning( |
| 232 | + f"ND2 file has {size} {ax_name} - using first only" |
| 233 | + ) |
| 234 | + # Take first element (works for both size=1 and size>1) |
| 235 | + img = np.take(img, 0, axis=i) |
| 236 | + |
| 237 | + # Result should now be YX, CYX, ZYX, or CZYX depending on original axes |
| 238 | + # nd2 preserves axis order from sizes dict (usually C, Z, Y, X) |
| 239 | + return img |
| 240 | + |
192 | 241 | elif ext == ".nrrd": |
193 | 242 | if not NRRD: |
194 | 243 | io_logger.critical( |
@@ -230,40 +279,47 @@ def imread_2D(img_file): |
230 | 279 | img_out (numpy.ndarray): The 3-channel image data as a NumPy array. |
231 | 280 | """ |
232 | 281 | img = imread(img_file) |
| 282 | + if img is None: |
| 283 | + raise ValueError(f"could not read image file {img_file}") |
233 | 284 | return transforms.convert_image(img, do_3D=False) |
234 | 285 |
|
235 | 286 |
|
236 | 287 | def imread_3D(img_file): |
237 | 288 | """ |
238 | 289 | Read in a 3D image file and convert it to have a channel axis last automatically. Attempts to do this for multi-channel and grayscale images. |
239 | 290 |
|
240 | | - If multichannel image, the channel axis is assumed to be the smallest dimension, and the z axis is the next smallest dimension. |
241 | | - Use `cellpose.io.imread()` to load the full image without selecting the z and channel axes. |
242 | | - |
| 291 | + For grayscale images (3D array), axis 0 is assumed to be the Z axis (e.g., Z x Y x X). |
| 292 | + For multichannel images (4D array), the channel axis is assumed to be the smallest dimension, |
| 293 | + and the Z axis is assumed to be the first remaining axis after the channel axis is removed. |
| 294 | +
|
| 295 | + Use ``cellpose.io.imread()`` to load the full image without automatic axis selection, |
| 296 | + then specify ``z_axis`` and ``channel_axis`` manually when calling ``model.eval``. |
| 297 | +
|
243 | 298 | Args: |
244 | 299 | img_file (str): The path to the image file. |
245 | 300 |
|
246 | 301 | Returns: |
247 | | - img_out (numpy.ndarray): The image data as a NumPy array. |
| 302 | + img_out (numpy.ndarray): The image data as a NumPy array with channels last, or None if loading fails. |
248 | 303 | """ |
249 | 304 | img = imread(img_file) |
| 305 | + if img is None: |
| 306 | + raise ValueError(f"could not read image file {img_file}") |
250 | 307 |
|
251 | 308 | dimension_lengths = list(img.shape) |
252 | 309 |
|
253 | 310 | # grayscale images: |
254 | 311 | if img.ndim == 3: |
255 | 312 | channel_axis = None |
256 | 313 | # guess at z axis: |
257 | | - z_axis = np.argmin(dimension_lengths) |
| 314 | + z_axis = 0 |
258 | 315 |
|
259 | 316 | elif img.ndim == 4: |
260 | 317 | # guess at channel axis: |
261 | 318 | channel_axis = np.argmin(dimension_lengths) |
262 | | - |
263 | | - # guess at z axis: |
264 | | - # set channel axis to max so argmin works: |
265 | | - dimension_lengths[channel_axis] = max(dimension_lengths) |
266 | | - z_axis = np.argmin(dimension_lengths) |
| 319 | + dimensions = list(range(img.ndim)) |
| 320 | + dimensions.pop(channel_axis) |
| 321 | + # guess at z axis as the first remaining dimension: |
| 322 | + z_axis = dimensions[0] |
267 | 323 |
|
268 | 324 | else: |
269 | 325 | raise ValueError(f'image shape error, 3D image must 3 or 4 dimensional. Number of dimensions: {img.ndim}') |
|
0 commit comments