孕橙智能试纸识别算法之客户端API说明

SDK简介

  • Shecare智能试纸识别SAAS SDK主要功能利用手机端的拍照功能对于目前市面上的排卵、早孕试纸进行自动拍照识别;该技术基于深度学习,在背景相对复杂的情况下识别宽松准确率可以达到90%,几乎接近人眼的识别水平;该SDK对于孕橙试纸识别宽松准确率超过95%,超过人眼的识别水平;
  • 由于采用了深度学习技术,Shecare的试纸识别算法可以在半人工干预的情况下实现自我反馈、学习以及更新,随着学习样本库的增加,Shecare 智能试纸识别SAAS SDK识别的准确率会越来越高;
  • 另外为了增强用户体验,Shecare 智能试纸识别SAAS SDK对于iOS和Android端采取native的集成方式,为了让集成快速落地,我们的API设计遵循接口简单化、高度配置化原则;
  • Shecare 智能试纸识别SAAS SDK目前支持市面上所有的条形、笔形以及卡型等排卵、早孕试纸;

    宽松准确率
    所谓的宽松准确率,指的是:

    a.试纸的阴性和阳性的判断不能错误;
    b.在满足条件1的前提下,读值相差一个档位不算错误;
    
  • 孕橙APP的排卵试纸和早孕试纸模块同样调用了该SDK,如果想快速了解该SDK,可以扫描下方的二维码下载APP进行最终的用户体验;

SDK更新日志

iOS

更新时间 版本 更新内容
2019-12-27 1.0 初始版本

Android

更新时间 版本 更新内容
2019-12-27 1.0 初始版本
2020-03-02 1.5.2 添加试纸单周期分析
添加停止试纸分析网络请求
2020-04-02 1.5.3 优化sdk识别速度
添加判断手机性能是否满足sdk要求
2020-06-24 1.5.5 优化sdk抠图速度

支持平台

目前孕橙智能试纸识别算法SDK支持以下平台:

  • iOS

  • Android

体验DEMO安装

为了让开发者快速上手,孕橙智能试纸识别算法SDK提供了体验Demo,并且该Demo提供完整的源码下载;详细的源码下载链接见下面的SDK代码说明

iOS安装

下载地址

iOS fir.im

Android安装

下载地址

Android fir.im

测试图片下载

为了快速体验Demo,可以下载下面的试纸,然后使用“图片选择“模式去体验我们的Demo:

三种识别模式

目前SDK Demo支持三种模式,即“智能扫描”模式、“手动裁剪”模式以及“选择图片”模式,每一种模式都有不同的应用场景;

智能扫描模式

所谓抠图模式,整个体验过程类似于二维码的“扫一扫”,具体的示例Demo的UI如下图,该模式有以下几点特性:

  1. 对于市面上所有的条形试纸均有效,但是对于非条形(如笔形、卡型等)试纸无效;
  2. 自动扫描、抠图以及自动出结果;
  3. 该模式需要用户主动点击“拍照”按钮;
  4. 注意该模式下需要把整条试纸放入“取景框”内;
  5. 一旦识别出结果,我们的API会弹出试纸“抠图确认”页面,在该页面我们的Demo可以指示出T线、C线位置,并且如果T、C线的位置如果错误,用户可以手动拖动T、C线进行修改;
  6. 打开拍照开关,会有“惊喜”;

手动裁剪模式

所谓裁剪模式,即需要用户把试纸的T、C线放入取景框中示意的裁剪框“中央”,并且尽量保证T、C线与水平线“竖直”,具体的示例Demo的UI如下图:

  1. 模式是解决非条形(如笔形、卡型等)试纸的“杀手锏”;
  2. 该模式对于条形试纸同样有效;
  3. 该模式需要用户手动点击“拍照”按钮;
  4. 一旦识别出结果,我们的API会弹出试纸“抠图确认”页面,在该页面我们的Demo可以指示出T线、C线位置,并且如果T、C线的位置如果错误,用户可以手动拖动T、C线进行修改;
  5. 该模式需要引导用户T线位于C线的左边;

选择图片模式

所谓“选择图片模式”,即对于用户已经拍照的、并且放入图片库的试纸进行识别,点击下图红色字体的指示按钮即可体验:

  1. 在该模式下,一旦选择的图片被识别,我们的API会弹出试纸“抠图确认”页面,在该页面我们的Demo可以指示出T线、C线位置,并且如果T、C线的位置如果错误,用户可以手动拖动T、C线进行修改;
  2. 如果这种模式下找不到试纸,我们的API可以弹出“裁剪图片”功能,让用户通过拖动、旋转、移动以及缩放把试纸区域拖动到裁剪区域,我们的APP基于此进行识别;

试纸识别结果确认框

在所有的模式下,只要API识别出试纸,我们的API都会弹出试纸识别确认框,如下图:

  1. 该确认框会指示出T、C线的位置;
  2. 用户如果认为T、C线不正确,可以拖动这2条线;
  3. 如果试纸左右识别颠倒,用户可以点击“水平翻转”进行翻转修改;
  4. 如果用户对于试纸识别结果不满意,可以点击“返回”按钮返回;

调用流程

我们的API,整体来说有两个接口:

  1. 一个接口的输入是视频流,然后对于视频流中的图片进行逐帧分析,然后给出结果,我们的“抠图模式”就是调用的这个API;
  2. 另外一个接口输入是图片,然后我们的API对于该图片进行分析,我们的“裁剪模式”、“选择图片模式”就是调用的这个API;

流程图

输入参数是图片的API流程

流程描述
  1. 调用方输入一张图片给SDK;
  2. SDK判断图片中是否有试纸图片,如果无则返回错误;
  3. 如果有,则弹出试纸识别结果确认框
  4. 在该弹出对话框上,用户可以根据识别结果进行T、C线拖动调整,翻转调整;
  5. 如果用户在页面点击了“返回”按钮,则试纸结果确认对话框消失,调用重新进行下一张图片的识别;
  6. 如果用户在页面点击了“确认”按钮,则试纸结果确认对话框消失,调用方获取返回的试纸T、C线位置,抠出的图片内容以及LH Level等;
1
2
3
4
5
6
7
几点约定:

1.对于输入的图片,我们要求必须是正方形,对于传入的长方形的图片,偶尔可能成功,但是不建议使用;
2.返回的T、C线为算法经过算法计算一个均值线;
3.虽然目前试纸API返回了试纸T、C线的4条边界“直线”,但是这4条“直线”边界本身可能不是一个严格的边界,有些看起来是白色区域,但是可能被包含在T、C线区域内,具体原因如下:
a.试纸的T、C边界本身就不是一个boundry clear的边界;
b.有时候肉眼无法看出的细微的“红色”,但是在计算机图形学里面可能就是一个色值,然后从T、C线的角度来说,可能会被计算在内;

输入参数是视频流的API流程图

流程描述
  1. 调用方输入视频流给SDK;
  2. SDK对于视频流进行解析成一帧帧图片;
  3. 选取其中的一帧图片,判断图片中是否有试纸图片,如果无则继续分析下一帧图片;
  4. 如果有,则弹出试纸识别结果确认框
  5. 在该弹出对话框上,用户可以根据识别结果进行T、C线拖动调整,翻转调整;
  6. 如果用户在页面点击了“返回”按钮,则试纸结果确认对话框消失,调用重新进行下一张图片的识别;
  7. 如果用户在页面点击了“确认”按钮,则试纸结果确认对话框消失,调用方获取返回的试纸T、C线位置,抠出的图片内容以及LH Level等;
1
2
3
4
5
几点约定:
1.返回的T、C线为算法经过算法计算一个均值线;
2.虽然目前试纸API返回了试纸T、C线的4条边界“直线”,但是这4条“直线”边界本身可能不是一个严格的边界,有些看起来是白色区域,但是可能被包含在T、C线区域内,具体原因如下:
a.试纸的T、C边界本身就不是一个boundry clear的边界;
b.有时候肉眼无法看出的细微的“红色”,但是在计算机图形学里面可能就是一个色值,然后从T、C线的角度来说,可能会被计算在内;

模块结构图

模块描述

我们的试纸识别技术基于深度学习框架,共分为三个模块:

  • 试纸识别API;
  • 错误反馈机制;
  • Shecare Cloud模块;

试纸识别API

整个识别过程共分为二个阶段,即一阶段的抠图(从图片或者视频流中抠出试纸条)以及二阶段的颜色识别;目前一阶段在本地实现,而二阶段是在云端实现,阶段二依赖于阶段一:也即是如果阶段一不成功,则不会进行阶段二;由于仅仅阶段二涉及到网络传输,并且这个仅仅传输抠出的非常小的试纸条,所以一次成功的识别网络传输的代价非常小;

错误反馈机制

由于我们的试纸识别技术基于深度学习,所以我们会对于一些错误的case进行搜集,譬如在试纸确认页面,如果用户拖动了T、C线;再譬如在试纸确认页面用户选择了返回等,这些反馈对于我们算法的优化提供数据支撑;

Shecare Cloud模块

该模块对于T、C线的值进行识别,并且对于用户输入的反馈进行处理,基于深度学习技术半自动化的优化我们的算法;

定制试纸识别结果确认框

对话框说明

  • 一旦智能试纸识别API从图片中识别出试纸,则弹出定制的试纸识别结果确认框(如下图),该确认框有如下作用:

  • 试纸识别结果识别是一件从“医疗级”来说非常严肃的事情,所以经过用户的double confirm是必要的;
  • 如果用户发现C、T线识别的位置不对,可以手动拖动进行修改,给试纸的结果识别带来一个潜在的“补救”机会;
  • 如果用户发现识别出试纸“左右颠倒”,可以手动拖动进行修改,给试纸的结果识别带来一个潜在的“补救”机会;
  • 调用方不需要处理确认页的“拖动”以及“点击”事件,更专注于自己的“业务逻辑”;
  • 由于该确认框由SDK弹出,所以为了保证该弹出框能够适配调用方不同UI风格,该确认框高度灵活配置;

相关配置项

如下图,定制项如下:

  • 确认界面的语言以及对应的描述;
  • “确认”以及“返回”按钮的颜色以及图片;
  • 抠出图片宽度外延的像素点;

试纸识别值的映射

目前Shecare智能试纸识别SAAS SDK对于排卵试纸的值返回共有8个值:0、5、10、15、20、25、45、65,在这8个值中:

  • 0代表无;
  • 25属于阴阳的分界,并且25属于阳;
  • 65最高值,属于强阳;
  • 其他的值由调用者根据自己的业务进行灵活的映射和配置;

SDK代码说明

iOS

代码示例

Demo 地址:https://e.coding.net/yuncheng/shecarepasdkdemo-ios/master.git (SDK 在 Demo 里)

函数说明

SDK Config

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
/** 设置 SDK 环境。默认是测试环境 YCSEnvironmentDebug */
@property (assign, nonatomic) YCSEnvironment environment;
/** 应用授权相关的 appID */
@property (nonatomic, copy) NSString *appID;
/** 应用授权相关的 appSecret */
@property (nonatomic, copy) NSString *appSecret;
/** 固定值,不需要设置 */
@property (nonatomic, copy) NSString *sessionId;
/** 用户身份标识符,全局唯一且同一个用户固定不变 */
@property (nonatomic, copy) NSString *userID;
/** 结果确认页 UI 主色调 */
@property (nonatomic, strong) UIColor *mainColor;
/** 结果确认页 字体 主色调 */
@property (nonatomic, strong) UIColor *textColor;
/** 结果确认页 “取消” 按钮图片 */
@property (nonatomic, strong) UIImage *cancelImg;
/** 结果确认页 “确认” 按钮图片 */
@property (nonatomic, strong) UIImage *confirmImg;
/** 结果确认页 T 滑块图片 */
@property (nonatomic, strong) UIImage *tImage;
/** 结果确认页 C 滑块图片 */
@property (nonatomic, strong) UIImage *cImage;
/** 结果确认页 标题 */
@property (nonatomic, copy) NSString *title;
/** 结果确认页 “翻转” 按钮标题 */
@property (nonatomic, copy) NSString *flipTitle;
/** 结果确认页 说明 */
@property (nonatomic, copy) NSString *comment;
/** 算法返回的图片是否需要 “外扩”,默认否 */
@property (nonatomic, assign, getter=isExtended) ``BOOL` `extended;
/** “外扩” 的像素(仅在 extended=true 时有效) */
@property (nonatomic, assign) NSInteger pixelOfExtended;
/** 扫描超时时长。默认为 15s,最少为 1s(仅在视频流扫描模式下有效) */
@property (nonatomic, assign) CFTimeInterval timeIntervalOfScan;
/** “连续成功” 的最少次数,默认为 5 次。为保证扫描结果准确性,建议采用 “连续多次扫描成功才认为整个流程成功” 的判定方法。(仅在视频流扫描模式下有效) */
@property (nonatomic, assign) NSUInteger numberOfSuccess;
/** 相同错误码连续出现的次数,默认为 3 次。算法可能在短时间内返回很多错误码,为保证用户体验,建议设置此值。用于控制 “相同错误码连续出现若干次,才在 UI 上提示用户” (仅在视频流扫描模式下有效) */
@property (nonatomic, assign) NSUInteger numberOfErrors;
/** 0未知;1 相册;2 拍照 */
@property (nonatomic, assign) SCImageType source;

主函数调用

1
2
3
4
5
6
7
8
/*! @brief 获取一张图片的扫描和分析结果
* @param image 用于扫描的试纸照片
* @param completion 完成回调,用于返回扫描和分析的结果
*/
-(``void``)getScanResultFromImage:(UIImage *)image completion:(``void` `(^)(SCPaperAnalysiserResult *result))completion;
/*! @brief 结束扫描和分析流程
*/
-(``void``)closeSession:(SCPaperAnalysiserResult * _Nullable)result;

返回值

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
/** 最终返回的抠图结果 */
@property (nonatomic, strong, readonly) UIImage *finalImage;
/** 算法返回的 四边形 坐标点 */
@property (nonatomic, strong, nullable) NSArray *maskPoints;
/** 算法返回的 错误码 */
@property (nonatomic, assign) SCErrorCode errorCode;
/** 算法返回的 模糊度 */
@property (nonatomic, assign) CGFloat blurExtent;
/** 算法返回的 C 线位置 */
@property (nonatomic, assign) CGFloat cPosition;
/** 算法返回的 T 线位置 */
@property (nonatomic, assign) CGFloat tPosition;
/** 算法返回的 T 线区域左边缘位置 */
@property (nonatomic, assign) CGFloat lhTlineLeft;
/** 算法返回的 T 线区域右边缘位置 */
@property (nonatomic, assign) CGFloat lhTlineRight;
/** 算法返回的 C 线区域左边缘位置 */
@property (nonatomic, assign) CGFloat lhClineLeft;
/** 算法返回的 C 线区域右边缘位置 */
@property (nonatomic, assign) CGFloat lhClineRight;
/** 用户确认的 C 线位置 */
@property (nonatomic, assign) CGFloat newCPosition;
/** 用户确认的 T 线位置 */
@property (nonatomic, assign) CGFloat newTPosition;
/** 是否翻转 */
@property (nonatomic, assign) ``BOOL` `flipped;
/** 算法返回的试纸分析结果 */
@property (nonatomic, assign) NSInteger lhResult;
/** 用户确认的试纸结果 */
@property (nonatomic, assign) NSInteger newLHResult;
/** 算法返回的 Ratio 值 */
@property (nonatomic, assign) CGFloat lhRatio;
/** 算法返回的试纸类型 */
@property (nonatomic, assign) SCPaperType paperType;
/** 试纸测试时间 */
@property (nonatomic, strong) NSDate *lhTime;
/** 试纸图片来源:1 相册;2 拍照 */
@property (nonatomic, assign) SCImageType source;

错误描述

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
/// 试纸抠图、分析错误码
typedef NS_ENUM(NSInteger, SCErrorCode) {

/// 抠图成功、分析成功,流程内没有错误产生;
SCErrorCodeNoError = 0,

/// SDK 校验失败或无效
SCErrorCodeSDKError = -2,
/// 未知错误(默认值);
SCErrorCodeUnknownError = -1,

/* 阶段 1 中间过程错误码(或单张照片扫描结果的错误码) */
/// 没有找到试纸;
SCErrorCodeNoPaper = 1,
/// 距离过远;
SCErrorCodeTooFar = 2,
/// 背景过脏;
SCErrorCodeTooDirty = 3,
/// 距离过近;
SCErrorCodeTooClose = 4,
/// 试纸不全;
SCErrorCodeNotCompleted = 5,
/// 神经网络加载错误;
SCErrorCodeHedNetError = 6,
/// 背景内不止一张试纸;
SCErrorCodeTooManyPapers = 7,
/// 曝光不足;
SCErrorCodeUnderExposure = 8,
/// 曝光过度;
SCErrorCodeExposed = 9,
/// 试纸局部曝光过度;
SCErrorCodePartlyExposed = 10,
/// 画面模糊;
SCErrorCodeBlurred = 11,
/// 曝光不足(OpenCV 二次确认);
SCErrorCodeUnderExposure2 = 12,

/* 阶段 1 结束错误码 */
/// 抠图失败,视频流扫描超时
SCErrorCodeVideoOutofDate = 17,

/* 阶段 2 错误码 */
/// 抠图成功,但是分析失败
SCErrorCodeGetValueError = 101,

/* 阶段 3 错误码 */
/// 抠图成功,分析成功,但是用户取消确认抠图和分析结果
SCErrorCodeUserCanceled = 201,
/// 抠图成功,分析成功,但未检测到参考线,请确认试纸有参考线显示
SCErrorCodeNoCLine = 202,
};

调用场景代码片段

“抠图模式”代码调用片段

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
// “视频抠图模式” 下,调用方需实现以下三个代理方法
-(void)analysiser:(SCPaperAnalysiser *)analysiser didGetVideoResult:(SCPaperAnalysiserResult *)result bkImage:(UIImage *)bkImage {
self.curOutputImage = bkImage;
// 根据 SDK 返回的 “试纸区域” 顶点坐标,绘制 “试纸区域” 到页面 UI
if (result.maskPoints.count == 0) {
// 没有返回顶点坐标时,刷新页面
[self setupMaskLayer:[self nullPoints]];
} else {
[self addMaskLayerWithPoints:result.maskPoints onImage:bkImage];
}
}

-(void)analysiser:(SCPaperAnalysiser *)analysiser didFinishVideoScan:(SCPaperAnalysiserResult *)result {
// SDK 回调,后续处理取决于业务需求,Demo 仅为示例
// SDK 成功返回抠图和分析结果
if (result.error.code == SCErrorCodeNoError) {
NSLog(@"Result: %@", result);

dispatch_async(dispatch_get_main_queue(), ^{
YCLHResultViewController *vc = [[YCLHResultViewController alloc] initWithResult:result];
[self.navigationController pushViewController:vc animated:YES];
});
} else if (SCErrorCodeVideoOutofDate == result.error.code) {
// 扫描超时,切换到手动裁剪模式
dispatch_async(dispatch_get_main_queue(), ^{
[self setupUIWithScanning:false];
self.switchBtn.selected = false;
});
} else {
// 其他错误
[self analysiser:analysiser didGetVideoError:[self analysiser:analysiser messageWithError:result.error]];
}
}

// SDK 返回错误信息时的处理
-(void)analysiser:(SCPaperAnalysiser *)analysiser didGetVideoError:(NSString *)errInfo {
dispatch_async(dispatch_get_main_queue(), ^{
if (errInfo.length > 0) {
[self.errorLbl setTitle:errInfo forState:UIControlStateNormal];
self.errorLbl.alpha = 1.0f;
} else {
self.errorLbl.alpha = 0.0f;
}
});
}

“裁剪模式”代码调用片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 调用方使用以下方法处理 “照片手动裁剪” 模式下的 SDK 调用
// 传入的 image 是 “经过了用户拖动、缩放、旋转等操作后的当前页面截图”
// points 是 “裁剪框” 的左上角和右下角顶点坐标
[[SCPaperAnalysiser shared] getScanResultFromSnapShot:image points:points completion:^(SCPaperAnalysiserResult * _Nonnull result) {
result.source = SCImageSourceAlbum;
dispatch_async(dispatch_get_main_queue(), ^{
YCLHResultViewController *vc = [[YCLHResultViewController alloc] initWithResult:result];
[self.navigationController pushViewController:vc animated:YES];
});
}];

// 调用方使用以下方法处理 “视频手动裁剪” 模式下的 SDK 调用,类似 “照片手动裁剪” 模式
// 传入的 image 是 “用户点击拍照时的那一帧”
// points 是 “裁剪框” 的左上角和右下角顶点坐标
[[SCPaperAnalysiser shared] getScanResultFromSnapShot:self.curOutputImage points:[self defaultPoints:self.curOutputImage] completion:^(SCPaperAnalysiserResult * _Nonnull result) {
result.source = SCImageSourceCamera;
dispatch_async(dispatch_get_main_queue(), ^{
YCLHResultViewController *vc = [[YCLHResultViewController alloc] initWithResult:result];
[self.navigationController pushViewController:vc animated:YES];
});
}];

“图片选择模式”代码调用片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 调用方使用以下方法实现 “图片选择模式” 下的 SDK 方法调用
SCPaperAnalysiserConfiguration.shared.source = SCImageSourceAlbum;
YCWeakSelf(self)
[[SCPaperAnalysiser shared] getScanResultFromImage:oriImage completion:^(SCPaperAnalysiserResult * _Nonnull result) {
YCStrongSelf(self)
// SDK 回调,后续处理取决于业务需求,Demo 仅为示例
if (result.error.code == SCErrorCodeNoError || result.error.code == SCErrorCodeNoCLine) {
NSLog(@"Result: %@", result);

dispatch_async(dispatch_get_main_queue(), ^{
YCLHResultViewController *vc = [[YCLHResultViewController alloc] initWithResult:result];
[self.navigationController pushViewController:vc animated:YES];
});
} else {
NSLog(@"Error: %@", result.error);

}
}];

“试纸结果识别确认框”样式代码调用片段

1
2
3
4
5
6
7
8
9
10
11
12
13
// SDK 配置参数,详细说明见 SDK Config 或 framework 头文件
SCPaperAnalysiserConfiguration *scConfig = [SCPaperAnalysiserConfiguration shared];
scConfig.extended = true;
scConfig.pixelOfExtended = 2;
scConfig.cancelImg = [UIImage imageNamed:@"test_paper_return"];
scConfig.confirmImg = [UIImage imageNamed:@"test_paper_confirm"];
scConfig.title = @"拍照结果";
scConfig.flipTitle = @"水平翻转";
scConfig.comment = @"请拖动TC线以确认TC线标注正确,水平翻转保证MAX箭头指向左边";
scConfig.cImage = [UIImage imageNamed:@"c_line_slices"];
scConfig.tImage = [UIImage imageNamed:@"t_line_slices"];
scConfig.mainColor = [UIColor colorWithHex:0xFF7486];
scConfig.textColor = [UIColor colorWithHex:0x444444];

如何debug

SDK 的日志会和应用里的 NSLog(Swift 对应 print)输出到一起,分别以 Shecare(Debug)、Shecare(Info)、Shecare(Error) 开头表示不同的日志等级。需要 Debug 时,把 Shecare 开头的日志上传即可。

Android

代码示例

Demo 地址:https://e.coding.net/yuncheng/ScPaperAnalysiserDemo_Android.git

ScPaperAnalysiserDemo_Android

一.引入试纸sdk库

1
api 'com.ikangtai.papersdk:ScPaperAnalysiserLib:1.5.5'

二.添加依赖库地址

1
maven { url 'https://dl.bintray.com/ikangtaijcenter123/ikangtai' }

三.使用方法

1
2
3
4
5
6
7
8
9
//网络配置需要在初始化sdk之前
//使用测试网络
Config.setTestServer(true);
//网络超时时间
Config.setNetTimeOut(30);

//判断手机性能是否满足sdk要求
1.SupportDeviceUtil.isSupport(getContext(),AppConstant.appId, AppConstant.appSecret)#第一次校验不准
2.application初始化中调用SupportDeviceUtil.isSupport(getContext(),AppConstant.appId, AppConstant.appSecret),实际判断处调用SupportDeviceUtil.isSupport(getContext())

1.初始化

1
2
//初始化sdk
paperAnalysiserClient = new PaperAnalysiserClient(getContext(), appId, appSecret, "xyl1@qq.com");

2.常规配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* log默认路径/data/Android/pageName/files/Documents/log.txt,可以通过LogUtils.getLogFilePath()获取
* 自定义log文件有两种方式,设置一次即可
* 1. {@link Config.Builder#logWriter(Writer)}
* 2. {@link Config.Builder#logFilePath(String)}
*/
String logFilePath = new File(FileUtil.createRootPath(getContext()), "log_test.txt").getAbsolutePath();
BufferedWriter logWriter = null;
try {
logWriter = new BufferedWriter(new FileWriter(logFilePath, true), 2048);
} catch (IOException e) {
e.printStackTrace();
}
//试纸识别sdk相关配置
Config config = new Config.Builder().pixelOfdExtended(true).paperMinHeight(PxDxUtil.dip2px(getContext(), 20)).uiOption(uiOption).logWriter(logWriter).build();
paperAnalysiserClient = new PaperAnalysiserClient(getContext(), appId, appSecret, "xyl1@qq.com",config);

3.UI配置

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
//定制试纸Ui显示
/**
* 标题
*/
String titleText = getContext().getString(com.ikangtai.papersdk.R.string.paper_result_dialog_title);
/**
* 标题颜色
*/
int titleTextColor = getContext().getResources().getColor(com.ikangtai.papersdk.R.color.color_444444);
/**
* 标尺线
*/
int tagLineImageResId = com.ikangtai.papersdk.R.drawable.paper_line;
/**
* t滑块图标
*/
int tLineResId = com.ikangtai.papersdk.R.drawable.test_paper_t_line;
/**
* c滑块图标
*/
int cLineResId = com.ikangtai.papersdk.R.drawable.test_paper_c_line;
/**
* 水平翻转文字
*/
String flipText = getContext().getString(com.ikangtai.papersdk.R.string.paper_result_dialog_flip);
/**
* 水平翻转文字颜色
*/
int flipTextColor = getContext().getResources().getColor(com.ikangtai.papersdk.R.color.color_67A3FF);
/**
* 提示文字
*/
String hintText = getContext().getString(com.ikangtai.papersdk.R.string.paper_result_dialog_hit);
/**
* 提示文字颜色
*/
int hintTextColor = getContext().getResources().getColor(com.ikangtai.papersdk.R.color.color_444444);
/**
* 返回按钮
*/
int backResId = com.ikangtai.papersdk.R.drawable.test_paper_return;
/**
* 确认按钮
*/
int confirmResId = com.ikangtai.papersdk.R.drawable.test_paper_confirm;
/**
* 返回按钮文字颜色
*/
int backButtonTextColor = getContext().getResources().getColor(com.ikangtai.papersdk.R.color.color_444444);
/**
* 确认按钮文字颜色
*/
int confirmButtonTextColor = getContext().getResources().getColor(com.ikangtai.papersdk.R.color.color_444444);
/**
* 显示底部按钮
*/
boolean visibleBottomButton = false;
UiOption uiOption = new UiOption.Builder()
.titleText(titleText)
.tagLineImageResId(tagLineImageResId)
.titleTextColor(titleTextColor)
.tLineResId(tLineResId)
.cLineResId(cLineResId)
.flipText(flipText)
.flipTextColor(flipTextColor)
.hintText(hintText)
.hintTextColor(hintTextColor)
.backResId(backResId)
.confirmResId(confirmResId)
.backButtonTextColor(backButtonTextColor)
.confirmButtonTextColor(confirmButtonTextColor)
.visibleBottomButton(visibleBottomButton)
.build();
//试纸识别sdk相关配置
Config config = new Config.Builder().pixelOfdExtended(true).margin(50).uiOption(uiOption).netTimeOutRetryCount(1).build();
paperAnalysiserClient.init(config);

4.调用识别试纸图片

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
paperAnalysiserClient.analysisBitmap(fileBitmap, new IBitmapAnalysisEvent() {
@Override
public void showProgressDialog() {
//显示加载框
LogUtils.d("Show Loading Dialog");
that.showProgressDialog(new View.OnClickListener() {

@Override
public void onClick(View v) {
//停止网络请求
paperAnalysiserClient.stopShowProgressDialog();
}
});
}

@Override
public void dismissProgressDialog() {
//隐藏加载框
LogUtils.d("Hide Loading Dialog");
}

@Override
public void cancel() {
LogUtils.d("取消试纸结果确认");
//试纸结果确认框取消
ToastUtils.show(getContext(), AiCode.getMessage(AiCode.CODE_201));
}

@Override
public void save(PaperResult paperResult) {
LogUtils.d("保存试纸分析结果:\n"+paperResult.toString());
//试纸结果确认框确认 显示试纸结果
if (paperResult.getErrNo() != 0) {
ToastUtils.show(getContext(), AiCode.getMessage(paperResult.getErrNo()));
}

}

@Override
public boolean analysisSuccess(PaperCoordinatesData paperCoordinatesData, Bitmap originSquareBitmap, Bitmap clipPaperBitmap) {
LogUtils.d("试纸自动抠图成功");
return false;
}

@Override
public void analysisError(PaperCoordinatesData paperCoordinatesData, String errorResult, int code) {
LogUtils.d("试纸自动抠图出错 code:" + code + " errorResult:" + errorResult);
//试纸抠图失败结果
ToastUtils.show(getContext(), AiCode.getMessage(code));

}

@Override
public void saasAnalysisError(String errorResult, int code) {
LogUtils.d("试纸分析出错 code:" + code + " errorResult:" + errorResult);
//试纸saas分析失败
ToastUtils.show(getContext(), AiCode.getMessage(code));

}
@Override
public void paperResultDialogShow(PaperResultDialog paperResultDialog) {
paperResultDialog.getHintTv().setGravity(Gravity.LEFT);
paperResultDialog.setSampleResId(R.drawable.confirm_sample_pic_lh);
}
});

5.识别视频流
TextureView视频预览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Camera.PreviewCallback mPreviewCallback = new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(final byte[] data, final Camera camera) {
if (paperAnalysiserClient.isObtainPreviewFrame()) {
return;
}
startTime = System.currentTimeMillis();
//视频上半部分正方形图片
//Bitmap originSquareBitmap= TensorFlowTools.convertFrameToBitmap(data, camera, surfaceView.getWidth(), surfaceView.getWidth(), TensorFlowTools.getDegree(getActivity()));
//Bitmap originSquareBitmap = TensorFlowTools.convertFrameToBitmap(data, camera, TensorFlowTools.getDegree(getActivity()));
Bitmap originSquareBitmap = ImageUtil.topCropBitmap(textureView.getBitmap());
paperAnalysiserClient.analysisCameraData(originSquareBitmap);
}
};
cameraUtil.initCamera(getActivity(), textureView, mPreviewCallback);

SurfaceView视频预览

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
Camera.PreviewCallback mPreviewCallback = new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(final byte[] data, final Camera camera) {
if (paperAnalysiserClient.isObtainPreviewFrame()) {
return;
}
startTime = System.currentTimeMillis();
//视频上半部分正方形图片
Bitmap originSquareBitmap = TensorFlowTools.convertFrameToBitmap(data, camera, TensorFlowTools.getDegree(getActivity()));
paperAnalysiserClient.analysisCameraData(originSquareBitmap);
}
};
cameraUtil.initCamera(getActivity(), surfaceView, mPreviewCallback);
//需要在analysisSuccess和analysisResult回调方法进行坐标转换
ICameraAnalysisEvent iCameraAnalysisEvent = new ICameraAnalysisEvent() {
@Override
public boolean analysisSuccess(PaperCoordinatesData paperCoordinatesData, Bitmap originSquareBitmap, Bitmap clipPaperBitmap) {
....
PaperCoordinatesData newPaperCoordinatesData = TensorFlowTools.convertPointToScreen(cameraUtil.getCurrentCamera(), surfaceView.getWidth(), surfaceView.getHeight(), paperCoordinatesData);
smartPaperMeasureContainerLayout.showAutoSmartPaperMeasure(newPaperCoordinatesData, originSquareBitmap);
return false;
}

@Override
public void analysisResult(PaperCoordinatesData paperCoordinatesData) {
....
PaperCoordinatesData newPaperCoordintaesData = TensorFlowTools.convertPointToScreen(cameraUtil.getCurrentCamera(), surfaceView.getWidth(), surfaceView.getHeight(), paperCoordinatesData);
smartPaperMeasureContainerLayout.showAutoSmartPaperMeasure(newPaperCoordintaesData, null);
}
}

