65.9K
CodeProject 正在变化。 阅读更多。
Home

FITS 转换为图像和图像透明度

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2016年4月8日

CPOL

7分钟阅读

viewsIcon

14486

downloadIcon

48

将 FITS 文件转换为已知的图像格式,并将这些图像用于“缩放”功能(图像透明度)

引言

本文共三部分。 

  1. 将FITS文件转换为更常见的图像格式
  2. 以动态透明度显示转换后的图像
  3. 播放延时视频,同时使用动态透明度。

必备组件

背景

本文中的代码相当直接,但找到代码却有些困难。 在工作中,我们观察太阳,试图理解它(研究),并试图进行空间天气预测(操作)。 

观察太阳的最佳工具之一是SDO卫星。(http://sdo.gsfc.nasa.gov/) 这颗卫星可以在不同的波段提供太阳日冕的观测(关于AIA仪器,还有其他仪器)。 AIA成像仪以几种波段呈现图像:094、131、171、193、211、304、335、1600、1700和4500埃。 每个波段显示不同的特征。 (取决于热量和密度)。

这个原型基于这样的想法:温度随着向外扩散而升高,穿过过渡区/日冕。

Temperature in the corona

 

 

 

 

 

 

 

 

 

 

因此,通过根据基础温度(开尔文)对图像进行排序,我们应该可以得到某种可缩放的3D图像。

事实证明并非如此,但这个工具仍然很有用,因为图像都经过了相同的校准(对齐中心、对齐盘面大小等),因此我们可以更容易地通过“缩放”来跟踪不同波段的特征。

关于FITS

FITS代表Flexible Image Transport System(灵活图像传输系统)。 它是一种天文学中常用的标准文件格式。 它旨在处理表格和图像(表格也是一种图像)。 一个FITS文件可以包含多个表格和图像,因此文件的大小也会因来源而异。 目前C#对它的支持不是很好。 而且唯一的可用库不允许使用像idl或python库那样多的特殊功能。

使用代码

读取FITS

在C#中尝试实现“图像化”之前,您需要将FITS文件转换为.Net可以处理的格式:bmp、jpg、png、tiff等。

这相当困难,因为关于FITS文件包含什么信息并不多。 例如,每个仪器都可以提供这样的文件,但具有不同类型的值。 正如我通过SDO AIA FITS文件发现的那样,像素值不是RGB。

FITS文件包含HDU部分。 Header Data Units(头部数据单元)。 每个单元包含两个部分:一个包含原始数据的**数据**部分和一个包含元数据的**头部**部分。 一些**头部**关键字是强制的,其他是可选的(或可以自由添加)。

这是转换方法。 它接收FITS文件的文件路径。
(此转换特定于SDO AIA格式。)

public static System.Drawing.Image Convert(string pathfitsfile){
            int totalmax = 0;
            int totalmin = 0;
            int wavelength = 0;
            

            System.Drawing.Image result = null;
            //http://fits.gsfc.nasa.gov/fits_libraries.html#CSharpFITS
            //Read the fits file
            nom.tam.fits.Fits fits = new nom.tam.fits.Fits(pathfitsfile);
            nom.tam.fits.BasicHDU basichdu;
            do{
                basichdu = fits.readHDU();
                if(basichdu != null){
                    basichdu.Info();
                }                                            //end if
            }                                                //end do
            while(basichdu != null);

            //loop through the Header Data Units
            for(int i = 0; i < fits.NumberOfHDUs; i++){
                basichdu = fits.getHDU(i);
                //this retreives the current bandwidth
                string card = basichdu.Header.GetCard(20);
                wavelength = System.Convert.ToInt32(card.Replace("WAVELNTH=", "").Trim());
                if(basichdu.GetType().FullName == "nom.tam.fits.ImageHDU"){
                    try{
                        nom.tam.fits.ImageHDU imghdu = (nom.tam.fits.ImageHDU)basichdu;
                        //get out the pixel array and the dimensions
                        Array [] array = (Array[])imghdu.Data.DataArray;
                        int x = imghdu.Axes[0];
                        int y = imghdu.Axes[1];
                        result = new System.Drawing.Bitmap(x, y);
                        System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(result);
                        int idx_x = 0;
                        int idx_y = 0;
                        //get the real min/max values.
                        //We need it to get the values between 0 - 255.
                        for(int j = array.Length-1; j >= 0; j--){
                            int[] row = (int[])array[j];
                            for(int k = 0; k < row.Length; k++){
                                totalmax = (row[k] > totalmax ? row[k] : totalmax);
                                totalmin = (row[k] < totalmin ? row[k] : totalmin);
                            }                                //end for
                        }

                        //offset the values so everything is positive
                        int offset = (int)(totalmin < 0.0 ? (-1)*totalmin : 0.0);
                        //bottom is top, top is bottom.
                        for(int j = array.Length-1; j >= 0; j--){
                            //pixels are NOT RGB values so here we convert it to an RGB value.
                            idx_x = 0;
                            idx_y++;
                            int[] row = (int[])array[j];
                            for(int k = 0; k < row.Length; k++){
                                double val = row[k];
                                //This was trial and error.
                                switch(wavelength){
                                    case 335:    
                                    case 94:
                                    case 131:
                                    break;
                                    case 304:    val /= 3;
                                    break;    
                                    case 171:
                                    case 193: 
                                    case 211: 
                                    case 1600:    val /= 32;
                                    break;
                                    case 1700:    val /= 128;
                                    break;
                                    case 4500:    val /= 1024;
                                    break;
                                    default:
                                    break;
                                }                            //end switch
                                //make sure all pixels are in range 0 - 255
                                val = (val > 255.0 ? val = 255.0 : val);
                                val = (val < 0.0 ? val = 0.0 : val);
                                System.Drawing.Color c = System.Drawing.Color.FromArgb(2013265920 + (int)val);
                                //call this function to color the image depending on wavelength
                                c = WeighRGBValue(System.Drawing.Color.FromArgb(c.A, c.B, c.B, c.B), wavelength);
                                ((System.Drawing.Bitmap)result).SetPixel(idx_x, idx_y, c);
                                idx_x++;
                            }
                            
                        }

                    }                                        //end try
                    catch(Exception ex){
                        string error = ex.Message;
                    }                                        //end catch
                }                                            //end if
            }                                                //end for
            return result;
        }

基本上,这段代码会剥离文件的**数据**部分(像素值),并将这些值转换为RGB。

如果您不使用 `WeighRGBValue` 方法,图像将是灰度的(这没问题)。 但为了后续缩放,您需要能够识别哪个特征属于哪个图像(或相应的)波段。 因此,我们对其进行着色。 我试图模拟网站上找到的原始SDO AIA颜色。

        //http://www.rapidtables.com/web/color/RGB_Color.htm
        private static System.Drawing.Color WeighRGBValue(System.Drawing.Color color, int wavelength){
            //Tried to get a similar color to the original site.
            System.Drawing.Color c;
            int alpha, red, green, blue;
            red = green = blue = 255;
            alpha = color.A;
            switch(wavelength){
                case 94:    //green : 00FF00
                            red = 0;
                            green = color.G;
                            blue = 0;
                break;
                case 131:    //teal : 008080
                            red = 0;
                            green = color.G;
                            blue = color.B;
                break;
                case 304:    //red : FF0000
                            red = color.R;
                            green = color.G / 5;
                            blue = color.B / 5;
                break;
                case 335:    //blue : 0000FF
                            red = (color.R / 5);
                            green = (color.G / 5);
                            blue = color.B;
                break;
                case 171:    //gold : FFD700
                            red = color.R;
                            green = (int)((double)color.G / 255.0 * 215.0);
                            blue = 0;
                break;
                case 193:    //copper : B87333
                            red = color.R;                                    //184 mapped to 255
                            green = (int)((double)color.G / 255.0 * 115.0);    //115 mapped to 255
                            blue = (int)((double)color.B / 255.0 * 51.0);    //50 mapped to 255
                break;
                case 211:    //purple : 800080
                            red = color.R;
                            green = 0;
                            blue = color.B;
                break;
                case 1600:    //ocher : BBBB00
                            red = (int)((double)color.R / 255.0 * 187.0);    //BB mapped to 255
                            green = (int)((double)color.G / 255.0 * 187.0);    //BB mapped to 255
                            blue = 0;
                break;
                case 1700:    //pink : FFC0CB
                            red = color.R;
                            green = (int)((double)color.G / 255.0 * 192.0);
                            blue = (int)((double)color.B / 255.0 * 203.0);
                break;
                case 4500:    //silver : C0C0C0
                            red = color.R;
                            green = color.G;
                            blue = color.B;
                break;
                default:    red = 0;
                            green = 0;
                            blue = 0;
                break;
            }                            //end switch
            c = System.Drawing.Color.FromArgb(alpha, red, green, blue);
            return c;
        }

 

简单来说,我们正在将灰色转移到另一种颜色,使其显示正确。 这涉及到一些试错。 我试图尽可能接近原始颜色,但也试图不丢失任何特征。

图像透明度

请注意,您也可以从SDO网站下载jpg图像,以跳过第一部分。

一旦您有了图像,就可以将它们一个一个地堆叠起来。 它们大小相同,太阳中心相同,盘面大小也相同。 我在项目源文件中添加了一行图像。

在WPF中,您为每个波段创建一个Image。 我使用温度作为基础值来排序我的图像。

  1. 304
  2. 131
  3. 171
  4. 193
  5. 211
  6. 335
  7. 1600
  8. 1700
  9. 4500

温度以开尔文(K)表示。

您可以在互联网上找到大部分信息,但这里也有一些
(不完全,但足以满足我们的需求)

波段 K(从) K(到) Order
0094 6300000   7
0131 400000 16000000 2
0171 630000   3
0193 1200000 20000000 4
0211 20000000   5
0304 50000   1
0335 25000000   6
1600      
1700      
4500      

 

 

 

 

排序基于K(从)。 值大部分是范围。(例如0131、0193)。

从磁盘加载图像可以这样做

string fn = rootfolder + @"\" + basefilename.Replace(bw, "0094");
                images["0094"].BeginInit();
                images["0094"].UriSource = new Uri(fn, UriKind.Absolute);
                images["0094"].EndInit();
                img_0094.Source = images["0094"];
                img_0094.Stretch = Stretch.Uniform;

这段代码加载0094图像。 对所有波段重复此操作。 我在XAML中手动添加和样式化了Image控件,但您也可以轻松地在代码中添加,并使用List或Array来保存控件。

请注意,每个波段的文件名是相似的(因此可以重用文件名并插入带有新波段的波段部分)。 您还可以使用文件名来更改“时间”参数,稍后在“延时视频”部分中使用。

为了缩放,我添加了一个滑块,并在其 `ValueChanged` 事件中添加了此代码。

            //0 - 1000
            if(slider_zoom.Value >= 0 && slider_zoom.Value < 100){
                img_0304.Opacity = (100 - slider_zoom.Value)/100;
                img_0131.Opacity = (0 + slider_zoom.Value) / 100;
            }
            if(slider_zoom.Value >= 100 && slider_zoom.Value < 200){
                img_0131.Opacity = (100 - (slider_zoom.Value - 100))/100;
                img_0171.Opacity = (0 + (slider_zoom.Value - 100))/100;
            }
            if(slider_zoom.Value >= 200 && slider_zoom.Value < 300){
                img_0171.Opacity = (100 - (slider_zoom.Value - 200))/100;
                img_0193.Opacity = (0 + (slider_zoom.Value - 200))/100;
            }
            if(slider_zoom.Value >= 300 && slider_zoom.Value < 400){
                img_0193.Opacity = (100 - (slider_zoom.Value - 300))/100;
                img_0211.Opacity = (0 + (slider_zoom.Value - 300))/100;

            }
            if(slider_zoom.Value >= 400 && slider_zoom.Value < 500){
                img_0211.Opacity = (100 - (slider_zoom.Value - 400))/100;
                img_0335.Opacity = (0 + (slider_zoom.Value - 400))/100;
            }
            if(slider_zoom.Value >= 500 && slider_zoom.Value < 600){
                img_0335.Opacity = (100 - (slider_zoom.Value - 500))/100;
                img_0094.Opacity = (0 + (slider_zoom.Value - 500))/100;
            }
            if(slider_zoom.Value >= 600 && slider_zoom.Value < 700){
                img_0094.Opacity = (100 - (slider_zoom.Value - 600))/100;
                img_1600.Opacity = (0 + (slider_zoom.Value - 600))/100;
            }
            if(slider_zoom.Value >= 700 && slider_zoom.Value < 800){
                img_1600.Opacity = (100 - (slider_zoom.Value - 700))/100;
                img_1700.Opacity = (0 + (slider_zoom.Value - 700))/100;
            }
            if(slider_zoom.Value >= 800 && slider_zoom.Value < 900){
                img_1700.Opacity = (100 - (slider_zoom.Value - 800))/100;
                img_4500.Opacity = (0 + (slider_zoom.Value - 800))/100;
            }
            if(slider_zoom.Value >= 900 && slider_zoom.Value < 1000){
                img_4500.Opacity = (100 - (slider_zoom.Value - 900))/100;
            }

我必须承认,这比我想象的要容易,而且进行得很顺利。

延时视频

这部分仍在建设中。 当然,任何想法都欢迎。

这是我的想法

基本上,每个波段需要两条轨道(双缓冲),在后台加载下一个图像,同时显示前一个。 这意味着您要处理20个Image控件,10个显示,10个在后台。(可能的内存问题)

我认为问题在于在循环遍历图像时动态更改 `Opacity` 属性。 由于这绝对是多线程的,所以可能会成为一个问题。

不用说,如果我能让它工作,我会在本节中发布。

关注点

这个原型项目的有趣之处在于它非常直观。 处理图像总是这样的。 您稍微改变一下代码,就能立即看到结果。

这是一个“空闲时间”项目,所以我也不确定能花多少时间,但我会尽力完成它。

关于fits转换

这被证明是一个很大的挑战。 C#可用的FITS库不是最好的,而且我了解到并非所有图像都由RGB像素组成。 将其转换为所需的格式被证明是一个巨大的挑战。 最终结果“尚可”,但仍然不像在SDO网页上看到的那样。 我非常确定我的试错过程可以改进,特别是跳过(或设置)阈值以上的像素值(在转换之前!)为黑色。 我看到一些python代码里是这么做的。 效果是太阳周围的阴霾应该会有些消失。 我们用来除以FITS像素值的任意值也可以改进。 我猜FITS头部中可能有一些缩放因子可以使用,但到目前为止我还没有找到。

关于图像透明度

起初,我以为你需要对图像本身进行操作才能实现这个功能,而不是改变控件的透明度。 由于这并不需要太多工作,我只是试了一下,效果非常好而且流畅。 不用说,这也可以用于其他类型的图像,在这些图像中,做类似的事情可能很有用。

延时视频

创建延时视频本身不应该太难。 您创建一个计时器,然后一个接一个地加载/显示图像。 在运行延时视频时尝试控制图像可能会是另一回事,但让10个波段同步运行同时玩转透明度将是一个巨大的挑战。

参考文献

  • “由NASA/SDO以及AIA、EVE和HMI科学团队提供。”

历史

  • 2016-04-08:版本1.0
© . All rights reserved.