diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/ComputeIntensityLevelTransformationsMaintenanceTask.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/ComputeIntensityLevelTransformationsMaintenanceTask.java new file mode 100644 index 0000000000000000000000000000000000000000..2ac1cb877d046b7267b09a38cef5aa30af64ebcf --- /dev/null +++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/ComputeIntensityLevelTransformationsMaintenanceTask.java @@ -0,0 +1,338 @@ +/* + * Copyright 2012 ETH Zuerich, CISD + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.systemsx.cisd.openbis.dss.etl; + +import java.io.File; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; + +import javax.sql.DataSource; + +import net.lemnik.eodsql.QueryTool; + +import org.apache.log4j.Logger; + +import ch.systemsx.cisd.base.image.IImageTransformerFactory; +import ch.systemsx.cisd.common.collections.ExtendedBlockingQueueFactory; +import ch.systemsx.cisd.common.collections.PersistentExtendedBlockingQueueDecorator; +import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException; +import ch.systemsx.cisd.common.image.IntensityRescaling.Levels; +import ch.systemsx.cisd.common.io.hierarchical_content.api.IHierarchicalContent; +import ch.systemsx.cisd.common.logging.LogCategory; +import ch.systemsx.cisd.common.logging.LogFactory; +import ch.systemsx.cisd.common.maintenance.IMaintenanceTask; +import ch.systemsx.cisd.common.utilities.PropertyUtils; +import ch.systemsx.cisd.openbis.dss.etl.dto.api.v1.transformations.ImageTransformation; +import ch.systemsx.cisd.openbis.dss.etl.dto.api.v1.transformations.ImageTransformationBuffer; +import ch.systemsx.cisd.openbis.dss.etl.jython.SimpleImageDataSetRegistrator; +import ch.systemsx.cisd.openbis.dss.generic.shared.IEncapsulatedOpenBISService; +import ch.systemsx.cisd.openbis.dss.generic.shared.IHierarchicalContentProvider; +import ch.systemsx.cisd.openbis.dss.generic.shared.ServiceProvider; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ExternalData; +import ch.systemsx.cisd.openbis.plugin.screening.shared.imaging.dataaccess.IImagingTransformerDAO; +import ch.systemsx.cisd.openbis.plugin.screening.shared.imaging.dataaccess.ImgChannelDTO; +import ch.systemsx.cisd.openbis.plugin.screening.shared.imaging.dataaccess.ImgImageDatasetDTO; +import ch.systemsx.cisd.openbis.plugin.screening.shared.imaging.dataaccess.ImgImageEnrichedDTO; +import ch.systemsx.cisd.openbis.plugin.screening.shared.imaging.dataaccess.ImgImageTransformationDTO; + +/** + * @author Jakub Straszewski + */ +public class ComputeIntensityLevelTransformationsMaintenanceTask implements IMaintenanceTask +{ + + protected static final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION, + ComputeIntensityLevelTransformationsMaintenanceTask.class); + + /* + * Settings + */ + + private static final String COMPUTE_MIN_MAX_LEVELS_KEY = "compute-min-max-levels"; + + private static final String DEFAULT_COMPUTE_MIN_MAX_LEVELS = "true"; + + private static final String MIN_LEVEL_KEY = "min-level"; + + private static final String MAX_LEVEL_KEY = "max-level"; + + private static final String INTENSITY_THRESHOLD_KEY = "intensity-threshold"; + + private static final String TRASNFORMATION_CODE_KEY = "transformation-code"; + + private static final String TRASNFORMATION_LABEL_KEY = "transformation-label"; + + private static final String TRANSFORMATION_DESC_KEY = "transformation-description"; + + private static final String IS_DEFAULT_TRANSFORMATION_KEY = "is-default-transformation"; + + private static final String STATUS_FILENAME_KEY = "status-filename"; + + private boolean computeMinMaxLevels; + + private int minLevel; + + private int maxLevel; + + private float intensityThreshold; + + private String transformationCode; + + private String transformationLabel; + + private String transformationDescription; + + private boolean isDefaultTransformation; + + private File queueFile; + + /* + * Resources + */ + private DataSource dataSource; + + private IImagingTransformerDAO dao; + + private IEncapsulatedOpenBISService service; + + private IHierarchicalContentProvider contentProvider; + + @Override + public void setUp(String pluginName, Properties properties) + { + this.dataSource = ServiceProvider.getDataSourceProvider().getDataSource(properties); + + dao = QueryTool.getQuery(dataSource, IImagingTransformerDAO.class); + dao.listSpots(0L); // testing correct database set up + service = ServiceProvider.getOpenBISService(); + contentProvider = ServiceProvider.getHierarchicalContentProvider(); + + computeMinMaxLevels = + Boolean.valueOf(properties.getProperty(COMPUTE_MIN_MAX_LEVELS_KEY, + DEFAULT_COMPUTE_MIN_MAX_LEVELS)); + + minLevel = Integer.valueOf(properties.getProperty(MIN_LEVEL_KEY, "-1")); + maxLevel = Integer.valueOf(properties.getProperty(MAX_LEVEL_KEY, "-1")); + + if (false == computeMinMaxLevels) + { + if (minLevel == -1 || maxLevel == -1) + { + throw new ConfigurationFailureException( + "The intensity ranges levels must be correctly defined if calculating them is disabled."); + } + } + + intensityThreshold = Float.valueOf(properties.getProperty(INTENSITY_THRESHOLD_KEY, "-1")); + if (computeMinMaxLevels && (intensityThreshold < 0 || intensityThreshold > 1)) + { + throw new ConfigurationFailureException( + "The " + + INTENSITY_THRESHOLD_KEY + + " property must be provided as a real number between 0 and 1 if the levels are calculated."); + } + + transformationCode = + PropertyUtils.getMandatoryProperty(properties, TRASNFORMATION_CODE_KEY); + + transformationLabel = properties.getProperty(TRASNFORMATION_LABEL_KEY, "Fixed rescaling"); + + transformationDescription = + properties + .getProperty( + TRANSFORMATION_DESC_KEY, + SimpleImageDataSetRegistrator.OPTIMAL_DATASET_INTENSITY_RESCALING_DESCRIPTION); + + String isDefaultTransformationString = + PropertyUtils.getMandatoryProperty(properties, IS_DEFAULT_TRANSFORMATION_KEY); + + isDefaultTransformation = Boolean.valueOf(isDefaultTransformationString); + + String queueFilePath = PropertyUtils.getMandatoryProperty(properties, STATUS_FILENAME_KEY); + queueFile = new File(queueFilePath); + } + + /** + * method overriden to create an underlying database connection. + */ + @Override + public void execute() + { + PersistentExtendedBlockingQueueDecorator<Long> queue = + ExtendedBlockingQueueFactory.<Long> createSmartPersist(queueFile); + + try + { + executeTransformations(queue); + } catch (Exception e) + { + operationLog.error(e); + } + queue.close(); + } + + private void executeTransformations(PersistentExtendedBlockingQueueDecorator<Long> queue) + { + long lastSeenId = getLastSeenIDFromQueue(queue); + + Long nextId = dao.tryGetNextDatasetId(lastSeenId); + + // if this value is null, it means there are no newer datasets + if (nextId != null) + { + if (false == dao.hasDatasetDefinedTransformation(nextId, transformationCode)) + { + executeTransformations(nextId); + } + storeLastSeenInTheQueue(queue, nextId); + } + } + + private void executeTransformations(long datasetId) + { + ImgImageDatasetDTO imageDataset = dao.tryGetImageDatasetById(datasetId); + List<ImgChannelDTO> channels = dao.getChannelsByDatasetId(datasetId); + + operationLog.info("Will create transformation " + transformationCode + " for the dataset " + + imageDataset.getPermId()); + + final Levels intensityRange; + if (computeMinMaxLevels) + { + intensityRange = computeIntensityRange(imageDataset, channels); + if (intensityRange != null) + { + operationLog.info("Calculated intensity ranges for dataset " + + imageDataset.getPermId() + ". " + intensityRange); + } + } else + { + intensityRange = new Levels(minLevel, maxLevel); + operationLog.info("Using default intensity range values " + intensityRange); + } + + // if intensity ranges are null, it means that this transformation cannot be applied to this + // dataset. Thus we do nothing + if (intensityRange != null) + { + IImageTransformerFactory factory = createImageTransformerFactory(intensityRange); + + for (ImgChannelDTO ch : channels) + { + // execute inserts on dao, but without a commit + addCommonIntensityRangeTransformation(ch, factory); + } + + // commits the dao transaction + dao.commit(); + operationLog.info("Succesfully applied intensity ranges transformation to " + + imageDataset.getPermId() + " on " + channels.size() + " channels."); + } else + { + operationLog.warn("Could not calculate intensity ranges for dataset " + + imageDataset.getPermId() + ". Will be skipped."); + } + } + + private IImageTransformerFactory createImageTransformerFactory(final Levels intensityRange) + { + ImageTransformationBuffer buffer = new ImageTransformationBuffer(); + + ImageTransformation imageTransformation = + buffer.appendRescaleGrayscaleIntensity(intensityRange.getMinLevel(), + intensityRange.getMaxLevel(), "N/A"); + + IImageTransformerFactory factory = imageTransformation.getImageTransformerFactory(); + return factory; + } + + /** + * Computes intensity ranges for all images in the dataset. + * + * @return calculated levels, or null if calculation cannot be done because of a permanent + * problem + */ + private Levels computeIntensityRange(ImgImageDatasetDTO imageDataset, + List<ImgChannelDTO> channels) + { + ExternalData dataset = service.tryGetDataSet(imageDataset.getPermId()); + + if (dataset == null) + { + operationLog + .warn("The dataset specified in the image_data_set doesn't exist in openbis."); + return null; + } + + IHierarchicalContent content = contentProvider.asContent(dataset); + + final List<File> imagePaths = new LinkedList<File>(); + + for (ImgChannelDTO ch : channels) + { + List<ImgImageEnrichedDTO> images = + dao.listHCSImages(imageDataset.getPermId(), ch.getCode()); + + for (ImgImageEnrichedDTO img : images) + { + File imageFile = content.getNode(img.getFilePath()).getFile(); + imagePaths.add(imageFile); + if (false == imageFile.exists()) + { + throw new IllegalStateException("The image file for dataset " + + imageDataset.getPermId() + " doesn't exist."); + } + } + } + + return SimpleImageDataSetRegistrator.tryComputeCommonIntensityRange(null, imagePaths, + intensityThreshold); + } + + private void addCommonIntensityRangeTransformation(ImgChannelDTO channel, + IImageTransformerFactory factory) + { + ImgImageTransformationDTO dto = + new ImgImageTransformationDTO(transformationCode, transformationLabel, + transformationDescription, isDefaultTransformation, channel.getId(), + factory, false); + + dao.addImageTransformation(dto); + } + + private void storeLastSeenInTheQueue(PersistentExtendedBlockingQueueDecorator<Long> queue, + long lastSeenId) + { + queue.offer(lastSeenId); + while (queue.size() > 1) + { + queue.remove(); + } + } + + private long getLastSeenIDFromQueue(PersistentExtendedBlockingQueueDecorator<Long> queue) + { + long lastSeenId = -1; + if (queue.size() > 0) + { + lastSeenId = queue.peek(); + } + return lastSeenId; + } + +} diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/v1/transformations/ImageTransformationBuffer.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/v1/transformations/ImageTransformationBuffer.java index d23e534f0288f61efd2390560dc19f20297dedf5..8e1db0f4b78bd68fb966c4cd3f61750a9267b0c0 100644 --- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/v1/transformations/ImageTransformationBuffer.java +++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/api/v1/transformations/ImageTransformationBuffer.java @@ -240,7 +240,7 @@ public class ImageTransformationBuffer * @param userFriendlyLabelOrNull label of the transformation. If null a default label is * assigned. */ - private static ImageTransformation createRescaleGrayscaleIntensity(int blackPointIntensity, + public static ImageTransformation createRescaleGrayscaleIntensity(int blackPointIntensity, int whitePointIntensity, String userFriendlyLabelOrNull) { if (blackPointIntensity > whitePointIntensity || blackPointIntensity < 0 diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/jython/SimpleImageDataSetRegistrator.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/jython/SimpleImageDataSetRegistrator.java index 856940a71e2b745f1b38d2799cd4792fb0eaf85c..ae8afdae4bb103431990328e5d0bf838da64176b 100644 --- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/jython/SimpleImageDataSetRegistrator.java +++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/jython/SimpleImageDataSetRegistrator.java @@ -75,7 +75,7 @@ import ch.systemsx.cisd.openbis.plugin.screening.shared.api.v1.dto.Geometry; */ public class SimpleImageDataSetRegistrator { - private static final String OPTIMAL_DATASET_INTENSITY_RESCALING_DESCRIPTION = + public static final String OPTIMAL_DATASET_INTENSITY_RESCALING_DESCRIPTION = "Optimal intensity rescaling for a series of images. " + "It allows to compare images of one plate's dataset to each other." + "At the same time it causes that the conversion to 8 bit color depth looses less information, " @@ -556,7 +556,13 @@ public class SimpleImageDataSetRegistrator return normalizedCodes; } - private static Levels tryComputeCommonIntensityRange(IImageReader readerOrNull, + /** + * Computes common intensity range for a list of files. + * + * @return calculated levels or null if calculation couldn't succeed because some images where + * not in gray scale + */ + public static Levels tryComputeCommonIntensityRange(IImageReader readerOrNull, List<File> imageFiles, float threshold) { String libraryName = (readerOrNull == null) ? null : readerOrNull.getLibraryName(); diff --git a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/shared/imaging/dataaccess/IImagingReadonlyQueryDAO.java b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/shared/imaging/dataaccess/IImagingReadonlyQueryDAO.java index f6eec6d11ca3783ecb1aa3fe045e66c4d62f89a6..3b2b6f18ed7b958ad9e6dc62a75d2d963483d211 100644 --- a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/shared/imaging/dataaccess/IImagingReadonlyQueryDAO.java +++ b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/shared/imaging/dataaccess/IImagingReadonlyQueryDAO.java @@ -238,6 +238,15 @@ public interface IImagingReadonlyQueryDAO extends BaseQuery @Select("select * from IMAGE_DATA_SETS where PERM_ID = ?{1}") public ImgImageDatasetDTO tryGetImageDatasetByPermId(String datasetPermId); + @Select("select * from IMAGE_DATA_SETS where ID = ?{1}") + public ImgImageDatasetDTO tryGetImageDatasetById(long datasetId); + + @Select("select id from IMAGE_DATA_SETS where ID > ?{1} order by id limit 1") + public Long tryGetNextDatasetId(long datasetId); + + @Select("select count(*) > 0 from channels ch, image_transformations it where ch.ds_id = ?{1} and it.channel_id = ch.id and it.code = upper(?{2})") + public boolean hasDatasetDefinedTransformation(long datasetId, String transformationCode); + @Select("select * from ANALYSIS_DATA_SETS where PERM_ID = ?{1}") public ImgAnalysisDatasetDTO tryGetAnalysisDatasetByPermId(String datasetPermId);