0%

Yolov3 是一个非常有效的用于目标检查的One-Stage模型,Backbone使用的是Darknet53,在平时用Keras比较多,所以想将其cfg和weights与训练权重转换成Keras的h5模型,在Github上找寻了一番,发现keras-yolo3这个工具非常有效,故基于此项目做了一定的修改keras_yolov3

修改点:增加了对模型输入尺寸的自定义

原作的input size是未定义的输入尺寸,默认为416,为了使得转换模型更加灵活

  • 将input size作为转换的输入参数
  • input size必须是32的整数倍

如何界定输出的Tensor大小

仔细阅读论文,你会发现,输出的tensor尺寸和输入的tensor尺寸是相关联的,具体如下

  • outputs[0].shape = [input_size / 32, input_size / 32, (num_classes + 5) * 3]
  • outputs[1].shape = [input_size * 2 / 32, input_size * 2 / 32, (num_classes + 5) * 3]
  • outputs[2].shape = [input_size * 4 / 32, input_size * 4 / 32, (num_classes + 5) * 3]

这个数据在后续转换为其他如TFLite的时候非常有用

我们知道,在Android应用开发的过程中,不能在主线程中处理耗时任务,否则会导致UI卡顿,甚至ANR,非常影响用户体验,这就需要把这些耗时的任务,转移到后台去运行。

主线程工作

  1. 更新UI(包括测量和更新View)
  2. 协调用户交互
  3. 接收生命周期事件

以下操作需要在独立的线程里面

  1. 处理图片资源
  2. 访问磁盘,读写文件
  3. 处理网络请求
  4. 任何其他耗时达到几百毫秒的操作
  5. 当应用没有运行在前台,并且要在后台同步数据的时候

后台应用挑战

  1. 后台应用会消耗设备的有限的资源,比如RAM和电池,这个可能会给用户带来不好的体验
  2. 为了最大化电池的使用,Android系统会强制限制后台应用程序
    • Android 6.0,引入了Doze 和 App standby
    • Android 7.0,限制隐式广播和引入Doze-on-the-Go
    • Android 8.0, 限制后台行为,比如定位和Wekelocks
    • Android 9.0, 引入App Standby Buckets

如何选择合适的方案

  1. 任务是否可以延迟,还是需要立刻执行?
  2. 独立工作还是依赖系统环境?
  3. 是否需要精确定时运行?

Google推荐的方案

  • WorkManager
    • Android Libraries (API Level 14+)
  • Forground Service
    • 告知用户正在处理的重要内容
    • 前台服务是可见的
  • AlarmManager
    • 定时任务
  • DownloadManager
    • HTTP长连接

具体可以参考下面的流程

参考

IntentService 是Android SDK中引入的一个非常好用的,有效的单线程后台任务工具

优点

  • 不影响UI响应
  • 不受生命周期影响

限制

  • 不能和UI直接交互
  • 任务顺序运行,一次只能运行一个任务
  • 不能被中断

创建方式

  1. 继承IntentService类
  2. 提供一个默认的构造函数
  3. 实现onHandleIntent(Intent workIntent)来处理后台任务
  4. 在Manifest文件中声明

使用IntentService

  1. 创建一个Intent,将任务数据用Intent发送给IntentService
  2. 调用startService(Intent workIntent)启动IntentService

如何将IntentService处理完的数据返回到Activity

  1. 使用LocalBroadcastManager,任务完成后,通过广播把数据用Intent返回到Activity

Demo

假设用后台服务来获取天气信息

  • WeatherFetchService.java

    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
    public class WeatherFetchService extends IntentService {
    private static final String TAG = "WeatherFetchService";
    private Random random;

    public WeatherFetchService() {
    super(TAG);
    random = new Random();
    }

    @Override
    protected void onHandleIntent(Intent intent) {
    if(intent == null){
    return;
    }
    String city = intent.getStringExtra(Constants.CITY_NAME);
    // Simulate weather information.
    try {
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    // Return result.
    int t = random.nextInt(100);
    intent.setAction(Constants.ACTION_WEATHER_RESULT);
    intent.putExtra(Constants.TEMPERATURE, t);
    LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent);
    }
    }
  • MainActivity.java

    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
    public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private BroadcastReceiver weatherResultReceiver;
    private TextView temperature;
    private EditText city;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    city = findViewById(R.id.city);
    temperature = findViewById(R.id.temperature);
    findViewById(R.id.refresh).setOnClickListener(this);
    findViewById(R.id.clear).setOnClickListener(this);

    weatherResultReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
    if(intent == null){
    return;
    }
    String action = intent.getAction();
    if(Constants.ACTION_WEATHER_RESULT.equals(action)){
    int temp = intent.getIntExtra(Constants.TEMPERATURE,Constants.TEMPERATURE_DEFAULT);
    temperature.setText(String.valueOf(temp));
    }
    }
    };
    IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction(Constants.ACTION_WEATHER_RESULT);

    LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(weatherResultReceiver, intentFilter);
    }

    @Override
    protected void onDestroy() {
    LocalBroadcastManager.getInstance(getApplicationContext()).unregisterReceiver(weatherResultReceiver);
    super.onDestroy();
    }

    @Override
    public void onClick(View v) {
    int id = v.getId();
    switch (id){
    case R.id.refresh:
    String cityName = city.getText().toString();
    Intent intent = new Intent(MainActivity.this, WeatherFetchService.class);
    intent.putExtra(Constants.CITY_NAME, cityName);
    startService(intent);
    break;
    case R.id.clear:
    city.setText("");
    temperature.setText("");
    break;
    default:
    break;
    }
    }
    }

参考

有时候我们需要在AndroidStudio中开发Android系统应用(带系统签名),为了方便开发,我们一般会生成一个debug.keystore,以下是操作步骤

找到系统源码的key

1
cd build/target/product/security/

生成pem文件

1
openssl pkcs8 -in platform.pk8 -inform DER -outform PEM -out shared.priv.pem -nocrypt

生成pk12文件

1
openssl pkcs12 -export -in platform.x509.pem -inkey shared.priv.pem -out shared.pk12 -name youralias

设置密码

  • 默认密码是android
  • 可以自己修改

生成keystore

1
keytool -importkeystore -deststorepass android -destkeypass android -destkeystore debug.keystore -srckeystore shared.pk12 -srcstoretype PKCS12 -srcstorepass android -alias youralias

总共会生成以下三个文件,其中debug.keystore就是我们需要的

1
2
3
shared.pk12
shared.priv.pem
debug.keystore

部署到AndroidStudio

可以参考之前的文章AndroidStudio Gradle 全局参数配置

TFLite 是Google基于TensorFlow 推出的移动端深度学习工具。我们可以利用这个工具,将训练好的模型部署到移动端设备上,支持(Android和iOS).

如何应用

选择模型

转换成TFLite模型

利用TensorFlow官方提供的转换工具,可以建多种不同类型的模型,转换成TFLite支持的模型。目前支持

  1. tf.GraphDef (.pb or .pbtxt)
  2. tf.keras (.h5)

部署到移动端设备上

将生成的.tflite模型部署到移动端或者嵌入式设备上。

优化模型

通过量化方式,将32位浮点型转换成更有效的8位整型,或者在移动端的GPU上运行。

实例分割Demo

下载Google提供的GPU版本DeeplabV3 tflite模型

下载地址

  1. 输入
  2. 输出

参考TensorFlow examples

里面提供了多种类型的Demo,支持各个平台

