diff --git a/source/App/DecoderApp/DecApp.cpp b/source/App/DecoderApp/DecApp.cpp
index 81882dcf1..824c804fe 100644
--- a/source/App/DecoderApp/DecApp.cpp
+++ b/source/App/DecoderApp/DecApp.cpp
@@ -600,6 +600,33 @@ uint32_t DecApp::decode()
                                                             layerOutputBitDepth, bitDepths);   // write mode
           }
         }
+
+#if JVET_AH0204_LENS_CORRECTION_SEI
+        if (!m_seiLoc.filename.empty() && !m_cVideoIOYuvSEILocFile[nalu.m_nuhLayerId].isOpen())
+        {
+          std::string filename = m_seiLoc.filename;
+          if (filename.compare("/dev/null") && m_cDecLib.getVPS() != nullptr && m_cDecLib.getVPS()->getMaxLayers() > 1
+              && xIsNaluWithinTargetOutputLayerIdSet(&nalu))
+          {
+            size_t pos = filename.find_last_of('.');
+            if (pos != std::string::npos)
+            {
+              filename.insert(pos, std::to_string(nalu.m_nuhLayerId));
+            }
+            else
+            {
+              filename.append(std::to_string(nalu.m_nuhLayerId));
+            }
+          }
+          if ((m_cDecLib.getVPS() != nullptr
+               && (m_cDecLib.getVPS()->getMaxLayers() == 1 || xIsNaluWithinTargetOutputLayerIdSet(&nalu)))
+              || m_cDecLib.getVPS() == nullptr)
+          {
+            m_cVideoIOYuvSEILocFile[nalu.m_nuhLayerId].open(filename, true, layerOutputBitDepth, layerOutputBitDepth,
+                                                            bitDepths);   // write mode
+          }
+        }
+#endif
       }
       if (!m_annotatedRegionsSEIFileName.empty())
       {
@@ -1039,6 +1066,16 @@ void DecApp::xDestroyDecLib()
     }
   }
 
+#if JVET_AH0204_LENS_CORRECTION_SEI
+  if (!m_seiLoc.filename.empty())
+  {
+    for (auto& recFile: m_cVideoIOYuvSEILocFile)
+    {
+      recFile.second.close();
+    }
+  }
+#endif
+
   // destroy decoder class
   m_cDecLib.destroy();
 }
@@ -1305,6 +1342,35 @@ void DecApp::xWriteOutput( PicList* pcListPic, uint32_t tId )
               m_clipOutputVideoToRec709Range);
           }
         }
+
+#if JVET_AH0204_LENS_CORRECTION_SEI
+        // Perform lens correction on decoded frame and write to corresponding output file
+        if (!m_seiLoc.filename.empty())
+        {
+          const Window& conf            = pcPic->getConformanceWindow();
+          const SPS*    sps             = pcPic->cs->sps;
+          ChromaFormat  chromaFormatIdc = sps->getChromaFormatIdc();
+          if (m_upscaledOutput)
+          {
+            m_cVideoIOYuvSEILocFile[pcPic->layerId].writeUpscaledPicture(
+              *sps, *pcPic->cs->pps, pcPic->getDisplayBufWithLensOpticalCorrection(), m_outputColourSpaceConvert,
+              m_packedYUVMode, m_upscaledOutput, ChromaFormat::UNDEFINED, m_clipOutputVideoToRec709Range,
+              m_upscaleFilterForDisplay);
+          }
+          else
+          {
+            m_cVideoIOYuvSEILocFile[pcPic->layerId].write(
+              pcPic->getRecoBuf().get(COMPONENT_Y).width, pcPic->getRecoBuf().get(COMPONENT_Y).height,
+              pcPic->getDisplayBufWithLensOpticalCorrection(), m_outputColourSpaceConvert, m_packedYUVMode,
+              conf.getWindowLeftOffset() * SPS::getWinUnitX(chromaFormatIdc),
+              conf.getWindowRightOffset() * SPS::getWinUnitX(chromaFormatIdc),
+              conf.getWindowTopOffset() * SPS::getWinUnitY(chromaFormatIdc),
+              conf.getWindowBottomOffset() * SPS::getWinUnitY(chromaFormatIdc), ChromaFormat::UNDEFINED,
+              m_clipOutputVideoToRec709Range);
+          }
+        }
+#endif
+
         writeLineToOutputLog(pcPic);
 #if JVET_AF0088_OMI_SEI
         if (!m_objectMaskInfoSEIFileName.empty())
@@ -1546,6 +1612,35 @@ void DecApp::xFlushOutput( PicList* pcListPic, const int layerId )
                 m_clipOutputVideoToRec709Range);
             }
           }
+
+#if JVET_AH0204_LENS_CORRECTION_SEI
+          // Perform lens correction on decoded frame and write to corresponding output file
+          if (!m_seiLoc.filename.empty())
+          {
+            const Window& conf            = pcPic->getConformanceWindow();
+            const SPS*    sps             = pcPic->cs->sps;
+            ChromaFormat  chromaFormatIdc = sps->getChromaFormatIdc();
+            if (m_upscaledOutput)
+            {
+              m_cVideoIOYuvSEILocFile[pcPic->layerId].writeUpscaledPicture(
+                *sps, *pcPic->cs->pps, pcPic->getDisplayBufWithLensOpticalCorrection(), m_outputColourSpaceConvert,
+                m_packedYUVMode, m_upscaledOutput, ChromaFormat::UNDEFINED, m_clipOutputVideoToRec709Range,
+                m_upscaleFilterForDisplay);
+            }
+            else
+            {
+              m_cVideoIOYuvSEILocFile[pcPic->layerId].write(
+                pcPic->getRecoBuf().get(COMPONENT_Y).width, pcPic->getRecoBuf().get(COMPONENT_Y).height,
+                pcPic->getDisplayBufWithLensOpticalCorrection(), m_outputColourSpaceConvert, m_packedYUVMode,
+                conf.getWindowLeftOffset() * SPS::getWinUnitX(chromaFormatIdc),
+                conf.getWindowRightOffset() * SPS::getWinUnitX(chromaFormatIdc),
+                conf.getWindowTopOffset() * SPS::getWinUnitY(chromaFormatIdc),
+                conf.getWindowBottomOffset() * SPS::getWinUnitY(chromaFormatIdc), ChromaFormat::UNDEFINED,
+                m_clipOutputVideoToRec709Range);
+            }
+          }
+#endif
+
           writeLineToOutputLog(pcPic);
 #if JVET_AF0088_OMI_SEI
           if (!m_objectMaskInfoSEIFileName.empty())
diff --git a/source/App/DecoderApp/DecApp.h b/source/App/DecoderApp/DecApp.h
index 68be62618..8f02fffaf 100644
--- a/source/App/DecoderApp/DecApp.h
+++ b/source/App/DecoderApp/DecApp.h
@@ -63,6 +63,9 @@ private:
   std::unordered_map<int, VideoIOYuv>      m_cVideoIOYuvReconFile;        ///< reconstruction YUV class
   std::unordered_map<int, VideoIOYuv>      m_videoIOYuvSEIFGSFile;       ///< reconstruction YUV with FGS class
   std::unordered_map<int, VideoIOYuv>      m_cVideoIOYuvSEICTIFile;       ///< reconstruction YUV with CTI class
+#if JVET_AH0204_LENS_CORRECTION_SEI
+  std::unordered_map<int, VideoIOYuv> m_cVideoIOYuvSEILocFile;   // reconstructed YUV with Lens Optical Correction
+#endif
 
   bool                                    m_ShutterFilterEnable;          ///< enable Post-processing with Shutter Interval SEI
   VideoIOYuv                              m_cTVideoIOYuvSIIPostFile;      ///< post-filtered YUV class
diff --git a/source/App/DecoderApp/DecAppCfg.cpp b/source/App/DecoderApp/DecAppCfg.cpp
index f18ca29b1..91521bcd8 100644
--- a/source/App/DecoderApp/DecAppCfg.cpp
+++ b/source/App/DecoderApp/DecAppCfg.cpp
@@ -139,6 +139,12 @@ bool DecAppCfg::parseCfg( int argc, char* argv[] )
   ("RandomAccessPos",           m_gdrPocRandomAccess,                  0,          "POC of GDR Random access picture\n")
 #endif // GDR_LEAK_TEST
     ;
+
+#if JVET_AH0204_LENS_CORRECTION_SEI
+  opts.addOptions()
+  ("SEILocFilename", m_seiLoc.filename, std::string(""), "Output filename for output with lens optical correction")
+  ;
+#endif
   // clang-format on
 
   po::setDefaults(opts);
diff --git a/source/App/DecoderApp/DecAppCfg.h b/source/App/DecoderApp/DecAppCfg.h
index 9a4171e9e..6bc125312 100644
--- a/source/App/DecoderApp/DecAppCfg.h
+++ b/source/App/DecoderApp/DecAppCfg.h
@@ -107,6 +107,12 @@ protected:
 #if GDR_LEAK_TEST
   int           m_gdrPocRandomAccess;                   ///<
 #endif // GDR_LEAK_TEST
+#if JVET_AH0204_LENS_CORRECTION_SEI
+  struct
+  {
+    std::string filename;
+  } m_seiLoc;
+#endif
 public:
   DecAppCfg();
   virtual ~DecAppCfg();
diff --git a/source/App/EncoderApp/EncApp.cpp b/source/App/EncoderApp/EncApp.cpp
index 5e517f4e1..234d6b808 100644
--- a/source/App/EncoderApp/EncApp.cpp
+++ b/source/App/EncoderApp/EncApp.cpp
@@ -1411,7 +1411,34 @@ void EncApp::xInitLibCfg( int layerIdx )
   m_cEncLib.setPoSEIProcessingOrder                              (m_poSEIProcessingOrder);
   m_cEncLib.setPoSEIPrefixByte                                   (m_poSEIPrefixByte);
 
+#if JVET_AH0204_LENS_CORRECTION_SEI
+  m_cEncLib.setLocSEIEnabled(m_seiLoc.enabled);
+  if (m_seiLoc.enabled)
+  {
+    m_cEncLib.setLocSEIPersistence(m_seiLoc.persistence);
+    m_cEncLib.setLocSEIFocalCenter(m_seiLoc.focalCenter);
+    m_cEncLib.setLocSEIFocalLength(m_seiLoc.focalLength);
+
+    if (m_seiLoc.rdModel == SEILensOpticalCorrection::RD_POLY3)
+    {
+      m_cEncLib.setLocSEIRdModelPoly3(m_seiLoc.rdPoly3k1);
+    }
+    else if (m_seiLoc.rdModel == SEILensOpticalCorrection::RD_POLY5)
+    {
+      m_cEncLib.setLocSEIRdModelPoly5(m_seiLoc.rdPoly5k1, m_seiLoc.rdPoly5k2);
+    }
 
+    if (m_seiLoc.tcaModel == SEILensOpticalCorrection::TCA_LINEAR)
+    {
+      m_cEncLib.setLocSEITcaModelLinear(m_seiLoc.tcaLinearKb, m_seiLoc.tcaLinearKr);
+    }
+    else if (m_seiLoc.tcaModel == SEILensOpticalCorrection::TCA_POLY3)
+    {
+      m_cEncLib.setLocSEITcaModelPoly3(m_seiLoc.tcaPoly3Br, m_seiLoc.tcaPoly3Cr, m_seiLoc.tcaPoly3Vr,
+                                       m_seiLoc.tcaPoly3Bb, m_seiLoc.tcaPoly3Cb, m_seiLoc.tcaPoly3Vb);
+    }
+  }
+#endif
 
   m_cEncLib.setPostFilterHintSEIEnabled(m_postFilterHintSEIEnabled);
   m_cEncLib.setPostFilterHintSEICancelFlag(m_postFilterHintSEICancelFlag);
diff --git a/source/App/EncoderApp/EncAppCfg.cpp b/source/App/EncoderApp/EncAppCfg.cpp
index f745f901a..69964e827 100644
--- a/source/App/EncoderApp/EncAppCfg.cpp
+++ b/source/App/EncoderApp/EncAppCfg.cpp
@@ -51,6 +51,9 @@
 
 #include "CommonLib/dtrace_next.h"
 #include "CommonLib/ProfileTierLevel.h"
+#if JVET_AH0204_LENS_CORRECTION_SEI
+#include "CommonLib/SEI.h"
+#endif
 
 #define MACRO_TO_STRING_HELPER(val) #val
 #define MACRO_TO_STRING(val) MACRO_TO_STRING_HELPER(val)
@@ -1757,6 +1760,31 @@ bool EncAppCfg::parseCfg( int argc, char* argv[] )
   ( "RPLofDepLayerInSH",                              m_rplOfDepLayerInSh,                      false, "define Reference picture lists in slice header instead of SPS for dependant layers")
     ;
 
+#if JVET_AH0204_LENS_CORRECTION_SEI
+  opts.addOptions()
+  ("SEILocEnable",       m_seiLoc.enabled,        0, "Enable Lens Optical Correction SEI")
+  ("SEILocPersistence",  m_seiLoc.persistence,    1, "Set Lens Optical Correction SEI persistence")
+  ("SEILocFocalCenterX", m_seiLoc.focalCenter.x, -1, "Focal center x position")
+  ("SEILocFocalCenterY", m_seiLoc.focalCenter.y, -1, "Focal center y position")
+  ("SEILocFocalLength",  m_seiLoc.focalLength,  0.0, "Focal length (in mm)")
+  ("SEILocRdModel",      m_seiLoc.rdModel,
+             (int)SEILensOpticalCorrection::RD_NONE, "Radial distortion model: -1 = None (default), 0 = Poly3, 1 = Poly5")
+  ("SEILocRdPoly3k1",    m_seiLoc.rdPoly3k1,    0.0, "k1 parameter for Poly3 radial distortion model")
+  ("SEILocRdPoly5k1",    m_seiLoc.rdPoly5k1,    0.0, "k1 parameter for Poly5 radial distortion model")
+  ("SEILocRdPoly5k2",    m_seiLoc.rdPoly5k2,    0.0, "k2 parameter for Poly5 radial distortion model")
+  ("SEILocTcaModel",     m_seiLoc.tcaModel,
+            (int)SEILensOpticalCorrection::TCA_NONE, "Transversal chromatic abberation model: -1 = None (default), 0 = Linear, 1 = Poly3")
+  ("SEILocTcaLinearKr",  m_seiLoc.tcaLinearKr,  1.0, "kr parameter for Linear TCA model")
+  ("SEILocTcaLinearKb",  m_seiLoc.tcaLinearKb,  1.0, "kb parameter for Linear TCA model")
+  ("SEILocTcaPoly3Br",   m_seiLoc.tcaPoly3Br,   0.0, "br parameter for Poly3 TCA model")
+  ("SEILocTcaPoly3Cr",   m_seiLoc.tcaPoly3Cr,   0.0, "cr parameter for Poly3 TCA model")
+  ("SEILocTcaPoly3Vr",   m_seiLoc.tcaPoly3Vr,   1.0, "vr parameter for Poly3 TCA model")
+  ("SEILocTcaPoly3Bb",   m_seiLoc.tcaPoly3Bb,   0.0, "bb parameter for Poly3 TCA model")
+  ("SEILocTcaPoly3Cb",   m_seiLoc.tcaPoly3Cb,   0.0, "cb parameter for Poly3 TCA model")
+  ("SEILocTcaPoly3Vb",   m_seiLoc.tcaPoly3Vb,   1.0, "vb parameter for Poly3 TCA model")
+  ;
+#endif
+
   opts.addOptions()
     ("TemporalFilter",               m_gopBasedTemporalFilterEnabled,                     false, "Enable GOP based temporal filter. Disabled per default")
     ("TemporalFilterPastRefs",       m_gopBasedTemporalFilterPastRefs,          TF_DEFAULT_REFS, "Number of past references for temporal prefilter")
