@ -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) |
||||
} |
||||
} |
After Width: | Height: | Size: 2.1 MiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 146 KiB |
After Width: | Height: | Size: 115 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 98 KiB |
After Width: | Height: | Size: 192 KiB |
After Width: | Height: | Size: 81 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 107 KiB |
After Width: | Height: | Size: 90 KiB |
After Width: | Height: | Size: 50 KiB |
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() |
||||
adapter.addData(MyNSFWBean(it.getNSFWScore(), it)) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
} |
||||
|
||||
data class MyNSFWBean(val nsfwScoreBean: NSFWScoreBean, val bitmap: Bitmap) |
||||
|
@ -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> |