有一張圖片如下所示:
Kimi上有一個功能,就是解析圖片內容,給出回答:
這樣可以用於拍照向AI提問的場景,我自己也有這方面的需求,因此動手實踐了一下。
自己動手實現的效果如下所示:
那麼自己如何實現呢?
可以透過新增一個OCR的功能來實現。中文圖片文字識別也就是OCR效果比較好的是百度開源的PaddleOCR,之前介紹過PaddleOCR的.NET繫結PaddleSharp,見這篇文章:C#使用PaddleOCR進行圖片文字識別。
之前使用PaddleOCR的時候,我已經在電腦上安裝了一個虛擬環境,因為需求比較簡單,就是將圖片進行文字識別之後返回文字就行了,因此今天玩個不一樣的,不用.NET繫結,直接呼叫Python指令碼就好了。
那麼現在拆解任務就是:
C#如何呼叫Python指令碼?
那麼就先來試一下,最簡單的呼叫,呼叫Python指令碼輸出一個Hello:
print("Hello")
可以使用 System.Diagnostics.Process
類來啟動一個外部程序來執行Python指令碼:
string pythonScriptPath = @"D:\學習路線\人工智慧\圖片文字識別\test.py"; // 替換為你的Python指令碼路徑
string pythonExecutablePath = @"D:\SoftWare\Anaconda\envs\paddle_env\python.exe"; // 替換為你的Python直譯器路徑
ProcessStartInfo start = new ProcessStartInfo();
start.FileName = pythonExecutablePath;
start.Arguments =$"{pythonScriptPath}";
start.UseShellExecute = false;
start.RedirectStandardOutput = true;
start.RedirectStandardError = true;
start.CreateNoWindow = true;
using (Process process = Process.Start(start))
{
using (System.IO.StreamReader reader = process.StandardOutput)
{
string result = reader.ReadToEnd();
MessageBox.Show(result);
}
using (System.IO.StreamReader errorReader = process.StandardError)
{
string errors = errorReader.ReadToEnd();
if (!string.IsNullOrEmpty(errors))
{
MessageBox.Show("Errors: " + errors);
}
}
}
其中ProcessStartInfo各屬性的解釋如下:
- FileName:
- 含義:指定要啟動的程式或文件的名稱。
- 示例:在這裡,
pythonExecutablePath
是 Python 直譯器的路徑,如"C:\path\to\python.exe"
。
- Arguments:
- 含義:指定傳遞給要啟動程式的命令列引數。
- 示例:在這裡,
pythonScriptPath
是你要執行的 Python 指令碼的路徑,如"C:\path\to\hello.py"
。
- UseShellExecute:
- 含義:指定是否使用作業系統 shell 來啟動程序。如果設定為
false
,則直接啟動程序;如果設定為true
,則透過 shell 啟動程序。 - 示例:在這裡,設定為
false
,表示不使用 shell 啟動程序,而是直接啟動 Python 直譯器。
- 含義:指定是否使用作業系統 shell 來啟動程序。如果設定為
- RedirectStandardOutput:
- 含義:指定是否將子程序的標準輸出重定向到
Process.StandardOutput
流。 - 示例:在這裡,設定為
true
,表示將 Python 指令碼的輸出重定向到Process.StandardOutput
,以便你可以讀取它。
- 含義:指定是否將子程序的標準輸出重定向到
- RedirectStandardError:
- 含義:指定是否將子程序的標準錯誤輸出重定向到
Process.StandardError
流。 - 示例:在這裡,設定為
true
,表示將 Python 指令碼的錯誤輸出重定向到Process.StandardError
,以便你可以讀取它。
- 含義:指定是否將子程序的標準錯誤輸出重定向到
- CreateNoWindow:
- 含義:指定是否在新視窗中啟動程序。如果設定為
true
,則不會建立新視窗;如果設定為false
,則會建立新視窗。 - 示例:在這裡,設定為
true
,表示不建立新視窗,即在後臺執行 Python 指令碼。
- 含義:指定是否在新視窗中啟動程序。如果設定為
現在檢視一下執行效果:
獲取到了Python指令碼輸出的值。
那麼再拆解一下任務,我們需要在命令列中傳入一個引數,該如何實現呢?
import sys
# 檢查是否有引數傳遞
if len(sys.argv) > 1:
n = sys.argv[1]
print(f"hello {n}")
else:
print("請提供一個引數")
只需修改下圖中,這兩處地方即可:
現在再來試下效果:
成功在命令列中傳入了一個引數。
那麼現在我們的準備工作已經做好了。
PaddleOCR的使用指令碼如下:
import sys
import logging
from paddleocr import PaddleOCR, draw_ocr
# Paddleocr目前支援的多語言語種可以透過修改lang引數進行切換
# 例如`ch`, `en`, `fr`, `german`, `korean`, `japan`
# 檢查是否有引數傳遞
if len(sys.argv) > 1:
imagePath = sys.argv[1]
else:
print("請提供一個引數")
# 配置日誌級別為 WARNING,這樣 DEBUG 和 INFO 級別的日誌資訊將被隱藏
logging.basicConfig(level=logging.WARNING)
# 建立一個自定義的日誌處理器,將日誌輸出到 NullHandler(不輸出)
class NullHandler(logging.Handler):
def emit(self, record):
pass
# 獲取 PaddleOCR 的日誌記錄器
ppocr_logger = logging.getLogger('ppocr')
# 移除所有預設的日誌處理器
for handler in ppocr_logger.handlers[:]:
ppocr_logger.removeHandler(handler)
# 新增自定義的 NullHandler
ppocr_logger.addHandler(NullHandler())
ocr = PaddleOCR(use_angle_cls=True, lang="ch") # need to run only once to download and load model into memory
img_path = imagePath
result = ocr.ocr(img_path, cls=True)
for idx in range(len(result)):
res = result[idx]
for line in res:
print(line[1][0])
在vs code中執行效果如下所示:
現在在WPF應用中呼叫結果如下:
現在圖片文字識別的部分已經搞定了。
現在就需要與大語言模型結合起來了,就是將識別出來的文字,丟給大語言模型。
可以這樣寫:
public async IAsyncEnumerable<string> GetAIResponse4(string question, string imagePath)
{
string pythonScriptPath = @"D:\學習路線\人工智慧\圖片文字識別\test.py"; // 替換為你的Python指令碼路徑
string pythonExecutablePath = @"D:\SoftWare\Anaconda\envs\paddle_env\python.exe"; // 替換為你的Python直譯器路徑
string arguments = imagePath; // 替換為你要傳遞的引數
ProcessStartInfo start = new ProcessStartInfo();
start.FileName = pythonExecutablePath;
start.Arguments = $"{pythonScriptPath} {arguments}";
start.UseShellExecute = false;
start.RedirectStandardOutput = true;
start.RedirectStandardError = true;
start.CreateNoWindow = true;
string result = "";
using (Process process = Process.Start(start))
{
using (System.IO.StreamReader reader = process.StandardOutput)
{
result = reader.ReadToEnd();
}
using (System.IO.StreamReader errorReader = process.StandardError)
{
string errors = errorReader.ReadToEnd();
if (!string.IsNullOrEmpty(errors))
{
MessageBox.Show("Errors: " + errors);
}
}
}
string skPrompt = """
獲取到的圖片內容:{{$PictureContent}}。
根據獲取到的資訊回答問題:{{$Question}}。
""";
await foreach (var str in _kernel.InvokePromptStreamingAsync(skPrompt, new() { ["PictureContent"] = result, ["Question"] = question }))
{
yield return str.ToString();
}
}
就可以實現如下的效果了:
全部程式碼可在https://github.com/Ming-jiayou/SimpleRAG看到。