开启GPU支持

  1. Module Gradle 依赖需要使用nightly版本
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    repositories {
    maven {
    url 'https://google.bintray.com/tensorflow'
    }
    }
    dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    /** ... **/
    implementation 'org.tensorflow:tensorflow-lite:0.0.1-gpu-experimental'
    }
  2. Java 代码中开启支持
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // NEW: Prepare GPU delegate.
    GpuDelegate delegate = new GpuDelegate();
    Interpreter.Options options = (new Interpreter.Options()).addDelegate(delegate);

    // Set up interpreter.
    Interpreter interpreter = new Interpreter(model, options);

    // Run inference.
    writeToInputTensor(inputTensor);
    interpreter.run(inputTensor, outputTensor);
    readFromOutputTensor(outputTensor);

    // Clean up.
    delegate.close();

Numpy中很多的基本操作函数,都有一个参数axis,比如:

  1. argmax 返回最大元素的索引
  2. argmin 返回最大元素的索引
  3. sum
  4. max
  5. min
  6. mean
  7. average
  8. median

官方解释

我们从numpy doc里面的argmax函数可以看到下面的解释(有删减)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def argmax(a, axis=None, out=None):
"""
Returns the indices of the maximum values along an axis.

Parameters
----------
a : array_like
Input array. # Numpy数组
axis : int, optional # 整数(可正可负),可选
By default, the index is into the flattened array, otherwise
along the specified axis.
# 默认是一个扁平数组,否则根据指定的axis
Returns
-------
index_array : ndarray of ints
Array of indices into the array. It has the same shape as `a.shape`
with the dimension along `axis` removed.
# 返回数组的一个切片,和a的shape相同,但是移除了axis这个维度。

官方 Examples

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
>>> a = np.arange(6).reshape(2,3)
>>> a
array([[0, 1, 2],
[3, 4, 5]])
>>> np.argmax(a) # 未指定axis,模式整个数组是一个扁平数组,取所有元素的最大值的下标
5
>>> np.argmax(a, axis=0) # a.shape=(2,3), 当axis=0时,输出的shape是(3)
array([1, 1, 1])
>>> np.argmax(a, axis=1) # 当axis=1时,输出的shape是(2)
array([2, 2])

Indexes of the maximal elements of a N-dimensional array:
# 不展开index, 保留原始结构
>>> ind = np.unravel_index(np.argmax(a, axis=None), a.shape)
>>> ind
(1, 2)
>>> a[ind]
5

>>> b = np.arange(6)
>>> b[1] = 5
>>> b
array([0, 5, 2, 3, 4, 5])
>>> np.argmax(b) # Only the first occurrence is returned. # 只输出第一个出现的最大值下标
1

"""

怎么样更好的理解axis呢?

argmax为例,功能是返回最大元素的索引。可以这么来理解。

  • 当数组是一维的,里面的元素就是一维数组里面的单个值,此时axis是没有作用的,只能取值为 0,比如

    1
    2
    3
    4
    >>> import numpy as np
    >>> a = np.array([1, 2, 3, 4, 5, 6, 7])
    >>> np.argmax(a)
    6
  • 当数组是二维的,就分了几种情况:

    • 不指定axis时,把整个数组当作一维数组来处理,假定数组是3x3,二维数组
      1
      2
      3
      4
      5
      6
      7
      8
      9
      >>> arr = np.array([[1, 3, 5],[2, 4, 6],[3, 5, 8]])
      >>> print(arr.shape)
      (3, 3)
      >>> print(arr)
      [[1 3 5]
      [2 4 6]
      [3 5 8]]
      >>> np.argmax(arr) # 不指定axis时,把整个数组展开成一维数组来处理
      8
    • axis=0时,假定数组是2x3,二维数组,输出的shape应该是有3个元素的索引的一维数组,按列统计,共有3列,给出每列最大值在列方向上的索引
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      >>> arr = np.array([[1, 3, 5],[6, 4, 2]])
      >>> print(arr)
      [[1 3 5]
      [6 4 2]]
      # 第一列最大值时6,在列方向上的索引为1
      # 第二列最大值为4,在列方向上的索引为1
      # 第二列最大值为5,在列方向上的索引为0
      >>> np.argmax(arr, axis=0)
      array([1, 1, 0], dtype=int64)
      >>> np.argmax(arr, axis=-2)
      array([1, 1, 0], dtype=int64)
    • axis=1时,假定数组是2x3,二维数组,输出的shape应该是有2个元素的索引的一维数组,按行统计,共有3行,给出每行最大值在行方向上的索引
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      >>> arr = np.array([[1, 3, 5],[6, 4, 2]])
      >>> print(arr)
      [[1 3 5]
      [6 4 2]]
      # 第一行最大值时5,在行方向上的索引为2
      # 第二行最大值为6,在行方向上的索引为0
      >>> np.argmax(arr, axis=1)
      array([2, 0], dtype=int64)
      >>> np.argmax(arr, axis=-1)
      array([2, 0], dtype=int64)
  • 当数组是三维数组时。

    • 不指定axis时,将数组展开成一维数组,很好理解
    • axis=0时,假定数组时2x3x2的三维数组,输出的shape应该是3x2个元素的索引的二维数组。
      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
      >>> arr = np.random.randint(12, size=(2, 3, 2))
      >>> print(arr)
      [[[11 8]
      [11 5]
      [ 7 0]]

      [[ 8 10]
      [ 9 5]
      [ 4 10]]]
      #
      >>> print(arr[0,:,:])
      [[11 8]
      [11 5]
      [ 7 0]]
      >>> print(arr[1,:,:])
      [[ 8 10]
      [ 9 5]
      [ 4 10]]
      # 分别对比 arr[0,:,:], arr[1,:,:] 对应位置,取其中最大值的索引,索引取值范围[0-1]
      # out[0][0] = index(max(11, 8)) = 0
      # out[0][1] = index(max(8, 10)) = 1
      # out[1][0] = index(max(11, 9)) = 0
      # out[1][1] = index(max(5, 5)) = 0
      # out[2][0] = index(max(7, 4)) = 0
      # out[2][1] = index(max(0, 10)) = 1
      # out = [[0, 1],
      # [0, 0],
      # [0, 1]]
      >>> print(np.argmax(arr, axis=0).shape)
      (3, 2)
      >>> np.argmax(arr, axis=0)
      array([[0, 1],
      [0, 0],
      [0, 1]], dtype=int64)
      >>> np.argmax(arr, axis=-3)
      array([[0, 1],
      [0, 0],
      [0, 1]], dtype=int64)
      >>> np.argmax(arr, axis=0-arr.ndim)
      array([[0, 1],
      [0, 0],
      [0, 1]], dtype=int64)
    • axis=1时,假定数组时2x3x2的三维数组,输出的shape应该是2x2个元素的索引的二维数组。
      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
      >>> arr = np.random.randint(12, size=(2, 3, 2))
      >>> print(arr)
      [[[11 8]
      [11 5]
      [ 7 0]]

      [[ 8 10]
      [ 9 5]
      [ 4 10]]]
      >>> print(arr[:,0,:])
      [[11 8]
      [ 8 10]]
      >>> print(arr[:,1,:])
      [[11 5]
      [ 9 5]]
      >>> print(arr[:,2,:])
      [[ 7 0]
      [ 4 10]]
      # 分别对比 arr[:,0,:], arr[:,1,:], arr[:,2,:] 对应位置,取其中最大值的索引,索引取值范围[0-2]
      # out[0][0] = index(max(11, 11, 7 )) = 0
      # out[0][1] = index(max(8 , 5, 0 )) = 0
      # out[1][0] = index(max(8 , 9, 4 )) = 1
      # out[1][1] = index(max(10, 5, 10)) = 0
      # out = [[0, 0],
      # [1, 0]]
      >>> print(np.argmax(arr, axis=1).shape)
      (2, 2)
      >>> np.argmax(arr, axis=1)
      array([[0, 0],
      [1, 0]], dtype=int64)
      >>> np.argmax(arr, axis=-2)
      array([[0, 0],
      [1, 0]], dtype=int64)
      >>> np.argmax(arr, axis=1-arr.ndim)
      array([[0, 0],
      [1, 0]], dtype=int64)
    • axis=2时,假定数组时2x3x2的三维数组,输出的shape应该是2x3个元素的索引的二维数组。我们将数组的三个维度依次称为行,列,高
      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
      >>> arr = np.random.randint(12, size=(2, 3, 2))
      >>> print(arr)
      [[[11 8]
      [11 5]
      [ 7 0]]

      [[ 8 10]
      [ 9 5]
      [ 4 10]]]
      >>> print(arr[:,:,0])
      [[11 11 7]
      [ 8 9 4]]
      >>> print(arr[:,:,1])
      [[ 8 5 0]
      [10 5 10]]
      >>> print(np.argmax(arr, axis=2).shape)
      (2, 3)
      # 分别对比 arr[:,:,0], arr[:,:,1] 对应位置,取其中最大值的索引,索引取值范围[0-1]
      # out[0][0] = index(max(11, 8)) = 0
      # out[0][1] = index(max(11, 5)) = 0
      # out[0][2] = index(max(7 , 0)) = 0
      # out[1][0] = index(max(8 , 10)) = 1
      # out[1][1] = index(max(9 , 5)) = 0
      # out[1][2] = index(max(4 , 10)) = 1
      # out = [[0, 0, 0],
      # [1, 0, 1]]
      >>> np.argmax(arr, axis=2)
      array([[0, 0, 0],
      [1, 0, 1]], dtype=int64)
      >>> np.argmax(arr, axis=-1)
      array([[0, 0, 0],
      [1, 0, 1]], dtype=int64)
      >>> np.argmax(arr, axis=2-arr.ndim)
      array([[0, 0, 0],
      [1, 0, 1]], dtype=int64)

先提出以下两个问题

  • 在开发Android应用时,如何将配置参数应用到所有的AndroidStudio项目中?
  • 有些敏感信息我们不希望包含在工程中,跟随代码提交,比如签名相关信息。

解决方案

我们可以利用Gradle的全局配置文件,为我们所有的项目配置相关参数,解决以上两个问题。

  • Windows平台在 C:\Users\user\.gradle 目录下
  • Linux平台在 /home/user/.gradle 目录下
  • Mac平台类似,找到gradle的目录即可

下面以配置签名信息为例:

  1. 打开gradle.properties文件,在文件末尾增加如下信息:
    1
    2
    3
    4
    KEYSTOREFILE=../../keystore/android.keystore
    KEYALIAS=release
    KEYPASSWORD=helloworld
    STOREPASSWORD=helloworld
  • 注意
    • 这里的配置信息是没有""双引号的,有引号会导致异常。
    • KEYSOTREFILE使用的是相对目录,相对于app/build.gradle文件所在的目录。
    • 比如我这个文件就位于和工程文件夹相同级别的目录
      1
      2
      3
      4
      5
      MyProject/app/build.gradle
      MyProject1/app/build.gradle
      MyProject2/app/build.gradle
      MyProject3/app/build.gradle
      keystore/android.keystore
  1. 在到对应的工程下面,打开app/build.gradle
  • 增加signingConfigs
  • 在buildTypes release里面增加signConfig配置
    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
    android {
    compileSdkVersion 28
    defaultConfig {
    applicationId "com.example.test"
    minSdkVersion 21
    targetSdkVersion 28
    versionCode 1
    versionName "1.0"
    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    // 增加签名配置
    signingConfigs{
    releaseConfig{
    keyAlias KEYALIAS //引用刚才配置的参数
    keyPassword KEYPASSWORD
    storePassword STOREPASSWORD
    storeFile file(KEYSTOREFILE)
    }
    }
    buildTypes {
    release {
    signingConfig signingConfigs.releaseConfig //引用配置文件
    minifyEnabled false
    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
    }
    }
  1. 这样配置后,即使代码提交了,也不会将签名信息提交,其他开发者把代码拉下来后,只需要到gradle.properties里面配置好对应的参数即可。其他全局配置也可以这么操作。