#include "MantidAPI/FileProperty.h" #include "MantidAPI/MultipleFileProperty.h" #include "MantidAPI/NumericAxis.h" #include "MantidAPI/RegisterFileLoader.h" #include "MantidDataHandling/LoadFITS.h" #include "MantidDataObjects/Workspace2D.h" #include "MantidKernel/BoundedValidator.h" #include "MantidKernel/UnitFactory.h" #include <boost/algorithm/string.hpp> #include <Poco/BinaryReader.h> #include <Poco/FileStream.h> #include <Poco/Path.h> using namespace Mantid::DataHandling; using namespace Mantid::DataObjects; using namespace Mantid::API; using namespace Mantid::Kernel; namespace Mantid { namespace DataHandling { // Register the algorithm into the AlgorithmFactory DECLARE_FILELOADER_ALGORITHM(LoadFITS) const std::string LoadFITS::g_BIT_DEPTH_NAME = "BitDepthName"; const std::string LoadFITS::g_ROTATION_NAME = "RotationName"; const std::string LoadFITS::g_AXIS_NAMES_NAME = "AxisNames"; const std::string LoadFITS::g_IMAGE_KEY_NAME = "ImageKeyName"; const std::string LoadFITS::g_HEADER_MAP_NAME = "HeaderMapFile"; const std::string LoadFITS::g_defaultImgType = "SAMPLE"; /** * Constructor. Just initialize everything to prevent issues. */ LoadFITS::LoadFITS() : m_headerScaleKey(), m_headerOffsetKey(), m_headerBitDepthKey(), m_headerRotationKey(), m_headerImageKeyKey(), m_headerAxisNameKeys(), m_mapFile(), m_pixelCount(0) { setupDefaultKeywordNames(); } /** * Return the confidence with with this algorithm can load the file * @param descriptor A descriptor for the file * @returns An integer specifying the confidence level. 0 indicates it will not * be used */ int LoadFITS::confidence(Kernel::FileDescriptor &descriptor) const { // Should really improve this to check the file header (of first file at // least) to make sure it contains the fields wanted return (descriptor.extension() == ".fits" || descriptor.extension() == ".fit") ? 80 : 0; } /** * Initialise the algorithm. Declare properties which can be set before execution * (input) or * read from after the execution (output). */ void LoadFITS::init() { // Specify file extensions which can be associated with a FITS file. std::vector<std::string> exts, exts2; // Declare the Filename algorithm property. Mandatory. Sets the path to the // file to load. exts.clear(); exts.push_back(".fits"); exts.push_back(".fit"); exts2.push_back(".*"); declareProperty(new MultipleFileProperty("Filename", exts), "The name of the input file (note that you can give " "multiple file names separated by commas)."); declareProperty(new API::WorkspaceProperty<API::Workspace>( "OutputWorkspace", "", Kernel::Direction::Output)); declareProperty( new Kernel::PropertyWithValue<bool>("LoadAsRectImg", false, Kernel::Direction::Input), "If enabled (not by default), the output Workspace2D will have " "one histogram per row and one bin per pixel, such that a 2D " "color plot (color fill plot) will display an image."); auto zeroOrPosDbl = boost::make_shared<BoundedValidator<double>>(); zeroOrPosDbl->setLower(0.0); declareProperty("FilterNoiseLevel", 0.0, zeroOrPosDbl, "Threshold to remove noisy pixels. Try 50 for example."); auto posInt = boost::make_shared<BoundedValidator<int>>(); posInt->setLower(1); declareProperty("BinSize", 1, posInt, "Rebunch n*n on both axes, generating pixels with sums of " "blocks of n by n original pixels."); auto posDbl = boost::make_shared<BoundedValidator<double>>(); posDbl->setLower(std::numeric_limits<double>::epsilon()); declareProperty("Scale", 80.0, posDbl, "Pixels per cm.", Kernel::Direction::Input); declareProperty( new FileProperty(g_HEADER_MAP_NAME, "", FileProperty::OptionalDirectory, "", Kernel::Direction::Input), "A file mapping header key names to non-standard names [line separated " "values in the format KEY=VALUE, e.g. BitDepthName=BITPIX] - do not use " "this if you want to keep compatibility with standard FITS files."); } /** * Execute the algorithm. */ void LoadFITS::exec() { // for non-standard headers, by default won't do anything mapHeaderKeys(); std::string fName = getPropertyValue("Filename"); std::vector<std::string> paths; boost::split(paths, fName, boost::is_any_of(",")); int binSize = getProperty("BinSize"); double noiseThresh = getProperty("FilterNoiseLevel"); bool loadAsRectImg = getProperty("LoadAsRectImg"); const std::string outWSName = getPropertyValue("OutputWorkspace"); doLoadFiles(paths, outWSName, loadAsRectImg, binSize, noiseThresh); } /** * Load header(s) from FITS file(s) into FITSInfo header * struct(s). This is usually the first step when loading FITS files * into workspaces or anything else. In the simplest case, paths has * only one string and only one header struct is added in headers. * * @param paths File name(s) * @param headers Vector where to store the header struct(s) * * @throws std::runtime_error if issues are found in the headers */ void LoadFITS::doLoadHeaders(const std::vector<std::string> &paths, std::vector<FITSInfo> &headers) { headers.resize(paths.size()); for (size_t i = 0; i < paths.size(); ++i) { headers[i].extension = ""; headers[i].filePath = paths[i]; // Get various pieces of information from the file header which are used // to // create the workspace try { parseHeader(headers[i]); } catch (std::exception &e) { // Unable to parse the header, throw. throw std::runtime_error( "Severe problem found while parsing the header of " "this FITS file (" + paths[i] + "). This file may not be standard FITS. Error description: " + e.what()); } // Get and convert specific standard header values which will are // needed to know how to load the data: BITPIX, NAXIS, NAXISi (where i = // 1..NAXIS, e.g. NAXIS2 for two axis). try { std::string tmpBitPix = headers[i].headerKeys[m_headerBitDepthKey]; if (boost::contains(tmpBitPix, "-")) { boost::erase_all(tmpBitPix, "-"); headers[i].isFloat = true; } else { headers[i].isFloat = false; } try { headers[i].bitsPerPixel = boost::lexical_cast<int>(tmpBitPix); } catch (std::exception &e) { throw std::runtime_error( "Coult not interpret the entry number of bits per pixel (" + m_headerBitDepthKey + ") as an integer. Error: " + e.what()); } // Check that the files use bit depths of either 8, 16 or 32 if (headers[i].bitsPerPixel != 8 && headers[i].bitsPerPixel != 16 && headers[i].bitsPerPixel != 32 && headers[i].bitsPerPixel != 64) throw std::runtime_error( "This algorithm only supports 8, 16, 32 or 64 " "bits per pixel. The header of file '" + paths[i] + "' says that its bit depth is: " + boost::lexical_cast<std::string>(headers[i].bitsPerPixel)); } catch (std::exception &e) { throw std::runtime_error( "Failed to process the '" + m_headerBitDepthKey + "' entry (bits per pixel) in the header of this file: " + paths[i] + ". Error description: " + e.what()); } try { // Add the image key, use the value in the FITS header if found, // otherwise default (to SAMPLE). auto it = headers[i].headerKeys.find(m_headerImageKeyKey); if (headers[i].headerKeys.end() != it) { headers[i].imageKey = it->second; } else { headers[i].imageKey = g_defaultImgType; } } catch (std::exception &e) { throw std::runtime_error("Failed to process the '" + m_headerImageKeyKey + "' entry (type of image: sample, dark, open) in " "the header of this file: " + paths[i] + ". Error description: " + e.what()); } try { headers[i].numberOfAxis = static_cast<int>(m_headerAxisNameKeys.size()); for (int j = 0; headers.size() > i && j < headers[i].numberOfAxis; ++j) { headers[i].axisPixelLengths.push_back(boost::lexical_cast<size_t>( headers[i].headerKeys[m_headerAxisNameKeys[j]])); // only debug level, when loading multiple files this is very verbose g_log.debug() << "Found axis length header entry: " << m_headerAxisNameKeys[j] << " = " << headers[i].axisPixelLengths.back() << std::endl; } // Various extensions to the FITS format are used elsewhere, and // must be parsed differently if used. This loader Loader // doesn't support this. headers[i].extension = headers[i].headerKeys["XTENSION"]; } catch (std::exception &e) { throw std::runtime_error( "Failed to process the '" + m_headerNAxisNameKey + "' entries (dimensions) in the header of this file: " + paths[i] + ". Error description: " + e.what()); } // scale parameter, header BSCALE in the fits standard if ("" == headers[i].headerKeys[m_headerScaleKey]) { headers[i].scale = 1; } else { try { headers[i].scale = boost::lexical_cast<double>( headers[i].headerKeys[m_headerScaleKey]); } catch (std::exception &e) { throw std::runtime_error( "Coult not interpret the entry number of bits per pixel (" + m_headerBitDepthKey + " = " + headers[i].headerKeys[m_headerScaleKey] + ") as a floating point number (double). Error: " + e.what()); } } // data offsset parameter, header BZERO in the fits standard if ("" == headers[i].headerKeys[m_headerOffsetKey]) { headers[i].offset = 0; } else { try { headers[i].offset = boost::lexical_cast<int>(headers[i].headerKeys[m_headerOffsetKey]); } catch (std::exception & /*e*/) { // still, second try with floating point format (as used for example // by // Starlight XPRESS cameras) try { double doff = boost::lexical_cast<double>( headers[i].headerKeys[m_headerOffsetKey]); double intPart; if (0 != modf(doff, &intPart)) { // anyway we'll do a cast, but warn if there was a fraction g_log.warning() << "The value given in the FITS header entry for the data " "offset (" + m_headerOffsetKey + " = " + headers[i].headerKeys[m_headerOffsetKey] + ") has a fractional part, and it will be ignored!" << std::endl; } headers[i].offset = static_cast<int>(doff); } catch (std::exception &e) { throw std::runtime_error( "Coult not interpret the entry number of data offset (" + m_headerOffsetKey + " = " + headers[i].headerKeys[m_headerOffsetKey] + ") as an integer number nor a floating point " "number (double). Error: " + e.what()); } } } // Check each header is valid/supported: standard (no extension to // FITS), and has two axis headerSanityCheck(headers[i], headers[0]); } } /** * Checks that a FITS header (once loaded) is valid/supported: * standard (no extension to FITS), and has two axis with the expected * dimensions. * * @param hdr FITS header struct loaded from a file - to check * * @param hdrFirst FITS header struct loaded from a (first) reference file - *to * compare against * * @throws std::exception if there's any issue or unsupported entry in the * header */ void LoadFITS::headerSanityCheck(const FITSInfo &hdr, const FITSInfo &hdrFirst) { bool valid = true; if (hdr.extension != "") { valid = false; g_log.error() << "File " << hdr.filePath << ": extensions found in the header." << std::endl; } if (hdr.numberOfAxis != 2) { valid = false; g_log.error() << "File " << hdr.filePath << ": the number of axes is not 2 but: " << hdr.numberOfAxis << std::endl; } // Test current item has same axis values as first item. if (hdr.axisPixelLengths[0] != hdrFirst.axisPixelLengths[0]) { valid = false; g_log.error() << "File " << hdr.filePath << ": the number of pixels in the first dimension differs " "from the first file loaded (" << hdrFirst.filePath << "): " << hdr.axisPixelLengths[0] << " != " << hdrFirst.axisPixelLengths[0] << std::endl; } if (hdr.axisPixelLengths[1] != hdrFirst.axisPixelLengths[1]) { valid = false; g_log.error() << "File " << hdr.filePath << ": the number of pixels in the second dimension differs" "from the first file loaded (" << hdrFirst.filePath << "): " << hdr.axisPixelLengths[0] << " != " << hdrFirst.axisPixelLengths[0] << std::endl; } // Check the format is correct and create the Workspace if (!valid) { // Invalid files, record error throw std::runtime_error( "An issue has been found in the header of this FITS file: " + hdr.filePath + ". This algorithm currently doesn't support FITS files with " "non-standard extensions, more than two axis " "of data, or has detected that all the files are " "not similar."); } } /** * Create FITS file information for each file selected. Loads headers * and data from the files and creates and fills the output * workspace(s). * * @param paths File names as given in the algorithm input property * * @param outWSName name of the output (group) workspace to create * * @param loadAsRectImg Load files with 1 spectrum per row and 1 bin * per column, so a color fill plot displays the image * * @param binSize size to rebin (1 == no re-bin == default) * * @param noiseThresh threshold for noise filtering * * @throw std::runtime_error when load fails (for example a memory * allocation issue, wrong rebin requested, etc.) */ void LoadFITS::doLoadFiles(const std::vector<std::string> &paths, const std::string &outWSName, bool loadAsRectImg, int binSize, double noiseThresh) { std::vector<FITSInfo> headers; doLoadHeaders(paths, headers); // No extension is set -> it's the standard format which we can parse. if (headers[0].numberOfAxis > 0) m_pixelCount += headers[0].axisPixelLengths[0]; // Presumably 2 axis, but futureproofing. for (int i = 1; i < headers[0].numberOfAxis; ++i) { m_pixelCount *= headers[0].axisPixelLengths[i]; } // Check consistency of binSize asap for (int i = 0; i < headers[0].numberOfAxis; ++i) { if (0 != (headers[0].axisPixelLengths[i] % binSize)) { throw std::runtime_error( "Cannot rebin this image in blocks of " + boost::lexical_cast<std::string>(binSize) + " x " + boost::lexical_cast<std::string>(binSize) + " pixels as requested because the size of dimension " + boost::lexical_cast<std::string>(i + 1) + " (" + boost::lexical_cast<std::string>(headers[0].axisPixelLengths[i]) + ") is not a multiple of the bin size."); } } MantidImage imageY(headers[0].axisPixelLengths[1], std::vector<double>(headers[0].axisPixelLengths[0])); MantidImage imageE(headers[0].axisPixelLengths[1], std::vector<double>(headers[0].axisPixelLengths[0])); size_t bytes = (headers[0].bitsPerPixel / 8) * m_pixelCount; std::vector<char> buffer; try { buffer.resize(bytes); } catch (std::exception &) { throw std::runtime_error( "Could not allocate enough memory to run when trying to allocate " + boost::lexical_cast<std::string>(bytes) + " bytes."); } // Create a group for these new workspaces, if the group already exists, add // to it. std::string groupName = outWSName; size_t fileNumberInGroup = 0; WorkspaceGroup_sptr wsGroup; if (!AnalysisDataService::Instance().doesExist(groupName)) { wsGroup = WorkspaceGroup_sptr(new WorkspaceGroup()); wsGroup->setTitle(groupName); } else { // Get the name of the latest file in group to start numbering from if (AnalysisDataService::Instance().doesExist(groupName)) wsGroup = AnalysisDataService::Instance().retrieveWS<WorkspaceGroup>(groupName); std::string latestName = wsGroup->getNames().back(); // Set next file number fileNumberInGroup = fetchNumber(latestName) + 1; } size_t totalWS = headers.size(); // Create a progress reporting object API::Progress progress(this, 0, 1, totalWS + 1); progress.report(0, "Loading file(s) into workspace(s)"); // Create first workspace (with instrument definition). This is also used as // a template for creating others Workspace2D_sptr latestWS; latestWS = makeWorkspace(headers[0], fileNumberInGroup, buffer, imageY, imageE, latestWS, loadAsRectImg, binSize, noiseThresh); progress.report(1, "First file loaded."); std::vector<Workspace2D_sptr> wsOrdered(totalWS); wsOrdered[0] = latestWS; if (isInstrOtherThanIMAT(headers[0])) { // For now we assume IMAT except when specific headers are found by // isInstrOtherThanIMAT() // // TODO: do this conditional on INSTR='IMAT' when we have proper IMAT .fits // files try { IAlgorithm_sptr loadInst = createChildAlgorithm("LoadInstrument"); std::string directoryName = Kernel::ConfigService::Instance().getInstrumentDirectory(); directoryName = directoryName + "/IMAT_Definition.xml"; loadInst->setPropertyValue("Filename", directoryName); loadInst->setProperty<MatrixWorkspace_sptr>( "Workspace", boost::dynamic_pointer_cast<MatrixWorkspace>(latestWS)); loadInst->execute(); } catch (std::exception &ex) { g_log.information("Cannot load the instrument definition. " + std::string(ex.what())); } } // don't feel tempted to parallelize this loop as it is - it uses the same // imageY and imageE buffers for all the workspaces for (int64_t i = 1; i < static_cast<int64_t>(totalWS); ++i) { latestWS = makeWorkspace(headers[i], fileNumberInGroup, buffer, imageY, imageE, latestWS, loadAsRectImg, binSize, noiseThresh); wsOrdered[i] = latestWS; progress.report("Loaded file " + boost::lexical_cast<std::string>(i + 1) + " of " + boost::lexical_cast<std::string>(totalWS)); } // Add to group - done here to maintain sequence for (size_t i = 0; i < wsOrdered.size(); ++i) { wsGroup->addWorkspace(wsOrdered[i]); } setProperty("OutputWorkspace", wsGroup); } /** * Read a single files header and populate an object with the information. * * @param headerInfo A FITSInfo file object to parse header * information into. This object must have its field filePath set to * the input file * * @throws various std::runtime_error etc. on read failure */ void LoadFITS::parseHeader(FITSInfo &headerInfo) { headerInfo.headerSizeMultiplier = 0; std::ifstream istr(headerInfo.filePath.c_str(), std::ios::binary); istr.seekg(0, istr.end); if (!(istr.tellg() > 0)) { throw std::runtime_error( "Found a file that is readable but empty (0 bytes size): " + headerInfo.filePath); } istr.seekg(0, istr.beg); Poco::BinaryReader reader(istr); // Iterate 80 bytes at a time until header is parsed | 2880 bytes is the // fixed header length of FITS // 2880/80 = 36 iterations required bool endFound = false; while (!endFound) { headerInfo.headerSizeMultiplier++; for (int i = 0; i < 36; ++i) { // Keep vect of each header item, including comments, and also keep a // map of individual keys. std::string part; reader.readRaw(80, part); headerInfo.headerItems.push_back(part); // Add key/values - these are separated by the = symbol. // If it doesn't have an = it's a comment to ignore. All keys should be // unique auto eqPos = part.find('='); if (eqPos > 0) { std::string key = part.substr(0, eqPos); std::string value = part.substr(eqPos + 1); // Comments are added after the value separated by a / symbol. Remove. auto slashPos = value.find('/'); if (slashPos > 0) value = value.substr(0, slashPos); boost::trim(key); boost::trim(value); if (key == "END") endFound = true; if (key != "") headerInfo.headerKeys[key] = value; } } } istr.close(); } /** * Creates and initialises a workspace with instrument definition and fills it * with data * * @param fileInfo information for the current file * * @param newFileNumber sequence number for the new file when added * into ws group * * @param buffer pre-allocated buffer to contain data values * @param imageY Object to set the Y data values in * @param imageE Object to set the E data values in * * @param parent A workspace which can be used to copy initialisation * information from (size/instrument def etc) * * @param loadAsRectImg if true, the new workspace will have one * spectrum per row and one bin per column, instead of the (default) * as many spectra as pixels. * * @param binSize size to rebin (1 == no re-bin == default) * * @param noiseThresh threshold for noise filtering * * @returns A newly created Workspace2D, as a shared pointer */ Workspace2D_sptr LoadFITS::makeWorkspace(const FITSInfo &fileInfo, size_t &newFileNumber, std::vector<char> &buffer, MantidImage &imageY, MantidImage &imageE, const Workspace2D_sptr parent, bool loadAsRectImg, int binSize, double noiseThresh) { // Create workspace (taking into account already here if rebinning is // going to happen) Workspace2D_sptr ws; if (!parent) { if (!loadAsRectImg) { size_t finalPixelCount = m_pixelCount / binSize * binSize; ws = boost::dynamic_pointer_cast<Workspace2D>( WorkspaceFactory::Instance().create("Workspace2D", finalPixelCount, 2, 1)); } else { ws = boost::dynamic_pointer_cast<Workspace2D>( WorkspaceFactory::Instance().create( "Workspace2D", fileInfo.axisPixelLengths[1] / binSize, // one bin per column fileInfo.axisPixelLengths[0] / binSize + 1, // one spectrum per row fileInfo.axisPixelLengths[0] / binSize)); } } else { ws = boost::dynamic_pointer_cast<Workspace2D>( WorkspaceFactory::Instance().create(parent)); } // this pixel scale property is used to set the workspace X values double cm_1 = getProperty("Scale"); // amount of width units (e.g. cm) per pixel double cmpp = 1; // cm per pixel == bin width if (0.0 != cm_1) cmpp /= cm_1; cmpp *= static_cast<double>(binSize); if (loadAsRectImg && 1 == binSize) { // set data directly into workspace readDataToWorkspace(fileInfo, cmpp, ws, buffer); } else { readDataToImgs(fileInfo, imageY, imageE, buffer); doFilterNoise(noiseThresh, imageY, imageE); // Note this can change the sizes of the images and the number of pixels if (1 == binSize) { ws->setImageYAndE(imageY, imageE, 0, loadAsRectImg, cmpp, false /* no parallel load */); } else { MantidImage rebinnedY(imageY.size() / binSize, std::vector<double>(imageY[0].size() / binSize)); MantidImage rebinnedE(imageE.size() / binSize, std::vector<double>(imageE[0].size() / binSize)); doRebin(binSize, imageY, imageE, rebinnedY, rebinnedE); ws->setImageYAndE(rebinnedY, rebinnedE, 0, loadAsRectImg, cmpp, false /* no parallel load */); } } try { ws->setTitle(Poco::Path(fileInfo.filePath).getFileName()); } catch (std::runtime_error &) { ws->setTitle(padZeros(newFileNumber, g_DIGIT_SIZE_APPEND)); } ++newFileNumber; addAxesInfoAndLogs(ws, loadAsRectImg, fileInfo, binSize, cmpp); return ws; } /** * Add information to the workspace being loaded: labels, units, logs related to * the image size, etc. * * @param ws workspace to manipulate * * @param loadAsRectImg if true, the workspace has one spectrum per * row and one bin per column * * @param fileInfo information for the current file * * @param binSize size to rebin (1 == no re-bin == default) * * @param cmpp centimeters per pixel (already taking into account * possible rebinning) */ void LoadFITS::addAxesInfoAndLogs(Workspace2D_sptr ws, bool loadAsRectImg, const FITSInfo &fileInfo, int binSize, double cmpp) { // add axes size_t width = fileInfo.axisPixelLengths[0] / binSize; size_t height = fileInfo.axisPixelLengths[1] / binSize; if (loadAsRectImg) { // width/X axis auto axw = new Mantid::API::NumericAxis(width + 1); axw->title() = "width"; for (size_t i = 0; i < width + 1; i++) { axw->setValue(i, static_cast<double>(i) * cmpp); } ws->replaceAxis(0, axw); // "cm" width label unit boost::shared_ptr<Kernel::Units::Label> unitLbl = boost::dynamic_pointer_cast<Kernel::Units::Label>( UnitFactory::Instance().create("Label")); unitLbl->setLabel("width", "cm"); ws->getAxis(0)->unit() = unitLbl; // height/Y axis auto axh = new Mantid::API::NumericAxis(height); axh->title() = "height"; for (size_t i = 0; i < height; i++) { axh->setValue(i, static_cast<double>(i) * cmpp); } ws->replaceAxis(1, axh); // "cm" height label unit unitLbl = boost::dynamic_pointer_cast<Kernel::Units::Label>( UnitFactory::Instance().create("Label")); unitLbl->setLabel("height", "cm"); ws->getAxis(1)->unit() = unitLbl; ws->isDistribution(true); } else { // TODO: what to do when loading 1pixel - 1 spectrum? } ws->setYUnitLabel("brightness"); // Add all header info to log. for (auto it = fileInfo.headerKeys.begin(); it != fileInfo.headerKeys.end(); ++it) { ws->mutableRun().removeLogData(it->first, true); ws->mutableRun().addLogData( new PropertyWithValue<std::string>(it->first, it->second)); } // Add rotational data to log. Clear first from copied WS auto it = fileInfo.headerKeys.find(m_sampleRotation); ws->mutableRun().removeLogData("Rotation", true); if (fileInfo.headerKeys.end() != it) { double rot = boost::lexical_cast<double>(it->second); if (rot >= 0) { ws->mutableRun().addLogData( new PropertyWithValue<double>("Rotation", rot)); } } // Add axis information to log. Clear first from copied WS ws->mutableRun().removeLogData("Axis1", true); ws->mutableRun().addLogData(new PropertyWithValue<int>( "Axis1", static_cast<int>(fileInfo.axisPixelLengths[0]))); ws->mutableRun().removeLogData("Axis2", true); ws->mutableRun().addLogData(new PropertyWithValue<int>( "Axis2", static_cast<int>(fileInfo.axisPixelLengths[1]))); // Add image key data to log. Clear first from copied WS ws->mutableRun().removeLogData("ImageKey", true); ws->mutableRun().addLogData( new PropertyWithValue<std::string>("ImageKey", fileInfo.imageKey)); } /** * Reads the data (FITS matrix) from a single FITS file into a * workspace (directly into the spectra, using one spectrum per image * row). * * @param fileInfo information on the FITS file to load, including its path * @param cmpp centimeters per pixel, to scale/normalize values * @param ws workspace with the required dimensions * @param buffer pre-allocated buffer to read from file * * @throws std::runtime_error if there are file input issues */ void LoadFITS::readDataToWorkspace(const FITSInfo &fileInfo, double cmpp, Workspace2D_sptr ws, std::vector<char> &buffer) { size_t bytespp = (fileInfo.bitsPerPixel / 8); size_t len = m_pixelCount * bytespp; readInBuffer(fileInfo, buffer, len); // create pointer of correct data type to void pointer of the buffer: uint8_t *buffer8 = reinterpret_cast<uint8_t *>(&buffer[0]); PARALLEL_FOR_NO_WSP_CHECK() for (int i = 0; i < static_cast<int>(fileInfo.axisPixelLengths[1]); ++i) { // rows Mantid::API::ISpectrum *specRow = ws->getSpectrum(i); double xval = static_cast<double>(i) * cmpp; std::fill(specRow->dataX().begin(), specRow->dataX().end(), xval); for (size_t j = 0; j < fileInfo.axisPixelLengths[0]; ++j) { // columns size_t start = ((i * (bytespp)) * fileInfo.axisPixelLengths[1]) + (j * (bytespp)); char tmpbuf; char *tmp = &tmpbuf; // Reverse byte order of current value std::reverse_copy(buffer8 + start, buffer8 + start + bytespp, tmp); double val = 0; if (fileInfo.bitsPerPixel == 8) val = static_cast<double>(*reinterpret_cast<uint8_t *>(tmp)); else if (fileInfo.bitsPerPixel == 16) val = static_cast<double>(*reinterpret_cast<uint16_t *>(tmp)); else if (fileInfo.bitsPerPixel == 32 && !fileInfo.isFloat) val = static_cast<double>(*reinterpret_cast<uint32_t *>(tmp)); else if (fileInfo.bitsPerPixel == 64 && !fileInfo.isFloat) val = static_cast<double>(*reinterpret_cast<uint64_t *>(tmp)); // cppcheck doesn't realise that these are safe casts else if (fileInfo.bitsPerPixel == 32 && fileInfo.isFloat) { // cppcheck-suppress invalidPointerCast val = static_cast<double>(*reinterpret_cast<float *>(tmp)); } else if (fileInfo.bitsPerPixel == 64 && fileInfo.isFloat) { // cppcheck-suppress invalidPointerCast val = *reinterpret_cast<double *>(tmp); } val = fileInfo.scale * val - fileInfo.offset; specRow->dataY()[j] = val; specRow->dataE()[j] = sqrt(val); } } } /** * Reads the data (FITS matrix) from a single FITS file into image * objects (Y and E). E is filled with the sqrt() of Y. * * @param fileInfo information on the FITS file to load, including its path * @param imageY Object to set the Y data values in * @param imageE Object to set the E data values in * @param buffer pre-allocated buffer to contain data values * * @throws std::runtime_error if there are file input issues */ void LoadFITS::readDataToImgs(const FITSInfo &fileInfo, MantidImage &imageY, MantidImage &imageE, std::vector<char> &buffer) { size_t bytespp = (fileInfo.bitsPerPixel / 8); size_t len = m_pixelCount * bytespp; readInBuffer(fileInfo, buffer, len); // create pointer of correct data type to void pointer of the buffer: uint8_t *buffer8 = reinterpret_cast<uint8_t *>(&buffer[0]); std::vector<char> buf(bytespp); char *tmp = &buf.front(); size_t start = 0; for (size_t i = 0; i < fileInfo.axisPixelLengths[1]; ++i) { // width for (size_t j = 0; j < fileInfo.axisPixelLengths[0]; ++j) { // height // If you wanted to PARALLEL_...ize these loops (which doesn't // seem to provide any speed up when loading images one at a // time, you cannot use the start+=bytespp at the end of this // loop. You'd need something like this: // // size_t start = // ((i * (bytespp)) * fileInfo.axisPixelLengths[1]) + // (j * (bytespp)); // Reverse byte order of current value std::reverse_copy(buffer8 + start, buffer8 + start + bytespp, tmp); double val = 0; if (fileInfo.bitsPerPixel == 8) val = static_cast<double>(*reinterpret_cast<uint8_t *>(tmp)); if (fileInfo.bitsPerPixel == 16) val = static_cast<double>(*reinterpret_cast<uint16_t *>(tmp)); if (fileInfo.bitsPerPixel == 32 && !fileInfo.isFloat) val = static_cast<double>(*reinterpret_cast<uint32_t *>(tmp)); if (fileInfo.bitsPerPixel == 64 && !fileInfo.isFloat) val = static_cast<double>(*reinterpret_cast<uint64_t *>(tmp)); // cppcheck doesn't realise that these are safe casts if (fileInfo.bitsPerPixel == 32 && fileInfo.isFloat) { // cppcheck-suppress invalidPointerCast val = static_cast<double>(*reinterpret_cast<float *>(tmp)); } if (fileInfo.bitsPerPixel == 64 && fileInfo.isFloat) { // cppcheck-suppress invalidPointerCast val = *reinterpret_cast<double *>(tmp); } val = fileInfo.scale * val - fileInfo.offset; imageY[i][j] = val; imageE[i][j] = sqrt(val); start += bytespp; } } } /** * Reads the data (FITS matrix) from a single FITS file into a * buffer. This simply reads the raw block of data, without doing any * re-scaling or adjustment. * * @param fileInfo information on the FITS file to load, including its path * @param buffer pre-allocated buffer where to read data * @param len amount of chars/bytes/octets to read * * @throws std::runtime_error if there are file input issues */ void LoadFITS::readInBuffer(const FITSInfo &fileInfo, std::vector<char> &buffer, size_t len) { std::string filename = fileInfo.filePath; Poco::FileStream file(filename, std::ios::in); file.seekg(g_BASE_HEADER_SIZE * fileInfo.headerSizeMultiplier); file.read(&buffer[0], len); if (!file) { throw std::runtime_error( "Error while reading file: " + filename + ". Tried to read " + boost::lexical_cast<std::string>(len) + " bytes but got " + boost::lexical_cast<std::string>(file.gcount()) + " bytes. The file and/or its headers may be wrong."); } // all is loaded file.close(); } /** * Apply a simple noise filter by averaging threshold-filtered * neighbor pixels (with 4-neighbohood / 4-connectivity). The * filtering is done in place for both imageY and imageE. * * @param thresh Threshold to apply on pixels * @param imageY raw data (Y values) * @param imageE raw data (E/error values) */ void LoadFITS::doFilterNoise(double thresh, MantidImage &imageY, MantidImage &imageE) { if (thresh <= 0.0) return; MantidImage goodY = imageY; MantidImage goodE = imageE; // TODO: this is not very smart about the edge pixels (leftmost and // rightmost columns, topmost and bottom rows) for (size_t j = 1; j < (imageY.size() - 1); ++j) { for (size_t i = 1; i < (imageY[0].size() - 1); ++i) { if (((imageY[j][i] - imageY[j][i - 1]) > thresh) && ((imageY[j][i] - imageY[j][i + 1]) > thresh) && ((imageY[j][i] - imageY[j - 1][i]) > thresh) && ((imageY[j][i] - imageY[j + 1][i]) > thresh)) goodY[j][i] = 0; else goodY[j][i] = 1; if (((imageE[j][i] - imageE[j][i - 1]) > thresh) && ((imageE[j][i] - imageE[j][i + 1]) > thresh) && ((imageE[j][i] - imageE[j - 1][i]) > thresh) && ((imageE[j][i] - imageE[j + 1][i]) > thresh)) goodE[j][i] = 0; else goodE[j][i] = 1; } } for (size_t j = 1; j < (imageY.size() - 1); ++j) { for (size_t i = 1; i < (imageY[0].size() - 1); ++i) { if (!goodY[j][i]) { if (goodY[j - 1][i] || goodY[j + 1][i] || goodY[j][i - 1] || goodY[j][i + 1]) { imageY[j][i] = goodY[j - 1][i] * imageY[j - 1][i] + goodY[j + 1][i] * imageY[j + 1][i] + goodY[j][i - 1] * imageY[j][i - 1] + goodY[j][i + 1] * imageY[j][i + 1]; } } if (!goodE[j][i]) { if (goodE[j - 1][i] || goodE[j + 1][i] || goodE[j][i - 1] || goodE[j][i + 1]) { imageE[j][i] = goodE[j - 1][i] * imageE[j - 1][i] + goodE[j + 1][i] * imageE[j + 1][i] + goodE[j][i - 1] * imageE[j][i - 1] + goodE[j][i + 1] * imageE[j][i + 1]; } } } } } /** * Group pixels in blocks of rebin X rebin. * * @param rebin bin size (n to make blocks of n*n pixels) * @param imageY raw data (Y values) * @param imageE raw data (E/error values) * @param rebinnedY raw data after rebin (Y values) * @param rebinnedE raw data after rebin (E/error values) */ void LoadFITS::doRebin(size_t rebin, MantidImage &imageY, MantidImage &imageE, MantidImage &rebinnedY, MantidImage &rebinnedE) { if (1 >= rebin) return; for (size_t j = 0; j < (rebinnedY.size() - rebin + 1); ++j) { for (size_t i = 0; i < (rebinnedY[0].size() - rebin + 1); ++i) { double accumY = 0.0; double accumE = 0.0; size_t origJ = j * rebin; size_t origI = i * rebin; for (size_t k = 0; k < rebin; ++k) { for (size_t l = 0; l < rebin; ++l) { accumY += imageY[origJ + k][origI + l]; accumE += imageE[origJ + k][origI + l]; } } rebinnedY[j][i] = accumY; rebinnedE[j][i] = accumE; } } } /** * Looks for headers used by specific instruments/cameras, or finds if * the instrument does not appear to be IMAT, which is the only one * for which we have a camera-instrument definition and because of * that is the only one loaded for the moment. * * @param hdr FITS header information * * @return whether this file seems to come from 'another' camera such * as Starlight Xpress, etc. */ bool LoadFITS::isInstrOtherThanIMAT(FITSInfo &hdr) { bool res = false; // Images taken with Starlight camera contain this header entry: // INSTRUME='Starlight Xpress CCD' auto it = hdr.headerKeys.find("INSTRUME"); if (hdr.headerKeys.end() != it && boost::contains(it->second, "Starlight")) { // For now, do nothing, just tell // Cameras used for HiFi and EMU are in principle only used // occasionally for calibration g_log.information() << "Found this in the file headers: " << it->first << " = " << it->second << ". This file seems to come from a Starlight camera, " "as used for calibration of the instruments HiFi and EMU (and" "possibly others). Note: not " "loading instrument definition." << std::endl; } return res; } /** * Sets several keyword names with default (and standard) values. You * don't want to change these unless you want to break compatibility * with the FITS standard. */ void LoadFITS::setupDefaultKeywordNames() { // Inits all the absolutely necessary keywords // standard headers (If SIMPLE=T) m_headerScaleKey = "BSCALE"; m_headerOffsetKey = "BZERO"; m_headerBitDepthKey = "BITPIX"; m_headerImageKeyKey = "IMAGE_TYPE"; // This is a "HIERARCH Image/Type= " m_headerRotationKey = "ROTATION"; m_headerNAxisNameKey = "NAXIS"; m_headerAxisNameKeys.push_back("NAXIS1"); m_headerAxisNameKeys.push_back("NAXIS2"); m_mapFile = ""; // extensions m_sampleRotation = "HIERARCH Sample/Tomo_Angle"; m_imageType = "HIERARCH Image/Type"; } /** * Maps the header keys to specified values */ void LoadFITS::mapHeaderKeys() { if ("" == getPropertyValue(g_HEADER_MAP_NAME)) return; // If a map file is selected, use that. std::string name = getPropertyValue(g_HEADER_MAP_NAME); std::ifstream fStream(name.c_str()); try { // Ensure valid file if (fStream.good()) { // Get lines, split words, verify and add to map. std::string line; std::vector<std::string> lineSplit; while (getline(fStream, line)) { boost::split(lineSplit, line, boost::is_any_of("=")); if (lineSplit[0] == g_ROTATION_NAME && lineSplit[1] != "") m_headerRotationKey = lineSplit[1]; if (lineSplit[0] == g_BIT_DEPTH_NAME && lineSplit[1] != "") m_headerBitDepthKey = lineSplit[1]; if (lineSplit[0] == g_AXIS_NAMES_NAME && lineSplit[1] != "") { m_headerAxisNameKeys.clear(); boost::split(m_headerAxisNameKeys, lineSplit[1], boost::is_any_of(",")); } if (lineSplit[0] == g_IMAGE_KEY_NAME && lineSplit[1] != "") { m_headerImageKeyKey = lineSplit[1]; } } fStream.close(); } else { throw std::runtime_error( "Error while trying to read header keys mapping file: " + name); } } catch (...) { g_log.error("Cannot load specified map file, using property values " "and/or defaults."); } } /** * Returns the trailing number from a string minus leading 0's (so 25 from * workspace_00025). * * @param name string with a numerical suffix * * @returns A numerical representation of the string minus leading characters * and leading 0's */ size_t LoadFITS::fetchNumber(const std::string &name) { std::string tmpStr = ""; for (auto it = name.end() - 1; isdigit(*it); --it) { tmpStr.insert(0, 1, *it); } while (tmpStr.length() > 0 && tmpStr[0] == '0') { tmpStr.erase(tmpStr.begin()); } return (tmpStr.length() > 0) ? boost::lexical_cast<size_t>(tmpStr) : 0; } /** * Adds 0's to the front of a number to create a string of size * totalDigitCount including number * * @param number input number to add padding to * * @param totalDigitCount width of the resulting string with 0s followed by * number * * @return A string with the 0-padded number */ std::string LoadFITS::padZeros(const size_t number, const size_t totalDigitCount) { std::ostringstream ss; ss << std::setw(static_cast<int>(totalDigitCount)) << std::setfill('0') << static_cast<int>(number); return ss.str(); } } // namespace DataHandling } // namespace Mantid