diff --git a/.dockerignore b/.dockerignore
index 2ca1beb2a56a4cb45c526deab8306417c095f6fe..85bef57491e7b9179dbcd5e3d2f084d84b715a1a 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,2 +1,7 @@
-.git
-python/__pycache__
+.git*
+Dockerfile
+**/__pycache__
+.readthedocs.yaml
+doc/*
+mkdocs.yml
+RELEASE_NOTES.txt
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index bd41bbf9a6151e4c3b0f47b7eab4147f0aa263d4..35dfe14298ee0b3506fd6023f6e6c40755f15681 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,269 +1,257 @@
-variables:
-  OTBTF_VERSION: 4.3.1
-  OTB_BUILD: /src/otb/build/OTB/build  # Local OTB build directory
-  OTBTF_SRC: /src/otbtf  # Local OTBTF source directory
-  OTB_TEST_DIR: $OTB_BUILD/Testing/Temporary  # OTB testing directory
-  ARTIFACT_TEST_DIR: $CI_PROJECT_DIR/testing
-  CRC_BOOK_TMP: /tmp/crc_book_tests_tmp
-  API_TEST_TMP: /tmp/api_tests_tmp
-  DATADIR: $CI_PROJECT_DIR/test/data
-  DOCKER_BUILDKIT: 1
-  DOCKER_DRIVER: overlay2
-  CACHE_IMAGE_BASE: $CI_REGISTRY_IMAGE:otbtf-base
-  CACHE_IMAGE_BUILDER: $CI_REGISTRY_IMAGE:builder
-  BRANCH_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
-  DEV_IMAGE: $CI_REGISTRY_IMAGE:cpu-basic-dev-testing
-  CI_REGISTRY_PUBIMG: $CI_REGISTRY_IMAGE:$OTBTF_VERSION
-  DOCKERHUB_BASE: mdl4eo/otbtf
-  DOCKERHUB_IMAGE_BASE: ${DOCKERHUB_BASE}:${OTBTF_VERSION}
-  CPU_BASE_IMG: ubuntu:22.04
-  GPU_BASE_IMG: nvidia/cuda:12.0.1-cudnn8-devel-ubuntu22.04
-
-image: $BRANCH_IMAGE
-
 workflow:
   rules:
-    - if: $CI_MERGE_REQUEST_ID || $CI_COMMIT_REF_NAME =~ /master/ # Execute jobs in merge request context, or commit in master branch
-    
+    # Ignore pipeline for filthy commits
+    - if: $CI_COMMIT_MESSAGE =~ /^wip.*/i
+      when: never
+    # Execute for MR, but avoid duplicated due to branch triggers in MR
+    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
+      when: never
+    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
+    # Else execute on tags or protected branches
+    - if: $CI_COMMIT_TAG
+    - if: $CI_COMMIT_REF_PROTECTED == "true"
+  auto_cancel:
+    on_new_commit: interruptible
+
 stages:
   - Build
   - Static Analysis
   - Documentation
   - Test
   - Applications Test
-  - Update dev image
   - Ship
 
+variables:
+  OTBTF_VERSION: 5.0.0-rc2
+  OTBTF_SRC: /src/otbtf  # OTBTF source directory path in image
+  DATADIR: $CI_PROJECT_DIR/test/data
+  CACHE_IMAGE_CPU: $CI_REGISTRY_IMAGE:build-cache-cpu
+  CACHE_IMAGE_GPU: $CI_REGISTRY_IMAGE:build-cache-gpu
+  BRANCH_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
+  BUILDX_BUILDER: container
+  BUILDX_NO_DEFAULT_ATTESTATIONS: 1
+
+default:
+  tags: [ godzilla ]
+  interruptible: true
+  image:
+    name: $BRANCH_IMAGE
+    pull_policy: always
+
 .docker_build_base:
-  allow_failure: false
-  tags: [godzilla]
-  image: docker:latest
+  image: docker:27.5.1
   services:
-    - name: docker:dind
+    - name: docker:27.5.1-dind
   before_script:
     - echo -n $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
-  timeout: 10 hours
-
+    - docker buildx create --name=container --driver=docker-container --use --bootstrap
+  after_script:
+    - docker buildx rm --keep-state container
 
-docker image:
-  extends: .docker_build_base
+docker_image:
   stage: Build
-  except:
+  only:
+    - merge_requests
     - develop
+  extends: .docker_build_base
   script:
     - >
-      docker build
-      --target otbtf-base
-      --cache-from $CACHE_IMAGE_BASE
-      --tag $CACHE_IMAGE_BASE
-      --build-arg BASE_IMG=$CPU_BASE_IMG
-      --build-arg BUILDKIT_INLINE_CACHE=1
-      "."
-    - docker push $CACHE_IMAGE_BASE
-    - >
-      docker build
-      --target builder
-      --cache-from $CACHE_IMAGE_BASE
-      --cache-from $CACHE_IMAGE_BUILDER
-      --tag $CACHE_IMAGE_BUILDER
-      --build-arg KEEP_SRC_OTB="true"
-      --build-arg BZL_CONFIGS=""
-      --build-arg BASE_IMG=$CPU_BASE_IMG
-      --build-arg BUILDKIT_INLINE_CACHE=1
-      --build-arg BZL_OPTIONS="--verbose_failures --remote_cache=$BAZELCACHE"
-      --build-arg OTBTESTS="true"
+      docker buildx build --push -t $BRANCH_IMAGE
+      --cache-from type=registry,ref=$CACHE_IMAGE_CPU
+      --cache-to type=registry,ref=$CACHE_IMAGE_CPU,mode=max
+      --build-arg DEV_IMAGE=true
       "."
-    - docker push $CACHE_IMAGE_BUILDER
-    - >
-      docker build
-      --cache-from $CACHE_IMAGE_BASE
-      --cache-from $CACHE_IMAGE_BUILDER
-      --cache-from $BRANCH_IMAGE
-      --cache-from $DEV_IMAGE
-      --tag $BRANCH_IMAGE
-      --build-arg KEEP_SRC_OTB="true"
-      --build-arg BZL_CONFIGS=""
-      --build-arg BASE_IMG=$CPU_BASE_IMG
-      --build-arg BUILDKIT_INLINE_CACHE=1
-      --build-arg BZL_OPTIONS="--verbose_failures --remote_cache=$BAZELCACHE"
-      --build-arg OTBTESTS="true"
-      "."
-    - docker push $BRANCH_IMAGE
 
 .static_analysis_base:
   stage: Static Analysis
   allow_failure: true
+  only:
+    - merge_requests
 
 flake8:
   extends: .static_analysis_base
   script:
-    - sudo pip install flake8
     - flake8 $OTBTF_SRC/otbtf --exclude=tensorflow_v1x
 
 pylint:
   extends: .static_analysis_base
   script:
-    - sudo pip install pylint
     - pylint $OTBTF_SRC/otbtf --ignore=tensorflow_v1x
 
 codespell:
+  rules:
   extends: .static_analysis_base
   script:
-    - sudo pip install codespell
-    - codespell otbtf
-    - codespell doc
+    - codespell otbtf doc README.md
 
 cppcheck:
   extends: .static_analysis_base
   script:
-    - sudo apt update && sudo apt install cppcheck -y
     - cd $OTBTF_SRC/ && cppcheck --enable=all --error-exitcode=1 -I include/ --suppress=missingInclude --suppress=unusedFunction .
 
-.doc_base:
+docs:
   stage: Documentation
+  tags: [ stable ]
+  needs: []
+  image: python:3.10-slim
+  variables:
+    PTH: public_test
+  rules:
+    - if: $CI_COMMIT_TAG
+      when: never
+    - if: $CI_COMMIT_REF_NAME == /master/
+      variables:
+        PTH: public
+    - changes:
+      - doc/**/*
+      - "*.{md,txt}"
+      - mkdocs.yml
+      - .readthedocs.yaml
   before_script:
     - pip install -r doc/doc_requirements.txt
-  artifacts:
-    paths:
-      - public
-      - public_test
-
-pages_test:
-  extends: .doc_base
-  except:
-    - master
   script:
-    - mkdocs build --site-dir public_test
-
-pages:
-  extends: .doc_base
-  only:
-    - master
-  script:
-    - mkdocs build --site-dir public
+    - mkdocs build --site-dir $PTH
   artifacts:
     paths:
       - public
+      - public_test
 
 .tests_base:
-  tags: [godzilla]
+  only:
+    - merge_requests
   artifacts:
-    paths:
-      - $ARTIFACT_TEST_DIR/*.*
+    reports:
+      junit: report_*.xml
     expire_in: 1 week
     when: on_failure
 
 ctest:
+  stage: Test
   extends: .tests_base
+  needs: ["docker_image"]
+  variables:
+    CTEST_OUTPUT_ON_FAILURE: 1
+    OTB_TEST_UNITS: "Tensorflow|PanSharpening|Projection|Transform|IOGDAL"
+    OTB_BUILD: /src/otb/build/OTB/build
+  script:
+    - cd /src/otb/otb && git lfs pull
+    - ln -s $CI_PROJECT_DIR/test/data $OTBTF_SRC/test/data
+    - ln -s $CI_PROJECT_DIR/test/models $OTBTF_SRC/test/models
+    - cd $OTB_BUILD
+    - ctest -L $OTB_TEST_UNITS --output-junit $CI_PROJECT_DIR/report_ctest.xml
+  after_script:
+    - cp -r $OTB_BUILD/Testing/Temporary/* $CI_PROJECT_DIR/artifacts_ctest
+  artifacts:
+    paths:
+      - $CI_PROJECT_DIR/artifacts_ctest
+
+python_api:
   stage: Test
+  extends: .tests_base
+  variables:
+    API_TEST_TMP: /tmp/api_tests_tmp
   script:
-    - sudo apt update && sudo apt install -y git-lfs 
-    - cd /src/otb/otb && sudo git lfs fetch --all && sudo git lfs pull
-    - cd $OTB_BUILD/
-    - sudo ctest -L OTBTensorflow
-    - sudo ctest -L OTBPanSharpening
-    - sudo ctest -L OTBProjection
-    - sudo ctest -L OTBTransform
-    - sudo ctest -L OTBIOGDAL
+    - mkdir $API_TEST_TMP
+    - TMPDIR=$API_TEST_TMP python -m pytest -svv --junitxml=report_api.xml test/api_unittest.py
   after_script:
-    - cp -r $OTB_TEST_DIR $ARTIFACT_TEST_DIR
+    - saved_model_cli show --dir $API_TEST_TMP/model_from_pimg --all
+    - cp -r $API_TEST_TMP $CI_PROJECT_DIR/artifacts_test_api
+  artifacts:
+    paths:
+      - $CI_PROJECT_DIR/artifacts_test_api
 
 .applications_test_base:
-  extends: .tests_base
   stage: Applications Test
-  before_script:
-    - pip install pytest pytest-cov pytest-order
-    - mkdir -p $ARTIFACT_TEST_DIR
-    - cd $CI_PROJECT_DIR
+  extends: .tests_base
+  needs: ["ctest", "python_api"]
+  #when: manual
 
 crc_book:
   extends: .applications_test_base
+  when: manual
+  allow_failure: true
+  variables:
+    CRC_BOOK_TMP: /tmp/crc_book_tests_tmp
   script:
     - mkdir -p $CRC_BOOK_TMP
-    - TMPDIR=$CRC_BOOK_TMP python -m pytest --junitxml=$CI_PROJECT_DIR/report_tutorial.xml $OTBTF_SRC/test/tutorial_unittest.py
+    - TMPDIR=$CRC_BOOK_TMP python -m pytest -v --junitxml=report_tutorial.xml test/tutorial_unittest.py
   after_script:
-    - cp $CRC_BOOK_TMP/*.* $ARTIFACT_TEST_DIR/
-    
+    - cp -r $CRC_BOOK_TMP $CI_PROJECT_DIR/artifacts_crc_book
+  artifacts:
+    paths:
+      - $CI_PROJECT_DIR/artifacts_crc_book
+
+decloud:
+  extends: .applications_test_base
+  when: manual
+  allow_failure: true
+  variables:
+    DATASET_DECLOUD: https://nextcloud.inrae.fr/s/aNTWLcH28zNomqk/download
+    DECLOUD_DATA_DIR: $CI_PROJECT_DIR/decloud_data
+  script:
+    - git clone https://forgemia.inra.fr/umr-tetis/decloud.git -b keras3
+    - pip install -r $PWD/decloud/requirements.txt
+    - wget -q $DATASET_DECLOUD -O file.zip && unzip file.zip
+    - pytest -v decloud/tests/train_from_tfrecords_unittest.py
+
 sr4rs:
   extends: .applications_test_base
+  variables:
+    DATASET_S2: https://nextcloud.inrae.fr/s/EZL2JN7SZyDK8Cf/download/sr4rs_sentinel2_bands4328_france2020_savedmodel.zip
+    DATASET_SR4RS: https://nextcloud.inrae.fr/s/kDms9JrRMQE2Q5z/download
   script:
-    - wget -O sr4rs_sentinel2_bands4328_france2020_savedmodel.zip
-      https://nextcloud.inrae.fr/s/EZL2JN7SZyDK8Cf/download/sr4rs_sentinel2_bands4328_france2020_savedmodel.zip
+    - wget -qO sr4rs_sentinel2_bands4328_france2020_savedmodel.zip $DATASET_S2
     - unzip -o sr4rs_sentinel2_bands4328_france2020_savedmodel.zip
-    - wget -O sr4rs_data.zip https://nextcloud.inrae.fr/s/kDms9JrRMQE2Q5z/download
+    - wget -qO sr4rs_data.zip $DATASET_SR4RS
     - unzip -o sr4rs_data.zip
     - rm -rf sr4rs
     - git clone https://github.com/remicres/sr4rs.git
     - export PYTHONPATH=$PYTHONPATH:$PWD/sr4rs
-    - python -m pytest --junitxml=$ARTIFACT_TEST_DIR/report_sr4rs.xml $OTBTF_SRC/test/sr4rs_unittest.py
-
-decloud:
-  extends: .applications_test_base
-  script:
-    - git clone https://github.com/CNES/decloud.git
-    - pip install -r $PWD/decloud/docker/requirements.txt
-    - wget https://nextcloud.inrae.fr/s/aNTWLcH28zNomqk/download -O file.zip && unzip file.zip
-    - export DECLOUD_DATA_DIR="$PWD/decloud_data"
-    - pytest decloud/tests/train_from_tfrecords_unittest.py
-
-otbtf_api:
-  extends: .applications_test_base
-  script:
-    - mkdir $API_TEST_TMP
-    - TMPDIR=$API_TEST_TMP python -m pytest --junitxml=$ARTIFACT_TEST_DIR/report_api.xml $OTBTF_SRC/test/api_unittest.py
-  after_script:
-    - cp $API_TEST_TMP/*.* $ARTIFACT_TEST_DIR/
+    - python -m pytest -v --junitxml=report_sr4rs.xml test/sr4rs_unittest.py
 
 geos_enabled:
   extends: .applications_test_base
   script:
-    - python -m pytest --junitxml=$ARTIFACT_TEST_DIR/report_geos_enabled.xml $OTBTF_SRC/test/geos_test.py
+    - python -m pytest -v --junitxml=report_geos_enabled.xml test/geos_test.py
 
 planetary_computer:
   extends: .applications_test_base
   script:
     - pip install pystac_client planetary_computer
-    - python -m pytest --junitxml=$ARTIFACT_TEST_DIR/report_pc_enabled.xml $OTBTF_SRC/test/pc_test.py
-
-imports:
-  extends: .applications_test_base
-  script:
-    - python -m pytest --junitxml=$ARTIFACT_TEST_DIR/report_imports.xml $OTBTF_SRC/test/imports_test.py
+    - python -m pytest -v --junitxml=report_pc_enabled.xml test/pc_test.py
 
 numpy_gdal_otb:
   extends: .applications_test_base
   script:
-    - python -m pytest --junitxml=$ARTIFACT_TEST_DIR/report_numpy.xml $OTBTF_SRC/test/numpy_test.py
+    - python -m pytest -v --junitxml=report_numpy.xml test/numpy_test.py
 
 rio:
   extends: .applications_test_base
   script:
-    - sudo pip install rasterio
-    - python -m pytest --junitxml=$ARTIFACT_TEST_DIR/report_rio.xml $OTBTF_SRC/test/rio_test.py
+    - pip install rasterio --no-binary rasterio
+    - python -m pytest -v --junitxml=report_rio.xml test/rio_test.py
 
 nodata:
   extends: .applications_test_base
   script:
-    - python -m pytest --junitxml=$ARTIFACT_TEST_DIR/report_nodata.xml $OTBTF_SRC/test/nodata_test.py
+    - python -m pytest -v --junitxml=report_nodata.xml test/nodata_test.py
 
-deploy_cpu-dev-testing:
-  stage: Update dev image
+.ship_base:
   extends: .docker_build_base
-  except:
-    - master
-  script:
-    - docker pull $BRANCH_IMAGE
-    - docker tag $BRANCH_IMAGE $DEV_IMAGE
-    - docker push $DEV_IMAGE
-
-.ship base:
-  extends: .docker_build_base
-  stage: Ship
   only:
-    - master
+    - tags
+  variables:
+    DOCKERHUB_BASE: mdl4eo/otbtf
+    CI_REGISTRY_PUBIMG: $CI_REGISTRY_IMAGE:$OTBTF_VERSION
+    DOCKERHUB_IMAGE_BASE: $DOCKERHUB_BASE:$OTBTF_VERSION
+  before_script:
+    - echo -n $DOCKERHUB_TOKEN | docker login -u mdl4eo --password-stdin
+    - echo -n $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
+    - docker buildx create --name=container --driver=docker-container --use --bootstrap
 
 deploy_cpu:
-  extends: .ship base
+  stage: Ship
+  extends: .ship_base
   variables:
     IMAGE_CPU: $CI_REGISTRY_PUBIMG-cpu
     IMAGE_CPUDEV: $CI_REGISTRY_PUBIMG-cpu-dev
@@ -272,51 +260,49 @@ deploy_cpu:
     DOCKERHUB_LATEST: $DOCKERHUB_BASE:latest
   script:
     # cpu
-    - docker build --build-arg BZL_OPTIONS="--remote_cache=$BAZELCACHE" --tag $IMAGE_CPU --build-arg BASE_IMG=$CPU_BASE_IMG --build-arg BZL_CONFIGS="" .
-    - docker push $IMAGE_CPU
+    - >
+      docker buildx build --push -t $IMAGE_CPU
+      --cache-from type=registry,ref=$CACHE_IMAGE_CPU
+      .
     # cpu-dev
-    - docker build --build-arg BZL_OPTIONS="--remote_cache=$BAZELCACHE" --tag $IMAGE_CPUDEV --build-arg BASE_IMG=$CPU_BASE_IMG --build-arg BZL_CONFIGS="" --build-arg KEEP_SRC_OTB=true .
-    - docker push $IMAGE_CPUDEV
+    - >
+      docker buildx build --push -t $IMAGE_CPUDEV
+      --cache-from type=registry,ref=$CACHE_IMAGE_CPU
+      --build-arg DEV_IMAGE=true
+      .
     # push images on dockerhub
-    - echo -n $DOCKERHUB_TOKEN | docker login -u mdl4eo --password-stdin
-    - docker tag $IMAGE_CPU $DOCKERHUB_CPU
-    - docker push $DOCKERHUB_CPU
-    - docker tag $IMAGE_CPUDEV $DOCKERHUB_CPUDEV
-    - docker push $DOCKERHUB_CPUDEV
+    - docker tag $IMAGE_CPU $DOCKERHUB_CPU && docker push $DOCKERHUB_CPU
+    - docker tag $IMAGE_CPUDEV $DOCKERHUB_CPUDEV && docker push $DOCKERHUB_CPUDEV
     # latest = cpu image
-    - docker tag $IMAGE_CPU $DOCKERHUB_LATEST
-    - docker push $DOCKERHUB_LATEST
+    - docker tag $IMAGE_CPU $DOCKERHUB_LATEST && docker push $DOCKERHUB_LATEST
 
 deploy_gpu:
-  extends: .ship base
+  stage: Ship
+  extends: .ship_base
   variables:
     IMAGE_GPU: $CI_REGISTRY_PUBIMG-gpu
     IMAGE_GPUDEV: $CI_REGISTRY_PUBIMG-gpu-dev
-    IMAGE_GPUOPT: $CI_REGISTRY_PUBIMG-gpu-opt
-    IMAGE_GPUOPTDEV: $CI_REGISTRY_PUBIMG-gpu-opt-dev
     DOCKERHUB_GPU: $DOCKERHUB_IMAGE_BASE-gpu
     DOCKERHUB_GPUDEV: $DOCKERHUB_IMAGE_BASE-gpu-dev
     DOCKERHUB_GPULATEST: $DOCKERHUB_BASE:latest-gpu
   script:
-    # gpu-opt
-    - docker build --build-arg BZL_OPTIONS="--remote_cache=$BAZELCACHE" --tag $IMAGE_GPUOPT --build-arg BASE_IMG=$GPU_BASE_IMG .
-    - docker push $IMAGE_GPUOPT
-    # gpu-opt-dev
-    - docker build --build-arg BZL_OPTIONS="--remote_cache=$BAZELCACHE" --tag $IMAGE_GPUOPTDEV --build-arg BASE_IMG=$GPU_BASE_IMG --build-arg KEEP_SRC_OTB=true .
-    - docker push $IMAGE_GPUOPTDEV
-    # gpu-basic
-    - docker build --build-arg BZL_OPTIONS="--remote_cache=$BAZELCACHE" --tag $IMAGE_GPU --build-arg BASE_IMG=$GPU_BASE_IMG --build-arg BZL_CONFIGS="" .
-    - docker push $IMAGE_GPU
-    # gpu-basic-dev
-    - docker build --build-arg BZL_OPTIONS="--remote_cache=$BAZELCACHE" --tag $IMAGE_GPUDEV --build-arg BZL_CONFIGS="" --build-arg BASE_IMG=$GPU_BASE_IMG --build-arg KEEP_SRC_OTB=true .
-    - docker push $IMAGE_GPUDEV
-    # push gpu-basic* images on dockerhub
-    - echo -n $DOCKERHUB_TOKEN | docker login -u mdl4eo --password-stdin
-    - docker tag $IMAGE_GPU $DOCKERHUB_GPU
-    - docker push $DOCKERHUB_GPU
-    - docker tag $IMAGE_GPUDEV $DOCKERHUB_GPUDEV
-    - docker push $DOCKERHUB_GPUDEV
+    # gpu
+    - >
+      docker buildx build --push -t $IMAGE_GPU
+      --cache-from type=registry,ref=$CACHE_IMAGE_GPU
+      --cache-from type=registry,ref=$CACHE_IMAGE_CPU
+      --cache-to type=registry,ref=$CACHE_IMAGE_GPU,mode=max
+      --build-arg WITH_CUDA=true
+      .
+    # gpu-dev
+    - >
+      docker buildx build --push -t $IMAGE_GPUDEV
+      --cache-from type=registry,ref=$CACHE_IMAGE_GPU
+      --cache-from type=registry,ref=$CACHE_IMAGE_CPU
+      --build-arg WITH_CUDA=true --build-arg DEV_IMAGE=true
+      .
+    # push gpu-* images on dockerhub
+    - docker tag $IMAGE_GPU $DOCKERHUB_GPU && docker push $DOCKERHUB_GPU
+    - docker tag $IMAGE_GPUDEV $DOCKERHUB_GPUDEV && docker push $DOCKERHUB_GPUDEV
     # latest-gpu = gpu image
-    - docker tag $IMAGE_GPU $DOCKERHUB_GPULATEST
-    - docker push $DOCKERHUB_GPULATEST
-
+    - docker tag $IMAGE_GPU $DOCKERHUB_GPULATEST && docker push $DOCKERHUB_GPULATEST
diff --git a/Dockerfile b/Dockerfile
index 904431e35349f5a27f35250e834594f5baa3f4d5..0aacaa5dae9321c3897e5c2564cfe17ca17c6fbf 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,114 +1,109 @@
-##### Configurable Dockerfile with multi-stage build - Author: Vincent Delbar
-## Mandatory
-ARG BASE_IMG
-
+##### OTBTF configurable Dockerfile with multi-stage build
 # ----------------------------------------------------------------------------
-# Init base stage - will be cloned as intermediate build env
-FROM $BASE_IMG AS otbtf-base
+# Init base stage - used for intermediate build env and final image
+
+# Freeze ubuntu version to avoid surprise rebuild
+FROM ubuntu:jammy-20250126 AS base-stage
+
 WORKDIR /tmp
 
-### System packages
-COPY tools/docker/build-deps-*.txt ./
+# System packages
 ARG DEBIAN_FRONTEND=noninteractive
+COPY system-dependencies.txt .
 RUN apt-get update -y && apt-get upgrade -y \
- && cat build-deps-cli.txt | xargs apt-get install --no-install-recommends -y \
+ && cat system-dependencies.txt | xargs apt-get install --no-install-recommends -y \
  && apt-get clean && rm -rf /var/lib/apt/lists/*
-### Python3 links and pip packages
-RUN ln -s /usr/bin/python3 /usr/local/bin/python && ln -s /usr/bin/pip3 /usr/local/bin/pip
-# Upgrade pip
-RUN pip install --no-cache-dir pip --upgrade
-# In case NumPy version is conflicting with system's gdal dep and may require venv
-ARG NUMPY_SPEC=""
-# This is to avoid https://github.com/tensorflow/tensorflow/issues/61551
-ARG PROTO_SPEC="==4.23.*"
-RUN pip install --no-cache-dir -U wheel mock six future tqdm deprecated "numpy$NUMPY_SPEC" "protobuf$PROTO_SPEC" packaging requests \
- && pip install --no-cache-dir --no-deps keras_applications keras_preprocessing
 
-# ----------------------------------------------------------------------------
-# Tmp builder stage - dangling cache should persist until "docker builder prune"
-FROM otbtf-base AS builder
-# A smaller value may be required to avoid OOM errors when building OTB
+# Env required during build and for the final image
+ENV PY=3.10
+ENV VIRTUAL_ENV=/opt/otbtf/venv
+ENV PATH="$VIRTUAL_ENV/bin:/opt/otbtf/bin:$PATH"
+ENV PYTHON_SITE_PACKAGES="$VIRTUAL_ENV/lib/python$PY/site-packages"
+ENV LD_LIBRARY_PATH=/opt/otbtf/lib
+# A smaller value may be used to limit bazel or to avoid OOM errors while building OTB
 ARG CPU_RATIO=1
 
-RUN mkdir -p /src/tf /opt/otbtf/bin /opt/otbtf/include /opt/otbtf/lib/python3
+# ----------------------------------------------------------------------------
+# Builder stage: bazel clang tensorflow
+FROM base-stage AS tf-build
 WORKDIR /src/tf
+RUN mkdir -p /opt/otbtf/bin /opt/otbtf/lib /opt/otbtf/include
 
-RUN git config --global advice.detachedHead false
-
-### TF
-
-ARG TF=v2.14.0
-ARG TENSORRT
-
-# Install bazelisk (will read .bazelversion and download the right bazel binary - latest by default)
-RUN wget -qO /opt/otbtf/bin/bazelisk https://github.com/bazelbuild/bazelisk/releases/latest/download/bazelisk-linux-amd64 \
- && chmod +x /opt/otbtf/bin/bazelisk \
- && ln -s /opt/otbtf/bin/bazelisk /opt/otbtf/bin/bazel
-
-ARG BZL_TARGETS="//tensorflow:libtensorflow_cc.so //tensorflow/tools/pip_package:build_pip_package"
-
-# "--config=opt" will enable 'march=native'
-# (otherwise read comments about CPU compatibility and edit CC_OPT_FLAGS in
-# build-env-tf.sh)
-ARG BZL_CONFIGS="--config=nogcp --config=noaws --config=nohdfs --config=opt"
-
-# "--compilation_mode opt" is already enabled by default (see tf repo .bazelrc
-# and configure.py)
-ARG BZL_OPTIONS="--verbose_failures --remote_cache=http://localhost:9090"
-
-# Build
-ARG ZIP_TF_BIN=false
-COPY tools/docker/build-env-tf.sh ./
-RUN git clone --single-branch -b $TF https://github.com/tensorflow/tensorflow.git
-RUN cd tensorflow \
- && export PATH=$PATH:/opt/otbtf/bin \
- && export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/otbtf/lib \
- && bash -c '\
-      source ../build-env-tf.sh \
-      && ./configure \
-      && export TMP=/tmp/bazel \
-      && BZL_CMD="build $BZL_TARGETS $BZL_CONFIGS $BZL_OPTIONS" \
-      && bazel $BZL_CMD --jobs="HOST_CPUS*$CPU_RATIO" '
-
-# Installation
-RUN apt update && apt install -y patchelf
-RUN cd tensorflow \
- && ./bazel-bin/tensorflow/tools/pip_package/build_pip_package /tmp/tensorflow_pkg \
- && pip3 install --no-cache-dir --prefix=/opt/otbtf /tmp/tensorflow_pkg/tensorflow*.whl \
- && ln -s /opt/otbtf/local/lib/python3.*/* /opt/otbtf/lib/python3 \
- && ln -s /opt/otbtf/local/bin/* /opt/otbtf/bin \
- && ln -s $(find /opt/otbtf -type d -wholename "*/dist-packages/tensorflow/include") /opt/otbtf/include/tf \
- # The only missing header in the wheel
- && cp tensorflow/cc/saved_model/tag_constants.h /opt/otbtf/include/tf/tensorflow/cc/saved_model/ \
- && cp tensorflow/cc/saved_model/signature_constants.h /opt/otbtf/include/tf/tensorflow/cc/saved_model/ \
- # Symlink external libs (required for MKL - libiomp5)
- && for f in $(find -L /opt/otbtf/include/tf -wholename "*/external/*/*.so"); do ln -s $f /opt/otbtf/lib/; done \
- # Compress and save TF binaries
- && ( ! $ZIP_TF_BIN || zip -9 -j --symlinks /opt/otbtf/tf-$TF.zip tensorflow/cc/saved_model/tag_constants.h tensorflow/cc/saved_model/signature_constants.h bazel-bin/tensorflow/libtensorflow_cc.so* /tmp/tensorflow_pkg/tensorflow*.whl ) \
- # Cleaning
- && rm -rf bazel-* /src/tf /root/.cache/ /tmp/*
+# Clang + LLVM
+ARG LLVM=18
 
-### OTB
+ADD https://apt.llvm.org/llvm.sh llvm.sh
+RUN bash ./llvm.sh $LLVM
+ENV CC=/usr/bin/clang-$LLVM
+ENV CXX=/usr/bin/clang++-$LLVM
+ENV BAZEL_COMPILER="/usr/bin/clang-$LLVM"
+RUN apt-get update -y && apt-get upgrade -y \
+ && apt-get install -y lld-$LLVM libomp-$LLVM-dev \
+ && apt-get clean && rm -rf /var/lib/apt/lists/*
 
-ARG OTB=release-9.0
-ARG OTBTESTS=false
+### Python venv and packages
+RUN virtualenv $VIRTUAL_ENV
+RUN pip install --no-cache-dir -U pip wheel
+# Numpy 2 support in TF is planned for 2.18, but isn't supported by most libraries for now
+ARG NUMPY="1.26.4"
+RUN pip install --no-cache-dir -U mock six future tqdm deprecated numpy==$NUMPY packaging requests
+ 
+# TensorFlow build arguments
+ARG TF=v2.18.0
+ARG WITH_CUDA=false
+# Custom compute capabilities, else use default one from .bazelrc
+ARG CUDA_CC
+ARG WITH_XLA=true
+ARG WITH_MKL=false
+
+# Install bazelisk: will read .bazelversion and download the right bazel binary
+ADD https://github.com/bazelbuild/bazelisk/releases/latest/download/bazelisk-linux-amd64  /opt/otbtf/bin/bazelisk
+RUN chmod +x /opt/otbtf/bin/bazelisk && ln -s /opt/otbtf/bin/bazelisk /opt/otbtf/bin/bazel
+
+# Build and install tf wheel
+ADD https://github.com/tensorflow/tensorflow.git#$TF tensorflow
+ARG BZL_TARGETS="//tensorflow:libtensorflow_cc.so //tensorflow/tools/pip_package:wheel"
+# You can use --build-arg BZL_OPTIONS="--remote_cache=http://..." at build time
+ARG BZL_OPTIONS
+
+# Run build with local bazel cache using docker mount
+RUN --mount=type=cache,target=/root/.cache/bazel \
+ cd tensorflow \
+ && BZL_CONFIGS="--repo_env=WHEEL_NAME=tensorflow_cpu --config=release_cpu_linux" \
+ && if [ "$WITH_CUDA" = "true" ] ; then BZL_CONFIGS="--repo_env=WHEEL_NAME=tensorflow --config=release_gpu_linux --config=cuda_wheel" ; fi \
+ && if [ -n "$CUDA_CC" ] ; then BZL_CONFIGS="$BZL_CONFIGS --repo_env=HERMETIC_CUDA_COMPUTE_CAPABILITIES=$CUDA_CC"; fi \
+ && if [ "$WITH_MKL" = "true" ] ; then BZL_CONFIGS="$BZL_CONFIGS --config=mkl" ; fi \
+ && if [ "$WITH_XLA" = "true" ] ; then BZL_CONFIGS="$BZL_CONFIGS --config=xla" ; fi \
+ && export HERMETIC_PYTHON_VERSION=$PY \
+ && echo "Build env:" && env \
+ && BZL_CMD="build $BZL_TARGETS $BZL_CONFIGS --announce_rc --verbose_failures $BZL_OPTIONS" \
+ && echo "Starting build with cmd: \"bazel $BZL_CMD\"" \
+ && bazel $BZL_CMD --jobs="HOST_CPUS*$CPU_RATIO" \
+ && TF_WHEEL=$(find bazel-bin/tensorflow/tools/pip_package/wheel_house/ -type f -name "tensorflow*.whl") \
+ && pip install --no-cache-dir "$TF_WHEEL$(! $WITH_CUDA || echo '[and-cuda]')" \
+ && ln -s $PYTHON_SITE_PACKAGES/tensorflow/include /opt/otbtf/include/tf \
+ && for f in $(find -L /opt/otbtf/include/tf -wholename "*/external/*/*.so"); do ln -s $f /opt/otbtf/lib/; done \
+ && TF_MISSING_HEADERS="tensorflow/cc/saved_model/tag_constants.h tensorflow/cc/saved_model/signature_constants.h" \
+ && cp $TF_MISSING_HEADERS /opt/otbtf/include/tf/tensorflow/cc/saved_model/ \
+ && mkdir /tmp/artifacts && mv bazel-bin/tensorflow/libtensorflow_cc.so* $TF_WHEEL $TF_MISSING_HEADERS /tmp/artifacts \
+ && rm -rf bazel-* /src/tf
 
-RUN mkdir /src/otb
+# ----------------------------------------------------------------------------
+# Builder stage: cmake gcc otb
+FROM base-stage AS otb-build
 WORKDIR /src/otb
 
+COPY --from=tf-build /opt/otbtf /opt/otbtf
+
 # SuperBuild OTB
-COPY tools/docker/build-flags-otb.txt ./
-RUN apt-get update -y \
- && apt-get install --reinstall ca-certificates -y \
- && update-ca-certificates \
- && git clone https://gitlab.orfeo-toolbox.org/orfeotoolbox/otb.git \
- && cd otb && git checkout $OTB
-
-# <---------------------------------------- Begin dirty hack
+ARG OTB=release-9.1
+ADD --keep-git-dir=true https://gitlab.orfeo-toolbox.org/orfeotoolbox/otb.git#$OTB otb
+
+# <------------------------------------------
 # This is a dirty hack for release 4.0.0alpha
 # We have to wait that OTB moves from C++14 to C++17
 # See https://gitlab.orfeo-toolbox.org/orfeotoolbox/otb/-/issues/2338
-RUN cd /src/otb/otb \
+RUN cd otb \
  && sed -i 's/CMAKE_CXX_STANDARD 14/CMAKE_CXX_STANDARD 17/g' CMakeLists.txt \
  && echo "" > Modules/Core/ImageManipulation/test/CMakeLists.txt \
  && echo "" > Modules/Core/Conversion/test/CMakeLists.txt \
@@ -116,81 +111,85 @@ RUN cd /src/otb/otb \
  && echo "" > Modules/Core/Edge/test/CMakeLists.txt \
  && echo "" > Modules/Core/ImageBase/test/CMakeLists.txt \
  && echo "" > Modules/Learning/DempsterShafer/test/CMakeLists.txt \
-# <---------------------------------------- End dirty hack
  && cd .. \
- && mkdir -p build \
+ && mkdir -p build /tmp/SuperBuild-downloads \
  && cd build \
- && if $OTBTESTS; then \
-      echo "-DBUILD_TESTING=ON" >> ../build-flags-otb.txt; fi \
- # Possible ENH: superbuild-all-dependencies switch, with separated build-deps-minimal.txt and build-deps-otbcli.txt)
- #&& if $OTB_SUPERBUILD_ALL; then sed -i -r "s/-DUSE_SYSTEM_([A-Z0-9]*)=ON/-DUSE_SYSTEM_\1=OFF/ " ../build-flags-otb.txt; fi \
- && OTB_FLAGS=$(cat "../build-flags-otb.txt") \
- && cmake ../otb/SuperBuild -DCMAKE_INSTALL_PREFIX=/opt/otbtf $OTB_FLAGS \
- && make -j $(python -c "import os; print(round( os.cpu_count() * $CPU_RATIO ))")
-
-### OTBTF - copy (without .git/) or clone repository
-COPY . /src/otbtf
+ && cmake ../otb/SuperBuild \
+     -DCMAKE_INSTALL_PREFIX=/opt/otbtf \
+     -DOTB_BUILD_FeaturesExtraction=ON \
+     -DOTB_BUILD_Hyperspectral=ON \
+     -DOTB_BUILD_Learning=ON \
+     -DOTB_BUILD_Miscellaneous=ON \
+     -DOTB_BUILD_RemoteModules=ON \
+     -DOTB_BUILD_SAR=ON \
+     -DOTB_BUILD_Segmentation=ON \
+     -DOTB_BUILD_StereoProcessing=ON \
+     -DDOWNLOAD_LOCATION=/tmp/SuperBuild-downloads \
+ && make -j $(python -c "import os; print(round( os.cpu_count() * $CPU_RATIO ))") \
+ && rm -rf /tmp/SuperBuild-downloads
+
+# Copy cpp and cmake files from build context (TODO: use `COPY --parents` feature when released)
+WORKDIR /src/otbtf
+COPY app ./app
+COPY include ./include
+COPY CMakeLists.txt otb-module.cmake ./
+RUN mkdir test
+COPY test/CMakeLists.txt test/*.cxx test/
+
+# Build OTBTF cpp
+ARG DEV_IMAGE=false
 RUN ln -s /src/otbtf /src/otb/otb/Modules/Remote/otbtf
-
-# Rebuild OTB with module
-ARG KEEP_SRC_OTB=false
 RUN cd /src/otb/build/OTB/build \
- && export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/otbtf/lib \
- && export PATH=$PATH:/opt/otbtf/bin \
  && cmake /src/otb/otb \
       -DCMAKE_INSTALL_PREFIX=/opt/otbtf \
-      -DOTB_WRAP_PYTHON=ON -DPYTHON_EXECUTABLE=/usr/bin/python3 \
-      -DOTB_USE_TENSORFLOW=ON -DModule_OTBTensorflow=ON \
+      -DOTB_WRAP_PYTHON=ON \
+      -DPython_EXECUTABLE=$(which python) \
+      -DOTB_USE_TENSORFLOW=ON \
+      -DModule_OTBTensorflow=ON \
       -Dtensorflow_include_dir=/opt/otbtf/include/tf \
-      -DTENSORFLOW_CC_LIB=/opt/otbtf/local/lib/python3.10/dist-packages/tensorflow/libtensorflow_cc.so.2 \
-      -DTENSORFLOW_FRAMEWORK_LIB=/opt/otbtf/local/lib/python3.10/dist-packages/tensorflow/libtensorflow_framework.so.2 \
+      -DTENSORFLOW_CC_LIB=$PYTHON_SITE_PACKAGES/tensorflow/libtensorflow_cc.so.2 \
+      -DTENSORFLOW_FRAMEWORK_LIB=$PYTHON_SITE_PACKAGES/tensorflow/libtensorflow_framework.so.2 \
+      $( [ "$DEV_IMAGE" != "true" ] || echo "-DBUILD_TESTING=ON" ) \
  && make install -j $(python -c "import os; print(round( os.cpu_count() * $CPU_RATIO ))") \
- # Cleaning
- && ( $KEEP_SRC_OTB || rm -rf /src/otb ) \
+ && ( [ "$DEV_IMAGE" = "true" ] || rm -rf /src/otb ) \
  && rm -rf /root/.cache /tmp/*
 
-# Symlink executable python files in PATH
-RUN for f in /src/otbtf/python/*.py; do if [ -x $f ]; then ln -s $f /opt/otbtf/bin/; fi; done
-
 # ----------------------------------------------------------------------------
-# Final stage
-FROM otbtf-base
+# Final stage: copy binaries from middle layers and install python module
+FROM base-stage AS final-stage
 LABEL maintainer="Remi Cresson <remi.cresson[at]inrae[dot]fr>"
 
-# Copy files from intermediate stage
-COPY --from=builder /opt/otbtf /opt/otbtf
-COPY --from=builder /src /src
-
 # System-wide ENV
-ENV PATH="/opt/otbtf/bin:$PATH"
-ENV LD_LIBRARY_PATH="/opt/otbtf/lib:$LD_LIBRARY_PATH"
-ENV PYTHONPATH="/opt/otbtf/lib/python3/dist-packages:/opt/otbtf/lib/otb/python"
-ENV OTB_APPLICATION_PATH="/opt/otbtf/lib/otb/applications"
-RUN pip install -e /src/otbtf
-
-# Default user, directory and command (bash is the entrypoint when using
-# 'docker create')
-RUN useradd -s /bin/bash -m otbuser
-WORKDIR /home/otbuser
+ENV OTB_INSTALL_DIR=/opt/otbtf
+ENV OTB_APPLICATION_PATH=/opt/otbtf/lib/otb/applications
+# For otbApplication and osgeo modules
+ENV PYTHONPATH="/opt/otbtf/lib/otb/python:/opt/otbtf/lib/python$PY/site-packages"
 
-# Admin rights without password
-ARG SUDO=true
-RUN if $SUDO; then \
-      usermod -a -G sudo otbuser \
-      && echo "otbuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers; fi
+# Add a standard user - this won't prevent ownership issues with volumes if you're not UID 1000
+RUN useradd -s /bin/bash -m otbuser
 
-# Set /src/otbtf ownership to otbuser (but you still need 'sudo -i' in order
-# to rebuild TF or OTB)
-RUN chown -R otbuser:otbuser /src/otbtf
+# Admin rights without password (not recommended, use `docker run -u root` instead)
+ARG SUDO=false
+RUN ! $SUDO || usermod -a -G sudo otbuser && echo "otbuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
 
-# This won't prevent ownership problems with volumes if you're not UID 1000
+# Allow user to install packages in prefix /opt/otbtf and venv without being root
+COPY --from=otb-build --chown=otbuser:otbuser /opt/otbtf /opt/otbtf
+COPY --from=otb-build --chown=otbuser:otbuser /src /src
 USER otbuser
 
-# User-only ENV
-ENV PATH="/home/otbuser/.local/bin:$PATH"
+# Install OTBTF python module
+WORKDIR /src/otbtf
+COPY otbtf ./otbtf
+COPY README.md pyproject.toml .
+RUN pip install -e .
+
+# Install test packages for dev image
+RUN ! $DEV_IMAGE || pip install codespell flake8 pylint pytest pytest-cov pytest-order
+
+WORKDIR /home/otbuser
 
 # Test python imports
-RUN python -c "import tensorflow"
-RUN python -c "import otbtf, tricks"
+RUN python -c "import tensorflow, keras"
 RUN python -c "import otbApplication as otb; otb.Registry.CreateApplication('ImageClassifierFromDeepFeatures')"
+RUN python -c "import otbtf"
 RUN python -c "from osgeo import gdal"
diff --git a/README.md b/README.md
index 9704bb430444a7e944c914c6d601ba1fae71103f..dc7683d14455d472e31576826a8b4fe38a1c332a 100644
--- a/README.md
+++ b/README.md
@@ -34,8 +34,8 @@ The documentation is available on [otbtf.readthedocs.io](https://otbtf.readthedo
 You can use our latest GPU enabled docker images.
 
 ```bash
-docker run --runtime=nvidia -ti mdl4eo/otbtf:latest-gpu otbcli_PatchesExtraction
-docker run --runtime=nvidia -ti mdl4eo/otbtf:latest-gpu python -c "import otbtf"
+docker run --gpus=all -ti mdl4eo/otbtf:latest-gpu otbcli_PatchesExtraction
+docker run --gpus=all -ti mdl4eo/otbtf:latest-gpu python -c "import otbtf"
 ```
 
 You can also build OTBTF from sources (see the documentation)
diff --git a/RELEASE_NOTES.txt b/RELEASE_NOTES.txt
index af4e4429edcf715d6e99305ab5ed096cce5f21d1..75d6e4b1c0cc4a7e715337a4281e4741d8f81cb8 100644
--- a/RELEASE_NOTES.txt
+++ b/RELEASE_NOTES.txt
@@ -1,3 +1,18 @@
+Version 4.4.0 (?? ??? ????)
+----------------------------------------------------------------
+* Bump OTB version to 9.1.0
+* Bump TensorFlow version to 2.18.0
+* Move python pip install to virtualenv /opt/otbtf/venv
+* Lock numpy version < 2.0
+* Upgrade TF build workflow to latest specs:
+  - Drop Docker build argument BASE_IMG (ubuntu jammy is used for every build)
+  - Use official bazel configs (targets --config=release_{cpu,gpu}) for wheels
+  - Compile with LLVM Clang 18, use "HERMETIC_CUDA" 12.5
+  - Use default compute capabilities from .bazelrc (sm_60,sm_70,sm_80,sm_89,compute_90)
+  - Add Docker build arguments WITH_CUDA=false, WITH_MKL=false, WITH_XLA=true
+  - TODO: update python code for Keras 3
+* TODO: move packaging spec to pyproject.toml
+
 Version 4.3.1 (02 jan 2024)
 ----------------------------------------------------------------
 * Fix a bug with PROJ due to OTB 9 packaging
diff --git a/doc/api_distributed.md b/doc/api_distributed.md
index 09ea86c35d976ba5de5cfe70d612ddf06afa4020..786a6f5cc6d9cfded3c7ef812acbcb6b54de88ae 100644
--- a/doc/api_distributed.md
+++ b/doc/api_distributed.md
@@ -98,7 +98,7 @@ The rest of the code is identical.
 
 !!! Warning
 
-    Be careful when calling `mymodel.save()` to export the SavedModel. When 
+    Be careful when calling `mymodel.export()` to export the SavedModel. When 
     multiple nodes are used in parallel, this can lead to a corrupt save.
     One good practice is to defer the call only to the master worker (e.g. node
     0). You can identify the master worker using `otbtf.model._is_chief()`.
diff --git a/doc/api_tutorial.md b/doc/api_tutorial.md
index cc08b9198469a97a083c16588976a9b2c9aa8785..9c47be74774afffd4d4cf21b59b5409b7796d69b 100644
--- a/doc/api_tutorial.md
+++ b/doc/api_tutorial.md
@@ -315,12 +315,24 @@ dataset
 
 ```python
     model.compile(
-        loss=tf.keras.losses.CategoricalCrossentropy(),
+        loss={TARGET_NAME: tf.keras.losses.CategoricalCrossentropy()},
         optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4),
-        metrics=[tf.keras.metrics.Precision(), tf.keras.metrics.Recall()]
+        metrics={
+            TARGET_NAME: [
+                tf.keras.metrics.Precision(), 
+                tf.keras.metrics.Recall()
+            ]
+        }
     )
 ```
 
+!!! Note
+
+    The losses and metrics must always be provided using a dict, to specify 
+    which output to use. This is mandatory since Keras 3, since OTBTF generates 
+    a bunch of extra outputs that are not used during optimization, but needed 
+    in the inference step.
+
 We can then train our model using Keras:
 
 ```python
diff --git a/doc/app_sampling.md b/doc/app_sampling.md
index b79ba7f4653e5c1951c85757571430b17fa63580..9a9fc047e2c85d4b3f410d28271b9e15c4ffa8d3 100644
--- a/doc/app_sampling.md
+++ b/doc/app_sampling.md
@@ -84,7 +84,7 @@ specific field of the input vector data.
 Typically, the *class* field can be used to generate a dataset suitable for a
 model that performs pixel wise classification.
 
-![Schema](https://gitlab.irstea.fr/remi.cresson/otbtf/-/raw/develop/doc/images/patches_extraction.png)
+![Schema](https://forgemia.inra.fr/orfeo-toolbox/otbtf/-/raw/develop/doc/images/patches_extraction.png)
 
 The application description can be displayed using:
 
diff --git a/doc/app_training.md b/doc/app_training.md
index eb3f4708f37121d5f63181eb2c2b08a783da9df9..c1e731ef65b69f936291a74a5377a25c3e8d238f 100644
--- a/doc/app_training.md
+++ b/doc/app_training.md
@@ -47,7 +47,7 @@ patches images, a convenient method consist in reading patches images as numpy
 arrays using OTB applications (e.g. `ExtractROI`) or GDAL, then do a
 `numpy.reshape` to the dimensions wanted.
 
-![Schema](https://gitlab.irstea.fr/remi.cresson/otbtf/-/raw/develop/doc/images/model_training.png)
+![Schema](https://forgemia.inra.fr/orfeo-toolbox/otbtf/-/raw/develop/doc/images/model_training.png)
 
 The application description can be displayed using:
 
diff --git a/doc/deprecated.md b/doc/deprecated.md
index 3f76c1dbd1d62e3313da1e85e4d3b8e8ecef8bdf..08f9e73ebef7fa2815cac8843ae0a63f51184206 100644
--- a/doc/deprecated.md
+++ b/doc/deprecated.md
@@ -35,4 +35,13 @@ training, etc. is done using the so-called `tensorflow.Strategy`
 
 !!! Note
 
-    Read our [tutorial](api_tutorial.html) to know more on working with Keras!
\ No newline at end of file
+    Read our [tutorial](api_tutorial.html) to know more on working with Keras!
+
+## Major changes between Keras 2 and 3
+
+- Use keras functions on keras objects, instead of tf ones
+- Most operations in `tf` namespace have moved to `keras.ops`
+- Function `model.save()` should be replaced by `model.export()`
+- Target layers for metrics must be explicitly named
+
+Read further instructions in the official [keras docs](https://keras.io/guides/migrating_to_keras_3/).
diff --git a/doc/docker_build.md b/doc/docker_build.md
index debc1ea8d5ae2994949b46a61e70a5ed14290ce3..80393b1eee5e3f8b37b764c2f30701c46b857154 100644
--- a/doc/docker_build.md
+++ b/doc/docker_build.md
@@ -1,162 +1,129 @@
 # Build your own docker images
 
-Docker build has to be called from the root of the repository (i.e. `docker 
-build .` or `bash tools/docker/multibuild.sh`).
-You can build a custom image using `--build-arg` and several config files :
+Docker build has to be called from the root of the repository
+ (i.e. `docker build .`.
+You can select target versions using `--build-arg`:
 
-- **Ubuntu** : `BASE_IMG` should accept any version, for additional packages 
-see *tools/docker/build-deps-cli.txt* and *tools/docker/build-deps-gui.txt*.
-- **TensorFlow** : `TF` arg for the git branch or tag + *build-env-tf.sh* and 
-BZL_* arguments for the build configuration. `ZIP_TF_BIN` allows you to save 
-compiled binaries if you want to install it elsewhere.
-- **OrfeoToolBox** : `OTB` arg for the git branch or tag + 
-*tools/docker/build-flags-otb.txt* to edit cmake flags. Set `KEEP_SRC_OTB` in 
-order to preserve OTB git directory.
+- **TensorFlow** : `TF` arg for the git branch or tag
+- **OrfeoToolBox** : `OTB` arg for the git branch or tag,
+ set `KEEP_SRC_OTB` in order to preserve OTB sources
 
-### Base images
+## Default build arguments
 
 ```bash
-CPU_IMG=ubuntu:22.04
-GPU_IMG=nvidia/cuda:12.1.0-devel-ubuntu22.04
-```
-
-### Default arguments
-
-```bash
-BASE_IMG                # mandatory
+# Limit CPU usage e.g. 0.75
 CPU_RATIO=1
-GUI=false
-NUMPY_SPEC="==1.19.*"
-TF=v2.12.0
-OTB=8.1.0
-BZL_TARGETS="//tensorflow:libtensorflow_cc.so //tensorflow/tools/pip_package:build_pip_package"
-BZL_CONFIGS="--config=nogcp --config=noaws --config=nohdfs --config=opt"
-BZL_OPTIONS="--verbose_failures --remote_cache=http://localhost:9090"
-ZIP_TF_BIN=false
-KEEP_SRC_OTB=false
-SUDO=true
-
-# NumPy version requirement :
-# TF <  2.4 : "numpy<1.19.0,>=1.16.0"
-# TF >= 2.4 : "numpy==1.19.*"
-# TF >= 2.8 : "numpy==1.22.*"
+# Can be used to install a specific numpy version
+NUMPY="1.26.4"
+# Git branch or tag to checkout
+TF=v2.18.0
+# Build with XLA
+WITH_XLA=true
+# Build with Intel MKL support
+WITH_MKL=false
+# Set to true to enable Nvidia GPU support
+WITH_CUDA=false
+# Custom compute capabilities, default are defined in repo tensorflow/.bazelrc
+# Currently "sm_60,sm_70,sm_80,sm_89,compute_90"
+CUDA_COMPUTE_CAPABILITIES=
+# Targets for bazel build cmd
+BZL_TARGETS="//tensorflow:libtensorflow_cc.so //tensorflow/tools/pip_package:wheel"
+# Available for additional bazel options, e.g. --remote_cache
+BZL_OPTIONS=
+# Git branch or tag to checkout
+OTB=release-9.1
+# Keep OTB sources and build test
+DEV_IMAGE=false
+# Enable sudo without password for "otbuser"
+SUDO=false
 ```
 
-### Bazel remote cache daemon
+## Bazel remote cache daemon
 
-If you just need to rebuild with different GUI or KEEP_SRC arguments, or may 
-be a different branch of OTB, bazel cache will help you to rebuild everything 
-except TF, even if the docker cache was purged (after `docker 
-[system|builder] prune`).
-In order to recycle the cache, bazel config and TF git tag should be exactly 
-the same, any change in *tools/docker/build-env-tf.sh* and `--build-arg` 
-(if related to bazel env, cuda, mkl, xla...) may result in a fresh new build.
+If you just need to rebuild with different arguments, or may be a different
+ branch of OTB, bazel cache will help you to rebuild everything except TF,
+ even if the docker cache was purged (after `docker [system|builder] prune`).
+In order to recycle the cache, bazel config and TF git tag should be exactly
+ the same, any change in Dockerfile or  `--build-arg` would create a new build.  
 
-Start a cache daemon - here with max 20GB but 10GB should be enough to save 2 
-TF builds (GPU and CPU):
+Start a cache daemon - 10GB should be enough to save 2 TF builds (GPU and CPU):
 
 ```bash
 mkdir -p $HOME/.cache/bazel-remote
-docker run --detach -u 1000:1000 -v $HOME/.cache/bazel-remote:/data \
-  -p 9090:8080 buchgr/bazel-remote-cache --max_size=20
+docker run --detach -u $UID:$GID -v $HOME/.cache/bazel-remote:/data \
+  -p 9090:8080 buchgr/bazel-remote-cache --max_size=10
 ```
 
-Then just add ` --network='host'` to the docker build command, or connect 
-bazel to a remote server - see 'BZL_OPTIONS'.  
-The other way of docker is a virtual bridge, but you'll need to edit the IP 
-address.  
+Then just add ` --network='host'` to the docker build command, or connect
+ bazel to a remote server - see 'BZL_OPTIONS'.  
+The other way of docker is a virtual bridge, but you'll need to edit the IP
+ address. Changing the BZL_OPTIONS will invalidate docker build cache.
 
 ## Images build examples
 
 ```bash
 # Build for CPU using default Dockerfiles args (without AWS, HDFS or GCP 
 # support)
-docker build --network='host' -t otbtf:cpu --build-arg BASE_IMG=ubuntu:22.04 .
-
-# Clear bazel config var (deactivate default optimizations and unset 
-# noaws/nogcp/nohdfs)
 docker build --network='host' -t otbtf:cpu \
-  --build-arg BASE_IMG=ubuntu:22.04 \
-  --build-arg BZL_CONFIGS= .
+  --build-arg BZL_OPTIONS="--remote_cache=http://localhost:9090" .
 
-# Enable MKL
-MKL_CONFIG="--config=nogcp --config=noaws --config=nohdfs --config=opt --config=mkl"
-docker build --network='host' -t otbtf:cpu-mkl \
-  --build-arg BZL_CONFIGS="$MKL_CONFIG" \
-  --build-arg BASE_IMG=ubuntu:22.04 .
-
-# Build for GPU (if you're building for your system only you should edit 
-# CUDA_COMPUTE_CAPABILITIES in build-env-tf.sh)
+# Build for GPU
 docker build --network='host' -t otbtf:gpu \
-  --build-arg BASE_IMG=nvidia/cuda:12.1.0-devel-ubuntu22.04 .
+  --build-arg BZL_OPTIONS="--remote_cache=http://localhost:9090" \
+  --build-arg WITH_CUDA=true \
+  .
 
 # Build latest TF and OTB, set git branches/tags to clone
-docker build --network='host' -t otbtf:gpu-dev \
-  --build-arg BASE_IMG=nvidia/cuda:12.1.0-devel-ubuntu22.04 \
-  --build-arg KEEP_SRC_OTB=true \
+docker build --network='host' -t otbtf:gpu \
+   --build-arg BZL_OPTIONS="--remote_cache=http://localhost:9090" \
+  --build-arg WITH_CUDA=true \
+  --build-arg DEV_IMAGE=true \
   --build-arg TF=nightly \
-  --build-arg OTB=develop .
-
-# Build old release (TF-2.1)
-docker build --network='host' -t otbtf:oldstable-gpu \
-  --build-arg BASE_IMG=nvidia/cuda:10.1-cudnn7-devel-ubuntu18.04 \
-  --build-arg TF=r2.1 \
-  --build-arg NUMPY_SPEC="<1.19" \
-  --build-arg BAZEL_OPTIONS="--noincompatible_do_not_split_linking_cmdline --verbose_failures --remote_cache=http://localhost:9090" .
-# You could edit the Dockerfile in order to clone an old branch of the repo
-# instead of copying files from the build context
+  --build-arg OTB=develop \
+  .
 ```
 
-### Build for another machine and save TF compiled files 
-
-Example with TF 2.5
+### Build only tensorflow C++ lib and python wheel to install outside of Docker
 
 ```bash
-# Use same ubuntu and CUDA version than your target machine, beware of CC 
-# optimization and CPU compatibility (set env variable CC_OPT_FLAGS and avoid 
-# "-march=native" if your Docker's CPU is optimized with AVX2/AVX512 but your 
-# target CPU isn't)
-docker build --network='host' -t otbtf:custom \
-  --build-arg BASE_IMG=nvidia/cuda:11.2.2-cudnn8-devel-ubuntu20.04 \
-  --build-arg TF=v2.5.0 \
-  --build-arg ZIP_TF_BIN=true .
-# Retrieve zip file
-docker run -v $HOME:/home/otbuser/volume otbtf:custom \
-  cp /opt/otbtf/tf-v2.5.0.zip /home/otbuser/volume
+docker build --network='host' --target=tf-build -t tf:gpu \
+  --build-arg BZL_OPTIONS="--remote_cache=http://localhost:9090" \
+  --build-arg WITH_CUDA=true \
+  .
 
 # Target machine shell
-cd $HOME
-unzip tf-v2.5.0.zip
-sudo mkdir -p /opt/tensorflow/lib
-sudo mv tf-v2.5.0/libtensorflow_cc* /opt/tensorflow/lib
+docker run -v gpu-build-artifacts:/artifacts tf:gpu mv /tmp/artifacts /artifacts
+cd gpu-build-artifacts
+sudo mv libtensorflow_cc* /usr/local/lib  # Or another path you may add to LD_LIBRARY_PATH
 # You may need to create a virtualenv, here TF and dependencies are installed 
 # next to user's pip packages
-pip3 install -U pip wheel mock six future deprecated "numpy==1.19.*"
+pip3 install -U pip wheel mock six future deprecated "numpy<2"
 pip3 install --no-deps keras_applications keras_preprocessing
-pip3 install tf-v2.5.0/tensorflow-2.5.0-cp38-cp38-linux_x86_64.whl
+pip3 install tensorflow-v2.18.0-cp310-cp310-linux_x86_64.whl
 
-TF_WHEEL_DIR="$HOME/.local/lib/python3.8/site-packages/tensorflow"
+TF_WHEEL_DIR="$HOME/.local/lib/python3.10/site-packages/tensorflow"
 # If you installed the wheel as regular user, with root pip it should be in 
 # /usr/local/lib/python3.*, or in your virtualenv lib/ directory
-mv tf-v2.5.0/tag_constants.h $TF_WHEEL_DIR/include/tensorflow/cc/saved_model/
-# Then recompile OTB with OTBTF using libraries in /opt/tensorflow/lib and 
+mv tag_constants.h signature_constants.h $TF_WHEEL_DIR/include/tensorflow/cc/saved_model/
+# From an OTB git source tree
+# Recompile OTB with OTBTF using libraries in /opt/tensorflow/lib and 
 # instructions in build_from_sources.md.
 cmake $OTB_GIT \
-    -DOTB_USE_TENSORFLOW=ON -DModule_OTBTensorflow=ON \
-    -DTENSORFLOW_CC_LIB=/opt/tensorflow/lib/libtensorflow_cc.so.2 \
-    -Dtensorflow_include_dir=$TF_WHEEL_DIR/include \
-    -DTENSORFLOW_FRAMEWORK_LIB=$TF_WHEEL_DIR/libtensorflow_framework.so.2 \
+  -DOTB_USE_TENSORFLOW=ON \
+  -DModule_OTBTensorflow=ON \
+  -DTENSORFLOW_CC_LIB="/usr/local/lib/libtensorflow_cc.so.2" \
+  -Dtensorflow_include_dir="$TF_WHEEL_DIR/include" \
+  -DTENSORFLOW_FRAMEWORK_LIB="$TF_WHEEL_DIR/libtensorflow_framework.so.2" \
 && make install -j 
 ```
 
 ### Debug build
-
-If you fail to build, you can log into the last layer and check CMake logs. 
-Run `docker images`, find the latest layer ID and run a tmp container 
+If you fail to build, you can log into the last layer and check CMake logs.
+ Run `docker images`, find the latest layer ID and run a tmp container
 (`docker run -it d60496d9612e bash`).
+**This is only possible when building with legacy docker config DOCKER_BUILDKIT=0**.
 You may also need to split some multi-command layers in the Dockerfile.
-If you see OOM errors during SuperBuild you should decrease CPU_RATIO (e.g. 
-0.75).
+ If you see OOM errors during SuperBuild you should decrease CPU_RATIO.
 
 ## Container examples
 
@@ -164,7 +131,7 @@ If you see OOM errors during SuperBuild you should decrease CPU_RATIO (e.g.
 # Pull GPU image and create a new container with your home directory as volume 
 # (requires apt package nvidia-docker2 and CUDA>=11.0)
 docker create --gpus=all --volume $HOME:/home/otbuser/volume -it \
-  --name otbtf-gpu mdl4eo/otbtf:3.3.2-gpu
+  --name otbtf-gpu mdl4eo/otbtf:5.0.0-gpu
 
 # Run interactive
 docker start -i otbtf-gpu
@@ -180,32 +147,14 @@ docker exec otbtf-gpu \
 Enter a development ready docker image:
 
 ```bash
-docker create --gpus=all -it --name otbtf-gpu-dev mdl4eo/otbtf:3.3.2-gpu-dev
-docker start -i otbtf-gpu-dev
-```
-
-Then, from the container shell:
-
-```bash
-sudo -i
+docker run -it --gpus=all -it --name otbtf-gpu-dev mdl4eo/otbtf:5.0.0-gpu-dev
+# Then, from the container shell:
 cd /src/otb/otb/Modules/Remote
 git clone https://gitlab.irstea.fr/raffaele.gaetano/otbSelectiveHaralickTextures.git
 cd /src/otb/build/OTB/build
 cmake -DModule_OTBAppSelectiveHaralickTextures=ON /src/otb/otb && make install -j
+exit
+docker container ls
 ```
 
-### Container with GUI
-
-GUI is disabled by default in order to save space, and because docker xvfb 
-isn't working properly with OpenGL.
-OTB GUI seems OK but monteverdi isn't working
-
-```bash
-docker build --network='host' -t otbtf:cpu-gui \
-  --build-arg BASE_IMG=ubuntu:22.04 \
-  --build-arg GUI=true .
-docker create -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=$DISPLAY -it \
-  --name otbtf-gui otbtf:cpu-gui
-docker start -i otbtf-gui
-$ mapla
-```
+Then you can user `docker commit` to save this container as a new image.
diff --git a/doc/docker_troubleshooting.md b/doc/docker_troubleshooting.md
index 4aa0d506d32aa42c264d580e5362fe37aa72e229..c5096717f8fcc43fa5cecef74f8cf680a69efae7 100644
--- a/doc/docker_troubleshooting.md
+++ b/doc/docker_troubleshooting.md
@@ -1,34 +1,24 @@
 # Docker troubleshooting
 
-You can find plenty of help on the web about docker. 
-This section only provides the basics for newcomers that are eager to use 
-OTBTF!
+You can find plenty of help on the web about docker.
+This section only provides the basics for newcomers
+ that are eager to use OTBTF!
 This section is largely inspired from the 
-[moringa docker help](https://gitlab.irstea.fr/raffaele.gaetano/moringa/blob/develop/docker/README.md). 
-Big thanks to the authors.
+[moringa docker help](https://gitlab.irstea.fr/raffaele.gaetano/moringa/blob/develop/docker/README.md).
+ Big thanks to the authors.
 
 ## Common errors
 
-### Manifest unknown
-
-```
-Error response from daemon: 
-manifest for nvidia/cuda:11.0-cudnn8-devel-ubuntu20.04 not found: 
-manifest unknown: manifest unknown
-```
-
-This means that the docker image is missing from dockerhub.
-
 ### failed call to cuInit
 
-```
+```raw
 failed call to cuInit: 
 UNKNOWN ERROR (303) / no NVIDIA GPU device is present: 
 /dev/nvidia0 does not exist
 ```
 
-Nvidia driver is missing or disabled, make sure to add 
-` --gpus=all` to your docker run or create command
+Nvidia driver is missing or disabled, make sure to add
+ ` --gpus=all` to your docker run or create command
 
 ## Useful diagnostic commands
 
@@ -87,7 +77,7 @@ docker create --interactive --tty --volume /home/$USER:/home/otbuser/ \
 !!! warning
 
     Beware of ownership issues, see 
-    [this section](#fix-volume-ownership-sissues).
+    [this section](#fix-volume-ownership-issues).
 
 ### Interactive session
 
diff --git a/doc/docker_use.md b/doc/docker_use.md
index 81211066432c95f60e546ae815ec31acaca8f593..0f9d95a935d71c17d7cce3cb2d9f3cf72e5db4ea 100644
--- a/doc/docker_use.md
+++ b/doc/docker_use.md
@@ -21,16 +21,18 @@ Read more in the following sections.
 Here is the list of the latest OTBTF docker images hosted on 
 [dockerhub](https://hub.docker.com/u/mdl4eo).
 Since OTBTF >= 3.2.1 you can find the latest docker images on 
-[gitlab.irstea.fr](https://gitlab.irstea.fr/remi.cresson/otbtf/container_registry).
+[gitlab.irstea.fr](https://gitlab.irstea.fr/remi.cresson/otbtf/container_registry) for 
+versions <= 4.3.0 and [forgemia.inra.fr](https://forgemia.inra.fr/orfeo-toolbox/otbtf/container_registry/) since version 4.3.1.
 
 | Name                                                                               | Os            | TF    | OTB   | Description            | Dev files | Compute capability |
 |------------------------------------------------------------------------------------| ------------- |-------|-------| ---------------------- | --------- | ------------------ |
-| **mdl4eo/otbtf:4.3.1-cpu**                                                         | Ubuntu Jammy  | r2.14 | 9.0.0 | CPU, no optimization   | no        | 5.2,6.1,7.0,7.5,8.6|
-| **mdl4eo/otbtf:4.3.1-cpu-dev**                                                     | Ubuntu Jammy  | r2.14 | 9.0.0 | CPU, no optimization (dev) |  yes  | 5.2,6.1,7.0,7.5,8.6|
-| **mdl4eo/otbtf:4.3.1-gpu**                                                         | Ubuntu Jammy  | r2.14 | 9.0.0 | GPU, no optimization   | no        | 5.2,6.1,7.0,7.5,8.6|
-| **mdl4eo/otbtf:4.3.1-gpu-dev**                                                     | Ubuntu Jammy  | r2.14 | 9.0.0 | GPU, no optimization (dev) | yes   | 5.2,6.1,7.0,7.5,8.6|
-| **gitlab.irstea.fr/remi.cresson/otbtf/container_registry/otbtf:4.3.1-gpu-opt**     | Ubuntu Jammy  | r2.14 | 9.0.0 | GPU with opt.          | no        | 5.2,6.1,7.0,7.5,8.6|
-| **gitlab.irstea.fr/remi.cresson/otbtf/container_registry/otbtf:4.3.1-gpu-opt-dev** | Ubuntu Jammy  | r2.14 | 9.0.0 | GPU with opt. (dev)    | yes       | 5.2,6.1,7.0,7.5,8.6|
+| **mdl4eo/otbtf:5.0.0-cpu**                                                         | Ubuntu Jammy  | r2.18 | 9.1.0 | CPU | no        | |
+| **mdl4eo/otbtf:5.0.0-cpu-dev**                                                     | Ubuntu Jammy  | r2.18 | 9.1.0 | CPU (dev) |  yes  | |
+| **mdl4eo/otbtf:5.0.0-gpu**                                                         | Ubuntu Jammy  | r2.18 | 9.1.0 | GPU | no        | sm_60,sm_70,sm_80,sm_89,compute_90 |
+| **mdl4eo/otbtf:5.0.0-gpu-dev**                                                     | Ubuntu Jammy  | r2.18 | 9.1.0 | GPU (dev) | yes   |  sm_60,sm_70,sm_80,sm_89,compute_90|
+[gitlab.irstea.fr](https://gitlab.irstea.fr/remi.cresson/otbtf/container_registry) 
+(before otbtf 4.3.0) and [forgemia](https://forgemia.inra.fr/orfeo-toolbox/otbtf/container_registry)
+(since otbtf 4.3.1).
 
 The list of older releases is available [here](#older-images).
 
@@ -43,10 +45,9 @@ The list of older releases is available [here](#older-images).
 
 ## GPU enabled docker 
 
-In Linux, this is quite straightforward. 
-Just follow the steps described in the 
-[nvidia-docker documentation](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html).
-You can then use the OTBTF `gpu` tagged docker images with the **NVIDIA runtime** : 
+In Linux, this is quite straightforward. Just follow the steps described in the
+ [nvidia-docker documentation](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html).
+You can then use the OTBTF `gpu` tagged docker images with the **NVIDIA runtime** :
 
 With Docker version earlier than 19.03 :
 
@@ -57,7 +58,7 @@ docker run --runtime=nvidia -ti mdl4eo/otbtf:latest-gpu bash
 With Docker version including and after 19.03 :
 
 ```bash
-docker run --gpus all -ti mdl4eo/otbtf:latest-gpu bash
+docker run --gpus=all -ti mdl4eo/otbtf:latest-gpu bash
 ```
 
 You can find some details on the **GPU docker image** and some **docker tips 
@@ -99,7 +100,6 @@ Troubleshooting:
     - [WSL user guide](https://docs.nvidia.com/cuda/wsl-user-guide/index.html)
     - [XSL GPU support](https://docs.docker.com/docker-for-windows/wsl/#gpu-support)
 
-
 ## Build your own images
 
 If you want to use optimization flags, change GPUs compute capability, etc. 
@@ -140,44 +140,30 @@ Here you can find the list of older releases of OTBTF:
 | **mdl4eo/otbtf:3.3.0-cpu-dev**                                                     | Ubuntu Focal  | r2.8   | 8.1.0 | CPU, no optimization (dev) |  yes  | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:3.3.0-gpu**                                                         | Ubuntu Focal  | r2.8   | 8.1.0 | GPU, no optimization   | no        | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:3.3.0-gpu-dev**                                                     | Ubuntu Focal  | r2.8   | 8.1.0 | GPU, no optimization (dev) | yes   | 5.2,6.1,7.0,7.5,8.6|
-| **gitlab.irstea.fr/remi.cresson/otbtf/container_registry/otbtf:3.3.0-gpu-opt**     | Ubuntu Focal  | r2.8   | 8.1.0 | GPU with opt.          | no        | 5.2,6.1,7.0,7.5,8.6|
-| **gitlab.irstea.fr/remi.cresson/otbtf/container_registry/otbtf:3.3.0-gpu-opt-dev** | Ubuntu Focal  | r2.8   | 8.1.0 | GPU with opt. (dev)    | yes       | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:3.3.2-cpu**                                                         | Ubuntu Focal  | r2.8   | 8.1.0 | CPU, no optimization   | no        | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:3.3.2-cpu-dev**                                                     | Ubuntu Focal  | r2.8   | 8.1.0 | CPU, no optimization (dev) |  yes  | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:3.3.2-gpu**                                                         | Ubuntu Focal  | r2.8   | 8.1.0 | GPU, no optimization   | no        | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:3.3.2-gpu-dev**                                                     | Ubuntu Focal  | r2.8   | 8.1.0 | GPU, no optimization (dev) | yes   | 5.2,6.1,7.0,7.5,8.6|
-| **gitlab.irstea.fr/remi.cresson/otbtf/container_registry/otbtf:3.3.2-gpu-opt**     | Ubuntu Focal  | r2.8   | 8.1.0 | GPU with opt.          | no        | 5.2,6.1,7.0,7.5,8.6|
-| **gitlab.irstea.fr/remi.cresson/otbtf/container_registry/otbtf:3.3.2-gpu-opt-dev** | Ubuntu Focal  | r2.8   | 8.1.0 | GPU with opt. (dev)    | yes       | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:3.3.3-cpu**                                                         | Ubuntu Focal  | r2.8   | 8.1.0 | CPU, no optimization   | no        | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:3.3.3-cpu-dev**                                                     | Ubuntu Focal  | r2.8   | 8.1.0 | CPU, no optimization (dev) |  yes  | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:3.3.3-gpu**                                                         | Ubuntu Focal  | r2.8   | 8.1.0 | GPU, no optimization   | no        | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:3.3.3-gpu-dev**                                                     | Ubuntu Focal  | r2.8   | 8.1.0 | GPU, no optimization (dev) | yes   | 5.2,6.1,7.0,7.5,8.6|
-| **gitlab.irstea.fr/remi.cresson/otbtf/container_registry/otbtf:3.3.3-gpu-opt**     | Ubuntu Focal  | r2.8   | 8.1.0 | GPU with opt.          | no        | 5.2,6.1,7.0,7.5,8.6|
-| **gitlab.irstea.fr/remi.cresson/otbtf/container_registry/otbtf:3.3.3-gpu-opt-dev** | Ubuntu Focal  | r2.8   | 8.1.0 | GPU with opt. (dev)    | yes       | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:3.4.0-cpu**                                                         | Ubuntu Focal  | r2.8   | 8.1.0 | CPU, no optimization   | no        | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:3.4.0-cpu-dev**                                                     | Ubuntu Focal  | r2.8   | 8.1.0 | CPU, no optimization (dev) |  yes  | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:3.4.0-gpu**                                                         | Ubuntu Focal  | r2.8   | 8.1.0 | GPU, no optimization   | no        | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:3.4.0-gpu-dev**                                                     | Ubuntu Focal  | r2.8   | 8.1.0 | GPU, no optimization (dev) | yes   | 5.2,6.1,7.0,7.5,8.6|
-| **gitlab.irstea.fr/remi.cresson/otbtf/container_registry/otbtf:3.4.0-gpu-opt**     | Ubuntu Focal  | r2.8   | 8.1.0 | GPU with opt.          | no        | 5.2,6.1,7.0,7.5,8.6|
-| **gitlab.irstea.fr/remi.cresson/otbtf/container_registry/otbtf:3.4.0-gpu-opt-dev** | Ubuntu Focal  | r2.8   | 8.1.0 | GPU with opt. (dev)    | yes       | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:4.0.0-cpu**                                                         | Ubuntu Jammy  | r2.12  | 8.1.0 | CPU, no optimization   | no        | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:4.0.0-cpu-dev**                                                     | Ubuntu Jammy  | r2.12  | 8.1.0 | CPU, no optimization (dev) |  yes  | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:4.0.0-gpu**                                                         | Ubuntu Jammy  | r2.12  | 8.1.0 | GPU, no optimization   | no        | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:4.0.0-gpu-dev**                                                     | Ubuntu Jammy  | r2.12  | 8.1.0 | GPU, no optimization (dev) | yes   | 5.2,6.1,7.0,7.5,8.6|
-| **gitlab.irstea.fr/remi.cresson/otbtf/container_registry/otbtf:4.0.0-gpu-opt**     | Ubuntu Jammy  | r2.12  | 8.1.0 | GPU with opt.          | no        | 5.2,6.1,7.0,7.5,8.6|
-| **gitlab.irstea.fr/remi.cresson/otbtf/container_registry/otbtf:4.0.0-gpu-opt-dev** | Ubuntu Jammy  | r2.12  | 8.1.0 | GPU with opt. (dev)    | yes       | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:4.1.0-cpu**                                                         | Ubuntu Jammy  | r2.12 | 8.1.0 | CPU, no optimization   | no        | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:4.1.0-cpu-dev**                                                     | Ubuntu Jammy  | r2.12 | 8.1.0 | CPU, no optimization (dev) |  yes  | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:4.1.0-gpu**                                                         | Ubuntu Jammy  | r2.12 | 8.1.0 | GPU, no optimization   | no        | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:4.1.0-gpu-dev**                                                     | Ubuntu Jammy  | r2.12 | 8.1.0 | GPU, no optimization (dev) | yes   | 5.2,6.1,7.0,7.5,8.6|
-| **gitlab.irstea.fr/remi.cresson/otbtf/container_registry/otbtf:4.1.0-gpu-opt**     | Ubuntu Jammy  | r2.12 | 8.1.0 | GPU with opt.          | no        | 5.2,6.1,7.0,7.5,8.6|
-| **gitlab.irstea.fr/remi.cresson/otbtf/container_registry/otbtf:4.1.0-gpu-opt-dev** | Ubuntu Jammy  | r2.12 | 8.1.0 | GPU with opt. (dev)    | yes       | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:4.2.0-cpu**                                                         | Ubuntu Jammy  | r2.12 | 8.1.0 | CPU, no optimization   | no        | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:4.2.0-cpu-dev**                                                     | Ubuntu Jammy  | r2.12 | 8.1.0 | CPU, no optimization (dev) |  yes  | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:4.2.0-gpu**                                                         | Ubuntu Jammy  | r2.12 | 8.1.0 | GPU, no optimization   | no        | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:4.2.0-gpu-dev**                                                     | Ubuntu Jammy  | r2.12 | 8.1.0 | GPU, no optimization (dev) | yes   | 5.2,6.1,7.0,7.5,8.6|
-| **gitlab.irstea.fr/remi.cresson/otbtf/container_registry/otbtf:4.2.0-gpu-opt**     | Ubuntu Jammy  | r2.12 | 8.1.0 | GPU with opt.          | no        | 5.2,6.1,7.0,7.5,8.6|
-| **gitlab.irstea.fr/remi.cresson/otbtf/container_registry/otbtf:4.2.0-gpu-opt-dev** | Ubuntu Jammy  | r2.12 | 8.1.0 | GPU with opt. (dev)    | yes       | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:4.2.1-cpu**                                                         | Ubuntu Jammy  | r2.12 | 8.1.2 | CPU, no optimization   | no        | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:4.2.1-cpu-dev**                                                     | Ubuntu Jammy  | r2.12 | 8.1.2 | CPU, no optimization (dev) |  yes  | 5.2,6.1,7.0,7.5,8.6|
 | **mdl4eo/otbtf:4.2.1-gpu**                                                         | Ubuntu Jammy  | r2.12 | 8.1.2 | GPU, no optimization   | no        | 5.2,6.1,7.0,7.5,8.6|
@@ -196,3 +182,7 @@ Here you can find the list of older releases of OTBTF:
 | **mdl4eo/otbtf:4.3.0-gpu-dev**                                                     | Ubuntu Jammy  | r2.14 | 9.0.0 | GPU, no optimization (dev) | yes   | 5.2,6.1,7.0,7.5,8.6|
 | **gitlab.irstea.fr/remi.cresson/otbtf/container_registry/otbtf:4.3.0-gpu-opt**     | Ubuntu Jammy  | r2.14 | 9.0.0 | GPU with opt.          | no        | 5.2,6.1,7.0,7.5,8.6|
 | **gitlab.irstea.fr/remi.cresson/otbtf/container_registry/otbtf:4.3.0-gpu-opt-dev** | Ubuntu Jammy  | r2.14 | 9.0.0 | GPU with opt. (dev)    | yes       | 5.2,6.1,7.0,7.5,8.6|
+| **mdl4eo/otbtf:4.3.1-cpu**                                          | Ubuntu Jammy  | r2.14 | 9.0.0 | CPU, no optimization   | no        | 5.2,6.1,7.0,7.5,8.6|
+| **mdl4eo/otbtf:4.3.1-cpu-dev**                                      | Ubuntu Jammy  | r2.14 | 9.0.0 | CPU, no optimization (dev) |  yes  | 5.2,6.1,7.0,7.5,8.6|
+| **mdl4eo/otbtf:4.3.1-gpu**                                          | Ubuntu Jammy  | r2.14 | 9.0.0 | GPU, no optimization   | no        | 5.2,6.1,7.0,7.5,8.6|
+| **mdl4eo/otbtf:4.3.1-gpu-dev**                                      | Ubuntu Jammy  | r2.14 | 9.0.0 | GPU, no optimization (dev) | yes   | 5.2,6.1,7.0,7.5,8.6|
diff --git a/otbtf/__init__.py b/otbtf/__init__.py
index 1ce624226bcf921418b5de8731eb1566d1486055..633ccf1bbbc6c05f3e84536b25c7c8bbc3e06e25 100644
--- a/otbtf/__init__.py
+++ b/otbtf/__init__.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 # ==========================================================================
 #
 #   Copyright 2018-2019 IRSTEA
@@ -20,7 +19,9 @@
 """
 OTBTF python module
 """
-import pkg_resources
+
+__version__ = "5.0.0-rc2"
+
 try:
     from otbtf.utils import read_as_np_arr, gdal_open  # noqa
     from otbtf.dataset import Buffer, PatchesReaderBase, PatchesImagesReader, \
@@ -34,4 +35,3 @@ except ImportError:
 from otbtf.tfrecords import TFRecords  # noqa
 from otbtf.model import ModelBase  # noqa
 from otbtf import layers, ops  # noqa
-__version__ = pkg_resources.require("otbtf")[0].version
diff --git a/otbtf/dataset.py b/otbtf/dataset.py
index cf2a0759e4c0b8e7788abb484737aae921e4727a..73ae88b9fccdad5ed78f9c37016acb4bebbdef29 100644
--- a/otbtf/dataset.py
+++ b/otbtf/dataset.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 # ==========================================================================
 #
 #   Copyright 2018-2019 IRSTEA
diff --git a/otbtf/examples/tensorflow_v1x/__init__.py b/otbtf/examples/tensorflow_v1x/__init__.py
index c77256a480843e5f109dfe04174daac4d76b44fb..796e4cbd9cd41936aef2c63a42a001a5c194dedc 100644
--- a/otbtf/examples/tensorflow_v1x/__init__.py
+++ b/otbtf/examples/tensorflow_v1x/__init__.py
@@ -38,7 +38,7 @@ Predicted label is a single pixel, for an input patch of size 16x16 (for an inpu
 The learning rate of the training operator can be adjusted using the *lr* placeholder.
 The following figure summarizes this architecture.
 
-<img src ="https://gitlab.irstea.fr/remi.cresson/otbtf/-/raw/develop/doc/images/savedmodel_simple_cnn.png" />
+<img src ="https://forgemia.inra.fr/orfeo-toolbox/otbtf/-/raw/develop/doc/images/savedmodel_simple_cnn.png" />
 
 ## Generate the model
 
@@ -143,7 +143,7 @@ otbcli_TensorflowModelServe \\
 The `create_savedmodel_simple_fcn.py` script enables you to create a fully
 convolutional model which does not use any stride.
 
-<img src ="https://gitlab.irstea.fr/remi.cresson/otbtf/-/raw/develop/doc/images/savedmodel_simple_fcnn.png" />
+<img src ="https://forgemia.inra.fr/orfeo-toolbox/otbtf/-/raw/develop/doc/images/savedmodel_simple_fcnn.png" />
 
 Thanks to that, once trained this model can be applied on the image to produce
 a landcover map at the same resolution as the input image, in a fully
@@ -208,7 +208,7 @@ available parameters.
 Let's train the M3 model from time series (TS) and Very High Resolution
 Satellite (VHRS) patches images.
 
-<img src ="https://gitlab.irstea.fr/remi.cresson/otbtf/-/raw/develop/doc/images/model_training.png" />
+<img src ="https://forgemia.inra.fr/orfeo-toolbox/otbtf/-/raw/develop/doc/images/model_training.png" />
 
 First, tell OTBTF that we want two sources: one for time series + one for
 VHR image
@@ -255,7 +255,7 @@ otbcli_TensorflowModelTrain \\
 Let's produce a land cover map using the M3 model from time series (TS) and
 Very High Resolution Satellite image (VHRS)
 
-<img src ="https://gitlab.irstea.fr/remi.cresson/otbtf/-/raw/develop/doc/images/classif_map.png" />
+<img src ="https://forgemia.inra.fr/orfeo-toolbox/otbtf/-/raw/develop/doc/images/classif_map.png" />
 
 Since we provide time series as the reference source (*source1*), the output
 classes are estimated at the same resolution. This model can be run in
@@ -363,7 +363,7 @@ See: Gaetano, R., Ienco, D., Ose, K., & Cresson, R. (2018). *A two-branch CNN
 architecture for land cover classification of PAN and MS imagery*. Remote
 Sensing, 10(11), 1746.
 
-<img src ="https://gitlab.irstea.fr/remi.cresson/otbtf/-/raw/develop/doc/images/savedmodel_simple_pxs_fcn.png" />
+<img src ="https://forgemia.inra.fr/orfeo-toolbox/otbtf/-/raw/develop/doc/images/savedmodel_simple_pxs_fcn.png" />
 
 Use `create_savedmodel_pxs_fcn.py` to generate this model.
 
diff --git a/otbtf/examples/tensorflow_v2x/deterministic/__init__.py b/otbtf/examples/tensorflow_v2x/deterministic/__init__.py
index d0ec3db4654aa19a87b31cf32003b2c3a2d2418d..03bb3133f473b2304fc74c4ca17e3e1342e27dcd 100644
--- a/otbtf/examples/tensorflow_v2x/deterministic/__init__.py
+++ b/otbtf/examples/tensorflow_v2x/deterministic/__init__.py
@@ -29,11 +29,11 @@ import tensorflow as tf
 x = tf.keras.Input(shape=[None, None, None], name="x")  # [1, h, w, N]
 
 # Compute norm on the last axis
-y = tf.norm(x, axis=-1)
+y = tf.keras.ops.norm(x, axis=-1)
 
 # Create model
 model = tf.keras.Model(inputs={"x": x}, outputs={"y": y})
-model.save("l2_norm_savedmodel")
+model.export("l2_norm_savedmodel")
 ```
 
 Run the code. The *l2_norm_savedmodel* file is created.
@@ -65,18 +65,19 @@ Let's consider a simple model that inputs two multispectral image (*x1* and
 The model is exported as a SavedModel named *scalar_product_savedmodel*
 
 ```python
-import tensorflow as tf
+import keras
 
 # Input
-x1 = tf.keras.Input(shape=[None, None, None], name="x1")  # [1, h, w, N]
-x2 = tf.keras.Input(shape=[None, None, None], name="x2")  # [1, h, w, N]
+x1 = keras.Input(shape=[None, None, None], name="x1")  # [1, h, w, N]
+x2 = keras.Input(shape=[None, None, None], name="x2")  # [1, h, w, N]
 
 # Compute scalar product
-y = tf.reduce_sum(tf.multiply(x1, x2), axis=-1)
+y = keras.ops.sum(keras.ops.multiply(x1, x2), axis=-1)
 
 # Create model
-model = tf.keras.Model(inputs={"x1": x1, "x2": x2}, outputs={"y": y})
-model.save("scalar_product_savedmodel")
+model = keras.Model(inputs={"x1": x1, "x2": x2}, outputs={"y": y})
+model.export("scalar_product_savedmodel")
+
 ```
 
 Run the code. The *scalar_product_savedmodel* file is created.
diff --git a/otbtf/examples/tensorflow_v2x/deterministic/l2_norm.py b/otbtf/examples/tensorflow_v2x/deterministic/l2_norm.py
index b23d86cb4016dcdd5eecb7eff65e8487392d4661..59e2b0a409de565b3810b22bf1cbeeb512bc06f5 100644
--- a/otbtf/examples/tensorflow_v2x/deterministic/l2_norm.py
+++ b/otbtf/examples/tensorflow_v2x/deterministic/l2_norm.py
@@ -14,14 +14,15 @@ otbcli_TensorflowModelServe \
 ```
 
 """
-import tensorflow as tf
+import keras
+
 
 # Input
-x = tf.keras.Input(shape=[None, None, None], name="x")  # [1, h, w, N]
+x = keras.Input(shape=[None, None, None], name="x")  # [1, h, w, N]
 
 # Compute norm on the last axis
-y = tf.norm(x, axis=-1)
+y = keras.ops.norm(x, axis=-1)
 
 # Create model
-model = tf.keras.Model(inputs={"x": x}, outputs={"y": y})
-model.save("l2_norm_savedmodel")
+model = keras.Model(inputs={"x": x}, outputs={"y": y})
+model.export("l2_norm_savedmodel")
diff --git a/otbtf/examples/tensorflow_v2x/deterministic/scalar_prod.py b/otbtf/examples/tensorflow_v2x/deterministic/scalar_prod.py
index 57127c5e01667dceeef23720ba550fecb0451f2c..c75d36313dda16c00cb425a9933a6a5aa40f24c3 100644
--- a/otbtf/examples/tensorflow_v2x/deterministic/scalar_prod.py
+++ b/otbtf/examples/tensorflow_v2x/deterministic/scalar_prod.py
@@ -16,15 +16,16 @@ OTB_TF_NSOURCES=2 otbcli_TensorflowModelServe \
 ```
 
 """
-import tensorflow as tf
+
+import keras
 
 # Input
-x1 = tf.keras.Input(shape=[None, None, None], name="x1")  # [1, h, w, N]
-x2 = tf.keras.Input(shape=[None, None, None], name="x2")  # [1, h, w, N]
+x1 = keras.Input(shape=[None, None, None], name="x1")  # [1, h, w, N]
+x2 = keras.Input(shape=[None, None, None], name="x2")  # [1, h, w, N]
 
 # Compute scalar product
-y = tf.reduce_sum(tf.multiply(x1, x2), axis=-1)
+y = keras.ops.sum(keras.ops.multiply(x1, x2), axis=-1)
 
 # Create model
-model = tf.keras.Model(inputs={"x1": x1, "x2": x2}, outputs={"y": y})
-model.save("scalar_product_savedmodel")
+model = keras.Model(inputs={"x1": x1, "x2": x2}, outputs={"y": y})
+model.export("scalar_product_savedmodel")
diff --git a/otbtf/examples/tensorflow_v2x/fcnn/fcnn_model.py b/otbtf/examples/tensorflow_v2x/fcnn/fcnn_model.py
index fcd14a2024e575f79a9f0c9a4a8e475a5e1d373b..4259390f3ea301fbd45aec50da462aef5d46c5e3 100644
--- a/otbtf/examples/tensorflow_v2x/fcnn/fcnn_model.py
+++ b/otbtf/examples/tensorflow_v2x/fcnn/fcnn_model.py
@@ -1,16 +1,18 @@
 """
 Implementation of a small U-Net like model
 """
+
 import logging
 
 import tensorflow as tf
+import keras
 
 from otbtf.model import ModelBase
 
 logging.basicConfig(
-    format='%(asctime)s %(levelname)-8s %(message)s',
+    format="%(asctime)s %(levelname)-8s %(message)s",
     level=logging.INFO,
-    datefmt='%Y-%m-%d %H:%M:%S'
+    datefmt="%Y-%m-%d %H:%M:%S",
 )
 
 # Number of classes estimated by the model
@@ -19,13 +21,13 @@ N_CLASSES = 2
 # Name of the input in the `FCNNModel` instance, also name of the input node
 # in the SavedModel
 INPUT_NAME = "input_xs"
+INPUT_SIGNATURE = tf.TensorSpec(
+    shape=[None, None, None, 4], dtype=tf.float32, name=INPUT_NAME
+)
 
 # Name of the output in the `FCNNModel` instance
 TARGET_NAME = "predictions"
 
-# Name (prefix) of the output node in the SavedModel
-OUTPUT_SOFTMAX_NAME = "predictions_softmax_tensor"
-
 
 class FCNNModel(ModelBase):
     """
@@ -51,7 +53,7 @@ class FCNNModel(ModelBase):
         Returns:
             dict of normalized inputs, ready to be used from `get_outputs()`
         """
-        return {INPUT_NAME: tf.cast(inputs[INPUT_NAME], tf.float32) * 0.0001}
+        return {INPUT_NAME: keras.ops.cast(inputs[INPUT_NAME], tf.float32) * 0.0001}
 
     def get_outputs(self, normalized_inputs: dict) -> dict:
         """
@@ -71,24 +73,24 @@ class FCNNModel(ModelBase):
         norm_inp = normalized_inputs[INPUT_NAME]
 
         def _conv(inp, depth, name):
-            conv_op = tf.keras.layers.Conv2D(
+            conv_op = keras.layers.Conv2D(
                 filters=depth,
                 kernel_size=3,
                 strides=2,
                 activation="relu",
                 padding="same",
-                name=name
+                name=name,
             )
             return conv_op(inp)
 
         def _tconv(inp, depth, name, activation="relu"):
-            tconv_op = tf.keras.layers.Conv2DTranspose(
+            tconv_op = keras.layers.Conv2DTranspose(
                 filters=depth,
                 kernel_size=3,
                 strides=2,
                 activation=activation,
                 padding="same",
-                name=name
+                name=name,
             )
             return tconv_op(inp)
 
@@ -101,40 +103,31 @@ class FCNNModel(ModelBase):
         out_tconv3 = _tconv(out_tconv2, 16, "tconv3") + out_conv1
         out_tconv4 = _tconv(out_tconv3, N_CLASSES, "classifier", None)
 
-        # Generally it is a good thing to name the final layers of the network
-        # (i.e. the layers of which outputs are returned from
-        # `MyModel.get_output()`). Indeed this enables to retrieve them for
-        # inference time, using their name. In case your forgot to name the
-        # last layers, it is still possible to look at the model outputs using
-        # the `saved_model_cli show --dir /path/to/your/savedmodel --all`
-        # command.
-        #
-        # Do not confuse **the name of the output layers** (i.e. the "name"
-        # property of the tf.keras.layer that is used to generate an output
-        # tensor) and **the key of the output tensor**, in the dict returned
-        # from `MyModel.get_output()`. They are two identifiers with a
-        # different purpose:
-        #  - the output layer name is used only at inference time, to identify
-        #    the output tensor from which generate the output image,
-        #  - the output tensor key identifies the output tensors, mainly to
-        #    fit the targets to model outputs during training process, but it
-        #    can also be used to access the tensors as tf/keras objects, for
-        #    instance to display previews images in TensorBoard.
-        softmax_op = tf.keras.layers.Softmax(name=OUTPUT_SOFTMAX_NAME)
+        softmax_op = keras.layers.Softmax()
         predictions = softmax_op(out_tconv4)
 
-        # note that we could also add additional outputs, for instance the
-        # argmax of the softmax:
+        # Model outputs are returned in a `dict`, where each key is an output
+        # name, and the value is the layer output. This naming have two
+        # functions:
+        #  - the output layer name is used at inference time, to identify
+        #    the output tensor from which generate the output image,
+        #  - the output layer name identifies the output tensors, to fit the
+        #    targets to model outputs, compute metrics, etc. during training
+        #    process. It can also be used to access the tensors as tf/keras
+        #    objects, for instance to display previews images in TensorBoard.
         #
-        # argmax_op = otbtf.layers.Argmax(name="labels")
-        # labels = argmax_op(predictions)
-        # return {TARGET_NAME: predictions, OUTPUT_ARGMAX_NAME: labels}
+        # Note that we could also add additional outputs, even outputs which
+        # are useless for the optimization process, for instance the argmax :
+        #   ```
+        #   argmax_op = otbtf.layers.Argmax()
+        #   labels = argmax_op(predictions)
+        #   return {TARGET_NAME: predictions, OUTPUT_ARGMAX_NAME: labels}
+        #   ```
         # The default extra outputs (i.e. output tensors with cropping in
         # physical domain) are append by `otbtf.ModelBase` for all returned
         # outputs of this function to be used at inference time (e.g.
-        # "labels_crop32", "labels_crop64", ...,
-        # "predictions_softmax_tensor_crop16", ..., etc).
-
+        # "labels_crop32", "labels_crop64", ..., "predictions__crop16", ...,
+        # etc).
         return {TARGET_NAME: predictions}
 
 
@@ -158,10 +151,12 @@ def dataset_preprocessing_fn(examples: dict):
     """
     return {
         INPUT_NAME: examples["input_xs_patches"],
-        TARGET_NAME: tf.one_hot(
-            tf.squeeze(tf.cast(examples["labels_patches"], tf.int32), axis=-1),
-            depth=N_CLASSES
-        )
+        TARGET_NAME: keras.ops.one_hot(
+            keras.ops.squeeze(
+                keras.ops.cast(examples["labels_patches"], tf.int32), axis=-1
+            ),
+            N_CLASSES,
+        ),
     }
 
 
@@ -185,23 +180,18 @@ def train(params, ds_train, ds_valid, ds_test):
         model = FCNNModel(dataset_element_spec=ds_train.element_spec)
 
         # Compile the model
-        # It is a good practice to use a `dict` to explicitly name the outputs
-        # over which the losses/metrics are computed.
-        # This ensures a better optimization control, and also avoids lots of
-        # useless outputs (e.g. metrics computed over extra outputs).
+        # Since Keras 3 it is mandatory to use a `dict` to explicitly name the
+        # outputs over which the losses/metrics are computed, e.g.
+        # `loss: {TARGET_NAME: "categorical_crossentropy"}`
         model.compile(
-            loss={
-                TARGET_NAME: tf.keras.losses.CategoricalCrossentropy()
-            },
-            optimizer=tf.keras.optimizers.Adam(
-                learning_rate=params.learning_rate
-            ),
+            loss={TARGET_NAME: keras.losses.CategoricalCrossentropy()},
+            optimizer=keras.optimizers.Adam(learning_rate=params.learning_rate),
             metrics={
                 TARGET_NAME: [
-                    tf.keras.metrics.Precision(class_id=1),
-                    tf.keras.metrics.Recall(class_id=1)
+                    keras.metrics.Precision(class_id=1),
+                    keras.metrics.Recall(class_id=1),
                 ]
-            }
+            },
         )
 
         # Summarize the model (in CLI)
@@ -215,4 +205,4 @@ def train(params, ds_train, ds_valid, ds_test):
             model.evaluate(ds_test, batch_size=params.batch_size)
 
         # Save trained model as SavedModel
-        model.save(params.model_dir)
+        model.export(params.model_dir)
diff --git a/otbtf/layers.py b/otbtf/layers.py
index ef65ec1c5ce593ca70d6c316ae018f6a46efa596..be2ec04c1ae2ba6745487eb6f73b316536b95d0e 100644
--- a/otbtf/layers.py
+++ b/otbtf/layers.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 # ==========================================================================
 #
 #   Copyright 2018-2019 IRSTEA
@@ -25,13 +24,14 @@ The utils module provides some useful keras layers to build deep nets.
 """
 from typing import List, Tuple, Any
 import tensorflow as tf
+import keras
 
 
 Tensor = Any
 Scalars = List[float] | Tuple[float]
 
 
-class DilatedMask(tf.keras.layers.Layer):
+class DilatedMask(keras.layers.Layer):
     """Layer to dilate a binary mask."""
     def __init__(self, nodata_value: float, radius: int, name: str = None):
         """
@@ -70,7 +70,7 @@ class DilatedMask(tf.keras.layers.Layer):
         return tf.cast(conv2d_out, tf.uint8)
 
 
-class ApplyMask(tf.keras.layers.Layer):
+class ApplyMask(keras.layers.Layer):
     """Layer to apply a binary mask to one input."""
     def __init__(self, out_nodata: float, name: str = None):
         """
@@ -95,7 +95,7 @@ class ApplyMask(tf.keras.layers.Layer):
         return tf.where(mask == 1, float(self.out_nodata), inp)
 
 
-class ScalarsTile(tf.keras.layers.Layer):
+class ScalarsTile(keras.layers.Layer):
     """
     Layer to duplicate some scalars in a whole array.
     Simple example with only one scalar = 0.152:
@@ -127,7 +127,7 @@ class ScalarsTile(tf.keras.layers.Layer):
         return tf.tile(inp, [1, tf.shape(ref)[1], tf.shape(ref)[2], 1])
 
 
-class Argmax(tf.keras.layers.Layer):
+class Argmax(keras.layers.Layer):
     """
     Layer to compute the argmax of a tensor.
 
@@ -165,7 +165,7 @@ class Argmax(tf.keras.layers.Layer):
         return argmax
 
 
-class Max(tf.keras.layers.Layer):
+class Max(keras.layers.Layer):
     """
     Layer to compute the max of a tensor.
 
diff --git a/otbtf/model.py b/otbtf/model.py
index 9958510bdf32dd147df273a264714c93d2543ee1..e20ff81b13aa54b87c924754eff646934b80ea24 100644
--- a/otbtf/model.py
+++ b/otbtf/model.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 # ==========================================================================
 #
 #   Copyright 2018-2019 IRSTEA
@@ -27,6 +26,7 @@ from typing import List, Dict, Any
 import abc
 import logging
 import tensorflow as tf
+import keras
 
 Tensor = Any
 TensorsDict = Dict[str, Tensor]
@@ -38,10 +38,10 @@ class ModelBase(abc.ABC):
     """
 
     def __init__(
-            self,
-            dataset_element_spec: tf.TensorSpec,
-            input_keys: List[str] = None,
-            inference_cropping: List[int] = None
+        self,
+        dataset_element_spec: tf.TensorSpec,
+        input_keys: List[str] = None,
+        inference_cropping: List[int] = None,
     ):
         """
         Model initializer, must be called **inside** the strategy.scope().
@@ -60,18 +60,14 @@ class ModelBase(abc.ABC):
         """
         # Retrieve dataset inputs shapes
         dataset_input_element_spec = dataset_element_spec[0]
-        logging.info(
-            "Dataset input element spec: %s", dataset_input_element_spec
-        )
+        logging.info("Dataset input element spec: %s", dataset_input_element_spec)
 
         if input_keys:
             self.dataset_input_keys = input_keys
             logging.info("Using input keys: %s", self.dataset_input_keys)
         else:
             self.dataset_input_keys = list(dataset_input_element_spec)
-            logging.info(
-                "Found dataset input keys: %s", self.dataset_input_keys
-            )
+            logging.info("Found dataset input keys: %s", self.dataset_input_keys)
 
         self.inputs_shapes = {
             key: dataset_input_element_spec[key].shape[1:]
@@ -116,7 +112,7 @@ class ModelBase(abc.ABC):
             if len(new_shape) > 2:
                 new_shape[0] = None
                 new_shape[1] = None
-            placeholder = tf.keras.Input(shape=new_shape, name=key)
+            placeholder = keras.Input(shape=new_shape, name=key)
             logging.info("New shape for input %s: %s", key, new_shape)
             model_inputs.update({key: placeholder})
         return model_inputs
@@ -158,10 +154,10 @@ class ModelBase(abc.ABC):
         return inputs
 
     def postprocess_outputs(
-            self,
-            outputs: TensorsDict,
-            inputs: TensorsDict = None,
-            normalized_inputs: TensorsDict = None
+        self,
+        outputs: TensorsDict,
+        inputs: TensorsDict = None,
+        normalized_inputs: TensorsDict = None,
     ) -> TensorsDict:
         """
         Post-process the model outputs.
@@ -185,21 +181,21 @@ class ModelBase(abc.ABC):
             for crop in self.inference_cropping:
                 extra_output_key = cropped_tensor_name(out_key, crop)
                 extra_output_name = cropped_tensor_name(
-                    out_tensor._keras_history.layer.name, crop
+                    out_tensor._keras_history.operation.name, crop
                 )
                 logging.info(
                     "Adding extra output for tensor %s with crop %s (%s)",
-                    out_key, crop, extra_output_name
+                    out_key,
+                    crop,
+                    extra_output_name,
                 )
                 cropped = out_tensor[:, crop:-crop, crop:-crop, :]
-                identity = tf.keras.layers.Activation(
-                    'linear', name=extra_output_name
-                )
+                identity = keras.layers.Identity(name=extra_output_name)
                 extra_outputs[extra_output_key] = identity(cropped)
 
         return extra_outputs
 
-    def create_network(self) -> tf.keras.Model:
+    def create_network(self) -> keras.Model:
         """
         This method returns the Keras model. This needs to be called
         **inside** the strategy.scope(). Can be reimplemented depending on the
@@ -214,27 +210,28 @@ class ModelBase(abc.ABC):
         logging.info("Model inputs: %s", inputs)
 
         # Normalize the inputs
-        normalized_inputs = self.normalize_inputs(inputs=inputs)
+        normalized_inputs = self.normalize_inputs(inputs)
         logging.info("Normalized model inputs: %s", normalized_inputs)
 
         # Build the model
-        outputs = self.get_outputs(normalized_inputs=normalized_inputs)
+        outputs = self.get_outputs(normalized_inputs)
         logging.info("Model outputs: %s", outputs)
 
         # Post-processing for inference
         postprocessed_outputs = self.postprocess_outputs(
-            outputs=outputs,
-            inputs=inputs,
-            normalized_inputs=normalized_inputs
+            outputs, inputs, normalized_inputs
         )
         outputs.update(postprocessed_outputs)
 
+        # Since Keras 3, outputs are named after the key in the returned
+        # dict of `get_outputs()`
+        outputs = {
+            key: keras.layers.Identity(name=key)(prediction)
+            for key, prediction in outputs.items()
+        }
+
         # Return the keras model
-        return tf.keras.Model(
-            inputs=inputs,
-            outputs=outputs,
-            name=self.__class__.__name__
-        )
+        return keras.Model(inputs=inputs, outputs=outputs, name=self.__class__.__name__)
 
     def summary(self, strategy=None):
         """
@@ -260,14 +257,13 @@ class ModelBase(abc.ABC):
             show_shapes: annotate with shapes values (True or False)
 
         """
-        assert self.model, "Plot() only works if create_network() has been " \
-                           "called beforehand"
+        assert self.model, (
+            "Plot() only works if create_network() has been " "called beforehand"
+        )
 
         # When multiworker strategy, only plot if the worker is chief
         if not strategy or _is_chief(strategy):
-            tf.keras.utils.plot_model(
-                self.model, output_path, show_shapes=show_shapes
-            )
+            keras.utils.plot_model(self.model, output_path, show_shapes=show_shapes)
 
 
 def _is_chief(strategy):
@@ -294,9 +290,11 @@ def _is_chief(strategy):
     if strategy.cluster_resolver:  # this means MultiWorkerMirroredStrategy
         task_type = strategy.cluster_resolver.task_type
         task_id = strategy.cluster_resolver.task_id
-        return (task_type == 'chief') \
-            or (task_type == 'worker' and task_id == 0) \
+        return (
+            (task_type == "chief")
+            or (task_type == "worker" and task_id == 0)
             or task_type is None
+        )
     # strategy with only one worker
     return True
 
diff --git a/otbtf/ops.py b/otbtf/ops.py
index ef5c52b94060833c568dfabfcd54e98103ac85aa..5a473562ed842b268fea46e962c2ae5d8c81a31b 100644
--- a/otbtf/ops.py
+++ b/otbtf/ops.py
@@ -26,6 +26,7 @@ and train deep nets.
 """
 from typing import List, Tuple, Any
 import tensorflow as tf
+import keras
 
 
 Tensor = Any
@@ -44,5 +45,7 @@ def one_hot(labels: Tensor, nb_classes: int):
         one-hot encoded vector (shape [x, y, nb_classes])
 
     """
-    labels_xy = tf.squeeze(tf.cast(labels, tf.int32), axis=-1)  # shape [x, y]
-    return tf.one_hot(labels_xy, depth=nb_classes)  # shape [x, y, nb_classes]
+    # shape [x, y]
+    labels_xy = keras.ops.squeeze(keras.ops.cast(labels, tf.int32), axis=-1)
+    # shape [x, y, nb_classes]
+    return keras.ops.one_hot(labels_xy, nb_classes)
diff --git a/otbtf/tfrecords.py b/otbtf/tfrecords.py
index e5ac08417cd80d3a95e019205fbf61eb8dbeb830..8d2ce781f7a45b574ae0fa5e1143493ba73a615b 100644
--- a/otbtf/tfrecords.py
+++ b/otbtf/tfrecords.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 # ==========================================================================
 #
 #   Copyright 2018-2019 IRSTEA
diff --git a/otbtf/utils.py b/otbtf/utils.py
index 1c552fbd5c403cb048c8faa4160f62e5ae176a46..66b43ca7c62b521e9e433dd776881497ed6ac4ae 100644
--- a/otbtf/utils.py
+++ b/otbtf/utils.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 # ==========================================================================
 #
 #   Copyright 2018-2019 IRSTEA
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..19f7c077f70d0c37c45937fe1506dfd18fbde19a
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,39 @@
+[build-system]
+requires = ["setuptools>=61", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "otbtf"
+description = "OTBTF: Orfeo ToolBox meets TensorFlow"
+readme = "README.md"
+requires-python = ">=3.8"
+dynamic = ["version"]
+keywords = [
+    "remote sensing",
+    "otb",
+    "orfeotoolbox",
+    "orfeo toolbox",
+    "tensorflow",
+    "deep learning",
+    "machine learning"
+]
+authors = [
+    { name = "Remi Cresson", email = "remi.cresson@inrae.fr" },
+]
+urls = { "Homepage" = "https://github.com/remicres/otbtf" }
+classifiers = [
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Topic :: Scientific/Engineering :: GIS",
+    "Topic :: Scientific/Engineering :: Image Processing",
+    "License :: OSI Approved :: Apache Software License",
+    "Operating System :: OS Independent"
+]
+dependencies = [ "keras>=3" ]
+
+[tool.setuptools]
+packages = ["otbtf"]
+
+[tool.setuptools.dynamic]
+version = { attr = "otbtf.__version__" }
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 8519fb1132c1f889eb2edb6853132ff08c747538..0000000000000000000000000000000000000000
--- a/setup.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# -*- coding: utf-8 -*-
-import setuptools
-
-with open("README.md", "r", encoding="utf-8") as fh:
-    long_description = fh.read()
-
-setuptools.setup(
-    name="otbtf",
-    version="4.3.1",
-    author="Remi Cresson",
-    author_email="remi.cresson@inrae.fr",
-    description="OTBTF: Orfeo ToolBox meets TensorFlow",
-    long_description=long_description,
-    long_description_content_type="text/markdown",
-    url="https://gitlab.irstea.fr/remi.cresson/otbtf",
-    classifiers=[
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-        "Topic :: Scientific/Engineering :: GIS",
-        "Topic :: Scientific/Engineering :: Image Processing",
-        "License :: OSI Approved :: Apache Software License",
-        "Operating System :: OS Independent",
-    ],
-    packages=setuptools.find_packages(),
-    python_requires=">=3.6",
-    keywords=["remote sensing",
-              "otb",
-              "orfeotoolbox",
-              "orfeo toolbox",
-              "tensorflow",
-              "deep learning",
-              "machine learning"
-              ],
-)
diff --git a/tools/docker/build-deps-cli.txt b/system-dependencies.txt
similarity index 73%
rename from tools/docker/build-deps-cli.txt
rename to system-dependencies.txt
index 49a4572b46825915b8b15812dc4ad3eb72b94b4c..53388447fbccf37f900d70ea6a073bfef9193b01 100644
--- a/tools/docker/build-deps-cli.txt
+++ b/system-dependencies.txt
@@ -1,25 +1,33 @@
 apt-transport-https
 ca-certificates
-curl
-cmake
+lsb-release
+software-properties-common
+gpg
 file
-g++
-gcc
+sudo
+zip
+unzip
+curl
+wget
+vim
+nano
+
+pkg-config
 git
-libc6-dev
+git-lfs
 libtool
-lsb-release
+libc6-dev
+swig
+cppcheck
+cmake
 make
-nano
 patch
-pkg-config
+patchelf
+g++
+gcc
+
 python3-dev
 python3-pip
 python3-setuptools
 python3-venv
-swig
-unzip
-vim
-wget
-sudo
-zip
\ No newline at end of file
+virtualenv
diff --git a/test/api_unittest.py b/test/api_unittest.py
index 2fe3fe38632d8739dca7a61458ec95d398b0170e..b2e3ba8541ada8b1c327d3961cbc249475520515 100644
--- a/test/api_unittest.py
+++ b/test/api_unittest.py
@@ -1,5 +1,4 @@
 #!/usr/bin/env python3
-# -*- coding: utf-8 -*-
 import unittest
 
 import pytest
@@ -7,8 +6,10 @@ import pytest
 from otbtf.examples.tensorflow_v2x.fcnn import create_tfrecords
 from otbtf.examples.tensorflow_v2x.fcnn import train_from_patchesimages
 from otbtf.examples.tensorflow_v2x.fcnn import train_from_tfrecords
-from otbtf.examples.tensorflow_v2x.fcnn.fcnn_model import INPUT_NAME, \
-    OUTPUT_SOFTMAX_NAME
+from otbtf.examples.tensorflow_v2x.fcnn.fcnn_model import (
+    INPUT_NAME,
+    TARGET_NAME
+)
 from otbtf.model import cropped_tensor_name
 from test_utils import resolve_paths, files_exist, run_command_and_compare
 
@@ -33,7 +34,7 @@ class APITest(unittest.TestCase):
         ])
         train_from_patchesimages.train(params=params)
         self.assertTrue(files_exist([
-            '$TMPDIR/model_from_pimg/keras_metadata.pb',
+            '$TMPDIR/model_from_pimg/fingerprint.pb',
             '$TMPDIR/model_from_pimg/saved_model.pb',
             '$TMPDIR/model_from_pimg/variables/variables.data-00000-of-00001',
             '$TMPDIR/model_from_pimg/variables/variables.index'
@@ -51,7 +52,7 @@ class APITest(unittest.TestCase):
                 f"-source1.placeholder {INPUT_NAME} "
                 "-model.dir $TMPDIR/model_from_pimg "
                 "-model.fullyconv on "
-                f"-output.names {cropped_tensor_name(OUTPUT_SOFTMAX_NAME, 16)} "
+                f"-output.names {cropped_tensor_name(TARGET_NAME, 16)} "
                 "-output.efieldx 32 "
                 "-output.efieldy 32 "
                 "-out \"$TMPDIR/classif_model4_softmax.tif?&gdal:co:compress=deflate\" uint8",
@@ -68,7 +69,7 @@ class APITest(unittest.TestCase):
                 f"-source1.placeholder {INPUT_NAME} "
                 "-model.dir $TMPDIR/model_from_pimg "
                 "-model.fullyconv on "
-                f"-output.names {cropped_tensor_name(OUTPUT_SOFTMAX_NAME, 32)} "
+                f"-output.names {cropped_tensor_name(TARGET_NAME, 32)} "
                 "-output.efieldx 64 "
                 "-output.efieldy 64 "
                 "-out \"$TMPDIR/classif_model4_softmax.tif?&gdal:co:compress=deflate\" uint8",
@@ -110,7 +111,7 @@ class APITest(unittest.TestCase):
         ])
         train_from_tfrecords.train(params=params)
         self.assertTrue(files_exist([
-            '$TMPDIR/model_from_tfrecs/keras_metadata.pb',
+            '$TMPDIR/model_from_tfrecs/fingerprint.pb',
             '$TMPDIR/model_from_tfrecs/saved_model.pb',
             '$TMPDIR/model_from_tfrecs/variables/variables.data-00000-of-00001',
             '$TMPDIR/model_from_tfrecs/variables/variables.index'
@@ -128,7 +129,7 @@ class APITest(unittest.TestCase):
                 f"-source1.placeholder {INPUT_NAME} "
                 "-model.dir $TMPDIR/model_from_pimg "
                 "-model.fullyconv on "
-                f"-output.names {cropped_tensor_name(OUTPUT_SOFTMAX_NAME, 16)} "
+                f"-output.names {cropped_tensor_name(TARGET_NAME, 16)} "
                 "-output.efieldx 32 "
                 "-output.efieldy 32 "
                 "-out \"$TMPDIR/classif_model4_softmax.tif?&gdal:co:compress=deflate\" uint8",
@@ -148,7 +149,7 @@ class APITest(unittest.TestCase):
                 f"-source1.placeholder {INPUT_NAME} "
                 "-model.dir $TMPDIR/model_from_pimg "
                 "-model.fullyconv on "
-                f"-output.names {cropped_tensor_name(OUTPUT_SOFTMAX_NAME, 32)} "
+                f"-output.names {cropped_tensor_name(TARGET_NAME, 32)} "
                 "-output.efieldx 64 "
                 "-output.efieldy 64 "
                 "-out \"$TMPDIR/classif_model4_softmax.tif?&gdal:co:compress=deflate\" uint8",
diff --git a/test/geos_test.py b/test/geos_test.py
index 7b8b39f9ec59822aff3c43c4ae62ed71f730e9a0..8cb36b4b1d93a709ee7ab570d609e57c2b791740 100644
--- a/test/geos_test.py
+++ b/test/geos_test.py
@@ -1,6 +1,4 @@
 #!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-import pytest
 import unittest
 from osgeo import ogr
 
diff --git a/test/imports_test.py b/test/imports_test.py
deleted file mode 100644
index e745ed5baacd0a8a7a141891e3fb497ad537b81c..0000000000000000000000000000000000000000
--- a/test/imports_test.py
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-import pytest
-import unittest
-
-class ImportsTest(unittest.TestCase):
-
-    def test_import_both1(self):
-        import tensorflow
-        self.assertTrue(tensorflow.__version__)
-        import otbApplication
-        self.assertTrue(otbApplication.Registry_GetAvailableApplications())
-
-
-    def test_import_both2(self):
-        import otbApplication
-        self.assertTrue(otbApplication.Registry_GetAvailableApplications())
-        import tensorflow
-        self.assertTrue(tensorflow.__version__)
-
-
-    def test_import_all(self):
-        import otbApplication
-        self.assertTrue(otbApplication.Registry_GetAvailableApplications())
-        import tensorflow
-        self.assertTrue(tensorflow.__version__)
-        from osgeo import gdal
-        self.assertTrue(gdal.__version__)
-        import numpy
-        self.assertTrue(numpy.__version__)
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/test/models/model5.py b/test/models/model5.py
index cc17d52edce5202befaee4f3fc8f7780fbc06f30..334d4f7989714f6d3f743958e99e07c5aa4bca6a 100644
--- a/test/models/model5.py
+++ b/test/models/model5.py
@@ -4,22 +4,21 @@ The input of this model must be a mono channel image.
 All 4 different output shapes supported in OTBTF are tested.
 
 """
-import tensorflow as tf
+import keras
 
 # Input
-x = tf.keras.Input(shape=[None, None, None], name="x")  # [b, h, w, c=1]
+x = keras.Input(shape=[None, None, None], name="x")  # [b, h, w, c=1]
 
 # Create reshaped outputs
-shape = tf.shape(x)
+shape = keras.ops.shape(x)
 b = shape[0]
 h = shape[1]
 w = shape[2]
-y1 = tf.reshape(x, shape=(b*h*w,))  # [b*h*w]
-y2 = tf.reshape(x, shape=(b*h*w, 1))  # [b*h*w, 1]
-y3 = tf.reshape(x, shape=(b, h, w))  # [b, h, w]
-y4 = tf.reshape(x, shape=(b, h, w, 1))  # [b, h, w, 1]
+y1 = keras.ops.reshape(x, shape=(b*h*w,))  # [b*h*w]
+y2 = keras.ops.reshape(x, shape=(b*h*w, 1))  # [b*h*w, 1]
+y3 = keras.ops.reshape(x, shape=(b, h, w))  # [b, h, w]
+y4 = keras.ops.reshape(x, shape=(b, h, w, 1))  # [b, h, w, 1]
 
 # Create model
-model = tf.keras.Model(inputs={"x": x}, outputs={"y1": y1, "y2": y2, "y3": y3, "y4": y4})
-model.save("model5")
-
+model = keras.Model(inputs={"x": x}, outputs={"y1": y1, "y2": y2, "y3": y3, "y4": y4})
+model.export("model5")
diff --git a/test/nodata_test.py b/test/nodata_test.py
index c3892153401d26f000b82da59547dcbc8c890ea7..32878f435ea53977b4e05c6baa4b07bad8720e59 100644
--- a/test/nodata_test.py
+++ b/test/nodata_test.py
@@ -1,11 +1,8 @@
 #!/usr/bin/env python3
-# -*- coding: utf-8 -*-
 import otbApplication
-import pytest
-import tensorflow as tf
+import keras
 import unittest
 
-import otbtf
 from test_utils import resolve_paths, compare
 
 
@@ -28,10 +25,10 @@ class NodataInferenceTest(unittest.TestCase):
         sm_dir = resolve_paths("$TMPDIR/l2_norm_savedmodel")
 
         # Create model
-        x = tf.keras.Input(shape=[None, None, None], name="x")
-        y = tf.norm(x, axis=-1)
-        model = tf.keras.Model(inputs={"x": x}, outputs={"y": y})
-        model.save(sm_dir)
+        x = keras.Input(shape=[None, None, None], name="x")
+        y = keras.ops.norm(x, axis=-1)
+        model = keras.Model(inputs={"x": x}, outputs={"y": y})
+        model.export(sm_dir)
 
         # Input image: f(x, y) = x * y if x > y else 0
         bmx = otbApplication.Registry.CreateApplication("BandMathX")
diff --git a/test/numpy_test.py b/test/numpy_test.py
index 55f0272ce4b2570c4ff0642ded73bf75baaa0d58..0d45a47f7019a7e617a138de00acbc614cfefca9 100644
--- a/test/numpy_test.py
+++ b/test/numpy_test.py
@@ -1,6 +1,4 @@
 #!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-import pytest
 import unittest
 import otbApplication
 from osgeo import gdal
diff --git a/test/pc_test.py b/test/pc_test.py
index 34544d02c56a4c3c4456aef5efa9f2d0815ef7e6..55a841d7cd85a088d6838736ebed1928d65e5d2f 100644
--- a/test/pc_test.py
+++ b/test/pc_test.py
@@ -1,6 +1,4 @@
 #!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-import pytest
 import unittest
 import planetary_computer
 import pystac_client
diff --git a/test/rio_test.py b/test/rio_test.py
index c6a0f2f1b4bd0f49ba68266f20005433987919ab..99bb0d57b5d4586035d48a83566f8ab996fbacb2 100644
--- a/test/rio_test.py
+++ b/test/rio_test.py
@@ -1,6 +1,4 @@
 #!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-import pytest
 import unittest
 import rasterio
 import rasterio.features
diff --git a/test/sr4rs_unittest.py b/test/sr4rs_unittest.py
index 89c3945f153304d04d35c23ef34513cf668120bc..87d887ddfc51ff7fb28d1caefc60d66ffac238ce 100644
--- a/test/sr4rs_unittest.py
+++ b/test/sr4rs_unittest.py
@@ -1,6 +1,4 @@
 #!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
 import unittest
 import os
 from pathlib import Path
diff --git a/test/test_utils.py b/test/test_utils.py
index 4554e28e3093e82d17faddf02ca0e14ad5536be1..2d9ba9c4d21abddb902b71bfb9dc0828183bf295 100644
--- a/test/test_utils.py
+++ b/test/test_utils.py
@@ -1,7 +1,8 @@
-import otbApplication
 import os
 from pathlib import Path
 
+import otbApplication
+
 
 def get_nb_of_channels(raster):
     """
diff --git a/test/tutorial_unittest.py b/test/tutorial_unittest.py
index af2b181c8442f8c7033b0e7ea94f39d03d262efc..886e017e94ddd4ca8c86427b3df6d2b5db7ad47f 100644
--- a/test/tutorial_unittest.py
+++ b/test/tutorial_unittest.py
@@ -1,5 +1,4 @@
 #!/usr/bin/env python3
-# -*- coding: utf-8 -*-
 import pytest
 import unittest
 from test_utils import run_command, run_command_and_test_exist, run_command_and_compare
diff --git a/tools/docker/build-env-tf.sh b/tools/docker/build-env-tf.sh
deleted file mode 100644
index e7703f0174a2f13dc5ec24939aeccffef4e01a7d..0000000000000000000000000000000000000000
--- a/tools/docker/build-env-tf.sh
+++ /dev/null
@@ -1,57 +0,0 @@
-### TF - bazel build env variables
-
-# As in official TF wheels, you'll need to remove "-march=native" to ensure
-# portability (avoid AVX2 / AVX512 compatibility issues)
-# You could also add CPUs instructions one by one, in this example to avoid
-# only AVX512 but enable commons optimizations like FMA, SSE4.2 and AVX2
-#export CC_OPT_FLAGS="-Wno-sign-compare --copt=-mavx --copt=-mavx2 --copt=-mfma --copt=-mfpmath=both --copt=-msse4.2"
-export CC_OPT_FLAGS="-march=native -Wno-sign-compare"
-export GCC_HOST_COMPILER_PATH=$(which gcc)
-export PYTHON_BIN_PATH=$(which python)
-export PYTHON_LIB_PATH="$($PYTHON_BIN_PATH -c 'import site; print(site.getsitepackages()[0])')"
-export TF_DOWNLOAD_CLANG=0
-export TF_ENABLE_XLA=1
-export TF_NEED_COMPUTECPP=0
-export TF_NEED_GDR=0
-export TF_NEED_JEMALLOC=1
-export TF_NEED_KAFKA=0
-export TF_NEED_MPI=0
-export TF_NEED_OPENCL=0
-export TF_NEED_OPENCL_SYCL=0
-export TF_NEED_VERBS=0
-export TF_SET_ANDROID_WORKSPACE=0
-export TF_NEED_CLANG=0
-# For MKL support BZL_CONFIGS+=" --config=mkl"
-#export TF_DOWNLOAD_MKL=1
-#export TF_NEED_MKL=0
-# Needed BZL_CONFIGS=" --config=nogcp --config=noaws --config=nohdfs"
-#export TF_NEED_S3=0
-#export TF_NEED_AWS=0
-#export TF_NEED_GCP=0
-#export TF_NEED_HDFS=0
-
-## GPU
-export TF_NEED_ROCM=0
-export TF_NEED_CUDA=0
-export CUDA_TOOLKIT_PATH=$(find /usr/local -maxdepth 1 -type d -name 'cuda-*')
-if  [ ! -z $CUDA_TOOLKIT_PATH ] ; then
-    if [ ! -z $TENSORRT ]; then
-        echo "Building tensorflow with TensorRT support"
-        apt install \
-            libnvinfer8=$TENSORRT \
-            libnvinfer-dev=$TENSORRT \
-            libnvinfer-plugin8=$TENSORRT \
-            libnvinfer-plugin-dev=$TENSORRT
-        export TF_TENSORRT_VERSION=$(cat $(find /usr/ -type f -name NvInferVersion.h) | grep '#define NV_TENSORRT_MAJOR' | cut -f3 -d' ')
-        export TF_NEED_TENSORRT=1
-    fi
-    export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$CUDA_TOOLKIT_PATH/lib64:$CUDA_TOOLKIT_PATH/lib64/stubs"
-    export TF_CUDA_VERSION=$(echo $CUDA_TOOLKIT_PATH | sed -r 's/.*\/cuda-(.*)/\1/')
-    export TF_CUDA_COMPUTE_CAPABILITIES="5.2,6.1,7.0,7.5,8.0,8.6,9.0"
-    export TF_NEED_CUDA=1
-    export TF_CUDA_CLANG=0
-    export TF_NEED_TENSORRT=0
-    export CUDNN_INSTALL_PATH="/usr/"
-    export TF_CUDNN_VERSION=$(sed -n 's/^#define CUDNN_MAJOR\s*\(.*\).*/\1/p' $CUDNN_INSTALL_PATH/include/cudnn_version.h)
-    export TF_NCCL_VERSION=2
-fi
diff --git a/tools/docker/build-flags-otb.txt b/tools/docker/build-flags-otb.txt
deleted file mode 100644
index 8c9b01233ed7754de0910fa011dc81e745ae7947..0000000000000000000000000000000000000000
--- a/tools/docker/build-flags-otb.txt
+++ /dev/null
@@ -1,8 +0,0 @@
--DOTB_BUILD_FeaturesExtraction=ON
--DOTB_BUILD_Hyperspectral=ON
--DOTB_BUILD_Learning=ON
--DOTB_BUILD_Miscellaneous=ON
--DOTB_BUILD_RemoteModules=ON
--DOTB_BUILD_SAR=ON
--DOTB_BUILD_Segmentation=ON
--DOTB_BUILD_StereoProcessing=ON
diff --git a/tools/docker/multibuild.sh b/tools/docker/multibuild.sh
deleted file mode 100644
index 9373d292469cbdd52cbf034c93d6edc8b9ba0869..0000000000000000000000000000000000000000
--- a/tools/docker/multibuild.sh
+++ /dev/null
@@ -1,75 +0,0 @@
-#!/bin/bash
-# Various docker builds using bazel cache
-RELEASE=3.5
-CPU_IMG=ubuntu:22.04
-GPU_IMG=nvidia/cuda:12.1.0-devel-ubuntu22.04
-
-## Bazel remote cache daemon
-mkdir -p $HOME/.cache/bazel-remote
-docker run -d -u 1000:1000 \
--v $HOME/.cache/bazel-remote:/data \
--p 9090:8080 \
-buchgr/bazel-remote-cache --max_size=20
-
-### CPU images
-
-# CPU-Dev
-docker build . \
---network='host' \
--t mdl4eo/otbtf:$RELEASE-cpu-dev \
---build-arg BASE_IMG=$CPU_IMG \
---build-arg KEEP_SRC_OTB=true
-
-# CPU
-docker build . \
---network='host' \
--t mdl4eo/otbtf:$RELEASE-cpu \
---build-arg BASE_IMG=$CPU_IMG
-
-# CPU-GUI
-docker build . \
---network='host' \
--t mdl4eo/otbtf:$RELEASE-cpu-gui \
---build-arg BASE_IMG=$CPU_IMG \
---build-arg GUI=true
-
-### CPU images with Intel MKL support
-MKL_CONF="--config=nogcp --config=noaws --config=nohdfs --config=mkl --config=opt"
-
-# CPU-MKL
-docker build . \
---network='host' \
--t mdl4eo/otbtf:$RELEASE-cpu-mkl \
---build-arg BASE_IMG=$CPU_IMG \
---build-arg BZL_CONFIGS="$MKL_CONF"
-
-# CPU-MKL-Dev
-docker build . \
---network='host' \
--t mdl4eo/otbtf:$RELEASE-cpu-mkl-dev \
---build-arg BASE_IMG=$CPU_IMG \
---build-arg BZL_CONFIGS="$MKL_CONF" \
---build-arg KEEP_SRC_OTB=true
-
-### GPU enabled images
-# Support is enabled if CUDA is found in /usr/local
-
-# GPU
-docker build . \
---network='host' \
--t mdl4eo/otbtf:$RELEASE-gpu-dev \
---build-arg BASE_IMG=$GPU_IMG \
---build-arg KEEP_SRC_OTB=true
-
-# GPU-Dev
-docker build . \
---network='host' \
--t mdl4eo/otbtf:$RELEASE-gpu \
---build-arg BASE_IMG=$GPU_IMG
-
-# GPU-GUI
-docker build . \
---network='host' \
--t mdl4eo/otbtf:$RELEASE-gpu-gui \
---build-arg BASE_IMG=$GPU_IMG \
---build-arg GUI=true
diff --git a/tricks/__init__.py b/tricks/__init__.py
deleted file mode 100644
index d22e7e96543aa57b3925a2738377110ab28943f4..0000000000000000000000000000000000000000
--- a/tricks/__init__.py
+++ /dev/null
@@ -1,96 +0,0 @@
-# -*- coding: utf-8 -*-
-# ==========================================================================
-#
-#   Copyright 2018-2019 IRSTEA
-#   Copyright 2020-2021 INRAE
-#
-#   Licensed under the Apache License, Version 2.0 (the "License");
-#   you may not use this file except in compliance with the License.
-#   You may obtain a copy of the License at
-#
-#          http://www.apache.org/licenses/LICENSE-2.0.txt
-#
-#   Unless required by applicable law or agreed to in writing, software
-#   distributed under the License is distributed on an "AS IS" BASIS,
-#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#   See the License for the specific language governing permissions and
-#   limitations under the License.
-#
-# ==========================================================================*/
-"""
-This module contains a set of python functions to interact with geospatial data
-and TensorFlow models.
-Starting from OTBTF >= 3.0.0, tricks is only used as a backward compatible stub
-for TF 1.X versions.
-"""
-import tensorflow.compat.v1 as tf
-from deprecated import deprecated
-from otbtf.utils import gdal_open, read_as_np_arr as read_as_np_arr_from_gdal_ds
-tf.disable_v2_behavior()
-
-
-@deprecated(version="3.0.0", reason="Please use otbtf.read_image_as_np() instead")
-def read_image_as_np(filename, as_patches=False):
-    """
-    Read a patches-image as numpy array.
-    :param filename: File name of the patches-image
-    :param as_patches: True if the image must be read as patches
-    :return 4D numpy array [batch, h, w, c] (batch = 1 when as_patches is False)
-    """
-
-    # Open a GDAL dataset
-    gdal_ds = gdal_open(filename)
-
-    # Return patches
-    return read_as_np_arr_from_gdal_ds(gdal_ds=gdal_ds, as_patches=as_patches)
-
-
-@deprecated(version="3.0.0", reason="Please consider using TensorFlow >= 2 to build your nets")
-def create_savedmodel(sess, inputs, outputs, directory):
-    """
-    Create a SavedModel from TF 1.X graphs
-    :param sess: The Tensorflow V1 session
-    :param inputs: List of inputs names (e.g. ["x_cnn_1:0", "x_cnn_2:0"])
-    :param outputs: List of outputs names (e.g. ["prediction:0", "features:0"])
-    :param directory: Path for the generated SavedModel
-    """
-    print("Create a SavedModel in " + directory)
-    graph = tf.compat.v1.get_default_graph()
-    inputs_names = {i: graph.get_tensor_by_name(i) for i in inputs}
-    outputs_names = {o: graph.get_tensor_by_name(o) for o in outputs}
-    tf.compat.v1.saved_model.simple_save(sess, directory, inputs=inputs_names, outputs=outputs_names)
-
-
-@deprecated(version="3.0.0", reason="Please consider using TensorFlow >= 2 to build and save your nets")
-def ckpt_to_savedmodel(ckpt_path, inputs, outputs, savedmodel_path, clear_devices=False):
-    """
-    Read a Checkpoint and build a SavedModel for some TF 1.X graph
-    :param ckpt_path: Path to the checkpoint file (without the ".meta" extension)
-    :param inputs: List of inputs names (e.g. ["x_cnn_1:0", "x_cnn_2:0"])
-    :param outputs: List of outputs names (e.g. ["prediction:0", "features:0"])
-    :param savedmodel_path: Path for the generated SavedModel
-    :param clear_devices: Clear TensorFlow devices positioning (True/False)
-    """
-    tf.compat.v1.reset_default_graph()
-    with tf.compat.v1.Session() as sess:
-        # Restore variables from disk
-        model_saver = tf.compat.v1.train.import_meta_graph(ckpt_path + ".meta", clear_devices=clear_devices)
-        model_saver.restore(sess, ckpt_path)
-
-        # Create a SavedModel
-        create_savedmodel(sess, inputs=inputs, outputs=outputs, directory=savedmodel_path)
-
-
-@deprecated(version="3.0.0", reason="Please use otbtf.read_image_as_np() instead")
-def read_samples(filename):
-    """
-   Read a patches image.
-   @param filename: raster file name
-   """
-    return read_image_as_np(filename, as_patches=True)
-
-
-# Aliases for backward compatibility
-# pylint: disable=invalid-name
-CreateSavedModel = create_savedmodel
-CheckpointToSavedModel = ckpt_to_savedmodel
diff --git a/tricks/ckpt2savedmodel.py b/tricks/ckpt2savedmodel.py
deleted file mode 100755
index ff22965f985623b318292790717ed32fa4e04536..0000000000000000000000000000000000000000
--- a/tricks/ckpt2savedmodel.py
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-# ==========================================================================
-#
-#   Copyright 2018-2019 IRSTEA
-#   Copyright 2020-2021 INRAE
-#
-#   Licensed under the Apache License, Version 2.0 (the "License");
-#   you may not use this file except in compliance with the License.
-#   You may obtain a copy of the License at
-#
-#          http://www.apache.org/licenses/LICENSE-2.0.txt
-#
-#   Unless required by applicable law or agreed to in writing, software
-#   distributed under the License is distributed on an "AS IS" BASIS,
-#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#   See the License for the specific language governing permissions and
-#   limitations under the License.
-#
-# ==========================================================================*/
-"""
-This application converts a checkpoint into a SavedModel, that can be used in
-TensorflowModelTrain or TensorflowModelServe OTB applications.
-This is intended to work mostly with tf.v1 models, since the models in tf.v2
-can be more conveniently exported as SavedModel (see how to build a model with
-keras in Tensorflow 2).
-"""
-import argparse
-from tricks.tricks import ckpt_to_savedmodel
-
-
-def main():
-    """
-    Main function
-    """
-    parser = argparse.ArgumentParser()
-    parser.add_argument("--ckpt", help="Checkpoint file (without the \".meta\" extension)", required=True)
-    parser.add_argument("--inputs", help="Inputs names (e.g. [\"x_cnn_1:0\", \"x_cnn_2:0\"])", required=True, nargs='+')
-    parser.add_argument("--outputs", help="Outputs names (e.g. [\"prediction:0\", \"features:0\"])", required=True,
-                        nargs='+')
-    parser.add_argument("--model", help="Output directory for SavedModel", required=True)
-    parser.add_argument('--clear_devices', dest='clear_devices', action='store_true')
-    parser.set_defaults(clear_devices=False)
-    params = parser.parse_args()
-
-    ckpt_to_savedmodel(ckpt_path=params.ckpt,
-                       inputs=params.inputs,
-                       outputs=params.outputs,
-                       savedmodel_path=params.model,
-                       clear_devices=params.clear_devices)
-
-
-if __name__ == "__main__":
-    main()