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安装
下载地址
Android安装
下载地址
测试图片下载
为了快速体验Demo,可以下载下面的试纸,然后使用“图片选择“模式去体验我们的Demo:
- https://yunchengfile.oss-cn-beijing.aliyuncs.com/others/shecare_45.jpeg
- https://yunchengfile.oss-cn-beijing.aliyuncs.com/others/shecare_0.jpeg
三种识别模式
目前SDK Demo支持三种模式,即“智能扫描”模式、“手动裁剪”模式以及“选择图片”模式,每一种模式都有不同的应用场景;
智能扫描模式
所谓抠图模式,整个体验过程类似于二维码的“扫一扫”,具体的示例Demo的UI如下图,该模式有以下几点特性:

- 对于市面上所有的条形试纸均有效,但是对于非条形(如笔形、卡型等)试纸无效;
- 自动扫描、抠图以及自动出结果;
- 该模式需要用户主动点击“拍照”按钮;
- 注意该模式下需要把整条试纸放入“取景框”内;
- 一旦识别出结果,我们的API会弹出试纸“抠图确认”页面,在该页面我们的Demo可以指示出T线、C线位置,并且如果T、C线的位置如果错误,用户可以手动拖动T、C线进行修改;
- 打开拍照开关,会有“惊喜”;
手动裁剪模式
所谓裁剪模式,即需要用户把试纸的T、C线放入取景框中示意的裁剪框“中央”,并且尽量保证T、C线与水平线“竖直”,具体的示例Demo的UI如下图:

- 模式是解决非条形(如笔形、卡型等)试纸的“杀手锏”;
- 该模式对于条形试纸同样有效;
- 该模式需要用户手动点击“拍照”按钮;
- 一旦识别出结果,我们的API会弹出试纸“抠图确认”页面,在该页面我们的Demo可以指示出T线、C线位置,并且如果T、C线的位置如果错误,用户可以手动拖动T、C线进行修改;
- 该模式需要引导用户T线位于C线的左边;
选择图片模式
所谓“选择图片模式”,即对于用户已经拍照的、并且放入图片库的试纸进行识别,点击下图红色字体的指示按钮即可体验:

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

- 该确认框会指示出T、C线的位置;
- 用户如果认为T、C线不正确,可以拖动这2条线;
- 如果试纸左右识别颠倒,用户可以点击“水平翻转”进行翻转修改;
- 如果用户对于试纸识别结果不满意,可以点击“返回”按钮返回;
调用流程
我们的API,整体来说有两个接口:
- 一个接口的输入是视频流,然后对于视频流中的图片进行逐帧分析,然后给出结果,我们的“抠图模式”就是调用的这个API;
- 另外一个接口输入是图片,然后我们的API对于该图片进行分析,我们的“裁剪模式”、“选择图片模式”就是调用的这个API;
流程图
输入参数是图片的API流程

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

流程描述
- 调用方输入视频流给SDK;
- SDK对于视频流进行解析成一帧帧图片;
- 选取其中的一帧图片,判断图片中是否有试纸图片,如果无则继续分析下一帧图片;
- 如果有,则弹出试纸识别结果确认框;
- 在该弹出对话框上,用户可以根据识别结果进行T、C线拖动调整,翻转调整;
- 如果用户在页面点击了“返回”按钮,则试纸结果确认对话框消失,调用重新进行下一张图片的识别;
- 如果用户在页面点击了“确认”按钮,则试纸结果确认对话框消失,调用方获取返回的试纸T、C线位置,抠出的图片内容以及LH Level等;
1 | 几点约定: |
模块结构图

模块描述
我们的试纸识别技术基于深度学习框架,共分为三个模块:
- 试纸识别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 | /** 设置 SDK 环境。默认是测试环境 YCSEnvironmentDebug */ |
主函数调用
1 | /*! @brief 获取一张图片的扫描和分析结果 |
返回值
1 | /** 最终返回的抠图结果 */ |
错误描述
1 | /// 试纸抠图、分析错误码 |
调用场景代码片段
“抠图模式”代码调用片段
1 | // “视频抠图模式” 下,调用方需实现以下三个代理方法 |
“裁剪模式”代码调用片段
1 | // 调用方使用以下方法处理 “照片手动裁剪” 模式下的 SDK 调用 |
“图片选择模式”代码调用片段
1 | // 调用方使用以下方法实现 “图片选择模式” 下的 SDK 方法调用 |
“试纸结果识别确认框”样式代码调用片段
1 | // SDK 配置参数,详细说明见 SDK Config 或 framework 头文件 |
如何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 | //网络配置需要在初始化sdk之前 |
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
65paperAnalysiserClient.analysisBitmap(fileBitmap, new IBitmapAnalysisEvent() {
public void showProgressDialog() {
//显示加载框
LogUtils.d("Show Loading Dialog");
that.showProgressDialog(new View.OnClickListener() {
public void onClick(View v) {
//停止网络请求
paperAnalysiserClient.stopShowProgressDialog();
}
});
}
public void dismissProgressDialog() {
//隐藏加载框
LogUtils.d("Hide Loading Dialog");
}
public void cancel() {
LogUtils.d("取消试纸结果确认");
//试纸结果确认框取消
ToastUtils.show(getContext(), AiCode.getMessage(AiCode.CODE_201));
}
public void save(PaperResult paperResult) {
LogUtils.d("保存试纸分析结果:\n"+paperResult.toString());
//试纸结果确认框确认 显示试纸结果
if (paperResult.getErrNo() != 0) {
ToastUtils.show(getContext(), AiCode.getMessage(paperResult.getErrNo()));
}
}
public boolean analysisSuccess(PaperCoordinatesData paperCoordinatesData, Bitmap originSquareBitmap, Bitmap clipPaperBitmap) {
LogUtils.d("试纸自动抠图成功");
return false;
}
public void analysisError(PaperCoordinatesData paperCoordinatesData, String errorResult, int code) {
LogUtils.d("试纸自动抠图出错 code:" + code + " errorResult:" + errorResult);
//试纸抠图失败结果
ToastUtils.show(getContext(), AiCode.getMessage(code));
}
public void saasAnalysisError(String errorResult, int code) {
LogUtils.d("试纸分析出错 code:" + code + " errorResult:" + errorResult);
//试纸saas分析失败
ToastUtils.show(getContext(), AiCode.getMessage(code));
}
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
15Camera.PreviewCallback mPreviewCallback = new Camera.PreviewCallback() {
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
30Camera.PreviewCallback mPreviewCallback = new Camera.PreviewCallback() {
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() {
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;
}
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 | public class PaperAnalysiserClient { |
SDK Config
1 |
|
错误描述
1 | //图片错误 |
调用场景代码片段
“抠图模式”代码调用片段
1 | "智能扫描"代码调用片段 |
“裁剪模式”代码调用片段
1 | "手动裁剪模式"代码调用片段 |
“图片选择模式”代码调用片段
1 | "图片选择模式"代码调用片段 |
“试纸结果识别确认框”样式代码调用片段
1 | //定制试纸Ui显示 |
如何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正常运行,具体设置如下图示:
