parent
e09312c5a5
commit
0317d3e8de
@ -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() |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
@ -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.1中的com.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.1中的okhttp |
|
||||||
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 ':nsfw' |
||||||
include ':app' |
include ':app' |
||||||
|
Loading…
Reference in new issue