diff --git a/notebooks/AGIPD/AGIPD_Correct_and_Verify.ipynb b/notebooks/AGIPD/AGIPD_Correct_and_Verify.ipynb
index ce2c71bf358eebc303326241b05a84fc356534ed..03f5dc521c66216de93efc0e1ff23b5a728350b0 100644
--- a/notebooks/AGIPD/AGIPD_Correct_and_Verify.ipynb
+++ b/notebooks/AGIPD/AGIPD_Correct_and_Verify.ipynb
@@ -17,22 +17,22 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "in_folder = \"/gpfs/exfel/exp/SPB/202131/p900230/raw\" # the folder to read data from, required\n",
-    "out_folder = \"/gpfs/exfel/data/scratch/ahmedk/test/remove/agipd_resolve_conf\"  # the folder to output to, required\n",
+    "in_folder = \"/gpfs/exfel/exp/MID/202201/p002834/raw\" # the folder to read data from, required\n",
+    "out_folder = \"/gpfs/exfel/data/scratch/esobolev/pycal_litfrm/p002834/r0225\"  # the folder to output to, required\n",
     "metadata_folder = \"\"  # Directory containing calibration_metadata.yml when run by xfel-calibrate\n",
     "sequences = [-1] # sequences to correct, set to -1 for all, range allowed\n",
     "modules = [-1] # modules to correct, set to -1 for all, range allowed\n",
     "train_ids = [-1] # train IDs to correct, set to -1 for all, range allowed\n",
-    "run = 275 # runs to process, required\n",
+    "run = 225 # runs to process, required\n",
     "\n",
-    "karabo_id = \"SPB_DET_AGIPD1M-1\" # karabo karabo_id\n",
+    "karabo_id = \"MID_DET_AGIPD1M-1\" # karabo karabo_id\n",
     "karabo_da = ['-1']  # a list of data aggregators names, Default [-1] for selecting all data aggregators\n",
     "receiver_template = \"{}CH0\" # inset for receiver devices\n",
     "path_template = 'RAW-R{:04d}-{}-S{:05d}.h5' # the template to use to access data\n",
     "instrument_source_template = '{}/DET/{}:xtdf'  # path in the HDF5 file to images\n",
     "index_source_template = 'INDEX/{}/DET/{}:xtdf/'  # path in the HDF5 file to images\n",
     "ctrl_source_template = '{}/MDL/FPGA_COMP'  # path to control information\n",
-    "karabo_id_control = \"SPB_IRU_AGIPD1M1\" # karabo-id for control device\n",
+    "karabo_id_control = \"MID_EXP_AGIPD1M1\" # karabo-id for control device\n",
     "\n",
     "slopes_ff_from_files = \"\" # Path to locally stored SlopesFF and BadPixelsFF constants, loaded in precorrection notebook\n",
     "\n",
@@ -88,7 +88,8 @@
     "use_ppu_device = ''  # Device ID for a pulse picker device to only process picked trains, empty string to disable\n",
     "ppu_train_offset = 0  # When using the pulse picker, offset between the PPU's sequence start and actually picked train\n",
     "\n",
-    "use_litframe_device = '' # Device ID for a lit frame finder device to only process illuminated frames, empty string to disable\n",
+    "use_litframe_finder = 'auto' # Process only illuminated frames: 'off' - disable, 'device' - use online device data, 'offline' - use offline algorithm, 'auto' - choose online/offline source automatically (default)\n",
+    "litframe_device_id = '' # Device ID for a lit frame finder device, empty string to auto detection\n",
     "energy_threshold = -1000 # The low limit for the energy (uJ) exposed by frames subject to processing. If -1000, selection by pulse energy is disabled\n",
     "\n",
     "use_xgm_device = ''  # DoocsXGM device ID to obtain actual photon energy, operating condition else.\n",
@@ -153,6 +154,9 @@
     "sns.set_context(\"paper\", font_scale=1.4)\n",
     "sns.set_style(\"ticks\")\n",
     "\n",
+    "from extra_redu import make_litframe_finder\n",
+    "from extra_redu.litfrm.utils import litfrm_run_report\n",
+    "\n",
     "import cal_tools\n",
     "import seaborn as sns\n",
     "from cal_tools import agipdalgs as calgs\n",
@@ -450,16 +454,35 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "if use_litframe_device:\n",
-    "    # check run for the AgipdLitFrameFinder device\n",
+    "if use_litframe_finder != 'off':\n",
+    "    if use_litframe_finder not in ['auto', 'offline', 'online']:\n",
+    "        raise ValueError(\"Unexpected value in 'use_litframe_finder'.\")\n",
     "\n",
-    "    if use_litframe_device + ':output' in dc.instrument_sources:\n",
-    "        # Use selection provided by the AgipdLitFrameFinder (if the device is recorded)\n",
-    "        cell_sel = LitFrameSelection(use_litframe_device, dc, train_ids, max_pulses, energy_threshold)\n",
-    "        train_ids = cell_sel.train_ids\n",
-    "    else:\n",
-    "        # Use range selection (if the device is not recorded)\n",
-    "        print(f\"WARNING: LitFrameFinder {use_litframe_device} device is not found.\")\n",
+    "    inst = karabo_id_control[:3]\n",
+    "    litfrm = make_litframe_finder(inst, dc, litframe_device_id)\n",
+    "    try:\n",
+    "        if use_litframe_finder != 'auto':\n",
+    "            r = litfrm.read_or_process()\n",
+    "        elif use_litframe_finder != 'offline':\n",
+    "            r = litfrm.process()\n",
+    "        elif use_litframe_finder != 'online':\n",
+    "            r = litfrm.read()\n",
+    "\n",
+    "        report = litfrm_run_report(r)\n",
+    "        print(\"Lit-frame patterns:\")\n",
+    "        for rec in report:\n",
+    "            frmintf = ', '.join(\n",
+    "                [':'.join([str(n) for n in slc]) for slc in rec[6]]\n",
+    "            )\n",
+    "            trsintf = ':'.join([str(n) for n in rec[2]])\n",
+    "            print(\n",
+    "                f\"{rec[1]:2d} {trsintf:25s} {rec[3]:4d} {rec[4]:3d} \"\n",
+    "                f\"{rec[5]:3d} [{frmintf}]\"\n",
+    "            )\n",
+    "        cell_sel = LitFrameSelection(r, train_ids, max_pulses, energy_threshold)\n",
+    "    except Exception as err:\n",
+    "        print(\"Cannot use AgipdLitFrameFinder due to:\")\n",
+    "        print(err)\n",
     "        cell_sel = CellRange(max_pulses, max_cells=mem_cells)\n",
     "else:\n",
     "    # Use range selection\n",
@@ -861,10 +884,11 @@
     "        tid, data = run_data.select(f'{detector_id}/DET/*', source).train_from_id(tid)\n",
     "    else:\n",
     "        tid, data = next(iter(run_data.select(f'{detector_id}/DET/*', source).trains(require_all=True)))\n",
-    "\n",
+    "        \n",
     "    # TODO: remove and use the keep_dims version after updating Extra-data.\n",
     "    # Avoid using default axis with sources of an expected scalar value per train.\n",
-    "    if len(range(*cell_sel.crange)) == 1 and source in ['image.blShift', 'image.cellId', 'image.pulseId']:\n",
+    "    nfrm = cell_sel.get_cells_on_trains([tid]).sum()\n",
+    "    if nfrm == 1 and source in ['image.blShift', 'image.cellId', 'image.pulseId']:\n",
     "        axis = 0\n",
     "    else:\n",
     "        axis = -3\n",
@@ -872,10 +896,8 @@
     "    stacked_data = stack_detector_data(\n",
     "        train=data, data=source, fillvalue=fillvalue, modules=modules, axis=axis)\n",
     "    # Add cellId dimension when correcting one cellId only.\n",
-    "    if (\n",
-    "        len(range(*cell_sel.crange)) == 1 and\n",
-    "        data_folder != run_folder  # avoid adding pulse dims for raw data.\n",
-    "    ):\n",
+    "    # avoid adding pulse dims for raw data.\n",
+    "    if (nfrm == 1 and data_folder != run_folder):\n",
     "        stacked_data = stacked_data[np.newaxis, ...]\n",
     "\n",
     "    return tid, stacked_data"
@@ -990,7 +1012,9 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {},
+   "metadata": {
+    "scrolled": false
+   },
    "outputs": [],
    "source": [
     "pulse_range = [np.min(pulseId[pulseId>=0]), np.max(pulseId[pulseId>=0])]\n",
@@ -1301,7 +1325,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.8.12"
+   "version": "3.8.11"
   }
  },
  "nbformat": 4,
diff --git a/setup.py b/setup.py
index 0b6b5cd16d3b2dbed7c58bdbbc238984d660aad9..ef4fbe0a2a4ae2f5b3ea987b406fe51e18be7468 100644
--- a/setup.py
+++ b/setup.py
@@ -103,6 +103,7 @@ install_requires = [
         "tabulate==0.8.6",
         "traitlets==4.3.3",
 	"xarray==2022.3.0",
+        "EXtra-redu @ git+ssh://git@git.xfel.eu:10022/dataAnalysis/data-reduction.git@41571d9cca7ec3811d56caa6a1f0177b275fdb02",  # noqa
 ]
 
 if "readthedocs.org" not in sys.executable:
diff --git a/src/cal_tools/agipdlib.py b/src/cal_tools/agipdlib.py
index 6a5ff22e5ec4670b09d9e7f16e92ff866bef021b..2d6e8ab9f802fe798e9161b047868f33f104bd73 100644
--- a/src/cal_tools/agipdlib.py
+++ b/src/cal_tools/agipdlib.py
@@ -253,11 +253,11 @@ class AgipdCtrl:
             # TODO: Validate if removing this and
             # and using NB value for old RAW data.
             error = ("ERROR: Unable to read bias_voltage from"
-            f" {voltage_src[0]}/{voltage_src[1].replace('.','/')}.")
+                     f" {voltage_src[0]}/{voltage_src[1].replace('.','/')}.")
 
             if default_voltage:
                 print(f"{error} Returning {default_voltage} "
-                "as default bias voltage value.")
+                      "as default bias voltage value.")
             else:
                 raise ValueError(error)
             return default_voltage
@@ -485,8 +485,8 @@ class AgipdCorrections:
         data_dict["valid_trains"][:n_valid_trains] = valid_train_ids
 
         # get cell selection for the images in this file
-        cm = ( self.cell_sel.CM_NONE if apply_sel_pulses
-                else self.cell_sel.CM_PRESEL )
+        cm = (self.cell_sel.CM_NONE if apply_sel_pulses
+              else self.cell_sel.CM_PRESEL)
 
         img_selected = self.cell_sel.get_cells_on_trains(
             valid_train_ids, cm=cm)
@@ -502,22 +502,21 @@ class AgipdCorrections:
 
         kw = {
             "unstack_pulses": False,
-            "pulses": np.nonzero(img_selected),
         }
-
         # [n_modules, n_imgs, 2, x, y]
         raw_data = agipd_comp.get_array("image.data", **kw)[0]
-        n_img = raw_data.shape[0]
+        frm_ix = np.flatnonzero(img_selected)
+        n_img = frm_ix.size
 
         data_dict['nImg'][0] = n_img
-        data_dict['data'][:n_img] = raw_data[:, 0]
-        data_dict['rawgain'][:n_img] = raw_data[:, 1]
+        data_dict['data'][:n_img] = raw_data[frm_ix, 0]
+        data_dict['rawgain'][:n_img] = raw_data[frm_ix, 1]
         data_dict['cellId'][:n_img] = agipd_comp.get_array(
-            "image.cellId", **kw)[0]
+            "image.cellId", **kw)[0, frm_ix]
         data_dict['pulseId'][:n_img] = agipd_comp.get_array(
-            "image.pulseId", **kw)[0]
+            "image.pulseId", **kw)[0, frm_ix]
         data_dict['trainId'][:n_img] = agipd_comp.get_array(
-            "image.trainId", **kw)[0]
+            "image.trainId", **kw)[0, frm_ix]
         return n_img
 
     def write_file(self, i_proc, file_name, ofile_name):
@@ -601,8 +600,8 @@ class AgipdCorrections:
         in a single thread.
         """
         def _compress_frame(i):
-            # Equivalent to the HDF5 'shuffle' filter: transpose bytes for better
-            # compression.
+            # Equivalent to the HDF5 'shuffle' filter: transpose bytes for
+            # better compression.
             shuffled = np.ascontiguousarray(
                 arr[i].view(np.uint8).reshape((-1, arr.itemsize)).transpose()
             )
@@ -973,7 +972,7 @@ class AgipdCorrections:
         # exclude non valid trains
         valid_trains = valid * dc_trains
 
-        return valid_trains[valid_trains!=0]
+        return valid_trains[valid_trains != 0]
 
     def apply_selected_pulses(self, i_proc: int) -> int:
         """Select sharedmem data indices to correct based on selected
@@ -1587,34 +1586,31 @@ class CellRange(CellSelection):
         return np.tile(self._sel_for_cm(self.flag, self.flag_cm, cm),
                        len(train_sel))
 
+
 class LitFrameSelection(CellSelection):
     """Selection of detector memery cells indicated as lit frames
     by the AgipdLitFrameFinder
     """
-    def __init__(self, dev: str, dc: DataCollection, train_ids: List[int],
+    def __init__(self, litfrmdata, train_ids: List[int],
                  crange: Optional[List[int]] = None,
                  energy_threshold: Optional[float] = None):
         """Initialize lit frame selection
 
-        :param dev: AgipdLitFrameFinder device name
-        :param dc: EXtra-data DataCollection of a run
+        :param litfrmdata: AgipdLitFrameFinder output data
         :param train_ids: the list of selected trains
         :param crange: range parameters of selected cells,
             list up to 3 elements
         """
         # read AgipdLitFrameFinder data
-        self.dev = dev
+        self.dev = litfrmdata.meta.litFrmDev
         self.crange = validate_selected_pulses(crange, self.ncell_max)
         self.ethr = energy_threshold
-        intr_src = dev + ':output'
-        nfrm = dc[intr_src, 'data.nFrame'].ndarray()
-        litfrm_train_ids = dc[intr_src, 'data.trainId'].ndarray()
-        litfrm = dc[intr_src, 'data.nPulsePerFrame'].ndarray() > 0
-        if (energy_threshold != -1000 and
-                'data.energyPerFrame' in dc.keys_for_source(intr_src)):
-
-            litfrm &= (dc[intr_src, 'data.energyPerFrame'].ndarray()
-                       > energy_threshold)
+
+        nfrm = litfrmdata.output.nFrame
+        litfrm_train_ids = litfrmdata.meta.trainId
+        litfrm = litfrmdata.output.nPulsePerFrame > 0
+        if energy_threshold != -1000:
+            litfrm &= litfrmdata.output.energyPerFrame > energy_threshold
 
         # apply range selection
         if crange is None: