智能截图整理工具封面

前面六篇分别讲了 OCR、超分、文搜图、人脸检测、截图分类和处理链。这篇是系列最后一篇,把所有能力整合起来,做一个完整可用的智能截图整理工具。

这个工具的最终形态很简单:用户导入截图,App 自动识别内容、分类、打标签、建索引,用户可以按分类浏览、用文字搜索、查看详情。所有处理都在本地完成,不上传图片。

这篇不讲单个 API 怎么调,而是讲怎么把前面六篇的能力组装成一个完整的产品。从项目结构到页面设计,再到数据持久化和异常兜底,把整个项目从头到尾捋一遍。

一、项目整体结构

我们的项目叫 ScreenshotOrganizer,工程结构如下:

entry/src/main/ets/
├── utils/              # 工具类
│   ├── OcrUtil.ets           # OCR 文字识别
│   ├── ImageEnhanceUtil.ets  # 图像超分
│   ├── FaceDetectUtil.ets    # 人脸检测
│   ├── ScreenshotClassifier.ets # 截图分类
│   └── ImageSearchUtil.ets   # 文搜图
├── pipeline/           # 处理链
│   └── PipelineRunner.ets   # 流水线调度
├── database/           # 数据层
│   ├── DbUtil.ets            # 数据库操作
│   └── ScreenshotDao.ets    # 截图数据访问
├── pages/              # 页面
│   ├── HomePage.ets         # 首页(分类浏览)
│   ├── SearchPage.ets       # 搜索页
│   └── DetailPage.ets       # 详情页
└── models/             # 数据模型
    └── ScreenshotItem.ets   # 截图实体

分层很清晰:utils 是单个能力封装,pipeline 把它们串起来,database 负责持久化,pages 是 UI 层。每一层职责单一,方便维护和测试。

二、核心页面怎么设计

整个应用三个核心页面:

首页:按分类展示截图。顶部是分类标签(全部/聊天/文档/票据/人像),下面是网格布局的截图缩略图。点击某个分类就筛选对应类别的截图。

搜索页:顶部一个搜索框,用户输入自然语言描述,下面显示匹配的截图结果。支持关键词和语义搜索两种模式。

详情页:点击某张截图进入,显示大图、OCR 识别出的文字、分类标签、人脸检测结果。用户可以手动修改分类。

项目架构图

三、关键代码怎么写

下面是首页的核心代码,文件位置在 pages/HomePage.ets

import { ScreenshotDao } from '../database/ScreenshotDao';
import { PipelineRunner } from '../pipeline/PipelineRunner';

@Entry
@Component
struct HomePage {
  @State screenshots: ScreenshotItem[] = [];
  @State filteredScreenshots: ScreenshotItem[] = [];
  @State currentCategory: string = 'all';
  @State isImporting: boolean = false;
  @State importProgress: number = 0;

  // 分类标签
  private categories = [
    { key: 'all', label: '全部' },
    { key: 'chat', label: '聊天' },
    { key: 'document', label: '文档' },
    { key: 'invoice', label: '票据' },
    { key: 'portrait', label: '人像' }
  ];

  aboutToAppear() {
    this.loadScreenshots();
  }

  // 从数据库加载截图列表
  async loadScreenshots() {
    this.screenshots = await ScreenshotDao.getAll();
    this.filterByCategory(this.currentCategory);
  }

  // 按分类筛选
  filterByCategory(category: string) {
    this.currentCategory = category;
    if (category === 'all') {
      this.filteredScreenshots = this.screenshots;
    } else {
      this.filteredScreenshots = this.screenshots.filter(
        item => item.category === category
      );
    }
  }

  // 导入截图并处理
  async onImportImages(images: image.PixelMap[]) {
    this.isImporting = true;
    this.importProgress = 0;

    // 调用处理链
    const results = await PipelineRunner.processImages(
      images.map((p, i) => ({ id: 'img_' + Date.now() + '_' + i, pixelMap: p })),
      (done, total) => {
        this.importProgress = Math.round((done / total) * 100);
      }
    );

    // 保存到数据库
    for (const [id, result] of results) {
      await ScreenshotDao.insert({
        id: id,
        category: result.category,
        ocrText: result.ocrText,
        hasFace: result.hasFace,
        tags: result.tags.join(','),
        vector: result.vector ? Array.from(result.vector) : []
      });
    }

    // 刷新列表
    await this.loadScreenshots();
    this.isImporting = false;
  }

