某截图软件 ocr 功能逆向

信息收集

首先使用 Process Monitor 或者 火绒剑 等监视工具查看该软件文件相关的操作,我们可以看到在截图时这个软件在读取以下文件


但是通过互联网搜索我们可以知道这几个文件是关于 qrcode 也就是二维码识别有关的,不要被带偏


而当我们点击识别图像,也就是 ocr 操作的时候,他会读取以下内容


很明显这是一个 sqlite 数据库文件,我们看看里面有什么


里面有个 OcrResult 的字段引起了我的关注,至此信息收集就完毕了

逆向

拖入 ida,搜索 OcrResult 字符串,就两个结果,并且很容易猜出来上面一个是有关 sqlite 创建的
 

图片描述


查看第二个字符串的引用,可以猜测这里是 sqlite 的写入


看该函数的上层,上面 sub_7FF60B4606D0 很明显是一个类的创建,进去看看


看看发现了什么,这正是我们要找的 ocr 功能相关的类
 

图片描述


通过分析该类的 vftable 我们大概可以猜出各个函数的名称


其中有个函数长这样,有很多图片相关的操作引起了我的注意


也就是上图中的 ??_7OcrRunnable@@6B@_02 dq offset ocrFunc? (这个函数我自己重命名了)

然后就是一个一个对函数进行猜测然后重命名,其中里面有一个函数似乎读取了模型,但是我们并搜不到该文件,答案是这个是一个 qt 的资源类型,qt 会将他打包在程序中,需要用到的时候从程序中读取出来,所以我们直接在 readMemFileAll 之后断点就能截取到数据啦


通过询问 ai ,我们可以知道这个文件可能是 ncnn 的格式文件,所以我们直接猜测该程序使用 ncnn 作为框架,进行 ocr 的识别


通过分析,大概的流程长这样,然后还有一些对于图像的操作,找模型识别出来的轮廓、找出文本区域


接着,程序会加载第二个 ai 模型来识别文本,而前一个模型是用来找出哪些区域需要识别文本的,其中 ocr_text_dict 为字典
 

图片描述


ai 模型调用部分和上面一样,最后会输出一堆 float 的数组


最后输出识别的文本比较麻烦,它主要长这样,通过分析,我们能够猜测和跟踪得出源代码差不多长这样

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

#include <algorithm>

#include <array>

#include <cassert>

#include <print>

#include <string>

#include <vector>

#include "Utils.h"

#include <opencv2/opencv.hpp>

#include "net.h"

#define DetectParamPath "../data/det.param"

#define DetectModelPath "../data/det.bin"

#define RecognizeParamPath "../data/rec.param"

#define RecognizeModelPath "../data/rec.bin"

#define CharDictPath "../data/ocr_text_dict.txt"

#define IsDebug 1

namespace Impl {

    /**

     *

     * @param cols

     * @param rows

     * @param outCols

     * @param outRows

     * @param maxSideConstraint 最大边长?

     * @param alignment 对齐大小

     * @return

     */

    uint64_t calcTargetSize(uint32_t cols, uint32_t rows, uint32_t *outCols, uint32_t *outRows, uint32_t maxSideConstraint, uint32_t alignment) {

        unsigned int *v7;

        if (cols < maxSideConstraint && rows < maxSideConstraint) {

            maxSideConstraint = cols;

            if (cols < rows)

                maxSideConstraint = rows;

            if (maxSideConstraint % alignment)

                maxSideConstraint += alignment - maxSideConstraint % alignment;

        }

        if (rows <= cols) {

            *outCols = maxSideConstraint;

            v7       = outRows;

            *outRows = maxSideConstraint * rows / cols;

        } else {

            v7       = outRows;

            *outRows = maxSideConstraint;

            *outCols = maxSideConstraint * cols / rows;

        }

        unsigned int v8 = *outCols % alignment;

        if (v8)

            *outCols = alignment + *outCols - v8;

        unsigned int v10    = *v7 % alignment;

        uint64_t     result = *v7 / alignment;

        if (v10)

            *v7 = alignment + *v7 - v10;

        return result;

    }

    std::vector<cv::Mat> getEachRegion(const cv::Mat &image, const cv::Mat &binaryMask) {

        cv::Mat newBinary = binaryMask.clone();

        cv::resize(newBinary, newBinary, cv::Size(image.cols, image.rows));

        std::vector<std::vector<cv::Point> > contours;

        std::vector<cv::Vec4i>               hierarchy;

        cv::findContours(newBinary, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);

#if IsDebug

        // 调试用

        cv::cvtColor(newBinary, newBinary, cv::COLOR_GRAY2BGR);

         

        cv::Mat              debugImageBinary = newBinary.clone();

        cv::Mat              debugImage       = image.clone();

#endif

        // 存储所有裁剪出的区域

        std::vector<cv::Mat> cropped_images;

        // 获取原始图像的尺寸,用于边界检查

        constexpr int padding_x = 10;

        constexpr int padding_y = 7;

        constexpr int min_area  = 4;

        std::println("find contours: {}", contours.size());

        for (size_t i = 0; i < contours.size(); ++i) {

            // 计算当前轮廓的原始边界框

            cv::Rect original_box = cv::boundingRect(contours[i]);

            // 过滤掉非常小的区域

            if (original_box.area() < min_area) {

                std::println("area too small: {}", original_box.area());

                continue;

            }

            // 计算扩展后的边界框

            cv::Rect expanded_box;

            expanded_box.x      = std::ranges::max(0, original_box.x - padding_x);

            expanded_box.y      = std::ranges::max(0, original_box.y - padding_y);

            expanded_box.width  = std::ranges::min(image.cols - expanded_box.x, original_box.width + padding_x * 2);

            expanded_box.height = std::ranges::min(image.rows - expanded_box.y, original_box.height + padding_y * 2);

            // 检查扩展后的框是否有效 (宽度和高度必须大于0)

            if (expanded_box.width <= 0 || expanded_box.height <= 0) {

                std::println("Skipping contour {} because expanded box is invalid.", i);

                continue;

            }

#if IsDebug

            // 调试线条

            cv::rectangle(debugImage, expanded_box, cv::Scalar(0, 255, 0, 255), 1);       // 绿色框// 绿色框

            cv::rectangle(debugImageBinary, expanded_box, cv::Scalar(0, 255, 0, 255), 1); // 绿色框

#endif

            // 使用扩展后的边界框裁剪原始图像

            cv::Mat cropped_region = image(expanded_box).clone();

            // 将裁剪出的图像添加到列表中

            cropped_images.emplace_back(cropped_region);

        }

#if IsDebug

        cv::imwrite("../output/debugImage.png", debugImage);

        cv::imwrite("../output/debugImageBinary.png", debugImageBinary);

#endif

        return cropped_images;

    }

}

class OCR {

private:

    ncnn::Net                detectNet_;

    ncnn::Net                recognizeNet_;

    std::vector<std::string> charDict_;

public:

    OCR() {

        detectNet_.load_param(DetectParamPath);

        detectNet_.load_model(DetectModelPath);

        recognizeNet_.load_param(RecognizeParamPath);

        recognizeNet_.load_model(RecognizeModelPath);

        charDict_ = Utils::getCharDict(CharDictPath);

    }

    // 检测文本区域,QtImage 的图像是 32bit (0xffRRGGBB)

    std::vector<cv::Mat> findRegions(const cv::Mat &image) {

        assert(image.type() == CV_8UC4 && "image type must be CV_8UC4(BGRA)");

        // 计算目标大小

        unsigned int calcCols, calcRows;

        Impl::calcTargetSize(image.cols, image.rows, &calcCols, &calcRows, 0x3C0u, 0x20u);

        ncnn::Mat out;

        ncnn::Mat in = ncnn::Mat::from_pixels_resize(

            image.data,

            ncnn::Mat::PIXEL_RGBA2RGB,

            image.cols,

            image.rows,

            // 如果不使用计算值则容易糊在一起

            static_cast<int>(calcCols),

            static_cast<int>(calcRows)

        );

        // 归一化

        uint32_t mean_vals[3]; // { 0.485f, 0.456f , 0.406f }

        uint32_t norm_vals[3]; // { 1 / 0.229f / 255.f, 1 / 0.224f / 255.f, 1 / 0.225f / 255.f <-这个对不上}

        mean_vals[0] = 0x3EF851EC;

        mean_vals[1] = 0x3EE978D5;

        mean_vals[2] = 0x3ECFDF3B;

        norm_vals[0] = 0x3C8C4936;

        norm_vals[1] = 0x3C8F6AD8;

        norm_vals[2] = 0x3C8EC7AB;

        in.substract_mean_normalize(reinterpret_cast<float *>(mean_vals), reinterpret_cast<float *>(norm_vals));

        // 推理

        ncnn::Extractor ex = detectNet_.create_extractor();

        ex.input("x", in);

        ex.extract("sigmoid_0.tmp_0", out, 0);

        // 归一化

        uint32_t norms2[3];

        norms2[0] = 0x437F0000;

        norms2[1] = 0x437F0000;

        norms2[2] = 0x437F0000;

        out.substract_mean_normalize(nullptr, reinterpret_cast<float *>(norms2));

        // 转换为灰度图

        cv::Mat binaryMask(out.h, out.w, CV_8U);

        out.to_pixels(binaryMask.data, ncnn::Mat::PIXEL_GRAY);

        // 二值化

        // xmm 00000000000000004053200000000000

        // xmm 0000000000000000406FE00000000000

        cv::threshold(binaryMask, binaryMask, 76.5, 255.0, cv::THRESH_BINARY);

        assert(binaryMask.type() == CV_8UC1 && "mask type must be CV_8UC1");

        return Impl::getEachRegion(image, binaryMask);

    }

    // 识别文本(输入 BGRA 图像,也就是 QtImage)

    std::vector<std::pair<float, std::string> > recognizeText(const cv::Mat &image) {

        assert(image.type() == CV_8UC4 && "image type must be CV_8UC4(BGRA)");

        ncnn::Mat out2;

        ncnn::Mat in2 = ncnn::Mat::from_pixels_resize(

            image.data,

            ncnn::Mat::PIXEL_BGRA2BGR,

            image.cols,

            image.rows,

            static_cast<int>(static_cast<float>(image.cols) / static_cast<float>(image.rows) * 48.0),

            0x30

        );

        constexpr uint32_t mean_vals2[3] { 0x42FF0000, 0x42FF0000, 0x42FF0000 };

        constexpr uint32_t norm_vals2[3] { 0x3C008081, 0x3C008081, 0x3C008081 };

        in2.substract_mean_normalize(reinterpret_cast<const float *>(mean_vals2), reinterpret_cast<const float *>(norm_vals2));

        ncnn::Extractor extractor = recognizeNet_.create_extractor();

        extractor.input("x", in2);

        extractor.extract("softmax_11.tmp_0", out2, 0); // 0x24A

        std::vector<std::pair<float, std::string> > results;

        // 逐行扫描

        int64_t lastIndex = 0;

        for (int hIndex = 0; hIndex < out2.h; ++hIndex) {

            const auto l1 = out2.row(hIndex);

            const auto l2 = out2.row(hIndex + 1);

            const auto maxElementPtr = std::ranges::max_element(l1, l2);

            const auto index         = maxElementPtr - l1;

            if (index > 0 && (hIndex <= 0 || index != lastIndex)) {

                results.emplace_back(*maxElementPtr, charDict_[index - 1]);

            }

            lastIndex = index;

        }

        return results;

    }

};

int main() {

    auto orginal_image = cv::imread("../test1.png", cv::IMREAD_UNCHANGED);

    cv::cvtColor(orginal_image, orginal_image, cv::COLOR_BGR2BGRA);

    auto ocr = OCR();

    for (const auto &element: ocr.findRegions(orginal_image)) {

        std::string t;

        for (auto &[score, text]: ocr.recognizeText(element)) {

            // std::println("Text: {}, Score: {}", text, score);

            t += text;

        }

        std::println("Text: {}", t);

    }

    return 0;

}

识别结果
 

图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值