Skip to content

Commit e896780

Browse files
authored
feat: support high resolution image outputs when possible (#147)
If the config preference is set to maximize quality, then also consider high resolution outputs. Switch to using a ResolutionSelector instead of the deprecated setTargetResolution API, as some of the higher resolution modes are only supported that way. Refs: #20
1 parent 9d357bb commit e896780

File tree

3 files changed

+42
-14
lines changed

3 files changed

+42
-14
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Added
9+
- Support high resolution output modes if the config preference is set to maximize quality. ([#20])
810

911
## [1.1.0] - 2025-07-21
1012
### Changed
@@ -39,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3941
### Added
4042
- Initial release
4143

44+
[#20]: https://github.com/FossifyOrg/Camera/issues/20
4245
[#97]: https://github.com/FossifyOrg/Camera/issues/97
4346

4447
[Unreleased]: https://github.com/FossifyOrg/Camera/compare/1.1.0...HEAD

app/src/main/kotlin/org/fossify/camera/helpers/ImageQualityManager.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import androidx.appcompat.app.AppCompatActivity
88
import androidx.camera.core.CameraSelector
99
import org.fossify.camera.extensions.config
1010
import org.fossify.camera.models.CameraSelectorImageQualities
11+
import org.fossify.camera.models.CaptureMode
1112
import org.fossify.camera.models.MySize
1213
import org.fossify.commons.extensions.showErrorToast
1314

@@ -32,8 +33,14 @@ class ImageQualityManager(private val activity: AppCompatActivity) {
3233
val configMap =
3334
characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
3435
?: continue
35-
val imageSizes = configMap.getOutputSizes(ImageFormat.JPEG)
36+
val standardImageSizes = configMap.getOutputSizes(ImageFormat.JPEG)
3637
.map { MySize(it.width, it.height) }
38+
val imageSizes = if (activity.config.captureMode == CaptureMode.MAXIMIZE_QUALITY) {
39+
standardImageSizes + configMap.getHighResolutionOutputSizes(ImageFormat.JPEG)
40+
.map { MySize(it.width, it.height) }
41+
} else {
42+
standardImageSizes
43+
}
3744
val cameraSelector = lensFacing.toCameraSelector()
3845
imageQualities.add(CameraSelectorImageQualities(cameraSelector, imageSizes))
3946
}

app/src/main/kotlin/org/fossify/camera/implementations/CameraXPreview.kt

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ import androidx.camera.core.Preview
3636
import androidx.camera.core.UseCase
3737
import androidx.camera.core.UseCaseGroup
3838
import androidx.camera.core.ViewPort
39+
import androidx.camera.core.resolutionselector.ResolutionSelector
40+
import androidx.camera.core.resolutionselector.ResolutionSelector.PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION
41+
import androidx.camera.core.resolutionselector.ResolutionSelector.PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE
42+
import androidx.camera.core.resolutionselector.ResolutionStrategy
43+
import androidx.camera.core.resolutionselector.ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER
3944
import androidx.camera.lifecycle.ProcessCameraProvider
4045
import androidx.camera.video.FallbackStrategy
4146
import androidx.camera.video.FileDescriptorOutputOptions
@@ -86,6 +91,7 @@ import org.fossify.commons.activities.BaseSimpleActivity
8691
import org.fossify.commons.extensions.toast
8792
import org.fossify.commons.helpers.PERMISSION_ACCESS_FINE_LOCATION
8893
import org.fossify.commons.helpers.ensureBackgroundThread
94+
import kotlin.math.abs
8995

9096
class CameraXPreview(
9197
private val activity: BaseSimpleActivity,
@@ -222,10 +228,10 @@ class CameraXPreview(
222228
val isFullSize = resolution.isFullScreen
223229
previewView.scaleType = if (isFullSize) ScaleType.FILL_CENTER else ScaleType.FIT_CENTER
224230
val rotation = previewView.display.rotation
225-
val rotatedResolution = getRotatedResolution(resolution, rotation)
231+
val targetResolution = Size(resolution.width, resolution.height)
226232

227-
val previewUseCase = buildPreview(rotatedResolution, rotation)
228-
val captureUseCase = getCaptureUseCase(rotatedResolution, rotation)
233+
val previewUseCase = buildPreview(targetResolution, rotation)
234+
val captureUseCase = getCaptureUseCase(targetResolution, rotation)
229235

230236
cameraProvider.unbindAll()
231237
camera = if (isFullSize) {
@@ -258,18 +264,10 @@ class CameraXPreview(
258264
setFlashlightState(config.flashlightState)
259265
}
260266

261-
private fun getRotatedResolution(resolution: MySize, rotationDegrees: Int): Size {
262-
return if (rotationDegrees == Surface.ROTATION_0 || rotationDegrees == Surface.ROTATION_180) {
263-
Size(resolution.height, resolution.width)
264-
} else {
265-
Size(resolution.width, resolution.height)
266-
}
267-
}
268-
269267
private fun buildPreview(resolution: Size, rotation: Int): Preview {
270268
return Preview.Builder()
271269
.setTargetRotation(rotation)
272-
.setTargetResolution(resolution)
270+
.setResolutionSelector(getResolutionSelector(resolution))
273271
.build().apply {
274272
setSurfaceProvider(previewView.surfaceProvider)
275273
}
@@ -295,7 +293,7 @@ class CameraXPreview(
295293
.setFlashMode(flashMode)
296294
.setJpegQuality(config.photoQuality)
297295
.setTargetRotation(rotation)
298-
.setTargetResolution(resolution)
296+
.setResolutionSelector(getResolutionSelector(resolution))
299297
.build()
300298
}
301299

@@ -306,6 +304,26 @@ class CameraXPreview(
306304
}
307305
}
308306

307+
private fun getResolutionSelector(resolution: Size): ResolutionSelector {
308+
return ResolutionSelector.Builder()
309+
.setResolutionStrategy(ResolutionStrategy(resolution, FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER))
310+
.setResolutionFilter { supportedSizes, rotationDegrees ->
311+
// Sort by closest image ratio
312+
supportedSizes.sortedBy {
313+
size -> abs(size.width / size.height.toFloat() - resolution.width / resolution.height.toFloat())
314+
}
315+
}
316+
.setAllowedResolutionMode(getAllowedResolutionMode())
317+
.build()
318+
}
319+
320+
private fun getAllowedResolutionMode(): Int {
321+
return when (config.captureMode) {
322+
CaptureMode.MINIMIZE_LATENCY -> PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION
323+
CaptureMode.MAXIMIZE_QUALITY -> PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE
324+
}
325+
}
326+
309327
private fun buildVideoCapture(): VideoCapture<Recorder> {
310328
val qualitySelector = QualitySelector.from(
311329
videoQualityManager.getUserSelectedQuality(cameraSelector).toCameraXQuality(),

0 commit comments

Comments
 (0)