0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

インスタ映えも! Flutter(Dart)とDjango(Python)を使って、簡単な写真加工アプリ(撮影機能あり)を作る方法

Last updated at Posted at 2025-01-01

Flutterで、写真を撮影するフォームを作り、その撮影した写真をバックエンドであるPythonに送り、Pythonの画像加工の技術を使って、セピア色にするのを作っていきたいと思います。

エディターは、Android Studioを使っていきます。

AD_4nXcbQtneDPpZqJ86SjGOnAoHMrDEj2lcfXl_gC_LE6y7ojOq58BhkvfKWH51wKPZs84uHjWxOHO_f8W8sDk7YRfpqjNgqLrtwyVzGnnLzkAxr_IhUWBXtn_k.jpg

Android StudioにFlutterの環境構築をするには、 こちらのサイトを参考にするといいです↓
https://note.com/qkuronekop/n/nb78b7c642d6a

Flutterの環境構築ができましたら、プロジェクトを作成していきます。

今回のプロジェクト名は、photoにします。

AD_4nXc-RiaswPof7PwxnHc7zk-_4aSmfRd4jLVvOAwpOYy_5fydkCztSDePBVb6GdUIg2xthwHLlJGloa2CIOVnw8J-_pc0bqSgbOJHhKXu3dFW9YuNC1oEmXmb.jpg

とりあえず、まずは、写真が撮影できるアプリを作成します。

実装では、以下のパッケージ

・camera: デバイス上のカメラを操作するのに必要
・path provider: 画像の保存場所を見つけてくれる
・path: 画像の保存先を作成してくれる
・video player: ビデオを再生してくれる

が必要となるので、コマンドで、

$ flutter pub get
$ flutter pub add path_provider
$ flutter pub add path
$ flutter pub add video_player

※ $マークは、コマンドライン(ターミナル)での入力を意味しているので、無視してください。

を打ってください。

android/app/build.gradleファイルのminSdkVersionを以下のように変更します。

android/app/build.gradle
plugins {
    id "com.android.application"
    id "kotlin-android"
    id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
    localPropertiesFile.withReader('UTF-8') { reader ->
        localProperties.load(reader)
    }
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
    flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
    flutterVersionName = '1.0'
}
android {
    namespace "com.example.photo"
    compileSdk flutter.compileSdkVersion
    ndkVersion flutter.ndkVersion
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
    }
    defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId "com.example.photo"
        // You can update the following values to match your application needs.
        // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
- minSdkVersion flutter.minSdkVersion
+ minSdkVersion 21
        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }
    buildTypes {
        release {
            // TODO: Add your own signing config for the release build.
            // Signing with the debug keys for now, so `flutter run --release` works.
            signingConfig signingConfigs.debug
        }
    }
}
flutter {
    source '../..'
}
dependencies {}

にします。

そして、カメラ機能を使う際に、許可を要求するための文言を追加します。

そのために、
ios/Runner/Info.plistを

ios/Runner/Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CADisableMinimumFrameDurationOnPhone</key>
	<true/>
	<key>CFBundleDevelopmentRegion</key>
	<string>$(DEVELOPMENT_LANGUAGE)</string>
	<key>CFBundleDisplayName</key>
	<string>Photo</string>
	<key>CFBundleExecutable</key>
	<string>$(EXECUTABLE_NAME)</string>
	<key>CFBundleIdentifier</key>
	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleName</key>
	<string>photo</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleShortVersionString</key>
	<string>$(FLUTTER_BUILD_NAME)</string>
	<key>CFBundleSignature</key>
	<string>????</string>
	<key>CFBundleVersion</key>
	<string>$(FLUTTER_BUILD_NUMBER)</string>
	<key>LSRequiresIPhoneOS</key>
	<true/>
+ <key>NSCameraUsageDescription</key>
+ <string>撮影をするために、カメラ機能を使う必要があります。</string>
+ <key>NSMicrophoneUsageDescription</key>
+ <string>撮影をする際に、マイク機能を使う場合があります。</string>
	<key>UIApplicationSupportsIndirectInputEvents</key>
	<true/>
	<key>UILaunchStoryboardName</key>
	<string>LaunchScreen</string>
	<key>UIMainStoryboardFile</key>
	<string>Main</string>
	<key>UISupportedInterfaceOrientations</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
		<string>UIInterfaceOrientationLandscapeLeft</string>
		<string>UIInterfaceOrientationLandscapeRight</string>
	</array>
	<key>UISupportedInterfaceOrientations~ipad</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
		<string>UIInterfaceOrientationPortraitUpsideDown</string>
		<string>UIInterfaceOrientationLandscapeLeft</string>
		<string>UIInterfaceOrientationLandscapeRight</string>
	</array>
</dict>
</plist>

というふうに変更します。

次に、lib/main.dartを、

lib/main.dart

import 'dart:io';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
Future<void> main() async {
  // Flutterのバインディングを初期化する。この関数を呼び出すことで、Flutterが完全に起動したことが保証される
  WidgetsFlutterBinding.ensureInitialized();
  // 使用可能なカメラを取得する非同期処理
  final cameras = await availableCameras();
  // 最初のカメラデバイスを取得(一般的には、デバイスに複数のカメラがある場合、最初に使うカメラを選択)
  final firstCamera = cameras.first;
  // 取得できているか確認
  print(firstCamera);
  // アプリケーションを実行する
  runApp(MyApp(camera: firstCamera));
}
class MyApp extends StatelessWidget {
  const MyApp({
    Key? key,
    required this.camera,
}) : super(key: key);
  // カメラ情報(デバイスのカメラ)
  final CameraDescription camera;
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'カメラ機能',
      theme: ThemeData(),
      home: TakePictureScreen(camera: camera),
    );
  }
}
// 写真撮影画面
class TakePictureScreen extends StatefulWidget {
  const TakePictureScreen({
    Key? key,
    required this.camera, // 使用するカメラ情報を受け取る
  }) : super(key: key);
  // カメラ情報
  final CameraDescription camera;
  @override
  TakePictureScreenState createState() => TakePictureScreenState();
}
class TakePictureScreenState extends State<TakePictureScreen> {
  late CameraController _controller; // カメラの操作を管理するコントローラー
  late Future<void> _initializeControllerFuture; // カメラの初期化を待つ
  @override
  void initState() {
    super.initState();
    // CameraControllerを初期化。使用するカメラと解像度を指定。
    _controller = CameraController(
      // 受け取ったカメラ情報を使用
    widget.camera,
      // カメラの解像度設定
      ResolutionPreset.medium,
    );
    // カメラの初期化処理を開始し、その完了を待つ
    _initializeControllerFuture = _controller.initialize();
  }
  @override
    void dispose() {
    // 画面が破棄される際にカメラコントローラーも破棄してリソースを解放
      _controller.dispose();
      super.dispose();
    }
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        body: Center(
          child: FutureBuilder<void>(
            // カメラ初期化が完了するのを待つ
            future: _initializeControllerFuture,
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.done) {
                // 初期化が完了したらカメラプレビューを表示
                return CameraPreview(_controller);
              } else {
                // 初期化中はローディングインジケーターを表示
                return const CircularProgressIndicator();
              }
            },
          ),
        ),
        // 写真を撮るためのボタン
        floatingActionButton: FloatingActionButton(
          onPressed: () async {
            // 写真を撮る(非同期処理)
            final image = await _controller.takePicture();
            // 写真を撮った後、次の画面へ遷移(撮影した画像を表示)
            await Navigator.of(context).push(
              MaterialPageRoute(
                builder: (context) => DisplayPictureScreen(imagePath: image.path),
                fullscreenDialog: true,
              ),
            );
          },
          child: const Icon(Icons.camera_alt),// カメラアイコン
        ),
      );
    }
  }
  // 撮影した写真を表示する画面
  class DisplayPictureScreen extends StatelessWidget {
    const DisplayPictureScreen({Key? key, required this.imagePath})
        : super(key: key);
    // 画像のファイルパス
    final String imagePath;
    @override
    Widget build(BuildContext context) {
      return Scaffold(
      appBar: AppBar(title: const Text('この写真でいいですか?')),
      body: Center(
        // 画面中央に撮影した画像を表示
          child: Image.file(File(imagePath))),
      );
  }
  }

にします。

それでは、さっそくテストをしてみましょう。

実機は、お使いのパソコンとスマホを使い、下のような専用のコードで、同期させます。

AD_4nXfdUZDaI576EOJRf8Ihi8DYWhuVg3g1tgeh-zOXaP_zyQsukNF-ANmoY3i15OJgbYbZSGf3L5Lufl9ZGjWK7tSmtm877avSqzXAyrASXvODg7CselgFa0qX.jpg

今回は、iphoneを使います。

そして、この開発ツールとしてXCodeを使います。

AD_4nXcUYSb19_c7WHBw7EMFVAgS3_f2b_sNq8i7iwRb3nlt9gL6kT2PbC_eYKdCBvr8PsiV5aZ0mPvoUth1yf2sgElo9StT2aHJvCdVEQjsexnVV5dZEtlTa6aM.jpg

XCodeの使い方やテストをする際のスマホの環境設定については、
こちらのサイトを参考にしてみてください↓

FlutterやCocoaPodsの依存関係の影響で、テストをする際にエラーが発生する場合があります。
その場合は、
ターミナルで、

$ flutter clean
$ flutter pub get

と入力し、Flutterのキャッシュと依存関係を再取得します。
次に、iOSの依存関係を管理する CocoaPods のインストールを確認します。

CocoaPodsがインストールされていない場合、インストールする方法を以下に記述します。

前提条件

・macOSがインストールされていること。
・Xcodeがインストールされていること。

CocoaPodsのインストール手順

Homebrewを使ったインストール(推奨) HomebrewはmacOS向けのパッケージマネージャーで、CocoaPodsもHomebrewを使ってインストールできます。

もしHomebrewがインストールされていない場合は、まずHomebrewをインストールしてください。

$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

次に、Homebrewを使ってCocoaPodsをインストールします。

$ brew install cocoapods

インストール後、podコマンドが使えるようになります。インストールが成功したことを確認するために、次のコマンドを実行してバージョンを確認できます。

$ pod --version

バージョン番号が表示されれば、インストールは正常に完了しています。

もしHomebrewを使わずにインストールしたい場合は、以下のコマンドを実行します。

sudo gem install cocoapods

インストール後、以下のコマンドでバージョンを確認できます。

$ pod --version

これでpodコマンドが使えるようになります。

CocoaPodsの初期設定

インストール後、最初にCocoaPodsをセットアップするために、以下のコマンドを実行します。

$ pod setup

これにより、CocoaPodsの依存関係を管理するためのローカルリポジトリが作成されます。この操作は初回のみに必要です。

CocoaPodsのキャッシュをクリアしてから、pod install を再度実行してみてください。
やり方は、
ターミナルで、

$ cd ios
$ pod cache clean --all
$ pod install

というコマンドを入力します。
Xcodeの設定を確認
Xcodeの設定で、iOSプロジェクトが適切にビルドされるか確認します。
Xcodeで ios/Runner.xcworkspace を開きます。

$ open ios/Runner.xcworkspace

※ iosディレクトリ内にいる場合は、ios/の部分は入りません。

Xcodeでプロジェクトをクリーンビルドします。

メニューから Product > Clean Build Folder を選択。
その後、Product > Build を実行します。

これで、エラーが解決されるかと思います。

下の画面のように、

AD_4nXeCMEgkUcfNMEwedu1w-FPhiu86hp0HBqdjgppZU9o2dhPdJx7xsAiCCx-3ev6D6tBeXK9ZgjaisbSYqFLGM8ezesyBcvlnfhXXJ1sSqe1lDCCgV-2pBlbb.jpg

(Build Succeeded)という表示が出れば、成功です。

そうなりましたら、

ターミナルで、

$ flutter run

と打ち、

AD_4nXdHPxJ4_wYRCeZ5q9thBTeiz68-h-LoJcwO5udawGBOLcFZQrNTn4yiWnbkhEKN3xwdSBMXtFq_M-jEDqMmPyOYzEk0ewtV_pFN9CX33d64PzE7z72Vqrup.jpg

というようなメッセージが出れば、成功です。

こうしたメッセージが出ると、

パソコンと同期しているスマホの画面が、
AD_4nXe26ka8s8hPp19dYGUD7S2cKRXbXk9yT0a-IBnQW_VW5IZE552nACEVfHmqZBOCfmUolZJC0yDvH4pFaUwTK9hBt7rAcCWMkMDM2Kvz3vYXlLcuzG_fDzB6.jpg

このように、スマホに映し出され、右下のカメラボタンを押すと、

AD_4nXejQlGy5aT6AHKRzTCV07HX9fp0n8m2KVPncPObRGHv61BfeiEk7j750vH4iAPyZfyt0SkyvXtBcH9kpcl_x0bH5TEnSrdb9Z05_fTLdr5iiU9zS-aJ10Mp.jpg

という画面に切り替われば成功です。

これが第一段階です。

これを発展させて、Djangoと連携して、ノスタルジックなセピア色の写真に変えるアプリにしていきます。

今度は、以下のパッケージが必要になります。

・image_gallery_saver: アプリ内で撮影した画像やダウンロードした画像を簡単にデバイスのギャラリー(画像フォルダ)に保存してくれます。
・http: Web サーバーとの通信や、外部 API とのやり取りが簡単に行えるようにする。

$ flutter pub add image_gallery_saver
$ flutter pub add http

と、コマンドで打ってください。

その後に、

ios/Runner/info.plistに、以下の文を追記します。

ios/Runner/info.plist
	<key>NSCameraUsageDescription</key>
	<string>撮影をするために、カメラ機能を使う必要があります。</string>
	<key>NSMicrophoneUsageDescription</key>
	<string>撮影をする際に、マイク機能を使う場合があります。</string>
+ <key>NSPhotoLibraryAddUsageDescription</key>
+ <string>撮影した写真を保存します</string>
	<key>UIApplicationSupportsIndirectInputEvents</key>
	<true/>

これらの記載をすると、

アプリを起動させた時、

アクセス権を求めています.png

というメッセージが出ます。

こちらのターミナルで、

AD_4nXeGkRwkERNLF7A8DZSnQqjvJdiQvq8PknPv3NdyER9FtIc5j3FIlNlHxiY1XhLWVJVRt5JMeBN2_J0CVUbnHqm5P_D8sBbKal9f5d9ppJxCYa9fDz3U66QV.jpg

アプリをまとめるディレクトリを作成していきます。

今回は、Cameraというアプリにしたいので、

$ mkdir Camera

と入力し、

$ cd Camera

で、Cameraディレクトリに入り、PythonのフレームワークであるDjangoをインストールします。

Djangoのインストールの仕方は、こちらのサイトを参考にするといいです。↓

Pythonで、撮影した写真を加工するためのファイルを作りたいので、

Djangoアプリを作成するために、以下のコマンドを入力します。
今回は、pythonという名前のプロジェクトを作成します。

$ django-admin startproject python

すると、ディレクトリ構造は、

python
    ├── manage.py
    └── python
        ├── __init__.py
        ├── asgi.py
        ├── settings.py
        ├── urls.py
        └── wsgi.py

となります。

そして、この中にアプリケーションを作成します。

撮影した写真を加工するprocessというアプリケーションを作りたいので、

アプリケーションを作成するには、manage.pyと同じディレクトリに入り、

$python manage.py startapp process

というコマンドを打ってください。

すると、ディレクトリ構造は、

python
├── manage.py
├── process
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
└── python
    ├── __init__.py
    ├── __pycache__
    │   ├── __init__.cpython-311.pyc
    │   └── settings.cpython-311.pyc
    ├── asgi.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

という構造に変わります。

パスを通すために、python/python/urls.pyに、

python/python/urls.py
"""processed_photo URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/4.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
    path("admin/", admin.site.urls),
    path("process/", include("process.urls")),
]

に変更。

そして、processディレクトリ内に、urls.pyファイルを作り、パスを通すために、以下のコードを書きます。

python/process/urls.py
from django.urls import path
from process import views
urlpatterns = [
    path("request/", views.CameraRequest, name="camera_request"),
]

processディレクトリの中のviews.pyに撮影した写真を送り、セピア色に加工するためのコードを書いていきます。

python/process/views.py

from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt
from PIL import Image
import io
def processed_with_sepia(image_file):
    # セピア色にする加工
    RGBImage = Image.open(image_file).convert("RGB")
    width, height = RGBImage.size
    sepia_image = Image.new("RGB", (width, height))
    for y in range(height):
        for x in range(width):
            r,g,b = RGBImage.getpixel((x, y))
            # セピア調の色合いに変化
            new_r = int(r * 0.393 + g * 0.769 + b * 0.189)
            new_g = int(r * 0.349 + g * 0.686 + b * 0.168)
            new_b = int(r * 0.272 + g * 0.534 + b * 0.131)
            # RGB値が255を超えないように調整
            new_r = min(new_r, 255)
            new_g = min(new_g, 255)
            new_b = min(new_b, 255)
            sepia_image.putpixel((x,y), (new_r, new_g, new_b))
        
    return sepia_image    
@csrf_exempt
def CameraRequest(request):
    if request.method == 'POST' and request.FILES.get('photo'):
        image_file = request.FILES.get('photo')
        if image_file:
            # デバッグ情報を表示するためにprint文を追加
            print('デバッグのコード:受け取った画像のファイル名 = ', image_file.name)
            # 画像を受け取って処理するコードを追加します
            processed_image = processed_with_sepia(image_file)
            
            with io.BytesIO() as output:
                processed_image.save(output,format='PNG')
                processed_image_binary = output.getvalue()
            # レスポンスとして処理済みの画像を返します
            response = HttpResponse(processed_image_binary, content_type = "image/png")
            response['Content-Disposition'] = 'attachment; filename="photo.png"'
            return response
        
    return HttpResponseBadRequest()

この作業が終わると、撮影した写真をFlutterからDjangoに渡すために、

lib/main.dartのコードを書き換えていきます。

FlutterからDjangoに写真を送る際、下の画像のように、

AD_4nXc1Co4bKvpjGSwnGX30kISZgyB2y7bLkiTuV4r7O6qI_RNZevFD_45luvVb6dzqmmPv9HFV54YSxdXgK0mrNRYy5CN5D43Fks07JDNI7bQ7kS4SYjKOnPvE.png

反時計回りに90度傾いた状態で、渡されてしまいます。

バックエンドであるDjango側でその画像を元の傾きに戻す処理を行うと、

画像が見切れてしまうため、

フロントエンドであるFlutter側で再度、元の傾きに戻す処理を行います。

このことを念頭に、Android Studioに戻り、

photoプロジェクト内の

lib/main.dartを以下のように書き換えます。

lib/main.dart
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:image_gallery_saver/image_gallery_saver.dart';
Future <void> main() async {
 // Flutterのバインディングを初期化する。この関数を呼び出すことで、Flutterが完全に起動したことが保証される
  WidgetsFlutterBinding.ensureInitialized();
  // 使用可能なカメラを取得する非同期処理
  final cameras = await availableCameras();
 // 最初のカメラデバイスを取得(一般的には、デバイスに複数のカメラがある場合、最初に使うカメラを選択)
  final firstCamera = cameras.first;
  runApp(MyApp(camera: firstCamera));
}
class MyApp extends StatelessWidget {
  const MyApp({
    Key? key,
    required this.camera,
  }) : super(key: key);
  final CameraDescription camera;
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'カメラ機能',
      theme: ThemeData(),
      home: TakePictureScreen(camera: camera),
    );
  }
}
class TakePictureScreen extends StatefulWidget {
  const TakePictureScreen({
    super.key,
    required this.camera,
  });
  final CameraDescription camera;
  @override
  TakePictureScreenState createState() => TakePictureScreenState();
}
class TakePictureScreenState extends State<TakePictureScreen> {
  late CameraController _controller;
  late Future<void> _initializeControllerFuture;
  @override
  void initState() {
    super.initState();
    _controller = CameraController(
       // 受け取ったカメラ情報を使用
      widget.camera,
      // 解像度を定義
      ResolutionPreset.medium,
    );
    // カメラの初期化処理を開始し、その完了を待つ
    _initializeControllerFuture = _controller.initialize();
  }
  @override
  void dispose() {
   // 画面が破棄される際にカメラコントローラーも破棄してリソースを解放
    _controller.dispose();
    super.dispose();
  }
  Future<void> _sendPhoto(File photo) async {
    var request = http.MultipartRequest(
      'POST',
      Uri.parse('http://IPアドレス(例:0.0.0.0:8000/process/request/'),);
    request.files.add(http.MultipartFile.fromBytes('photo', await photo.readAsBytes(), filename: 'photo.png',));
    try {
      var response = await request.send();
      if (response.statusCode == 200) {
        var contentBytes = await response.stream.toBytes();
        var base64Image = base64Encode(contentBytes);
        //表示用の画面に移動
        await Navigator.of(context).push(
          MaterialPageRoute(
            builder: (context) => DisplayProcessedPictureScreen(imagePath: 'data:image/png;base64,$base64Image'),
            fullscreenDialog : true,
          ),
        );
      } else {
        // 失敗時の処理
        print('デバッグのコード:レスポンスステータスコード = ${response.statusCode}');
      }
    }  catch (e) {
      // エラー時の処理
      print('デバッグのコード: エラーが発生しました: $e');
    }
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        // FutureBuilderで初期化を待ってからプレビューを表示 (それまではインジケータを表示)
        child: FutureBuilder<void>(
          future: _initializeControllerFuture,
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.done) {
              return CameraPreview(_controller);
            } else {
              return const CircularProgressIndicator();
            }
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          // 写真を撮る
          final image = await _controller.takePicture();
          await _sendPhoto(File(image.path));
          // 表示用の画面に遷移
          await Navigator.of(context).push(
            MaterialPageRoute(
              builder: (context) => DisplayPictureScreen(imagePath: image.path),
              fullscreenDialog: false,
            ),
          );
        },
        child: const Icon(Icons.camera_alt),
      ),
    );
  }
}
// 撮影した写真を表示する画面
class DisplayPictureScreen extends StatelessWidget {
  const DisplayPictureScreen({super.key, required this.imagePath})  ;
  final String imagePath;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.blue,
        title: const Text(
          '通常',
          style: TextStyle(
            color: Colors.white, // 文字の色を白色に設定
            fontWeight: FontWeight.bold, // 文字を太字
          ),
        ),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Image.file(File(imagePath)),
            SizedBox(
                height: 20,
                width: 80),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                    onPressed: () {
                      // 「保存する」ボタンを押した時の処理
                      _saveImage(context, imagePath);
                    },
                    child: Text(
                      '保存する',
                      style: TextStyle(
                        color: Colors.white, //テキストを白字に設定
                        fontWeight: FontWeight.bold, //テキストを太字
                      ),
                    ),
                    style: ButtonStyle(
                      backgroundColor: MaterialStateProperty.all<Color>(Colors.blue),
                    )
                ),
                SizedBox(
                    height: 20,
                    width: 80),
                ElevatedButton(
                    onPressed: () {
                      // 「撮り直す」ボタンが押された時の処理
                      Navigator.of(context).pop();
                    },
                    child: Text(
                      '撮り直す',
                      style: TextStyle(
                        color: Colors.white, //テキストを白字に設定
                        fontWeight: FontWeight.bold, //テキストを太字
                      ),
                    ),
                    style: ButtonStyle(
                      backgroundColor: MaterialStateProperty.all<Color>(Colors.green),
                    )
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
  Future<void> _saveImage(BuildContext context, String imagePath) async {
    try {
      final result =await ImageGallerySaver.saveFile(imagePath);
      if (result != null) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('画像が保存されました')),
        );
      } else {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('画像の保存に失敗しました')),
        );
      }
    } catch (e) {
      print('画像の保存時にエラーが発生しました: $e');
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('画像の保存時にエラーが発生しました')),
      );
    }
  }
}
// 処理後の画像を受け取る
class DisplayProcessedPictureScreen extends StatefulWidget {
  final String imagePath;
  const DisplayProcessedPictureScreen({super.key, required this.imagePath});
  @override
  DisplayProcessedPictureScreenState createState() =>
      DisplayProcessedPictureScreenState();
}
class DisplayProcessedPictureScreenState extends State<DisplayProcessedPictureScreen> {
  Future<http.Response>? _fetchImageFuture;
  @override
  void initState() {
    super.initState();
    _fetchImage();
  }
  Future<void> _fetchImage() async {
    final base64Image = widget.imagePath
        .split(',')
        .last;
    final decodedBytes = base64Decode(base64Image);
    setState(() {
      _fetchImageFuture =
          Future.value(http.Response.bytes(decodedBytes, 200, headers: {
            HttpHeaders.contentTypeHeader: 'image/png',
          }));
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          backgroundColor: Colors.redAccent,
          title: const Text(
              'ノスタルジック',
              style: TextStyle(
                color: Colors.white, // 文字の色を白に設定
                fontWeight: FontWeight.bold, // 文字を太字
              ))),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            FutureBuilder<http.Response>(
              future: _fetchImageFuture,
              builder: (context, snapshot) {
                if (snapshot.hasData) {
                  final response = snapshot.data!;
                  if (response.statusCode == 200) {
                    final responseData = response.bodyBytes;
                    return Transform.rotate(
                      angle: 90 * pi /180,
                      child:// 反時計回りに90度回転
                      Image.memory(
                        responseData,
                        fit: BoxFit
                            .contain,
                      ),  // 画像をウィジェット内に適切に表示させるために適応させるfitパラメータを追加
                      alignment: Alignment.center, //回転の中心を設定
                    );
                  } else {
                    // レスポンスが不正なステイタスコードを返した場合のエラーメッセージ
                    return Text('画像の取得中にエラーが発生しました。');
                  }
                }
                return const CircularProgressIndicator();
              },
            ),
            SizedBox(height: 80),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () {
                    // 「保存する」ボタンを押した時の処理
                    _savedImage(context, widget.imagePath);
                  },
                  child: Text(
                    '保存する',
                    style: TextStyle(
                      color: Colors.white, // テキストを白字に設定
                      fontWeight: FontWeight.bold, // テキストを太字
                    ),
                  ),
                  style: ButtonStyle(
                    backgroundColor: MaterialStateProperty.all<Color>(Colors.blue), //ボタンの背景色を青色に設定
                  ),
                ),
              ],
            ),
            SizedBox(height: 10),
            ElevatedButton(
                onPressed: () {
                  // 「撮り直す」ボタンが押された時の処理
                  Navigator.of(context).pop();
                },
                child:Text(
                  '破棄',
                  style: TextStyle(
                    color: Colors.white, // テキストを白色に設定
                    fontWeight: FontWeight.bold, // テキストを太字
                  ),
                ),
                style: ButtonStyle(
                  backgroundColor: MaterialStateProperty.all<Color>(Colors.red), //ボタンの背景色を赤色に設定
                )
            ),
          ],
        ),
      ),
    );
  }
  Future<void> _savedImage(BuildContext context, String imagePath) async {
    try {
      final base64Image = widget.imagePath
          .split(',')
          .last;
      final decodedBytes = base64Decode(base64Image);
      final rotatedBytes = await _rotatedImage(decodedBytes, 90);
      // 画像を保存
      final result = await ImageGallerySaver.saveImage(rotatedBytes);
      if (result != null) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('画像が保存されました')),
        );
      } else {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('画像の保存に失敗しました')),
        );
      }
    } catch (e) {
      print('画像の保存時にエラーが発生しました: $e');
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('画像の保存時にエラーが発生しました')),
      );
    }
  }
  Future<Uint8List> _rotatedImage(Uint8List imageBytes, double angle) async {
    // 画像をデコード
    final image = await decodeImageFromList(imageBytes);
    // 回転後のサイズを計算
    final double radians = angle * ( pi / 180);
    final double sine = sin(radians);
    final double cosin = cos(radians);
    final double newWidth = (image.width * cosin + image.height * sine).abs();
    final double newHeight = (image.width * sine + image.height * cosin).abs();
    // 新しいサイズのキャンバスを作成
    final recorder = PictureRecorder();
    final canvas = Canvas(recorder, Rect.fromPoints(Offset(0.0,0.0), Offset(newWidth, newHeight)));
    // 画像を描画して回転
    canvas.translate(newWidth / 2, newHeight / 2);
    canvas.rotate(radians);
    canvas.drawImage(image, Offset(-image.width / 2, -image.height / 2), Paint());
    // キャンバスを終了し、画像を取得
    final picture = recorder.endRecording();
    final img = await picture.toImage(newWidth.toInt(), newHeight.toInt());
    final byteData = await img.toByteData(format: ImageByteFormat.png);
    return byteData!.buffer.asUint8List();
  }
}

最終的なinfo.plistは、

info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CADisableMinimumFrameDurationOnPhone</key>
	<true/>
	<key>CFBundleDevelopmentRegion</key>
	<string>$(DEVELOPMENT_LANGUAGE)</string>
	<key>CFBundleDisplayName</key>
	<string>Take Photo</string>
	<key>CFBundleExecutable</key>
	<string>$(EXECUTABLE_NAME)</string>
	<key>CFBundleIdentifier</key>
	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleName</key>
	<string>photo</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleShortVersionString</key>
	<string>$(FLUTTER_BUILD_NAME)</string>
	<key>CFBundleSignature</key>
	<string>????</string>
	<key>CFBundleVersion</key>
	<string>$(FLUTTER_BUILD_NUMBER)</string>
	<key>LSRequiresIPhoneOS</key>
	<true/>
	<key>NSCameraUsageDescription</key>
	<string>撮影をするために、カメラ機能を使う必要があります。</string>
	<key>NSMicrophoneUsageDescription</key>
	<string>撮影をする際に、マイク機能を使う場合があります。</string>
+ <key>NSPhotoLibraryAddUsageDescription</key>
+ <string>撮影した写真を保存します</string>
	<key>UIApplicationSupportsIndirectInputEvents</key>
	<true/>
	<key>UILaunchStoryboardName</key>
	<string>LaunchScreen</string>
	<key>UIMainStoryboardFile</key>
	<string>Main</string>
	<key>UISupportedInterfaceOrientations</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
		<string>UIInterfaceOrientationLandscapeLeft</string>
		<string>UIInterfaceOrientationLandscapeRight</string>
	</array>
	<key>UISupportedInterfaceOrientations~ipad</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
		<string>UIInterfaceOrientationPortraitUpsideDown</string>
		<string>UIInterfaceOrientationLandscapeLeft</string>
		<string>UIInterfaceOrientationLandscapeRight</string>
	</array>
</dict>
</plist>

となり、

pubspec.yamlは、

pubspec.yaml


name: photo
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
  sdk: '>=3.3.4 <4.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
  flutter:
    sdk: flutter
  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.6
  camera: ^0.11.0+2
  path_provider: ^2.1.5
  path: ^1.9.0
  video_player: ^2.9.2
  image_gallery_saver: ^2.0.3
  http: ^1.2.2
dev_dependencies:
  flutter_test:
    sdk: flutter
  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^3.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true
  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg
  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/assets-and-images/#resolution-aware
  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages
  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/custom-fonts/#from-packages

というコードになっていれば、OKです。

では、この作成したphotoプロジェクトを

先ほど、作成したCameraディレクトリ内にコピーしていきます。

なので、photoプロジェクトのパスをAndroid Studio内のターミナルで、

$ pwd

と打ち、取得します。

※ 表示されたメッセージがphotoプロジェクトのパスになります。

そして、次に、Cameraディレクトリ内で、同じく、

$ pwd

と打ち、

$ cp -r [photoプロジェクトのパス]  [Cameraディレクトリのパス/photo]

とコマンドで入力してください。

このようなディレクトリ構造になっていれば大丈夫です。

Camera
├── photo
└── python
     └── process

より詳しい全体的なディレクトリ構造は、

Camera
├── python
│     ├── Dockerfile
│     ├── manage.py
│     ├── process
│     │   ├── __init__.py
│     │   ├── admin.py
│     │   ├── apps.py
│     ├── migrations
│     │   │   └── __init__.py
│     │   ├── models.py
│     │   ├── tests.py
│     │   ├── urls.py
│     │   └── views.py
│     └── python
│           ├── __init__.py
│           ├── __pycache__
│           │   ├── __init__.cpython-311.pyc
│           │   └── settings.cpython-311.pyc
│           ├── asgi.py
│           ├── settings.py
│           ├── urls.py
│           └── wsgi.py
└── photo   # Flutterアプリケーション
      

となっていれば大丈夫です。

では、photoディレクトリ内で、

$ flutter run

を実行してみましょう。

もし、
AD_4nXfz_0yHJ-pCj52kIf3UfTZsls8d4q3bvmnDYmnow46Tr96yXF6njJT1xW5Of0ixevnR2-sYKTKs-TOYdp4w1fgtkhMc8FojgpvEh5hpMjj9WhkszEEFJZkT.jpg

このような画面が出てきてしまったら、

下の画面のように、XCodeのSettings項目を押し、
About Xcode.jpg

Components内にある
エラーメッセージで表示している必要としているデバイス(僕の場合は、iOS 18.2)を選び、「GET」ボタンを押してください。

AD_4nXeMI8zSRxCMDxOxDIa1kles8II9cx2-Xg9qdJ-Xpl4PCU1bm1Rm5uK4vnPX9GQyLwKdSxZjuOVwApmM2SRtPkNuB9Rv_wpoX0KyYj6x4YIVotH6pE46C3An.jpg

これをすることで、エラーが解消されるかと思います。

では、これまでに書いたコードが動くかどうかテストをしていきます。

FlutterとDjangoは、手動で動かしていきます。

撮影した写真を加工するために、まずは、Djangoのサーバーを動かしていきます。

Djangoを動かす前に、

サーバーのCORS設定を確認(特にローカル開発環境)
モバイルデバイスからのリクエストをサーバーが受け入れるためには、サーバーのCORS(Cross-Origin Resource Sharing)設定が適切に設定されている必要があります。

特に、FlutterアプリがAndroidやiOS端末で実行されている場合、サーバー側でCORSが適切に設定されていないと、リクエストが拒否されることがあります。

例えば、DjangoでCORSを許可するには、django-cors-headersをインストールして設定します。

$ pip install django-cors-headers

次に、settings.pyで設定します。

python/python/settings.py
INSTALLED_APPS = [
    ...
    'corsheaders',
    ...
]

MIDDLEWARE = [
    ...
    'corsheaders.middleware.CorsMiddleware',
    ...
]

# 必要に応じて設定(すべてのドメインからのリクエストを許可する場合)
CORS_ALLOW_ALL_ORIGINS = True

もし、下のようなエラーが出た場合は、
AD_4nXeSaQ0FxHBeeqBbkSI_rhtDLxfxaBjAkRUiygMbK_yo2o2cats6GlimbzbCvBTSkNpx6t-0D3gtnxv5vNOUHey9BqD6SPjeUp5Tx5vgEfpgz0JbfnAA_fZl.jpg

Pythonのsslモジュールが利用できないことが原因です。sslモジュールは、SSL/TLS接続を扱うために必要ですが、現在のPython環境ではこれが有効になっていない可能性があります。

  1. Pythonのsslモジュールのインストールを確認

この問題は、Pythonをインストールする際にsslサポートが無効化された場合に発生します。これを解決するために、次の手順を試してみてください。

  1. pyenvの再インストール
    PythonのSSLサポートを有効にするために、pyenvでPythonを再インストールする際に、opensslを適切にリンクさせてインストールします。次のコマンドを試してください:
$ env PATH="$(brew --prefix openssl@3)/bin:$PATH" pyenv install 3.11.4

openssl@3を正しくリンクさせてPythonをインストールします。これにより、PythonがSSLモジュールを適切に利用できるようになります。

  1. pyenvのPythonバージョンを再設定

インストールが完了したら、pyenvでインストールしたPythonバージョンを使用するように設定します。

$ pyenv global 3.11.4
  1. Pythonの依存関係の再インストール

SSLモジュールが有効になったら、必要なパッケージを再インストールします。

$ pip install --upgrade pip
$ pip install django-cors-headers

これで、pipが正しくHTTPS経由でパッケージをダウンロードできるようになるはずです。

これらを踏まえて、python/python/settings.pyの全体的な内容は、

python/python/settings.py
"""
Django settings for python project.
Generated by 'django-admin startproject' using Django 4.2.2.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
from pathlib import Path
import os
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-uvw0i=(例:########################)'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = [IPアドレス(:'0.0.0.0')]
# Application definition
INSTALLED_APPS = [
+ 'corsheaders',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
+ 'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'python.urls'
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
WSGI_APPLICATION = 'python.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+ # 必要に応じて設定(全てのドメインからのリクエストを許可する場合)
+ CORS_ALLOW_ALL_ORIGINS = True

に変わります。

では、Cameraディレクトリ内のpython(manage.pyがある)ディレクトリ内で、

$ python manage.py runserver IPアドレス(例:0.0.0.0:8000)

というコマンドを打ってください。
AD_4nXdncdKCfLgOUElkjg7ZRRj9h7NzYyV9anDICcgEwO8wCs25fHKPx7aoKad5Aj6ZpU7FBOEI6dPtXAPYnI3LZhCNhYaNYRj2XgXq46a9Y484o9jS4bFBYgDO.jpg

上のようなメッセージが、出れば成功です。

それでは、スマホとパソコンをコードで同期してください。

次は、フロントエンドであるFlutterを動かしていきたいので、photoディレクトリ内で、

$ flutter run

とコマンドを打ってください。

立ち上がるまでに、時間がかかりますが、
DevTools.jpg

上のようなメッセージが出れば成功です。

すると、同期したスマホの画面が切り替わり、

AD_4nXdPGD2cS46u4K8PFPlrGKdaFhILQLOwXilTiK_MkWs7n2ipo5Gt3GdyHD89_w93O3pxCii0CN5jo-2pswR-w8HmZkkQy9L2NZspb4yieCHL3J-byuawBRqJ.jpg

という画面に変わります。

そして、また、カメラボタンを押すと、

ノスタルジック.jpg

今度は、上のような画面が現れるかと思います。

この画像に変われば、Djangoとの通信ができています。

そして、「保存する」ボタンを押すと、
ノスタルジック.jpg

というメッセージが出ます。

そして、写真アルバムを見てみると、

AD_4nXftq_43nPIIV2yYlogYYjeD3xX56Vr-XYIxKBpqbq7CiUs07fmB7srLiBqwiYDkAglIpmjxHdTNC8sONP1wtN-8tbmvvadBSi6Mp-DnbTxG9pnMwYj6aVWW.jpg

ちゃんと保存されていますね。

次に、「破棄」ボタンを押すと、

AD_4nXe6EpBM-tfBGfNXCJyc8G9jpBjBqEmdQqGXq3-NAejdXxEe4ulEJ_aQOmciNBycoBk8B2k7Bnq6BMXZxqB0-la3QDAx64KEmby2q5xynQj3kHo5fxcsuVT5.jpg

加工をしていない画面に遷移します。

こちらも、「保存する」ボタンを押すと、

AD_4nXf6NE6XXsSLF8ig8eRtXNnUiAAGUky_KGShDrMAMmzITmkPrSVsAGyrUN02w5DxYNL9DbpLQWnD1XOnrh1qTjJZfAF2MIzK8qkD5NfGrVm6xaZwfLFRBN5R.jpg

というメッセージが出ます。

また、写真アルバムを確認すると、

AD_4nXeDO7jsjKfV6TpIRQl_DgYdEKcYue--zkGKHW0mpXG58L_Dxy6ww2Ma6qU51WH17hxqNNKxHTz9QV5RiWOTTR-uo0Cgd78YyIY9XNnBdtRJ2JYxXH6iJt2L.jpg

こちらもちゃんと保存されています。

そして、「撮り直す」ボタンを押すと、

AD_4nXdPGD2cS46u4K8PFPlrGKdaFhILQLOwXilTiK_MkWs7n2ipo5Gt3GdyHD89_w93O3pxCii0CN5jo-2pswR-w8HmZkkQy9L2NZspb4yieCHL3J-byuawBRqJ.jpg

また、撮影画面に戻ります。

ここまでできれば、成功です。

これで完成です。

ここまでの工程は、Githubに上げておきます↓

もし、加工する写真をもっと赤味を強くしたり、青みがかったりしたい場合は、

python/process/views.py

new_rnew_gnew_bの数値を変えて、お好みの数値に設定してみてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?