在 Matlab 中从 .ravi 文件中提取温度

Extracting Temperatures from .ravi file in Matlab

我的问题

很像这里的 post:,我有一个 .ravi 文件(一个辐射视频文件,与 .avi 非常相似),我正在尝试提取温度在其中,将它们与其他传感器数据一起使用。

当您下载“PIX 连接软件”时,可以在文档 (http://infrarougekelvin.com/en/optris-logiciel-eng/) 中找到示例文件。不幸的是,根据文档,温度信息以 16 位格式存储,Matlab 似乎对此不太满意。

我是如何尝试解决问题的

我尝试按照前面提到的 post 中的说明进行操作,但不知何故我很难达到结果,甚至接近正确的温度。Original Picture with temperatures in the Optris Software

我尝试用不同的方法阅读视频: 起初我希望使用Matlab中的videorecorder特性:

video   = VideoReader(videoPath);
frame1  = video.read(1);
imagesc(frame1)

但这只会导致这张糟糕的画面,这正是我在尝试在像 vlc 这样的媒体播放器中播放 .ravi 文件时所看到的。

First try with videorecorder function

然后我尝试查看我的文件的二进制表示并注意到,我可以在某个标记处分隔帧 Beginning of a new frame in binary representation

所以我尝试用 matlab fread 函数读取文件:

fileID               = fopen(videoPath);    
[headerInfo,~]       = fread(fileID,[1,123392],'uint8');
[imageMatrix,count]  = fread(fileID,[video.width, video.height],'uint16', 'b');
imagesc(imageMatrix')

现在图像看起来更好了,你至少可以看到制动盘,但似乎较高的温度有某种偏移,仍然缺失,图像是正确的。

此外,我从文件中读取的值与实际温度相去甚远,正如其他 post 和文档所建议的那样。

Getting somewhere!

我的问题

我是不是漏掉了什么重要的东西?有人能给我指出正确的方向,在哪里看或如何从我的视频中获取实际温度吗?因为它与另一个 post 中的 cpp 代码一起工作,我猜这可能是一个 matlab 问题。

获取原始帧数据的一个相对简单的解决方案是将 RAVI 视频文件转换为原始视频文件格式。

您可以使用 FFmpeg(命令行工具)将 RAVI 格式转换为 RAW 格式。

示例:

ffmpeg -y -f avi -i "Sequence_LED_Holder.ravi" -vcodec rawvideo "Sequence_LED_Holder.yuv"

YUV(原始二进制数据)文件,可以通过 MATLAB 使用 fread 函数简单地读取。
注意:.yuv 只是原始视频文件的约定(FFmpeg 使用)- 实际像素格式不是 YUV,而是 int16 格式。

您可以尝试手动解析 RAVI 文件,但使用 FFmpeg 更简单。

原始文件格式由一个接一个的原始视频帧组成,没有headers。
在我们的例子中,每帧是 width*height*2 字节。
像素类型为 int16(可能包含负值)。

红外视频帧没有颜色信息。
这些颜色只是使用调色板创建并用于可视化的“假色”。
代码示例使用来自不同 IR 相机制造商的调色板。

获取温度:
我找不到将像素值转换为等效温度的方法。
我没有阅读文档 - 有可能某处记录了转换。


MATLAB 代码示例应用以下阶段:

  • 使用 FFmpeg 将 RAVI 文件格式转换为 RAW 视频文件格式。
  • 将视频帧读取为 [cols, rows] 大小 int16 矩阵。
  • 删除可能包含数据(不是像素)的第一行。
  • 使用线性对比拉伸 - 用于可视化。
  • 应用伪色 - 用于可视化。
  • 显示处理后的视频帧。

这是代码示例:

%ravi_file_name = 'Brake disc.ravi';
%ravi_file_name = 'Combustion process.ravi';
%ravi_file_name = 'Electronic board.ravi';
%ravi_file_name = 'Sequence_carwheels.ravi';
%ravi_file_name = 'Sequence_drop.ravi';
ravi_file_name = 'Sequence_LED_Holder.ravi';
%ravi_file_name = 'Steel workpiece with hole.ravi';

yuv_file_name = strrep(ravi_file_name, '.ravi', '.yuv'); % Same file name with .yuv extension.

% Get video resolution.
vidinfo = mmfileinfo(ravi_file_name);
cols = vidinfo.Video.Width;
rows = vidinfo.Video.Height;


% Execute ffmpeg (in the system shell) for converting RAVI to raw data file.
% Remark: download FFmpeg if needed, and make sure ffmpeg executable is in the execution path.
if ~exist(yuv_file_name, 'file')
    % Remark: For some of the video files, cmdout returns a string with lots of meta-data 
    [status, cmdout] = system(sprintf('ffmpeg -y -f avi -i "%s" -vcodec rawvideo "%s"', ravi_file_name, yuv_file_name));
    if (status ~= 0)
        fprintf(cmdout);
        error(['Error: ffmpeg status = ', num2str(status)]);
    end
end

% Get the number of frames according to file size.
filesize = getfield(dir(yuv_file_name), 'bytes');
n_frames = filesize / (cols*rows*2);

f = fopen(yuv_file_name, 'r');

% Iterate the frames (skip the last frame).
for i = 1:n_frames-1
    % Read frame as cols x rows and int16 type.
    % The data is signed (int16) and not uint16.
    I = fread(f, [cols, rows], '*int16')';
  
    % It looks like the first line contains some data (not pixels).
    data_line = I(1, :);
    I = I(2:end, :);
    
    % Apply linear stretch - in order to "see something"...
    J = imadjust(I, stretchlim(I, [0.02, 0.98]));
    
    % Apply false colors - just for visualization.
    K = ColorizeIr(J);

    if (i == 1)
        figure;
        h = imshow(K, []); %h = imshow(J, []);
        impixelinfo
    else
        if ~isvalid(h)
            break;
        end
        h.CData = K; %h.CData = J;
    end
    
    pause(0.05);
end

fclose(f);

imwrite(uint16(J+2^15), 'J.tif'); % Write J as uint16 image.
imwrite(K, 'K.png'); % Write K image (last frame).



% Colorize the IR video frame with "false colors".
function J = ColorizeIr(I)
    % The palette apply different IR manufacture - don't expect the result to resemble OPTRIS output.
    % https://groups.google.com/g/flir-lepton/c/Cm8lGQyspmk
    colormapIronBlack = uint8([...
        255, 255, 255, 253, 253, 253, 251, 251, 251, 249, 249, 249, 247, 247,...
        247, 245, 245, 245, 243, 243, 243, 241, 241, 241, 239, 239, 239, 237,...
        237, 237, 235, 235, 235, 233, 233, 233, 231, 231, 231, 229, 229, 229,...
        227, 227, 227, 225, 225, 225, 223, 223, 223, 221, 221, 221, 219, 219,...
        219, 217, 217, 217, 215, 215, 215, 213, 213, 213, 211, 211, 211, 209,...
        209, 209, 207, 207, 207, 205, 205, 205, 203, 203, 203, 201, 201, 201,...
        199, 199, 199, 197, 197, 197, 195, 195, 195, 193, 193, 193, 191, 191,...
        191, 189, 189, 189, 187, 187, 187, 185, 185, 185, 183, 183, 183, 181,...
        181, 181, 179, 179, 179, 177, 177, 177, 175, 175, 175, 173, 173, 173,...
        171, 171, 171, 169, 169, 169, 167, 167, 167, 165, 165, 165, 163, 163,...
        163, 161, 161, 161, 159, 159, 159, 157, 157, 157, 155, 155, 155, 153,...
        153, 153, 151, 151, 151, 149, 149, 149, 147, 147, 147, 145, 145, 145,...
        143, 143, 143, 141, 141, 141, 139, 139, 139, 137, 137, 137, 135, 135,...
        135, 133, 133, 133, 131, 131, 131, 129, 129, 129, 126, 126, 126, 124,...
        124, 124, 122, 122, 122, 120, 120, 120, 118, 118, 118, 116, 116, 116,...
        114, 114, 114, 112, 112, 112, 110, 110, 110, 108, 108, 108, 106, 106,...
        106, 104, 104, 104, 102, 102, 102, 100, 100, 100, 98, 98, 98, 96, 96,...
        96, 94, 94, 94, 92, 92, 92, 90, 90, 90, 88, 88, 88, 86, 86, 86, 84, 84,...
        84, 82, 82, 82, 80, 80, 80, 78, 78, 78, 76, 76, 76, 74, 74, 74, 72, 72,...
        72, 70, 70, 70, 68, 68, 68, 66, 66, 66, 64, 64, 64, 62, 62, 62, 60, 60,...
        60, 58, 58, 58, 56, 56, 56, 54, 54, 54, 52, 52, 52, 50, 50, 50, 48, 48,...
        48, 46, 46, 46, 44, 44, 44, 42, 42, 42, 40, 40, 40, 38, 38, 38, 36, 36,...
        36, 34, 34, 34, 32, 32, 32, 30, 30, 30, 28, 28, 28, 26, 26, 26, 24, 24,...
        24, 22, 22, 22, 20, 20, 20, 18, 18, 18, 16, 16, 16, 14, 14, 14, 12, 12,...
        12, 10, 10, 10, 8, 8, 8, 6, 6, 6, 4, 4, 4, 2, 2, 2, 0, 0, 0, 0, 0, 9,...
        2, 0, 16, 4, 0, 24, 6, 0, 31, 8, 0, 38, 10, 0, 45, 12, 0, 53, 14, 0,...
        60, 17, 0, 67, 19, 0, 74, 21, 0, 82, 23, 0, 89, 25, 0, 96, 27, 0, 103,...
        29, 0, 111, 31, 0, 118, 36, 0, 120, 41, 0, 121, 46, 0, 122, 51, 0, 123,...
        56, 0, 124, 61, 0, 125, 66, 0, 126, 71, 0, 127, 76, 1, 128, 81, 1, 129,...
        86, 1, 130, 91, 1, 131, 96, 1, 132, 101, 1, 133, 106, 1, 134, 111, 1,...
        135, 116, 1, 136, 121, 1, 136, 125, 2, 137, 130, 2, 137, 135, 3, 137,...
        139, 3, 138, 144, 3, 138, 149, 4, 138, 153, 4, 139, 158, 5, 139, 163,...
        5, 139, 167, 5, 140, 172, 6, 140, 177, 6, 140, 181, 7, 141, 186, 7,...
        141, 189, 10, 137, 191, 13, 132, 194, 16, 127, 196, 19, 121, 198, 22,...
        116, 200, 25, 111, 203, 28, 106, 205, 31, 101, 207, 34, 95, 209, 37,...
        90, 212, 40, 85, 214, 43, 80, 216, 46, 75, 218, 49, 69, 221, 52, 64,...
        223, 55, 59, 224, 57, 49, 225, 60, 47, 226, 64, 44, 227, 67, 42, 228,...
        71, 39, 229, 74, 37, 230, 78, 34, 231, 81, 32, 231, 85, 29, 232, 88,...
        27, 233, 92, 24, 234, 95, 22, 235, 99, 19, 236, 102, 17, 237, 106, 14,...
        238, 109, 12, 239, 112, 12, 240, 116, 12, 240, 119, 12, 241, 123, 12,...
        241, 127, 12, 242, 130, 12, 242, 134, 12, 243, 138, 12, 243, 141, 13,...
        244, 145, 13, 244, 149, 13, 245, 152, 13, 245, 156, 13, 246, 160, 13,...
        246, 163, 13, 247, 167, 13, 247, 171, 13, 248, 175, 14, 248, 178, 15,...
        249, 182, 16, 249, 185, 18, 250, 189, 19, 250, 192, 20, 251, 196, 21,...
        251, 199, 22, 252, 203, 23, 252, 206, 24, 253, 210, 25, 253, 213, 27,...
        254, 217, 28, 254, 220, 29, 255, 224, 30, 255, 227, 39, 255, 229, 53,...
        255, 231, 67, 255, 233, 81, 255, 234, 95, 255, 236, 109, 255, 238, 123,...
        255, 240, 137, 255, 242, 151, 255, 244, 165, 255, 246, 179, 255, 248,...
        193, 255, 249, 207, 255, 251, 221, 255, 253, 235, 255, 255, 24]);
    
    lutR = colormapIronBlack(1:3:end);
    lutG = colormapIronBlack(2:3:end);
    lutB = colormapIronBlack(3:3:end);
    
    % Convert I to uint8
    I = im2uint8(I);
    R = lutR(I+1);
    G = lutG(I+1);
    B = lutB(I+1);
    
    J = cat(3, R, G, B);
end

示例输出:


更新:

Python 使用 OpenCV 的代码示例(未着色):

使用Python和OpenCV,我们可以跳过FFmpeg转换部分。
我们可以从 RAVI 文件中获取未解码的 RAW 视频,而不是将 RAVI 文件转换为 YUV 文件。

  • 打开视频文件并设置 CAP_PROP_FORMAT 属性 以获取未解码的 RAW 视频:
    cap = cv2.VideoCapture(ravi_file_name)
    cap.set(cv2.CAP_PROP_FORMAT, -1)  # Format of the Mat objects. Set value -1 to fetch undecoded RAW video streams (as Mat 8UC1).

读取视频帧(使用 ret, frame = cap.read())时,未解码的帧被读取为 uint8 个元素的“长”行向量。

  • 将帧转换为 int16 类型,并重塑为 cols x rows:
    首先,我们必须将向量元素“查看”为 int16 个元素(相对于 uint8 个元素):frame.view(np.int16)
    其次,我们必须将向量重塑为矩阵。
    转换和整形代码:
    frame = frame.view(np.int16).reshape(rows, cols)

完成 Python 代码示例:

import numpy as np
import cv2

ravi_file_name = 'Sequence_LED_Holder.ravi'

cap = cv2.VideoCapture(ravi_file_name)  # Opens a video file for capturing

# Fetch undecoded RAW video streams
cap.set(cv2.CAP_PROP_FORMAT, -1)  # Format of the Mat objects. Set value -1 to fetch undecoded RAW video streams (as Mat 8UC1). [Using cap.set(cv2.CAP_PROP_CONVERT_RGB, 0) is not working]

cols  = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))  # Get video frames width
rows = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))  # Get video frames height

while True:
    ret, frame = cap.read()  # Read next video frame (undecoded frame is read as long row vector).

    if not ret:
        break  # Stop reading frames when ret = False (after the last frame is read).

    # View frame as int16 elements, and reshape to cols x rows (each pixel is signed 16 bits)
    frame = frame.view(np.int16).reshape(rows, cols)

    # It looks like the first line contains some data (not pixels).
    # data_line = frame[0, :]
    frame_roi = frame[1:, :]  # Ignore the first row.

    # Normalizing frame to range [0, 255], and get the result as type uint8 (this part is used just for making the data visible).
    normed = cv2.normalize(frame_roi, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)

    cv2.imshow('normed', normed)  # Show the normalized video frame
    cv2.waitKey(10)

cap.release()
cv2.destroyAllWindows()

示例输出:

注:

每帧按像素保存 16 位值,低字节在前,高字节在后。要找到温度,您必须应用此公式:temp = (hi * 256.0 + lo) / 10.0 - 100.0.

使用低值,您可以创建灰度图像。我成功地将这种方法用于旧的 Pi-160 Optris 相机。然而,对于新的 PI-450,由于 PI Connect 现在不支持二进制导出,因此更加困难。

我用 Ffmpeg 测试了解决方案,但没有成功。你得到一个16位的数据文件,但是实际数据偏移量不对,导致温度异常。

你成功了吗?

二进制读取示例:

在大多数使用Ffmpeg处理的ravi文件中,原始图像的第一行都有非像素值。 第一行存储一些冗余信息,例如图像宽度和高度。 我们必须跳过对应于图像宽度的这一行。由于数据值是 16 位,我们必须乘以 2,才能得到二进制数据的准确偏移量。我们还必须计算图像的确切大小:imageLength = Frame size - (image width * 2)。 在另一种情况下,数据来自文件的开头,我们可以使用帧大小 (w * h * 2) 来复制二进制数据并更新偏移量。 要知道是否有必要计算数据偏移量,我们只需查看图像高度即可。如果这个值是奇数,那意味着有一个补充的第一行,因此我们应用校正。如果值为偶数,则不对数据偏移量进行修正。

解析原始 ravi 文件时也是如此。首先我们要找到movi标签在文件中的偏移量。如果 movi 标签后面跟着 ix00 标签,这意味着我们在一系列值之后给出了偏移量和每个帧相对于 movi 标签偏移量的大小。真实数据在文件中的其他地方。如果 ix00 标签不存在,这意味着数据就在 movi chunck 内部,在 00db 标志之后,并且是逐帧的。在最后一种情况下,我们还可以查找 idx1 标签(在文件末尾),它可以访问每个帧的确切偏移量和大小。

这两种方法都允许以灰度或伪彩色表示相当正确的图像,但 libirimager 工具包提供的温度公式 (float t = (float)data[i] / 10.f - 100.f) 不正确,我不明白为什么,因为当我使用 PI-160 相机生成的原始数据时公式是正确的。

Fmmpeg test

我找到了另一种方法。在最近的 ravi Optris 文件中,我们可以在 INFO 块中获取温度范围。然后,很容易找到原始数据中的最小值和最大值,并根据温标进行插值。 with correct temperatures