更新说明文档

pull/34/head
赵文文 5 years ago
parent e6860453b6
commit 0ccebe68ff
  1. 1
      OpenNSFW/.gitignore
  2. 38
      OpenNSFW/build.gradle
  3. 0
      OpenNSFW/consumer-rules.pro
  4. 21
      OpenNSFW/proguard-rules.pro
  5. 22
      OpenNSFW/src/androidTest/java/com/zwy/opennsfw/ExampleInstrumentedTest.kt
  6. 1
      OpenNSFW/src/main/AndroidManifest.xml
  7. BIN
      OpenNSFW/src/main/assets/nsfw.tflite
  8. 228
      OpenNSFW/src/main/java/com/zwy/opennsfw/core/Classifier.kt
  9. 23
      OpenNSFW/src/main/java/com/zwy/opennsfw/core/Config.kt
  10. 46
      OpenNSFW/src/main/java/com/zwy/opennsfw/utils/kt.kt
  11. 16
      OpenNSFW/src/test/java/com/zwy/opennsfw/ExampleUnitTest.kt
  12. 1
      README.md
  13. 1
      README_EN.md
  14. 13
      app/build.gradle
  15. BIN
      app/src/main/assets/demopic.png
  16. BIN
      app/src/main/assets/img/tim8.jpeg
  17. BIN
      app/src/main/assets/img/timg.jpeg
  18. BIN
      app/src/main/assets/img/timg10.jpeg
  19. BIN
      app/src/main/assets/img/timg11.jpeg
  20. BIN
      app/src/main/assets/img/timg12.jpeg
  21. BIN
      app/src/main/assets/img/timg13.jpeg
  22. BIN
      app/src/main/assets/img/timg14.jpeg
  23. BIN
      app/src/main/assets/img/timg15.jpeg
  24. BIN
      app/src/main/assets/img/timg2.jpeg
  25. BIN
      app/src/main/assets/img/timg3.jpeg
  26. BIN
      app/src/main/assets/img/timg5.jpeg
  27. BIN
      app/src/main/assets/img/timg7.jpeg
  28. BIN
      app/src/main/assets/img/timg9.jpeg
  29. 54
      app/src/main/java/com/zww/sample/MainActivity.kt
  30. 14
      app/src/main/res/layout/activity_main.xml
  31. 25
      app/src/main/res/layout/item_main.xml
  32. 20
      nsfw/src/main/java/com/zwy/nsfw/NSFWHelper.kt

@ -1 +0,0 @@
/build

@ -1,38 +0,0 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 29
buildToolsVersion "29.0.3"
defaultConfig {
minSdkVersion 16
targetSdkVersion 29
versionCode 5
versionName "1.3.5"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
//noinspection GradleCompatible
implementation 'com.android.support:appcompat-v7:28.0.0'
testImplementation 'junit:junit:4.13'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
implementation 'org.tensorflow:tensorflow-lite:2.1.0'
implementation 'org.tensorflow:tensorflow-lite-gpu:2.1.0'
}

@ -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,22 +0,0 @@
package com.zwy.opennsfw
import android.support.test.InstrumentationRegistry
import android.support.test.runner.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* 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("com.zwy.opennsfw.test", appContext.packageName)
}
}

@ -1 +0,0 @@
<manifest package="com.zwy.opennsfw" />

@ -1,228 +0,0 @@
package com.zwy.opennsfw.core
import NsfwBean
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.os.SystemClock
import d
import e
import mClassifier
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.lang.Math.max
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.MappedByteBuffer
import java.nio.channels.FileChannel
class Classifier private constructor(config: Config) {
/**
* DIM_BATCH_SIZE
*/
private val DIM_BATCH_SIZE = 1
/**
* DIM_PIXEL_SIZE
*/
private val DIM_PIXEL_SIZE = 3
/**
* 数据宽高
*/
private val INPUT_WIDTH = 224
/**
* 数据宽高
*/
private val INPUT_HEIGHT = 224
/**
* 通道
*/
private val BYTES_PER_CHANNEL_NUM = 4
/**
* Resize后的数据源
*/
private val intValues = IntArray(INPUT_WIDTH * INPUT_HEIGHT)
/**
* Tensorflow Lite
*/
private var tflite: Interpreter
/**
* 喂入模型的最终数据源
*/
private var imgData: ByteBuffer
companion object {
private var config = Config(context = null)
private var instance: Classifier? = null
get() {
if (field == null) {
if (config.context == null) throw RuntimeException("context函数未调用,请使用Classifier.Build().context(context)初始化")
field = Classifier(config)
mClassifier = field
}
return field
}
@Synchronized
private fun get(config: Config): Classifier {
this.config = config
return instance!!
}
}
class Build {
private val config = Config(context = null)
fun isOpenGPU(isOpenGPU: Boolean): Build {
config.isOpenGPU = isOpenGPU
return this
}
fun numThreads(numThreads: Int): Build {
config.numThreads = numThreads
return this
}
fun context(context: Context): Build {
config.context = context
return this
}
fun nsfwModuleFilePath(nsfwModuleFilePath: String): Build {
config.nsfwModuleFilePath = nsfwModuleFilePath
return this
}
fun build(): Classifier {
return get(config)
}
}
init {
try {
tflite =
Interpreter(
loadModelFile(context = config.context!!),
getTfLiteOptions(config.isOpenGPU)
)
if (config.isOpenGPU) "开启GPU加速成功".d()
} catch (e: Exception) {
"不支持GPU加速".e()
tflite = Interpreter(loadModelFile(context = config.context!!), getTfLiteOptions(false))
}
imgData = ByteBuffer.allocateDirect(
DIM_BATCH_SIZE
* INPUT_WIDTH
* INPUT_HEIGHT
* DIM_PIXEL_SIZE
* BYTES_PER_CHANNEL_NUM
)
imgData.order(ByteOrder.LITTLE_ENDIAN)
}
private fun getTfLiteOptions(isOpenGPU: Boolean = true): Interpreter.Options {
return Interpreter.Options().apply {
if (isOpenGPU) this.addDelegate(GpuDelegate())
this.setNumThreads(config.numThreads)
this.setAllowBufferHandleOutput(true)
this.setAllowFp16PrecisionForFp32(true)
}
}
private fun loadModelFile(context: Context): MappedByteBuffer {
val fileDescriptor = context.assets.openFd("nsfw.tflite")
val inputStream = FileInputStream(fileDescriptor.fileDescriptor)
val fileChannel = inputStream.channel
val startOffset = fileDescriptor.startOffset
val declaredLength = fileDescriptor.declaredLength
return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength)
}
private fun convertBitmapToByteBuffer(bitmap_: Bitmap) {
SystemClock.uptimeMillis().let { startTime ->
imgData.rewind()
//把每个像素的颜色值转为int 存入intValues
bitmap_.getPixels(
intValues,
0,
INPUT_WIDTH,
max((bitmap_.height - INPUT_HEIGHT) / 2, 0),
max((bitmap_.width - INPUT_WIDTH) / 2, 0),
INPUT_WIDTH,
INPUT_HEIGHT
)
for (color in intValues) {
imgData.putFloat((Color.blue(color) - 104).toFloat())
imgData.putFloat((Color.green(color) - 117).toFloat())
imgData.putFloat((Color.red(color) - 123).toFloat())
}
"数据装载成功,耗时:${(SystemClock.uptimeMillis() - startTime)} ms".d()
}
}
// # 根据路径获取图片 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 run(bitmap: Bitmap): NsfwBean {
SystemClock.uptimeMillis().let { startTime ->
//缩放位图时是否应使用双线性过滤。如果这是正确的,则在缩放时将使用双线性滤波,从而以较差的性能为代价具有更好的图像质量。如果这是错误的,则使用最近邻居缩放,这将使图像质量较差但速度更快。推荐的默认设置是将滤镜设置为“ true”,因为双线性滤镜的成本通常很小,并且改善的图像质量非常重要
ByteArrayOutputStream().let { stream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)
stream.close()
convertBitmapToByteBuffer(Bitmap.createScaledBitmap(bitmap, 256, 256, true))
// out
Array(1) { FloatArray(2) }.apply {
tflite.run(imgData, this)
val nsfw = NsfwBean(this[0][0], this[0][1])
"扫描完成[${nsfw}]耗时: ${(SystemClock.uptimeMillis() - startTime)} ms".d()
return nsfw
}
}
}
}
// fun bitmap2RGB(bitmap: Bitmap): ByteArray? {
// val bytes = bitmap.byteCount //返回可用于储存此位图像素的最小字节数
// val buffer =
// ByteBuffer.allocate(bytes) // 使用allocate()静态方法创建字节缓冲区
// bitmap.copyPixelsToBuffer(buffer) // 将位图的像素复制到指定的缓冲区
// val rgba = buffer.array()
// val pixels = ByteArray(rgba.size / 4 * 3)
// val count = rgba.size / 4
//
// //Bitmap像素点的色彩通道排列顺序是RGBA
// for (i in 0 until count) {
// pixels[i * 3] = rgba[i * 4] //R
// pixels[i * 3 + 1] = rgba[i * 4 + 1] //G
// pixels[i * 3 + 2] = rgba[i * 4 + 2] //B
// }
// return pixels
// }
}

@ -1,23 +0,0 @@
package com.zwy.opennsfw.core
import android.content.Context
data class Config(
/**
* 是否开启GPU加速
*/
var isOpenGPU: Boolean = true,
/**
* 扫描占用的线程数
*/
var numThreads: Int = 1,
/**
* 全局配置的context
*/
var context: Context?,
/**
* nsfw模型存放目录
*/
var nsfwModuleFilePath: String? = null
)

@ -1,46 +0,0 @@
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Log
import com.zwy.opennsfw.core.Classifier
import java.io.File
import java.math.RoundingMode
import java.text.DecimalFormat
private val TAG = "OpenNSFWLogTag"
var mClassifier: Classifier? = null
fun String.d() = Log.d(TAG, this)
fun String.e() = Log.e(TAG, this)
open class NsfwBean(val sfw: Float, val nsfw: Float) {
override fun toString(): String {
val floatFormat = DecimalFormat("0.0000")
floatFormat.roundingMode = RoundingMode.HALF_UP
// return "适宜度(非色情程度):${floatFormat.format(sfw * 100)}%,不适宜度(涉黄程度):${floatFormat.format(nsfw * 100)}%"
return "sfw:$sfw,nsfw:$nsfw%"
}
}
fun Bitmap.getNsfwScore(): NsfwBean {
checkNull()
return mClassifier!!.run(this)
}
fun File.getNsfwScore(): NsfwBean {
checkNull()
val bitmap = try {
BitmapFactory.decodeFile(this.path)
} catch (e: Exception) {
val m = "File2Bitmap过程失败,请确认文件是否存在或是否为图片"
m.e()
throw RuntimeException(m)
}
return bitmap.getNsfwScore()
}
private fun checkNull() {
if (mClassifier == null) throw RuntimeException("Classifier未初始化,请使用Classifier.Build().context(context)初始化后再试")
}

@ -1,16 +0,0 @@
package com.zwy.opennsfw
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* 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)
}
}

@ -16,6 +16,7 @@
### 本项目移除测试图片,请下载Demo后自行配图测试
#### 测试图片
![图片](https://github.com/devzwy/open_nsfw_android/blob/dev/demopic.png)
### 开始使用

@ -14,6 +14,7 @@
>>> Python and C + + have two ways to feed data. You can select PB model or tflite file according to your needs. Please refer to the above link for details. Java can only load PB model at present. Other platforms can [Google]( https://www.google.cn )
### This project removes the test picture, please download demo and test by yourself
#### picture
![图片](https://github.com/devzwy/open_nsfw_android/blob/dev/demopic.png)

@ -38,8 +38,7 @@ dependencies {
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
implementation project(path: ':nsfw_initializer')
implementation 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'
@ -47,6 +46,16 @@ dependencies {
//
implementation 'com.yanzhenjie:permission:2.0.3'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
//
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4'
//
implementation 'com.github.bumptech.glide:glide:4.11.0'
kapt 'com.github.bumptech.glide:compiler:4.11.0'
// implementation 'com.zwy.nsfw:nsfw_initializer:1.3.7'
// implementation 'com.zwy.nsfw:nsfw:1.3.7'
implementation project(path: ':nsfw_initializer')
implementation project(path: ':nsfw')
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

@ -1,25 +1,65 @@
package com.zww.sample
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.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.viewholder.BaseViewHolder
import com.zwy.nsfw.NSFWScoreBean
import com.zwy.nsfw.getNSFWScore
import kotlinx.android.synthetic.main.activity_main.*
import java.util.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mRecyclerView.layoutManager = GridLayoutManager(this, 2)
mRecyclerView.adapter =
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))
holder.getView<TextView>(R.id.tv).apply {
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"))
}
}
}
}
}.also { adapter ->
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 {
it.getNSFWScore()
it.recycle()
resources.assets.also { asset ->
asset.list("img")?.forEach {
"img/$it".also { assetFilePath ->
BitmapFactory.decodeStream(asset.open(assetFilePath)).also {
adapter.addData(MyNSFWBean(it.getNSFWScore(), it))
}
}
}
}
}
}
}
}
data class MyNSFWBean(val nsfwScoreBean: NSFWScoreBean, val bitmap: Bitmap)

@ -3,4 +3,18 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:gravity="center"
android:background="@color/colorPrimaryDark"
android:textSize="14sp"
android:textColor="#ffffff"
android:text="离线鉴黄" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/mRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="200dp"
android:layout_height="200dp">
<ImageView
android:id="@+id/iv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="5dp"
android:scaleType="centerCrop"/>
<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginLeft="5dp"
android:layout_marginBottom="5dp"
android:textColor="#ffffff"
android:gravity="center"
android:layout_marginRight="5dp"
android:background="#C1FF9800" />
</RelativeLayout>

@ -1,7 +1,6 @@
package com.zwy.nsfw
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Color
@ -16,7 +15,7 @@ import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.MappedByteBuffer
import java.nio.channels.FileChannel
import java.security.AccessControlContext
import java.text.DecimalFormat
object NSFWHelper {
@ -146,13 +145,16 @@ object NSFWHelper {
// out
Array(1) { FloatArray(2) }.apply {
mInterpreter.run(imgData, this)
return NSFWScoreBean(
this[0][1],
this[0][0],
timeConsumingToLoadData,
SystemClock.uptimeMillis() - startTime
).also {
log("扫描完成 -> $it")
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")
}
}
}
}

Loading…
Cancel
Save