@@ -3546,6 +3574,48 @@ bool EncAppCfg::parseCfg( int argc, char* argv[] )
       m_driSEINonlinearModel[i]   = cfg_driSEINonlinearModel.values.size() > i ? cfg_driSEINonlinearModel.values[i] : 0;
     }
   }
+#if JVET_AH0204_LENS_CORRECTION_SEI
+  if (m_seiLoc.enabled)
+  {
+    CHECK(m_seiLoc.focalCenter.x < -1 || m_seiLoc.focalCenter.x >= m_sourceWidth, "SEILocFocalCenterX is out of range");
+    CHECK(m_seiLoc.focalCenter.y < -1 || m_seiLoc.focalCenter.y >= m_sourceHeight,
+          "SEILocFocalCenterY is out of range");
+
+    CHECK(m_seiLoc.focalLength < 0.0 || m_seiLoc.focalLength > 6553.5, "SEILocFocalLength is out of range");
+
+    CHECK(m_seiLoc.rdModel < SEILensOpticalCorrection::RD_NONE || m_seiLoc.rdModel > SEILensOpticalCorrection::RD_POLY5,
+          "SEILocRdModel is out of range");
+
+    if (m_seiLoc.rdModel == SEILensOpticalCorrection::RD_POLY3)
+    {
+      CHECK(m_seiLoc.rdPoly3k1 < -1.0 || m_seiLoc.rdPoly3k1 >= 1.0, "SEILocRdPoly3k1 is out of range");
+    }
+    else if (m_seiLoc.rdModel == SEILensOpticalCorrection::RD_POLY5)
+    {
+      CHECK(m_seiLoc.rdPoly5k1 < -1.0 || m_seiLoc.rdPoly5k1 >= 1.0, "SEILocRdPoly5k1 is out of range");
+      CHECK(m_seiLoc.rdPoly5k2 < -1.0 || m_seiLoc.rdPoly5k2 >= 1.0, "SEILocRdPoly5k2 is out of range");
+    }
+
+    CHECK(m_seiLoc.tcaModel < SEILensOpticalCorrection::TCA_NONE
+            || m_seiLoc.tcaModel > SEILensOpticalCorrection::TCA_POLY3,
+          "SEILocTcaModel is out of range");
+
+    if (m_seiLoc.tcaModel == SEILensOpticalCorrection::TCA_LINEAR)
+    {
+      CHECK(m_seiLoc.tcaLinearKb < 0.0 || m_seiLoc.tcaLinearKb >= 2.0, "SEILocTcaLinearKb is out of range");
+      CHECK(m_seiLoc.tcaLinearKr < 0.0 || m_seiLoc.tcaLinearKr >= 2.0, "SEILocTcaLinearKr is out of range");
+    }
+    else if (m_seiLoc.tcaModel == SEILensOpticalCorrection::TCA_POLY3)
+    {
+      CHECK(m_seiLoc.tcaPoly3Bb < -1.0 || m_seiLoc.tcaPoly3Bb >= 1.0, "SEILocTcaPoly3Bb is out of range");
+      CHECK(m_seiLoc.tcaPoly3Cb < -1.0 || m_seiLoc.tcaPoly3Cb >= 1.0, "SEILocTcaPoly3Cb is out of range");
+      CHECK(m_seiLoc.tcaPoly3Vb < 0.0 || m_seiLoc.tcaPoly3Vb >= 2.0, "SEILocTcaPoly3Vb is out of range");
+      CHECK(m_seiLoc.tcaPoly3Br < -1.0 || m_seiLoc.tcaPoly3Br >= 1.0, "SEILocTcaPoly3Br is out of range");
+      CHECK(m_seiLoc.tcaPoly3Cr < -1.0 || m_seiLoc.tcaPoly3Cr >= 1.0, "SEILocTcaPoly3Cr is out of range");
+      CHECK(m_seiLoc.tcaPoly3Vr < 0.0 || m_seiLoc.tcaPoly3Vr >= 2.0, "SEILocTcaPoly3Vr is out of range");
+    }
+  }
+#endif
   m_reshapeCW.binCW.resize(3);
   m_reshapeCW.rspFps     = m_frameRate.getIntValRound();
   m_reshapeCW.rspPicSize = m_sourceWidth*m_sourceHeight;
diff --git a/source/App/EncoderApp/EncAppCfg.h b/source/App/EncoderApp/EncAppCfg.h
index 066c70873..4a7bfed97 100644
--- a/source/App/EncoderApp/EncAppCfg.h
+++ b/source/App/EncoderApp/EncAppCfg.h
@@ -956,6 +956,29 @@ protected:
   bool     m_xsdMetricTypeWPSNR;
   bool     m_xsdMetricTypeWSPSNR;
 #endif
+
+#if JVET_AH0204_LENS_CORRECTION_SEI
+  struct
+  {
+    int      enabled;
+    int      persistence;
+    Position focalCenter;
+    double   focalLength;
+    int      rdModel;
+    double   rdPoly3k1;
+    double   rdPoly5k1;
+    double   rdPoly5k2;
+    int      tcaModel;
+    double   tcaLinearKb;
+    double   tcaLinearKr;
+    double   tcaPoly3Br;
+    double   tcaPoly3Cr;
+    double   tcaPoly3Vr;
+    double   tcaPoly3Bb;
+    double   tcaPoly3Cb;
+    double   tcaPoly3Vb;
+  } m_seiLoc;
+#endif
   std::string m_summaryOutFilename;                           ///< filename to use for producing summary output file.
   std::string m_summaryPicFilenameBase;                       ///< Base filename to use for producing summary picture output files. The actual filenames used will have I.txt, P.txt and B.txt appended.
   uint32_t        m_summaryVerboseness;                           ///< Specifies the level of the verboseness of the text output.