7.调用完成释放资源

1
paperAnalysiserClient.closeSession();

8.混淆代码过滤

1
2
3
4
5
-dontwarn  com.ikangtai.papersdk.**
-keep class com.ikangtai.papersdk.** {*;}
-keepclasseswithmembernames class *{
native <methods>;
}

函数说明

主函数调用

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
public class PaperAnalysiserClient {
/**
* @param context
* @param appId appId
* @param appSecret appSecret
* @param unionId 用户唯一id
*/
public PaperAnalysiserClient(Context context, String appId, String appSecret, String unionId);
public PaperAnalysiserClient(Context context, String appId, String appSecret, String unionId,Config config);
/**
* 初始化
* @param config
*/
public void init(Config config);
/**
* 识别试纸图片并弹出试纸识别结果弹框
* @param fileBitmap 试纸图片bitmap
* @param event 识别结果回调
*/
public void analysisBitmap(final Bitmap fileBitmap, final IBitmapAnalysisEvent event);
/**
* 处理Camera 视频流
* 需要配置Config.analysisSuccessCount,默认三次成功即显示试纸识别结果
*/
public void analysisCameraData(final Bitmap originSquareBitmap);
/**
* 从视频拍照调用手动裁剪模式
* @param bitmap
* @param upLeftPoint 左上角坐标
* @param rightBottomPoint 右下角坐标
* @param event 回调
*/
public void analysisClipBitmapFromCamera(Bitmap bitmap, Point upLeftPoint, Point rightBottomPoint, final IBaseAnalysisEvent event);
/**
* 从相册选择调用手动裁剪模式
* @param bitmap
* @param upLeftPoint 左上角坐标
* @param rightBottomPoint 右下角坐标
* @param event 回调
*/
public void analysisClipBitmapFromPhoto(Bitmap bitmap, Point upLeftPoint, Point rightBottomPoint, final IBaseAnalysisEvent event);
/**
* 手动修改lhValue值
*/
public void updatePaperValue(int paperValue) ;
/**
* 试纸单周期分析算法
* @param papers 单周期papers
* @param event 回调
*/
public void paperCycleAnalysis(ArrayList<PaperCycleAnalysisReq.Paper> papers, final ICycleAnalysisResultEvent event);
/**
* 停止loading加载
*/
public void stopShowProgressDialog() ;
/**
* 完成流程关闭Session
*/
public void closeSession();
}

SDK Config

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
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
 
//试纸sdk配置
public class Config {
/**
* debug模式控制台打印日志
*/
private static boolean debug = true;
/**
* 使用测试服
*/
private static boolean testServer = false;
/**
* 模糊阈值
*/
private int blurLimitValue = 25;
/**
* 同一个错误码连续出现2次
*/
private int analysisErrorCount = 2;

/**
* 试纸存储文件夹路径
*/
private String filePath;

/**
* 日志存储路径
*/
private String logFilePath;
/**
* 是否外扩
*/
private boolean pixelOfdExtended;
/**
* 外扩像素
*/
private int margin;
/**
* 弹框ui配置
*/
private UiOption uiOption;
/**
* 网络请求超时时间
*/
private static int netTimeOut = 10;
/**
* 日志文件句柄
*/
private Writer logWriter;
}

//弹框Ui配置
public class UiOption {
/**
* 标题
*/
private String titleText;
/**
* 标题颜色
*/
private int titleTextColor;
/**
* 标尺线
*/
private int tagLineImageResId;
/**
* t滑块图标
*/
private int tLineResId;
/**
* c滑块图标
*/
private int cLineResId;
/**
* 水平翻转文字
*/
private String flipText;
/**
* 水平翻转文字颜色
*/
private int flipTextColor;
/**
* 提示文字
*/
private String hintText;
/**
* 提示文字颜色
*/
private int hintTextColor;
/**
* 返回图片
*/
private int backResId;
/**
* 确认图片
*/
private int confirmResId;
/**
* tc线宽度
*/
private float tcLineWidth;
/**
* 返回按钮背景
*/
private int backButtonBgResId;
/**
* 确认按钮背景
*/
private int confirmButtonBgResId;
/**
* 返回按钮文字
*/
private String backButtonText;
/**
* 确认按钮文字
*/
private String confirmButtonText;
/**
* 显示底部按钮
*/
private boolean visibleBottomButton;
/**
* 返回按钮文字颜色
*/
private int backButtonTextColor;
/**
* 确认按钮文字颜色
*/
private int confirmButtonTextColor;
}


//基类回调
public interface IBaseAnalysisEvent {
/**
* 显示加载框
*/
void showProgressDialog();

/**
* 隐藏加载框
*/
void dismissProgressDialog();

/**
* 试纸结果弹框取消
*/
void cancel();

/**
* 试纸结果弹框保存
*
* @param paperResult 最终确认保存数据
*/
void save(PaperResult paperResult);

/**
* SaaS识别错误
* @param errorResult 错误描述
* @param code 错误码
*/
void saasAnalysisError(String errorResult, int code);

/**
* 试纸结果确认弹框
* @param PaperResultDialog
*
*/
void paperResultDialogShow(PaperResultDialog var1);

}
//试纸抠图回调
public interface IBitmapAnalysisEvent extends IBaseAnalysisEvent{

/**
* 试纸抠图成功返回结果
*
* @param paperCoordinatesData 抠图结果数据,包括坐标点、模糊值
* @param originSquareBitmap 试纸正方形bitmap
* @param clipPaperBitmap 抠图试纸条
*/
boolean analysisSuccess(PaperCoordinatesData paperCoordinatesData, Bitmap originSquareBitmap, Bitmap clipPaperBitmap);

/**
* 试纸抠图错误
* @param paperCoordinatesData 抠图结果数据,包括坐标点、模糊值可能为空
* @param errorResult 错误描述
* @param code 错误码
*/
void analysisError(PaperCoordinatesData paperCoordinatesData,String errorResult, int code);

}
//视频分析回调
public interface ICameraAnalysisEvent extends IBitmapAnalysisEvent {


/**
* AI分析过程中产生的结果
*
* @param paperCoordinatesData
*/
void analysisResult(PaperCoordinatesData paperCoordinatesData);

/**
* AI分析15s结束
*
* @param originSquareBitmap 结束时原始正方形
* @param code
*/
void analysisEnd(Bitmap originSquareBitmap, int code, String errorResult);

/**
* AI分析取消
*
* @param originSquareBitmap 取消时原始正方形
* @param code
*/
void analysisCancel(Bitmap originSquareBitmap, int code, String errorResult);
}

//试纸识别结果
public class PaperCoordinatesData {
/**
* 错误码
*/
private int code;
/**
* 模糊值
*/
private double blurValue;
/**
* 上下左右坐标
*/
private Point point1;
private Point point2;
private Point point3;
private Point point4;
/**
* 抠图结果mat对象,可转换成试纸条bitmap
*/
private Mat imageL;
}
//试纸识别弹框保存数据
public class PaperResult {
/**
* id
*/
private String paperId;
/**
* 试纸时间
*/
private String paperTime;
/**
* 错误码
*/
private int errNo;
/**
* 错误描述
*/
private String errMsg;
/**
* tc线比例
*/
private double tLinePos;
private double cLinePos;
/**
* 试纸类型
*/
private int paperType;
/**
* 失败结果
*/
private double paperValue;
/**
* ratio结果
*/
private double ratioValue;
/**
* 是否翻转
*/
private boolean flip;
/**
* 模糊度
*/
private double blurValue;
/**
* 试纸抠图结果
*/
private Bitmap paperBitmap;
/**
* 试纸抠图不加margin结果
*/
private Bitmap noMarginBitmap;
/**
* tc线左右边界位置
*/
private double clineLeft;
private double clineRight;
private double tlineLeft;
private double tlineRight;
}

错误描述

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
//图片错误
codes.put(CODE_IMAGE_ERROR, "图片错误");
//未知错误
codes.put(CODE_ERROR, "未知错误");
//抠图成功
codes.put(CODE_0, "抠图成功");
//没有试纸;错误提示文案:没有找到试纸
codes.put(CODE_1, "没有找到试纸");
//距离过远;错误提示文案:距离过远,请调整拍摄距离
codes.put(CODE_2, "距离过远,请调整拍摄距离");
// 背景过脏;错误提示文案:背景有干扰,请在浅色纯背景下拍摄
codes.put(CODE_3, "背景有干扰,请在浅色纯背景下拍摄");
// 距离过近;错误提示文案:距离过近,请调整拍摄距离
codes.put(CODE_4, "距离过近,请调整拍摄距离");
//有残缺;错误提示文案:试纸不全,请保持全部试纸处在取景框内
codes.put(CODE_5, "试纸不全,请保持全部试纸处在取景框内");
// 神经网络处理错误;错误提示文案:算法处理中,请稍候
codes.put(CODE_6, "算法处理中,请稍候");
// 两张试纸;错误提示文案:一次只能拍摄一张试纸
codes.put(CODE_7, "一次只能拍摄一张试纸");
// 曝光不足;错误提示文案:光线太暗,请调整光线或打开闪光灯后拍摄
codes.put(CODE_8, "光线太暗,请调整光线或打开闪光灯后拍摄");
// 曝光过度;错误提示文案:光线太强,请调整光线或关掉闪光灯后拍摄
codes.put(CODE_9, "光线太强,请调整光线或关掉闪光灯后拍摄");
// 试纸局部过度曝光;错误提示文案:局部光线太强,请调整光线或关掉闪光灯后拍摄
codes.put(CODE_10, "局部光线太强,请调整光线或关掉闪光灯后拍摄");
// 画面模糊;错误提示文案:画面模糊,请保持手机稳定或重新对焦
codes.put(CODE_11, "画面模糊,请保持手机稳定或重新对焦");
// 曝光不足;错误提示文案:光线太暗,请调整光线或打开闪光灯后拍摄
codes.put(CODE_12, "光线太暗,请调整光线或打开闪光灯后拍摄");
//视频流扫描超时
codes.put(CODE_17, "视频流扫描超时");
/// 手动裁剪失败,请检查传入的图片和坐标点
codes.put(CODE_50, "手动裁剪失败,请检查传入的图片和坐标点");
//抠图成功,分析成功,但是用户取消确认抠图和分析结果。
codes.put(CODE_201, "用户取消");
//抠图成功,分析成功,但未检测到参考线,请确认试纸有参考线显示
codes.put(CODE_202, "未检测到参考线,请确认试纸有参考线显示");
//抠图成功,但是分析失败
codes.put(CODE_101, "试纸分析出错");
//SDK 校验失败或无效
codes.put(CODE_SDK_ERROR, "SDK 校验失败或无效");
//网络错误
codes.put(CODE_NET_ERROR, "网络错误");
codes.put(CODE_501, "缺少相关参数");
codes.put(CODE_503, "频率过高,关入小黑屋");
codes.put(CODE_500, "认证错误");
codes.put(CODE_502, "超时,认证无效");
codes.put(CODE_301, "参数不合法");
codes.put(CODE_302, "分析结果转化json出错");
codes.put(CODE_303, "分析失败,服务返回");
codes.put(CODE_304, "图片base64编解码失败");
codes.put(CODE_510, "分析失败");
codes.put(CODE_511, "试纸无效");

通过AiCode.getMessage(CODE)获取错误描述

调用场景代码片段

“抠图模式”代码调用片段

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
"智能扫描"代码调用片段

/**
* 在Camera 使用TextureView实时预览回调进行识别抠图识别
*/
private Camera.PreviewCallback mPreviewCallback = new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(final byte[] data, final Camera camera) {
if (paperAnalysiserClient.isObtainPreviewFrame()) {
return;
}
//视频上半部分正方形图片
//Bitmap originSquareBitmap = TensorFlowTools.convertFrameToBitmap(data, camera, TensorFlowTools.getDegree(getActivity()));
Bitmap originSquareBitmap = ImageUtil.topCropBitmap(textureView.getBitmap());
paperAnalysiserClient.analysisCameraData(originSquareBitmap);
}
};

