2020年8月31日 星期一

OpenCV erode、dilate(形態學處理)

當使用完二值化或邊緣偵測後,畫面總會殘留一些白點雜訊,或特徵會有破洞等狀況,為了不影響後續的辨識,這時可用型態學的方式,如侵蝕或膨脹將不想要的特徵給去除。

侵蝕

void erode(InputArray src, OutputArray dst, InputArray kernel, Point anchor=Point(-1,-1), int iterations=1, int borderType=BORDER_CONSTANT, const Scalar& borderValue=morphologyDefaultBorderValue())

InputArray src:輸入影像
OutputArray dst:輸出影像
InputArray kernel:輸入核心
Point anchor:錨點位置
int iterations:疊代次數
nt borderType:邊界模式

膨脹

void dilate(InputArray src, OutputArray dst, InputArray kernel, Point anchor=Point(-1,-1), int iterations=1, int borderType=BORDER_CONSTANT, const Scalar& borderValue=morphologyDefaultBorderValue())

InputArray src:輸入影像
OutputArray dst:輸出影像
InputArray kernel:輸入核心
Point anchor:錨點位置
int iterations:疊代次數
nt borderType:邊界模式

但不論是侵蝕或膨脹,其預設的kernel都是3x3,如果想定義自己的kernel,可利用下列函示

Mat getStructuringElement(int shape, Size esize, Point anchor = Point(-1, -1));
int shape:元素形狀
Size esize:建構元素大小(kernal size)
Point anchor:錨點位置


程式碼如下:
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main(){

	Mat SrcImg = imread("dog.jpg", CV_LOAD_IMAGE_COLOR);
	Mat GrayImg, BinaryImg;
	Mat ErodeImg, DilateImg, UserErodeImg, UserDilateImg;

	cvtColor(SrcImg, GrayImg, CV_BGR2GRAY);

	Mat Element = getStructuringElement(MORPH_ELLIPSE, Size(7, 7));
	erode(GrayImg, ErodeImg, Mat(), Point(-1, -1), 1);
	erode(GrayImg, UserErodeImg, Element, Point(-1, -1), 1);
	dilate(GrayImg, DilateImg, Mat(), Point(-1, -1), 1);
	dilate(GrayImg, UserDilateImg, Element, Point(-1, -1), 1);

	imshow("GrayImg", GrayImg);
	imshow("ErodeImg", ErodeImg);
	imshow("UserErodeImg", UserErodeImg);
	imshow("DilateImg", DilateImg);
	imshow("UserDilateImg", UserDilateImg);

	waitKey(0);
	return 0;
}

GrayImg

ErodeImg

UserErodeImg

DilateImg

UserDilateImg

2020年8月1日 星期六

OpenCV Threshold(二值化)

為了從影像找出我們想要的特徵,一般會使用二值化(Threshold)先將前景(foreground)與背景(background)初步分離出來,以減少不必要的訊息量(雜訊),底下就來介紹一下OpenCV常用的threshold函式,我自己初分成三種類型。

第一種:基本款的二值化(依據閥值進行分類的二值化)

double cv::threshold(InputArray src,OutputArray dst,double thresh,double maxval,int type)
InputArray src:輸入的影像來源
OutputArray dst:輸出的影像來源
double thresh:判定閥值
double maxval:最大值(當像素值大於或小於判定閥值時,用此參數取代)
int type:設定二值化模式,常見有THRESH_BINARY、THRESH_BINARY_INV、THRESH_TRUNC、THRESH_TOZERO、THRESH_TOZERO_INV五種。
THRESH_BINARY:將大於閥值的灰度值設為最大值,反之設為0
THRESH_BINARY_INV:將大於閥值的灰度值設為0,反之設為最大值
THRESH_TRUNC:將大於閥值的灰度值設為閥值,反之則不變
THRESH_TOZERO:將大於閥值的灰度值不變,反之則設為0
THRESH_TOZERO_INV:將大於閥值的灰度值設為0,反之則不變

第二種:進階版的二值化(依據亮度分佈,自動定義閥值進行分類的二值化)

跟第一種幾乎一樣,差別在於設定的二值化模式不同,可選擇為THRESH_OTSU跟THRESH_TRIANGLE兩種,THRESH_OTSU比較適用在有兩個分峰值情況下使用,而THRESH_TRIANGLE則用在單一峰值比較合適。


第三種:強大版的二值化(依據區域不同,適應不同閥值進行分類的二值化)

void adaptiveThreshold(InputArray src, OutputArray dst,double maxValue,int adaptiveMethod, int thresholdType, int blockSize, double C)
InputArray src:輸入的影像來源
OutputArray dst:輸出的影像來源
double maxval:最大值(當像素值大於或小於判定閥值時,用此參數取代)
int adaptiveMethod:設定自適應模式,有ADAPTIVE_THRESH_MEAN_C(平均法)、ADAPTIVE_THRESH_GAUSSIAN_C(高斯法)兩種。
nt thresholdType:設定二值化模式,即前面提到的二值化五種模式
int blockSize:區塊大小
double C:常數

總結:

上述三種方式,我覺得第一種使用單純但無法用於環境光源變化大的地方、第二種雖然可以自動定義閥值,但使用時若影像亮度分佈沒有明顯的兩個或一個峰值,則效果可能有限,第三種則適應能力較強,處理後的效果還有點像邊緣偵測,也許可用在其他地方上。


現在就來實作一下吧,為了比較好理解基本款二值化操作,我設計了一張圖片,當中分成了五個區域,由左至右亮度值分別為(50、100、150、200、250),讓我們更容易看出不同的差異性。

程式碼如下:
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main(){

	Mat SrcImg = imread("GradientImg.bmp", CV_LOAD_IMAGE_COLOR);
	Mat GrayImg, BinaryImg, BinaryInvImg, BinaryTruncImg, BinaryToZeroImg, BinaryToZeroInvImg;
	Mat OtsuImg, TriangleImg, AdaptiveThImg;

	cvtColor(SrcImg, GrayImg, CV_BGR2GRAY);
	threshold(GrayImg, BinaryImg, 150, 255, THRESH_BINARY);//將大於閥值的灰度值設為最大值,反之設為0
	threshold(GrayImg, BinaryInvImg, 150, 255, THRESH_BINARY_INV);//將大於閥值的灰度值設為0,反之設為最大值
	threshold(GrayImg, BinaryTruncImg, 190, 255, THRESH_TRUNC);//將大於閥值的灰度值設為閥值,反之則不變
	threshold(GrayImg, BinaryToZeroImg, 150, 255, THRESH_TOZERO);//將大於閥值的灰度值不變,反之則設為0
	threshold(GrayImg, BinaryToZeroInvImg, 150, 255, THRESH_TOZERO_INV);//將大於閥值的灰度值設為0,反之則不變
	threshold(GrayImg, OtsuImg, 0, 255, THRESH_OTSU);//自動設定最佳閥值,適用於有兩個峰值的情況
	threshold(GrayImg, TriangleImg, 250, 255, THRESH_TRIANGLE);//自動設定最佳閥值,適用只有一個峰值的情況
	adaptiveThreshold(GrayImg, AdaptiveThImg, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 5, 1);

	imshow("GradientImg", SrcImg);
	imshow("BinaryImg", BinaryImg);
	imshow("BinaryInvImg", BinaryInvImg);
	imshow("BinaryTruncImg", BinaryTruncImg);
	imshow("BinaryToZeroImg", BinaryToZeroImg);
	imshow("BinaryToZeroInvImg", BinaryToZeroInvImg);
	imshow("OtsuImg", OtsuImg);
	imshow("TriangleImg", TriangleImg);
	imshow("AdaptiveThImg", AdaptiveThImg);

	waitKey(0);
	return 0;
}


GradientImg

BinaryImg

BinaryInvImg

BinaryTruncImg

BinaryToZeroImg

BinaryToZeroInvImg

OtsuImg


TriangleImg

AdaptiveImg










2020年6月14日 星期日

OpenCV EllipseRect(橢圓擬合功能)

如果想將影像中的圖形,擬合成橢圓形並畫出來,那要怎麼做呢?
使用到下列的函式fitEllipse,可以將findContours算出來的contours作為輸入
並用RotatedRect的函式去接橢圓形的資訊。

C++: RotatedRect fitEllipse(InputArray points)

//---------------------------------------------程式碼-------------------------------------------------
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main(){

	Mat PatternImg = imread("Pattern1.png", CV_LOAD_IMAGE_COLOR);
	Mat GrayImg, BinaryImg;
	vector<vector<Point> > contours;
	vector<Vec4i> hierarchy;
	resize(PatternImg, PatternImg, Size(), 0.5, 0.5);
	cvtColor(PatternImg, GrayImg, CV_BGR2GRAY);
	threshold(GrayImg, BinaryImg, 125, 255, CV_THRESH_BINARY);

	findContours(BinaryImg, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE, Point(0, 0));

	Point2f LongPtA1, LongPtA2, ShortPtB1, ShortPtB2;
	for (int i = 0; i< contours.size(); i++)
	{
		if (contours[i].size()<=5)
			continue;
		RotatedRect EllipseRect = fitEllipse(contours[i]);
		
		float Angle = (EllipseRect.angle - 90) * 3.1415972 / 180.0;

		LongPtA1.x = EllipseRect.center.x + cos(Angle)*EllipseRect.size.height / 2;
		LongPtA1.y = EllipseRect.center.y + sin(Angle)*EllipseRect.size.height / 2;
		LongPtA2.x = EllipseRect.center.x - cos(Angle)*EllipseRect.size.height / 2;
		LongPtA2.y = EllipseRect.center.y - sin(Angle)*EllipseRect.size.height / 2;

		ShortPtB1.x = EllipseRect.center.x + sin(-Angle)*EllipseRect.size.width / 2;
		ShortPtB1.y = EllipseRect.center.y + cos(-Angle)*EllipseRect.size.width / 2;
		ShortPtB2.x = EllipseRect.center.x - sin(-Angle)*EllipseRect.size.width / 2;
		ShortPtB2.y = EllipseRect.center.y - cos(-Angle)*EllipseRect.size.width / 2;

		ellipse(PatternImg, EllipseRect, Scalar(0, 0, 255), 2, CV_AA);

		circle(PatternImg, Point(EllipseRect.center.x, EllipseRect.center.y), 5, Scalar(0, 0, 255), -1);
		circle(PatternImg, LongPtA1, 3, Scalar(0, 255, 255), -1);
		circle(PatternImg, LongPtA2, 3, Scalar(0, 255, 255), -1);
		circle(PatternImg, ShortPtB1, 3, Scalar(255, 255, 0), -1);
		circle(PatternImg, ShortPtB2, 3, Scalar(255, 255, 0), -1);
	}
	
	imshow("ResultImg", PatternImg);
	imwrite("ResultImg.bmp", PatternImg);
	waitKey(0);
	return 0;
}
//---------------------------------------------程式碼-------------------------------------------------
結果圖


2020年5月3日 星期日

OpenCV 貼圖功能(copyto)

如果,想要將一張圖片直接貼到到另一張圖片,並用OpenCV要如何做呢?答案是可以利用copyto來做到這個功能喔~

就如先前說明的文章來看,copyto如果原影像與目標影像大小是一樣的,就會直接在原影像位置直接做修改,若希望有類似去背的功能,copyto也可以幫忙實現出來喔!

在copyto的函示中,有兩種用法,第一種就是直接把影像貼上去,第二種搭配Mask,可以對輸入影像去背,但只對Mask影像中黑色部分,其他則保留

copyTo (OutputArray m)
copyTo (OutputArray m, InputArray mask)

#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main(){

	Mat DogImg = imread("dog.jpg", CV_LOAD_IMAGE_COLOR);
	Mat OpenCVImg = imread("OpencvImg.png", CV_LOAD_IMAGE_COLOR);
	Mat RoiImg, GrayImg, MaskImg, InvMaskImg;

	resize(OpenCVImg, OpenCVImg, Size(200, 200));
	imshow("OpenCVImg", OpenCVImg);

	cvtColor(OpenCVImg, GrayImg, CV_BGR2GRAY);
	threshold(GrayImg, MaskImg, 200, 255, CV_THRESH_BINARY);
	bitwise_not(MaskImg, InvMaskImg);
	imshow("InvMaskImg", InvMaskImg);

	RoiImg = DogImg(Rect(750, 450, OpenCVImg.cols, OpenCVImg.rows));
	OpenCVImg.copyTo(RoiImg, InvMaskImg);
	imshow("DogImg", DogImg);

	waitKey(0);
	return 0;

} 


2020年3月3日 星期二

C# 使用C++ DLL檔

最近因為工作需求,需要用C++寫OpenCV演算法,並從C#去使用其DLL檔,雖然我比較喜歡使用C++ OpenCV搭配Qt,但公司不允許只好照做摟~~~


Step1:加入一個c++的win 32Console App專案,名稱為SysAlgorithm,並記得選擇為DLL與空專案。



Step2:加入三個新項目,分別如下:(超偷懶只放add 和subtract)

SysAlgorithm.cpp:

#include "SysAlgorithm.h"

Calculator::Calculator()
{

}

Calculator::~Calculator()
{

}

int Calculator::add(int a, int b)
{
return(a + b);
}

int Calculator::subtract(int a, int b)
{
return(a - b);
}

SysAlgorithm.h:

#pragma once
#ifndef SYSALGORITHM_H
#define SYSALGORITHM_H

#include <fstream>
#include <string>

class Calculator
{
public:
Calculator();
~Calculator();

int add(int a, int b);
int subtract(int a, int b);
};

#endif // !SYSALGORITHM_H

Extern.cpp:

#ifdef SYSALGORITHM_EXPORTS
#define SYSALGORITHM_API __declspec(dllexport)
#include "SysAlgorithm.h"

extern "C" SYSALGORITHM_API int add(int a, int b)
{
Calculator Calculator;
return Calculator.add(a, b);
}

extern "C" SYSALGORITHM_API int subtract(int a, int b)
{
Calculator Calculator;
return Calculator.subtract(a, b);
}

#else
#define SYSALGORITHM_API __declspec(dllimport)
#endif

在Extern.cpp中,SYSALGORITHM_EXPORTS,綠色字要跟專案名稱一樣才行

Step3:修改專案屬性設定執行環境為x64與可以支援clr,如下圖



Step4:新增一個 c#的Console App專案,跟介面有關所以名稱設為SysUI,如下圖




Step5:修改Form1.cs內容

Form1.cs:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace SysUI
{
    public partial class Form1 : Form
    {
        [DllImport("SysAlgorithm.dll", EntryPoint = "add", CallingConvention = CallingConvention.Cdecl)]
        private static extern int add(int a, int b);

        [DllImport("SysAlgorithm.dll", EntryPoint = "subtract", CallingConvention = CallingConvention.Cdecl)]
        private static extern int subtract(int a, int b);

        public Form1()
        {
            InitializeComponent();
            label1.Text = add(3, 5).ToString();
            label2.Text = subtract(3, 5).ToString();
        }

    }
}

-----------------------------------------
在Form1.cs中,要加入using System.Runtime.InteropServices,才能讀取外部dll檔案,並且加入參考,說明dll檔的位置,如下圖所示。



Step6:最後!!修改專案屬性設定執行環境為x64,並將C#專案設成初始啟動專案,不然會直接開成c++專案,而因為dll本身是無法被直接啟動,所以會跳錯喔!!




Step7:測試結果如下~

關於接DLL檔回傳值部分,可以參考C# 使用C++ DLL檔(回傳值)




2019年11月17日 星期日

VS 2013 with Qt 5.8.0 install 環境建置

Step1.先下載Qt5.8.0(https://download.qt.io/archive/qt/5.8/5.8.0/)
,因為我的電腦是64位元、VS2013,所以選擇如下。

跟Qt-vsaddin tool(https://marketplace.visualstudio.com/items?itemName=TheQtCompany.QtVisualStudioTools),如下所示。

Step2.開始安裝嘍,首先先安裝Qt5.8.0
我沒有帳號,只好跳過瞜
設定安裝的路徑,可以自由選擇位置,並完成安裝。
再來就是安裝Qt-vsaddin tool,就是下一步就對了....





Step3.開啟VS 2013,確認是否有多一個Qt的Tool選項。

Step4.設定Qt的路徑,點選QT VS TOOLS中的Qt options。

先Add一個新路徑,名稱會在路徑設定完成後自己產生,將新路徑設定為自己安裝Qt資料夾中的msvc2013_64。

Step5.建立專案,選擇Qt中的Qt GUI Application



按下一步後,要選取常用的三個,分別是Core、GUI、Widgets
最後按下完成就可以了。




Step6.最後找尋自己安裝Qt資料夾中的msvc2013_64\bin,複製所有的dll檔,並放入到專案中的Release&Debug後,執行可跳出platform,即代表順利將環境建置完成摟~~

2019年9月22日 星期日

OpenCV clone、copyTo(影像複製)

在影像處理中,往往需要將特定的影像進行複製,在這邊介紹一下我在OpenCV常用的影像複製,主要有兩種方式,第一種為clone、第二種為copyTo,因為運算子 = 有時會被混淆,所以我會一起做說明。



運算子=


利用運算子=的方式,使得DogImg 與DogCloneImg 都會指到同一個記憶體空間,所以在使用
完threshold,兩張影像都會一起被處理,其概念就像是c++中,指標指到一樣地方,故會一起被修改。


////-------------------分隔線
#include <cstdio>
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main(){

Mat DogImg = imread("dog1.jpg", CV_LOAD_IMAGE_GRAYSCALE);
Mat DogCloneImg;

DogCloneImg = DogImg;
threshold(DogImg, DogImg, 150, 255, CV_THRESH_BINARY);

imshow("DogImg", DogImg);
imshow("DogCloneImg", DogCloneImg);

waitKey(0);
return 0;

}
/////////-------------

結果圖:





複製方法一(clone)


因為是使用clone,所以DogImg 的記憶體位置被改變了,且複製了CatImg的影像數據
,並因為記憶體位置被改變,所以DogCloneImg 影像數據沒有被改變。


////-------------------分隔線
#include <cstdio>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main(){
Mat DogImg = imread("dog1.jpg", CV_LOAD_IMAGE_COLOR);
Mat CatImg = imread("cat1.jpg", CV_LOAD_IMAGE_COLOR);
Mat DogCloneImg;

resize(DogImg, DogImg, Size(CatImg.cols, CatImg.rows));

DogCloneImg = DogImg;
DogImg = CatImg.clone();

imshow("DogImg", DogImg);
imshow("CatImg", CatImg);
imshow("DogCloneImg", DogCloneImg);

waitKey(0);
return 0;
}
/////////-------------

結果圖:

複製方法二(copyTo與原尺寸相同情況下 )

因為是使用copyto且DogImg與CatImg尺寸相同 ,所以不會改變DogImg 的記憶體位置,並
因為DogImg 與DogCloneImg 記憶體位置相同,所以在將CatImg的影像數據複製給DogImg 時
DogCloneImg也一起被改變。

////-------------------分隔線
#include <cstdio>
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main(){

Mat DogImg = imread("dog1.jpg", CV_LOAD_IMAGE_COLOR);
Mat CatImg = imread("cat1.jpg", CV_LOAD_IMAGE_COLOR);
Mat DogCloneImg;

resize(DogImg, DogImg, Size(CatImg.cols, CatImg.rows));

DogCloneImg = DogImg;
CatImg.copyTo(DogImg);

imshow("DogImg", DogImg);
imshow("CatImg", CatImg);
imshow("DogCloneImg", DogCloneImg);

waitKey(0);
return 0;

}

/////////-------------
結果圖:

複製方法二(copyTo與原尺寸不相同情況下 )

因為是使用copyto且拿掉resize,所以DogImg與CatImg尺寸不相同 ,因此雖然一樣是使用copyto,但會先建立新的記憶體位置,所以結果會與clone複製一樣,而DogCloneImg尺寸會保持原大小。

////-------------------分隔線
#include <cstdio>
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main(){

Mat DogImg = imread("dog1.jpg", CV_LOAD_IMAGE_COLOR);
Mat CatImg = imread("cat1.jpg", CV_LOAD_IMAGE_COLOR);
Mat DogCloneImg;

//resize(DogImg, DogImg, Size(CatImg.cols, CatImg.rows));

DogCloneImg = DogImg;
CatImg.copyTo(DogImg);

imshow("DogImg", DogImg);
imshow("CatImg", CatImg);
imshow("DogCloneImg", DogCloneImg);

waitKey(0);
return 0;

}

/////////-------------
結果圖:

2019年3月16日 星期六

OpenCV 影像讀取(imread)、影像建立(Mat & create)、影像顯示(imshow)、影像儲存(imwrite)

OpenCV 影像讀取(imread)、影像建立(Mat & create)、影像顯示(imshow)、影像儲存(imwrite)


影像屬性

首先,我們要先了解一張影像的屬性,常有的屬性如下:

1.影像長(cols)與寬(rows)
2.影像顏色,一般分成彩色(color image)與灰階(gray image),因彩色至少有三種顏色組成,所以為三通道,灰階則為一通道組成。
3.影像深度(depth),代表影像要使用多大的空間去儲存影像資料,如CV_8U,代表空間是8位元無符號。
4.影像類型(Type),除了第三點提到的深度資訊外,再額外包含通道數,看你是彩色還是單色。如果是8位元無符號的三通道影像,就是CV_8UC3。


影像讀取(imread)

Mat imread( const String& filename, int flags = IMREAD_COLOR );

const String& filename:代表讀檔的檔名
int flags = IMREAD_COLOR:代表影像讀進來的顏色模式,最常選擇彩色、灰階、原影像等模式
然後回傳矩陣。


影像建立(Mat & create)

Mat(int rows, int cols, int type, const Scalar& s);
一般我都是用這個建立影像空間

int rows:代表欲建立影像之高度
int cols:代表欲建立影像之寬度
int type:代表欲建立影像類型
const Scalar& s:代表欲建立影像亮度值

Mat::create(int rows,int cols, int type )
主要用於重新分配影像大小、空間與通道用

int rows:代表欲建立影像之高度
int cols:代表欲建立影像之寬度
int type:代表欲建立影像類型

例如:
Mat SampleImg(50, 50, CV_32FC3, Scalar(255, 0, 0));
SampleImg.create(100, 60, CV_8UC(3));//改成100 x 60  CV_8U 三通道的影像

影像顯示(imshow)

void imshow(const String& winname, InputArray mat);

const String:代表顯示的視窗名稱
InputArray mat:代表欲顯示的影像矩陣


影像儲存(imwrite)

bool imwrite( const String& filename, InputArray img,const std::vector);

const String& filename:代表欲儲存的影像檔名(包含影像格式,常見的有bmp、png、jpg等..)
InputArray img:代表欲儲存的影像矩陣
const std::vector():代表設定的影像品質,可寫可不寫,如果想設定可參考下方寫法

vector<int> ImageQuality;
ImageQuality.push_back(CV_IMWRITE_JPEG_QUALITY);
ImageQuality.push_back(80); //自行設定壓縮品質,如果沒有額外設定,預設就是95
imwrite("MyGreenImg.bmp", GreenImg, ImageQuality);


///////------------------------------------------我是分隔線------------------

#include <cstdio>
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main(){

Mat GreenImg(500, 500, CV_8UC3, Scalar(0, 255, 0));//建立一張影像500 x 500 三通道 全綠色

imshow("GreenImg", GreenImg);
imwrite("MyGreenImg.bmp", GreenImg);//儲存一張影像,名字為MyGreenImg,副檔名為bmp

Mat LoadGreenImg = imread("MyGreenImg.bmp", CV_LOAD_IMAGE_COLOR);//讀取上面儲存的影像
imshow("LoadGreenImg", LoadGreenImg);

waitKey(0);
return 0;
}

///////------------------------------------------我還是分隔線--------------

跑出來的結果會有兩張影像,分別為自己建立的純綠色影像,另一個則是讀取剛剛儲存的影像並顯示~