Skip to content

Commit 13f433b

Browse files
committed
* allow to crop the image to inner region
* tweak auto-detection algorithm (it works better un some cases and worst in others)
1 parent a022972 commit 13f433b

File tree

7 files changed

+141
-34
lines changed

7 files changed

+141
-34
lines changed

app/src/main/java/com/dan/perspective/AppFragment.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ open class AppFragment(val activity: MainActivity) : Fragment() {
88
open fun onBack(homeButton: Boolean) {
99
}
1010

11+
open fun onActivate() {
12+
}
13+
1114
fun showToast(message: String) {
1215
activity.showToast(message)
1316
}

app/src/main/java/com/dan/perspective/MainActivity.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ class MainActivity : AppCompatActivity() {
4242
transaction.remove(prevFragment)
4343
transaction.commit()
4444

45+
item.second.onActivate()
46+
4547
supportActionBar?.setDisplayHomeAsUpEnabled(stack.size > 1)
4648
supportActionBar?.title = item.first
4749

app/src/main/java/com/dan/perspective/MainFragment.kt

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ import android.media.MediaScannerConnection
99
import android.net.Uri
1010
import android.os.Bundle
1111
import android.os.Parcelable
12+
import android.util.Log
1213
import android.view.*
1314
import androidx.appcompat.app.AppCompatActivity
1415
import androidx.documentfile.provider.DocumentFile
1516
import com.dan.perspective.databinding.MainFragmentBinding
1617
import org.opencv.android.Utils
17-
import org.opencv.core.Core.mean
18-
import org.opencv.core.Core.minMaxLoc
1918
import org.opencv.core.CvType.*
2019
import org.opencv.core.Mat
20+
import org.opencv.core.Rect
2121
import org.opencv.core.Size
2222
import org.opencv.imgproc.Imgproc.*
2323
import java.io.File
@@ -45,13 +45,23 @@ class MainFragment(activity: MainActivity) : AppFragment(activity) {
4545

4646
private var inputImage = Mat()
4747
private var outputImage = Mat()
48+
private var outputImageCropped = Mat()
4849
private var menuSave: MenuItem? = null
4950
private var menuPrevPerspective: MenuItem? = null
51+
private val sharedParams = SharedParams()
5052

5153
private lateinit var binding: MainFragmentBinding //: ActivityMainBinding by lazy { ActivityMainBinding.inflate(layoutInflater) }
5254
private var outputName = Settings.DEFAULT_NAME
5355
private var inputUri: Uri? = null
5456

57+
private fun updateToSharedParams() {
58+
sharedParams.cropped = binding.switchCrop.isChecked
59+
}
60+
61+
private fun updateFromSharedParams() {
62+
binding.switchCrop.isChecked = sharedParams.cropped
63+
}
64+
5565
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
5666
inflater.inflate(R.menu.main_menu, menu)
5767

@@ -214,7 +224,12 @@ class MainFragment(activity: MainActivity) : AppFragment(activity) {
214224

215225
warpImageAsync()
216226
if (outputImage.empty()) return
217-
val bitmap = matToBitmap(outputImage) ?: return
227+
228+
val bitmap = matToBitmap(
229+
if (binding.switchCrop.isChecked)
230+
outputImageCropped
231+
else
232+
outputImage) ?: return
218233

219234
setBusyDialogTitleAsync(MSG_SAVE)
220235

@@ -312,17 +327,37 @@ class MainFragment(activity: MainActivity) : AppFragment(activity) {
312327

313328
val perspectiveMat = getPerspectiveTransform(srcMat, destMat)
314329
warpPerspective(inputImage, outputImage, perspectiveMat, inputImage.size(), INTER_LANCZOS4)
330+
331+
val destLeftInt = destLeft.toInt() + 1
332+
val destRightInt = destRight.toInt() - 1
333+
val destTopInt = destTop.toInt() + 1
334+
val destBottomInt = destBottom.toInt() - 1
335+
336+
outputImageCropped = Mat(
337+
outputImage,
338+
Rect(
339+
destLeftInt,
340+
destTopInt,
341+
destRightInt - destLeftInt - 1,
342+
destBottomInt - destTopInt - 1))
315343
}
316344

317345
private fun showPreview() {
318346
if (inputImage.empty()) return
319347
if (outputImage.empty()) return
320-
val bitmap = matToBitmap(outputImage) ?: return
348+
val bitmap = matToBitmap( outputImage) ?: return
349+
val bitmapCropped = matToBitmap( outputImageCropped ) ?: return
321350
runOnUiThread {
322-
PreviewFragment.show(activity, bitmap)
351+
updateToSharedParams()
352+
PreviewFragment.show(activity, bitmap, bitmapCropped, sharedParams)
323353
}
324354
}
325355

356+
override fun onActivate() {
357+
updateFromSharedParams()
358+
super.onActivate()
359+
}
360+
326361
private fun warpImage() {
327362
if (inputImage.empty()) return
328363
if (!outputImage.empty()) {
@@ -338,6 +373,7 @@ class MainFragment(activity: MainActivity) : AppFragment(activity) {
338373

339374
private fun clearOutputImage() {
340375
outputImage.release()
376+
outputImageCropped.release()
341377
}
342378

343379
private fun lineIntersection( line1: Pair<PointF, PointF>, line2: Pair<PointF, PointF> ): PointF? {
@@ -376,44 +412,34 @@ class MainFragment(activity: MainActivity) : AppFragment(activity) {
376412
Size( AUTO_DETECT_WORK_SIZE.toDouble(), AUTO_DETECT_WORK_SIZE.toDouble()) ,
377413
0.0,
378414
0.0,
379-
INTER_AREA
415+
INTER_NEAREST_EXACT
380416
)
381417

382418
cvtColor( image, image, COLOR_BGR2GRAY )
383-
384-
//blur reduce number of edge detected
385-
blur( image, image, Size(3.0, 3.0) )
386-
387-
val meanValue = mean(image).`val`[0].toInt()
388-
val minMax = minMaxLoc(image)
389-
val minVal = minMax.minVal
390-
val maxVal = minMax.maxVal
391-
val lowerThreshold = (minVal + meanValue) / 2
392-
val upperThreshold = (maxVal + meanValue) / 2
419+
blur(image, image, Size(9.0,9.0))
420+
threshold( image, image, 0.0,255.0,THRESH_BINARY + THRESH_OTSU)
393421

394422
val edges = Mat()
395-
Canny( image, edges, lowerThreshold, upperThreshold )
423+
Canny( image, edges, 100.0, 200.0 )
396424

397425
val hLines = mutableListOf<Pair<Point, Point>>()
398426
val vLines = mutableListOf<Pair<Point, Point>>()
399427

400-
for( threshold in 200 downTo 100 step 20 ) {
401-
hLines.clear()
402-
vLines.clear()
403-
404-
val lines = Mat()
405-
HoughLinesP(edges, lines, 1.0, PI / 1024, threshold, 300.0, 100.0)
406-
if (lines.empty()) continue
407-
428+
val lines = Mat()
429+
HoughLinesP(edges, lines, 1.0, PI / 360, 10, 100.0, 100.0)
430+
if (!lines.empty()) {
408431
for (lineIndex in 0 until lines.rows()) {
409-
val startPoint = Point( lines.get(lineIndex, 0)[0].toInt(), lines.get(lineIndex, 0)[1].toInt() )
410-
val endPoint = Point( lines.get(lineIndex, 0)[2].toInt(), lines.get(lineIndex, 0)[3].toInt() )
432+
val line = lines.get(lineIndex, 0)
433+
val startPoint = Point( line[0].toInt(), line[1].toInt() )
434+
val endPoint = Point( line[2].toInt(), line[3].toInt() )
411435
val delta = Point( abs(endPoint.x - startPoint.x), abs(endPoint.y - startPoint.y) )
412436
val ratio = min(delta.x, delta.y).toFloat() / max(delta.x, delta.y)
413437

414-
// avoid lines that are too harsh
438+
// avoid lines that are too close to 45°
415439
if (ratio >= 0.3) continue
416440

441+
Log.i("ABCDEFG", "dx: ${delta.x}, dy: ${delta.y}")
442+
417443
if (delta.x > delta.y) {
418444
if (startPoint.y >= minValue && endPoint.y >= minValue && startPoint.y <= maxValue && endPoint.y <= maxValue) {
419445
hLines.add( Pair(startPoint, endPoint) )
@@ -424,8 +450,6 @@ class MainFragment(activity: MainActivity) : AppFragment(activity) {
424450
}
425451
}
426452
}
427-
428-
if (hLines.size >= 2 && vLines.size >= 2) break
429453
}
430454

431455
val hLineTop: Pair<Point, Point>

app/src/main/java/com/dan/perspective/PreviewFragment.kt

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,55 @@ import android.os.Bundle
55
import android.view.*
66
import com.dan.perspective.databinding.PreviewFragmentBinding
77

8-
class PreviewFragment(activity: MainActivity, private val bitmap: Bitmap) : AppFragment(activity) {
8+
class PreviewFragment(
9+
activity: MainActivity,
10+
private val bitmap: Bitmap,
11+
private val bitmapCropped: Bitmap,
12+
private val sharedParams: SharedParams
13+
) : AppFragment(activity) {
14+
915
companion object {
10-
fun show(activity: MainActivity, bitmap: Bitmap) {
11-
activity.pushView( "Perspective Preview", PreviewFragment(activity, bitmap) )
16+
fun show(activity: MainActivity, bitmap: Bitmap, bitmapCropped: Bitmap, sharedParams: SharedParams) {
17+
activity.pushView(
18+
"Perspective Preview",
19+
PreviewFragment(activity, bitmap, bitmapCropped, sharedParams)
20+
)
1221
}
1322
}
1423

24+
private var _binding: PreviewFragmentBinding? = null
25+
26+
private fun updateImage() {
27+
val binding = _binding ?: return
28+
29+
binding.imagePreview.setBitmap(
30+
if (sharedParams.cropped)
31+
bitmapCropped
32+
else
33+
bitmap
34+
)
35+
}
36+
1537
override fun onCreateView(
1638
inflater: LayoutInflater,
1739
container: ViewGroup?,
1840
savedInstanceState: Bundle?
1941
): View {
2042
val binding = PreviewFragmentBinding.inflate(inflater)
21-
binding.imagePreview.setBitmap(bitmap)
43+
binding.switchCropped.isChecked = sharedParams.cropped
44+
_binding = binding
45+
updateImage()
46+
47+
binding.switchCropped.setOnCheckedChangeListener { _, isChecked ->
48+
sharedParams.cropped = isChecked
49+
updateImage()
50+
}
51+
2252
return binding.root
2353
}
54+
55+
override fun onDestroyView() {
56+
_binding = null
57+
super.onDestroyView()
58+
}
2459
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.dan.perspective
2+
3+
class SharedParams {
4+
var cropped = false
5+
}

app/src/main/res/layout/main_fragment.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@
4141
android:layout_height="wrap_content"
4242
android:layout_weight="1" />
4343

44+
<Switch
45+
android:id="@+id/switchCrop"
46+
android:layout_width="wrap_content"
47+
android:layout_height="wrap_content"
48+
android:text="Crop" />
49+
50+
<TextView
51+
android:id="@+id/textView6"
52+
android:layout_width="wrap_content"
53+
android:layout_height="wrap_content"
54+
android:layout_weight="1" />
55+
4456
<Button
4557
android:id="@+id/buttonPreview"
4658
style="@style/Widget.MaterialComponents.Button.OutlinedButton"

app/src/main/res/layout/preview_fragment.xml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,32 @@
99
android:layout_height="match_parent"
1010
android:orientation="vertical">
1111

12+
<LinearLayout
13+
android:layout_width="match_parent"
14+
android:layout_height="wrap_content"
15+
android:orientation="horizontal">
16+
17+
<TextView
18+
android:id="@+id/textView"
19+
android:layout_width="wrap_content"
20+
android:layout_height="wrap_content"
21+
android:layout_weight="1" />
22+
23+
<Switch
24+
android:id="@+id/switchCropped"
25+
android:layout_width="wrap_content"
26+
android:layout_height="wrap_content"
27+
android:paddingTop="16dp"
28+
android:paddingBottom="16dp"
29+
android:text="Crop" />
30+
31+
<TextView
32+
android:id="@+id/textView5"
33+
android:layout_width="wrap_content"
34+
android:layout_height="wrap_content"
35+
android:layout_weight="1" />
36+
</LinearLayout>
37+
1238
<com.dan.perspective.TouchImageView
1339
android:id="@+id/imagePreview"
1440
android:layout_width="match_parent"

0 commit comments

Comments
 (0)