  build() {
    Column() {
      // 分类标签栏
      Row({ space: 12 }) {
        ForEach(this.categories, (cat) => {
          Text(cat.label)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .backgroundColor(this.currentCategory === cat.key
              ? '#007aff' : '#f0f0f0')
            .fontColor(this.currentCategory === cat.key
              ? Color.White : Color.Black)
            .borderRadius(16)
            .onClick(() => this.filterByCategory(cat.key))
        })
      }
      .padding(8)

      // 截图网格
      if (this.isImporting) {
        Column() {
          Text('正在导入处理... ' + this.importProgress + '%')
          Progress({ value: this.importProgress, total: 100 })
        }
        .layoutWeight(1)
        .justifyContent(FlexAlign.Center)
      } else {
        Grid() {
          ForEach(this.filteredScreenshots, (item: ScreenshotItem) => {
            GridItem() {
              Column() {
                Image(item.thumbnailPath)
                  .width('100%')
                  .height(120)
                Text(item.category)
                  .fontSize(12)
                  .fontColor('#666')
              }
            }
          }, (item: ScreenshotItem) => item.id)
        }
        .columnsTemplate('1fr 1fr 1fr')
        .layoutWeight(1)
      }
    }
  }
}

这段代码是整个应用的主页面。导入截图的时候调 PipelineRunner 跑处理链,处理完存数据库,然后刷新列表。分类标签点击就筛选对应的截图。

数据库操作怎么写?文件位置 database/ScreenshotDao.ets

import { relationalStore } from '@kit.ArkData';

export class ScreenshotDao {
  private static store: relationalStore.RdbStore | null = null;

  // 初始化数据库
  static async init(): Promise<void> {
    if (!this.store) {
      this.store = await relationalStore.getRdbStore(
        getContext(this),
        {
          name: 'screenshots.db',
          securityLevel: relationalStore.SecurityLevel.S1
        }
      );
      
      // 建表
      await this.store.executeSql(`
        CREATE TABLE IF NOT EXISTS screenshots (
          id TEXT PRIMARY KEY,
          category TEXT,
          ocr_text TEXT,
          has_face INTEGER,
          tags TEXT,
          vector BLOB,
          created_at INTEGER
        )
      `);
    }
  }

  // 插入截图记录
  static async insert(item: ScreenshotItem): Promise<void> {
    if (!this.store) await this.init();
    
    await this.store!.insert('screenshots', {
      id: item.id,
      category: item.category,
      ocr_text: item.ocrText,
      has_face: item.hasFace ? 1 : 0,
      tags: item.tags,
      vector: item.vector ? new Uint8Array(item.vector.buffer) : null,
      created_at: Date.now()
    });
  }

  // 查询全部
  static async getAll(): Promise<ScreenshotItem[]> {
    if (!this.store) await this.init();
    
    const result = await this.store!.querySql(
      'SELECT * FROM screenshots ORDER BY created_at DESC'
    );
    
    const items: ScreenshotItem[] = [];
    while (result.goToNextRow()) {
      items.push({
        id: result.getValue(result.getColumnIndex('id')),
        category: result.getValue(result.getColumnIndex('category')),
        ocrText: result.getValue(result.getColumnIndex('ocr_text')),
        hasFace: result.getValue(result.getColumnIndex('has_face')) === 1,
        tags: result.getValue(result.getColumnIndex('tags')),
        vector: []
      });
    }
    result.close();
    return items;
  }
}

首页效果图

四、异常兜底怎么设计

一个能上线的工具,异常兜底比主流程更重要。我们做了这几层防护:

1. 处理失败降级:单张截图处理失败了,存成"未分类",不影响其他截图。用户可以手动重新处理。

2. 数据库损坏恢复:如果数据库文件损坏,启动的时候检测到就重建一个空库,不崩溃。老数据丢了总比应用打不开好。

3. 内存不足保护:处理大量图片的时候,监控内存占用。如果内存快满了,暂停处理,提示用户分批导入。

4. 模型加载失败降级:端侧 AI 模型加载失败的情况虽然少,但要有兜底。检测到模型不可用,提示用户"当前设备不支持 AI 识别功能",但基础的图片浏览功能还能用。

五、上线前要做的检查

最后上线前,我们跑了一轮完整的检查清单:

性能检查:单张截图处理时间控制在 3 秒以内,批量 20 张不超过 30 秒。用 DevEco Studio 的 Profiler 看内存和 CPU 占用。

稳定性检查:连续导入 100 张截图不崩溃,内存不泄漏。反复进出页面 50 次,内存稳定不涨。

兼容性检查:在不同尺寸的设备上测试——手机、折叠屏、平板。布局要自适应,不能在大屏上就一块小图。

隐私合规:所有处理都在本地,不上传图片。隐私说明里写清楚数据怎么存、怎么用、怎么删。

做完这些,这个智能截图整理工具才算真正能用了。从第一篇讲 OCR 开始,到这里做一个完整的项目,整个系列就算闭环了。端侧 AI 视觉能力看起来都是单点的小功能,但组合起来就能做出很实用的产品。HarmonyOS 7 把这些能力都做成了系统级 API,开发者不用自己训练模型,直接调接口就行——这才是端侧 AI 真正的价值。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