/// <summary> /// Open specified stream. /// </summary> /// /// <param name="stream">Stream to open.</param> /// /// <returns>Returns number of images found in the specified stream.</returns> /// /// <exception cref="FormatException">Not a FITS image format.</exception> /// <exception cref="NotSupportedException">Format of the FITS image is not supported.</exception> /// <exception cref="ArgumentException">The stream contains invalid (broken) FITS image.</exception> /// public int Open(Stream stream) { // close previous decoding Close(); this.imageInfo = ReadHeader(stream); this.stream = stream; this.dataPosition = stream.Seek(0, SeekOrigin.Current); return(imageInfo.TotalFrames); }
/// <summary> /// Decode first frame of FITS image. /// </summary> /// /// <param name="stream">Source stream, which contains encoded image.</param> /// /// <returns>Returns decoded image frame.</returns> /// /// <exception cref="FormatException">Not a FITS image format.</exception> /// <exception cref="NotSupportedException">Format of the FITS image is not supported.</exception> /// <exception cref="ArgumentException">The stream contains invalid (broken) FITS image.</exception> /// public Bitmap DecodeSingleFrame(Stream stream) { FITSImageInfo imageInfo = ReadHeader(stream); // check if there any image frame if (imageInfo.TotalFrames == 0) { throw new ArgumentException("The FITS stream does not contain any image in main section."); } // read and return first frame return(ReadImageFrame(stream, imageInfo)); }
/// <summary> /// Creates a new object that is a copy of the current instance. /// </summary> /// /// <returns>A new object that is a copy of this instance.</returns> /// public override object Clone() { FITSImageInfo clone = new FITSImageInfo(width, height, bitsPerPixel, frameIndex, totalFrames); clone.originalBitsPerPixl = originalBitsPerPixl; clone.minDataValue = minDataValue; clone.maxDataValue = maxDataValue; clone.telescope = telescope; clone.acquiredObject = acquiredObject; clone.observer = observer; clone.instrument = instrument; return(clone); }
/// <summary> /// Decode specified frame. /// </summary> /// /// <param name="frameIndex">Image frame to decode.</param> /// <param name="imageInfo">Receives information about decoded frame.</param> /// /// <returns>Returns decoded frame.</returns> /// /// <exception cref="NullReferenceException">No image stream was opened previously.</exception> /// <exception cref="ArgumentOutOfRangeException">Stream does not contain frame with specified index.</exception> /// <exception cref="ArgumentException">The stream contains invalid (broken) FITS image.</exception> /// public Bitmap DecodeFrame(int frameIndex, out ImageInfo imageInfo) { // check requested frame index if (frameIndex >= this.imageInfo.TotalFrames) { throw new ArgumentOutOfRangeException("Currently opened stream does not contain frame with specified index."); } // seek to the required frame stream.Seek(dataPosition + frameIndex * this.imageInfo.Width * this.imageInfo.Height * Math.Abs(this.imageInfo.OriginalBitsPerPixl) / 8, SeekOrigin.Begin); // read required frame Bitmap image = ReadImageFrame(stream, this.imageInfo); // provide also frame information imageInfo = (FITSImageInfo)this.imageInfo.Clone(); imageInfo.FrameIndex = frameIndex; return(image); }
// Read image frame from the specified stream (current stream's position is used) private static unsafe Bitmap ReadImageFrame(Stream stream, FITSImageInfo imageInfo) { int width = imageInfo.Width; int height = imageInfo.Height; // create new bitmap Bitmap image = (imageInfo.BitsPerPixel == 8) ? Tools.CreateGrayscaleImage(width, height) : new Bitmap(width, height, PixelFormat.Format16bppGrayScale); // lock it BitmapData imageData = image.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, image.PixelFormat); int originalBitsPerPixel = imageInfo.OriginalBitsPerPixl; int stride = imageData.Stride; byte *ptr = (byte *)imageData.Scan0.ToPointer(); double min = imageInfo.MinDataValue; double max = imageInfo.MaxDataValue; // check number of bits per pixel and load image appropriately if (imageInfo.BitsPerPixel == 16) { // 16 bpp grayscale image double coef = 65535.0 / (max - min); // prepare a buffer for one line int lineSize = width * Math.Abs(originalBitsPerPixel) / 8; byte[] line = new byte[lineSize]; byte[] temp = new byte[8]; // load all rows for (int y = height - 1; y >= 0; y--) { // load next line if (Tools.ReadStream(stream, line, 0, lineSize) < lineSize) { throw new ArgumentException("The stream does not contain valid FITS image."); } // fill next image row ushort *row = (ushort *)(ptr + stride * y); for (int x = 0, i = 0; x < width; x++, row++) { double value = 0; switch (originalBitsPerPixel) { case 16: // 16 bit signed integer { short tempValue = 0; unchecked { tempValue = (short)((line[i++] << 8) + line[i++]); } value = tempValue; break; } case 32: // 32 bit signed integer { temp[3] = line[i++]; temp[2] = line[i++]; temp[1] = line[i++]; temp[0] = line[i++]; value = BitConverter.ToInt32(temp, 0); break; } case -32: // 32 bit float { temp[3] = line[i++]; temp[2] = line[i++]; temp[1] = line[i++]; temp[0] = line[i++]; value = BitConverter.ToSingle(temp, 0); break; } case -64: // 64 bit double { temp[7] = line[i++]; temp[6] = line[i++]; temp[5] = line[i++]; temp[4] = line[i++]; temp[3] = line[i++]; temp[2] = line[i++]; temp[1] = line[i++]; temp[0] = line[i++]; value = BitConverter.ToDouble(temp, 0); break; } } *row = (ushort)((value - min) * coef); } } } else { // 8 bpp grayscale image double coef = 255.0 / (max - min); // prepare a buffer for one line byte[] line = new byte[width]; // load all rows for (int y = height - 1; y >= 0; y--) { // load next line if (Tools.ReadStream(stream, line, 0, width) < width) { throw new ArgumentException("The stream does not contain valid FITS image."); } // fill next image row byte *row = ptr + stride * y; for (int x = 0; x < width; x++, row++) { *row = (byte)(((double)line[x] - min) * coef); } } } // unlock image and return it image.UnlockBits(imageData); return(image); }
// Read and process FITS header. After the header is read stream pointer will // point to data or extension. private static FITSImageInfo ReadHeader(Stream stream) { byte[] headerRecord = new byte[80]; int recordsRead = 1; bool endKeyWasFound = false; FITSImageInfo imageInfo = new FITSImageInfo(); // read first record and check for correct image if ( (Tools.ReadStream(stream, headerRecord, 0, 80) < 80) || (Encoding.UTF8.GetString(headerRecord, 0, 8) != "SIMPLE ")) { throw new FormatException("The stream does not contatin FITS image."); } else { // check if the image has standard FITS format if (Encoding.UTF8.GetString(headerRecord, 10, 70).Split('/')[0].Trim() != "T") { throw new NotSupportedException("The stream contains not standard FITS data file."); } } // read header and locate data block while (true) { // read next record if (Tools.ReadStream(stream, headerRecord, 0, 80) < 80) { throw new ArgumentException("The stream does not contain valid FITS image."); } recordsRead++; // get keyword string keyword = Encoding.UTF8.GetString(headerRecord, 0, 8); // skip commenct and history if ((keyword == "COMMENT ") || (keyword == "HISTORY ")) { continue; } // check if it is end of header keyword if (keyword == "END ") { endKeyWasFound = true; } if (endKeyWasFound) { if (recordsRead % 36 == 0) { // found data or extension header // make a small check of some header values if ((imageInfo.BitsPerPixel == 0) || (imageInfo.Width == 0) || (imageInfo.Height == 0)) { imageInfo.TotalFrames = 0; } // let's return here and let other routines process data break; } } else { // get string representation of value/comments string strValue = Encoding.UTF8.GetString(headerRecord, 10, 70); // check important keywords if (keyword == "BITPIX ") { int value = ExtractIntegerValue(strValue); if ((value != 8) && (value != 16) && (value != 32) && (value != -32) && (value != -64)) { throw new NotSupportedException("Data format (" + value + ") is not supported."); } // bits per pixel imageInfo.BitsPerPixel = (value == 8) ? 8 : 16; imageInfo.OriginalBitsPerPixl = value; } else if (Encoding.UTF8.GetString(headerRecord, 0, 5) == "NAXIS") { // information about data axis int value = ExtractIntegerValue(strValue); // check axis switch (headerRecord[5]) { // number of axis case (byte)' ': switch (value) { case 1: default: throw new NotSupportedException("FITS files with data dimension of " + value + " are not supported."); case 0: // the stream does not have an image, do nothing break; case 2: // the stream has 1 2D image imageInfo.TotalFrames = 1; break; case 3: // the stream has 3D image - series of 2D images break; } break; // length of 1st axis case (byte)'1': imageInfo.Width = value; break; // length of 2nd axis case (byte)'2': imageInfo.Height = value; break; // length of 3rd axis case (byte)'3': imageInfo.TotalFrames = value; break; } } else if (keyword == "TELESCOP") { imageInfo.Telescope = ExtractStringValue(strValue); } else if (keyword == "OBJECT ") { imageInfo.Object = ExtractStringValue(strValue); } else if (keyword == "OBSERVER") { imageInfo.Observer = ExtractStringValue(strValue); } else if (keyword == "INSTRUME") { imageInfo.Instrument = ExtractStringValue(strValue); } // --- for debugging --- /* if ( keyword[0] != ' ' ) * { * System.Diagnostics.Debug.Write( keyword ); * if ( headerRecord[8] == '=' ) * { * System.Diagnostics.Debug.WriteLine( " = " + strValue ); * } * else * { * System.Diagnostics.Debug.WriteLine( "" ); * } * } */ // --- --- } } // scan all available data to find minimum and maximum values, // which will be used for scaling. the scan is done here (not while // reading actual frame) because FITS file may have set of images // packed into data cube, so entire scan of all the data is required. // if is stream is seekable if (!stream.CanSeek) { throw new ArgumentException("The stream must be seekable."); } // remember current position long dataPos = stream.Seek(0, SeekOrigin.Current); // data size int lineLength = imageInfo.Width * (Math.Abs(imageInfo.OriginalBitsPerPixl) / 8); int totalLines = imageInfo.Height * imageInfo.TotalFrames; int originalBitsPerPixel = imageInfo.OriginalBitsPerPixl; byte[] buffer = new byte[lineLength]; byte[] temp = new byte[8]; // min and max values double min = double.MaxValue; double max = double.MinValue; for (int i = 0; i < totalLines; i++) { // read next line if (Tools.ReadStream(stream, buffer, 0, lineLength) < lineLength) { throw new ArgumentException("The stream does not contain valid FITS image."); } // scan the line for (int j = 0; j < lineLength;) { double value = 0; // read values accordint to their format switch (originalBitsPerPixel) { case 8: // 8 bit unsigned integer value = buffer[j++]; break; case 16: // 16 bit signed integer { short tempValue = 0; unchecked { tempValue = (short)((buffer[j++] << 8) | buffer[j++]); } value = tempValue; break; } case 32: // 32 bit signed integer { temp[3] = buffer[j++]; temp[2] = buffer[j++]; temp[1] = buffer[j++]; temp[0] = buffer[j++]; value = BitConverter.ToInt32(temp, 0); break; } case -32: // 32 bit float { temp[3] = buffer[j++]; temp[2] = buffer[j++]; temp[1] = buffer[j++]; temp[0] = buffer[j++]; value = BitConverter.ToSingle(temp, 0); break; } case -64: // 64 bit double { temp[7] = buffer[j++]; temp[6] = buffer[j++]; temp[5] = buffer[j++]; temp[4] = buffer[j++]; temp[3] = buffer[j++]; temp[2] = buffer[j++]; temp[1] = buffer[j++]; temp[0] = buffer[j++]; value = BitConverter.ToDouble(temp, 0); break; } } if (value > max) { max = value; } if (value < min) { min = value; } } } imageInfo.MaxDataValue = max; imageInfo.MinDataValue = min; // restore stream position to the begining of data stream.Seek(dataPos, SeekOrigin.Begin); return(imageInfo); }
/// <summary> /// Close decoding of previously opened stream. /// </summary> /// /// <remarks><para>The method does not close stream itself, but just closes /// decoding cleaning all associated data with it.</para></remarks> /// public void Close() { stream = null; imageInfo = null; }
// Read image frame from the specified stream (current stream's position is used) private static unsafe Bitmap ReadImageFrame(Stream stream, FITSImageInfo imageInfo) { int width = imageInfo.Width; int height = imageInfo.Height; // create new bitmap Bitmap image = (imageInfo.BitsPerPixel == 8) ? Tools.CreateGrayscaleImage(width, height) : new Bitmap(width, height, PixelFormat.Format16bppGrayScale); // lock it BitmapData imageData = image.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, image.PixelFormat); int originalBitsPerPixel = imageInfo.OriginalBitsPerPixl; int stride = imageData.Stride; byte* ptr = (byte*)imageData.Scan0.ToPointer(); double min = imageInfo.MinDataValue; double max = imageInfo.MaxDataValue; // check number of bits per pixel and load image appropriately if (imageInfo.BitsPerPixel == 16) { // 16 bpp grayscale image double coef = 65535.0 / (max - min); // prepare a buffer for one line int lineSize = width * Math.Abs(originalBitsPerPixel) / 8; byte[] line = new byte[lineSize]; byte[] temp = new byte[8]; // load all rows for (int y = height - 1; y >= 0; y--) { // load next line if (Tools.ReadStream(stream, line, 0, lineSize) < lineSize) throw new ArgumentException("The stream does not contain valid FITS image."); // fill next image row ushort* row = (ushort*)(ptr + stride * y); for (int x = 0, i = 0; x < width; x++, row++) { double value = 0; switch (originalBitsPerPixel) { case 16: // 16 bit signed integer { short tempValue = 0; unchecked { tempValue = (short)((line[i++] << 8) + line[i++]); } value = tempValue; break; } case 32: // 32 bit signed integer { temp[3] = line[i++]; temp[2] = line[i++]; temp[1] = line[i++]; temp[0] = line[i++]; value = BitConverter.ToInt32(temp, 0); break; } case -32: // 32 bit float { temp[3] = line[i++]; temp[2] = line[i++]; temp[1] = line[i++]; temp[0] = line[i++]; value = BitConverter.ToSingle(temp, 0); break; } case -64: // 64 bit double { temp[7] = line[i++]; temp[6] = line[i++]; temp[5] = line[i++]; temp[4] = line[i++]; temp[3] = line[i++]; temp[2] = line[i++]; temp[1] = line[i++]; temp[0] = line[i++]; value = BitConverter.ToDouble(temp, 0); break; } } *row = (ushort)((value - min) * coef); } } } else { // 8 bpp grayscale image double coef = 255.0 / (max - min); // prepare a buffer for one line byte[] line = new byte[width]; // load all rows for (int y = height - 1; y >= 0; y--) { // load next line if (Tools.ReadStream(stream, line, 0, width) < width) throw new ArgumentException("The stream does not contain valid FITS image."); // fill next image row byte* row = ptr + stride * y; for (int x = 0; x < width; x++, row++) { *row = (byte)(((double)line[x] - min) * coef); } } } // unlock image and return it image.UnlockBits(imageData); return image; }
// Read and process FITS header. After the header is read stream pointer will // point to data or extension. private static FITSImageInfo ReadHeader(Stream stream) { byte[] headerRecord = new byte[80]; int recordsRead = 1; bool endKeyWasFound = false; FITSImageInfo imageInfo = new FITSImageInfo(); // read first record and check for correct image if ( (Tools.ReadStream(stream, headerRecord, 0, 80) < 80) || (Encoding.UTF8.GetString(headerRecord, 0, 8) != "SIMPLE ")) { throw new FormatException("The stream does not contatin FITS image."); } else { // check if the image has standard FITS format if (Encoding.UTF8.GetString(headerRecord, 10, 70).Split('/')[0].Trim() != "T") { throw new NotSupportedException("The stream contains not standard FITS data file."); } } // read header and locate data block while (true) { // read next record if (Tools.ReadStream(stream, headerRecord, 0, 80) < 80) { throw new ArgumentException("The stream does not contain valid FITS image."); } recordsRead++; // get keyword string keyword = Encoding.UTF8.GetString(headerRecord, 0, 8); // skip commenct and history if ((keyword == "COMMENT ") || (keyword == "HISTORY ")) continue; // check if it is end of header keyword if (keyword == "END ") endKeyWasFound = true; if (endKeyWasFound) { if (recordsRead % 36 == 0) { // found data or extension header // make a small check of some header values if ((imageInfo.BitsPerPixel == 0) || (imageInfo.Width == 0) || (imageInfo.Height == 0)) { imageInfo.TotalFrames = 0; } // let's return here and let other routines process data break; } } else { // get string representation of value/comments string strValue = Encoding.UTF8.GetString(headerRecord, 10, 70); // check important keywords if (keyword == "BITPIX ") { int value = ExtractIntegerValue(strValue); if ((value != 8) && (value != 16) && (value != 32) && (value != -32) && (value != -64)) { throw new NotSupportedException("Data format (" + value + ") is not supported."); } // bits per pixel imageInfo.BitsPerPixel = (value == 8) ? 8 : 16; imageInfo.OriginalBitsPerPixl = value; } else if (Encoding.UTF8.GetString(headerRecord, 0, 5) == "NAXIS") { // information about data axis int value = ExtractIntegerValue(strValue); // check axis switch (headerRecord[5]) { // number of axis case (byte)' ': switch (value) { case 1: default: throw new NotSupportedException("FITS files with data dimension of " + value + " are not supported."); case 0: // the stream does not have an image, do nothing break; case 2: // the stream has 1 2D image imageInfo.TotalFrames = 1; break; case 3: // the stream has 3D image - series of 2D images break; } break; // length of 1st axis case (byte)'1': imageInfo.Width = value; break; // length of 2nd axis case (byte)'2': imageInfo.Height = value; break; // length of 3rd axis case (byte)'3': imageInfo.TotalFrames = value; break; } } else if (keyword == "TELESCOP") { imageInfo.Telescope = ExtractStringValue(strValue); } else if (keyword == "OBJECT ") { imageInfo.Object = ExtractStringValue(strValue); } else if (keyword == "OBSERVER") { imageInfo.Observer = ExtractStringValue(strValue); } else if (keyword == "INSTRUME") { imageInfo.Instrument = ExtractStringValue(strValue); } // --- for debugging --- /* if ( keyword[0] != ' ' ) { System.Diagnostics.Debug.Write( keyword ); if ( headerRecord[8] == '=' ) { System.Diagnostics.Debug.WriteLine( " = " + strValue ); } else { System.Diagnostics.Debug.WriteLine( "" ); } } */ // --- --- } } // scan all available data to find minimum and maximum values, // which will be used for scaling. the scan is done here (not while // reading actual frame) because FITS file may have set of images // packed into data cube, so entire scan of all the data is required. // if is stream is seekable if (!stream.CanSeek) { throw new ArgumentException("The stream must be seekable."); } // remember current position long dataPos = stream.Seek(0, SeekOrigin.Current); // data size int lineLength = imageInfo.Width * (Math.Abs(imageInfo.OriginalBitsPerPixl) / 8); int totalLines = imageInfo.Height * imageInfo.TotalFrames; int originalBitsPerPixel = imageInfo.OriginalBitsPerPixl; byte[] buffer = new byte[lineLength]; byte[] temp = new byte[8]; // min and max values double min = double.MaxValue; double max = double.MinValue; for (int i = 0; i < totalLines; i++) { // read next line if (Tools.ReadStream(stream, buffer, 0, lineLength) < lineLength) throw new ArgumentException("The stream does not contain valid FITS image."); // scan the line for (int j = 0; j < lineLength; ) { double value = 0; // read values accordint to their format switch (originalBitsPerPixel) { case 8: // 8 bit unsigned integer value = buffer[j++]; break; case 16: // 16 bit signed integer { short tempValue = 0; unchecked { tempValue = (short)((buffer[j++] << 8) | buffer[j++]); } value = tempValue; break; } case 32: // 32 bit signed integer { temp[3] = buffer[j++]; temp[2] = buffer[j++]; temp[1] = buffer[j++]; temp[0] = buffer[j++]; value = BitConverter.ToInt32(temp, 0); break; } case -32: // 32 bit float { temp[3] = buffer[j++]; temp[2] = buffer[j++]; temp[1] = buffer[j++]; temp[0] = buffer[j++]; value = BitConverter.ToSingle(temp, 0); break; } case -64: // 64 bit double { temp[7] = buffer[j++]; temp[6] = buffer[j++]; temp[5] = buffer[j++]; temp[4] = buffer[j++]; temp[3] = buffer[j++]; temp[2] = buffer[j++]; temp[1] = buffer[j++]; temp[0] = buffer[j++]; value = BitConverter.ToDouble(temp, 0); break; } } if (value > max) max = value; if (value < min) min = value; } } imageInfo.MaxDataValue = max; imageInfo.MinDataValue = min; // restore stream position to the begining of data stream.Seek(dataPos, SeekOrigin.Begin); return imageInfo; }
/// <summary> /// Close decoding of previously opened stream. /// </summary> /// /// <remarks><para>The method does not close stream itself, but just closes /// decoding cleaning all associated data with it.</para></remarks> /// public void Close() { stream = null; imageInfo = null; }
/// <summary> /// Decode specified frame. /// </summary> /// /// <param name="frameIndex">Image frame to decode.</param> /// <param name="imageInfo">Receives information about decoded frame.</param> /// /// <returns>Returns decoded frame.</returns> /// /// <exception cref="NullReferenceException">No image stream was opened previously.</exception> /// <exception cref="ArgumentOutOfRangeException">Stream does not contain frame with specified index.</exception> /// <exception cref="ArgumentException">The stream contains invalid (broken) FITS image.</exception> /// public Bitmap DecodeFrame(int frameIndex, out ImageInfo imageInfo) { // check requested frame index if (frameIndex >= this.imageInfo.TotalFrames) { throw new ArgumentOutOfRangeException("Currently opened stream does not contain frame with specified index."); } // seek to the required frame stream.Seek(dataPosition + frameIndex * this.imageInfo.Width * this.imageInfo.Height * Math.Abs(this.imageInfo.OriginalBitsPerPixl) / 8, SeekOrigin.Begin); // read required frame Bitmap image = ReadImageFrame(stream, this.imageInfo); // provide also frame information imageInfo = (FITSImageInfo)this.imageInfo.Clone(); imageInfo.FrameIndex = frameIndex; return image; }
/// <summary> /// Open specified stream. /// </summary> /// /// <param name="stream">Stream to open.</param> /// /// <returns>Returns number of images found in the specified stream.</returns> /// /// <exception cref="FormatException">Not a FITS image format.</exception> /// <exception cref="NotSupportedException">Format of the FITS image is not supported.</exception> /// <exception cref="ArgumentException">The stream contains invalid (broken) FITS image.</exception> /// public int Open(Stream stream) { // close previous decoding Close(); this.imageInfo = ReadHeader(stream); this.stream = stream; this.dataPosition = stream.Seek(0, SeekOrigin.Current); return imageInfo.TotalFrames; }
/// <summary> /// Creates a new object that is a copy of the current instance. /// </summary> /// /// <returns>A new object that is a copy of this instance.</returns> /// public override object Clone() { FITSImageInfo clone = new FITSImageInfo(width, height, bitsPerPixel, frameIndex, totalFrames); clone.originalBitsPerPixl = originalBitsPerPixl; clone.minDataValue = minDataValue; clone.maxDataValue = maxDataValue; clone.telescope = telescope; clone.acquiredObject = acquiredObject; clone.observer = observer; clone.instrument = instrument; return clone; }