迁移仓库

pull/34/head
Jason 4 years ago
parent e09312c5a5
commit 0317d3e8de
  1. 15
      .idea/codeStyles/Project.xml
  2. 1
      .idea/gradle.xml
  3. 6
      app/build.gradle
  4. 1
      app/src/main/AndroidManifest.xml
  5. 32
      app/src/main/java/com/zww/sample/JavaApp.java
  6. 22
      app/src/main/java/com/zww/sample/KtApp.kt
  7. 65
      app/src/main/java/com/zww/sample/MainActivity.kt
  8. 6
      build.gradle
  9. 169
      nsfw/build.gradle
  10. 207
      nsfw/src/main/java/com/zwy/nsfw/NSFWHelper.kt
  11. 43
      nsfw/src/main/java/com/zwy/nsfw/utils.kt
  12. 340
      nsfw/src/main/java/io/github/devzwy/nsfw/NSFWHelper.kt
  13. 29
      nsfw/src/main/java/io/github/devzwy/nsfw/utils.kt
  14. 1
      nsfw_initializer/.gitignore
  15. 148
      nsfw_initializer/build.gradle
  16. 0
      nsfw_initializer/consumer-rules.pro
  17. 21
      nsfw_initializer/proguard-rules.pro
  18. 24
      nsfw_initializer/src/androidTest/java/com/zwy/nsfw_initializer/ExampleInstrumentedTest.kt
  19. 11
      nsfw_initializer/src/main/AndroidManifest.xml
  20. 45
      nsfw_initializer/src/main/java/com/zwy/nsfw_initializer/NSFWInitProvider.kt
  21. 17
      nsfw_initializer/src/test/java/com/zwy/nsfw_initializer/ExampleUnitTest.kt
  22. 1
      settings.gradle

@ -27,9 +27,18 @@
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="java.util" withSubpackages="false" static="false" />
<package name="kotlinx.android.synthetic" withSubpackages="true" static="false" />
<package name="io.ktor" withSubpackages="true" static="false" />
<package name="java.util" alias="false" withSubpackages="false" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />

@ -13,7 +13,6 @@
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/nsfw" />
<option value="$PROJECT_DIR$/nsfw_initializer" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />

@ -54,11 +54,11 @@ dependencies {
kapt 'com.github.bumptech.glide:compiler:4.11.0'
implementation 'com.zwy.nsfw:nsfw_initializer:1.3.9'
implementation 'com.zwy.nsfw:nsfw:1.3.9'
// implementation 'com.zwy.nsfw:nsfw_initializer:1.3.9'
// implementation 'com.zwy.nsfw:nsfw:1.3.9'
implementation 'org.tensorflow:tensorflow-lite:2.1.0'
implementation 'org.tensorflow:tensorflow-lite-gpu:2.1.0'
// implementation project(path: ':nsfw_initializer')
// implementation project(path: ':nsfw')
implementation project(path: ':nsfw')
}

@ -13,6 +13,7 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:name=".KtApp"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">

@ -0,0 +1,32 @@
package com.zww.sample;
import android.annotation.SuppressLint;
import android.app.Application;
import android.widget.Toast;
import io.github.devzwy.nsfw.NSFWHelper;
import kotlin.Unit;
import kotlin.jvm.functions.Function0;
import kotlin.jvm.functions.Function1;
public class JavaApp extends Application {
@SuppressLint("NewApi")
@Override
public void onCreate() {
super.onCreate();
NSFWHelper.INSTANCE.initHelper(this, this.getFilesDir().getPath() + "/nsfw.tflite", true, 4, new Function0<Unit>() {
@Override
public Unit invoke() {
Toast.makeText(JavaApp.this, "初始化成功", Toast.LENGTH_SHORT).show();
return null;
}
}, new Function1<String, Unit>() {
@Override
public Unit invoke(String s) {
Toast.makeText(JavaApp.this, s, Toast.LENGTH_SHORT).show();
return null;
}
});
}
}

@ -0,0 +1,22 @@
package com.zww.sample
import android.app.Application
import android.widget.Toast
import io.github.devzwy.nsfw.NSFWHelper
class KtApp:Application() {
override fun onCreate() {
super.onCreate()
NSFWHelper.openDebugLog()
NSFWHelper.initHelper(
context = this,
modelPath = "${this.filesDir.path}/nsfw.tflite",
isOpenGPU = true,
onInitError = {
Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
},
onInitSuccess = {
Toast.makeText(this, "初始化成功", Toast.LENGTH_SHORT).show()
})
}
}

@ -4,17 +4,16 @@ import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.GridLayoutManager
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.viewholder.BaseViewHolder
import com.zwy.nsfw.NSFWScoreBean
import com.zwy.nsfw.getNSFWScore
import io.github.devzwy.nsfw.NSFWHelper
import kotlinx.android.synthetic.main.activity_main.*
import java.util.*
import kotlin.random.Random
class MainActivity : BaseActivity() {
override fun initData() {
@ -23,19 +22,34 @@ class MainActivity : BaseActivity() {
object : BaseQuickAdapter<MyNSFWBean, BaseViewHolder>(R.layout.item_main) {
@SuppressLint("SetTextI18n")
override fun convert(holder: BaseViewHolder, item: MyNSFWBean) {
Glide.with(this@MainActivity).load(item.bitmap).into(holder.getView(R.id.iv))
Glide.with(this@MainActivity).load(item.bitmap)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(holder.getView(R.id.iv))
holder.getView<TextView>(R.id.tv).apply {
item.nsfwScoreBean.let {
NSFWHelper.getNSFWScore(item.bitmap) {
this.text =
"nsfw:${it.nsfwScore}\nsfw:${it.sfwScore}\n扫描耗时:${it.timeConsumingToScanData} ms"
if (it.nsfwScore>0.7){
if (it.nsfwScore > 0.7) {
this.setBackgroundColor(Color.parseColor("#C1FF0000"))
}else if (it.nsfwScore>0.5){
} else if (it.nsfwScore > 0.5) {
this.setBackgroundColor(Color.parseColor("#C1FF9800"))
}else {
} else {
this.setBackgroundColor(Color.parseColor("#803700B3"))
}
}
// item.nsfwScoreBean.let {
// this.text =
// "nsfw:${it.nsfwScore}\nsfw:${it.sfwScore}\n扫描耗时:${it.timeConsumingToScanData} ms"
// if (it.nsfwScore > 0.7) {
// this.setBackgroundColor(Color.parseColor("#C1FF0000"))
// } else if (it.nsfwScore > 0.5) {
// this.setBackgroundColor(Color.parseColor("#C1FF9800"))
// } else {
// this.setBackgroundColor(Color.parseColor("#803700B3"))
// }
// }
}
}
@ -43,15 +57,32 @@ class MainActivity : BaseActivity() {
adapter.setAnimationWithDefault(BaseQuickAdapter.AnimationType.ScaleIn)
adapter.isAnimationFirstOnly = false
resources.assets.also { asset ->
asset.list("img")?.forEach {
"img/$it".also { assetFilePath ->
BitmapFactory.decodeStream(asset.open(assetFilePath)).also {
adapter.addData(MyNSFWBean(it.getNSFWScore(), it))
Thread(Runnable {
for (i in 1..50) {
BitmapFactory.decodeStream(
resources.assets.open(
"img/${
resources.assets.list(
"img"
)!![Random.nextInt(
1, resources.assets.list(
"img"
)!!.size
)]
}"
)
).also { bitmap ->
runOnUiThread {
adapter.addData(MyNSFWBean(bitmap))
}
}
}
}
}).start()
}
}
@ -62,4 +93,4 @@ class MainActivity : BaseActivity() {
}
}
data class MyNSFWBean(val nsfwScoreBean: NSFWScoreBean, val bitmap: Bitmap)
data class MyNSFWBean(val bitmap: Bitmap)

@ -1,9 +1,9 @@
buildscript {
ext.kotlin_version = '1.3.72'
ext.libVersion = '1.3.9'
ext.libVersion = '1.5.0'
repositories {
google()
jcenter()
google()
mavenCentral()
}
dependencies {
@ -17,8 +17,8 @@ buildscript {
allprojects {
repositories {
google()
jcenter()
google()
mavenCentral()
}

@ -1,9 +1,15 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'com.github.dcendents.android-maven'
apply plugin: 'com.jfrog.bintray'
//apply plugin: 'com.github.dcendents.android-maven'
//apply plugin: 'com.jfrog.bintray'
apply plugin: 'org.jetbrains.dokka'
apply plugin: 'maven-publish'
group 'com.zwy.nsfw'
version "$libVersion"
android {
compileSdkVersion 30
buildToolsVersion "30.0.2"
@ -40,15 +46,16 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation 'org.tensorflow:tensorflow-lite:2.1.0'
implementation 'org.tensorflow:tensorflow-lite-gpu:2.1.0'
api 'org.tensorflow:tensorflow-lite:2.1.0'
api 'org.tensorflow:tensorflow-lite-gpu:2.1.0'
//
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2"
}
version = "$libVersion"
//version = "$libVersion"
//group = "com.zwy.nsfw"
//publish {
// userOrg = 'devzwy'
@ -64,38 +71,116 @@ version = "$libVersion"
// options.addStringOption('charSet', 'UTF-8')
//}
def siteUrl = 'https://github.com/devzwy/open_nsfw_android' // (GitHub地址)
def gitUrl = 'https://github.com/devzwy/open_nsfw_android.git' // Git仓库的url 便
group = "com.zwy.nsfw" // ****groupId , com.squareup.okhttp3:okhttp:3.4.1com.squareup.okhttp3部分
install {
repositories.mavenInstaller {
pom {
project {
packaging 'aar'
name 'nsfw' //
url siteUrl
def gitUrl = 'https://github.com/devzwy/open_nsfw_android.git' // Git仓库的url
group = "io.github.devzwy" // ****groupId , com.squareup.okhttp3:okhttp:3.4.1com.squareup.okhttp3部分
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
def lUsername = properties.getProperty("username")
def lPassword = properties.getProperty("password")
publishing {
publications {
//
publishMyProjectMiniexcel(MavenPublication) {
// jar包
// from components.java
// //
// artifact generateSourcesJar
// // javadoc
// artifact generateJavadoc
pom {
name = "nsfw"
description = "android端离线鉴黄库"
url = "https://github.com/devzwy/open_nsfw_android"
licenses {
license {
name = 'The Apache Software License, Version 2.0'
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
name = "The Apache License, Version 2.0"
url = "http://www.apache.org/licenses/LICENSE-2.0.txt"
}
}
developers {
developer {
id 'z' //
name 'Jason' //
email 'dev_zwy@aliyun.com' //
id = "devzwy"
name = "devzwy"
email = "dev_zwy@aliyun.com"
}
}
scm {
connection gitUrl
developerConnection gitUrl
url siteUrl
connection = gitUrl
developerConnection = gitUrl
url = siteUrl
}
}
}
}
// https://oss.sonatype.org/#nexus-search;
//Properties properties = new Properties()
//properties.load(project.rootProject.file('local.properties').newDataInputStream())
//bintray {
// user = properties.getProperty("bintray.username") // local.properties bintray.user
// key = properties.getProperty("bintray.password") // local.properties bintray.apikey
// configurations = ['archives']
repositories {
//configurations = ['archives']
// Release版本可在版本号后面带上'-RELEASE'
maven {
name 'Release'
url 'https://oss.sonatype.org/service/local/staging/deploy/maven2'
credentials {
username = lUsername
password = lPassword
}
}
// Snapshot快照版本必须在版本号后面加上'-SNAPSHOT'
// maven {
// name = 'Snapshot'
// url = 'https://oss.sonatype.org/content/repositories/snapshots'
// credentials {
// username = "${NEXUS_USERNAME}"
// password = "${NEXUS_PASSWORD}"
// }
// }
}
}
//install {
// repositories.mavenInstaller {
// pom {
// project {
// packaging 'aar'
// name 'nsfw' //
// url siteUrl
// licenses {
// license {
// name = 'The Apache Software License, Version 2.0'
// url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
// }
// }
// developers {
// developer {
// id 'z' //
// name 'Jason' //
// email 'dev_zwy@aliyun.com' //
// }
// }
// scm {
// connection gitUrl
// developerConnection gitUrl
// url siteUrl
// }
// }
// }
// }
//}
task generateSourcesJar(type: Jar) {
group = 'jar'
from android.sourceSets.main.java.srcDirs
@ -130,19 +215,19 @@ artifacts {
archives generateSourcesJar //sourcesJar
}
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
bintray {
user = properties.getProperty("bintray.user") // local.properties bintray.user
key = properties.getProperty("bintray.apikey") // local.properties bintray.apikey
configurations = ['archives']
pkg {
repo = "maven" //(****)bintray中自己新建仓库的名字
name = "nsfw" //****JCenter上的项目名字com.squareup.okhttp3:okhttp:3.4.1okhttp
websiteUrl = siteUrl
vcsUrl = gitUrl
licenses = ["Apache-2.0"]
publish = true
desc = 'android端离线鉴黄库'
}
}
//Properties properties = new Properties()
//properties.load(project.rootProject.file('local.properties').newDataInputStream())
//bintray {
// user = properties.getProperty("bintray.user") // local.properties bintray.user
// key = properties.getProperty("bintray.apikey") // local.properties bintray.apikey
// configurations = ['archives']
// pkg {
// repo = "maven" //(****)bintray中自己新建仓库的名字
// name = "nsfw" //****JCenter上的项目名字com.squareup.okhttp3:okhttp:3.4.1okhttp
// websiteUrl = siteUrl
// vcsUrl = gitUrl
// licenses = ["Apache-2.0"]
// publish = true
// desc = 'android端离线鉴黄库'
// }
//}

@ -1,207 +0,0 @@
package com.zwy.nsfw
import android.content.Context
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Color
import android.os.SystemClock
import org.tensorflow.lite.Interpreter
import org.tensorflow.lite.gpu.GpuDelegate
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.MappedByteBuffer
import java.nio.channels.FileChannel
import java.text.DecimalFormat
object NSFWHelper {
private lateinit var mInterpreter: Interpreter
private lateinit var imgData: ByteBuffer
/**
* 数据宽高
*/
private val INPUT_WIDTH = 224
/**
* 数据宽高
*/
private val INPUT_HEIGHT = 224
/**
* SDK是否初始化完成
*/
private var isSDKInit = false
/**
* 手动初始化时必须传入模型文件的路径否则会有异常抛出
* 自动初始化需要将模型存放在资源文件assets根目录下,且名称必须为'nsfw.tflite'不可改动
*/
fun init(context: Context? = null, modelPath: String? = null) {
if (context == null && modelPath==null) throw RuntimeException("初始化失败,您必须选择一种初始化方式,初始化方式一:NSFWHelper.init(context = this@Application),初始化方式二:[implementation 'com.zwy.nsfw:nsfw_initializer:+'],初始化方式三:NSFWHelper.init(modelPath = \"模型文件存放路径\")。说明:方式一和方式二均需要手动将模型文件放在Assets根目录下,并命名为nsfw.tflite,方式三适用于产品对apk大小控制严格,无法将模型文件直接放在apk中,可在用户打开Apk后台静默下载后指定模型路径进行初始化,三种方式任选其一即可")
if (modelPath != null && modelPath.isNotEmpty()) {
log("手动初始化...")
File(modelPath).let { modelFile ->
if (!modelFile.exists()) throw FileNotFoundException("模型文件路径配置错误,请重新配置,如果确定路径正确请检测文件权限是否申请")
mInterpreter = try {
initInterpreterByFile(modelFile, true)
} catch (e: Exception) {
initInterpreterByFile(modelFile)
}
}
} else {
loadModelFile(context!!).let {
if (it == null) throw Resources.NotFoundException("资源文件下未找到模型文件,请检测模型是否在assets目录下,名称是否为nsfw.tflite")
mInterpreter = try {
initInterpreterByMappedByteBuffer(it, true).also {
log("NSFW自动初始化成功,已开启GPU加速")
}
} catch (e: Exception) {
initInterpreterByMappedByteBuffer(it).also {
log("NSFW自动初始化成功,未开启GPU加速")
}
}
}
}
imgData = ByteBuffer.allocateDirect(1 * INPUT_WIDTH * INPUT_HEIGHT * 3 * 4)
imgData.order(ByteOrder.LITTLE_ENDIAN)
isSDKInit = true
}
//
//初始化方式一:NSFWHelper.init(context = this@Application),初始化方式二:[implementation 'com.zwy.nsfw:nsfw_initializer:+'],初始化方式三:NSFWHelper.init(modelPath = "模型文件存放路径")。说明:方式一和方式二均需要手动将模型文件放在Assets根目录下,并命名为nsfw.tflite,方式三适用于产品对apk大小控制严格,无法将模型文件直接放在apk中,可在用户打开Apk后台静默下载后指定模型路径进行初始化,三种方式任选其一即可
//
//
/**
* 关闭日志输出
*/
fun disEnableLog() {
isOpenLog = false
}
/**
* 装载扫描数据
*/
private fun convertBitmapToByteBuffer(bitmap_: Bitmap): Long {
SystemClock.uptimeMillis().let { startTime ->
imgData.rewind()
IntArray(INPUT_WIDTH * INPUT_HEIGHT).let {
//把每个像素的颜色值转为int 存入intValues
bitmap_.getPixels(
it,
0,
INPUT_WIDTH,
Math.max((bitmap_.height - INPUT_HEIGHT) / 2, 0),
Math.max((bitmap_.width - INPUT_WIDTH) / 2, 0),
INPUT_WIDTH,
INPUT_HEIGHT
)
for (color in it) {
imgData.putFloat((Color.blue(color) - 104).toFloat())
imgData.putFloat((Color.green(color) - 117).toFloat())
imgData.putFloat((Color.red(color) - 123).toFloat())
}
}
return SystemClock.uptimeMillis() - startTime
}
}
// # 根据路径获取图片 Image.open(path)
// # 判断图片的像素格式是否为RGB,如果不是RGB则转换为RGB(24位彩色图像,每个像素用24个bit表示,分别表示红色、绿色和蓝色三个通道)
// # 重制图片大小为256*256并使用 官方解释:若要调整大小,请对所有可能影响输出值的像素使用线性插值计算输出像素值。对于其他变换,使用输入图像中2x2环境上的线性插值。
// # 对resize的结果转换为io流并保存为JPEG格式
// # Convert to 64-bit floating point.asType float32 定义变量存储转换后的32位float图片数据
// # 获取图片的宽高,截取 224*224大小 x从16位开始,取到16+224位置 y亦是如此
// # 将取的数值转换位float32
// # 将每一个颜色值*255
// # 将每一个颜色减去一定的阈值 104....
// # [[[127.64.-18]]] 转换为 [[[[127.64.-18]]]]
// # 使用index关键字喂入模型
// # 删除所有单维度的条目
// # 输出扫描结果
fun getNSFWScore(bitmap: Bitmap): NSFWScoreBean {
if (!isSDKInit) throw RuntimeException(
"SDK未初始化,请初始化后使用。方式一:如果您的模型文件存放在Assets下并名称必须为:nsfw.tflite,请直接引用[implementation 'com.zwy.nsfw:nsfw_initializer:1.3.7']可免去初始化过程(模型文件在demo中有存放)" +
"方式二:否则需要指定模型文件的路径,使用:'NSFWHelper.init(modelPath = \"模型文件的路径\")'进行初始化,两者任选其一即可(后者适用于如果模型置于Apk中导致Apk体积超出产品预算时,可将模型文件后台下载至用户手机中后指定路径进行手动初始化)"
)
SystemClock.uptimeMillis().let { startTime ->
//缩放位图时是否应使用双线性过滤。如果这是正确的,则在缩放时将使用双线性滤波,从而以较差的性能为代价具有更好的图像质量。如果这是错误的,则使用最近邻居缩放,这将使图像质量较差但速度更快。推荐的默认设置是将滤镜设置为“ true”,因为双线性滤镜的成本通常很小,并且改善的图像质量非常重要
ByteArrayOutputStream().let { stream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)
stream.close()
convertBitmapToByteBuffer(
Bitmap.createScaledBitmap(
bitmap,
256,
256,
true
)
).let { timeConsumingToLoadData ->
// out
Array(1) { FloatArray(2) }.apply {
mInterpreter.run(imgData, this)
DecimalFormat("0.000").let {
return NSFWScoreBean(
it.format(this[0][1]).toFloat(),
it.format(this[0][0]).toFloat(),
timeConsumingToLoadData,
SystemClock.uptimeMillis() - startTime
).also {
log("扫描完成 -> $it")
}
}
}
}
}
}
}
@Throws(Exception::class)
private fun initInterpreterByFile(modelFile: File, isOpenGPU: Boolean = false): Interpreter {
return Interpreter(
modelFile,
Interpreter.Options().also {
it.setNumThreads(4)
if (isOpenGPU) it.addDelegate(GpuDelegate())
it.setAllowBufferHandleOutput(true)
it.setAllowFp16PrecisionForFp32(true)
})
}
@Throws(Exception::class)
private fun initInterpreterByMappedByteBuffer(
modelMappedByteBuffer: MappedByteBuffer,
isOpenGPU: Boolean = false
): Interpreter {
return Interpreter(
modelMappedByteBuffer,
Interpreter.Options().also {
it.setNumThreads(4)
if (isOpenGPU) it.addDelegate(GpuDelegate())
it.setAllowBufferHandleOutput(true)
it.setAllowFp16PrecisionForFp32(true)
})
}
private fun loadModelFile(context: Context): MappedByteBuffer? {
try {
context.assets.openFd("nsfw.tflite").let { fileDescriptor ->
return FileInputStream(fileDescriptor.fileDescriptor).channel.map(
FileChannel.MapMode.READ_ONLY,
fileDescriptor.startOffset,
fileDescriptor.declaredLength
)
}
} catch (e: Exception) {
return null
}
}
}

@ -1,43 +0,0 @@
package com.zwy.nsfw
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Log
import java.io.File
@JvmField
var isOpenLog = true
fun Any.log(content: String) {
if (isOpenLog)
Log.d("NSFW", content)
}
fun Any.logE(content: String) {
if (isOpenLog)
Log.e("NSFW", content)
}
/**
* [nsfwScore]不适宜度
* [sfwScore]适宜度
* [TimeConsumingToLoadData]装载数据耗时
* [TimeConsumingToScanData]扫描数据耗时
*/
data class NSFWScoreBean(
val nsfwScore: Float,
val sfwScore: Float,
val timeConsumingToLoadData: Long,
val timeConsumingToScanData: Long
) {
override fun toString(): String {
return "nsfwScore:$nsfwScore, sfwScore:$sfwScore, TimeConsumingToLoadData:$timeConsumingToLoadData ms, TimeConsumingToScanData=$timeConsumingToScanData ms)"
}
}
fun Bitmap.getNSFWScore() = NSFWHelper.getNSFWScore(this)
fun File.getNSFWScore() = try {
BitmapFactory.decodeFile(this.path)
} catch (e: Exception) {
throw RuntimeException("File2Bitmap过程失败,请确认文件是否存在或是否为图片")
}.getNSFWScore()

@ -0,0 +1,340 @@
package io.github.devzwy.nsfw
import android.app.Application
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
import android.os.SystemClock
import android.util.Log
import kotlinx.coroutines.*
import org.tensorflow.lite.Interpreter
import org.tensorflow.lite.gpu.GpuDelegate
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.lang.Exception
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.channels.FileChannel
import java.text.DecimalFormat
object NSFWHelper {
/*为空时表示未初始化SDK*/
private var nsfwApplication: Application? = null
/*扫描器*/
private lateinit var mInterpreter: Interpreter
/*数据宽*/
private val INPUT_WIDTH = 224
/*数据高*/
private val INPUT_HEIGHT = 224
/*日志输出控制*/
private var isEnableLog = false
/**
* NSFW初始化函数 内部日志默认关闭调试环境可使用openDebugLog()开启日志
* [application] 建议传入application,传入activity可能会有内存泄漏
* [modelPath] 模型文件路径为空时将默认从Assets下读取
* [isOpenGPU] 是否开启GPU扫描加速部分机型兼容不友好的可关闭默认开启
* [numThreads] 扫描数据时内部分配的线程 默认4
* [onInitSuccess] 初始化成功的回调
* [onInitError] 初始化失败的回调携带一个string
*/
fun initHelper(
context: Application,
modelPath: String? = null,
isOpenGPU: Boolean = true,
numThreads: Int = 4,
onInitSuccess: (() -> Unit)? = null,
onInitError: ((String) -> Unit)? = null
) {
nsfwApplication?.let {
logD("NSFWHelper已初始化,自动跳过本次初始化!")
onInitError?.let { it1 -> it1("请勿重复初始化") }
return
}
nsfwApplication = context
getInterpreterOptions(isOpenGPU, numThreads).let { options ->
if (modelPath.isNullOrEmpty()) {
logD("未传入模型路径,尝试从Assets下读取'nsfw.tflite'模型文件")
//指定模型为空时默认寻找assets目录下名称为nsfw.tflite的模型
try {
mInterpreter = Interpreter(
nsfwApplication!!.assets.openFd("nsfw.tflite")
.let { fileDescriptor ->
FileInputStream(fileDescriptor.fileDescriptor).channel.map(
FileChannel.MapMode.READ_ONLY,
fileDescriptor.startOffset,
fileDescriptor.declaredLength
)
}, options
)
} catch (e: Exception) {
nsfwApplication = null
logE("未从Assets下成功读取'nsfw.tflite'模型")
onInitError?.let { it("未从Assets下成功读取'nsfw.tflite'模型") }
return
}
logD("从Assets下加载模型文件成功!")
} else {
logD("尝试从传入的模型路径读取模型")
//指定路径下寻找模型文件进行初始化
try {
modelPath.let {
File(it).let { modelFile ->
modelFile.exists().assetBoolean({
mInterpreter = Interpreter(
modelFile,
options
)
}, {
throw FileNotFoundException("未找到模型文件")
})
}
}
logD("模型加载成功!")
} catch (e: Exception) {
nsfwApplication = null
logE("模型配置错误,读取失败")
onInitError?.let { it("未能正确读取到模型文件 '${modelPath}'") }
return
}
}
}
logD("NSFWHelper初始化成功!${if (isOpenGPU) "GPU加速已成功开启" else "GPU加速未开启"}")
onInitSuccess?.let { it() }
}
/**
* 开启日志
*/
fun openDebugLog() {
isEnableLog = true
}
private fun logD(content: String) {
if (isEnableLog) Log.d(javaClass.name, content)
}
private fun logE(content: String) {
if (isEnableLog) Log.e(javaClass.name, content)
}
private fun getInterpreterOptions(openGPU: Boolean, numThreads: Int): Interpreter.Options {
return Interpreter.Options().also {
it.setNumThreads(numThreads)
if (openGPU) {
it.addDelegate(GpuDelegate())
/*CPU转到GPU可以直接读取或直接写入数据到GPU中的硬件缓冲区并绕过可避免的memory copies*/
it.setAllowBufferHandleOutput(true)
/*CPU转到GPU处理提升扫描速度*/
it.setAllowFp16PrecisionForFp32(true)
}
}
}
/**
* 同步扫描文件NSFW数值
*/
fun getNSFWScore(file: File): NSFWScoreBean {
nsfwApplication?.let {
return getNSFWScore(BitmapFactory.decodeFile(file.path))
}
throw NSFWUnInitException()
}
/**
* 异步扫描文件NSFW数值
*/
fun getNSFWScore(file: File, onResult: ((NSFWScoreBean) -> Unit)) {
if (nsfwApplication == null) {
throw NSFWUnInitException()
}
GlobalScope.launch(Dispatchers.IO) {
getNSFWScore(BitmapFactory.decodeFile(file.path)).let { result ->
withContext(Dispatchers.Main) {
onResult(result)
}
}
}
}
/**
* 同步扫描文件NSFW数值
*/
fun getNSFWScore(filePath: String): NSFWScoreBean {
nsfwApplication?.let {
return getNSFWScore(BitmapFactory.decodeFile(filePath))
}
throw NSFWUnInitException()
}
/**
* 异步扫描文件NSFW数值
*/
fun getNSFWScore(filePath: String, onResult: ((NSFWScoreBean) -> Unit)) {
if (nsfwApplication == null) {
throw NSFWUnInitException()
}
GlobalScope.launch(Dispatchers.IO) {
getNSFWScore(BitmapFactory.decodeFile(filePath)).let { result ->
withContext(Dispatchers.Main) {
onResult(result)
}
}
}
}
/**
* 同步扫描bitmap
*/
fun getNSFWScore(bitmap: Bitmap): NSFWScoreBean {
nsfwApplication?.let {
SystemClock.uptimeMillis().let { startTime ->
//缩放位图时是否应使用双线性过滤。如果这是正确的,则在缩放时将使用双线性滤波,从而以较差的性能为代价具有更好的图像质量。如果这是错误的,则使用最近邻居缩放,这将使图像质量较差但速度更快。推荐的默认设置是将滤镜设置为“ true”,因为双线性滤镜的成本通常很小,并且改善的图像质量非常重要
ByteArrayOutputStream().let { stream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)
stream.close()
convertBitmapToByteBuffer(
Bitmap.createScaledBitmap(
bitmap,
256,
256,
true
)
).let { result ->
// out
Array(1) { FloatArray(2) }.apply {
synchronized(this@NSFWHelper) {
mInterpreter.run(result.imgData, this)
DecimalFormat("0.000").let {
return NSFWScoreBean(
it.format(this[0][1]).toFloat(),
it.format(this[0][0]).toFloat(),
result.exceTime,
SystemClock.uptimeMillis() - startTime
).also {
logD("扫描完成(${result}) -> $it")
}
}
}
}
}
}
}
}
throw NSFWUnInitException()
}
/**
* 异步扫描文件NSFW数值
*/
fun getNSFWScore(bitmap: Bitmap, onResult: ((NSFWScoreBean) -> Unit)) {
// # 根据路径获取图片 Image.open(path)
// # 判断图片的像素格式是否为RGB,如果不是RGB则转换为RGB(24位彩色图像,每个像素用24个bit表示,分别表示红色、绿色和蓝色三个通道)
// # 重制图片大小为256*256并使用 官方解释:若要调整大小,请对所有可能影响输出值的像素使用线性插值计算输出像素值。对于其他变换,使用输入图像中2x2环境上的线性插值。
// # 对resize的结果转换为io流并保存为JPEG格式
// # Convert to 64-bit floating point.asType float32 定义变量存储转换后的32位float图片数据
// # 获取图片的宽高,截取 224*224大小 x从16位开始,取到16+224位置 y亦是如此
// # 将取的数值转换位float32
// # 将每一个颜色值*255
// # 将每一个颜色减去一定的阈值 104....
// # [[[127.64.-18]]] 转换为 [[[[127.64.-18]]]]
// # 使用index关键字喂入模型
// # 删除所有单维度的条目
// # 输出扫描结果
if (nsfwApplication == null) {
logE("未初始化")
throw NSFWUnInitException()
}
GlobalScope.launch(Dispatchers.IO) {
getNSFWScore(bitmap).let { result ->
withContext(Dispatchers.Main) {
onResult(result)
}
}
}
}
/**
* 装载扫描数据
*/
private fun convertBitmapToByteBuffer(bitmap_: Bitmap): CovertBitmapResultBean {
ByteBuffer.allocateDirect(1 * INPUT_WIDTH * INPUT_HEIGHT * 3 * 4).let { imgData ->
imgData.order(ByteOrder.LITTLE_ENDIAN)
SystemClock.uptimeMillis().let { startTime ->
imgData.rewind()
IntArray(INPUT_WIDTH * INPUT_HEIGHT).let {
//把每个像素的颜色值转为int 存入intValues
bitmap_.getPixels(
it,
0,
INPUT_WIDTH,
Math.max((bitmap_.height - INPUT_HEIGHT) / 2, 0),
Math.max((bitmap_.width - INPUT_WIDTH) / 2, 0),
INPUT_WIDTH,
INPUT_HEIGHT
)
for (color in it) {
imgData.putFloat((Color.blue(color) - 104).toFloat())
imgData.putFloat((Color.green(color) - 117).toFloat())
imgData.putFloat((Color.red(color) - 123).toFloat())
}
}
return CovertBitmapResultBean(imgData, SystemClock.uptimeMillis() - startTime)
}
}
}
}

@ -0,0 +1,29 @@
package io.github.devzwy.nsfw
import java.lang.Exception
import java.nio.ByteBuffer
/**
* [nsfwScore]不适宜度
* [sfwScore]适宜度
* [TimeConsumingToLoadData]装载数据耗时
* [TimeConsumingToScanData]扫描数据耗时
*/
data class NSFWScoreBean(
val nsfwScore: Float,
val sfwScore: Float,
val timeConsumingToLoadData: Long,
val timeConsumingToScanData: Long
) {
override fun toString(): String {
return "nsfwScore:$nsfwScore, sfwScore:$sfwScore, TimeConsumingToLoadData:$timeConsumingToLoadData ms, TimeConsumingToScanData=$timeConsumingToScanData ms)"
}
}
fun Boolean.assetBoolean(onTrue: ()-> Unit,onFalse: ()-> Unit){
if (this) onTrue() else onFalse()
}
class NSFWUnInitException:Exception("请调用NSFWHelper.init(...)函数后再试!")
data class CovertBitmapResultBean(val imgData: ByteBuffer,val exceTime:Long)

@ -1 +0,0 @@
/build

@ -1,148 +0,0 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'com.github.dcendents.android-maven'
apply plugin: 'com.jfrog.bintray'
apply plugin: 'org.jetbrains.dokka'
android {
compileSdkVersion 30
buildToolsVersion "30.0.2"
defaultConfig {
minSdkVersion 21
targetSdkVersion 30
versionCode 139
versionName "$libVersion"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
sourceSets {
main {
java {
include '**/*.java'
include '**/*.kt'
}
}
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
api project(path: ':nsfw')
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
//group = "com.zwy.nsfw"
//publish {
// userOrg = 'devzwy'
// repoName = 'maven'
// artifactId = 'nsfw_initializer'
// desc = 'android端离线鉴黄库快速初始化库'
// website = 'https://github.com/devzwy'
//}
//
//tasks.withType(Javadoc) {//
// options.addStringOption('Xdoclint:none', '-quiet')
// options.addStringOption('encoding', 'UTF-8')
// options.addStringOption('charSet', 'UTF-8')
//}
version = "$libVersion"
def siteUrl = 'https://github.com/devzwy/open_nsfw_android' // (GitHub地址)
def gitUrl = 'https://github.com/devzwy/open_nsfw_android.git' // Git仓库的url 便
group = "com.zwy.nsfw" // ****groupId , com.squareup.okhttp3:okhttp:3.4.1com.squareup.okhttp3部分
install {
repositories.mavenInstaller {
pom {
project {
packaging 'aar'
name 'nsfw_initializer' //
url siteUrl
licenses {
license {
name = 'The Apache Software License, Version 2.0'
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
developers {
developer {
id 'z' //
name 'Jason' //
email 'dev_zwy@aliyun.com' //
}
}
scm {
connection gitUrl
developerConnection gitUrl
url siteUrl
}
}
}
}
}
task generateSourcesJar(type: Jar) {
group = 'jar'
from android.sourceSets.main.java.srcDirs
classifier = 'sources'
}
task sourcesJar(type: Jar) {
from android.sourceSets.main.java.srcDirs
classifier = 'sources'
}
task javadoc(type: Javadoc) {
source = android.sourceSets.main.java.srcDirs
options.encoding = "UTF-8"
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
}
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
}
task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) {
outputFormat = 'javadoc'
outputDirectory = javadoc.destinationDir
}
task generateJavadoc(type: Jar, dependsOn: dokkaJavadoc) {
group = 'jar'
classifier = 'javadoc'
from javadoc.destinationDir
}
artifacts {
archives generateJavadoc //javadocJar
archives generateSourcesJar //sourcesJar
}
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
bintray {
user = properties.getProperty("bintray.user") // local.properties bintray.user
key = properties.getProperty("bintray.apikey") // local.properties bintray.apikey
configurations = ['archives']
pkg {
repo = "maven" //(****)bintray中自己新建仓库的名字
name = "nsfw_initializer" //****JCenter上的项目名字com.squareup.okhttp3:okhttp:3.4.1okhttp
websiteUrl = siteUrl
vcsUrl = gitUrl
licenses = ["Apache-2.0"]
publish = true
desc = 'android端离线鉴黄库快速初始化库'
}
}

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

@ -1,24 +0,0 @@
package com.zwy.nsfw_initializer
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("zwy.nsfw.initializer.test", appContext.packageName)
}
}

@ -1,11 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zwy.nsfw_initializer">
<application>
<provider
android:name="com.zwy.nsfw_initializer.NSFWInitProvider"
android:authorities="${applicationId}.nsfwinitprovider"
android:enabled="true"
android:exported="false" />
</application>
</manifest>

@ -1,45 +0,0 @@
package com.zwy.nsfw_initializer
import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri
import com.zwy.nsfw.NSFWHelper
class NSFWInitProvider : ContentProvider() {
override fun insert(uri: Uri, values: ContentValues?): Uri? {
return null
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
return null
}
override fun onCreate(): Boolean {
NSFWHelper.init(requireNotNull(context),null)
return true
}
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int {
return 0
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
return 0
}
override fun getType(uri: Uri): String? {
return null
}
}

@ -1,17 +0,0 @@
package com.zwy.nsfw_initializer
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

@ -1,3 +1,2 @@
include ':nsfw_initializer'
include ':nsfw'
include ':app'

Loading…
Cancel
Save