cameraUtil.initCamera(getActivity(), textureView, mPreviewCallback);
//抠图结果回调
private ICameraAnalysisEvent iCameraAnalysisEvent = new ICameraAnalysisEvent() {
@Override
public boolean analysisSuccess(PaperCoordinatesData paperCoordinatesData, Bitmap originSquareBitmap, Bitmap clipPaperBitmap) {
if (scanMode == MANUALSMART) {
return false;
}
LogUtils.d("试纸自动抠图成功");
ToastUtils.show(getContext(), "抠图最终结果");
return false;
}

@Override
public void analysisResult(PaperCoordinatesData paperCoordinatesData) {
if (scanMode == MANUALSMART) {
return;
}
LogUtils.d("试纸自动抠图画线");
Log.d("xyl", "抠图耗时 " + (System.currentTimeMillis() - startTime));
ToastUtils.show(getContext(), "抠图中间结果");
}

@Override
public void analysisEnd(Bitmap originSquareBitmap, int code, String errorResult) {
LogUtils.d("试纸抠图结束 code:" + code + " errorResult:" + errorResult);
ToastUtils.show(getContext(), "抠图超时");
scanMode = MANUALSMART;
}

@Override
public void analysisCancel(Bitmap originSquareBitmap, int code, String errorResult) {
LogUtils.d("试纸抠图取消 code:" + code + " errorResult:" + errorResult);
ToastUtils.show(getContext(), "切换抠图模式,取消视频流分析");
}

@Override
public void showProgressDialog() {
LogUtils.d("Show Loading Dialog");
that.showProgressDialog(new View.OnClickListener() {

@Override
public void onClick(View v) {
//停止网络请求
paperAnalysiserClient.stopShowProgressDialog();
}
});
}

@Override
public void dismissProgressDialog() {
LogUtils.d("Hide Loading Dialog");
}

@Override
public void cancel() {
LogUtils.d("取消试纸结果确认");

}

@Override
public void save(PaperResult paperResult) {
LogUtils.d("保存试纸分析结果:\n" + paperResult.toString());
smartPaperMeasureContainerLayout.showAutoSmartPaperMeasure(null, null);
if (paperResult.getErrNo() != 0) {
ToastUtils.show(getContext(), AiCode.getMessage(paperResult.getErrNo()));
}
}

@Override
public void analysisError(PaperCoordinatesData paperCoordinatesData, String errorResult, int code) {
if (scanMode == MANUALSMART) {
return;
}
LogUtils.d("试纸抠图出错 code:" + code + " errorResult:" + errorResult);
LogUtils.d("抠图耗时 " + (System.currentTimeMillis() - startTime));

}

@Override
public void saasAnalysisError(String errorResult, int code) {
LogUtils.d("试纸分析错误 code:" + code + " errorResult:" + errorResult);
ToastUtils.show(getContext(), errorResult + code);

}
@Override
public void paperResultDialogShow(PaperResultDialog paperResultDialog) {

}
};

/**
* 在Camera 使用SurfaceView实时预览回调进行识别抠图识别
*/
Camera.PreviewCallback mPreviewCallback = new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(final byte[] data, final Camera camera) {
if (paperAnalysiserClient.isObtainPreviewFrame()) {
return;
}
startTime = System.currentTimeMillis();
//视频上半部分正方形图片
Bitmap originSquareBitmap = TensorFlowTools.convertFrameToBitmap(data, camera, TensorFlowTools.getDegree(getActivity()));
paperAnalysiserClient.analysisCameraData(originSquareBitmap);
}
};
cameraUtil.initCamera(getActivity(),surfaceView, mPreviewCallback);
//需要在analysisSuccess和analysisResult回调方法进行坐标转换
ICameraAnalysisEvent iCameraAnalysisEvent = new ICameraAnalysisEvent() {
@Override
public boolean analysisSuccess(PaperCoordinatesData paperCoordinatesData, Bitmap originSquareBitmap, Bitmap clipPaperBitmap) {
....
PaperCoordinatesData newPaperCoordinatesData = TensorFlowTools.convertPointToScreen(cameraUtil.getCurrentCamera(), surfaceView.getWidth(), surfaceView.getHeight(), paperCoordinatesData);
smartPaperMeasureContainerLayout.showAutoSmartPaperMeasure(newPaperCoordinatesData, originSquareBitmap);
return false;
}

@Override
public void analysisResult(PaperCoordinatesData paperCoordinatesData) {
....
PaperCoordinatesData newPaperCoordintaesData = TensorFlowTools.convertPointToScreen(cameraUtil.getCurrentCamera(), surfaceView.getWidth(), surfaceView.getHeight(), paperCoordinatesData);
smartPaperMeasureContainerLayout.showAutoSmartPaperMeasure(newPaperCoordintaesData, null);
}
}

paperAnalysiserClient.setCameraDataCallback(iCameraAnalysisEvent);

“裁剪模式”代码调用片段

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
"手动裁剪模式"代码调用片段
//从相册选择抠图失败手动裁剪模式试纸识别
paperAnalysiserClient.analysisClipBitmapFromPhoto(clipBitmap, upLeftPoint, rightBottomPoint, new IBaseAnalysisEvent() {
@Override
public void showProgressDialog() {
LogUtils.d("Show Loading Dialog");
that.showProgressDialog(new View.OnClickListener() {

@Override
public void onClick(View v) {
//停止网络请求
paperAnalysiserClient.stopShowProgressDialog();
}
});
}

@Override
public void dismissProgressDialog() {
LogUtils.d("Hide Loading Dialog");
}

@Override
public void cancel() {
LogUtils.d("取消试纸结果确认");
}

@Override
public void save(PaperResult paperResult) {
LogUtils.d("保存试纸分析结果:\n"+paperResult.toString());
if (paperResult.getErrNo() != 0) {
ToastUtils.show(getContext(), AiCode.getMessage(paperResult.getErrNo()));
}
}

@Override
public void saasAnalysisError(String errorResult, int code) {
LogUtils.d("试纸分析出错 code:" + code + " errorResult:" + errorResult);

}
@Override
public void paperResultDialogShow(PaperResultDialog paperResultDialog) {

}
});


//从视频拍照页面手动裁剪模式试纸识别
paperAnalysiserClient.analysisClipBitmapFromCamera(fileBitmap, upLeftPoint, rightBottomPoint, new IBaseAnalysisEvent() {
@Override
public void showProgressDialog() {
LogUtils.d("Show Loading Dialog");
that.showProgressDialog(new View.OnClickListener() {

@Override
public void onClick(View v) {
//停止网络请求
paperAnalysiserClient.stopShowProgressDialog();
}
});
}

@Override
public void dismissProgressDialog() {
LogUtils.d("Show Loading Dialog");
}

@Override
public void cancel() {
LogUtils.d("取消试纸结果确认");
ToastUtils.show(getContext(), AiCode.getMessage(AiCode.CODE_201));
}

@Override
public void save(PaperResult paperResult) {
LogUtils.d("保存试纸分析结果:\n" + paperResult.toString());
if (paperResult.getErrNo() != 0) {
ToastUtils.show(getContext(), AiCode.getMessage(paperResult.getErrNo()));
}
}

@Override
public void saasAnalysisError(String errorResult, int code) {
LogUtils.d("试纸分析出错 code:" + code + " errorResult:" + errorResult);
ToastUtils.show(getContext(), AiCode.getMessage(code));

}
@Override
public void paperResultDialogShow(PaperResultDialog paperResultDialog) {

}
});

“图片选择模式”代码调用片段

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
"图片选择模式"代码调用片段
paperAnalysiserClient.analysisBitmap(fileBitmap, new IBitmapAnalysisEvent() {
@Override
public void showProgressDialog() {
//显示加载框
LogUtils.d("Show Loading Dialog");
that.showProgressDialog(new View.OnClickListener() {

@Override
public void onClick(View v) {
//停止网络请求
paperAnalysiserClient.stopShowProgressDialog();
}
});
}

@Override
public void dismissProgressDialog() {
//隐藏加载框
LogUtils.d("Hide Loading Dialog");
}

@Override
public void cancel() {
LogUtils.d("取消试纸结果确认");
//试纸结果确认框取消
ToastUtils.show(getContext(), AiCode.getMessage(AiCode.CODE_201));
}

@Override
public void save(PaperResult paperResult) {
LogUtils.d("保存试纸分析结果:\n"+paperResult.toString());
if (paperResult.getErrNo() != 0) {
ToastUtils.show(getContext(), AiCode.getMessage(paperResult.getErrNo()));
}
}

@Override
public boolean analysisSuccess(PaperCoordinatesData paperCoordinatesData, Bitmap originSquareBitmap, Bitmap clipPaperBitmap) {
LogUtils.d("试纸自动抠图成功");
//试纸抠图成功结果
endTime = System.currentTimeMillis();
return false;
}

@Override
public void analysisError(PaperCoordinatesData paperCoordinatesData, String errorResult, int code) {
LogUtils.d("试纸自动抠图出错 code:" + code + " errorResult:" + errorResult);
//试纸抠图失败结果
ToastUtils.show(getContext(), AiCode.getMessage(code));
}

@Override
public void saasAnalysisError(String errorResult, int code) {
LogUtils.d("试纸分析出错 code:" + code + " errorResult:" + errorResult);
//试纸saas分析失败
ToastUtils.show(getContext(), AiCode.getMessage(code));

}
@Override
public void paperResultDialogShow(PaperResultDialog paperResultDialog) {

}
});

