Shadow correction: Retinex-based processing to preserve flat tint areas
This commit is contained in:
@@ -22,6 +22,7 @@ import org.opencv.core.MatOfDouble
|
|||||||
import org.opencv.core.Scalar
|
import org.opencv.core.Scalar
|
||||||
import org.opencv.core.Size
|
import org.opencv.core.Size
|
||||||
import org.opencv.imgproc.Imgproc
|
import org.opencv.imgproc.Imgproc
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
fun enhanceCapturedImage(img: Mat): Mat {
|
fun enhanceCapturedImage(img: Mat): Mat {
|
||||||
return if (isColoredDocument(img)) {
|
return if (isColoredDocument(img)) {
|
||||||
@@ -31,8 +32,8 @@ fun enhanceCapturedImage(img: Mat): Mat {
|
|||||||
result
|
result
|
||||||
} else {
|
} else {
|
||||||
Log.i("PostProcessing", "grayscale document")
|
Log.i("PostProcessing", "grayscale document")
|
||||||
val gray = correctLighting(img)
|
val gray = multiScaleRetinex(img)
|
||||||
val contrastedGray = enhanceContrastGray(gray)
|
val contrastedGray = enhanceContrastAuto(gray)
|
||||||
val result = Mat()
|
val result = Mat()
|
||||||
Imgproc.cvtColor(contrastedGray, result, Imgproc.COLOR_GRAY2BGR)
|
Imgproc.cvtColor(contrastedGray, result, Imgproc.COLOR_GRAY2BGR)
|
||||||
result
|
result
|
||||||
@@ -54,35 +55,106 @@ fun isColoredDocument(img: Mat, threshold: Double = 4.0): Boolean {
|
|||||||
return result > threshold
|
return result > threshold
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO the radius should depend on the image size
|
private fun multiScaleRetinex(img: Mat, kernelSizes: List<Double> = listOf(30.0, 500.0)): Mat {
|
||||||
fun correctLighting(img: Mat, radius: Int = 100): Mat {
|
// Convert to grayscale (1 channel)
|
||||||
val gray = Mat()
|
val gray = Mat()
|
||||||
|
if (img.channels() == 4) {
|
||||||
|
Imgproc.cvtColor(img, gray, Imgproc.COLOR_BGRA2GRAY)
|
||||||
|
} else if (img.channels() == 3) {
|
||||||
Imgproc.cvtColor(img, gray, Imgproc.COLOR_BGR2GRAY)
|
Imgproc.cvtColor(img, gray, Imgproc.COLOR_BGR2GRAY)
|
||||||
|
} else {
|
||||||
|
img.copyTo(gray)
|
||||||
|
}
|
||||||
|
|
||||||
|
val imgFloat = Mat()
|
||||||
|
gray.convertTo(imgFloat, CvType.CV_32F)
|
||||||
|
Core.add(imgFloat, Scalar(1.0), imgFloat) // img + 1
|
||||||
|
|
||||||
|
val weight = 1.0 / kernelSizes.size
|
||||||
|
val retinex = Mat.zeros(gray.size(), CvType.CV_32F)
|
||||||
|
|
||||||
|
val logImg = Mat()
|
||||||
|
Core.log(imgFloat, logImg)
|
||||||
|
|
||||||
val blur = Mat()
|
val blur = Mat()
|
||||||
val kernelSize = 2 * radius + 1
|
val logBlur = Mat()
|
||||||
Imgproc.GaussianBlur(gray, blur, Size(kernelSize.toDouble(), kernelSize.toDouble()), 0.0)
|
val diff = Mat()
|
||||||
|
|
||||||
val normalized = Mat()
|
for (kernelSize in kernelSizes) {
|
||||||
Core.divide(gray, blur, normalized, 255.0)
|
Imgproc.boxFilter(imgFloat, blur, -1, Size(kernelSize, kernelSize))
|
||||||
return normalized
|
Core.add(blur, Scalar(1.0), blur)
|
||||||
|
Core.log(blur, logBlur)
|
||||||
|
|
||||||
|
Core.subtract(logImg, logBlur, diff)
|
||||||
|
val diffGray = Mat()
|
||||||
|
if (diff.channels() > 1) {
|
||||||
|
Imgproc.cvtColor(diff, diffGray, Imgproc.COLOR_BGRA2GRAY)
|
||||||
|
} else {
|
||||||
|
diff.copyTo(diffGray)
|
||||||
|
}
|
||||||
|
Core.addWeighted(retinex, 1.0, diffGray, weight, 0.0, retinex)
|
||||||
|
diffGray.release()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enhanceContrastGray(img: Mat): Mat {
|
// Normalize
|
||||||
val flat = img.reshape(0, 1)
|
val minMax = Core.minMaxLoc(retinex)
|
||||||
val sorted = Mat()
|
val normalized = Mat()
|
||||||
Core.sort(flat, sorted, Core.SORT_ASCENDING)
|
Core.subtract(retinex, Scalar(minMax.minVal), normalized)
|
||||||
|
val scale = if (minMax.maxVal > minMax.minVal) 255.0 / (minMax.maxVal - minMax.minVal) else 1.0
|
||||||
|
Core.multiply(normalized, Scalar(scale), normalized)
|
||||||
|
|
||||||
val totalPixels = sorted.cols()
|
val result = Mat()
|
||||||
val pLow = sorted[0, (totalPixels * 0.01).toInt()][0]
|
normalized.convertTo(result, CvType.CV_8U)
|
||||||
val pHigh = sorted[0, (totalPixels * 0.95).toInt()][0]
|
|
||||||
|
// Cleanup
|
||||||
|
gray.release()
|
||||||
|
imgFloat.release()
|
||||||
|
retinex.release()
|
||||||
|
logImg.release()
|
||||||
|
blur.release()
|
||||||
|
logBlur.release()
|
||||||
|
diff.release()
|
||||||
|
normalized.release()
|
||||||
|
|
||||||
val result = Mat(img.size(), img.type())
|
|
||||||
img.convertTo(result, CvType.CV_32F)
|
|
||||||
Core.subtract(result, Scalar(pLow), result)
|
|
||||||
Core.multiply(result, Scalar(255.0 * 1.03 / (pHigh - pLow)), result)
|
|
||||||
Core.min(result, Scalar(255.0), result)
|
|
||||||
Core.max(result, Scalar(0.0), result)
|
|
||||||
result.convertTo(result, CvType.CV_8U)
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun enhanceContrastAuto(img: Mat): Mat {
|
||||||
|
val gray = if (img.channels() == 1) img else {
|
||||||
|
val tmp = Mat()
|
||||||
|
Imgproc.cvtColor(img, tmp, Imgproc.COLOR_BGR2GRAY)
|
||||||
|
tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flatten and sort pixel values
|
||||||
|
val flat = Mat()
|
||||||
|
gray.reshape(1, 1).convertTo(flat, CvType.CV_32F)
|
||||||
|
val sortedVals = Mat()
|
||||||
|
Core.sort(flat, sortedVals, Core.SORT_ASCENDING)
|
||||||
|
|
||||||
|
val totalPixels = sortedVals.cols()
|
||||||
|
val pLow = sortedVals.get(0, (totalPixels * 0.005).toInt())[0]
|
||||||
|
val pHigh = sortedVals.get(0, (totalPixels * 0.80).toInt())[0]
|
||||||
|
|
||||||
|
flat.release()
|
||||||
|
sortedVals.release()
|
||||||
|
|
||||||
|
val imgF = Mat()
|
||||||
|
img.convertTo(imgF, CvType.CV_32F)
|
||||||
|
val adjusted = Mat()
|
||||||
|
Core.subtract(imgF, Scalar(pLow), adjusted)
|
||||||
|
Core.multiply(adjusted, Scalar(255.0 / max((pHigh - pLow), 1.0)), adjusted)
|
||||||
|
Core.min(adjusted, Scalar(255.0), adjusted)
|
||||||
|
Core.max(adjusted, Scalar(0.0), adjusted)
|
||||||
|
|
||||||
|
val result = Mat()
|
||||||
|
adjusted.convertTo(result, CvType.CV_8U)
|
||||||
|
imgF.release()
|
||||||
|
adjusted.release()
|
||||||
|
|
||||||
|
val final = Mat()
|
||||||
|
Core.convertScaleAbs(result, final, 1.15, -25.0)
|
||||||
|
result.release()
|
||||||
|
|
||||||
|
return final
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user