parent
66d0f48f05
commit
d8a2e0783f
@ -0,0 +1,22 @@ |
||||
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) |
||||
} |
||||
} |
@ -0,0 +1,2 @@ |
||||
<manifest |
||||
package="com.zwy.opennsfw"/> |
@ -0,0 +1,205 @@ |
||||
//package com.zwy.opennsfw |
||||
// |
||||
//import android.content.Context |
||||
//import android.content.res.AssetManager |
||||
//import android.graphics.Bitmap |
||||
//import android.graphics.Color |
||||
//import android.os.SystemClock |
||||
//import android.util.Log |
||||
//import org.tensorflow.lite.Interpreter |
||||
//import org.tensorflow.lite.gpu.GpuDelegate |
||||
//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(assetManager: AssetManager, isGPU: Boolean?, numThreads: Int) { |
||||
// |
||||
// /** |
||||
// * 数据宽高 |
||||
// */ |
||||
// 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) |
||||
// |
||||
// /** |
||||
// * 载入模型的客户端 |
||||
// */ |
||||
// private var tfliteModel: MappedByteBuffer? = null |
||||
// |
||||
// /** |
||||
// * GPU代理 |
||||
// */ |
||||
// private var gpuDelegate: GpuDelegate? = null |
||||
// |
||||
// /** |
||||
// * Tensorflow Lite |
||||
// */ |
||||
// private var tflite: Interpreter? = null |
||||
// |
||||
// /** |
||||
// * 喂入模型的最终数据源 |
||||
// */ |
||||
// private val imgData: ByteBuffer? |
||||
// |
||||
// |
||||
// init { |
||||
// tfliteModel = loadModelFile(assetManager) |
||||
// val tfliteOptions = Interpreter.Options() |
||||
// if (isGPU == true) { |
||||
// gpuDelegate = GpuDelegate() |
||||
// tfliteOptions.addDelegate(gpuDelegate) |
||||
// } |
||||
// tfliteOptions.setNumThreads(numThreads) |
||||
// tflite = Interpreter(tfliteModel!!, tfliteOptions) |
||||
// |
||||
// val tensor = tflite!!.getInputTensor(tflite!!.getInputIndex("input")) |
||||
// val stringBuilder = (" \n" |
||||
// + "dataType : " + |
||||
// tensor.dataType() + |
||||
// "\n" + |
||||
// "numBytes : " + |
||||
// tensor.numBytes() + |
||||
// "\n" + |
||||
// "numDimensions : " + |
||||
// tensor.numDimensions() + |
||||
// "\n" + |
||||
// "numElements : " + |
||||
// tensor.numElements() + |
||||
// "\n" + |
||||
// "shape : " + |
||||
// tensor.shape().size) |
||||
// Log.d(TAG, stringBuilder) |
||||
// |
||||
// imgData = ByteBuffer.allocateDirect( |
||||
// DIM_BATCH_SIZE |
||||
// * INPUT_WIDTH |
||||
// * INPUT_HEIGHT |
||||
// * DIM_PIXEL_SIZE |
||||
// * BYTES_PER_CHANNEL_NUM |
||||
// ) |
||||
// |
||||
// imgData!!.order(ByteOrder.LITTLE_ENDIAN) |
||||
// Log.d(TAG, "Tensorflow Lite Image Classifier Initialization Success.") |
||||
// } |
||||
// |
||||
// /** |
||||
// * Memory-map the model file in Assets. |
||||
// */ |
||||
// private fun loadModelFile(assetManager: AssetManager): MappedByteBuffer { |
||||
// val context:Context |
||||
// val fileDescriptor = assetManager.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) |
||||
// } |
||||
// |
||||
// |
||||
// /** |
||||
// * Writes Image data into a `ByteBuffer`. |
||||
// */ |
||||
// private fun convertBitmapToByteBuffer(bitmap_: Bitmap) { |
||||
// if (imgData == null || bitmap_ == null) { |
||||
// return |
||||
// } |
||||
// imgData.rewind() |
||||
// val W = bitmap_.width |
||||
// val H = bitmap_.height |
||||
// |
||||
// val w_off = max((W - INPUT_WIDTH) / 2, 0) |
||||
// val h_off = max((H - INPUT_HEIGHT) / 2, 0) |
||||
// |
||||
// //把每个像素的颜色值转为int 存入intValues |
||||
// bitmap_.getPixels(intValues, 0, INPUT_WIDTH, h_off, w_off, INPUT_WIDTH, INPUT_HEIGHT) |
||||
// // Convert the image to floating point. |
||||
// val startTime = SystemClock.uptimeMillis() |
||||
// for (color in intValues) { |
||||
// val r1 = Color.red(color) |
||||
// val g1 = Color.green(color) |
||||
// val b1 = Color.blue(color) |
||||
// |
||||
// val rr1 = r1 - 123 |
||||
// val gg1 = g1 - 117 |
||||
// val bb1 = b1 - 104 |
||||
// |
||||
// imgData.putFloat(bb1.toFloat()) |
||||
// imgData.putFloat(gg1.toFloat()) |
||||
// imgData.putFloat(rr1.toFloat()) |
||||
// } |
||||
// val endTime = SystemClock.uptimeMillis() |
||||
// Log.d(TAG, "Timecost to put values into ByteBuffer: " + (endTime - startTime) + "ms") |
||||
// } |
||||
// |
||||
// fun run(bitmap: Bitmap): NsfwBean { |
||||
// |
||||
// val bitmap_256 = Bitmap.createScaledBitmap(bitmap, 256, 256, true) |
||||
// |
||||
// //Writes image data into byteBuffer |
||||
// convertBitmapToByteBuffer(bitmap_256) |
||||
// |
||||
// val startTime = SystemClock.uptimeMillis() |
||||
// // out |
||||
// val outArray = Array(1) { FloatArray(2) } |
||||
// |
||||
// tflite!!.run(imgData, outArray) |
||||
// |
||||
// val endTime = SystemClock.uptimeMillis() |
||||
// |
||||
// Log.d(TAG, "SFW score :" + outArray[0][0] + ",NSFW score :" + outArray[0][1]) |
||||
// Log.d(TAG, "Timecost to run model inference: " + (endTime - startTime) + "ms") |
||||
// return NsfwBean(outArray[0][0], outArray[0][1]) |
||||
// } |
||||
// |
||||
// /** |
||||
// * Closes the interpreter and model to release resources. |
||||
// */ |
||||
// fun close() { |
||||
// if (tflite != null) { |
||||
// tflite!!.close() |
||||
// tflite = null |
||||
// Log.d(TAG, "Tensorflow Lite Image Classifier close.") |
||||
// } |
||||
// if (gpuDelegate != null) { |
||||
// gpuDelegate!!.close() |
||||
// Log.d(TAG, "Tensorflow Lite Image gpuDelegate close.") |
||||
// gpuDelegate = null |
||||
// } |
||||
// tfliteModel = null |
||||
// Log.d(TAG, "Tensorflow Lite destroyed.") |
||||
// } |
||||
// |
||||
// companion object { |
||||
// |
||||
// val TAG = "open_nsfw_android" |
||||
// /** |
||||
// * Dimensions of inputs. |
||||
// */ |
||||
// private val DIM_BATCH_SIZE = 1 |
||||
// |
||||
// private val DIM_PIXEL_SIZE = 3 |
||||
// |
||||
// fun create(assetManager: AssetManager, isAddGpuDelegate: Boolean?, numThreads: Int): Classifier { |
||||
// return Classifier(assetManager, isAddGpuDelegate!!, numThreads) |
||||
// } |
||||
// |
||||
// } |
||||
// |
||||
//} |
@ -0,0 +1,180 @@ |
||||
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 mClassifier |
||||
import org.tensorflow.lite.Interpreter |
||||
import org.tensorflow.lite.gpu.GpuDelegate |
||||
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 lateinit var tflite: Interpreter |
||||
|
||||
/** |
||||
* 喂入模型的最终数据源 |
||||
*/ |
||||
private lateinit 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 build(): Classifier { |
||||
return get(config) |
||||
} |
||||
} |
||||
|
||||
init { |
||||
|
||||
val tfliteOptions = Interpreter.Options() |
||||
if (config.isOpenGPU) tfliteOptions.addDelegate(GpuDelegate()) |
||||
tfliteOptions.setNumThreads(config.numThreads) |
||||
tflite = Interpreter(loadModelFile(config.context!!), tfliteOptions) |
||||
|
||||
imgData = ByteBuffer.allocateDirect( |
||||
DIM_BATCH_SIZE |
||||
* INPUT_WIDTH |
||||
* INPUT_HEIGHT |
||||
* DIM_PIXEL_SIZE |
||||
* BYTES_PER_CHANNEL_NUM |
||||
) |
||||
|
||||
imgData.order(ByteOrder.LITTLE_ENDIAN) |
||||
} |
||||
|
||||
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) { |
||||
imgData.rewind() |
||||
val W = bitmap_.width |
||||
val H = bitmap_.height |
||||
|
||||
val w_off = max((W - INPUT_WIDTH) / 2, 0) |
||||
val h_off = max((H - INPUT_HEIGHT) / 2, 0) |
||||
|
||||
//把每个像素的颜色值转为int 存入intValues |
||||
bitmap_.getPixels(intValues, 0, INPUT_WIDTH, h_off, w_off, INPUT_WIDTH, INPUT_HEIGHT) |
||||
|
||||
val startTime = SystemClock.uptimeMillis() |
||||
|
||||
for (color in intValues) { |
||||
val r1 = Color.red(color) |
||||
val g1 = Color.green(color) |
||||
val b1 = Color.blue(color) |
||||
|
||||
val rr1 = r1 - 123 |
||||
val gg1 = g1 - 117 |
||||
val bb1 = b1 - 104 |
||||
|
||||
imgData.putFloat(bb1.toFloat()) |
||||
imgData.putFloat(gg1.toFloat()) |
||||
imgData.putFloat(rr1.toFloat()) |
||||
} |
||||
val endTime = SystemClock.uptimeMillis() |
||||
"数据装载成功,耗时:${(endTime - startTime)} ms".d() |
||||
} |
||||
|
||||
fun run(bitmap: Bitmap): NsfwBean { |
||||
|
||||
val bitmap_256 = Bitmap.createScaledBitmap(bitmap, 256, 256, true) |
||||
|
||||
convertBitmapToByteBuffer(bitmap_256) |
||||
|
||||
val startTime = SystemClock.uptimeMillis() |
||||
// out |
||||
val outArray = Array(1) { FloatArray(2) } |
||||
|
||||
tflite.run(imgData, outArray) |
||||
|
||||
val endTime = SystemClock.uptimeMillis() |
||||
|
||||
"扫描完成,耗时: ${(endTime - startTime)} ms".d() |
||||
return NsfwBean(outArray[0][0], outArray[0][1]) |
||||
} |
||||
} |
@ -0,0 +1,12 @@ |
||||
package com.zwy.opennsfw.core |
||||
|
||||
import android.content.Context |
||||
|
||||
data class Config( |
||||
/** |
||||
* 是否开启GPU加速 |
||||
*/ |
||||
var isOpenGPU: Boolean = false, |
||||
var numThreads: Int = 1, |
||||
var context: Context? |
||||
) |
@ -0,0 +1,45 @@ |
||||
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) |
||||
|
||||
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)}%" |
||||
} |
||||
} |
||||
|
||||
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)初始化后再试") |
||||
} |
||||
|
@ -0,0 +1,16 @@ |
||||
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) |
||||
} |
||||
} |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
@ -0,0 +1,11 @@ |
||||
package com.example.open_nsfw_android |
||||
|
||||
import android.app.Application |
||||
import com.zwy.opennsfw.core.Classifier |
||||
|
||||
class DemoApplication : Application() { |
||||
override fun onCreate() { |
||||
super.onCreate() |
||||
Classifier.Build().context(this).build() |
||||
} |
||||
} |
@ -1,174 +0,0 @@ |
||||
package com.example.open_nsfw_android |
||||
|
||||
import android.Manifest |
||||
import android.annotation.SuppressLint |
||||
import android.app.ProgressDialog |
||||
import android.content.Intent |
||||
import android.content.pm.PackageManager |
||||
import android.graphics.BitmapFactory |
||||
import android.os.Bundle |
||||
import android.support.v4.app.ActivityCompat |
||||
import android.support.v4.content.ContextCompat |
||||
import android.support.v7.app.AppCompatActivity |
||||
import android.support.v7.widget.LinearLayoutManager |
||||
import android.util.Log |
||||
import android.view.View |
||||
import android.widget.Toast |
||||
import com.bumptech.glide.Glide |
||||
import com.luck.picture.lib.PictureSelector |
||||
import com.luck.picture.lib.config.PictureConfig |
||||
import com.luck.picture.lib.config.PictureMimeType |
||||
import com.luck.picture.lib.entity.LocalMedia |
||||
import com.zwy.nsfw.api.NSFWHelper |
||||
import com.zwy.nsfw.core.NSFWConfig |
||||
import com.zwy.nsfw.kotlin.getNsfwScore |
||||
import kotlinx.android.synthetic.main.activity_main.* |
||||
import java.io.File |
||||
import kotlin.concurrent.thread |
||||
|
||||
|
||||
class MainActivity : AppCompatActivity(), View.OnClickListener { |
||||
|
||||
var nsfwHelper: NSFWHelper? = null |
||||
var mainAdapter: MainAdapter? = null |
||||
var index = 0 |
||||
var listData: ArrayList<MyNsfwBean> = ArrayList() |
||||
var selectList: List<LocalMedia>? = null |
||||
|
||||
var progressDialog: ProgressDialog? = null |
||||
@SuppressLint("SetTextI18n") |
||||
override fun onCreate(savedInstanceState: Bundle?) { |
||||
super.onCreate(savedInstanceState) |
||||
setContentView(R.layout.activity_main) |
||||
initNsfwHelper() |
||||
initAdapter() |
||||
initClickListener() |
||||
tv_version.text = "当前版本:${this.packageManager.getPackageInfo(packageName, 0).versionName}" |
||||
if (ContextCompat.checkSelfPermission( |
||||
this, |
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE |
||||
) != PackageManager.PERMISSION_GRANTED |
||||
) { //表示未授权时 |
||||
//进行授权 |
||||
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1); |
||||
} |
||||
|
||||
} |
||||
|
||||
override fun onClick(v: View) { |
||||
when (v.id) { |
||||
R.id.bt_sc_assets -> { |
||||
reScAssetsImgs() |
||||
} |
||||
R.id.bt_sc_from_other -> { |
||||
PictureSelector.create(this) |
||||
.openGallery(PictureMimeType.ofImage())//全部.ofAll()、图片.、视频.ofVideo()、音频.ofAudio() |
||||
.maxSelectNum(20)// 最大图片选择数量 int |
||||
.minSelectNum(1)// 最小选择数量 int |
||||
.imageSpanCount(3)// 每行显示个数 int |
||||
.selectionMode(PictureConfig.MULTIPLE)// 多选 or 单选 or PictureConfig.SINGLE |
||||
.previewImage(true)// 是否可预览图片 true or false |
||||
.isCamera(false)// 是否显示拍照按钮 true or false |
||||
.isZoomAnim(true)// 图片列表点击 缩放效果 默认true |
||||
.selectionMedia(selectList) |
||||
.sizeMultiplier(0.5f)// glide 加载图片大小 0~1之间 如设置 .glideOverride()无效 |
||||
.previewEggs(true)// 预览图片时 是否增强左右滑动图片体验(图片滑动一半即可看到上一张是否选中) true or false |
||||
.forResult(0x01);//结果回调onActivityResult code |
||||
} |
||||
R.id.bt_sc_from_internet ->{ |
||||
startActivity(Intent(this,Main2Activity::class.java)) |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { |
||||
super.onActivityResult(requestCode, resultCode, data) |
||||
if (requestCode == 0x01 && resultCode == RESULT_OK) { |
||||
selectList = PictureSelector.obtainMultipleResult(data) |
||||
if (selectList != null && selectList?.size ?: 0 > 0) |
||||
reScFromImgs(selectList!!) |
||||
} |
||||
} |
||||
|
||||
|
||||
private fun initClickListener() { |
||||
bt_sc_assets.setOnClickListener(this) |
||||
bt_sc_from_other.setOnClickListener(this) |
||||
bt_sc_from_internet.setOnClickListener(this) |
||||
} |
||||
|
||||
private fun initAdapter() { |
||||
mainAdapter = MainAdapter(null) |
||||
rv.layoutManager = LinearLayoutManager(this) |
||||
rv.adapter = mainAdapter |
||||
} |
||||
|
||||
private fun initNsfwHelper() { |
||||
nsfwHelper = NSFWHelper.init(NSFWConfig(assets)) |
||||
} |
||||
|
||||
private fun reScFromImgs(list: List<LocalMedia>) { |
||||
progressDialog = ProgressDialog.show(this, "提示", "请稍后") |
||||
index = 0 |
||||
mainAdapter?.setNewData(null) |
||||
listData = ArrayList<MyNsfwBean>() |
||||
Thread(Runnable { |
||||
for (lm in list) { |
||||
val bitmap = BitmapFactory.decodeFile(lm.path) |
||||
|
||||
val nsfwScore = bitmap.getNsfwScore(assets) |
||||
|
||||
// listData.add(MyNsfwBean(0.0f, 0.0f, lm.path, bitmap)) |
||||
listData.add(MyNsfwBean(nsfwScore.sfw, nsfwScore.nsfw, lm.path, bitmap)) |
||||
// val nsfwBean = nsfwHelper?.scanBitmap(bitmap)!! |
||||
// listData[index].sfw = nsfwBean.sfw |
||||
// listData[index].nsfw = nsfwBean.nsfw |
||||
// rv.scrollToPosition(index) |
||||
index++ |
||||
|
||||
|
||||
} |
||||
runOnUiThread { |
||||
mainAdapter?.setNewData(listData) |
||||
mainAdapter?.notifyDataSetChanged() |
||||
progressDialog?.dismiss() |
||||
} |
||||
}).start() |
||||
} |
||||
|
||||
private fun reScAssetsImgs() { |
||||
progressDialog = ProgressDialog.show(this, "提示", "请稍后") |
||||
index = 0 |
||||
mainAdapter?.setNewData(null) |
||||
listData = ArrayList<MyNsfwBean>() |
||||
thread(true) { |
||||
for (a in resources.assets.list("img")) { |
||||
val path = "img/${a}" |
||||
val b = BitmapFactory.decodeStream(resources.assets.open(path)) |
||||
listData.add(MyNsfwBean(0f, 0f, path, b)) |
||||
val nsfwBean = nsfwHelper?.scanBitmap(b)!! |
||||
listData[index].sfw = nsfwBean.sfw |
||||
listData[index].nsfw = nsfwBean.nsfw |
||||
|
||||
index++ |
||||
} |
||||
runOnUiThread { |
||||
mainAdapter?.setNewData(listData) |
||||
mainAdapter?.notifyDataSetChanged() |
||||
progressDialog?.dismiss() |
||||
// rv.scrollToPosition(index) |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
override fun onBackPressed() { |
||||
finish() |
||||
} |
||||
|
||||
override fun onDestroy() { |
||||
super.onDestroy() |
||||
nsfwHelper?.destroyFactory() |
||||
} |
||||
} |
@ -1,4 +1,4 @@ |
||||
package com.example.open_nsfw_android |
||||
package com.example.open_nsfw_android.util |
||||
|
||||
import android.graphics.Bitmap |
||||
|
@ -0,0 +1,143 @@ |
||||
package com.example.open_nsfw_android.view |
||||
|
||||
import android.Manifest |
||||
import android.content.Intent |
||||
import android.content.pm.PackageManager |
||||
import android.graphics.BitmapFactory |
||||
import android.os.Bundle |
||||
import android.support.v4.app.ActivityCompat |
||||
import android.support.v4.content.ContextCompat |
||||
import android.support.v7.app.AppCompatActivity |
||||
import android.support.v7.widget.LinearLayoutManager |
||||
import android.widget.Toast |
||||
import com.example.open_nsfw_android.R |
||||
import com.example.open_nsfw_android.util.MainAdapter |
||||
import com.example.open_nsfw_android.util.MyNsfwBean |
||||
import com.luck.picture.lib.PictureSelector |
||||
import com.luck.picture.lib.config.PictureConfig |
||||
import com.luck.picture.lib.config.PictureMimeType |
||||
import com.luck.picture.lib.entity.LocalMedia |
||||
import getNsfwScore |
||||
import kotlinx.android.synthetic.main.activity_main.* |
||||
import java.io.File |
||||
|
||||
class MainAty : AppCompatActivity() { |
||||
|
||||
var selectList: List<LocalMedia>? = null |
||||
|
||||
lateinit var mMainAdapter: MainAdapter |
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) { |
||||
super.onCreate(savedInstanceState) |
||||
setContentView(R.layout.activity_main) |
||||
|
||||
init() |
||||
|
||||
} |
||||
|
||||
|
||||
fun init() { |
||||
//检测权限 |
||||
checkPermissions() |
||||
//初始化适配器 |
||||
initAdapter() |
||||
//识别assets目录 |
||||
bt_sc_assets.setOnClickListener { assetsListSc() } |
||||
//选择相册进行扫描 |
||||
bt_sc_from_other.setOnClickListener { selectImgFromD() } |
||||
//跳转网络图片识别页面 |
||||
bt_sc_from_internet.setOnClickListener { startActivity(Intent(this, Main2Activity::class.java)) } |
||||
} |
||||
|
||||
|
||||
private fun initAdapter() { |
||||
mMainAdapter = MainAdapter(null) |
||||
rv.layoutManager = LinearLayoutManager(this) |
||||
rv.adapter = mMainAdapter |
||||
} |
||||
|
||||
/** |
||||
* 相册选择 |
||||
*/ |
||||
private fun selectImgFromD() { |
||||
PictureSelector.create(this) |
||||
.openGallery(PictureMimeType.ofImage())//全部.ofAll()、图片.、视频.ofVideo()、音频.ofAudio() |
||||
.maxSelectNum(20)// 最大图片选择数量 int |
||||
.minSelectNum(1)// 最小选择数量 int |
||||
.imageSpanCount(3)// 每行显示个数 int |
||||
.selectionMode(PictureConfig.MULTIPLE)// 多选 or 单选 or PictureConfig.SINGLE |
||||
.previewImage(true)// 是否可预览图片 true or false |
||||
.isCamera(false)// 是否显示拍照按钮 true or false |
||||
.isZoomAnim(true)// 图片列表点击 缩放效果 默认true |
||||
.selectionMedia(selectList) |
||||
.sizeMultiplier(0.5f)// glide 加载图片大小 0~1之间 如设置 .glideOverride()无效 |
||||
.previewEggs(true)// 预览图片时 是否增强左右滑动图片体验(图片滑动一半即可看到上一张是否选中) true or false |
||||
.forResult(0x01);//结果回调onActivityResult code } |
||||
} |
||||
|
||||
/** |
||||
* 扫描assets目录 |
||||
*/ |
||||
private fun assetsListSc() { |
||||
mMainAdapter.setNewData(null) |
||||
Toast.makeText(this, "请稍等...", Toast.LENGTH_LONG).show() |
||||
Thread(Runnable { |
||||
resources.assets.list("img")?.forEach { |
||||
val bitmap = BitmapFactory.decodeStream(resources.assets.open("img/$it")) |
||||
val nsfwScore = bitmap.getNsfwScore() |
||||
addDataToAdapter( |
||||
MyNsfwBean( |
||||
nsfwScore.sfw, |
||||
nsfwScore.nsfw, |
||||
"img/$it", |
||||
bitmap |
||||
) |
||||
) |
||||
} |
||||
}).start() |
||||
} |
||||
|
||||
private fun addDataToAdapter(mMyNsfwBean: MyNsfwBean) { |
||||
runOnUiThread { mMainAdapter.addData(mMyNsfwBean) } |
||||
} |
||||
|
||||
/** |
||||
* 检测权限 |
||||
*/ |
||||
private fun checkPermissions() { |
||||
if (ContextCompat.checkSelfPermission( |
||||
this, |
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE |
||||
) != PackageManager.PERMISSION_GRANTED |
||||
) { |
||||
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1); |
||||
} |
||||
} |
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { |
||||
super.onActivityResult(requestCode, resultCode, data) |
||||
if (requestCode == 0x01 && resultCode == RESULT_OK) { |
||||
selectList = PictureSelector.obtainMultipleResult(data) |
||||
selectList?.let { |
||||
mMainAdapter.setNewData(null) |
||||
Toast.makeText(this, "请稍等...", Toast.LENGTH_LONG).show() |
||||
} |
||||
Thread(Runnable { |
||||
selectList?.forEach { |
||||
val file = File(it.path) |
||||
val nsfwScore = file.getNsfwScore() |
||||
addDataToAdapter( |
||||
MyNsfwBean( |
||||
nsfwScore.sfw, |
||||
nsfwScore.nsfw, |
||||
it.path, |
||||
BitmapFactory.decodeStream(file.inputStream()) |
||||
) |
||||
) |
||||
} |
||||
}).start() |
||||
|
||||
} |
||||
} |
||||
|
||||
} |
@ -1,5 +1,5 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
<background android:drawable="@drawable/ic_launcher_background" /> |
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" /> |
||||
<background android:drawable="@drawable/ic_launcher_background"/> |
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/> |
||||
</adaptive-icon> |
@ -1,5 +1,5 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
<background android:drawable="@drawable/ic_launcher_background" /> |
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" /> |
||||
<background android:drawable="@drawable/ic_launcher_background"/> |
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/> |
||||
</adaptive-icon> |
@ -1,26 +0,0 @@ |
||||
package com.zwy.nsfw; |
||||
|
||||
import android.content.Context; |
||||
import android.support.test.InstrumentationRegistry; |
||||
import android.support.test.runner.AndroidJUnit4; |
||||
|
||||
import org.junit.Test; |
||||
import org.junit.runner.RunWith; |
||||
|
||||
import static org.junit.Assert.*; |
||||
|
||||
/** |
||||
* Instrumented test, which will execute on an Android device. |
||||
* |
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a> |
||||
*/ |
||||
@RunWith(AndroidJUnit4.class) |
||||
public class ExampleInstrumentedTest { |
||||
@Test |
||||
public void useAppContext() { |
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getTargetContext(); |
||||
|
||||
assertEquals("com.zwy.nsfw.test", appContext.getPackageName()); |
||||
} |
||||
} |
@ -1,2 +0,0 @@ |
||||
<manifest package="com.zwy.nsfw" xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
</manifest> |
@ -1,42 +0,0 @@ |
||||
package com.zwy.nsfw.api |
||||
|
||||
import android.graphics.Bitmap |
||||
import com.zwy.nsfw.core.Classifier |
||||
import com.zwy.nsfw.core.NSFWConfig |
||||
import com.zwy.nsfw.core.NsfwBean |
||||
|
||||
object NSFWHelper { |
||||
|
||||
private lateinit var classifier: Classifier |
||||
|
||||
private var isInit = false |
||||
|
||||
@Synchronized |
||||
fun init(nsfwConfig: NSFWConfig): NSFWHelper { |
||||
classifier = Classifier.create(nsfwConfig.assetMannager, nsfwConfig.userGPU, 10) |
||||
isInit = true |
||||
return this |
||||
} |
||||
|
||||
|
||||
/** |
||||
* 同步扫描图片(建议使用者自己放在线程中处理) |
||||
*/ |
||||
@Synchronized |
||||
fun scanBitmap(bitmap: Bitmap): NsfwBean { |
||||
if (!isInit) { |
||||
throw ExceptionInInitializerError("Initialize the scanned object 'NSFWHelper.init(nsfwConfig: NSFWConfig)' by calling the function first.") |
||||
} |
||||
return classifier.run(bitmap) |
||||
} |
||||
|
||||
|
||||
/** |
||||
* 不会销毁该单利和配置的参数,建议页面处理完成后调用该代码对扫描器进行关闭,关闭后下次扫描不需要再次调用init方法 |
||||
*/ |
||||
fun destroyFactory() { |
||||
classifier.close() |
||||
} |
||||
|
||||
|
||||
} |
@ -1,197 +0,0 @@ |
||||
package com.zwy.nsfw.core |
||||
|
||||
import android.content.res.AssetManager |
||||
import android.graphics.Bitmap |
||||
import android.graphics.Color |
||||
import android.os.SystemClock |
||||
import android.util.Log |
||||
import org.tensorflow.lite.Interpreter |
||||
import org.tensorflow.lite.gpu.GpuDelegate |
||||
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(assetManager: AssetManager, isGPU: Boolean?, numThreads: Int) { |
||||
|
||||
/** |
||||
* tensor input img size |
||||
*/ |
||||
private val INPUT_WIDTH = 224 |
||||
private val INPUT_HEIGHT = 224 |
||||
|
||||
/** |
||||
* BytesPerChannel |
||||
*/ |
||||
private val BYTES_PER_CHANNEL_NUM = 4 |
||||
|
||||
/** |
||||
* Preallocated buffers for storing image data in. |
||||
*/ |
||||
private val intValues = IntArray(INPUT_WIDTH * INPUT_HEIGHT) |
||||
/** |
||||
* The loaded TensorFlow Lite model. |
||||
*/ |
||||
private var tfliteModel: MappedByteBuffer? = null |
||||
|
||||
/** |
||||
* Optional GPU delegate for accleration. |
||||
*/ |
||||
private var gpuDelegate: GpuDelegate? = null |
||||
|
||||
/** |
||||
* An instance of the driver class to run model inference with Tensorflow Lite. |
||||
*/ |
||||
private var tflite: Interpreter? = null |
||||
|
||||
/** |
||||
* A ByteBuffer to hold image data, to be feed into Tensorflow Lite as inputs. |
||||
*/ |
||||
private val imgData: ByteBuffer? |
||||
|
||||
init { |
||||
tfliteModel = loadModelFile(assetManager) |
||||
val tfliteOptions = Interpreter.Options() |
||||
if (isGPU == true) { |
||||
gpuDelegate = GpuDelegate() |
||||
tfliteOptions.addDelegate(gpuDelegate) |
||||
} |
||||
tfliteOptions.setNumThreads(numThreads) |
||||
tflite = Interpreter(tfliteModel!!, tfliteOptions) |
||||
|
||||
val tensor = tflite!!.getInputTensor(tflite!!.getInputIndex("input")) |
||||
val stringBuilder = (" \n" |
||||
+ "dataType : " + |
||||
tensor.dataType() + |
||||
"\n" + |
||||
"numBytes : " + |
||||
tensor.numBytes() + |
||||
"\n" + |
||||
"numDimensions : " + |
||||
tensor.numDimensions() + |
||||
"\n" + |
||||
"numElements : " + |
||||
tensor.numElements() + |
||||
"\n" + |
||||
"shape : " + |
||||
tensor.shape().size) |
||||
Log.d(TAG, stringBuilder) |
||||
|
||||
imgData = ByteBuffer.allocateDirect( |
||||
DIM_BATCH_SIZE |
||||
* INPUT_WIDTH |
||||
* INPUT_HEIGHT |
||||
* DIM_PIXEL_SIZE |
||||
* BYTES_PER_CHANNEL_NUM |
||||
) |
||||
|
||||
imgData!!.order(ByteOrder.LITTLE_ENDIAN) |
||||
Log.d(TAG, "Tensorflow Lite Image Classifier Initialization Success.") |
||||
} |
||||
|
||||
/** |
||||
* Memory-map the model file in Assets. |
||||
*/ |
||||
private fun loadModelFile(assetManager: AssetManager): MappedByteBuffer { |
||||
val fileDescriptor = assetManager.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) |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Writes Image data into a `ByteBuffer`. |
||||
*/ |
||||
private fun convertBitmapToByteBuffer(bitmap_: Bitmap) { |
||||
if (imgData == null || bitmap_ == null) { |
||||
return |
||||
} |
||||
imgData.rewind() |
||||
val W = bitmap_.width |
||||
val H = bitmap_.height |
||||
|
||||
val w_off = max((W - INPUT_WIDTH) / 2, 0) |
||||
val h_off = max((H - INPUT_HEIGHT) / 2, 0) |
||||
|
||||
//把每个像素的颜色值转为int 存入intValues |
||||
bitmap_.getPixels(intValues, 0, INPUT_WIDTH, h_off, w_off, INPUT_WIDTH, INPUT_HEIGHT) |
||||
// Convert the image to floating point. |
||||
val startTime = SystemClock.uptimeMillis() |
||||
for (color in intValues) { |
||||
val r1 = Color.red(color) |
||||
val g1 = Color.green(color) |
||||
val b1 = Color.blue(color) |
||||
|
||||
val rr1 = r1 - 123 |
||||
val gg1 = g1 - 117 |
||||
val bb1 = b1 - 104 |
||||
|
||||
imgData.putFloat(bb1.toFloat()) |
||||
imgData.putFloat(gg1.toFloat()) |
||||
imgData.putFloat(rr1.toFloat()) |
||||
} |
||||
val endTime = SystemClock.uptimeMillis() |
||||
Log.d(TAG, "Timecost to put values into ByteBuffer: " + (endTime - startTime) + "ms") |
||||
} |
||||
|
||||
fun run(bitmap: Bitmap): NsfwBean { |
||||
|
||||
val bitmap_256 = Bitmap.createScaledBitmap(bitmap, 256, 256, true) |
||||
|
||||
//Writes image data into byteBuffer |
||||
convertBitmapToByteBuffer(bitmap_256) |
||||
|
||||
val startTime = SystemClock.uptimeMillis() |
||||
// out |
||||
val outArray = Array(1) { FloatArray(2) } |
||||
|
||||
tflite!!.run(imgData, outArray) |
||||
|
||||
val endTime = SystemClock.uptimeMillis() |
||||
|
||||
Log.d(TAG, "SFW score :" + outArray[0][0] + ",NSFW score :" + outArray[0][1]) |
||||
Log.d(TAG, "Timecost to run model inference: " + (endTime - startTime) + "ms") |
||||
return NsfwBean(outArray[0][0], outArray[0][1]) |
||||
} |
||||
|
||||
/** |
||||
* Closes the interpreter and model to release resources. |
||||
*/ |
||||
fun close() { |
||||
if (tflite != null) { |
||||
tflite!!.close() |
||||
tflite = null |
||||
Log.d(TAG, "Tensorflow Lite Image Classifier close.") |
||||
} |
||||
if (gpuDelegate != null) { |
||||
gpuDelegate!!.close() |
||||
Log.d(TAG, "Tensorflow Lite Image gpuDelegate close.") |
||||
gpuDelegate = null |
||||
} |
||||
tfliteModel = null |
||||
Log.d(TAG, "Tensorflow Lite destroyed.") |
||||
} |
||||
|
||||
companion object { |
||||
|
||||
val TAG = "open_nsfw_android" |
||||
/** |
||||
* Dimensions of inputs. |
||||
*/ |
||||
private val DIM_BATCH_SIZE = 1 |
||||
|
||||
private val DIM_PIXEL_SIZE = 3 |
||||
|
||||
fun create(assetManager: AssetManager, isAddGpuDelegate: Boolean?, numThreads: Int): Classifier { |
||||
return Classifier(assetManager, isAddGpuDelegate!!, numThreads) |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -1,10 +0,0 @@ |
||||
package com.zwy.nsfw.core |
||||
|
||||
import android.content.res.AssetManager |
||||
|
||||
/** |
||||
* 配置扫描参数 |
||||
* [assetMannager]资源管理器,用于读取lib下的tfLite文件 |
||||
* [userGPU]是否开启GPU加速 默认关闭,部分手机开启后会有奔溃 |
||||
*/ |
||||
data class NSFWConfig(val assetMannager: AssetManager, val userGPU: Boolean = false) |
@ -1,3 +0,0 @@ |
||||
package com.zwy.nsfw.core |
||||
|
||||
class NsfwBean(val sfw: Float, val nsfw: Float) |
@ -1,28 +0,0 @@ |
||||
package com.zwy.nsfw.kotlin |
||||
|
||||
import android.content.res.AssetManager |
||||
import android.graphics.Bitmap |
||||
import android.graphics.BitmapFactory |
||||
import com.zwy.nsfw.api.NSFWHelper |
||||
import com.zwy.nsfw.core.NSFWConfig |
||||
import com.zwy.nsfw.core.NsfwBean |
||||
import java.io.File |
||||
|
||||
|
||||
fun Bitmap.getNsfwScore(mAssetManager: AssetManager): NsfwBean { |
||||
val nsfwBean = NSFWHelper.init(NSFWConfig(mAssetManager)).scanBitmap(this) |
||||
NSFWHelper.destroyFactory() |
||||
return nsfwBean |
||||
} |
||||
|
||||
fun File.getNsfwScore(mAssetManager: AssetManager): NsfwBean { |
||||
val bitmap = try { |
||||
BitmapFactory.decodeFile(this.path) |
||||
} catch (e: Exception) { |
||||
return NsfwBean(0f, 0f) |
||||
} |
||||
|
||||
val nsfwBean = NSFWHelper.init(NSFWConfig(mAssetManager)).scanBitmap(bitmap) |
||||
NSFWHelper.destroyFactory() |
||||
return nsfwBean |
||||
} |
@ -1,3 +0,0 @@ |
||||
<resources> |
||||
<string name="app_name">nsfw</string> |
||||
</resources> |
@ -1,17 +0,0 @@ |
||||
package com.zwy.nsfw; |
||||
|
||||
import org.junit.Test; |
||||
|
||||
import static org.junit.Assert.*; |
||||
|
||||
/** |
||||
* Example local unit test, which will execute on the development machine (host). |
||||
* |
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a> |
||||
*/ |
||||
public class ExampleUnitTest { |
||||
@Test |
||||
public void addition_isCorrect() { |
||||
assertEquals(4, 2 + 2); |
||||
} |
||||
} |
@ -1 +1 @@ |
||||
include ':app', ':nsfw' |
||||
include ':app', ':OpenNSFW' |
||||
|
Loading…
Reference in new issue