当前位置: 首页 > article >正文

【OpenCV】imread函数的简单分析

目录

  • 1.imread()
    • 1.1 imread()
    • 1.2 imread_()
      • 1.2.1 查找解码器(findDecoder)
      • 1.2.2 读取数据头(JpegDecoder-->readHeader)
        • 1.2.2.1 初始化错误信息(jpeg_std_error)
        • 1.2.2.2 创建jpeg解压缩对象(jpeg_create_decompress)
        • 1.2.2.3 数据拷贝(jpeg_stdio_src)
        • 1.2.2.4 设置markers(jpeg_save_markers)
        • 1.2.2.5 Jpeg读取头部(jpeg_read_header)
      • 1.2.3 读取数据(JpegDecoder-->readData)
        • 1.2.3.1 解压缩初始化(jpeg_start_decompress)
          • 1.2.3.1.1 主控制器初始化(jinit_master_decompress)
          • 1.2.3.1.2 输出初始化(output_pass_setup)
        • 1.2.3.2 解压缩(jpeg_read_scanlines)
          • 1.2.3.2.1 熵解码(decode_mcu)
          • 1.2.3.2.2 反变换量化(inverse_DCT)
          • 1.2.3.2.3 后处理(post_process_data)
        • 1.2.3.3 解压缩结束(jpeg_finish_decompress)

OpenCV是图像处理领域中一个非常重要的工具库,其中包含了许多传统图像处理算法,还支持深度图像处理的接口,是视频图像领域工程师需要熟悉的内容。这里简单分析一下imread()中C++版本的工作流程

1.imread()

imread()用于从指定文件中读取一张图片,返回一个OpenCV matrix(Mat格式)。读取图片的格式支持bmp,gif,jpeg等等,其中exif信息指的是Exchangeable Image File Format,即可交换图像文件格式,这种格式主要用于数码相机和其它影像设备拍摄的照片和视频中,它能够在jpeg、tiff等图像文件格式中嵌入诸如拍摄时间、设备型号、曝光参数、地理定位等信息,即元信息

由于读取的文件是压缩的格式(如jpeg等),所以在读取过程中需要先进行解码,然后转换成为cv::mat格式,才能够自由的使用。imread()整体框图如下所示,并以读取jpeg图片为例,大致流程为
(1)查找合适的解码器(findDecoder())
(2)设置图像处理参数
(3)解析头信息(decoder->readHeader())
  (a)初始化错误信息(jpeg_std_err())
  (b)创建解压缩对象(jpeg_create_decompress())
  (c)数据拷贝(jpeg_buffer_src())
  (e)解析头信息(jpeg_read_header())
(4)验证图像参数
(5)解析图像内容(decoder->readData())
  (a)初始化解压缩函数(jpeg_start_decompress())
  (b)执行解压缩(jpeg_read_scanlines)
  (c)确保数据处理完成并释放(jpeg_finish_decompress())
在这里插入图片描述

1.1 imread()

函数的声明位于sources/modules/imgcodecs/src/loadsave.hpp

/** @brief Loads an image from a file.# 支持的格式,包括bmp,git,jpeg等等
-   Windows bitmaps - \*.bmp, \*.dib (always supported)
-   GIF files - \*.gif (always supported)
-   JPEG files - \*.jpeg, \*.jpg, \*.jpe (see the *Note* section)@note
# 读取时,根据文件的内容来确定格式,而不是文件名后缀
-   The function determines the type of an image by its content, not by the file extension.
# 读取文件之后,会对文件进行解码,并以BGR顺序存储通道
-   In the case of color images, the decoded images will have the channels stored in **B G R** order.@param filename Name of the file to be loaded.
@param flags Flag that can take values of `cv::ImreadModes`.
*/
// 读取的格式 flag 缺省为 BGR
CV_EXPORTS_W Mat imread( const String& filename, int flags = IMREAD_COLOR_BGR );

定义位于sources/modules/imgcodecs/src/loadsave.cpp

/*** Read an image**  This function merely calls the actual implementation above and returns itself.** @param[in] filename File to load* @param[in] flags Flags you wish to set.
*/
Mat imread( const String& filename, int flags )
{// 性能分析和追踪的宏,记录了调用函数名和进入的时间CV_TRACE_FUNCTION();/// create the basic containerMat img;/// load the data// 读取数据入口imread_( filename, flags, img );/// return a reference to the datareturn img;
}

1.2 imread_()

imread_()函数具体执行读取图像文件的任务,主要工作流程是:
(1)根据图像文件来查找可用的解码器(findDecoder)
(2)设置图像处理参数
(3)读取图像文件头(readHeader)
(4)读取及解码图像数据(readData)

static bool
imread_( const String& filename, int flags, OutputArray mat )
{/// Search for the relevant decoder to handle the imagery// 图片解码器ImageDecoder decoder;#ifdef HAVE_GDALif(flags != IMREAD_UNCHANGED && (flags & IMREAD_LOAD_GDAL) == IMREAD_LOAD_GDAL ){decoder = GdalDecoder().newDecoder();}else{
#endif/* 1.根据图像文件来查找可用的解码器 */ decoder = findDecoder( filename ); 	// 用于后续scale、readHeader等操作
#ifdef HAVE_GDAL}
#endif/// if no decoder was found, return nothing.if( !decoder ){return 0;}/* 2.设置图像处理参数 */// 设置缩放因子int scale_denom = 1;if( flags > IMREAD_LOAD_GDAL ){if( flags & IMREAD_REDUCED_GRAYSCALE_2 )scale_denom = 2;else if( flags & IMREAD_REDUCED_GRAYSCALE_4 )scale_denom = 4;else if( flags & IMREAD_REDUCED_GRAYSCALE_8 )scale_denom = 8;}// Try to decode image by RGB instead of BGR.// 尝试用RGB格式来解码图像,而不是BGRif (flags & IMREAD_COLOR_RGB && flags != IMREAD_UNCHANGED){decoder->setRGB(true);}/// set the scale_denom in the driverdecoder->setScale( scale_denom );/// set the filename in the driverdecoder->setSource( filename );try{// read the header to make sure it succeeds/* 3.读取文件头 */ if( !decoder->readHeader() )return 0;}catch (const cv::Exception& e){CV_LOG_ERROR(NULL, "imread_('" << filename << "'): can't read header: " << e.what());return 0;}catch (...){CV_LOG_ERROR(NULL, "imread_('" << filename << "'): can't read header: unknown exception");return 0;}// established the required input image size// 确保输入的图像尺寸有效Size size = validateInputImageSize(Size(decoder->width(), decoder->height()));// grab the decoded type// 获取解码的类型const int type = calcType(decoder->type(), flags);// 矩阵为空,则创建新的矩阵;否则,检查当前矩阵的信息if (mat.empty()){mat.create( size.height, size.width, type );}else{CV_CheckEQ(size, mat.size(), "");CV_CheckTypeEQ(type, mat.type(), "");CV_Assert(mat.isContinuous());}// read the image dataMat real_mat = mat.getMat();const void * original_ptr = real_mat.data;bool success = false;try{	/* 4.读取及解码图像数据 */if (decoder->readData(real_mat)){CV_CheckTrue(original_ptr == real_mat.data, "Internal imread issue");success = true;}}catch (const cv::Exception& e){CV_LOG_ERROR(NULL, "imread_('" << filename << "'): can't read data: " << e.what());}catch (...){CV_LOG_ERROR(NULL, "imread_('" << filename << "'): can't read data: unknown exception");}if (!success){mat.release();return false;}if( decoder->setScale( scale_denom ) > 1 ) // if decoder is JpegDecoder then decoder->setScale always returns 1{resize( mat, mat, Size( size.width / scale_denom, size.height / scale_denom ), 0, 0, INTER_LINEAR_EXACT);}/// optionally rotate the data if EXIF orientation flag says soif (!mat.empty() && (flags & IMREAD_IGNORE_ORIENTATION) == 0 && flags != IMREAD_UNCHANGED ){ApplyExifOrientation(decoder->getExifTag(ORIENTATION), mat);}return true;
}

1.2.1 查找解码器(findDecoder)

查找一个合适的解码器,例如bmp格式或者jpeg格式,使用的是BmpDecoder或JpegDecoder

static ImageDecoder findDecoder( const String& filename ) {size_t i, maxlen = 0;/// iterate through list of registered codecsImageCodecInitializer& codecs = getCodecs();for( i = 0; i < codecs.decoders.size(); i++ ){size_t len = codecs.decoders[i]->signatureLength();maxlen = std::max(maxlen, len);}/// Open the fileFILE* f= fopen( filename.c_str(), "rb" );/// in the event of a failure, return an empty image decoderif( !f ) {CV_LOG_WARNING(NULL, "imread_('" << filename << "'): can't open/read file: check file path/integrity");return ImageDecoder();}// read the file signatureString signature(maxlen, ' ');// 读取图像文件的前几个字符,这几个字符描述了当前文件的格式maxlen = fread( (void*)signature.c_str(), 1, maxlen, f );fclose(f);signature = signature.substr(0, maxlen);/// compare signature against all decoders// 寻找decoder列表中是否有这几个字符for( i = 0; i < codecs.decoders.size(); i++ ){if( codecs.decoders[i]->checkSignature(signature) )return codecs.decoders[i]->newDecoder();}/// If no decoder was found, return base typereturn ImageDecoder();
}

1.2.2 读取数据头(JpegDecoder–>readHeader)

数据头的读取函数由具体的解码器实现,以JpegDecoder为例
(1)初始化错误信息(jpeg_std_error)
(2)创建解码对象(jpeg_create_decompress)
(3)准备接收数据(jpeg_stdio_src)
(4)设置信息保留字段(jpeg_save_markers)
(5)解析头部(jpeg_read_header)

bool  JpegDecoder::readHeader()
{volatile bool result = false;close(); // 清理历史信息JpegState* state = new JpegState;m_state = state;/* 1.初始化错误信息 */state->cinfo.err = jpeg_std_error(&state->jerr.pub);state->jerr.pub.error_exit = error_exit;/* 一种非局部跳转的机制,用来处理异常或者错误情况,首次调用 setjmp 时,它会保存当前的执行环境到 buffer 中,并返回 0,这允许你在代码中设置一个“检查点”,如果后续发生错误或其他条件触发 longjmp,程序控制流将立即返回到这个 setjmp 调用的地方,并恢复当时保存的环境状态*/if( setjmp( state->jerr.setjmp_buffer ) == 0 ){/* 2.创建解码对象 */jpeg_create_decompress( &state->cinfo );// 如果内存中已经存入数据,则从source中拷贝数据到cinfo中;// 否则,从文件中读取数据if( !m_buf.empty() ){jpeg_buffer_src(&state->cinfo, &state->source);state->source.pub.next_input_byte = m_buf.ptr();state->source.pub.bytes_in_buffer = m_buf.cols*m_buf.rows*m_buf.elemSize();}else{/* 3.准备接收数据 */m_f = fopen( m_filename.c_str(), "rb" );if( m_f )jpeg_stdio_src( &state->cinfo, m_f );}if (state->cinfo.src != 0){/*4.设置信息保留字段(1) 保留 JPEG 图像中的 EXIF 数据(EXIF 一般放在 APP1 中)(2) 0xffff 表示最大长度用于在解码过程中保留图像的元信息*/jpeg_save_markers(&state->cinfo, APP1, 0xffff);/*5.读取并解析 JPEG 文件的头部信息(1) TRUE表示允许对jpeg头部进行一些修正,例如不完整的头部执行后,cinfo 结构体中将填充图像的基本信息:(a) 宽度(image_width)(b) 高度(image_height)(c) 颜色通道数(num_components)(d) 数据精度(data_precision)(e) 是否是彩色图像等*/jpeg_read_header( &state->cinfo, TRUE );// 设置 JPEG 解码器的输出尺寸缩放比例state->cinfo.scale_num=1;state->cinfo.scale_denom = m_scale_denom;m_scale_denom=1; // trick! to know which decoder used scale_denom see imread_// 根据当前解码器配置(包括缩放参数)计算最终输出图像的尺寸jpeg_calc_output_dimensions(&state->cinfo);m_width = state->cinfo.output_width;m_height = state->cinfo.output_height;// 通道数m_type = state->cinfo.num_components > 1 ? CV_8UC3 : CV_8UC1;result = true;}}return result;
}
1.2.2.1 初始化错误信息(jpeg_std_error)

错误信息的初始化使用的是函数指针

GLOBAL(struct jpeg_error_mgr *)
jpeg_std_error(struct jpeg_error_mgr *err)
{memset(err, 0, sizeof(struct jpeg_error_mgr));err->error_exit = error_exit;err->emit_message = emit_message;err->output_message = output_message;err->format_message = format_message;err->reset_error_mgr = reset_error_mgr;/* Initialize message table pointers */err->jpeg_message_table = jpeg_std_message_table;err->last_jpeg_message = (int)JMSG_LASTMSGCODE - 1;return err;
}

错误退出函数error_exit(),其中METHODDEF的定义为static,即将函数的返回值定义为static类型。查阅资料得知,这样定义是为了适配不同的编译环境和函数调用约定,暂时还不确定不同环境的区别

1.2.2.2 创建jpeg解压缩对象(jpeg_create_decompress)

jpeg_create_decompress是一个宏定义,调用函数jpeg_CreateDecompress()

#define jpeg_create_decompress(cinfo) \jpeg_CreateDecompress((cinfo), JPEG_LIB_VERSION, \(size_t)sizeof(struct jpeg_decompress_struct))

jpeg_CreateDecompress()定义位于sources\3rdparth\libjpeg\jdapimin.c中

// #define GLOBAL(type)            type
// 指定函数的链接属性(例如,在某些平台上可能被定义为 __declspec(dllexport) 或其他平台特定的关键字)
GLOBAL(void)
jpeg_CreateDecompress(j_decompress_ptr cinfo, int version, size_t structsize)
{// ...// 初始化结构体{struct jpeg_error_mgr *err = cinfo->err;void *client_data = cinfo->client_data; /* ignore Purify complaint here */memset(cinfo, 0, sizeof(struct jpeg_decompress_struct));cinfo->err = err;cinfo->client_data = client_data;}cinfo->is_decompressor = TRUE;/* Initialize a memory manager instance for this object */jinit_memory_mgr((j_common_ptr)cinfo);// ...
}
1.2.2.3 数据拷贝(jpeg_stdio_src)

函数的作用是设置标准输入源的函数

GLOBAL(void)
jpeg_stdio_src(j_decompress_ptr cinfo, FILE *infile)
{my_src_ptr src;/* The source object and input buffer are made permanent so that a series* of JPEG images can be read from the same file by calling jpeg_stdio_src* only before the first one.  (If we discarded the buffer at the end of* one image, we'd likely lose the start of the next one.)*/if (cinfo->src == NULL) {     /* first time for this JPEG object? */cinfo->src = (struct jpeg_source_mgr *)(*cinfo->mem->alloc_small) ((j_common_ptr)cinfo, JPOOL_PERMANENT,sizeof(my_source_mgr));src = (my_src_ptr)cinfo->src;src->buffer = (JOCTET *)(*cinfo->mem->alloc_small) ((j_common_ptr)cinfo, JPOOL_PERMANENT,INPUT_BUF_SIZE * sizeof(JOCTET));} else if (cinfo->src->init_source != init_source) {/* It is unsafe to reuse the existing source manager unless it was created* by this function.  Otherwise, there is no guarantee that the opaque* structure is the right size.  Note that we could just create a new* structure, but the old structure would not be freed until* jpeg_destroy_decompress() was called.*/ERREXIT(cinfo, JERR_BUFFER_SIZE);}src = (my_src_ptr)cinfo->src;src->pub.init_source = init_source;// 从文件中读取数据,填充到buffer中src->pub.fill_input_buffer = fill_input_buffer;src->pub.skip_input_data = skip_input_data;src->pub.resync_to_restart = jpeg_resync_to_restart; /* use default method */src->pub.term_source = term_source;src->infile = infile;src->pub.bytes_in_buffer = 0; /* forces fill_input_buffer on first read */src->pub.next_input_byte = NULL; /* until buffer loaded */
}
1.2.2.4 设置markers(jpeg_save_markers)

该函数的作用是告诉 libjpeg 在解码 JPEG 文件时,是否应该保存某些特定类型的标记段(marker segment),并设置这些标记段的最大保存长度。标记段用marker_code描述,最大长度用length_limit描述

GLOBAL(void)
jpeg_save_markers(j_decompress_ptr cinfo, int marker_code,unsigned int length_limit)
{// .../* Choose processor routine to use.* APP0/APP14 have special requirements.*/if (length_limit) {// 需要保存marker数据processor = save_marker;/* If saving APP0/APP14, save at least enough for our internal use. */if (marker_code == (int)M_APP0 && length_limit < APP0_DATA_LEN)length_limit = APP0_DATA_LEN;else if (marker_code == (int)M_APP14 && length_limit < APP14_DATA_LEN)length_limit = APP14_DATA_LEN;} else {// 不需要保存marker数据processor = skip_variable;/* If discarding APP0/APP14, use our regular on-the-fly processor. */if (marker_code == (int)M_APP0 || marker_code == (int)M_APP14)processor = get_interesting_appn;}// ...
}
1.2.2.5 Jpeg读取头部(jpeg_read_header)

函数是 libjpeg 库的一部分,用于读取 JPEG 数据流中的头部信息

GLOBAL(int)
jpeg_read_header (j_decompress_ptr cinfo, boolean require_image)
{// .../*jpeg_consume_input负责从输入源读取并处理 JPEG 数据,这里是处理头部数据(1) JPEG_REACHED_SOS: 已经读到了扫描开始标记(SOS)(2) JPEG_REACHED_EOI: 已经到达了图像结束标记(EOI)(3) JPEG_SUSPENDED: 输入数据不足,需要更多数据才能继续*/retcode = jpeg_consume_input(cinfo);switch (retcode) {case JPEG_REACHED_SOS: // 成功解析了图像的头信息retcode = JPEG_HEADER_OK;break;case JPEG_REACHED_EOI: // 没有找到图像,找到了EOI符号if (require_image)		/* Complain if application wanted an image */ERREXIT(cinfo, JERR_NO_IMAGE);/* Reset to start state; it would be safer to require the application to* call jpeg_abort, but we can't change it now for compatibility reasons.* A side effect is to free any temporary memory (there shouldn't be any).*/jpeg_abort((j_common_ptr) cinfo); /* sets state = DSTATE_START */retcode = JPEG_HEADER_TABLES_ONLY;break;case JPEG_SUSPENDED: // 输入数据暂时不足,导致操作挂起,则不执行任何额外操作,直接返回/* no work */break;}return retcode;
}

这里的jpeg_consume_input()的作用是从输入源读取并处理 JPEG 头部数据

GLOBAL(int)
jpeg_consume_input(j_decompress_ptr cinfo)
{int retcode = JPEG_SUSPENDED;/* NB: every possible DSTATE value should be listed in this switch */switch (cinfo->global_state) {case DSTATE_START:/* Start-of-datastream actions: reset appropriate modules */// 重置输入控制器,确保其处于初始状态(*cinfo->inputctl->reset_input_controller) (cinfo);/* Initialize application's data source module */// 初始化输入源(例如文件或内存缓冲区),准备读取数据(*cinfo->src->init_source) (cinfo);// 设置状态为 DSTATE_INHEADER,进入读取头部阶段cinfo->global_state = DSTATE_INHEADER;// 继续进入下一个 case 分支FALLTHROUGH                 /*FALLTHROUGH*/case DSTATE_INHEADER:// 处理输入数据的头部retcode = (*cinfo->inputctl->consume_input) (cinfo);// 返回值为 JPEG_REACHED_SOS(找到了 SOS 段),说明已经读到了图像数据的起始位置if (retcode == JPEG_REACHED_SOS) { /* Found SOS, prepare to decompress *//* Set up default parameters based on header data */// 设置默认解码参数(基于头部信息)default_decompress_parms(cinfo);/* Set global state: ready for start_decompress */// 状态更新为 DSTATE_READY,表示可以开始解码图像了cinfo->global_state = DSTATE_READY;}break;case DSTATE_READY:/* Can't advance past first SOS until start_decompress is called */retcode = JPEG_REACHED_SOS;break;// 这些状态表示解码过程正在进行中,例如正在预扫描、主扫描、缓冲图像数据等case DSTATE_PRELOAD:case DSTATE_PRESCAN:case DSTATE_SCANNING:case DSTATE_RAW_OK:case DSTATE_BUFIMAGE:case DSTATE_BUFPOST:case DSTATE_STOPPING:retcode = (*cinfo->inputctl->consume_input) (cinfo);break;default: // 如果当前状态不在预期范围内,则抛出错误,说明程序内部状态异常ERREXIT1(cinfo, JERR_BAD_STATE, cinfo->global_state);}return retcode;
}

reset_input_controller()的定义位于sources/3rdparty/libjpeg/jdinput.c中

METHODDEF(void)
reset_input_controller (j_decompress_ptr cinfo)
{my_inputctl_ptr inputctl = (my_inputctl_ptr) cinfo->inputctl;// 数据处理函数设置为consume_markers,处理标记符inputctl->pub.consume_input = consume_markers;inputctl->pub.has_multiple_scans = FALSE; /* "unknown" would be better */inputctl->pub.eoi_reached = FALSE;inputctl->inheaders = 1;/* Reset other modules */(*cinfo->err->reset_error_mgr) ((j_common_ptr) cinfo);(*cinfo->marker->reset_marker_reader) (cinfo);/* Reset progression state -- would be cleaner if entropy decoder did this */cinfo->coef_bits = NULL;
}

consume_markers()的定义如下,其工作流程大致为

/*consume_markers()↓检查是否已到 EOI?├─ 是 → 返回 JPEG_REACHED_EOI└─ 否 → 进入主循环↓read_markers() 读取下一个 marker↓根据返回值处理:JPEG_REACHED_SOS → 处理 SOS(首次/多次)JPEG_REACHED_EOI → 设置 eoi_reachedJPEG_SUSPENDED → 返回暂停状态其他 → 返回原值
*/
METHODDEF(int)
consume_markers (j_decompress_ptr cinfo)
{my_inputctl_ptr inputctl = (my_inputctl_ptr) cinfo->inputctl;int val;if (inputctl->pub.eoi_reached) /* After hitting EOI, read no further */return JPEG_REACHED_EOI;for (;;) {			/* Loop to pass pseudo SOS marker */// 不断读取markerval = (*cinfo->marker->read_markers) (cinfo);switch (val) {case JPEG_REACHED_SOS:	/* Found SOS */if (inputctl->inheaders) { /* 1st SOS 第一个SOS标识符 */if (inputctl->inheaders == 1)initial_setup(cinfo); // 初始设置(如分配内存、初始化 DCT 参数等)if (cinfo->comps_in_scan == 0) { /* pseudo SOS marker */ /* 假 SOS(伪标记)*/inputctl->inheaders = 2; // 等待下一次真正的 SOSbreak;}inputctl->inheaders = 0;/* Note: start_input_pass must be called by jdmaster.c* before any more input can be consumed.  jdapimin.c is* responsible for enforcing this sequencing.*/} else {			/* 2nd or later SOS marker */if (! inputctl->pub.has_multiple_scans)ERREXIT(cinfo, JERR_EOI_EXPECTED); /* Oops, I wasn't expecting this! */if (cinfo->comps_in_scan == 0) /* unexpected pseudo SOS marker */break;start_input_pass(cinfo); // 开始新的扫描}return val;case JPEG_REACHED_EOI:	/* Found EOI */inputctl->pub.eoi_reached = TRUE;if (inputctl->inheaders) { /* Tables-only datastream, apparently */ /* 只有表头,没有图像数据 */if (cinfo->marker->saw_SOF)ERREXIT(cinfo, JERR_SOF_NO_SOS);} else {/* Prevent infinite loop in coef ctlr's decompress_data routine* if user set output_scan_number larger than number of scans.*/if (cinfo->output_scan_number > cinfo->input_scan_number)cinfo->output_scan_number = cinfo->input_scan_number;}return val;case JPEG_SUSPENDED:return val;default:return val;}}
}

1.2.3 读取数据(JpegDecoder–>readData)

前面正确解析了图像数据的头部,现在读取图像数据并解析,主要有4个步骤:
(1)分析信息,包括通道数、Exif信息
(2)解码初始化(jpeg_start_decompress)
(3)读取数据,进行解码(jpeg_read_scanlines)
(4)结束解压缩(jpeg_finish_decompress)

bool  JpegDecoder::readData( Mat& img )
{volatile bool result = false;const bool color = img.channels() > 1;if( m_state && m_width && m_height ){jpeg_decompress_struct* cinfo = &((JpegState*)m_state)->cinfo;JpegErrorMgr* jerr = &((JpegState*)m_state)->jerr;if( setjmp( jerr->setjmp_buffer ) == 0 ){// ...// See https://github.com/opencv/opencv/issues/25274// Conversion CMYK->BGR is not supported in libjpeg-turbo.// So supporting both directly and indirectly is necessary.bool doDirectRead = false;/* 1.分析信息 */if( color ) /* 彩色图像 (多通道) */{if( cinfo->num_components != 4 ) // 不是4通道{
#ifdef JCS_EXTENSIONS // default 1cinfo->out_color_space = m_use_rgb ? JCS_EXT_RGB : JCS_EXT_BGR;cinfo->out_color_components = 3;doDirectRead = true; // BGR -> BGR
#elsecinfo->out_color_space = JCS_RGB;cinfo->out_color_components = 3;doDirectRead = m_use_rgb ? true : false; // RGB -> BGR
#endif}else{cinfo->out_color_space = JCS_CMYK; // CMYK格式cinfo->out_color_components = 4;doDirectRead = false; // CMYK -> BGR}}else /* 灰度图像 */{if( cinfo->num_components != 4 ){cinfo->out_color_space = JCS_GRAYSCALE;cinfo->out_color_components = 1;doDirectRead = true; // GRAY -> GRAY}else{cinfo->out_color_space = JCS_CMYK;cinfo->out_color_components = 4;doDirectRead = false; // CMYK -> GRAY}}// Check for Exif marker APP1/* 寻找 Exif 信息所在的 APP1 标记 */jpeg_saved_marker_ptr exif_marker = NULL;jpeg_saved_marker_ptr cmarker = cinfo->marker_list;while( cmarker && exif_marker == NULL ){if (cmarker->marker == APP1)exif_marker = cmarker;cmarker = cmarker->next;}// Parse Exif dataif( exif_marker ){const std::streamsize offsetToTiffHeader = 6; //bytes from Exif size field to the first TIFF headerif (exif_marker->data_length > offsetToTiffHeader){m_exif.parseExif(exif_marker->data + offsetToTiffHeader, exif_marker->data_length - offsetToTiffHeader);}}/* 2.解码初始化 */ jpeg_start_decompress( cinfo );if( doDirectRead){for( int iy = 0 ; iy < m_height; iy ++ ){uchar* data = img.ptr<uchar>(iy);/* 3.读取数据,进行解码 */if (jpeg_read_scanlines( cinfo, &data, 1 ) != 1) return false;}}else{JSAMPARRAY buffer = (*cinfo->mem->alloc_sarray)((j_common_ptr)cinfo,JPOOL_IMAGE, m_width*4, 1 );for( int iy = 0 ; iy < m_height; iy ++ ){uchar* data = img.ptr<uchar>(iy);if (jpeg_read_scanlines( cinfo, buffer, 1 ) != 1) return false;// ...}}result = true;/* 4.结束解压缩 */ jpeg_finish_decompress( cinfo );}}return result;
}
1.2.3.1 解压缩初始化(jpeg_start_decompress)

jpeg_start_decompress()是libjpeg中的一个函数,用于解压缩jpeg图像前的初始化。如果只读取一张jpeg图片,函数只会被调用一次,此时cinfo->global_state应该是DSTATE_READY状态,会调用jinit_master_decompress()执行主控制器的初始化

GLOBAL(boolean)
jpeg_start_decompress (j_decompress_ptr cinfo)
{if (cinfo->global_state == DSTATE_READY) {/* First call: initialize master control, select active modules */jinit_master_decompress(cinfo);if (cinfo->buffered_image) { // 使用图像缓冲模式,返回true,后续工作交给jpeg_start_output完成/* No more work here; expecting jpeg_start_output next */cinfo->global_state = DSTATE_BUFIMAGE;return TRUE;}cinfo->global_state = DSTATE_PRELOAD;}if (cinfo->global_state == DSTATE_PRELOAD) {// ...cinfo->output_scan_number = cinfo->input_scan_number;} else if (cinfo->global_state != DSTATE_PRESCAN)ERREXIT1(cinfo, JERR_BAD_STATE, cinfo->global_state);/* Perform any dummy output passes, and set up for the final pass */return output_pass_setup(cinfo);
}
1.2.3.1.1 主控制器初始化(jinit_master_decompress)

jinit_master_decompress()定义位于sources\3rdparty\libjpeg\jdmaster.c中,会根据上下文信息初始化主控制器

GLOBAL(void)
jinit_master_decompress (j_decompress_ptr cinfo)
{my_master_ptr master;master = (my_master_ptr) (*cinfo->mem->alloc_small)((j_common_ptr) cinfo, JPOOL_IMAGE, SIZEOF(my_decomp_master));cinfo->master = &master->pub;// 在输出图像数据之前初始化各个解码模块master->pub.prepare_for_output_pass = prepare_for_output_pass;// 结束输出master->pub.finish_output_pass = finish_output_pass;master->pub.is_dummy_pass = FALSE;// 据图像格式和用户配置,选择并初始化所有需要用到的解码模块master_selection(cinfo);
}

master_selection()初始化一系列模块,包括熵解码、IDCT、缓冲区控制器等

LOCAL(void)
master_selection (j_decompress_ptr cinfo)
{// ... /* Post-processing: in particular, color conversion first */if (! cinfo->raw_data_out) {if (master->using_merged_upsample) {
#ifdef UPSAMPLE_MERGING_SUPPORTEDjinit_merged_upsampler(cinfo); /* does color conversion too */
#elseERREXIT(cinfo, JERR_NOT_COMPILED);
#endif} else {jinit_color_deconverter(cinfo);jinit_upsampler(cinfo);}jinit_d_post_controller(cinfo, cinfo->enable_2pass_quant);}/* Inverse DCT */jinit_inverse_dct(cinfo); // 初始化IDCT/* Entropy decoding: either Huffman or arithmetic coding. */if (cinfo->arith_code)jinit_arith_decoder(cinfo); // 初始化算术解码器else {jinit_huff_decoder(cinfo); // 初始化huffman解码器}/* Initialize principal buffer controllers. */use_c_buffer = cinfo->inputctl->has_multiple_scans || cinfo->buffered_image;jinit_d_coef_controller(cinfo, use_c_buffer); // 初始化缓冲区控制器// ...
#endif /* D_MULTISCAN_FILES_SUPPORTED */
}
1.2.3.1.2 输出初始化(output_pass_setup)
LOCAL(boolean)
output_pass_setup (j_decompress_ptr cinfo)
{if (cinfo->global_state != DSTATE_PRESCAN) {/* First call: do pass setup */(*cinfo->master->prepare_for_output_pass) (cinfo); // 首次调用,初始化output passcinfo->output_scanline = 0;cinfo->global_state = DSTATE_PRESCAN;}/* Loop over any required dummy passes */while (cinfo->master->is_dummy_pass) {
#ifdef QUANT_2PASS_SUPPORTED/* Crank through the dummy pass */while (cinfo->output_scanline < cinfo->output_height) {JDIMENSION last_scanline;/* Call progress monitor hook if present */if (cinfo->progress != NULL) {cinfo->progress->pass_counter = (long) cinfo->output_scanline;cinfo->progress->pass_limit = (long) cinfo->output_height;(*cinfo->progress->progress_monitor) ((j_common_ptr) cinfo);}/* Process some data */last_scanline = cinfo->output_scanline;(*cinfo->main->process_data) (cinfo, (JSAMPARRAY) NULL,&cinfo->output_scanline, (JDIMENSION) 0);if (cinfo->output_scanline == last_scanline)return FALSE;		/* No progress made, must suspend */}/* Finish up dummy pass, and set up for another one */// 初始化输出(*cinfo->master->finish_output_pass) (cinfo);(*cinfo->master->prepare_for_output_pass) (cinfo);cinfo->output_scanline = 0;
#elseERREXIT(cinfo, JERR_NOT_COMPILED);
#endif /* QUANT_2PASS_SUPPORTED */}/* Ready for application to drive output pass through* jpeg_read_scanlines or jpeg_read_raw_data.*/cinfo->global_state = cinfo->raw_data_out ? DSTATE_RAW_OK : DSTATE_SCANNING;return TRUE;
}

prepare_for_output_pass()会初始化一些解码模块,包括反离散余弦变换(IDCT),系数读取/解码,cconvert 颜色空间转换(如 YCbCr → RGB),upsample上采样(chroma 上采样),cquantize 颜色量化(用于减少颜色数量),post 后处理缓冲区控制,main主输出控制器

METHODDEF(void)
prepare_for_output_pass (j_decompress_ptr cinfo)
{my_master_ptr master = (my_master_ptr) cinfo->master;if (master->pub.is_dummy_pass) { /* 虚拟输出(无输出) */
#ifdef QUANT_2PASS_SUPPORTED/* Final pass of 2-pass quantization */master->pub.is_dummy_pass = FALSE;(*cinfo->cquantize->start_pass) (cinfo, FALSE);(*cinfo->post->start_pass) (cinfo, JBUF_CRANK_DEST);(*cinfo->main->start_pass) (cinfo, JBUF_CRANK_DEST);
#elseERREXIT(cinfo, JERR_NOT_COMPILED);
#endif /* QUANT_2PASS_SUPPORTED */} else {if (cinfo->quantize_colors && cinfo->colormap == NULL) { // 启用了颜色量化但还没有颜色表/* Select new quantization method */if (cinfo->two_pass_quantize && cinfo->enable_2pass_quant) { // 检查是1遍量化还是2遍cinfo->cquantize = master->quantizer_2pass;master->pub.is_dummy_pass = TRUE;} else if (cinfo->enable_1pass_quant) {cinfo->cquantize = master->quantizer_1pass;} else {ERREXIT(cinfo, JERR_MODE_CHANGE);}}// 初始化反离散余弦变换(*cinfo->idct->start_pass) (cinfo);// 初始化系数解码(*cinfo->coef->start_output_pass) (cinfo);if (! cinfo->raw_data_out) {if (! master->using_merged_upsample)(*cinfo->cconvert->start_pass) (cinfo); // 初始化颜色空间转换(*cinfo->upsample->start_pass) (cinfo); // 初始化上采样if (cinfo->quantize_colors) (*cinfo->cquantize->start_pass) (cinfo, master->pub.is_dummy_pass); // 初始化颜色量化(*cinfo->post->start_pass) (cinfo,(master->pub.is_dummy_pass ? JBUF_SAVE_AND_PASS : JBUF_PASS_THRU)); // 后处理缓冲区控制(*cinfo->main->start_pass) (cinfo, JBUF_PASS_THRU); // 主输出控制器}}// ...
}

finish_output_pass()中会增加一个pass的计数器

METHODDEF(void)
finish_output_pass (j_decompress_ptr cinfo)
{my_master_ptr master = (my_master_ptr) cinfo->master;if (cinfo->quantize_colors)(*cinfo->cquantize->finish_pass) (cinfo);master->pass_number++;
}
1.2.3.2 解压缩(jpeg_read_scanlines)
/*(1) scanlines表示处理的一行数据(2) max_lines表示要处理多少行,调用时通常为1
*/
GLOBAL(JDIMENSION)
jpeg_read_scanlines (j_decompress_ptr cinfo, JSAMPARRAY scanlines,JDIMENSION max_lines)
{JDIMENSION row_ctr;if (cinfo->global_state != DSTATE_SCANNING)ERREXIT1(cinfo, JERR_BAD_STATE, cinfo->global_state);/*(1) output_height在jpeg_read_header()中确定,解压缩之后的图像为800x600,那么output_height=600(2) output_scanline表示扫描行数,不能超过output_height*/if (cinfo->output_scanline >= cinfo->output_height) {WARNMS(cinfo, JWRN_TOO_MUCH_DATA);return 0;}/* Call progress monitor hook if present */if (cinfo->progress != NULL) {cinfo->progress->pass_counter = (long) cinfo->output_scanline;cinfo->progress->pass_limit = (long) cinfo->output_height;(*cinfo->progress->progress_monitor) ((j_common_ptr) cinfo);}/* Process some data */row_ctr = 0;(*cinfo->main->process_data) (cinfo, scanlines, &row_ctr, max_lines); // 数据处理cinfo->output_scanline += row_ctr;return row_ctr;
}

cinfo->main->process_data通常情况下调用的是process_data_context_main(),进入了主解码函数当中,随后调用coef->decompress_data进行解码(在coef模块中),最后进行post->post_process_data后处理(在post模块中)

METHODDEF(void)
process_data_context_main(j_decompress_ptr cinfo, _JSAMPARRAY output_buf,JDIMENSION *out_row_ctr, JDIMENSION out_rows_avail)
{my_main_ptr main_ptr = (my_main_ptr)cinfo->main;/* Read input data if we haven't filled the main buffer yet */// 如果main buffer未满,先解压缩数据,填充bufferif (!main_ptr->buffer_full) {if (!(*cinfo->coef->_decompress_data) (cinfo,main_ptr->xbuffer[main_ptr->whichptr]))return;                   /* suspension forced, can do nothing more */main_ptr->buffer_full = TRUE;       /* OK, we have an iMCU row to work with */main_ptr->iMCU_row_ctr++;   /* count rows received */}/* Postprocessor typically will not swallow all the input data it is handed* in one call (due to filling the output buffer first).  Must be prepared* to exit and restart.  This switch lets us keep track of how far we got.* Note that each case falls through to the next on successful completion.*/switch (main_ptr->context_state) {case CTX_POSTPONED_ROW: /* 处理被推迟的行 *//* Call postprocessor using previously set pointers for postponed row */(*cinfo->post->_post_process_data) (cinfo,main_ptr->xbuffer[main_ptr->whichptr],&main_ptr->rowgroup_ctr,main_ptr->rowgroups_avail, output_buf,out_row_ctr, out_rows_avail);if (main_ptr->rowgroup_ctr < main_ptr->rowgroups_avail)return;                   /* Need to suspend */main_ptr->context_state = CTX_PREPARE_FOR_IMCU;if (*out_row_ctr >= out_rows_avail)return;                   /* Postprocessor exactly filled output buf */FALLTHROUGH                 /*FALLTHROUGH*/case CTX_PREPARE_FOR_IMCU: /* 准备处理下一个iMCU行 *//* Prepare to process first M-1 row groups of this iMCU row */main_ptr->rowgroup_ctr = 0;main_ptr->rowgroups_avail = (JDIMENSION)(cinfo->_min_DCT_scaled_size - 1);/* Check for bottom of image: if so, tweak pointers to "duplicate"* the last sample row, and adjust rowgroups_avail to ignore padding rows.*/if (main_ptr->iMCU_row_ctr == cinfo->total_iMCU_rows)set_bottom_pointers(cinfo);main_ptr->context_state = CTX_PROCESS_IMCU;FALLTHROUGH                 /*FALLTHROUGH*/case CTX_PROCESS_IMCU: /* 实际处理iMCU行 *//* Call postprocessor using previously set pointers */(*cinfo->post->_post_process_data) (cinfo,main_ptr->xbuffer[main_ptr->whichptr],&main_ptr->rowgroup_ctr,main_ptr->rowgroups_avail, output_buf,out_row_ctr, out_rows_avail);if (main_ptr->rowgroup_ctr < main_ptr->rowgroups_avail)return;                   /* Need to suspend *//* After the first iMCU, change wraparound pointers to normal state */if (main_ptr->iMCU_row_ctr == 1)set_wraparound_pointers(cinfo);/* Prepare to load new iMCU row using other xbuffer list */main_ptr->whichptr ^= 1;    /* 0=>1 or 1=>0 */main_ptr->buffer_full = FALSE;/* Still need to process last row group of this iMCU row, *//* which is saved at index M+1 of the other xbuffer */main_ptr->rowgroup_ctr = (JDIMENSION)(cinfo->_min_DCT_scaled_size + 1);main_ptr->rowgroups_avail = (JDIMENSION)(cinfo->_min_DCT_scaled_size + 2);main_ptr->context_state = CTX_POSTPONED_ROW;}
}

解码数据使用的是sources\3rdparty\libjpeg\jdcoefct.c下decompress_onepass()函数,进行了熵解码(entropy->decode_mcu)和反变换量化(inverse_DCT)的工作

METHODDEF(int)
decompress_onepass(j_decompress_ptr cinfo, _JSAMPIMAGE output_buf)
{// .../* Loop to process as much as one whole iMCU row */for (yoffset = coef->MCU_vert_offset; yoffset < coef->MCU_rows_per_iMCU_row;yoffset++) {for (MCU_col_num = coef->MCU_ctr; MCU_col_num <= last_MCU_col;MCU_col_num++) {/* Try to fetch an MCU.  Entropy decoder expects buffer to be zeroed. */jzero_far((void *)coef->MCU_buffer[0],(size_t)(cinfo->blocks_in_MCU * sizeof(JBLOCK)));if (!cinfo->entropy->insufficient_data)cinfo->master->last_good_iMCU_row = cinfo->input_iMCU_row;if (!(*cinfo->entropy->decode_mcu) (cinfo, coef->MCU_buffer)) { // 熵解码/* Suspension forced; update state counters and exit */coef->MCU_vert_offset = yoffset;coef->MCU_ctr = MCU_col_num;return JPEG_SUSPENDED;}/* Only perform the IDCT on blocks that are contained within the desired* cropping region.*/if (MCU_col_num >= cinfo->master->first_iMCU_col &&MCU_col_num <= cinfo->master->last_iMCU_col) {/* Determine where data should go in output_buf and do the IDCT thing.* We skip dummy blocks at the right and bottom edges (but blkn gets* incremented past them!).  Note the inner loop relies on having* allocated the MCU_buffer[] blocks sequentially.*/blkn = 0;               /* index of current DCT block within MCU */for (ci = 0; ci < cinfo->comps_in_scan; ci++) {// ...for (yindex = 0; yindex < compptr->MCU_height; yindex++) {if (cinfo->input_iMCU_row < last_iMCU_row ||yoffset + yindex < compptr->last_row_height) {output_col = start_col;for (xindex = 0; xindex < useful_width; xindex++) { // 反变换量化(*inverse_DCT) (cinfo, compptr,(JCOEFPTR)coef->MCU_buffer[blkn + xindex],output_ptr, output_col);output_col += compptr->_DCT_scaled_size;}}blkn += compptr->MCU_width;output_ptr += compptr->_DCT_scaled_size;}}}}/* Completed an MCU row, but perhaps not an iMCU row */coef->MCU_ctr = 0;}// ...
}
1.2.3.2.1 熵解码(decode_mcu)

jpeg格式编码时的步骤为变换量化和熵编码,解码时首先进行熵解码,decode_mcu()调用的是jdhuff.c中的decode_mcu(),更底层的不再深入

1.2.3.2.2 反变换量化(inverse_DCT)

inverse_DCT()调用了jddctint.c中的函数,支持多种DCT变换格式,例如非8x8的变换jpeg_idct_8x16、jpeg_idct_16x16等等,也支持不同的速度,例如jpeg_idct_islow、jpeg_idct_ifast,支持浮点运算,例如jpeg_idct_float。默认情况下,使用的是_jpeg_idct_islow(),不再深入分析

1.2.3.2.3 后处理(post_process_data)

post->post_process_data()对图像进行后处理,我在调试的时候发现会跳转到jdsample.c的sep_upsample()函数中,进行上采样,其中会调用fullsize_upsample(),随后调用jdcolor.c的ycc_rgb_convert()函数进行上采样数据的格式转换

METHODDEF(void)
sep_upsample(j_decompress_ptr cinfo, _JSAMPIMAGE input_buf,JDIMENSION *in_row_group_ctr, JDIMENSION in_row_groups_avail,_JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,JDIMENSION out_rows_avail)
{// .../* Fill the conversion buffer, if it's empty */if (upsample->next_row_out >= cinfo->max_v_samp_factor) {for (ci = 0, compptr = cinfo->comp_info; ci < cinfo->num_components;ci++, compptr++) {/* Invoke per-component upsample method.  Notice we pass a POINTER* to color_buf[ci], so that fullsize_upsample can change it.*/// 上采样,调用了fullsize_upsample()(*upsample->methods[ci]) (cinfo, compptr,input_buf[ci] + (*in_row_group_ctr * upsample->rowgroup_height[ci]),upsample->color_buf + ci);}upsample->next_row_out = 0;}// ...// 颜色转换(*cinfo->cconvert->_color_convert) (cinfo, upsample->color_buf,(JDIMENSION)upsample->next_row_out,output_buf + *out_row_ctr,(int)num_rows);// ...
}
1.2.3.3 解压缩结束(jpeg_finish_decompress)

jpeg_finish_decompress 函数是 libjpeg 库中用于完成 JPEG 图像解压缩过程的一个关键函数。它的主要职责是确保所有剩余的数据都被正确处理,并且资源被适当地释放

GLOBAL(boolean)
jpeg_finish_decompress (j_decompress_ptr cinfo)
{if ((cinfo->global_state == DSTATE_SCANNING ||cinfo->global_state == DSTATE_RAW_OK) && ! cinfo->buffered_image) {/* Terminate final pass of non-buffered mode */if (cinfo->output_scanline < cinfo->output_height)ERREXIT(cinfo, JERR_TOO_LITTLE_DATA);(*cinfo->master->finish_output_pass) (cinfo);cinfo->global_state = DSTATE_STOPPING;} else if (cinfo->global_state == DSTATE_BUFIMAGE) {/* Finishing after a buffered-image operation */cinfo->global_state = DSTATE_STOPPING;} else if (cinfo->global_state != DSTATE_STOPPING) {/* STOPPING = repeat call after a suspension, anything else is error */ERREXIT1(cinfo, JERR_BAD_STATE, cinfo->global_state);}/* Read until EOI */while (! cinfo->inputctl->eoi_reached) {if ((*cinfo->inputctl->consume_input) (cinfo) == JPEG_SUSPENDED)return FALSE;		/* Suspend, come back later */}/* Do final cleanup */(*cinfo->src->term_source) (cinfo); // 终止数据源传输数据/* We can use jpeg_abort to release memory and reset global_state */jpeg_abort((j_common_ptr) cinfo);return TRUE;
}

相关文章:

【OpenCV】imread函数的简单分析

目录 1.imread()1.1 imread()1.2 imread_()1.2.1 查找解码器&#xff08;findDecoder&#xff09;1.2.2 读取数据头&#xff08;JpegDecoder-->readHeader&#xff09;1.2.2.1 初始化错误信息&#xff08;jpeg_std_error&#xff09;1.2.2.2 创建jpeg解压缩对象&#xff08;…...

【Linux实践系列】:进程间通信:万字详解共享内存实现通信

&#x1f525; 本文专栏&#xff1a;Linux Linux实践项目 &#x1f338;作者主页&#xff1a;努力努力再努力wz &#x1f4aa; 今日博客励志语录&#xff1a; 人生就像一场马拉松&#xff0c;重要的不是起点&#xff0c;而是坚持到终点的勇气 ★★★ 本文前置知识&#xff1a; …...

【笔记】BCEWithLogitsLoss

工作原理 BCEWithLogitsLoss 是 PyTorch 中的一个损失函数&#xff0c;用于二分类问题。 它结合了 Sigmoid 激活函数和二元交叉熵&#xff08;Binary Cross Entropy, BCE&#xff09;损失在一个类中。 这不仅简化了代码&#xff0c;而且通过数值稳定性优化提高了模型训练的效…...

Oracle SYSTEM/UNDO表空间损坏的处理思路

Oracle SYSTEM/UNDO表空间损坏是比较棘手的故障&#xff0c;通常会导致数据库异常宕机进而无法打开数据库。数据库的打开故障处理起来相对比较麻烦&#xff0c;读者可以参考本书第5章进一步了解该类故障的处理过程。如果数据库没有备份&#xff0c;通常需要设置官方不推荐的隐含…...

为什么 cout<<“中文你好“ 能正常输出中文

一, 简答: 受python3字符串模型影响得出的下文C字符串模型结论 是错的&#xff01;C的字符串和python2的字符串模型类似&#xff0c;也就是普通的字符串是ASCII字符串和字节串两种语义&#xff0c;类似重载或多态&#xff0c;有时候解释为整数&#xff0c;有时候是字节串。Uni…...

Leetcode 3547. Maximum Sum of Edge Values in a Graph

Leetcode 3547. Maximum Sum of Edge Values in a Graph 1. 解题思路2. 代码实现 题目链接&#xff1a;3547. Maximum Sum of Edge Values in a Graph 1. 解题思路 这一题主要是在问题的分析上面。由题意易知&#xff0c;事实上给定的图必然只可能存在三种可能的结构&#x…...

关于Go语言的开发环境的搭建

1.Go开发环境的搭建 其实对于GO语言的这个开发环境的搭建的过程&#xff0c;类似于java的开发环境搭建&#xff0c;我们都是需要去安装这个开发工具包的&#xff0c;也就是俗称的这个SDK&#xff0c;他是对于我们的程序进行编译的&#xff0c;不然我们写的这个代码也是跑不起来…...

Flutter PIP 插件 ---- 为iOS 重构PipController, Demo界面,更好的体验

接上文 Flutter PIP 插件 ---- 新增PipActivity&#xff0c;Android 11以下支持自动进入PIP Mode 项目地址 PIP&#xff0c; pub.dev也已经同步发布 pip 0.0.3&#xff0c;你的加星和点赞&#xff0c;将是我继续改进最大的动力 在之前的界面设计中&#xff0c;还原动画等体验一…...

Redis 基本命令与操作全面解析:从入门到实战

前言 Redis 作为高性能内存数据库&#xff0c;其丰富的命令体系是发挥强大功能的基础。掌握 Redis 的基本命令&#xff0c;不仅能实现数据的高效读写&#xff0c;还能深入理解其内存模型与工作机制。本文将系统梳理 Redis 的核心命令&#xff0c;涵盖连接操作、键管理、数据类…...

数据库管理-第325期 ADG Failover后该做啥(20250513)

数据库管理325期 2025-05-13 数据库管理-第325期 ADG Failover后该做啥&#xff08;20250513&#xff09;1 故障处置2 恢复原主库3 其他操作总结 数据库管理-第325期 ADG Failover后该做啥&#xff08;20250513&#xff09; 作者&#xff1a;胖头鱼的鱼缸&#xff08;尹海文&a…...

SQLi-Labs 第21-24关

Less-21 http://127.0.0.1/sqli-labs/Less-21/ 1&#xff0c;抓个请求包看看 分析分析cookie被base64URL编码了&#xff0c;解码之后就是admin 2&#xff0c;那么这个网站的漏洞利用方式也是和Less-20关一样的&#xff0c;只是攻击语句要先base64编码&#xff0c;再URL编码&…...

Oracle — 数据管理

介绍 Oracle数据库作为全球领先的关系型数据库管理系统&#xff0c;其数据管理能力以高效性、安全性和智能化为核心。系统通过多维度技术实现海量数据的存储与实时处理&#xff0c;支持高并发事务操作与复杂分析查询&#xff0c;满足企业关键业务需求。在安全领域&#xff0c;O…...

在 Qt Creator 中为 QDockWidget 设置隐藏和显示按钮

在 Qt Creator 中为 QDockWidget 设置隐藏和显示按钮 是的&#xff0c;QDockWidget 内置了隐藏和显示的功能&#xff0c;可以通过以下几种方式实现&#xff1a; 1. 使用 QDockWidget 自带的关闭按钮 QDockWidget 默认带有一个关闭按钮&#xff0c;可以通过以下代码启用&…...

LS-NET-012-TCP的交互过程详解

LS-NET-012-TCP的交互过程详解 附加&#xff1a;TCP如何保障数据传输 TCP的交互过程详解 一、TCP协议核心交互流程 TCP协议通过三次握手建立连接、数据传输、四次挥手终止连接三大阶段实现可靠传输。整个过程通过序列号、确认应答、窗口控制等机制保障传输可靠性。 1.1 三次…...

每日算法刷题Day1 5.9:leetcode数组3道题,用时1h

1.LC寻找数组的中心索引(简单) 数组和字符串 - LeetBook - 力扣&#xff08;LeetCode&#xff09;全球极客挚爱的技术成长平台 思想: 计算总和和左侧和&#xff0c;要让左侧和等于右侧和&#xff0c;即左侧和总和-左侧和-当前数字 代码 c代码: class Solution { public:i…...

解构认知边界:论万能方法的本体论批判与方法论重构——基于跨学科视阈的哲学-科学辩证

一、哲学维度的本体论批判 &#xff08;1&#xff09;理性主义的坍缩&#xff1a;从笛卡尔幻想到哥德尔陷阱 笛卡尔在《方法论》中构建的理性主义范式&#xff0c;企图通过"普遍怀疑-数学演绎"双重机制确立绝对方法体系。然而哥德尔不完备定理&#xff08;Gdel, 19…...

PVE WIN10直通无线网卡蓝牙

在 Proxmox VE (PVE) 中直通 Intel AC3165 无线网卡的 **蓝牙模块**&#xff08;通常属于 USB 设备&#xff0c;而非 PCIe 设备&#xff09;需要特殊处理&#xff0c;因为它的蓝牙部分通常通过 USB 连接&#xff0c;而 Wi-Fi 部分才是 PCIe 设备。以下是详细步骤&#xff1a; …...

第六节第二部分:抽象类的应用-模板方法设计模式

模板方法设计模式的写法 建议使用final关键字修饰模板方法 总结 代码&#xff1a; People(父类抽象类) package com.Abstract3; public abstract class People {/*设计模板方法设计模式* 1.定义一个模板方法出来*/public final void write(){System.out.println("\t\t\t…...

在另一个省发布抖音作品,IP属地会随之变化吗?

你是否曾有过这样的疑惑&#xff1a;出差旅游时在外地发布了一条抖音视频&#xff0c;评论区突然冒出“IP怎么显示xx省了&#xff1f;”的提问&#xff1f;随着各大社交平台上线“IP属地”功能&#xff0c;用户的地理位置标识成为公开信息&#xff0c;而属地显示的“灵敏性”也…...

卷积神经网络-从零开始构建一个卷积神经网络

目录 一、什么是卷积神经网络CNN 1.1、核心概念 1.2、卷积层 二、什么是卷积计算 2.1、卷积计算的例子: 2.2、点积 2.3、卷积与点积的关系 2.4、Padding(填充) 2.4.1、Padding的主要作用 1、控制输出特征图尺寸 2、保留边缘信息 3. 支持深层网络训练 2.4.2、Str…...

力扣-101.对称二叉树

题目描述 给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 class Solution { public:bool check(TreeNode* p,TreeNode* q){if(!p&&!q)return true;if(!p&&q||!q&&p)return false;if(p->val!q->val)return false;return check(p…...

Tomcat和Nginx的主要区别

1、功能定位 Nginx&#xff1a;核心是高并发HTTP服务器和反向代理服务器&#xff0c;擅长处理静态资源&#xff08;如HTML、图片&#xff09;和负载均衡。Tomcat&#xff1a;是Java应用服务器&#xff0c;主要用于运行动态内容&#xff08;如JSP、Servlet&#xff09;&#xf…...

贪心算法:最小生成树

假设无向图为&#xff1a; A-B:1 A-C:3 B-C:1 B-D:4 C-D:1 C-E:5 D-E:6 一、使用Prim算法&#xff1a; public class Prim {//声明了两个静态常量&#xff0c;用于辅助 Prim 算法的实现private static final int V 5;//点数private static final int INF Integer.MA…...

uniapp-文件查找失败:‘@dcloudio/uni-ui/lib/uni-icons/uni-icons.vue‘

uniapp-文件查找失败&#xff1a;‘dcloudio/uni-ui/lib/uni-icons/uni-icons.vue’ 今天在HBuilderX中使用uniapp开发微信小程序时遇到了这个问题&#xff0c;就是找不到uni-ui组件 当时创建项目&#xff0c;选择了一个中间带的底部带选项卡模板&#xff0c;并没有选择内置u…...

Vue2.x 和 Vue3.x 对比-差异

Vue3的优点 diff算法的提升 vue2中的虚拟DOM是全量的对比&#xff0c;也就是不管是写死的还是动态节点都会一层层比较&#xff0c;浪费时间在静态节点上。 vue3新增静态标记&#xff08;patchflag &#xff09;&#xff0c;与之前虚拟节点对比&#xff0c;只对比带有patch fla…...

MacOS 用brew 安装、配置、启动Redis

MacOS 用brew 安装、配置、启动Redis 一、安装 brew install redis 二、启动 brew services start redis 三、用命令行检测 set name tom get name...

agentmain对业务的影响

前面一篇已经说了java agent技术主要有premain和agentmain两种形式&#xff0c;如果大部分业务已经在线上运行的话&#xff0c;不方便用premain的方式来实现&#xff0c;所以agentmain的方式是更加通用、灵活的 由于RASP是与用户业务运行在同一个jvm中的 &#xff0c;所以RASP…...

精益数据分析(56/126):创业阶段的划分与精益数据分析实践

精益数据分析&#xff08;56/126&#xff09;&#xff1a;创业阶段的划分与精益数据分析实践 在创业和数据分析的探索之旅中&#xff0c;理解创业阶段的划分以及与之对应的精益数据分析方法至关重要。今天&#xff0c;依旧怀揣着与大家共同进步的心态&#xff0c;深入研读《精…...

archlinux中挂载macOS的硬盘

问&#xff1a; 你好&#xff0c;我如何在archlinux中挂载macOS的硬盘呢&#xff1f;/dev/sda4 5344161792 7813773311 2469611520 1.2T Apple HFS/HFS AI回答&#xff1a; 你好&#xff01;在 Arch Linux 中挂载 macOS 的 HFS 或 HFS 硬盘&#xff08;例如 /dev/sda4&#x…...

JVM Optimization Learning(七)-GC

一、JVM Optimization 1、进程溢出调查 模拟 如何开启GC日志 如何开启GC日志 一般来说&#xff0c;JDK8及以下版本通过以下参数来开启GC日志&#xff1a; -XX:PrintGCDetails -XX:PrintGCDateStamps -Xloggc:gc.log如果是在JDK9及以上的版本&#xff0c;则格式略有不同&…...