diff --git a/source/Lib/CommonLib/Picture.cpp b/source/Lib/CommonLib/Picture.cpp
index 21cbfc877..d37ba55b6 100644
--- a/source/Lib/CommonLib/Picture.cpp
+++ b/source/Lib/CommonLib/Picture.cpp
@@ -1520,6 +1520,49 @@ PelUnitBuf Picture::getDisplayBuf()
   return *m_invColourTransfBuf;
 }
 
+#if JVET_AH0204_LENS_CORRECTION_SEI
+void Picture::createLensOpticalCorrectionProcessor(bool firstPictureInSequence, LensOpticalCorrection* locProc,
+                                                   PelStorage* locBuf, int width, int height, ChromaFormat cf,
+                                                   int bitDepth, const VUI* vui)
+{
+  m_locSei.proc = locProc;
+  m_locSei.buf  = locBuf;
+
+  if (firstPictureInSequence)
+  {
+    // Create and initialize the Colour Transform Processor
+    m_locSei.proc->create(width, height, cf, bitDepth, vui);
+
+    // Frame level PelStorage buffer created to apply the Colour Transform
+    m_locSei.buf->create(UnitArea(chromaFormat, Area(0, 0, width, height)));
+  }
+}
+
+PelUnitBuf Picture::getDisplayBufWithLensOpticalCorrection()
+{
+  auto seiIter =
+    std::find_if(SEIs.begin(), SEIs.end(),
+                 [](const SEI* sei) { return sei->payloadType() == SEI::PayloadType::LENS_OPTICAL_CORRECTION; });
+
+  const bool foundLocSei = seiIter != SEIs.end();
+  if (foundLocSei)
+  {
+    SEI* sei           = *seiIter;
+    m_locSei.proc->sei = *reinterpret_cast<SEILensOpticalCorrection*>(sei);
+  }
+
+  if (!m_locSei.proc->sei.cancelFlag && (foundLocSei || m_locSei.proc->sei.persistenceFlag))
+  {
+    m_locSei.proc->process(getRecoBuf(), *m_locSei.buf);
+
+    return *m_locSei.buf;
+  }
+  else
+  {
+    return getRecoBuf();
+  }
+}
+#endif
 
 void Picture::copyToPic(const SPS *sps, PelStorage *pcPicYuvSrc, PelStorage *pcPicYuvDst)
 {
diff --git a/source/Lib/CommonLib/Picture.h b/source/Lib/CommonLib/Picture.h
index b988f97ba..087ca14d0 100644
--- a/source/Lib/CommonLib/Picture.h
+++ b/source/Lib/CommonLib/Picture.h
@@ -51,6 +51,9 @@
 #include "SEIColourTransform.h"
 #include <deque>
 #include "SEIFilmGrainSynthesizer.h"
+#if JVET_AH0204_LENS_CORRECTION_SEI
+#include "LensOpticalCorrection.h"
+#endif
 
 class SEI;
 class AQpLayer;
@@ -94,6 +97,19 @@ struct Picture : public UnitArea
   void              createColourTransfProcessor(bool firstPictureInSequence, SEIColourTransformApply* ctiCharacteristics, PelStorage* ctiBuf, int width, int height, ChromaFormat fmt, int bitDepth);
   PelUnitBuf        getDisplayBuf();
 
+#if JVET_AH0204_LENS_CORRECTION_SEI
+  struct
+  {
+    PelStorage*            buf;
+    LensOpticalCorrection* proc;
+  } m_locSei;
+
+  void       createLensOpticalCorrectionProcessor(bool firstPictureInSequence, LensOpticalCorrection* locProc,
+                                                  PelStorage* locBuf, int width, int height, ChromaFormat fmt, int bitDepth,
+                                                  const VUI* vui);
+  PelUnitBuf getDisplayBufWithLensOpticalCorrection();
+#endif
+
   SEIMessages m_nnpfcActivated;
 
   void copyToPic(const SPS *sps, PelStorage *pcPicYuvSrc, PelStorage *pcPicYuvDst);
diff --git a/source/Lib/CommonLib/SEI.h b/source/Lib/CommonLib/SEI.h
index 5b14686fa..9eccadb41 100644
--- a/source/Lib/CommonLib/SEI.h
+++ b/source/Lib/CommonLib/SEI.h
@@ -40,6 +40,9 @@
 #include <cstring>
 
 #include "CommonDef.h"
+#if JVET_AH0204_LENS_CORRECTION_SEI
+#include "Common.h"
+#endif
 #include "libmd5/MD5.h"
 
 //! \ingroup CommonLib
@@ -116,6 +119,9 @@ public:
 #endif
 #if JVET_AF0167_MULTI_PLANE_IMAGE_INFO_SEI
     MULTIPLANE_IMAGE_INFO, // payload_type value TBD
+#endif
+#if JVET_AH0204_LENS_CORRECTION_SEI
+    LENS_OPTICAL_CORRECTION,   // payload_type value TBD
 #endif
   };
 
@@ -1478,6 +1484,78 @@ public:
   std::vector<int> m_filterHintValues;   // values stored in linear array, [ ( ( component * sizeY + y ) * SizeX ) + x ]
 };
 
+#if JVET_AH0204_LENS_CORRECTION_SEI
+class SEILensOpticalCorrection : public SEI
+{
+public:
+  PayloadType payloadType() const { return PayloadType::LENS_OPTICAL_CORRECTION; }
+
+  SEILensOpticalCorrection()                                    = default;
+  SEILensOpticalCorrection(const SEILensOpticalCorrection& sei) = default;
+  virtual ~SEILensOpticalCorrection() {}
+
+  struct FocalParameters
+  {
+    Position center;
+    uint16_t focalLength;
+  };
+
+  // All parameters use a .24 fixed-point representation
+  static constexpr int FRAC_BITS = 24;
+
+  enum
+  {
+    RD_NONE  = -1,
+    RD_POLY3 = 0,
+    RD_POLY5 = 1,
+  };
+
+  struct RdPoly3
+  {
+    int32_t k1;
+  };
+
+  struct RdPoly5
+  {
+    int32_t k1;
+    int32_t k2;
+  };
+
+  enum
+  {
+    TCA_NONE   = -1,
+    TCA_LINEAR = 0,
+    TCA_POLY3  = 1,
+  };
+
+  struct TcaLinear
+  {
+    int32_t kr;
+    int32_t kb;
+  };
+
+  struct TcaPoly3
+  {
+    int32_t br;
+    int32_t cr;
+    int32_t vr;
+    int32_t bb;
+    int32_t cb;
+    int32_t vb;
+  };
+
+  bool cancelFlag;
+  bool persistenceFlag;
+
+  std::optional<FocalParameters>                    focalParams;
+  std::variant<std::monostate, RdPoly3, RdPoly5>    rdParams;    // radial distortion
+  std::variant<std::monostate, TcaLinear, TcaPoly3> tcaParams;   // transversal chromatic aberration
+
+  bool hasRdParams() const { return !std::holds_alternative<std::monostate>(rdParams); }
+  bool hasTcaParams() const { return !std::holds_alternative<std::monostate>(tcaParams); }
+};
+#endif
+
 SEINeuralNetworkPostFilterCharacteristics* getNnpfcWithGivenId(const SEIMessages &seiList, uint32_t nnpfaTargetId);
 SEINeuralNetworkPostFilterCharacteristics* getSuperResolutionNnpfc(const SEIMessages &seiList);
 //! \}
diff --git a/source/Lib/CommonLib/TypeDef.h b/source/Lib/CommonLib/TypeDef.h
index 804001822..fde64d352 100644
--- a/source/Lib/CommonLib/TypeDef.h
+++ b/source/Lib/CommonLib/TypeDef.h
@@ -78,6 +78,7 @@
 
 #define JVET_AG0045_AI_MARKER_SEI                         1
 #define JVET_AF0141_IMAGE_METADATA_SEIS                   1
+#define JVET_AH0204_LENS_CORRECTION_SEI 1
 
 //########### place macros to be be kept below this line ###############
 
diff --git a/source/Lib/DecoderLib/DecLib.cpp b/source/Lib/DecoderLib/DecLib.cpp
index 25c199246..4752a950d 100644
--- a/source/Lib/DecoderLib/DecLib.cpp
+++ b/source/Lib/DecoderLib/DecLib.cpp
@@ -2098,6 +2098,13 @@ void DecLib::xActivateParameterSets( const InputNALUnit nalu )
     m_pcPic->createColourTransfProcessor(m_firstPictureInSequence, &m_colourTranfParams, &m_invColourTransfBuf,
                                          pps->getPicWidthInLumaSamples(), pps->getPicHeightInLumaSamples(),
                                          sps->getChromaFormatIdc(), sps->getBitDepth(ChannelType::LUMA));
+#if JVET_AH0204_LENS_CORRECTION_SEI
+    m_pcPic->createLensOpticalCorrectionProcessor(m_firstPictureInSequence, &m_locSei.proc, &m_locSei.buf,
+                                                  pps->getPicWidthInLumaSamples(), pps->getPicHeightInLumaSamples(),
+                                                  sps->getChromaFormatIdc(), sps->getBitDepth(ChannelType::LUMA),
+                                                  sps->getVuiParameters());
+#endif
+
     m_firstPictureInSequence = false;
     m_pcPic->createTempBuffers( m_pcPic->cs->pps->pcv->maxCUWidth, false, false, true, false );
     m_pcPic->cs->createTemporaryCsData((bool)m_pcPic->cs->sps->getPLTMode());
diff --git a/source/Lib/DecoderLib/DecLib.h b/source/Lib/DecoderLib/DecLib.h
index 53ca18d26..79361a911 100644
--- a/source/Lib/DecoderLib/DecLib.h
+++ b/source/Lib/DecoderLib/DecLib.h
@@ -142,6 +142,14 @@ private:
   PelStorage              m_grainBuf;
   SEIColourTransformApply m_colourTranfParams;
   PelStorage              m_invColourTransfBuf;
+#if JVET_AH0204_LENS_CORRECTION_SEI
+  struct
+  {
+    LensOpticalCorrection proc;
+    PelStorage            buf;
+  } m_locSei;
+#endif
+
   bool                    m_firstSliceInSequence[MAX_VPS_LAYERS];
   bool                    m_firstSliceInBitstream;
   bool                    m_isFirstAuInCvs;
diff --git a/source/Lib/DecoderLib/SEIread.cpp b/source/Lib/DecoderLib/SEIread.cpp
index b5a94cd3e..8a4519b44 100644
--- a/source/Lib/DecoderLib/SEIread.cpp
+++ b/source/Lib/DecoderLib/SEIread.cpp
@@ -570,6 +570,12 @@ bool SEIReader::xReadSEImessage(SEIMessages& seis, const NalUnitType nalUnitType
       sei = new SEIXmpMetadata();
       xParseSEIXmpMetadata((SEIXmpMetadata &) *sei, payloadSize, pDecodedMessageOutputStream);
       break;
+#endif
+#if JVET_AH0204_LENS_CORRECTION_SEI
+    case SEI::PayloadType::LENS_OPTICAL_CORRECTION:
+      sei = new SEILensOpticalCorrection;
+      xParseSEILensCorrection((SEILensOpticalCorrection&) *sei, payloadSize, pDecodedMessageOutputStream);
+      break;
 #endif
     default:
       for (uint32_t i = 0; i < payloadSize; i++)
@@ -3575,6 +3581,120 @@ void SEIReader::xParseSEIPostFilterHint(SEIPostFilterHint &sei, uint32_t payload
   }
 }
 
+#if JVET_AH0204_LENS_CORRECTION_SEI
+void SEIReader::xParseSEILensCorrection(SEILensOpticalCorrection& sei, uint32_t payloadSize,
+                                        std::ostream* pDecodedMessageOutputStream)
+{
+  uint32_t val;
+  output_sei_message_header(sei, pDecodedMessageOutputStream, payloadSize);
+
+  sei_read_flag(pDecodedMessageOutputStream, val, "loc_cancel_flag");
+  sei.cancelFlag = val != 0;
+
+  if (!sei.cancelFlag)
+  {
+    sei_read_flag(pDecodedMessageOutputStream, val, "loc_focal_parameters_present_flag");
+
+    if (val != 0)
+    {
+      SEILensOpticalCorrection::FocalParameters focalParams;
+
+      sei_read_code(pDecodedMessageOutputStream, 16, val, "loc_focal_center_x");
+      focalParams.center.x = val;
+      sei_read_code(pDecodedMessageOutputStream, 16, val, "loc_focal_center_y");
+      focalParams.center.y = val;
+      sei_read_code(pDecodedMessageOutputStream, 16, val, "loc_focal_length");
+      focalParams.focalLength = val;
+
+      sei.focalParams = focalParams;
+    }
+
+    auto fromUnsignedCode = [](int32_t x)
+    {
+      const int msb = 1 << SEILensOpticalCorrection::FRAC_BITS;
+      return (x & (msb - 1)) - (x & msb);
+    };
+    const int numBits = SEILensOpticalCorrection::FRAC_BITS + 1;
+
+    sei_read_flag(pDecodedMessageOutputStream, val, "loc_radial_distortion_parameters_present_flag");
+
+    if (val != 0)
+    {
+      sei_read_code(pDecodedMessageOutputStream, 8, val, "loc_radial_distortion_model_id");
+      CHECK(val > SEILensOpticalCorrection::RD_POLY5, "Unknown value for loc_radial_distortion_model_id");
+
+      if (val == SEILensOpticalCorrection::RD_POLY3)
+      {
+          SEILensOpticalCorrection::RdPoly3 rdParams;
+
+          sei_read_code(pDecodedMessageOutputStream, numBits, val, "loc_rd_poly3_k1");
+          rdParams.k1 = fromUnsignedCode(val);
+
+          sei.rdParams = rdParams;
+      }
+      else if (val == SEILensOpticalCorrection::RD_POLY5)
+      {
+          SEILensOpticalCorrection::RdPoly5 rdParams;
+
+          sei_read_code(pDecodedMessageOutputStream, numBits, val, "loc_rd_poly5_k1");
+          rdParams.k1 = fromUnsignedCode(val);
+
+          sei_read_code(pDecodedMessageOutputStream, numBits, val, "loc_rd_poly5_k2");
+          rdParams.k2 = fromUnsignedCode(val);
+
+          sei.rdParams = rdParams;
+      }
+    }
+
+    sei_read_flag(pDecodedMessageOutputStream, val, "loc_tca_parameters_present_flag");
+    if (val != 0)
+    {
+      sei_read_code(pDecodedMessageOutputStream, 8, val, "loc_tca_model_id");
+      CHECK(val > SEILensOpticalCorrection::TCA_POLY3, "Unknown value for loc_tca_model_id");
+
+      if (val == SEILensOpticalCorrection::TCA_LINEAR)
+      {
+          SEILensOpticalCorrection::TcaLinear tcaParams;
+
+          sei_read_code(pDecodedMessageOutputStream, numBits, val, "loc_tca_kr");
+          tcaParams.kr = val;
+          sei_read_code(pDecodedMessageOutputStream, numBits, val, "loc_tca_kb");
+          tcaParams.kb = val;
+
+          sei.tcaParams = tcaParams;
+      }
+      else if (val == SEILensOpticalCorrection::TCA_POLY3)
+      {
+          SEILensOpticalCorrection::TcaPoly3 tcaParams;
+
+          sei_read_code(pDecodedMessageOutputStream, numBits, val, "loc_tca_br");
+          tcaParams.br = fromUnsignedCode(val);
+
+          sei_read_code(pDecodedMessageOutputStream, numBits, val, "loc_tca_cr");
+          tcaParams.cr = fromUnsignedCode(val);
+
+          sei_read_code(pDecodedMessageOutputStream, numBits, val, "loc_tca_vr");
+          tcaParams.vr = val;
+
+          sei_read_code(pDecodedMessageOutputStream, numBits, val, "loc_tca_bb");
+          tcaParams.bb = fromUnsignedCode(val);
+
+          sei_read_code(pDecodedMessageOutputStream, numBits, val, "loc_tca_cb");
+          tcaParams.cb = fromUnsignedCode(val);
+
+          sei_read_code(pDecodedMessageOutputStream, numBits, val, "loc_tca_vb");
+          tcaParams.vb = val;
+
+          sei.tcaParams = tcaParams;
+      }
+    }
+
+    sei_read_flag(pDecodedMessageOutputStream, val, "loc_persistence_flag");
+    sei.persistenceFlag = val != 0;
+  }
+}
+#endif
+
 #if JVET_S0257_DUMP_360SEI_MESSAGE
 void SeiCfgFileDump::write360SeiDump (std::string decoded360MessageFileName, SEIMessages& seis, const SPS* sps)
 {
diff --git a/source/Lib/DecoderLib/SEIread.h b/source/Lib/DecoderLib/SEIread.h
index 1123cc975..a35463cab 100644
--- a/source/Lib/DecoderLib/SEIread.h
+++ b/source/Lib/DecoderLib/SEIread.h
@@ -133,6 +133,10 @@ protected:
   void xParseSEIJfifMetadata (SEIJfifMetadata &sei, uint32_t payLoadSize, std::ostream *pDecodedMessageOutputStream);
   void xParseSEIXmpMetadata  (SEIXmpMetadata  &sei, uint32_t payLoadSize, std::ostream *pDecodedMessageOutputStream);
 #endif
+#if JVET_AH0204_LENS_CORRECTION_SEI
+  void xParseSEILensCorrection(SEILensOpticalCorrection& sei, uint32_t payloadSize,
+                               std::ostream* pDecodedMessageOutputStream);
+#endif
 
   void sei_read_scode(std::ostream *pOS, uint32_t length, int& code, const char *pSymbolName);
   void sei_read_code(std::ostream *pOS, uint32_t length, uint32_t &ruiCode, const char *pSymbolName);
diff --git a/source/Lib/EncoderLib/EncCfg.h b/source/Lib/EncoderLib/EncCfg.h
index 5c3a675e6..3342669a3 100644
--- a/source/Lib/EncoderLib/EncCfg.h
+++ b/source/Lib/EncoderLib/EncCfg.h
@@ -954,6 +954,32 @@ protected:
   std::vector<uint8_t> m_jfifSEIData;
 
 #endif
+#if JVET_AH0204_LENS_CORRECTION_SEI
+  struct
+  {
+    bool   enabled     = false;
+    bool   persistence = false;
+
+    Position focalCenter = Position(-1, -1);
+    double   focalLength = 0.0;
+
+    int    rdModelId   = -1;
+    double rdPoly3k1   = 0.0;
+    double rdPoly5k1   = 0.0;
+    double rdPoly5k2   = 0.0;
+
+    int    tcaModelId  = -1;
+    double tcaLinearKr = 0.0;
+    double tcaLinearKb = 0.0;
+    double tcaPoly3Br  = 0.0;
+    double tcaPoly3Cr  = 0.0;
+    double tcaPoly3Vr  = 0.0;
+    double tcaPoly3Bb  = 0.0;
+    double tcaPoly3Cb  = 0.0;
+    double tcaPoly3Vb  = 0.0;
+  } m_locSEI;
+#endif
+
   bool      m_constrainedRaslEncoding;
 
   //====== Weighted Prediction ========
@@ -2735,6 +2761,63 @@ public:
   const std::vector<uint8_t>& getJfifSEIJfifData() const { return m_jfifSEIData; }
   void                        setJfifSEIJfifData(std::vector<uint8_t> source) { m_jfifSEIData = source; }
 #endif
+#if JVET_AH0204_LENS_CORRECTION_SEI
+  void   setLocSEIEnabled(bool b) { m_locSEI.enabled = b; }
+  bool   getLocSEIEnabled() const { return m_locSEI.enabled; }
+  void   setLocSEIPersistence(bool b) { m_locSEI.persistence = b; }
+  bool   getLocSEIPersistence() const { return m_locSEI.persistence; }
+
+  void     setLocSEIFocalCenter(Position p) { m_locSEI.focalCenter = p; }
+  Position getLocSEIFocalCenter() const { return m_locSEI.focalCenter; }
+  void     setLocSEIFocalLength(double v) { m_locSEI.focalLength = v; }
+  double   getLocSEIFocalLength() const { return m_locSEI.focalLength; }
+
+  int    getLocSEIRdModelId() const { return m_locSEI.rdModelId; }
+  double getLocSEIRdPoly3k1() const { return m_locSEI.rdPoly3k1; }
+  double getLocSEIRdPoly5k1() const { return m_locSEI.rdPoly5k1; }
+  double getLocSEIRdPoly5k2() const { return m_locSEI.rdPoly5k2; }
+
+  void setLocSEIRdModelPoly3(double k1)
+  {
+    m_locSEI.rdModelId = SEILensOpticalCorrection::RD_POLY3;
+    m_locSEI.rdPoly3k1 = k1;
+  }
+
+  void setLocSEIRdModelPoly5(double k1, double k2)
+  {
+    m_locSEI.rdModelId = SEILensOpticalCorrection::RD_POLY5;
+    m_locSEI.rdPoly5k1 = k1;
+    m_locSEI.rdPoly5k2 = k2;
+  }
+
+  int    getLocSEITcaModelId() const { return m_locSEI.tcaModelId; }
+  double getLocSEITcaLinearKr() const { return m_locSEI.tcaLinearKr; }
+  double getLocSEITcaLinearKb() const { return m_locSEI.tcaLinearKb; }
+  double getLocSEITcaPoly3Br() const { return m_locSEI.tcaPoly3Br; }
+  double getLocSEITcaPoly3Cr() const { return m_locSEI.tcaPoly3Cr; }
+  double getLocSEITcaPoly3Vr() const { return m_locSEI.tcaPoly3Vr; }
+  double getLocSEITcaPoly3Bb() const { return m_locSEI.tcaPoly3Bb; }
+  double getLocSEITcaPoly3Cb() const { return m_locSEI.tcaPoly3Cb; }
+  double getLocSEITcaPoly3Vb() const { return m_locSEI.tcaPoly3Vb; }
+
+  void setLocSEITcaModelLinear(double kr, double kb)
+  {
+    m_locSEI.tcaModelId  = SEILensOpticalCorrection::TCA_LINEAR;
+    m_locSEI.tcaLinearKr = kr;
+    m_locSEI.tcaLinearKb = kb;
+  }
+
+  void setLocSEITcaModelPoly3(double br, double cr, double vr, double bb, double cb, double vb)
+  {
+    m_locSEI.tcaModelId = SEILensOpticalCorrection::TCA_POLY3;
+    m_locSEI.tcaPoly3Br = br;
+    m_locSEI.tcaPoly3Cr = cr;
+    m_locSEI.tcaPoly3Vr = vr;
+    m_locSEI.tcaPoly3Bb = bb;
+    m_locSEI.tcaPoly3Cb = cb;
+    m_locSEI.tcaPoly3Vb = vb;
+  }
+#endif
 
   void         setUseWP               ( bool b )                     { m_useWeightedPred   = b;    }
   void         setWPBiPred            ( bool b )                     { m_useWeightedBiPred = b;    }
diff --git a/source/Lib/EncoderLib/EncGOP.cpp b/source/Lib/EncoderLib/EncGOP.cpp
index 61d0520ac..7b5ddc0b1 100644
--- a/source/Lib/EncoderLib/EncGOP.cpp
+++ b/source/Lib/EncoderLib/EncGOP.cpp
@@ -931,6 +931,16 @@ void EncGOP::xCreateIRAPLeadingSEIMessages (SEIMessages& seiMessages, const SPS
   {
     xCreateNNPostFilterCharacteristicsSEIMessages(seiMessages);
   }
+
+#if JVET_AH0204_LENS_CORRECTION_SEI
+  if (m_pcCfg->getLocSEIEnabled())
+  {
+    auto sei = new SEILensOpticalCorrection;
+    m_seiEncoder.initSEILensOpticalCorrection(sei);
+    seiMessages.push_back(sei);
+  }
+#endif
+
   if (m_pcCfg->getPoSEIEnabled())
   {
     SEIProcessingOrderInfo *seiProcessingOrder = new SEIProcessingOrderInfo;
diff --git a/source/Lib/EncoderLib/SEIEncoder.cpp b/source/Lib/EncoderLib/SEIEncoder.cpp
index 611d82c69..d3ec9ea6d 100644
--- a/source/Lib/EncoderLib/SEIEncoder.cpp
+++ b/source/Lib/EncoderLib/SEIEncoder.cpp
@@ -744,6 +744,87 @@ void SEIEncoder::initSEIPostFilterHint(SEIPostFilterHint *seiPostFilterHint)
   }
 }
 
+#if JVET_AH0204_LENS_CORRECTION_SEI
+void SEIEncoder::initSEILensOpticalCorrection(SEILensOpticalCorrection* sei)
+{
+  CHECK(!m_isInitialized, "The lens optical correction SEI message needs to be initialized");
+  CHECK(sei == nullptr, "Failed to get the handler to the SEI message");
+
+  sei->cancelFlag = false;
+  sei->persistenceFlag = m_pcCfg->getLocSEIPersistence();
+
+  const Position focalCenter = m_pcCfg->getLocSEIFocalCenter();
+  const double   focalLength = m_pcCfg->getLocSEIFocalLength();
+
+  const bool codeFocalParams = focalCenter != Position(-1, -1) || focalLength != 0.0;
+  if (codeFocalParams)
+  {
+    SEILensOpticalCorrection::FocalParameters focalParams;
+
+    focalParams.center.x    = focalCenter.x == -1 ? m_pcCfg->getSourceWidth() / 2 : focalCenter.x;
+    focalParams.center.y    = focalCenter.y == -1 ? m_pcCfg->getSourceHeight() / 2 : focalCenter.y;
+    focalParams.focalLength = int(focalLength * 10.0 + 0.5);
+
+    sei->focalParams = focalParams;
+  }
+
+  auto quantize = [](double x) -> int32_t
+  { return int32_t(floor(x * (1 << SEILensOpticalCorrection::FRAC_BITS) + 0.5)); };
+
+  auto quantizeAndClampSigned = [&quantize](double x) -> int32_t
+  {
+    return std::clamp(quantize(x), -(1 << SEILensOpticalCorrection::FRAC_BITS),
+                      (1 << SEILensOpticalCorrection::FRAC_BITS) - 1);
+  };
+
+  auto quantizeAndClampUnsigned = [&quantize](double x) -> int32_t
+  { return std::clamp(quantize(x), 0, (2 << SEILensOpticalCorrection::FRAC_BITS) - 1); };
+
+  const int rdModelId = m_pcCfg->getLocSEIRdModelId();
+  if (rdModelId == SEILensOpticalCorrection::RD_POLY3)
+  {
+    SEILensOpticalCorrection::RdPoly3 rdParams;
+
+    rdParams.k1 = quantizeAndClampSigned(m_pcCfg->getLocSEIRdPoly3k1());
+
+    sei->rdParams = rdParams;
+  }
+  else if (rdModelId == SEILensOpticalCorrection::RD_POLY5)
+  {
+    SEILensOpticalCorrection::RdPoly5 rdParams;
+
+    rdParams.k1 = quantizeAndClampSigned(m_pcCfg->getLocSEIRdPoly5k1());
+    rdParams.k2 = quantizeAndClampSigned(m_pcCfg->getLocSEIRdPoly5k2());
+
+    sei->rdParams = rdParams;
+  }
+
+  const int tcaModelId = m_pcCfg->getLocSEITcaModelId();
+  if (tcaModelId == SEILensOpticalCorrection::TCA_LINEAR)
+  {
+    SEILensOpticalCorrection::TcaLinear tcaParams;
+
+    tcaParams.kr = quantizeAndClampUnsigned(m_pcCfg->getLocSEITcaLinearKr());
+    tcaParams.kb = quantizeAndClampUnsigned(m_pcCfg->getLocSEITcaLinearKb());
+
+    sei->tcaParams = tcaParams;
+  }
+  else if (tcaModelId == SEILensOpticalCorrection::TCA_POLY3)
+  {
+    SEILensOpticalCorrection::TcaPoly3 tcaParams;
+
+    tcaParams.br = quantizeAndClampSigned(m_pcCfg->getLocSEITcaPoly3Br());
+    tcaParams.cr = quantizeAndClampSigned(m_pcCfg->getLocSEITcaPoly3Cr());
+    tcaParams.vr = quantizeAndClampUnsigned(m_pcCfg->getLocSEITcaPoly3Vr());
+    tcaParams.bb = quantizeAndClampSigned(m_pcCfg->getLocSEITcaPoly3Bb());
+    tcaParams.cb = quantizeAndClampSigned(m_pcCfg->getLocSEITcaPoly3Cb());
+    tcaParams.vb = quantizeAndClampUnsigned(m_pcCfg->getLocSEITcaPoly3Vb());
+
+    sei->tcaParams = tcaParams;
+  }
+}
+#endif
+
 template <typename T>
 static void readTokenValue(T            &returnedValue, /// value returned
                            bool         &failed,        /// used and updated
diff --git a/source/Lib/EncoderLib/SEIEncoder.h b/source/Lib/EncoderLib/SEIEncoder.h
index 26a5230e0..6c7e986c8 100644
--- a/source/Lib/EncoderLib/SEIEncoder.h
+++ b/source/Lib/EncoderLib/SEIEncoder.h
@@ -121,6 +121,9 @@ public:
   void initSEIJfifMetadata(SEIJfifMetadata *sei);
   void initSEIXmpMetadata(SEIXmpMetadata *sei);
 #endif
+#if JVET_AH0204_LENS_CORRECTION_SEI
+  void initSEILensOpticalCorrection(SEILensOpticalCorrection* sei);
+#endif
 private:
   EncCfg* m_pcCfg;
   EncLib* m_pcEncLib;
diff --git a/source/Lib/EncoderLib/SEIwrite.cpp b/source/Lib/EncoderLib/SEIwrite.cpp
index 7278b0636..b00fe66c9 100644
--- a/source/Lib/EncoderLib/SEIwrite.cpp
+++ b/source/Lib/EncoderLib/SEIwrite.cpp
@@ -218,6 +218,11 @@ void SEIWriter::xWriteSEIpayloadData(OutputBitstream &bs, const SEI &sei, HRD &h
   case SEI::PayloadType::XMP_METADATA:
     xWriteSEIXmpMetadata(*static_cast<const SEIXmpMetadata*>(&sei));
     break;
+#endif
+#if JVET_AH0204_LENS_CORRECTION_SEI
+  case SEI::PayloadType::LENS_OPTICAL_CORRECTION:
+    xWriteSEILensOpticalCorrection(*static_cast<const SEILensOpticalCorrection*>(&sei));
+    break;
 #endif
   default:
     THROW("Trying to write unhandled SEI message");
@@ -2137,4 +2142,72 @@ void SEIWriter::xWriteSEIPostFilterHint(const SEIPostFilterHint &sei)
     }
   }
 }
+
+#if JVET_AH0204_LENS_CORRECTION_SEI
+void SEIWriter::xWriteSEILensOpticalCorrection(const SEILensOpticalCorrection& sei)
+{
+  xWriteFlag(sei.cancelFlag ? 1 : 0, "loc_cancel_flag");
+
+  if (!sei.cancelFlag)
+  {
+    xWriteFlag(sei.focalParams ? 1 : 0, "loc_focal_parameters_present_flag");
+
+    if (sei.focalParams)
+    {
+      const SEILensOpticalCorrection::FocalParameters& focalParams = sei.focalParams.value();
+
+      xWriteCode(focalParams.center.x, 16, "loc_focal_center_x");
+      xWriteCode(focalParams.center.y, 16, "loc_focal_center_y");
+      xWriteCode(focalParams.focalLength, 16, "loc_focal_length");
+    }
+
+    auto      toUnsignedCode = [](int32_t x) { return x & ((2 << SEILensOpticalCorrection::FRAC_BITS) - 1); };
+    const int numBits        = SEILensOpticalCorrection::FRAC_BITS + 1;
+
+    xWriteFlag(sei.hasRdParams() ? 1 : 0, "loc_radial_distortion_parameters_present_flag");
+
+    if (std::holds_alternative<SEILensOpticalCorrection::RdPoly3>(sei.rdParams))
+    {
+      const auto& rdModel = std::get<SEILensOpticalCorrection::RdPoly3>(sei.rdParams);
+
+      xWriteCode(SEILensOpticalCorrection::RD_POLY3, 8, "loc_radial_distortion_model_id");
+      xWriteCode(toUnsignedCode(rdModel.k1), numBits, "loc_rd_poly3_k1");
+    }
+    else if (std::holds_alternative<SEILensOpticalCorrection::RdPoly5>(sei.rdParams))
+    {
+      const auto& rdModel = std::get<SEILensOpticalCorrection::RdPoly5>(sei.rdParams);
+
+      xWriteCode(SEILensOpticalCorrection::RD_POLY5, 8, "loc_radial_distortion_model_id");
+      xWriteCode(toUnsignedCode(rdModel.k1), numBits, "loc_rd_poly5_k1");
+      xWriteCode(toUnsignedCode(rdModel.k2), numBits, "loc_rd_poly5_k2");
+    }
+
+    xWriteFlag(sei.hasTcaParams() ? 1 : 0, "loc_tca_parameters_present_flag");
+
+    if (std::holds_alternative<SEILensOpticalCorrection::TcaLinear>(sei.tcaParams))
+    {
+      const auto& tcaModel = std::get<SEILensOpticalCorrection::TcaLinear>(sei.tcaParams);
+
+      xWriteCode(SEILensOpticalCorrection::TCA_LINEAR, 8, "loc_tca_model_id");
+      xWriteCode(tcaModel.kr, numBits, "loc_tca_kr");
+      xWriteCode(tcaModel.kb, numBits, "loc_tca_kb");
+    }
+    else if (std::holds_alternative<SEILensOpticalCorrection::TcaPoly3>(sei.tcaParams))
+    {
+      const auto& tcaModel = std::get<SEILensOpticalCorrection::TcaPoly3>(sei.tcaParams);
+
+      xWriteCode(SEILensOpticalCorrection::TCA_POLY3, 8, "loc_tca_model_id");
+      xWriteCode(toUnsignedCode(tcaModel.br), numBits, "loc_tca_br");
+      xWriteCode(toUnsignedCode(tcaModel.cr), numBits, "loc_tca_cr");
+      xWriteCode(tcaModel.vr, numBits, "loc_tca_vr");
+      xWriteCode(toUnsignedCode(tcaModel.bb), numBits, "loc_tca_bb");
+      xWriteCode(toUnsignedCode(tcaModel.cb), numBits, "loc_tca_cb");
+      xWriteCode(tcaModel.vb, numBits, "loc_tca_vb");
+    }
+
+    xWriteFlag(sei.persistenceFlag ? 1 : 0, "loc_persistence_flag");
+  }
+}
+#endif
+
 //! \}
diff --git a/source/Lib/EncoderLib/SEIwrite.h b/source/Lib/EncoderLib/SEIwrite.h
index 789d2ea89..763964f0a 100644
--- a/source/Lib/EncoderLib/SEIwrite.h
+++ b/source/Lib/EncoderLib/SEIwrite.h
@@ -109,6 +109,9 @@ protected:
   void xWriteSEIShutterInterval(const SEIShutterIntervalInfo& sei);
 #if JVET_AF0167_MULTI_PLANE_IMAGE_INFO_SEI
   void xWriteSEIMultiplaneImageInfo(const SEIMultiplaneImageInfo& sei);
+#endif
+#if JVET_AH0204_LENS_CORRECTION_SEI
+  void xWriteSEILensOpticalCorrection(const SEILensOpticalCorrection& sei);
 #endif
   void xWriteByteAlign();
   void xWriteSEINeuralNetworkPostFilterCharacteristics(const SEINeuralNetworkPostFilterCharacteristics& sei);
