// Copyright (c) the JPEG XL Project Authors. All rights reserved. // // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file.
Status ParamsPostInit(CompressParams* p) { if (!p->manual_noise.empty() &&
p->manual_noise.size() != NoiseParams::kNumNoisePoints) { return JXL_FAILURE("Invalid number of noise lut entries");
} if (!p->manual_xyb_factors.empty() && p->manual_xyb_factors.size() != 3) { return JXL_FAILURE("Invalid number of XYB quantization factors");
} if (!p->modular_mode && p->butteraugli_distance == 0.0) {
p->butteraugli_distance = kMinButteraugliDistance;
} if (p->original_butteraugli_distance == -1.0) {
p->original_butteraugli_distance = p->butteraugli_distance;
} if (p->resampling <= 0) {
p->resampling = 1; // For very low bit rates, using 2x2 resampling gives better results on // most photographic images, with an adjusted butteraugli score chosen to // give roughly the same amount of bits per pixel. if (!p->already_downsampled && p->butteraugli_distance >= 20) {
p->resampling = 2;
p->butteraugli_distance = 6 + ((p->butteraugli_distance - 20) * 0.25);
}
} if (p->ec_resampling <= 0) {
p->ec_resampling = p->resampling;
} returntrue;
}
// We don't add noise at low butteraugli distances because the original // noise is stored within the compressed image and adding noise makes things // worse. if (ApplyOverride(cparams.noise, dist >= kMinButteraugliForNoise) ||
cparams.photon_noise_iso > 0 ||
cparams.manual_noise.size() == NoiseParams::kNumNoisePoints) {
flags |= FrameHeader::kNoise;
}
if (cparams.modular_mode) {
frame_header->encoding = FrameEncoding::kModular; if (cparams.modular_group_size_shift == -1) {
frame_header->group_size_shift = 1; // no point using groups when only one group is full and the others are // less than half full: multithreading will not really help much, while // compression does suffer if (xsize <= 400 && ysize <= 400) {
frame_header->group_size_shift = 2;
}
} else {
frame_header->group_size_shift = cparams.modular_group_size_shift;
}
}
if (jpeg_data) { // we are transcoding a JPEG, so we don't get to choose
frame_header->encoding = FrameEncoding::kVarDCT;
frame_header->x_qm_scale = 2;
frame_header->b_qm_scale = 2;
JXL_RETURN_IF_ERROR(SetChromaSubsamplingFromJpegData(
*jpeg_data, &frame_header->chroma_subsampling));
JXL_RETURN_IF_ERROR(SetColorTransformFromJpegData(
*jpeg_data, &frame_header->color_transform));
} else {
frame_header->color_transform = cparams.color_transform; if (!cparams.modular_mode &&
(frame_header->chroma_subsampling.MaxHShift() != 0 ||
frame_header->chroma_subsampling.MaxVShift() != 0)) { return JXL_FAILURE( "Chroma subsampling is not supported in VarDCT mode when not " "recompressing JPEGs");
}
} if (frame_header->color_transform != ColorTransform::kYCbCr &&
(frame_header->chroma_subsampling.MaxHShift() != 0 ||
frame_header->chroma_subsampling.MaxVShift() != 0)) { return JXL_FAILURE( "Chroma subsampling is not supported when color transform is not " "YCbCr");
}
frame_header->flags = FrameFlagsFromParams(cparams); // Non-photon noise is not supported in the Modular encoder for now. if (frame_header->encoding != FrameEncoding::kVarDCT &&
cparams.photon_noise_iso == 0 && cparams.manual_noise.empty()) {
frame_header->UpdateFlag(false, FrameHeader::Flags::kNoise);
}
frame_header->dc_level = frame_info.dc_level; if (frame_header->dc_level > 2) { // With 3 or more progressive_dc frames, the implementation does not yet // work, see enc_cache.cc. return JXL_FAILURE("progressive_dc > 2 is not yet supported");
} if (cparams.progressive_dc > 0 &&
(cparams.ec_resampling != 1 || cparams.resampling != 1)) { return JXL_FAILURE("Resampling not supported with DC frames");
} if (cparams.resampling != 1 && cparams.resampling != 2 &&
cparams.resampling != 4 && cparams.resampling != 8) { return JXL_FAILURE("Invalid resampling factor");
} if (cparams.ec_resampling != 1 && cparams.ec_resampling != 2 &&
cparams.ec_resampling != 4 && cparams.ec_resampling != 8) { return JXL_FAILURE("Invalid ec_resampling factor");
} // Resized frames. if (frame_info.frame_type != FrameType::kDCFrame) {
frame_header->frame_origin = frame_info.origin;
size_t ups = 1; if (cparams.already_downsampled) ups = cparams.resampling;
// TODO(lode): this is not correct in case of odd original image sizes in // combination with cparams.already_downsampled. Likely these values should // be set to respectively frame_header->default_xsize() and // frame_header->default_ysize() instead, the original (non downsampled) // intended decoded image dimensions. But it may be more subtle than that // if combined with crop. This issue causes custom_size_or_origin to be // incorrectly set to true in case of already_downsampled with odd output // image size when no cropping is used.
frame_header->frame_size.xsize = xsize * ups;
frame_header->frame_size.ysize = ysize * ups; if (frame_info.origin.x0 != 0 || frame_info.origin.y0 != 0 ||
frame_header->frame_size.xsize != frame_header->default_xsize() ||
frame_header->frame_size.ysize != frame_header->default_ysize()) {
frame_header->custom_size_or_origin = true;
}
} // Upsampling.
frame_header->upsampling = cparams.resampling; const std::vector<ExtraChannelInfo>& extra_channels =
frame_header->nonserialized_metadata->m.extra_channel_info;
frame_header->extra_channel_upsampling.clear();
frame_header->extra_channel_upsampling.resize(extra_channels.size(),
cparams.ec_resampling);
frame_header->save_as_reference = frame_info.save_as_reference;
// Set blending-related information. if (frame_info.blend || frame_header->custom_size_or_origin) { // Set blend_channel to the first alpha channel. These values are only // encoded in case a blend mode involving alpha is used and there are more // than one extra channels.
size_t index = 0; if (frame_info.alpha_channel == -1) { if (extra_channels.size() > 1) { for (size_t i = 0; i < extra_channels.size(); i++) { if (extra_channels[i].type == ExtraChannel::kAlpha) {
index = i; break;
}
}
}
} else {
index = static_cast<size_t>(frame_info.alpha_channel);
JXL_ENSURE(index == 0 || index < extra_channels.size());
}
frame_header->blending_info.alpha_channel = index;
frame_header->blending_info.mode =
frame_info.blend ? frame_info.blendmode : BlendMode::kReplace;
frame_header->blending_info.source = frame_info.source;
frame_header->blending_info.clamp = frame_info.clamp; constauto& extra_channel_info = frame_info.extra_channel_blending_info; for (size_t i = 0; i < extra_channels.size(); i++) { if (i < extra_channel_info.size()) {
frame_header->extra_channel_blending_info[i] = extra_channel_info[i];
} else {
frame_header->extra_channel_blending_info[i].alpha_channel = index;
BlendMode default_blend = frame_info.blendmode; if (extra_channels[i].type != ExtraChannel::kBlack && i != index) { // K needs to be blended, spot colors and other stuff gets added
default_blend = BlendMode::kAdd;
}
frame_header->extra_channel_blending_info[i].mode =
frame_info.blend ? default_blend : BlendMode::kReplace;
frame_header->extra_channel_blending_info[i].source = 1;
}
}
}
if (jpeg_data) {
frame_header->UpdateFlag(false, FrameHeader::kUseDcFrame);
frame_header->UpdateFlag(true, FrameHeader::kSkipAdaptiveDCSmoothing);
}
returntrue;
}
// Invisible (alpha = 0) pixels tend to be a mess in optimized PNGs. // Since they have no visual impact whatsoever, we can replace them with // something that compresses better and reduces artifacts near the edges. This // does some kind of smooth stuff that seems to work. // Replace invisible pixels with a weighted average of the pixel to the left, // the pixel to the topright, and non-invisible neighbours. // Produces downward-blurry smears, with in the upwards direction only a 1px // edge duplication but not more. It would probably be better to smear in all // directions. That requires an alpha-weighed convolution with a large enough // kernel though, which might be overkill... void SimplifyInvisible(Image3F* image, const ImageF& alpha, bool lossless) { for (size_t c = 0; c < 3; ++c) { for (size_t y = 0; y < image->ysize(); ++y) { float* JXL_RESTRICT row = image->PlaneRow(c, y); constfloat* JXL_RESTRICT prow =
(y > 0 ? image->PlaneRow(c, y - 1) : nullptr); constfloat* JXL_RESTRICT nrow =
(y + 1 < image->ysize() ? image->PlaneRow(c, y + 1) : nullptr); constfloat* JXL_RESTRICT a = alpha.Row(y); constfloat* JXL_RESTRICT pa = (y > 0 ? alpha.Row(y - 1) : nullptr); constfloat* JXL_RESTRICT na =
(y + 1 < image->ysize() ? alpha.Row(y + 1) : nullptr); for (size_t x = 0; x < image->xsize(); ++x) { if (a[x] == 0) { if (lossless) {
row[x] = 0; continue;
} float d = 0.f;
row[x] = 0; if (x > 0) {
row[x] += row[x - 1];
d++; if (a[x - 1] > 0.f) {
row[x] += row[x - 1];
d++;
}
} if (x + 1 < image->xsize()) { if (y > 0) {
row[x] += prow[x + 1];
d++;
} if (a[x + 1] > 0.f) {
row[x] += 2.f * row[x + 1];
d += 2.f;
} if (y > 0 && pa[x + 1] > 0.f) {
row[x] += 2.f * prow[x + 1];
d += 2.f;
} if (y + 1 < image->ysize() && na[x + 1] > 0.f) {
row[x] += 2.f * nrow[x + 1];
d += 2.f;
}
} if (y > 0 && pa[x] > 0.f) {
row[x] += 2.f * prow[x];
d += 2.f;
} if (y + 1 < image->ysize() && na[x] > 0.f) {
row[x] += 2.f * nrow[x];
d += 2.f;
} if (d > 1.f) row[x] /= d;
}
}
}
}
}
void ComputeChromacityAdjustments(const CompressParams& cparams, const Image3F& opsin, const Rect& rect,
FrameHeader* frame_header) { if (frame_header->encoding != FrameEncoding::kVarDCT ||
cparams.max_error_mode) { return;
} // 1) Distance based approach for chromacity adjustment: float x_qm_scale_steps[3] = {2.5f, 5.5f, 9.5f};
frame_header->x_qm_scale = 3; for (float x_qm_scale_step : x_qm_scale_steps) { if (cparams.original_butteraugli_distance > x_qm_scale_step) {
frame_header->x_qm_scale++;
}
} // 2) Pixel-based approach for chromacity adjustment: // look at the individual pixels and make a guess how difficult // the image would be based on the worst case pixel.
PixelStatsForChromacityAdjustment pixel_stats; if (cparams.speed_tier <= SpeedTier::kSquirrel) {
pixel_stats.Calc(&opsin, rect);
} // For X take the most severe adjustment.
frame_header->x_qm_scale = std::max<int>(
frame_header->x_qm_scale, 2 + pixel_stats.HowMuchIsXChannelPixelized()); // B only adjusted by pixel-based approach.
frame_header->b_qm_scale = 2 + pixel_stats.HowMuchIsBChannelPixelized();
}
void ComputeNoiseParams(const CompressParams& cparams, bool streaming_mode, bool color_is_jpeg, const Image3F& opsin, const FrameDimensions& frame_dim,
FrameHeader* frame_header, NoiseParams* noise_params) { if (cparams.photon_noise_iso > 0) {
*noise_params = SimulatePhotonNoise(frame_dim.xsize, frame_dim.ysize,
cparams.photon_noise_iso);
} elseif (cparams.manual_noise.size() == NoiseParams::kNumNoisePoints) { for (size_t i = 0; i < NoiseParams::kNumNoisePoints; i++) {
noise_params->lut[i] = cparams.manual_noise[i];
}
} elseif (frame_header->encoding == FrameEncoding::kVarDCT &&
frame_header->flags & FrameHeader::kNoise && !color_is_jpeg &&
!streaming_mode) { // Don't start at zero amplitude since adding noise is expensive -- it // significantly slows down decoding, and this is unlikely to // completely go away even with advanced optimizations. After the // kNoiseModelingRampUpDistanceRange we have reached the full level, // i.e. noise is no longer represented by the compressed image, so we // can add full noise by the noise modeling itself. staticconstfloat kNoiseModelingRampUpDistanceRange = 0.6; staticconstfloat kNoiseLevelAtStartOfRampUp = 0.25; staticconstfloat kNoiseRampupStart = 1.0; // TODO(user) test and properly select quality_coef with smooth // filter float quality_coef = 1.0f; constfloat rampup = (cparams.butteraugli_distance - kNoiseRampupStart) /
kNoiseModelingRampUpDistanceRange; if (rampup < 1.0f) {
quality_coef = kNoiseLevelAtStartOfRampUp +
(1.0f - kNoiseLevelAtStartOfRampUp) * rampup;
} if (rampup < 0.0f) {
quality_coef = kNoiseRampupStart;
} if (!GetNoiseParameter(opsin, noise_params, quality_coef)) {
frame_header->flags &= ~FrameHeader::kNoise;
}
}
}
Status DownsampleColorChannels(const CompressParams& cparams, const FrameHeader& frame_header, bool color_is_jpeg, Image3F* opsin) { if (color_is_jpeg || frame_header.upsampling == 1 ||
cparams.already_downsampled) { returntrue;
} if (frame_header.encoding == FrameEncoding::kVarDCT &&
frame_header.upsampling == 2) { // TODO(lode): use the regular DownsampleImage, or adapt to the custom // coefficients, if there is are custom upscaling coefficients in // CustomTransformData if (cparams.speed_tier <= SpeedTier::kSquirrel) { // TODO(lode): DownsampleImage2_Iterative is currently too slow to // be used for squirrel, make it faster, and / or enable it only for // kitten.
JXL_RETURN_IF_ERROR(DownsampleImage2_Iterative(opsin));
} else {
JXL_RETURN_IF_ERROR(DownsampleImage2_Sharper(opsin));
}
} else {
JXL_ASSIGN_OR_RETURN(*opsin,
DownsampleImage(*opsin, frame_header.upsampling));
} if (frame_header.encoding == FrameEncoding::kVarDCT) {
JXL_RETURN_IF_ERROR(PadImageToBlockMultipleInPlace(opsin));
} returntrue;
}
template <size_t L, typename V, typename R> void FindIndexOfSumMaximum(const V* array, R* idx, V* sum) {
static_assert(L > 0);
V maxval = 0;
V val = 0;
R maxidx = 0; for (size_t i = 0; i < L; ++i) {
val += array[i]; if (val > maxval) {
maxval = val;
maxidx = i;
}
}
*idx = maxidx;
*sum = maxval;
}
Status EncodeGlobalDCInfo(const PassesSharedState& shared, BitWriter* writer,
AuxOut* aux_out) { // Encode quantizer DC and global scale.
QuantizerParams params = shared.quantizer.GetParams();
JXL_RETURN_IF_ERROR(
WriteQuantizerParams(params, writer, LayerType::Quant, aux_out));
JXL_RETURN_IF_ERROR(EncodeBlockCtxMap(shared.block_ctx_map, writer, aux_out));
JXL_RETURN_IF_ERROR(ColorCorrelationEncodeDC(shared.cmap.base(), writer,
LayerType::Dc, aux_out)); returntrue;
}
// In streaming mode, this function only performs the histogram clustering and // saves the histogram bitstreams in enc_state, the actual AC global bitstream // is written in OutputAcGlobal() function after all the groups are processed.
Status EncodeGlobalACInfo(PassesEncoderState* enc_state, BitWriter* writer,
ModularFrameEncoder* enc_modular, AuxOut* aux_out) {
PassesSharedState& shared = enc_state->shared;
JxlMemoryManager* memory_manager = enc_state->memory_manager();
JXL_RETURN_IF_ERROR(DequantMatricesEncode(memory_manager, shared.matrices,
writer, LayerType::Quant, aux_out,
enc_modular));
size_t num_histo_bits = CeilLog2Nonzero(shared.frame_dim.num_groups); if (!enc_state->streaming_mode && num_histo_bits != 0) {
JXL_RETURN_IF_ERROR(
writer->WithMaxBits(num_histo_bits, LayerType::Ac, aux_out, [&] {
writer->Write(num_histo_bits, shared.num_histograms - 1); returntrue;
}));
}
for (size_t i = 0; i < num_passes; i++) {
JXL_DEBUG_V(2, "Encoding AC group %u [abs %" PRIuS "] pass %" PRIuS,
group_index, ac_group_id, i); if (frame_header.encoding == FrameEncoding::kVarDCT) {
JXL_RETURN_IF_ERROR(EncodeGroupTokenizedCoefficients(
group_index, i, enc_state->histogram_idx[group_index], *enc_state,
ac_group_code(i, group_index), my_aux_out));
} // Write all modular encoded data (color?, alpha, depth, extra channels)
JXL_RETURN_IF_ERROR(enc_modular->EncodeStream(
ac_group_code(i, group_index), my_aux_out, LayerType::ModularAcGroup,
ModularStreamId::ModularAC(ac_group_id, i)));
JXL_DEBUG_V(2, "AC group %u [abs %" PRIuS "] pass %" PRIuS " encoded size is %" PRIuS " bits",
group_index, ac_group_id, i,
ac_group_code(i, group_index)->BitsWritten());
} returntrue;
};
JXL_RETURN_IF_ERROR(RunOnPool(pool, 0, num_groups, resize_aux_outs,
process_group, "EncodeGroupCoefficients")); // Resizing aux_outs to 0 also Assimilates the array. static_cast<void>(resize_aux_outs(0));
for (std::unique_ptr<BitWriter>& bw : *group_codes) {
JXL_RETURN_IF_ERROR(bw->WithMaxBits(8, LayerType::Ac, aux_out, [&] {
bw->ZeroPadToByte(); // end of group. returntrue;
}));
} returntrue;
}
// Make the image patch bigger than the currently processed group in streaming // mode so that we can take into account border pixels around the group when // computing inverse Gaborish and adaptive quantization map. int max_border = enc_state.streaming_mode ? kBlockDim : 0;
Rect frame_rect(0, 0, frame_data.xsize, frame_data.ysize);
Rect frame_area_rect = Rect(x0, y0, xsize, ysize);
Rect patch_rect = frame_area_rect.Extend(max_border, frame_rect);
JXL_ENSURE(patch_rect.IsInside(frame_rect));
// Allocating a large enough image avoids a copy when padding.
JXL_ASSIGN_OR_RETURN(
Image3F color,
Image3F::Create(memory_manager, RoundUpToBlockDim(patch_rect.xsize()),
RoundUpToBlockDim(patch_rect.ysize())));
JXL_RETURN_IF_ERROR(color.ShrinkTo(patch_rect.xsize(), patch_rect.ysize()));
std::vector<ImageF> extra_channels(num_extra_channels); for (auto& extra_channel : extra_channels) {
JXL_ASSIGN_OR_RETURN(
extra_channel,
ImageF::Create(memory_manager, patch_rect.xsize(), patch_rect.ysize()));
}
ImageF* alpha = alpha_eci ? &extra_channels[alpha_idx] : nullptr;
ImageF* black = black_eci ? &extra_channels[black_idx] : nullptr; bool has_interleaved_alpha = false;
JxlChunkedFrameInputSource input = frame_data.GetInputSource(); if (!jpeg_data) {
JXL_RETURN_IF_ERROR(CopyColorChannels(input, patch_rect, frame_info,
metadata->m, pool, &color, alpha,
&has_interleaved_alpha));
}
JXL_RETURN_IF_ERROR(CopyExtraChannels(input, patch_rect, frame_info,
metadata->m, has_interleaved_alpha,
pool, &extra_channels));
enc_state.cparams = cparams;
Image3F linear_storage;
Image3F* linear = nullptr;
if (!jpeg_data) { if (frame_header.color_transform == ColorTransform::kXYB &&
frame_info.ib_needs_color_transform) { if (frame_header.encoding == FrameEncoding::kVarDCT &&
cparams.speed_tier <= SpeedTier::kKitten) {
JXL_ASSIGN_OR_RETURN(linear_storage,
Image3F::Create(memory_manager, patch_rect.xsize(),
patch_rect.ysize()));
linear = &linear_storage;
}
JXL_RETURN_IF_ERROR(ToXYB(c_enc, metadata->m.IntensityTarget(), black,
pool, &color, cms, linear));
} else { // Nothing to do. // RGB or YCbCr: forward YCbCr is not implemented, this is only used when // the input is already in YCbCr // If encoding a special DC or reference frame: input is already in XYB.
} bool lossless = cparams.IsLossless(); if (alpha && !alpha_eci->alpha_associated &&
frame_header.frame_type == FrameType::kRegularFrame &&
!ApplyOverride(cparams.keep_invisible, cparams.IsLossless()) &&
cparams.ec_resampling == cparams.resampling &&
!cparams.disable_perceptual_optimizations) { // simplify invisible pixels
SimplifyInvisible(&color, *alpha, lossless); if (linear) {
SimplifyInvisible(linear, *alpha, lossless);
}
}
JXL_RETURN_IF_ERROR(PadImageToBlockMultipleInPlace(&color));
}
// Rectangle within color that corresponds to the currently processed group in // streaming mode.
Rect group_rect(x0 - patch_rect.x0(), y0 - patch_rect.y0(),
RoundUpToBlockDim(xsize), RoundUpToBlockDim(ysize));
if (enc_state.initialize_global_state && !jpeg_data) {
ComputeChromacityAdjustments(cparams, color, group_rect,
&mutable_frame_header);
}
if (cparams.ec_resampling != 1 && !cparams.already_downsampled) { for (ImageF& ec : extra_channels) {
JXL_ASSIGN_OR_RETURN(ec, DownsampleImage(ec, cparams.ec_resampling));
}
}
if (!enc_state.streaming_mode) {
group_rect = Rect(color);
}
if (frame_header.encoding == FrameEncoding::kVarDCT) {
enc_state.passes.resize(enc_state.progressive_splitter.GetNumPasses()); for (PassesEncoderState::PassData& pass : enc_state.passes) {
pass.ac_tokens.resize(shared.frame_dim.num_groups);
} if (jpeg_data) {
JXL_RETURN_IF_ERROR(ComputeJPEGTranscodingData(
*jpeg_data, frame_header, pool, &enc_modular, &enc_state));
} else {
JXL_RETURN_IF_ERROR(ComputeVarDCTEncodingData(
frame_header, linear, &color, group_rect, cms, pool, &enc_modular,
&enc_state, aux_out));
}
JXL_RETURN_IF_ERROR(ComputeAllCoeffOrders(enc_state, frame_dim)); if (!enc_state.streaming_mode) {
shared.num_histograms = 1;
enc_state.histogram_idx.resize(frame_dim.num_groups);
}
JXL_RETURN_IF_ERROR(
TokenizeAllCoefficients(frame_header, pool, &enc_state));
}
if (cparams.modular_mode || !extra_channels.empty()) {
JXL_RETURN_IF_ERROR(enc_modular.ComputeEncodingData(
frame_header, metadata->m, &color, extra_channels, group_rect,
frame_dim, frame_area_rect, &enc_state, cms, pool, aux_out, /*do_color=*/cparams.modular_mode));
}
if (!enc_state.streaming_mode) { if (cparams.speed_tier < SpeedTier::kTortoise ||
!cparams.ModularPartIsLossless() || cparams.responsive ||
!cparams.custom_fixed_tree.empty()) { // Use local trees if doing lossless modular, unless at very slow speeds.
JXL_RETURN_IF_ERROR(enc_modular.ComputeTree(pool));
JXL_RETURN_IF_ERROR(enc_modular.ComputeTokens(pool));
}
mutable_frame_header.UpdateFlag(shared.image_features.patches.HasAny(),
FrameHeader::kPatches);
mutable_frame_header.UpdateFlag(shared.image_features.splines.HasAny(),
FrameHeader::kSplines);
}
JXL_RETURN_IF_ERROR(EncodeGroups(frame_header, &enc_state, &enc_modular, pool,
group_codes, aux_out)); if (enc_state.streaming_mode) { const size_t group_index = enc_state.dc_group_index;
enc_modular.ClearStreamData(ModularStreamId::VarDCTDC(group_index));
enc_modular.ClearStreamData(ModularStreamId::ACMetadata(group_index));
enc_modular.ClearModularStreamData();
} returntrue;
}
// The center of the image is either given by parameters or chosen // to be the middle of the image by default if center_x, center_y resp. // are not provided.
// The center of the group containing the center of the image.
int64_t cx = (imag_cx / group_dim) * group_dim + group_dim / 2;
int64_t cy = (imag_cy / group_dim) * group_dim + group_dim / 2; // This identifies in what area of the central group the center of the image // lies in. double direction = -std::atan2(imag_cy - cy, imag_cx - cx); // This identifies the side of the central group the center of the image // lies closest to. This can take values 0, 1, 2, 3 corresponding to left, // bottom, right, top.
int64_t side = std::fmod((direction + 5 * kPi / 4), 2 * kPi) * 2 / kPi; auto get_distance_from_center = [&](size_t gid) {
Rect r = frame_dim.GroupRect(gid);
int64_t gcx = r.x0() + group_dim / 2;
int64_t gcy = r.y0() + group_dim / 2;
int64_t dx = gcx - cx;
int64_t dy = gcy - cy; // The angle is determined by taking atan2 and adding an appropriate // starting point depending on the side we want to start on. double angle = std::remainder(
std::atan2(dy, dx) + kPi / 4 + side * (kPi / 2), 2 * kPi); // Concentric squares in clockwise order. return std::make_pair(std::max(std::abs(dx), std::abs(dy)), angle);
};
std::sort(ac_group_order.begin(), ac_group_order.end(),
[&](coeff_order_t a, coeff_order_t b) { return get_distance_from_center(a) < get_distance_from_center(b);
});
std::vector<coeff_order_t> inv_ac_group_order(ac_group_order.size(), 0); for (size_t i = 0; i < ac_group_order.size(); i++) {
inv_ac_group_order[ac_group_order[i]] = i;
} for (size_t i = 0; i < num_passes; i++) {
size_t pass_start = permutation->size(); for (coeff_order_t v : inv_ac_group_order) {
permutation->push_back(pass_start + v);
}
}
std::vector<std::unique_ptr<BitWriter>> new_group_codes(group_codes->size()); for (size_t i = 0; i < permutation->size(); i++) {
new_group_codes[(*permutation)[i]] = std::move((*group_codes)[i]);
}
group_codes->swap(new_group_codes); returntrue;
}
// Assert that this metadata is correctly set up for the compression params, // this should have been done by enc_file.cc
JXL_ENSURE(metadata->m.xyb_encoded ==
(cparams.color_transform == ColorTransform::kXYB));
if (frame_data.IsJPEG() && cparams.color_transform == ColorTransform::kXYB) { return JXL_FAILURE("Can't add JPEG frame to XYB codestream");
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.