“试纸结果识别确认框”样式代码调用片段

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
//定制试纸Ui显示
/**
``* 标题
*/
String titleText = getContext().getString(com.ikangtai.papersdk.R.string.paper_result_dialog_title);
/**
* 标题颜色
*/
int titleTextColor = getContext().getResources().getColor(com.ikangtai.papersdk.R.color.color_444444);
/**
* 标尺线
*/
int tagLineImageResId = com.ikangtai.papersdk.R.drawable.paper_line;
/**
* t滑块图标
*/
int tLineResId = com.ikangtai.papersdk.R.drawable.test_paper_t_line;
/**
* c滑块图标
*/
int cLineResId = com.ikangtai.papersdk.R.drawable.test_paper_c_line;
/**
* 水平翻转文字
*/
String flipText = getContext().getString(com.ikangtai.papersdk.R.string.paper_result_dialog_flip);
/**
* 水平翻转文字颜色
*/
int flipTextColor = getContext().getResources().getColor(com.ikangtai.papersdk.R.color.color_67A3FF);
/**
* 提示文字
*/
String hintText = getContext().getString(com.ikangtai.papersdk.R.string.paper_result_dialog_hit);
/**
* 提示文字颜色
*/
int hintTextColor = getContext().getResources().getColor(com.ikangtai.papersdk.R.color.color_444444);
/**
* 返回图片
*/
int backResId = com.ikangtai.papersdk.R.drawable.test_paper_return;
/**
* 确认图片
*/
int confirmResId = com.ikangtai.papersdk.R.drawable.test_paper_confirm;
/**
* tc线默认值宽度
*/
float tcLineWidth = getContext().getResources().getDimension(com.ikangtai.papersdk.R.dimen.dp_2);
/**
* 返回按钮背景
*/
int backButtonBgResId = com.ikangtai.papersdk.R.drawable.paper_button_drawable;
/**
* 确认按钮背景
*/
int confirmButtonBgResId = com.ikangtai.papersdk.R.drawable.paper_button_drawable;
/**
* 返回按钮文字
*/
String backButtonText = getContext().getString(com.ikangtai.papersdk.R.string.paper_result_back);
/**
* 确认按钮文字
*/
String confirmButtonText = getContext().getString(com.ikangtai.papersdk.R.string.paper_result_confirm);
/**
* 返回按钮文字颜色
*/
int backButtonTextColor = getContext().getResources().getColor(com.ikangtai.papersdk.R.color.color_444444);
/**
* 确认按钮文字颜色
*/
int confirmButtonTextColor = getContext().getResources().getColor(com.ikangtai.papersdk.R.color.color_444444);
/**
* 显示底部按钮
*/
boolean visibleBottomButton = false;

UiOption uiOption = new UiOption.Builder()
.titleText(titleText)
.tagLineImageResId(tagLineImageResId)
.titleTextColor(titleTextColor)
.tLineResId(tLineResId)
.cLineResId(cLineResId)
.flipText(flipText)
.flipTextColor(flipTextColor)
.hintText(hintText)
.hintTextColor(hintTextColor)
.backResId(backResId)
.confirmResId(confirmResId)
.tcLineWidth(tcLineWidth)
.backButtonBgResId(backButtonBgResId)
.backButtonText(backButtonText)
.confirmButtonBgResId(confirmButtonBgResId)
.confirmButtonText(confirmButtonText)
.backButtonTextColor(backButtonTextColor)
.confirmButtonTextColor(confirmButtonTextColor)
.visibleBottomButton(visibleBottomButton)
.build();
//试纸识别sdk相关配置
Config config = new Config.Builder().pixelOfdExtended(true).margin(10).uiOption(uiOption).build();
//初始化sdk
paperAnalysiserClient = new PaperAnalysiserClient(getContext(), AppConstant.appId, AppConstant.appSecret, "xyl1@qq.com",config);

如何debug

-log tag:paper-analysis-log
-log默认路径:/data/Android/pageName/files/Documents/log.txt,通过LogUtils.getLogFilePath()获取到
-可以自定义log 文件写入流new Config.Builder().logWriter(),或者设置log文件路径:new Config.Builder().logFilePath()

术语说明

N/A

Q&A

抠图模式的网络传输量大吗?

在抠图模式下,我们的SDK一次识别仅仅传输很小的试纸条(注意不是全景图,而是抠出的试纸条),仅仅这个这个小小的试纸条会被发送到云端做识别,所以整个网络传输200K都不到,这是一个非常小的网络传输;而视频流仅仅在本地做识别,没有任何的网络传输。

关于代码中unionID的说明

对于调用方来说,调用方负责uniondID的生成,uniodID有以下约束:

  • 每一个用户有一个独立的unionID,该unionID不可与其他用户的unionID重复,并且在该用户的整个生命周期内该unionID应该是始终保持不变;
  • 孕橙要求uniodID字符串最大长度是45,以16进制表示的话最短是40个字节,一个最简单的unionID的生成方式就是SHA1(userID);
  • unionID的作用:
    • 是为了保证shecare SAAS可以全生命周期的追踪这个用户的试纸数据,为用户单周期的试纸解读提供一个基本的数据备份;
    • 一旦用户出现问题,可以更方便以及快速的去debug以定位原因;

如何查看Debug日志?

在我们的体验Demo里面,可以直接看到我们的API输出结果

OpenCV兼容性问题

Android OpenCV版本 :3.2.0

测试环境和正式环境区别

SDK识别调用频率限制

同一IP限制200次/分钟

输入的试纸图片数据是正方形

Shecare SDK识别库是在正方形图片基础上进行的机器学习,抠图识别正方形正确率也会比较高,暂时只保证正方形抠图结果的正确性

输入试纸图片文件大小限制

限制输入图片大小在5M以内

输出抠图结果试纸条分辨率

Android SDK会把宽度限制在1080以内,防止处理大图出现OOM

输出抠图试纸太窄

可以控制SDK输出试纸条外扩像素,实现试纸条有一定的外边距

Android:

1
new Config.Builder().pixelOfdExtended(true).margin(10)

输出抠图结果试纸条会出现透明缺角

在进行试纸外扩的时候,需要使用试纸抠图坐标数据转换,造成了可能会丢失一些边角

试纸在复杂背景抠图结果可能不一致

识别算法也在学习升级过程中,如有出错率比较大的照片,麻烦联系客服提供原始照片,Shecare这边会对算法相应学习

试纸logo类型未定义

联系对接客服,提供相应的LOGO图片素材,进行试纸LOGO打标学习

替换或隐藏试纸结果确认框样例说明

联系对接客服,进行相关配置

替换或隐藏power by shecare说明

联系对接客服,进行相关配置

APP添加信任

正常情况下,由于iOS的APP是由测试证书打包的,所以无法启动,需要改动以下设置才可以保证demo正常运行,具体设置如下图示: