Dockerイメージを軽くする「マルチステージビルド」について

前回の記事では、複数のコンテナを操る docker-compose.yml の書き方を紹介した。今回は、そのコンテナの中身を定義する Dockerfile について書く。

今回作成したバックエンド(Java/Spring Boot)とフロントエンド(React/Node.js)のDockerfileには、「マルチステージビルド」と使っている。

これを使うと、「イメージサイズが小さくなり、セキュリティも向上する」

バックエンド編:Java (Spring Boot)

Javaアプリを動かすには、ビルドツールの「Maven」や「Gradle」が必要だが、実行時にこれらは不要となる。そこで、ビルドする段階と、実行する段階を分ける。

# --- Stage 1: ビルド環境 ---
FROM maven:3.9-eclipse-temurin-25 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
# テストはCIでやる前提でスキップし、ビルド時間を短縮
RUN mvn clean package -DskipTests

# --- Stage 2: 実行環境 ---
FROM eclipse-temurin:25-jre
WORKDIR /app
# ビルド環境(Stage 1)で作った成果物(jar)だけをコピー
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

  • AS build と COPY –from=build: 最初の FROM で「build」という名前をつけ、そこで mvn package して jarファイルを作る。2つ目の FROM が本番用イメージ。ここには Maven もソースコードも含まれない。あるのは 「JRE(Java実行環境)」と「jarファイル」だけ。 これにより、数百MB単位で容量を節約できる。
  • eclipse-temurin:25-jre: JDK(開発キット)ではなく、JRE(実行環境)を選んでいる。コンパイラなどが含まれないため、攻撃者に悪用されるリスクが減る。

フロントエンド編:Node.js (React/Vueなど)

フロントエンド開発では npm run dev サーバーを使うが、本番環境でNode.jsサーバーをそのまま動かすのはパフォーマンス的に最適ではない。 ビルドした静的ファイル(HTML/CSS/JS)を、高速なWebサーバーである Nginx に配信させる。

# --- Stage 1: ビルド環境 ---
FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
# npm install より高速で、lockファイルに忠実な ci を使用
RUN npm ci
COPY . .
RUN npm run build

# --- Stage 2: 配信環境 (Nginx) ---
FROM nginx:alpine
# ビルド成果物(dist)をNginxの公開ディレクトリに配置
COPY --from=build /app/dist /usr/share/nginx/html
# SPA用の設定ファイルをコピー
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

  • Node.js はビルド時のみ使用: Reactなどのコードはブラウザで動くJavaScriptに変換(トランスパイル)する必要がある。これには Node.js が必要だが、変換が終われば Node.js 自体は不要となる。 最終的なイメージは nginx:alpine ベースになるため、サイズはわずか数十MBに収まる。
  • npm ci の採用: npm install ではなく npm ci を使っている。これは package-lock.json を厳密に守ってインストールを行うコマンドで、CI/CDやDockerビルドでの再現性を保証する。
  • カスタムNginx設定: ReactのようなSPA(シングルページアプリケーション)では、どのURLにアクセスしても index.html を返す設定が必要になるため、デフォルト設定を上書きしている。

まとめ

今回作成した2つのDockerfileに共通するのは、「作る道具(Maven/Node)」と「動かす場所(JRE/Nginx)」を明確に分けている点となる。

Docker Composeで、Webアプリ開発環境を作る

今回は、典型的な「フロントエンド(React等) + バックエンド(Spring/Node等) + データベース(MariaDB)」の構成を例に docker-compose.yml を書いた。

今回の構成

3つのコンテナ(Service)が専用のネットワーク内で連携し、データはボリュームによって永続化される構成。

完成した docker-compose.yml

以下が今回解説する設定ファイル。例として小説検索システム(Novel Search System)のようなアプリケーションを想定している。

services:
  backend:
    build: ./backend
    container_name: novel-backend
    ports:
      - "8080:8080"
    environment:
      - DB_URL=jdbc:mariadb://novel-db:3306/${DB_NAME}
      # ... (省略)
    depends_on:
      novel-db:
        condition: service_healthy # ここが重要!
    networks:
      - novel-network

  frontend:
    build: ./frontend
    container_name: novel-frontend
    ports:
      - "80:80"
    depends_on:
      - backend
    networks:
      - novel-network

  novel-db:
    image: mariadb:12.1
    container_name: novel-db
    environment:
      - MARIADB_DATABASE=${DB_NAME}
      # ... (省略)
    healthcheck:
      test: ["CMD-SHELL", "mariadb-admin ping -h localhost -u root -p$$MARIADB_ROOT_PASSWORD || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 10
    volumes:
      - db_data:/var/lib/mysql
      - ./backend/sql:/docker-entrypoint-initdb.d
    networks:
      - novel-network

networks:
  novel-network:
    driver: bridge

volumes:
  db_data:

1.データベースの「起動待ち」を完璧にする healthcheck

Docker Composeでよくあるトラブルが、「DBが完全に立ち上がる前にバックエンドが接続に行き、エラーで落ちる」という現象。

単なる depends_on: – novel-db だけでは、「コンテナが作成された」ことしか確認できず、「DB接続の準備ができた」ことまでは保証されない。

そこでHealthcheck(ヘルスチェック) を行う。

novel-db:
    # ...
    healthcheck:
      test: ["CMD-SHELL", "mariadb-admin ping ..."]
      interval: 10s
      retries: 10

ここでは mariadb-admin ping コマンドを定期的に実行し、DBが応答するかを監視している。

backend側では以下のように記述する。

backend:
    depends_on:
      novel-db:
        condition: service_healthy

これにより、「DBのヘルスチェックがOKになるまで、バックエンドを起動させない」という制御が可能になる。

2.データの永続化と初期データの投入

データベースをコンテナ化する際、コンテナを削除するとデータも消えてしまう。これを防ぐのが volumes 。

volumes:
      - db_data:/var/lib/mysql
      - ./backend/sql:/docker-entrypoint-initdb.d

  • db_data:/var/lib/mysql: ホスト側(Docker管理領域)にデータを保存する。コンテナを作り直しても、小説データやユーザーデータは消えない。
  • ./backend/sql:/docker-entrypoint-initdb.d: MariaDBやMySQLの公式イメージは、/docker-entrypoint-initdb.d に置かれた .sql ファイルを、初回起動時に自動実行してくれる。 テーブル作成や初期データの投入を自動化できるため、開発メンバー全員が docker compose up 一発で同じDB状態から開発をスタートできる。

EclipseからVS Codeに開発環境を移した

フロントエンドの開発をしたかったので、VS Codeを触ってみた。

殆ど問題なく作業を進めることが出来たが、一部戸惑った点を書き残しておく。

起動構成はJSON形式で書く

{
    "configurations": [
        {
            "type": "java",
            "name": "Spring Boot App",
            "request": "launch",
            "mainClass": "com.example.novel.NovelApplication",
            "projectName": "",
            "env": {
                "DEV_DB_URL": "jdbc:mariadb://url",
                "DEV_DB_USERNAME": "id",
                "DEV_DB_PASSWORD": "pass"
            }
        },
        {
            "type": "node",
            "name": "React App",
            "request": "launch",
            "runtimeExecutable": "npm",
            "runtimeArgs": [
                "run-script",
                "dev"
            ],
            "cwd": "${workspaceFolder}/frontend",
            "console": "integratedTerminal"
        }
    ]
}

1. ひとつ目の設定:Java (Spring Boot) アプリケーション用

Javaのプログラム(Spring Bootなど)を起動するための設定。

  • "type": "java"
    • Java拡張機能を使ってデバッグ
  • "name": "Spring Boot App"
    • VS Codeの「実行とデバッグ」画面のプルダウンメニューに表示される名前
  • "request": "launch"
    • 新しくプログラムを起動するという意味(対義語は "attach" で、既に動いているプロセスに接続する場合に使う)
  • "mainClass": "com.example.novel.NovelApplication"
    • プログラムのエントリーポイント(public static void main メソッドがあるクラス)を指定
  • "projectName": ""
    • 通常、VS Codeが認識しているJavaプロジェクト名(フォルダ名など)が入る
    • 空欄 "" のままだと、複数のプロジェクトがある場合に起動できない可能性がある。1つだけなら問題なし
  • "env"
    • プログラム実行時にOSの環境変数として渡す値を設定している
    • アプリケーション側(application.properties など)で、DB接続情報をハードコードせずに、ここから読み込むようにしている
      • DEV_DB_URL: 接続先データベースのURL
      • DEV_DB_USERNAME: データベースのユーザーID
      • DEV_DB_PASSWORD: データベースのパスワード

2. ふたつ目の設定:Node.js (NPM) アプリケーション用

フロントエンド開発(React, Vueなど)の開発サーバーを起動するための設定。

  • "type": "node"
    • Node.js用のデバッグ
  • "name": "React App"
    • VS Codeの「実行とデバッグ」画面のプルダウンメニューに表示される名前
  • "request": "launch"
    • 新しくプログラムを起動するという意味
  • "runtimeExecutable": "npm"
    • 直接 node コマンドを実行するのではなく、npm コマンドを実行する
  • "runtimeArgs": [ "run-script", "dev" ]
    • npm コマンドに渡す引数
    • つまり、ターミナルで npm run-script dev (または単に npm run dev)と打つのと同じコマンドが実行される
  • "cwd": "${workspaceFolder}/frontend"
    • Current Working Directory(作業ディレクトリ)の指定
    • ${workspaceFolder} はVS Codeで開いているルートフォルダを指す
  • "console": "integratedTerminal"
    • ログの出力先を「デバッグコンソール」ではなく、VS Codeの**「統合ターミナル」**にする

VS Codeの設定はJSON形式で書く

{
    "java.configuration.updateBuildConfiguration": "automatic",
    "java.compile.nullAnalysis.mode": "automatic",
    "maven.terminal.customEnv": [
        {
            "environmentVariable": "DEV_DB_URL",
            "value": "jdbc:mariadb://url"
        },
        {
            "environmentVariable": "DEV_DB_USERNAME",
            "value": "id"
        },
        {
            "environmentVariable": "DEV_DB_PASSWORD",
            "value": "pass"
        }
    ]
}

1. Javaのビルド・解析設定

VS CodeがJavaプロジェクトをどう扱うかという、エディタの挙動設定

  • "java.configuration.updateBuildConfiguration": "automatic"
    • 意味: pom.xml(や build.gradle)が書き換えられた際、自動的にプロジェクト構成を更新する
    • Eclipseで言うと: プロジェクトを右クリックして行う 「Maven > プロジェクトの更新 (Alt+F5)」を、保存のたびに全自動でやってくれる機能
    • メリット: 依存ライブラリを追加した際、いちいち手動更新しなくてもすぐにimportできるようになる
  • "java.compile.nullAnalysis.mode": "automatic"
    • 意味: Javaのコードに対するNull(ヌル)分析機能のモード
    • 動作: プロジェクト内にNull注釈(@Nullable@NonNull など)が含まれている場合、自動的にNullポインタ参照の警告を出してくれる
    • メリット: 「ここでNullになる可能性があります」という警告がエディタ上で出るようになり、NullPointerExceptionを未然に防ぎやすくなる

2. Maven実行時の環境変数設定

  • "maven.terminal.customEnv": [ ... ]
    • 意味: VS CodeのMaven拡張機能を使ってコマンド(例: mvn spring-boot:runmvn test)を実行する際、一時的にセットする環境変数を定義

Spring Security 7にアップグレードしたらxmlでの設定が使えなくなった

XMLファイルでSpring Securityを設定するSecurityConfigは非推奨になっていたが、今回完全に削除されたのかクラスファイルが見つからなくなった。

流石にJavaファイルで設定する方式に変更した。

元のxmlファイルは下記の通り。
※ExtendedAuthenticationSuccessHandler、ExtendedAuthenticationFailureHandlerはログイン成功失敗時に独自処理を行うためのラッパークラス。

<?xml version="1.0" encoding="UTF-8"?>

<beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd">

    <!-- Resources not processed by spring security filters -->
    <http pattern="/images/**" security="none" />
    <http pattern="/scripts/**" security="none" />
    <http pattern="/styles/**" security="none" />

    <http>
        <intercept-url pattern="/error" access="permitAll" />
        <intercept-url pattern="/login*/**" access="permitAll" />
        <intercept-url pattern="/signup*" access="permitAll" />
        <intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')" />
        <intercept-url pattern="/**" access="isAuthenticated()" />

        <form-login login-page="/login" authentication-success-handler-ref="authenticationSuccessHandler" authentication-failure-handler-ref="authenticationFailureHandler" />
        <remember-me user-service-ref="userDetails" key="aaa" />
        <logout logout-url="/logout" logout-success-url="/login" invalidate-session="true" delete-cookies="aaa" />
    </http>

    <authentication-manager alias="authenticationManager">
        <authentication-provider user-service-ref="userDetails">
            <password-encoder ref="passwordEncoder" />
        </authentication-provider>
    </authentication-manager>

    <beans:bean id="authenticationSuccessHandler" class="common.webapp.filter.ExtendedAuthenticationSuccessHandler">
        <beans:constructor-arg value="/top" />
    </beans:bean>

    <beans:bean id="authenticationFailureHandler" class="common.webapp.filter.ExtendedAuthenticationFailureHandler">
        <beans:property name="exceptionMappings">
            <beans:props>
                <beans:prop key="org.springframework.security.authentication.DisabledException">/login/accountDisabled</beans:prop>
                <beans:prop key="org.springframework.security.authentication.LockedException">/login/accountLocked</beans:prop>
                <beans:prop key="org.springframework.security.authentication.AccountExpiredException">/login/accountExpired</beans:prop>
                <beans:prop key="org.springframework.security.authentication.CredentialsExpiredException">/login/credentialsExpired</beans:prop>
                <beans:prop key="org.springframework.security.authentication.BadCredentialsException">/login/badCredentials</beans:prop>
            </beans:props>
        </beans:property>
    </beans:bean>

    <beans:bean id="webexpressionHandler" class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler" />

</beans:beans>

これに対して、Javaファイルは下記の通り。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final UserDetailsService userDetailsService;

    public SecurityConfig(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/images/**", "/scripts/**", "/styles/**",
                                 "/error", "/login*/**", 
                                 "/signup*").permitAll()
                .requestMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .successHandler(authenticationSuccessHandler())
                .failureHandler(authenticationFailureHandler())
                .permitAll()
            )
            .rememberMe(remember -> remember
                .userDetailsService(userDetailsService)
                .key("aaa")
            )
            .logout(logout -> logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID", "aaa")
            );

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    public PasswordEncoder passwordTokenEncoder() {
        return new BCryptPasswordEncoder();
    }

    AuthenticationSuccessHandler authenticationSuccessHandler() {
        return new ExtendedAuthenticationSuccessHandler("/top");
    }

    AuthenticationFailureHandler authenticationFailureHandler() {
        // 例外マッピングの設定
        Properties mappings = new Properties();
        mappings.put(DisabledException.class.getName(), "/login/accountDisabled");
        mappings.put(LockedException.class.getName(), "/login/accountLocked");
        mappings.put(AccountExpiredException.class.getName(), "/login/accountExpired");
        mappings.put(CredentialsExpiredException.class.getName(), "/login/credentialsExpired");
        mappings.put(BadCredentialsException.class.getName(), "/login/badCredentials");

        ExtendedAuthenticationFailureHandler handler = new ExtendedAuthenticationFailureHandler();
        handler.setExceptionMappings(mappings);
        return handler;
    }
}

Java 25のパフォーマンス改善点で気になったところ

特に気になった点だけ調べてみた。

ウォームアップ短縮(JEP 515)

Java アプリケーションは JIT コンパイルによって実行速度を最適化するが、その「最適化」に到達するまでのウォームアップ時間は長年の課題だった。特に短命なサービスやバースト型のワークロードでは、起動直後のレスポンス低下が無視出来ない。

JEP 515 では、過去の実行から収集したプロファイル情報を AOT キャッシュとして保存し、次回の起動時に利用できる仕組みが導入された。これにより、

  • 初期起動から JIT 最適化が効いた状態に近いパフォーマンスを発揮
  • マイクロサービスやサーバレス環境の「コールドスタート問題」を緩和
  • ウォームアップを待たずにスループットやレイテンシが安定

といったメリットがある。

トレーニング実行(プロファイル収集)

まずはアプリを「本番に近い負荷テスト」、「代表的な入力データ」等で実行し、JIT がどのメソッドをよく使うかを記録する。

java -XX:AOTCacheOutput=app-prof.aot -jar myapp.jar

本番実行(プロファイル利用)

保存したプロファイルを読み込み、本番環境で起動する。

java -XX:AOTCache=app-prof.aot -jar myapp.jar

Compact Object Headers(JEP 519)

Java のオブジェクトはヒープ上で管理され、各オブジェクトには「ヘッダ情報」が付与されている。従来は 12 バイトが標準だったが、JEP 519 では ヘッダを 8 バイトに圧縮するオプションが提供された。

これにより、

  • メモリ削減:多数の小さなオブジェクトを扱うアプリで特に有効。ヒープ効率が上がるため GC 回数も減少する。
  • キャッシュ効率の改善:メモリ占有が減ることで CPU キャッシュに収まるデータ量が増え、アクセスレイテンシが低下する。
  • スループット向上

利用方法は、JVM 起動時に以下を指定する。

java -XX:+UseCompactObjectHeaders -jar myapp.jar

デフォルトでONになっていないということは、全ての環境で改善するわけではないことを表している。

  • COH ではオブジェクトヘッダに格納されていた情報を圧縮/再配置するため、hashCode/同期多用のアプリでは遅くなる可能性
  • ヒープダンプ解析ツールや JVMTI を使った低レベルの計測では、
    オブジェクトレイアウトが従来と変わるため、互換性のリスクあり
  • メモリ帯域が十分に広いサーバー、CPU キャッシュが大きい最新世代のCPUでは効果が限定的になる場合がある(逆に、組込み系やクラウドVMのようにメモリ制約がある環境で効果大)

適用シナリオ

これらの最適化はすべてのアプリケーションで均等に効くわけではない。効果が大きいシナリオは以下の通りとなる。

ウォームアップ短縮(JEP 515)

  • サーバレス、マイクロサービス
  • 短命な CLI ツールやバッチ処理

Compact Object Headers(JEP 519)

  • ヒープ上に数百万単位のオブジェクトを保持するシステム
  • 高トラフィックの Web サービス
  • データ処理や分析基盤

まとめ

Java 25 の JEP 515 と JEP 519 は、単なる言語仕様の追加ではなく、実行基盤のボトルネックを狙い撃ちした改善となっている。
起動直後のパフォーマンスが課題であればウォームアップ短縮を、ヒープ使用量やGCコストに悩んでいるなら Compact Object Headers を試す価値がある。

YOLOでYouTubeの映像から物体検出する

YouTubeのライブ映像を取り込んで物体検出してみる。応用すると風景のライブ映像に野生動物が写ったらアラートを出す。録画するなど出来ると思う。

前回の下記部分のカメラ映像読み込み部分をYouTubeに置き換える。

# Load image source
cap = cv2.VideoCapture(0)

前回のパッケージに加えて「pip install yt-dlp」を実行しておく。

実装

モジュールの読み込み
from yt_dlp import YoutubeDL
YouTubeのライブ配信ページURLから、再生用の実URL(多くはHLSのm3u8)を取得する関数
def get_best_live_url(youtube_url: str) -> str:
    ydl_opts = {
        'quiet': True,
        'no_warnings': True,
        'skip_download': True,
        # Filters in order of ease of live performance
        'format': 'best[protocol^=m3u8]/best[ext=mp4]/best'
    }
    with YoutubeDL(ydl_opts) as ydl:
        info = ydl.extract_info(youtube_url, download=False)
        # `url` contains a URL that can be played directly (assuming m3u8)
        stream_url = info.get('url')
        if not stream_url:
            for f in info.get('formats', []):
                if 'm3u8' in (f.get('protocol') or ''):
                    stream_url = f.get('url')
                    break
        if not stream_url:
            raise RuntimeError('Unable to get live playback URL.')
        return stream_url
YouTubeの動画、ライブ配信を開く関数
def open_live_capture(stream_url: str) -> cv2.VideoCapture:
    cap = cv2.VideoCapture(stream_url)
    # Latency reduction (enabled builds only)
    try:
        cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
    except Exception:
        pass
    return cap
前回コードの変更部分

前回のコードの「cap = cv2.VideoCapture(0)」を下記に置き換える。「youtube_url」に取り込みたい動画やライブ映像のURLを入力する。

youtube_url = 'https://www.youtube.com/watch?v=好きな動画のURL'

stream_url = get_best_live_url(youtube_url)
print('stream:', stream_url)
cap = open_live_capture(stream_url)

if not cap.isOpened():
    raise RuntimeError('Failed to open VideoCapture. Please use an FFmpeg-enabled build of OpenCV.')

Android 16の新機能について

自分のスマホにAndroid 16がそろそろ配信されそうなため、主にプライバシー、パフォーマンス、バッテリー最適化、ゲーム関連について調べてみた。

プライバシーとセキュリティの強化について

1. アプリの権限リクエストがより細分化・透明化

写真・動画へのアクセス制限の強化

これまで「すべての写真を許可/拒否」のような大まかな選択肢しかなかったストレージアクセスに対して、Android 16では:

  • 特定の写真のみ選択的に許可
  • アクセス履歴の表示
  • 一時的なアクセス(ワンタイムパーミッション)

など、ユーザーがどのデータに対して、どのアプリに許可したかを明確に管理できるようになる。

位置情報とマイク・カメラの利用制限

  • 「常時許可」は廃止され、アプリがアクティブな時だけ許可が基本に
  • バックグラウンドでのマイク・カメラの使用はユーザーに通知
  • 通知バーで現在利用中のセンサー(マイク、カメラ、GPS)がリアルタイムで表示

2. Google Play プロテクトの進化(リアルタイムAIスキャン)

Android 16では、Google Play プロテクトにAIによるリアルタイム挙動監視機能が追加される。

  • 新規インストール時にアプリのコードや動作傾向をクラウドAIが即座に解析
  • 不審な動作がある場合は即時警告・ブロック
  • 「このアプリは類似アプリと比べて異常に多くのデータを送信しています」など、行動ベースで通知

これにより、インストール前だけでなく使用中のセキュリティも常時監視されるようになる。

3. システムアップデートの分割と即時性向上(モジュラー化)

Android 16では「Project Mainline」がさらに進化し、以下のような構成に:

  • セキュリティコンポーネントがOSと切り離され、Google Play経由で個別更新が可能
  • 端末メーカーやキャリアのアップデート配信を待たずに、月次パッチレベルで配布
  • 更新は自動かつバックグラウンドで完了

これにより、ゼロデイ攻撃への即応性が格段に上がるとされる。

4. アプリトラッキング・クロスアプリ識別子の制限

Appleの「App Tracking Transparency」に近い機能がAndroid 16でも強化されている。

  • アプリごとの広告ID取得がデフォルトで無効化
  • アプリがユーザーを他のアプリやサービスを横断して追跡する行為に対し、事前に明確な許可を求めるUIが表示
  • 許可履歴やデータ共有先の表示がユーザー向けに視覚化

これにより、「勝手に広告が最適化される」不快感を低減する。

5. プライバシーダッシュボードの強化

「設定」→「プライバシー」にあるダッシュボード画面が大幅に刷新された。

  • 各アプリがいつ、何のデータにアクセスしたかを時系列で表示
  • 「カレンダーへのアクセス:○月×日 14:22」など具体的な操作ログ付き
  • 不要と判断したアクセスは即座に取り消し可能

パフォーマンスとバッテリー最適化について

OSレベルでの最適化と、AIによる動的制御を組み合わせた省電力かつ高速なシステム設計が実現される。特に、バックグラウンド処理やメモリ割り当て、充電中の動作制御に重点が置かれている。

1. 高効率スケジューラ:Dynamic Task Prioritization(DTP)

Android 16のカーネルスケジューラは、タスク実行時にアプリの重要度や使用頻度を学習し、CPU・GPUリソースを動的に割り当てる機構が導入された。

  • アクティブアプリの処理を即時優先
  • バックグラウンドアプリは低負荷スレッドに回される
  • AIがユーザーの利用傾向を継続的に学習し、最適な実行タイミングを決定

これにより、操作時のラグ減少や描画高速化が実現しつつ、リソースの無駄遣いも抑制される。

2. バックグラウンド処理の制限強化

Android 16では、Dozeモード/App Standby Bucket(ASB)/Foreground Serviceの動作制御が進化している。

  • アプリを「Active/Working set/Frequent/Rare/Restricted」に自動分類
  • 端末の未使用時間が長いほど、該当アプリのバックグラウンド実行権限が縮小
  • バッテリー消費の激しいアプリに対し、強制スリープ処理を自動で適用

また、ユーザーは「バッテリー使用量ランキング」から該当アプリの活動詳細を確認し、手動制御も可能となる。

3. AIによる電力最適化:Adaptive Battery v2

Android 16では「Adaptive Battery」が第2世代にアップグレードされ、ディープラーニングによる予測制御が導入された。

  • アプリごとの使用傾向を解析し、不要なプロセスの事前抑制
  • 毎日の行動パターンに基づき、電力供給を時間帯ごとに最適配分
  • 充電パターンを学習してバッテリー劣化を抑える

たとえば、就寝中の充電では、急速充電を避けて「夜間スロー充電モード」に自動切り替えされる。

4. バッテリー保護機能の強化

Android 16対応端末では、以下のようなハードウェア連携機能もサポートされる。

  • 温度・電圧監視機構の統合により、過熱や過充電をOSが自動制御
  • 一定の温度や電圧条件を超えると、充電速度を一時的に抑制
  • 「バッテリー寿命延長モード」では、満充電を80〜85%で制御するオプションも搭載

これにより、バッテリー膨張や性能劣化のリスクを抑え、3〜5年の長期使用を前提とした保護設計が可能になる?

5. 開発者向け電力監視ツールの拡充

アプリ開発者向けに提供されるツールも進化した。

  • Android Studio Profilerでの電力消費分析が高精度化
  • 各フレームごとのCPU負荷、GPU描画負荷、ネットワーク使用率、バッテリー消費をリアルタイムで可視化
  • 開発段階で省電力設計を意識したUI設計が可能

さらに、Battery Usage Stats APIの改良により、アプリ内部からも消費電力量を把握・最適化できる仕組みが追加される。

ゲーム・マルチメディア機能の進化について

Android 16では、モバイルデバイスを「ゲーム機」、「エンタメプレイヤー」として本格的に使うための土台が大幅に強化された。特にGPU制御、オーディオ出力、ディスプレイ対応が進化し、高性能端末のポテンシャルを最大限に引き出せる設計としている。

1. グラフィックスと描画性能の最適化

Vulkan APIのさらなる強化

  • Android 16ではVulkan 1.3 APIに対応(端末依存あり)
  • アセットの事前読み込み(Pipeline Caching)により読み込み時間を短縮
  • メッシュシェーダーや拡張バッファ制御によって、3D表現のリアリティ向上
  • GPUとCPUの同期処理の遅延(frame pacing)を大幅に削減

これにより、FPSやオープンワールド系ゲームでも滑らかで安定した描画が可能になり、90fpsや120fps表示にも対応しやすくなる。

ハードウェアレベルのレイテンシー改善

  • Graphics HAL(ハードウェア抽象化レイヤ)の改良により、タッチ入力→描画までの遅延を最小化

高速表示が求められるゲームでも、瞬時の反応性が得られる。

2. ゲームダッシュボードの標準搭載

従来はSamsungやASUSなどの一部メーカーUIに限られていた「ゲームツール機能」が、Android 16では標準化された。

  • フレームレート(FPS)モニタリング
  • スクリーンレコード(録画)やライブ配信
  • 通知の一時ブロック
  • タッチ感度や視野角の即時調整
  • リソース節約モードへの切り替え

これにより、ゲーム中のUX改善と集中力維持がしやすくなる。

3. コントローラー対応と操作性の強化

Android 16ではBluetooth HIDプロファイルが拡張され、DualSense(PS5)やXboxコントローラーの高精度入力に正式対応する。

  • アナログトリガー、触覚フィードバック、ジャイロセンサーなども一部ゲームでサポート
  • 外付けゲームパッドやUSB-C接続型ゲームデバイスとのレイテンシー最小化

さらに、画面操作においてもスワイプ範囲のカスタマイズや、指の誤反応防止のタッチ制御アルゴリズムが強化された。

4. マルチメディア:映像と音響の進化

映像体験の進化

  • HDR10+、Dolby Vision、HLG などの最新HDR規格に幅広く対応
  • 輝度レンジ、色域、階調表現が向上し、映画・配信視聴での没入感アップ
  • ハードウェア対応端末では、自動トーンマッピング/暗部補正などのリアルタイム補正が可能

オーディオ機能の進化

  • Spatial Audio(空間オーディオ):対応コンテンツで立体音響再生が可能に
  • 360度の音場感覚
  • 頭の動きに追従する「ヘッドトラッキング機能」にも対応(対応イヤホン必要)

Bluetooth LE Audio対応:

  • 超低遅延通信
  • LC3コーデックによる高音質+省電力
  • 複数デバイス同時接続(ブロードキャスト)への拡張

これにより、ゲーム・動画どちらにおいてもプロレベルのオーディオ体験が可能となる。

5. XR(AR/VR)デバイスへの基盤整備

Android 16では、今後の拡張現実(XR)プラットフォーム展開を見据えて、

  • Googleの新しいARフレームワーク「ARCore 4.0」に最適化
  • 空間マッピングや深度認識の精度向上
  • ヘッドセット連携に向けた低遅延ビデオ出力APIを統合

Meta QuestやSnapdragon Spacesなどの外部XRエコシステムとの統合も視野に入れた設計となっており、将来的にはAndroid端末をXRハブとして活用する流れも予想される。

Spring Data JPA 3.5でSpecification.whereが非推奨になった件

/**
 * Simple static factory method to add some syntactic sugar around a {@link Specification}.
 *
 * @apiNote with 4.0, this method will no longer accept {@literal null} specifications.
 * @param <T> the type of the {@link Root} the resulting {@literal Specification} operates on.
 * @param spec can be {@literal null}.
 * @return guaranteed to be not {@literal null}.
 * @since 2.0
 * @deprecated since 3.5, to be removed with 4.0 as we no longer want to support {@literal null} specifications.
 */
@Deprecated(since = "3.5.0", forRemoval = true)
static <T> Specification<T> where(@Nullable Specification<T> spec) {
    return spec == null ? (root, query, builder) -> null : spec;
}

非推奨になったけれども、まだ代替手段はないようだ。Nullを許容しなくなることを警告するためのものらしい。

The where method merely caters for the broken nullability allowance and is replaced by where(PredicateSpecification) in 4.0.

まだ確定ではないかもしれないが、mainでは下記の様に修正されている。

/**
 * Simple static factory method to add some syntactic sugar translating {@link PredicateSpecification} to
 * {@link Specification}.
 *
 * @param <T> the type of the {@link Root} the resulting {@literal Specification} operates on.
 * @param spec the {@link PredicateSpecification} to wrap.
 * @return guaranteed to be not {@literal null}.
 */
static <T> Specification<T> where(PredicateSpecification<T> spec) {

    Assert.notNull(spec, "PredicateSpecification must not be null");

    return (root, update, criteriaBuilder) -> spec.toPredicate(root, criteriaBuilder);
}

データベース上に「Null」が登録されていることと、Java変数が「Null」であることは意味合いが違うから、そのまま「Null」を渡すことをNGとした感じだろうか。

@Deprecatedを付けるのはまだ早かったのではないだろうか。(同じようにNullを許容しなくなる他のメソッドにはついていないし)

Stable Diffusion WebUI Forge版をインストールする

概要

Stable Diffusion WebUI Forge版は、オリジナル「Automatic1111 Stable Diffusion WebUI」を土台としつつ、開発・運用をより快適にするための最適化と実験的機能を盛り込んだもの。

特徴

  • 高速化・低メモリ化
  • 各種バックエンド対応
  • 一括インストーラ(Windows向け)
  • 実験的機能の研究プラットフォーム

インストール(Windows向け)

gitもpythonも事前インストールは不要。全て含まれているファイルが提供されている。

以下のページにアクセスして、

https://github.com/lllyasviel/stable-diffusion-webui-forge

下記のファイルをダウンロードする。

Just use this one-click installation package (with git and python included).
>>> Click Here to Download One-Click Package (CUDA 12.1 + Pytorch 2.3.1) <<<

任意のフォルダに解答したら、update.batを実行する(初回のみ)。

処理が終わったら、run.batを実行する。

Ultralytics YOLOで物体検出する

目的

カメラ映像にリアルタイムで物体名をラベル付けする。

環境

Winodows 11(Ubuntuでも動いた。Ubuntu on Raspberry Pi 4でも動いた。)
Python 3.12
PyTorch(指定したバージョンのCUDAを使用したいなど、理由があれば先に入れておく。後続の作業で自動的にインストールすることもできるので、必要に応じて)

pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126

ライセンス

Ultralytics YOLOを無償で使うならAGPL-3.0 Licenseになる。

AGPL-3.0 License: This OSI-approved open-source license is perfect for students, researchers, and enthusiasts. It encourages open collaboration and knowledge sharing. See the LICENSE file for full details.

仮想環境を作成してインストール

mkdir yolo
cd yolo
python -m venv --system-site-packages venv

venv\Scripts\activate

pip install ultralytics

動作確認

yolo detect predict model=yolo11n.pt

Ultralytics 8.3.103 🚀 Python-3.12.9 torch-2.6.0+cu126 CUDA:0 (NVIDIA GeForce RTX 4060 Ti, 16380MiB)
YOLO11n summary (fused): 100 layers, 2,616,248 parameters, 0 gradients, 6.5 GFLOPs

image 1/2 D:\yolo\venv\Lib\site-packages\ultralytics\assets\bus.jpg: 640×480 4 persons, 1 bus, 44.3ms
image 2/2 D:\yolo\venv\Lib\site-packages\ultralytics\assets\zidane.jpg: 384×640 2 persons, 1 tie, 45.9ms
Speed: 3.5ms preprocess, 45.1ms inference, 62.8ms postprocess per image at shape (1, 3, 384, 640)
Results saved to runs\detect\predict

※以下はRaspberry Pi 4で動かしたときの実行結果。(さすがに遅い)

Ultralytics 8.3.105 🚀 Python-3.12.3 torch-2.6.0+cpu CPU (Cortex-A72)
YOLO11n summary (fused): 100 layers, 2,616,248 parameters, 0 gradients, 6.5 GFLOPs

image 1/2 /home/xxx/yolo/venv/lib/python3.12/site-packages/ultralytics/assets/bus.jpg: 640×480 4 persons, 1 bus, 1180.9ms
image 2/2 /home/xxx/yolo/venv/lib/python3.12/site-packages/ultralytics/assets/zidane.jpg: 384×640 2 persons, 1 tie, 935.2ms
Speed: 16.4ms preprocess, 1058.1ms inference, 6.1ms postprocess per image at shape (1, 3, 384, 640)
Results saved to runs/detect/predict

※Raspberry Pi 5ならNCNNを使用することも検討。Pi 4ではncnnのエクスポートがエラーになって動かなかった。
pip install ncnn
yolo export model=yolo11n.pt format=ncnn

実装

こちら(https://www.ejtech.io/learn/yolo-on-raspberry-pi)を参考にさせて頂いた。

以下のコードではyolo11lを使っている。Raspberry Piで実行するならyolo11nかyolo11sのどちらかのみになるかと思う。
こちら(https://docs.ultralytics.com/ja/models/yolo11/)からダウンロードすることが出来る。

import time

import cv2
import numpy as np

from ultralytics import YOLO

# Load the model into memory and get labemap
model = YOLO('yolo11l.pt', task='detect')
labels = model.names

# Load image source
cap = cv2.VideoCapture(0)

# Set bounding box colors (using the Tableu 10 color scheme)
bbox_colors = [(164,120,87), (68,148,228), (93,97,209), (178,182,133), (88,159,106), 
              (96,202,231), (159,124,168), (169,162,241), (98,118,150), (172,176,184)]

# Initialize control and status variables
avg_frame_rate = 0
frame_rate_buffer = []
fps_avg_len = 200

# Begin inference loop
while True:

    t_start = time.perf_counter()
    # Load frame from image source
    ret, frame = cap.read()
    if (frame is None) or (not ret):
        print('Unable to read frames from the camera. This indicates the camera is disconnected or not working. Exiting program.')
        break

    # Run inference on frame
    results = model(frame, verbose=False)

    # Extract results
    detections = results[0].boxes

    # Initialize variable for basic object counting example
    object_count = 0

    # Go through each detection and get bbox coords, confidence, and class
    for i in range(len(detections)):

        # Get bounding box coordinates
        # Ultralytics returns results in Tensor format, which have to be converted to a regular Python array
        xyxy_tensor = detections[i].xyxy.cpu() # Detections in Tensor format in CPU memory
        xyxy = xyxy_tensor.numpy().squeeze() # Convert tensors to Numpy array
        xmin, ymin, xmax, ymax = xyxy.astype(int) # Extract individual coordinates and convert to int

        # Get bounding box class ID and name
        classidx = int(detections[i].cls.item())
        classname = labels[classidx]

        # Get bounding box confidence
        conf = detections[i].conf.item()

        # Draw box if confidence threshold is high enough
        if conf > 0.5:

            color = bbox_colors[classidx % 10]
            cv2.rectangle(frame, (xmin,ymin), (xmax,ymax), color, 2)

            label = f'{classname}: {int(conf*100)}%'
            labelSize, baseLine = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1) # Get font size
            label_ymin = max(ymin, labelSize[1] + 10) # Make sure not to draw label too close to top of window
            cv2.rectangle(frame, (xmin, label_ymin-labelSize[1]-10), (xmin+labelSize[0], label_ymin+baseLine-10), color, cv2.FILLED) # Draw white box to put label text in
            cv2.putText(frame, label, (xmin, label_ymin-7), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1) # Draw label text

            # Basic example: count the number of objects in the image
            object_count = object_count + 1

    # Calculate and draw framerate (if using video, USB, or Picamera source)
    cv2.putText(frame, f'FPS: {avg_frame_rate:0.2f}', (10,20), cv2.FONT_HERSHEY_SIMPLEX, .7, (0,255,255), 2) # Draw framerate
    
    # Display detection results
    cv2.putText(frame, f'Number of objects: {object_count}', (10,40), cv2.FONT_HERSHEY_SIMPLEX, .7, (0,255,255), 2) # Draw total number of detected objects
    cv2.imshow('YOLO detection results',frame) # Display image

    # If inferencing on individual images, wait for user keypress before moving to next image. Otherwise, wait 5ms before moving to next frame.
    key = cv2.waitKey(5)
    
    if key == ord('q') or key == ord('Q'): # Press 'q' to quit
        break
    elif key == ord('s') or key == ord('S'): # Press 's' to pause inference
        cv2.waitKey()
    elif key == ord('p') or key == ord('P'): # Press 'p' to save a picture of results on this frame
        cv2.imwrite('capture.png',frame)
    
    # Calculate FPS for this frame
    t_stop = time.perf_counter()
    frame_rate_calc = float(1/(t_stop - t_start))

    # Append FPS result to frame_rate_buffer (for finding average FPS over multiple frames)
    if len(frame_rate_buffer) >= fps_avg_len:
        temp = frame_rate_buffer.pop(0)
        frame_rate_buffer.append(frame_rate_calc)
    else:
        frame_rate_buffer.append(frame_rate_calc)

    # Calculate average FPS for past frames
    avg_frame_rate = np.mean(frame_rate_buffer)

# Clean up
print(f'Average pipeline FPS: {avg_frame_rate:.2f}')
cap.release()
cv2.destroyAllWindows()

Modelをyolo11n.ptにしてRaspberry Pi 4でも動かしたが、0.9 FPSしか出なかった。Raspberry Pi 5やRaspberry Pi AI HAT+が欲しくなる。