有一张图片如下所示:

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:SoftWareAnacondaenvspaddle_envpython.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:pathtopython.exe"。
- Arguments:
- 含义:指定传递给要启动程序的命令行参数。
- 示例:在这里,
pythonScriptPath是你要执行的 Python 脚本的路径,如"C:pathtohello.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:SoftWareAnacondaenvspaddle_envpython.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看到。