Skip to main content

panorama_tiler/logic/
tiler.rs

1use crate::TilerError;
2use crate::config::{DownscalingMethod, TilerConfig};
3use crate::logic::b83;
4use fast_image_resize as fr;
5use image::{Rgb, RgbImage};
6use rayon::prelude::*;
7use std::collections::BTreeSet;
8
9/// A representation of an individual generated tile.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct TileItem {
12    pub level: u32,
13    pub face: char,
14    pub col: u32,
15    pub row: u32,
16    pub image: RgbImage,
17}
18
19/// A representation of a fallback cube face tile.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct FallbackItem {
22    pub face: char,
23    pub image: RgbImage,
24}
25
26/// Container holding the raw outputs of the multi-resolution pipeline.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct GeneratedTiles {
29    pub tiles: Vec<TileItem>,
30    pub fallback_tiles: Vec<FallbackItem>,
31    pub missing_tiles_str: Option<String>,
32    pub levels: u32,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
36struct MissingTile {
37    face_idx: usize,
38    level: u32,
39    col: u32,
40    row: u32,
41}
42
43/// Checks if a region in an image contains exclusively background pixels without generating allocations.
44fn is_region_empty(
45    img: &RgbImage,
46    left: u32,
47    upper: u32,
48    width: u32,
49    height: u32,
50    bg_color: Rgb<u8>,
51) -> bool {
52    let stride = img.width() as usize * 3;
53    let raw = img.as_raw();
54    let bg_slice = bg_color.0; // [u8; 3]
55
56    for y in upper..(upper + height) {
57        let row_start = (y as usize * stride) + (left as usize * 3);
58        let row_end = row_start + (width as usize * 3);
59        let row_slice = &raw[row_start..row_end];
60
61        if row_slice.chunks_exact(3).any(|pixel| pixel != bg_slice) {
62            return false;
63        }
64    }
65    true
66}
67
68/// Breaks down each of the high-res faces into multi-resolution pyramids and tiles.
69#[allow(clippy::too_many_lines)]
70pub fn generate_pyramid(
71    faces: &[(char, RgbImage)],
72    config: &TilerConfig,
73    clamped_tile_size: u32,
74    actual_cube_size: u32,
75) -> Result<GeneratedTiles, TilerError> {
76    let tile_size = clamped_tile_size.min(actual_cube_size);
77    let face_letters = ['f', 'b', 'u', 'd', 'l', 'r'];
78    let bg_color = Rgb(config.output.background_color);
79
80    let levels = {
81        let ratio = f64::from(actual_cube_size) / f64::from(tile_size);
82        let mut l = (ratio.log2().ceil() as u32) + 1;
83        if l >= 2 && actual_cube_size / 2u32.pow(l - 2) == tile_size {
84            l -= 1; // Edge case matching Python script adjustments
85        }
86        l
87    };
88
89    let mut level_sizes = vec![0; (levels + 1) as usize];
90    let mut current_size = actual_cube_size;
91    for level in (1..=levels).rev() {
92        level_sizes[level as usize] = current_size;
93        current_size /= 2;
94    }
95
96    let is_partial = config.angles.haov < 360.0 || config.angles.vaov < 180.0;
97
98    // Generate pyramids across all faces in parallel
99    let results = faces
100        .par_iter()
101        .enumerate()
102        .map(|(f_idx, &(letter, ref full_face))| {
103            let mut local_tiles = Vec::new();
104            let mut local_missing = Vec::new();
105
106            let mut recursive_face: Option<RgbImage> = None;
107
108            let mut resizer = fr::Resizer::new();
109            let resize_options = fr::ResizeOptions::new()
110                .resize_alg(fr::ResizeAlg::Convolution(fr::FilterType::Lanczos3));
111
112            for level in (1..=levels).rev() {
113                let size = level_sizes[level as usize];
114                let num_tiles_wide_high = (f64::from(size) / f64::from(tile_size)).ceil() as u32;
115                let mut current_face_allocated;
116
117                let active_face: &RgbImage = if level == levels {
118                    full_face
119                } else {
120                    match config.output.downscaling_method {
121                        DownscalingMethod::Recursive => {
122                            let mut downscaled = RgbImage::new(size, size);
123                            let source = recursive_face.as_ref().unwrap_or(full_face);
124                            resizer.resize(source, &mut downscaled, Some(&resize_options))?;
125                            recursive_face.insert(downscaled)
126                        }
127                        DownscalingMethod::Direct => {
128                            current_face_allocated = RgbImage::new(size, size);
129                            resizer.resize(
130                                full_face,
131                                &mut current_face_allocated,
132                                Some(&resize_options),
133                            )?;
134                            &current_face_allocated
135                        }
136                    }
137                };
138
139                for row in 0..num_tiles_wide_high {
140                    for col in 0..num_tiles_wide_high {
141                        let left = col * tile_size;
142                        let upper = row * tile_size;
143                        let width = tile_size.min(size - left);
144                        let height = tile_size.min(size - upper);
145
146                        // Avoid allocating new sub-images if the entire region is verified empty
147                        if is_partial
148                            && is_region_empty(active_face, left, upper, width, height, bg_color)
149                        {
150                            local_missing.push(MissingTile {
151                                face_idx: f_idx,
152                                level,
153                                col,
154                                row,
155                            });
156                        } else {
157                            let tile_crop =
158                                image::imageops::crop_imm(active_face, left, upper, width, height)
159                                    .to_image();
160                            local_tiles.push(TileItem {
161                                level,
162                                face: letter,
163                                col,
164                                row,
165                                image: tile_crop,
166                            });
167                        }
168                    }
169                }
170            }
171            Ok::<_, TilerError>((local_tiles, local_missing))
172        })
173        .collect::<Result<Vec<_>, TilerError>>();
174    let (tiles_nested, missing_nested): (Vec<Vec<TileItem>>, Vec<Vec<MissingTile>>) =
175        results?.into_iter().unzip();
176
177    let tiles: Vec<TileItem> = tiles_nested.into_iter().flatten().collect();
178    let missing_tiles: Vec<MissingTile> = missing_nested.into_iter().flatten().collect();
179
180    // Process missing tiles string
181    let missing_tiles_str = if missing_tiles.is_empty() {
182        None
183    } else {
184        let mut missing_set: BTreeSet<MissingTile> = missing_tiles.into_iter().collect();
185
186        // Strip children of missing parents to save space
187        let mut redundant = Vec::new();
188        for &t in &missing_set {
189            if t.level > 1 {
190                let parent = MissingTile {
191                    face_idx: t.face_idx,
192                    level: t.level - 1,
193                    col: t.col / 2,
194                    row: t.row / 2,
195                };
196                if missing_set.contains(&parent) {
197                    redundant.push(t);
198                }
199            }
200        }
201        for r in redundant {
202            missing_set.remove(&r);
203        }
204
205        // Format and Base83 compress the remaining missing tile configurations
206        let mut sorted_missing: Vec<MissingTile> = missing_set.into_iter().collect();
207        sorted_missing.sort();
208
209        let mut missing_str = String::new();
210        let mut prev_face: Option<usize> = None;
211        let mut prev_level: Option<u32> = None;
212        let mut num_tile_digits = 1;
213
214        for mt in sorted_missing {
215            if Some(mt.face_idx) != prev_face {
216                missing_str.push('!');
217                missing_str.push(face_letters[mt.face_idx]);
218                prev_level = None;
219            }
220            if Some(mt.level) != prev_level {
221                missing_str.push('>');
222                missing_str.push_str(&b83::encode(&[mt.level], 1));
223                let level_size = level_sizes[mt.level as usize];
224                let max_tile_num = (f64::from(level_size) / f64::from(tile_size)).ceil() as u32 - 1;
225                num_tile_digits = (f64::from(max_tile_num + 1).log(83.0).ceil() as usize).max(1);
226            }
227            missing_str.push_str(&b83::encode(&[mt.col, mt.row], num_tile_digits));
228            prev_face = Some(mt.face_idx);
229            prev_level = Some(mt.level);
230        }
231        Some(missing_str)
232    };
233
234    // Generate fallback files if fallback size is defined
235    let mut fallback_tiles = Vec::new();
236    if config.output.fallback_size > 0 {
237        let mut resizer = fr::Resizer::new();
238        let resize_options = fr::ResizeOptions::new()
239            .resize_alg(fr::ResizeAlg::Convolution(fr::FilterType::Lanczos3));
240
241        for &(letter, ref full_face) in faces {
242            let mut resized_face =
243                RgbImage::new(config.output.fallback_size, config.output.fallback_size);
244            resizer.resize(full_face, &mut resized_face, Some(&resize_options))?;
245
246            fallback_tiles.push(FallbackItem {
247                face: letter,
248                image: resized_face,
249            });
250        }
251    }
252
253    Ok(GeneratedTiles {
254        tiles,
255        fallback_tiles,
256        missing_tiles_str,
257        levels,
258    })
259}