2023. 3. 26. 00:26ㆍDevelopers 공간 [Shorts]/Frontend
<분류>
A. 수단
- OS/Platform/Tool : Linux, Kubernetes(k8s), Docker, AWS
- Package Manager : node.js, yarn, brew,
- Compiler/Transpillar : React, Nvcc, gcc/g++, Babel, Flutter
- Module Bundler : React, Webpack, Parcel
B. 언어
- C/C++, python, Javacsript, Typescript, Go-Lang, CUDA, Dart, HTML/CSS
C. 라이브러리 및 프레임워크 및 SDK
- OpenCV, OpenCL, FastAPI, PyTorch, Tensorflow, Nsight
1. What? (현상)
Flutter의 Camera 패키지를 활용해 촬영하는 경우, CameraPreview를 위치시키는데, CameraPreview가 위치하는 그대로 화면을 저장하는 것이 생각보다 쉽지 않았습니다.
가장 먼저 생각한 것은 아래 그림에서 두번째 방법으로 된 것 과 같이 해당 Preview를 LayoutBuilder를 활용해 해당 크기로 우겨넣는 방법으로 해보았습니다.
LayoutBuilder(
builder: (context, constraints) {
return SizedBox(
width: constraints.maxWidth,
height: constraints.maxHeight,
child: CameraPreview(camController),
);
},
하지만 위와 같은 방식으로 구현하는 경우 아마 아래와 같은 에러메시지가 나올 뿐 아니라, 카메라 Preview또한 찌그러진 화면으로 보일 겁니다. 왜냐면 CameraPreview 전체를 모두 Resize 해서 내가 원하는 공간으로 찌그러트려 넣기 때문입니다.
Height of Platform View type: [plugins.flutter.io/camera_1] may not be set.
Defaulting to `height: 100%`.
Set `style.height` to any appropriate value to stop this message.
Width of Platform View type: [plugins.flutter.io/camera_1] may not be set.
Defaulting to `width: 100%`.
Set `style.width` to any appropriate value to stop this message.
따라서 다음에는 위와 그림에서 세번째 방법과 같이 CameraPreview를 밑에 깔아 놓고 내가 원하는 레이아웃을 위에 쌓는 방법으로 구현해보았습니다.
Stack(
children: [
CameraPreview(viewModel.camController);
Container(
height: p2_1_Height_Padding,
color: kMainColor,
),
)
하지만 이 방법을 활용했을 때의 문제는 "보이는 곳"만 저장을 해야하는데 어떻게 저장해야할 지 모르곘다는 것입니다. 다시 설명하자면, 먼저 보통 저장할 때 아래와 같은 코드를 구현해 만들 것입니다.
-----------------------------------------------------------------------------------------------------
<Flutter에서 Camera를 활용해 촬영하는 두가지 방법>
- 1. Image Picker를 활용하는 방법 : ScreenShot등을 활용하기 위한 ImagePicker 패키지를 활용해 촬영하는 방법입니다. 하지만 아래 사진과 같이 바로 찍히는 것이 아닌, 촬영을 위한 page로 navigate 해서 촬영하기 때문에, 직접 만들 때는 불편한 부분이 있습니다.
import 'package:image_picker/image_picker.dart';
Future<XFile?> takePicture() async {
final ImagePicker _picker = ImagePicker();
final XFile? photo = await _picker.pickImage(source: ImageSource.camera);
return photo
}
- 2. 기본적인 camera Controller를 활용하는 방법 : 우리가 생각한 것처럼 바로 사진을 얻어내는 방법입니다.
Future<XFile?> takePicture() async {
if (camController == null || !camController.value.isInitialized) {
print('Error');
return null;
}
if (camController.value.isTakingPicture) {
// A capture is already pending, do nothing.
print('Error');
return null;
}
try {
final XFile photo = await camController.takePicture();
return photo;
} on CameraException catch (e) {
print('Error');
return null;
}
}
-----------------------------------------------------------------------------------------------------
takePicture() 함수에 대한 자세한 내용은 이 글과 관련성이 적어 위의 더보기를 눌러 참조하시면 좋습니다.
촬영후에 얻은 파일은 XFile이라는 포맷입니다.
이 때 화면의 크기와 똑같이 이미지 상에서의 위치를 찾아서 crop을 해주어야하는데, imageSize와 화면크기(physical, logical pixel 모두)와는 전혀 독립적인 값이어서 계산해서 crop하기가 쉽지 않다는 것입니다.
아래 예시에서는 logical pixel에 있어서 전체 높이에 대한 비율을 활용해 image height로 비율적으로 잘라내려고 시도했으나, 정확하지 않았습니다.
void onShutterPressed(BuildContext context) async {
XFile? tempfile = await takePicture();
if (tempfile != null) {
ImageProperties properties = await FlutterNativeImage.getImageProperties(tempfile.path);
imageFile = await FlutterNativeImage.cropImage(tempfile.path, 0,
(properties.height!.toDouble()*(p2_1_Height_Padding(context)+p2_1_Height_Top)/MediaQuery.of(context).size.height).toInt(), properties.width!.toInt(), properties.width!.toInt());
if (imageFile != null)
print("Good");
} else {
print("Error");
}
}
따라서, 어떻게 화면을 구성하고, 저장해야 보이는 부분만 카메라로 저장할 수 있을지를 정리해보고자 합니다.
참고로 카메라를 활용해 얻은 파일을 저장할 때 쓰는 포맷들은 아래 더보기를 클릭해 참조하실 수 있습니다.
-----------------------------------------------------------------------------------------------------
<File Format의 종류와 그 사이의 변환 방법>
- 기본적인 File
- XFile file1
- XFile --> File : File(file1.path)
- File file2
- File --> File : File(file2.path)
- File --> Image : Image.file(file2)
- File --> Uint8List : file2.readAsBytesSync()
- XFile file1
- 이미지와 관련된 Decoded File
- (import 'package:image/src/image/image.dart') 기본 Image file3
- (import 'dart.ui') ui.Image file4
- ui.image --> ByteData : file4.toByteData(format: ui.ImageByteFormat.png)
- (import 'package:image/image.dart' as Img) Image file5
- Img --> Uint8List : file5.toUint8List()
- Img --> Uint8List : img.encodePng(file5)
- Binary와 관련된 Encoded File
- Uint8List file6
- Uint8List --> File : file2.writeAsBytes(file6)
- UInt8List --> Image : decodeImageFromList(file6)
- UInt8List --> Img : img.decodeImage(file6)
- ByteData file7
- ByteData --> Uint8List : file7.buffer.asUint8List();
- Uint8List file6
<Image와 관련된 File에서 사이즈를 얻는 방법>
- 기본적인 방법 (이미지와 무관)
- MediaQuery.of(context).size.height, MediaQuery.of(context).size.width
- WidgetsBinding.instance.window.physicalSize.height
- window.physicalSize.height
- 이미지 패키지를 활용한 방법
- ImageProperties structure활용 : path를 알고 있을 때
- ImageProperties properties = FlutterNativeImage.getImageProperties(file2.path)
- properties.height, properties.width
- 기본 Image의 member 변수 : path를 알고 있을 때
- var decodedImage = await decodeImageFromList(File(file2.path).readAsBytesSync());
- decodedImage.width, decodedImage.height
- ImageProperties structure활용 : path를 알고 있을 때
-----------------------------------------------------------------------------------------------------
2. Why? (원인)
- X
3. How? (해결책)
- 앞서 말씀드렸듯이, 얻어낸 이미지의 정확한 위치를 화면 크기를 통한 계산을 통해 Crop하기는 어렵습니다.
- 먼저, 화면 구성은 위 그림중 "세 번째 방법"이 아닌, "두번째 방법"과 같이 각자의 화면 구성으로 하되, LayoutBuilder를 사용하지 않고, 아래 코드와 같이 해당 위치에 view를 중앙에 위치시키기만 할 것 입니다.
- AspectRatio :child 크기를 특정 가로 세로 비율에 맞게 지정하는 위젯
- ClipRect :각진 모서리를 둥글게 만들고 을 때 사용하는 위젯이지만, 기본적으로 원하는 view의 바깥부분에 대해 crop하지 않고 painting만을 하지 않도록 막는 기능이 있으며, 아래에서는 우리가 원하는 부분만 painting하도록 하기 위해 사용하고 있습니다.
- Transform.scale() : 2D 평면을 따라 자식의 크기를 조정하는 위젯을 만드는 데에 쓰입니다.
return
AspectRatio(
aspectRatio: 1,
child: ClipRect(
child: Transform.scale(
scale: viewModel.camController.value.aspectRatio,
child: Center(
child: CameraPreview(
viewModel.camController
),
),
),
),
);
- "중앙"에 위치시켰으므로, 촬영시에 중앙을 기준으로 잘라내면 됩니다.
- 아래 cropSize를 min으로 구성한 이유는 width와 height과 바뀌었을 때를 대비하기 위함입니다.
- 중앙에 위치시키고, 찌그러 트리지는 않았으므로, 높이와 너비의 비율을 활용해 정확하게 보이는 부분만 crop해서 얻어 낼 수 있습니다.(이 글의 예시는 좌우가 1:1일 때를 예시로 보였습니다.)
import 'package:flutter_native_image/flutter_native_image.dart';
void onShutterPressed(BuildContext context) async {
XFile? tempfile = await takePicture();
if (tempfile != null) {
ImageProperties properties = await FlutterNativeImage.getImageProperties(tempfile.path);
var cropSize = min(properties.width!, properties.height!);
int offsetX = (properties.width! - cropSize) ~/2;
int offsetY = (properties.height! - cropSize) ~/2;
imageFile = await FlutterNativeImage.cropImage(tempfile.path, offsetX, offsetY, cropSize, cropSize);
if (imageFile != null)
print("Good");
} else {
print("Error");
}
}
아래 더보기는 다른 Crop 방법(copyCrop)을 활용한 방법입니다.
** bakeOrientation() 함수는 가끔씩 device에서 현재 Orientation과 다르게 촬영되는 경우를 방지하기 위한 함수입니다.
-----------------------------------------------------------------------------------------------------
import 'package:image/image.dart' as img;
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
void onShutterPressed(BuildContext context) async {
XFile? tempfile = await takePicture();
if (tempfile != null) {
ImageProperties properties = await FlutterNativeImage.getImageProperties(tempfile.path);
var cropSize = min(properties.width!, properties.height!);
int offsetX = (properties.width! - cropSize) ~/2;
int offsetY = (properties.height! - cropSize) ~/2;
final img.Image? capturedImage = img.decodeImage(await tempfile.readAsBytes());
if(capturedImage==null){
print("Error");
return;
}
final img.Image orientedImage = img.bakeOrientation(capturedImage);
final img.Image croppedImage = img.copyCrop(
orientedImage,
x: offsetX,
y: offsetY,
width: cropSize,
height: cropSize
);
final temppath = join(
(await getTemporaryDirectory()).path,
'temptk.png',
);
File imageFile = File(temppath);
await imageFile!.writeAsBytes(img.encodePng(croppedImage));
if (imageFile != null)
print("Good");
} else {
print("Error");
}
}
-----------------------------------------------------------------------------------------------------
https://stackoverflow.com/questions/51348166/how-to-square-crop-a-flutter-camera-preview
https://blog.logrocket.com/flutter-camera-plugin-deep-dive-with-examples/#retrieve-image-video-files
https://pub.dev/documentation/image/latest/image/copyCrop.html
'Developers 공간 [Shorts] > Frontend' 카테고리의 다른 글
[Flutter] Portrait, Landscape mode 막기 (0) | 2023.03.28 |
---|---|
[Flutter] ios 셋팅 수정 및 다양한 상황에서 문제 해결하기 (0) | 2023.03.27 |
[Flutter] 이미지와 파일을 위한 Storage 다루기 (0) | 2023.03.25 |
[Flutter] Flutter에서 KakaoMap WebView API 활용하기 (0) | 2023.03.24 |
[Flutter] Stacked 패키지 Reactive Provider 설계하기 (0) | 2023.03.19 |