diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..603b140 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..681f41a --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,116 @@ + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
\ No newline at end of file diff --git a/.idea/dictionaries/yari.xml b/.idea/dictionaries/yari.xml new file mode 100644 index 0000000..c6d2115 --- /dev/null +++ b/.idea/dictionaries/yari.xml @@ -0,0 +1,7 @@ + + + + stitcher + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..169fd0d --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b6ea2b1 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..60dd722 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,47 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 29 + defaultConfig { + applicationId 'com.yariksoffice.javaopencvplaygroung' + minSdkVersion 16 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + multiDexEnabled true + } + buildTypes { + debug { + minifyEnabled false + shrinkResources false + } + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + incremental true + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'com.squareup.picasso:picasso:2.5.2' + implementation "com.github.chrisbanes:PhotoView:2.3.0" + + implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + implementation 'io.reactivex.rxjava2:rxjava:2.2.11' + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + + implementation 'org.bytedeco:javacv:1.4.4' + implementation 'org.bytedeco.javacpp-presets:opencv:4.0.1-1.4.4:android-arm64' + // u can drop redundant platforms here + implementation 'org.bytedeco.javacpp-presets:opencv:4.0.1-1.4.4:android-arm' + implementation 'org.bytedeco.javacpp-presets:opencv:4.0.1-1.4.4:android-x86' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a1db6f2 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/yariksoffice/javaopencvplaygroung/FileUtil.kt b/app/src/main/java/com/yariksoffice/javaopencvplaygroung/FileUtil.kt new file mode 100644 index 0000000..1bb3556 --- /dev/null +++ b/app/src/main/java/com/yariksoffice/javaopencvplaygroung/FileUtil.kt @@ -0,0 +1,82 @@ +package com.yariksoffice.javaopencvplaygroung + +import android.content.Context +import android.net.Uri +import android.os.Environment +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.* + +class FileUtil(private val context: Context) { + + @Throws(IOException::class) + fun urisToFiles(uris: List): List { + val files = ArrayList(uris.size) + for (uri in uris) { + val file = createTempFile() + writeUriToFile(uri, file) + files.add(file) + } + return files + } + + @Throws(IOException::class) + private fun createTempFile(): File { + // don't need read/write permission for this directory starting from android 19 + val root = requirePicturesDirectory() + root.mkdirs() // make sure that directory exists + + val date = SimpleDateFormat(DATE_FORMAT_TEMPLATE, Locale.getDefault()).format(Date()) + val filePrefix = IMAGE_NAME_TEMPLATE.format(date) + return File.createTempFile(filePrefix, JPG_EXTENSION, root) + } + + @Throws(IOException::class) + private fun writeUriToFile(target: Uri, destination: File) { + val inputStream = context.contentResolver.openInputStream(target)!! + val outputStream = FileOutputStream(destination) + inputStream.use { input -> + outputStream.use { out -> + input.copyTo(out) + } + } + } + + fun createResultFile(): File { + val pictures = requirePicturesDirectory() + //noinspection ConstantConditions,ResultOfMethodCallIgnored + pictures.mkdirs() + return File("${pictures.absolutePath}$RESULT_FILE_NAME") + } + + private fun requirePicturesDirectory(): File { + return context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) ?: throw IOException( + "Can't access folder") + } + + fun cleanUpWorkingDirectory() { + deleteFile(requirePicturesDirectory()) + } + + // there is no build in function for deleting folders. <3 + private fun deleteFile(file: File) { + if (file.isDirectory) { + val entries = file.listFiles() + if (entries != null) { + for (entry in entries) { + deleteFile(entry) + } + } + } + file.delete() + } + + companion object { + private const val RESULT_FILE_NAME = "/result.jpg" + private const val DATE_FORMAT_TEMPLATE = "yyyyMMdd_HHmmss" + private const val IMAGE_NAME_TEMPLATE = "IMG_%s_" + private const val JPG_EXTENSION = ".jpg" + } +} diff --git a/app/src/main/java/com/yariksoffice/javaopencvplaygroung/ImageStitcher.kt b/app/src/main/java/com/yariksoffice/javaopencvplaygroung/ImageStitcher.kt new file mode 100644 index 0000000..f177217 --- /dev/null +++ b/app/src/main/java/com/yariksoffice/javaopencvplaygroung/ImageStitcher.kt @@ -0,0 +1,70 @@ +package com.yariksoffice.javaopencvplaygroung + +import android.net.Uri + +import org.bytedeco.javacpp.opencv_core.Mat +import org.bytedeco.javacpp.opencv_core.MatVector +import org.bytedeco.javacpp.opencv_stitching.Stitcher + +import java.io.File + +import io.reactivex.Single +import org.bytedeco.javacpp.opencv_imgcodecs.imread + +import org.bytedeco.javacpp.opencv_imgcodecs.imwrite +import org.bytedeco.javacpp.opencv_stitching.Stitcher.ERR_CAMERA_PARAMS_ADJUST_FAIL +import org.bytedeco.javacpp.opencv_stitching.Stitcher.ERR_HOMOGRAPHY_EST_FAIL +import org.bytedeco.javacpp.opencv_stitching.Stitcher.ERR_NEED_MORE_IMGS +import java.lang.Exception + +class StitcherInput(val uris: List, val stitchMode: Int) + +sealed class StitcherOutput { + class Success(val file: File) : StitcherOutput() + class Failure(val e: Exception) : StitcherOutput() +} + +class ImageStitcher(private val fileUtil: FileUtil) { + + fun stitchImages(input: StitcherInput): Single { + return Single.fromCallable { + fileUtil.cleanUpWorkingDirectory() + val files = fileUtil.urisToFiles(input.uris) + val vector = filesToMatVector(files) + stitch(vector, input.stitchMode) + } + } + + private fun stitch(images: MatVector, stitchMode: Int): StitcherOutput { + val result = Mat() + val stitcher = Stitcher.create(stitchMode) + val status = stitcher.stitch(images, result) + + return if (status == Stitcher.OK) { + val resultFile = fileUtil.createResultFile() + imwrite(resultFile.absolutePath, result) + StitcherOutput.Success(resultFile) + } else { + val e = RuntimeException("Can't stitch images: " + getStatusDescription(status)) + StitcherOutput.Failure(e) + } + } + + @Suppress("SpellCheckingInspection") + private fun getStatusDescription(status: Int): String { + return when (status) { + ERR_NEED_MORE_IMGS -> "ERR_NEED_MORE_IMGS" + ERR_HOMOGRAPHY_EST_FAIL -> "ERR_HOMOGRAPHY_EST_FAIL" + ERR_CAMERA_PARAMS_ADJUST_FAIL -> "ERR_CAMERA_PARAMS_ADJUST_FAIL" + else -> "UNKNOWN" + } + } + + private fun filesToMatVector(files: List): MatVector { + val images = MatVector(files.size.toLong()) + for (i in files.indices) { + images.put(i.toLong(), imread(files[i].absolutePath)) + } + return images + } +} diff --git a/app/src/main/java/com/yariksoffice/javaopencvplaygroung/MainActivity.kt b/app/src/main/java/com/yariksoffice/javaopencvplaygroung/MainActivity.kt new file mode 100644 index 0000000..db98859 --- /dev/null +++ b/app/src/main/java/com/yariksoffice/javaopencvplaygroung/MainActivity.kt @@ -0,0 +1,119 @@ +package com.yariksoffice.javaopencvplaygroung + +import android.app.Activity +import android.app.ProgressDialog +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.ImageView +import android.widget.RadioGroup +import android.widget.Toast + +import com.squareup.picasso.MemoryPolicy +import com.squareup.picasso.Picasso + +import org.bytedeco.javacpp.opencv_stitching.Stitcher + +import androidx.appcompat.app.AppCompatActivity +import com.yariksoffice.javaopencvplaygroung.StitcherOutput.Failure +import com.yariksoffice.javaopencvplaygroung.StitcherOutput.Success +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.PublishSubject + +class MainActivity : AppCompatActivity() { + + private lateinit var imageView: ImageView + private lateinit var radioGroup: RadioGroup + + private lateinit var imageStitcher: ImageStitcher + private lateinit var disposable: Disposable + + private val stitcherInputRelay = PublishSubject.create() + + @Suppress("DEPRECATION") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + imageView = findViewById(R.id.image) + radioGroup = findViewById(R.id.radio_group) + imageStitcher = ImageStitcher(FileUtil(applicationContext)) + + findViewById(R.id.button).setOnClickListener { chooseImages() } + + val dialog = ProgressDialog(this).apply { + setMessage(getString(R.string.processing_images)) + setCancelable(false) + } + + disposable = stitcherInputRelay.switchMapSingle { + imageStitcher.stitchImages(it) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSubscribe { dialog.show() } + .doOnSuccess { dialog.dismiss() } + } + .subscribe({ processResult(it) }, { processError(it) }) + } + + private fun chooseImages() { + val intent = Intent(Intent.ACTION_GET_CONTENT) + .setType(INTENT_IMAGE_TYPE) + .putExtra(EXTRA_ALLOW_MULTIPLE, true) + startActivityForResult(intent, CHOOSE_IMAGES) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == CHOOSE_IMAGES && resultCode == Activity.RESULT_OK && data != null) { + val clipData = data.clipData + val images = if (clipData != null) { + List(clipData.itemCount) { clipData.getItemAt(it).uri } + } else { + listOf(data.data!!) + } + processImages(images) + } + } + + private fun processImages(uris: List) { + imageView.setImageDrawable(null) // reset preview + val isScansChecked = radioGroup.checkedRadioButtonId == R.id.radio_scan + val stitchMode = if (isScansChecked) Stitcher.SCANS else Stitcher.PANORAMA + stitcherInputRelay.onNext(StitcherInput(uris, stitchMode)) + } + + private fun processError(e: Throwable) { + Log.e(TAG, "", e) + Toast.makeText(this, e.message + "", Toast.LENGTH_LONG).show() + } + + private fun processResult(output: StitcherOutput) { + when (output) { + is Success -> showImage(output) + is Failure -> processError(output.e) + } + } + + private fun showImage(output: Success) { + Picasso.with(this) + .load(output.file) + .memoryPolicy(MemoryPolicy.NO_STORE, MemoryPolicy.NO_CACHE) + .into(imageView) + } + + override fun onDestroy() { + super.onDestroy() + disposable.dispose() + } + + companion object { + private const val TAG = "TAG" + private const val EXTRA_ALLOW_MULTIPLE = "android.intent.extra.ALLOW_MULTIPLE" + private const val INTENT_IMAGE_TYPE = "image/*" + private const val CHOOSE_IMAGES = 777 + } +} diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..1f6bb29 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..0d025f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..a298675 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + +