如何自己動手實現一個圖片解答小助手

mingupupup發表於2024-10-09

有一張圖片如下所示:

Kimi上有一個功能,就是解析圖片內容,給出回答:

image-20241008185201298

這樣可以用於拍照向AI提問的場景,我自己也有這方面的需求,因此動手實踐了一下。

自己動手實現的效果如下所示:

image-20241008185722470

那麼自己如何實現呢?

可以透過新增一個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各屬性的解釋如下:

  1. FileName
    • 含義:指定要啟動的程式或文件的名稱。
    • 示例:在這裡,pythonExecutablePath 是 Python 直譯器的路徑,如 "C:\path\to\python.exe"
  2. Arguments
    • 含義:指定傳遞給要啟動程式的命令列引數。
    • 示例:在這裡,pythonScriptPath 是你要執行的 Python 指令碼的路徑,如 "C:\path\to\hello.py"
  3. UseShellExecute
    • 含義:指定是否使用作業系統 shell 來啟動程序。如果設定為 false,則直接啟動程序;如果設定為 true,則透過 shell 啟動程序。
    • 示例:在這裡,設定為 false,表示不使用 shell 啟動程序,而是直接啟動 Python 直譯器。
  4. RedirectStandardOutput
    • 含義:指定是否將子程序的標準輸出重定向到 Process.StandardOutput 流。
    • 示例:在這裡,設定為 true,表示將 Python 指令碼的輸出重定向到 Process.StandardOutput,以便你可以讀取它。
  5. RedirectStandardError
    • 含義:指定是否將子程序的標準錯誤輸出重定向到 Process.StandardError 流。
    • 示例:在這裡,設定為 true,表示將 Python 指令碼的錯誤輸出重定向到 Process.StandardError,以便你可以讀取它。
  6. CreateNoWindow
    • 含義:指定是否在新視窗中啟動程序。如果設定為 true,則不會建立新視窗;如果設定為 false,則會建立新視窗。
    • 示例:在這裡,設定為 true,表示不建立新視窗,即在後臺執行 Python 指令碼。

現在檢視一下執行效果:

image-20241008191313678

獲取到了Python指令碼輸出的值。

那麼再拆解一下任務,我們需要在命令列中傳入一個引數,該如何實現呢?

import sys

# 檢查是否有引數傳遞
if len(sys.argv) > 1:
    n = sys.argv[1]
    print(f"hello {n}")
else:
    print("請提供一個引數")

只需修改下圖中,這兩處地方即可:

image-20241008191639208

現在再來試下效果:

image-20241008191805671

成功在命令列中傳入了一個引數。

那麼現在我們的準備工作已經做好了。

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中執行效果如下所示:

image-20241008192131148

現在在WPF應用中呼叫結果如下:

image-20241009104512270

現在圖片文字識別的部分已經搞定了。

現在就需要與大語言模型結合起來了,就是將識別出來的文字,丟給大語言模型。

可以這樣寫:

 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();
     }
 }

就可以實現如下的效果了:

image-20241009104837084

全部程式碼可在https://github.com/Ming-jiayou/SimpleRAG看到。

相關文章