Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide more error feedback during image import in the GUI #83

Merged
merged 4 commits into from
Nov 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/release/release_v1.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ See the [table of CLI changes](../cli.md#changes-in-magellanmapper-v15) for a su
- Plane index is only added when exporting multiple planes
- Improvements to image import
- Single plane RAW images can be loaded when importing files from a directory, in addition to multiplane RAW files
- Skips single plane files that give errors (eg non-image files in the input directory)
- Provides import error feedback in the GUI
- The known parts of the import image shape are populated even if the full shape is not known
- The Bio-Formats library has been updated to support more file formats (from Bio-Formats 5.1.8 to 6.6.0 via Python-Bioformats 1.1.0 to 4.0.5, respectively)
- Fixed to disable the import directory button when metadata is insufficient
Expand Down
10 changes: 10 additions & 0 deletions magmap/gui/import_threads.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from magmap.io import importer
from magmap.settings import config

_logger = config.logger.getChild(__name__)


class SetupImportThread(QtCore.QThread):
"""Thread for setting up file import by extracting image metadata.
Expand Down Expand Up @@ -81,6 +83,14 @@ def run(self):
img5d = importer.import_multiplane_images(
self.chl_paths, self.prefix, self.import_md, config.series,
fn_feedback=self.fn_feedback)

except Exception as e:
# provide feedback for any errors during import
self.fn_feedback(f"Error during import:\n{e}")
if config.log_path:
self.fn_feedback(f"See log for more info: {config.log_path}\n")
_logger.exception(e)

finally:
if img5d is not None:
# set up the image for immediate use within MagellanMapper
Expand Down
2 changes: 1 addition & 1 deletion magmap/io/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ def process_cli_args():
log_path = os.path.join(
config.user_app_dirs.user_data_dir, "out.log")
# log to file
logs.add_file_handler(config.logger, log_path)
config.log_path = logs.add_file_handler(config.logger, log_path)

# redirect standard out/error to logging
sys.stdout = logs.LogWriter(config.logger.info)
Expand Down
81 changes: 44 additions & 37 deletions magmap/io/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1162,44 +1162,51 @@ def import_files():
for filei, file in enumerate(chl_files):
libmag.printcb("importing {}".format(file), fn_feedback)
try:
# load standard image types
img = io.imread(file)
except ValueError:
# load as a RAW image file
img = np.memmap(
file, dtype=import_md[config.MetaKeys.DTYPE],
shape=tuple(import_md[config.MetaKeys.SHAPE][2:4]),
mode="r")

if rgb_to_grayscale and img.ndim >= 3 and img.shape[2] == 3:
# assume that 3-channel images are RGB
# TODO: remove rgb_to_grayscale since must give single channel?
print("converted from 3-channel (assuming RGB) to grayscale")
img = color.rgb2gray(img)

if img5d is None:
# generate an array for all planes and channels based on
# dimensions of the first extracted plane and any channel keys
shape = [1, len(chl_files), *img.shape]
try:
# load standard image types
img = io.imread(file)
except ValueError:
# load as a RAW image file
img = np.memmap(
file, dtype=import_md[config.MetaKeys.DTYPE],
shape=tuple(import_md[config.MetaKeys.SHAPE][2:4]),
mode="r")

if rgb_to_grayscale and img.ndim >= 3 and img.shape[2] == 3:
# assume that 3-channel images are RGB
# TODO: remove rgb_to_grayscale since must give single chl?
libmag.printcb(
"Converted from 3-channel (assuming RGB) to grayscale",
fn_feedback)
img = color.rgb2gray(img)

if img5d is None:
# generate an array for all planes and channels based on
# dims of the first extracted plane and any channel keys
shape = [1, len(chl_files), *img.shape]
if num_chls > 1:
shape.append(num_chls)
os.makedirs(
os.path.dirname(filename_image5d_npz), exist_ok=True)
img5d = np.lib.format.open_memmap(
filename_image5d_npz, mode="w+", dtype=img.dtype,
shape=tuple(shape))

# insert plane, without using channel dimension if no channel
# designators were found in file names
if num_chls > 1:
shape.append(num_chls)
os.makedirs(
os.path.dirname(filename_image5d_npz), exist_ok=True)
img5d = np.lib.format.open_memmap(
filename_image5d_npz, mode="w+", dtype=img.dtype,
shape=tuple(shape))

# insert plane, without using channel dimension if no channel
# designators were found in file names
if num_chls > 1:
img5d[0, filei, ..., chli] = img
else:
img5d[0, filei] = img

# measure near low/high intensity values
low, high = np.percentile(img, (0.5, 99.5))
lows.append(low)
highs.append(high)
img5d[0, filei, ..., chli] = img
else:
img5d[0, filei] = img

# measure near low/high intensity values
low, high = np.percentile(img, (0.5, 99.5))
lows.append(low)
highs.append(high)
except ValueError as e1:
libmag.printcb(
f"Could not load '{file}'; skipping it because of error: "
f"{e1}", fn_feedback)

lows_chls.append(min(lows))
highs_chls.append(max(highs))
Expand Down
3 changes: 3 additions & 0 deletions magmap/settings/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ class Verbosity(Enum):
#: :class:`logging.Logger`: Root logger for the application.
logger = logs.setup_logger()

#: Path to log file.
log_path: Optional[pathlib.Path] = None


# IMAGE FILES

Expand Down
18 changes: 10 additions & 8 deletions magmap/settings/logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ def update_log_level(logger, level):
return logger


def add_file_handler(logger, path, backups=5):
def add_file_handler(
logger: logging.Logger, path: str, backups: int = 5) -> pathlib.Path:
"""Add a rotating log file handler with a new log file.

Rotates the file each time this function is called for the given number
Expand All @@ -107,17 +108,18 @@ def add_file_handler(logger, path, backups=5):
creating a log filed named with an incremented number (eg ``out1.log``).

Args:
logger (:class:`logging.Logger`): Logger to update.
path (str): Path to log. Increments to ``path<n>.<ext>`` if the
logger: Logger to update.
path: Path to log. Increments to ``path<n>.<ext>`` if the
file at ``path`` cannot be rotated.
backups (int): Number of backups to maintain; defaults to 5.
backups: Number of backups to maintain; defaults to 5.

Returns:
:class:`logging.Logger`: The logger for chained calls.
The log output path.

"""
# check if log file already exists
pathl = pathlib.Path(path)
path_log = pathl
roll = pathl.is_file()

# create a rotations file handler to manage number of backups while
Expand All @@ -129,8 +131,8 @@ def add_file_handler(logger, path, backups=5):
try:
# if the existing file at path cannot be rotated, increment the
# filename to create a new series of rotating log files
path_log = (pathl if i == 0 else
f"{pathl.parent / pathl.stem}{i}{pathl.suffix}")
path_log = pathl if i == 0 else pathlib.Path(
f"{pathl.parent / pathl.stem}{i}{pathl.suffix}")
logger.debug(f"Trying logger path: {path_log}")
handler_file = handlers.RotatingFileHandler(
path_log, backupCount=backups)
Expand All @@ -150,4 +152,4 @@ def add_file_handler(logger, path, backups=5):
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
logger.addHandler(handler_file)

return logger
return path_log